mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-08 17:19:10 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2325ba52d4 |
No files matched your search
@@ -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`, `graphics_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)), `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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Generated
-2
@@ -1588,7 +1588,6 @@ dependencies = [
|
||||
"horizon-mod-api-gen",
|
||||
"horizon-narrator",
|
||||
"horizon-optifine",
|
||||
"horizon-polytone",
|
||||
"horizon-protocol",
|
||||
"horizon-shader-api",
|
||||
"horizon-shaders",
|
||||
@@ -1776,7 +1775,6 @@ dependencies = [
|
||||
name = "horizon-polytone"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
+2
-63
@@ -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
|
||||
|
||||
@@ -290,30 +256,3 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## fdlibm
|
||||
|
||||
`crates/horizon-core/src/jmath/fdlibm.rs` and the `fdlibm_asin`, `fdlibm_atan` and
|
||||
`fdlibm_atan2` functions in `crates/horizon-core/src/geometry/trig.rs` are Rust ports of
|
||||
[fdlibm](https://www.netlib.org/fdlibm/) 5.3, Sun's freely distributable math library, whose
|
||||
results the Java SE API specifies for `StrictMath`. `fdlibm.rs` ports `e_log.c`, `e_log10.c`,
|
||||
`s_cbrt.c` and `e_hypot.c`, and, with fused multiply-adds, `s_sin.c`, `s_cos.c`, `k_sin.c`,
|
||||
`k_cos.c` and `e_rem_pio2.c`; `trig.rs` ports `e_asin.c`, `s_atan.c` and `e_atan2.c`. This
|
||||
notice also heads both files:
|
||||
|
||||
```
|
||||
Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
|
||||
|
||||
Developed at SunSoft, a Sun Microsystems, Inc. business.
|
||||
Permission to use, copy, modify, and distribute this
|
||||
software is freely granted, provided that this notice
|
||||
is preserved.
|
||||
```
|
||||
|
||||
## Starling
|
||||
|
||||
`crates/horizon-client-render/src/touch/layout.rs` is adapted from the touch layout of
|
||||
[Starling](https://github.com/starling-client/starling), an Android port of Horizon, at commit
|
||||
[150a414b09a1](https://github.com/starling-client/starling/tree/150a414b09a18c900489d9462a7fc035dae744b7)
|
||||
(`android` branch), written mostly by Vasysik. Starling is licensed under the same MIT license as
|
||||
Horizon (Copyright (c) 2026 Ludovic Marie and the Horizon contributors).
|
||||
@@ -917,46 +917,6 @@ impl NumericProperty {
|
||||
/// (`None` removes a prototype component).
|
||||
pub type Components = BTreeMap<String, Value>;
|
||||
|
||||
/// A stack's components read in place: its patch first, then its item's defaults. Reading one copies
|
||||
/// nothing, where [`ItemModels::components`] clones the defaults for a stack with a patch (an enchanted
|
||||
/// sword's are a few dozen JSON values, on every draw of it).
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ComponentsView<'a> {
|
||||
prototype: &'a Components,
|
||||
patch: &'a BTreeMap<String, Option<Value>>,
|
||||
}
|
||||
|
||||
impl<'a> ComponentsView<'a> {
|
||||
pub fn get(&self, key: &str) -> Option<&'a Value> {
|
||||
match self.patch.get(key) {
|
||||
Some(patched) => patched.as_ref(),
|
||||
None => self.prototype.get(key),
|
||||
}
|
||||
}
|
||||
pub fn contains_key(&self, key: &str) -> bool {
|
||||
self.get(key).is_some()
|
||||
}
|
||||
/// Components with no patch: `components` as they are.
|
||||
pub fn of(components: &'a Components) -> Self {
|
||||
static NO_PATCH: BTreeMap<String, Option<Value>> = BTreeMap::new();
|
||||
Self {
|
||||
prototype: components,
|
||||
patch: &NO_PATCH,
|
||||
}
|
||||
}
|
||||
/// The merged map, for what needs one whole.
|
||||
pub fn to_map(&self) -> Components {
|
||||
let mut out = self.prototype.clone();
|
||||
for (key, value) in self.patch {
|
||||
match value {
|
||||
Some(v) => out.insert(key.clone(), v.clone()),
|
||||
None => out.remove(key),
|
||||
};
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// An item stack as the resolver reads it.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct StackView {
|
||||
@@ -1127,15 +1087,14 @@ impl ItemModels {
|
||||
if stack.patch.is_empty() {
|
||||
return std::borrow::Cow::Borrowed(prototype);
|
||||
}
|
||||
std::borrow::Cow::Owned(self.components_view(stack).to_map())
|
||||
}
|
||||
/// The stack's components, read in place (see [`ComponentsView`]).
|
||||
pub fn components_view<'a>(&'a self, stack: &'a StackView) -> ComponentsView<'a> {
|
||||
static NONE: Components = BTreeMap::new();
|
||||
ComponentsView {
|
||||
prototype: self.prototypes.get(&stack.item).unwrap_or(&NONE),
|
||||
patch: &stack.patch,
|
||||
let mut out = prototype.clone();
|
||||
for (key, value) in &stack.patch {
|
||||
match value {
|
||||
Some(v) => out.insert(key.clone(), v.clone()),
|
||||
None => out.remove(key),
|
||||
};
|
||||
}
|
||||
std::borrow::Cow::Owned(out)
|
||||
}
|
||||
/// `ItemModelResolver.updateForTopItem`.
|
||||
pub fn resolve(&self, stack: &StackView, ctx: &Context, env: &dyn ItemEnv) -> ResolvedItem {
|
||||
@@ -1153,7 +1112,7 @@ impl ItemModels {
|
||||
ctx: &Context,
|
||||
env: &dyn ItemEnv,
|
||||
) {
|
||||
let components = self.components_view(stack);
|
||||
let components = self.components(stack);
|
||||
let Some(Value::String(model_id)) = components.get("minecraft:item_model") else {
|
||||
return;
|
||||
};
|
||||
@@ -1163,7 +1122,7 @@ impl ItemModels {
|
||||
let mut state = Resolve {
|
||||
models: self,
|
||||
stack,
|
||||
components,
|
||||
components: &components,
|
||||
ctx,
|
||||
env,
|
||||
};
|
||||
@@ -1181,7 +1140,7 @@ impl ItemModels {
|
||||
self.properties(stack).2
|
||||
}
|
||||
fn properties(&self, stack: &StackView) -> (bool, bool, f32) {
|
||||
match self.components_view(stack).get("minecraft:item_model") {
|
||||
match self.components(stack).get("minecraft:item_model") {
|
||||
Some(Value::String(id)) => identifier(id)
|
||||
.ok()
|
||||
.and_then(|id| self.definitions.get(&id))
|
||||
@@ -1200,7 +1159,7 @@ impl ItemModels {
|
||||
struct Resolve<'a> {
|
||||
models: &'a ItemModels,
|
||||
stack: &'a StackView,
|
||||
components: ComponentsView<'a>,
|
||||
components: &'a Components,
|
||||
ctx: &'a Context<'a>,
|
||||
env: &'a dyn ItemEnv,
|
||||
}
|
||||
@@ -1496,9 +1455,9 @@ impl Resolve<'_> {
|
||||
fn numeric(&self, property: &NumericProperty) -> f32 {
|
||||
let owner = self.ctx.owner;
|
||||
match property {
|
||||
NumericProperty::BundleFullness => self
|
||||
.env
|
||||
.bundle_fullness(self.stack, &self.components.to_map()),
|
||||
NumericProperty::BundleFullness => {
|
||||
self.env.bundle_fullness(self.stack, self.components)
|
||||
}
|
||||
NumericProperty::Compass {
|
||||
wobble,
|
||||
target,
|
||||
|
||||
@@ -63,7 +63,7 @@ pub(super) fn decode(m: &Map<String, Value>) -> Result<Value> {
|
||||
/// with a matching level. An empty predicate list still needs the component.
|
||||
pub(super) fn matches(
|
||||
predicate: &Value,
|
||||
components: ComponentsView<'_>,
|
||||
components: &Components,
|
||||
env: &dyn ItemEnv,
|
||||
) -> Option<bool> {
|
||||
let (id, expected) = predicate.as_object()?.iter().next()?;
|
||||
@@ -143,17 +143,13 @@ mod tests {
|
||||
json!({"minecraft:density":0}),
|
||||
)]);
|
||||
assert_eq!(
|
||||
matches(
|
||||
&json!({"minecraft:enchantments":[{}]}),
|
||||
ComponentsView::of(&components),
|
||||
&NoEnv
|
||||
),
|
||||
matches(&json!({"minecraft:enchantments":[{}]}), &components, &NoEnv),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
matches(
|
||||
&json!({"minecraft:enchantments":[{"enchantments":"minecraft:density"}]}),
|
||||
ComponentsView::of(&components),
|
||||
&components,
|
||||
&NoEnv
|
||||
),
|
||||
Some(false)
|
||||
@@ -170,21 +166,12 @@ mod tests {
|
||||
"minecraft:enchantments".into(),
|
||||
json!({"minecraft:density":3}),
|
||||
)]);
|
||||
assert_eq!(
|
||||
matches(&predicate, ComponentsView::of(&components), &Tags),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
matches(&predicate, ComponentsView::of(&components), &NoEnv),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(matches(&predicate, &components, &Tags), Some(true));
|
||||
assert_eq!(matches(&predicate, &components, &NoEnv), Some(false));
|
||||
components.insert(
|
||||
"minecraft:enchantments".into(),
|
||||
json!({"minecraft:density":2}),
|
||||
);
|
||||
assert_eq!(
|
||||
matches(&predicate, ComponentsView::of(&components), &Tags),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(matches(&predicate, &components, &Tags), Some(false));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn identifier_adds_default_namespace() {
|
||||
@@ -112,49 +111,3 @@ fn custom_condition_validates_its_name_and_both_branches() {
|
||||
assert!(matches!(node("example:charged", empty).unwrap(),
|
||||
ItemModelNode::Condition { property: ConditionProperty::Custom(name), .. } if name == "example:charged"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_patched_stack_reads_its_patch_over_its_items_defaults() {
|
||||
let mut models = ItemModels::default();
|
||||
models.prototypes.insert(
|
||||
"minecraft:diamond_sword".into(),
|
||||
Components::from_iter([
|
||||
(
|
||||
"minecraft:item_model".into(),
|
||||
json!("minecraft:diamond_sword"),
|
||||
),
|
||||
("minecraft:max_damage".into(), json!(1561)),
|
||||
("minecraft:rarity".into(), json!("common")),
|
||||
]),
|
||||
);
|
||||
let mut stack = StackView::new("minecraft:diamond_sword", 1);
|
||||
stack.patch.insert(
|
||||
"minecraft:enchantments".into(),
|
||||
Some(json!({"minecraft:sharpness": 5})),
|
||||
);
|
||||
stack
|
||||
.patch
|
||||
.insert("minecraft:rarity".into(), Some(json!("rare")));
|
||||
stack.patch.insert("minecraft:max_damage".into(), None);
|
||||
let view = models.components_view(&stack);
|
||||
assert_eq!(view.get("minecraft:rarity"), Some(&json!("rare")));
|
||||
assert_eq!(view.get("minecraft:max_damage"), None, "a removed default");
|
||||
assert_eq!(
|
||||
view.get("minecraft:item_model"),
|
||||
Some(&json!("minecraft:diamond_sword"))
|
||||
);
|
||||
assert!(view.contains_key("minecraft:enchantments"));
|
||||
assert!(!view.contains_key("minecraft:custom_name"));
|
||||
// Every key reads as the merged map has it.
|
||||
let merged = models.components(&stack);
|
||||
for key in [
|
||||
"minecraft:rarity",
|
||||
"minecraft:max_damage",
|
||||
"minecraft:item_model",
|
||||
"minecraft:enchantments",
|
||||
"minecraft:custom_name",
|
||||
] {
|
||||
assert_eq!(view.get(key), merged.get(key), "{key}");
|
||||
}
|
||||
assert_eq!(view.to_map(), *merged);
|
||||
}
|
||||
@@ -290,12 +290,12 @@ pub fn normalize_quaternion(q: [f32; 4]) -> [f32; 4] {
|
||||
[x * inv, y * inv, z * inv, w * inv]
|
||||
}
|
||||
/// JOML `Math.sin(float)` without fast math.
|
||||
pub fn joml_sin(rad: f32) -> f32 {
|
||||
fn joml_sin(rad: f32) -> f32 {
|
||||
f64::from(rad).sin() as f32
|
||||
}
|
||||
/// JOML `Math.cosFromSin(float, float)` without fast math: the magnitude from
|
||||
/// the sine, the sign from the angle's quadrant (exact zeros at right angles).
|
||||
pub fn cos_from_sin(sin: f32, angle: f32) -> f32 {
|
||||
fn cos_from_sin(sin: f32, angle: f32) -> f32 {
|
||||
let cos = (1.0 - sin * sin).sqrt();
|
||||
let tau = (std::f64::consts::PI * 2.0) as f32;
|
||||
let a = angle + std::f32::consts::FRAC_PI_2;
|
||||
|
||||
@@ -217,10 +217,9 @@ pub fn stack(tag: &Tag) -> Option<Tag> {
|
||||
Some(Tag::compound(out))
|
||||
}
|
||||
/// `ItemStackTemplate.CODEC`: an item id or `{id, count?, components?}`.
|
||||
/// Encoding uses the primary record form, including for a bare item id.
|
||||
fn template(tag: &Tag) -> Option<Tag> {
|
||||
match tag {
|
||||
Tag::String(_) => Some(Tag::compound([("id", tag.clone())])),
|
||||
Tag::String(_) => Some(tag.clone()),
|
||||
Tag::Compound(_) => {
|
||||
let mut out = vec![("id", string(&text(tag.get("id"))?))];
|
||||
if let Some(count) = int(tag.get("count")).filter(|c| *c != 1) {
|
||||
@@ -1012,69 +1011,3 @@ mod tests {
|
||||
assert_eq!(items[1].get("count"), Some(&Tag::Int(5)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pot_template_tests {
|
||||
use super::*;
|
||||
use horizon_world::nbt::parse_snbt;
|
||||
|
||||
#[test]
|
||||
fn decorated_pot_string_template_load_save_uses_item_compound() {
|
||||
let parse = |s: &str| parse_snbt(s).expect("valid input NBT").0;
|
||||
let input = |front: &str| {
|
||||
let mut tag = parse(
|
||||
r#"{id:"minecraft:decorated_pot",x:4,y:200,z:4,sherds:{back:{id:"minecraft:brick"},right:{id:"minecraft:archer_pottery_sherd"}},item:{id:"minecraft:wheat",count:3,components:{"minecraft:custom_name":"H290 item"}}}"#,
|
||||
);
|
||||
let Tag::Compound(root) = &mut tag else {
|
||||
unreachable!()
|
||||
};
|
||||
let Some(Tag::Compound(sides)) = root.get_mut(&"sherds".into()) else {
|
||||
unreachable!()
|
||||
};
|
||||
sides.insert("front".into(), parse(front));
|
||||
tag
|
||||
};
|
||||
let other_fields = |loaded: &Tag, saved: &Tag| {
|
||||
assert_eq!(saved.get("id"), loaded.get("id"));
|
||||
for key in ["x", "y", "z", "item"] {
|
||||
assert_eq!(saved.get(key), loaded.get(key), "{key}");
|
||||
}
|
||||
let loaded_sides = loaded.get("sherds").unwrap();
|
||||
let saved_sides = saved.get("sherds").unwrap();
|
||||
for side in ["back", "right"] {
|
||||
assert_eq!(saved_sides.get(side), loaded_sides.get(side), "{side}");
|
||||
}
|
||||
assert_eq!(saved_sides.get("left"), None);
|
||||
};
|
||||
let plain = r#"{id:"minecraft:angler_pottery_sherd"}"#;
|
||||
let defaults = r#"{id:"minecraft:angler_pottery_sherd",count:1,components:{}}"#;
|
||||
let rich = r#"{id:"minecraft:angler_pottery_sherd",count:3,components:{"minecraft:custom_data":{h290:1},"minecraft:custom_name":"H290 sherd"}}"#;
|
||||
// Actual canonical load/save entry, not the private template helper.
|
||||
for (front, expected) in [(plain, plain), (defaults, plain), (rich, rich)] {
|
||||
let loaded = input(front);
|
||||
let original = loaded.clone();
|
||||
let saved = canonical(&loaded).expect("actual decorated-pot codec");
|
||||
other_fields(&loaded, &saved);
|
||||
assert_eq!(
|
||||
saved.get("sherds").unwrap().get("front"),
|
||||
Some(&parse(expected))
|
||||
);
|
||||
assert_eq!(canonical(&saved).as_ref(), Some(&saved));
|
||||
assert_eq!(loaded, original, "read-only canonicalization");
|
||||
}
|
||||
println!(
|
||||
"H290 actual canonical compound/default/count/components, other sides/item/name and idempotency controls admitted"
|
||||
);
|
||||
let loaded = input(r#""minecraft:angler_pottery_sherd""#);
|
||||
let original = loaded.clone();
|
||||
let saved = canonical(&loaded).expect("actual decorated-pot codec");
|
||||
other_fields(&loaded, &saved);
|
||||
assert_eq!(loaded, original, "read-only canonicalization");
|
||||
assert_eq!(
|
||||
saved.get("sherds").unwrap().get("front"),
|
||||
Some(&parse(plain)),
|
||||
"string template must save the primary item compound",
|
||||
);
|
||||
assert_eq!(canonical(&saved).as_ref(), Some(&saved));
|
||||
}
|
||||
}
|
||||
@@ -412,11 +412,10 @@ pub fn to_text_with(
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// TranslatableContents.decompose: Language.getOrDefault(key, fallback), so the
|
||||
// fallback is the template itself, never looked up as another key.
|
||||
let translated = Text::translate(key, &args);
|
||||
match o.get("fallback").and_then(Value::as_str) {
|
||||
Some(fallback) => Text::translate_with_fallback(key, fallback, &args),
|
||||
None => Text::translate(key, &args),
|
||||
Some(fallback) if translated.plain() == key => Text::translate(fallback, &args),
|
||||
_ => translated,
|
||||
}
|
||||
} else if let Some(sel) = o.get("selector").and_then(Value::as_str) {
|
||||
resolve(sel)
|
||||
@@ -514,43 +513,6 @@ pub fn name_text(tag: &Tag) -> Text {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::lang::{self, Language, ResourceSource};
|
||||
|
||||
struct Mem(std::collections::HashMap<String, Vec<u8>>);
|
||||
impl ResourceSource for Mem {
|
||||
fn read(&self, path: &str) -> Option<Vec<u8>> {
|
||||
self.0.get(path).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
/// TranslatableContents.decompose: Language.getOrDefault(key, fallback). A missing key
|
||||
/// shows the fallback as written, even when the fallback is another key the language has.
|
||||
#[test]
|
||||
fn a_translatable_fallback_stays_literal() {
|
||||
let pack = Mem([(
|
||||
"assets/minecraft/lang/en_us.json".to_owned(),
|
||||
br#"{"real.key": "Translated", "greet": "Hello %s"}"#.to_vec(),
|
||||
)]
|
||||
.into());
|
||||
let ((), _) = lang::scoped(Language::load(&[&pack], "en_us"), || {
|
||||
let parse = |v: Value| to_text(&v, &mut |_| Text::empty()).plain();
|
||||
let missing = serde_json::json!({"translate": "missing.key", "fallback": "real.key"});
|
||||
assert_eq!(parse(missing), "real.key");
|
||||
// The fallback is a template: its arguments are filled in.
|
||||
let args = serde_json::json!({"translate": "missing.key", "fallback": "Hi %s", "with": ["Steve"]});
|
||||
assert_eq!(parse(args), "Hi Steve");
|
||||
// A key the language has wins over its fallback.
|
||||
let found =
|
||||
serde_json::json!({"translate": "greet", "fallback": "real.key", "with": ["Alex"]});
|
||||
assert_eq!(parse(found), "Hello Alex");
|
||||
// A component built with the constructor keeps its meaning through its JSON.
|
||||
let built = Text::translate_with_fallback("missing.key", "real.key", &[]);
|
||||
let again = to_text(&built.component(), &mut |_| Text::empty());
|
||||
assert_eq!(again.plain(), built.plain());
|
||||
assert_eq!(again.component(), built.component());
|
||||
});
|
||||
}
|
||||
|
||||
/// SnbtGrammar: a compound or list may end with one comma; two in a row
|
||||
/// stay an error.
|
||||
#[test]
|
||||
|
||||
@@ -46,21 +46,6 @@ thread_local! {
|
||||
}
|
||||
/// The current language's `bidirectional`, read for every drawn string.
|
||||
static BIDIRECTIONAL: AtomicBool = AtomicBool::new(false);
|
||||
/// Counts the languages set (`set`): another count is another language, so text looked up
|
||||
/// before it may read otherwise.
|
||||
static GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
thread_local! {
|
||||
/// Counts the scopes this thread entered and left (`scoped`), which no other thread sees.
|
||||
static SCOPE_GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
/// Which language text looks up in now, on this thread, as a count that changes whenever that
|
||||
/// can: both counts only grow, so their sum changes when either does.
|
||||
pub fn generation() -> u64 {
|
||||
GENERATION.load(Ordering::Relaxed) + SCOPE_GENERATION.get()
|
||||
}
|
||||
fn scope_changed() {
|
||||
SCOPE_GENERATION.set(SCOPE_GENERATION.get() + 1);
|
||||
}
|
||||
|
||||
/// A source of resource files: the prepared root plus packs, highest priority last.
|
||||
pub trait ResourceSource {
|
||||
@@ -539,7 +524,6 @@ pub fn available(sources: &[&dyn ResourceSource]) -> Vec<LanguageInfo> {
|
||||
pub fn set(language: Language) {
|
||||
BIDIRECTIONAL.store(language.bidirectional, Ordering::Relaxed);
|
||||
*CURRENT.write().unwrap_or_else(|e| e.into_inner()) = Some(Arc::new(language));
|
||||
GENERATION.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
pub fn current() -> Option<Arc<Language>> {
|
||||
if let Some(language) = SCOPE.with(|s| s.borrow().as_ref().map(|s| s.language.clone())) {
|
||||
@@ -559,7 +543,6 @@ pub fn scoped<R>(language: Language, f: impl FnOnce() -> R) -> (R, Vec<String>)
|
||||
impl Drop for Restore {
|
||||
fn drop(&mut self) {
|
||||
SCOPE.with(|s| *s.borrow_mut() = self.0.take());
|
||||
scope_changed();
|
||||
}
|
||||
}
|
||||
let outer = SCOPE.with(|s| {
|
||||
@@ -569,7 +552,6 @@ pub fn scoped<R>(language: Language, f: impl FnOnce() -> R) -> (R, Vec<String>)
|
||||
})
|
||||
});
|
||||
let _restore = Restore(outer);
|
||||
scope_changed();
|
||||
let result = f();
|
||||
let missed = SCOPE.with(|s| {
|
||||
s.borrow_mut()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! NBT paths (`NbtPathArgument`): `foo.bar[0]`, `[]`, `list[{id:1}]`,
|
||||
//! `{root:match}`, with get, set, insert and remove on a tag tree, and the
|
||||
//! partial NBT comparison (`NbtUtils.compareNbt`) selectors and conditions use.
|
||||
pub use horizon_world::nbt::compare_nbt;
|
||||
use horizon_world::nbt::{Compound, JavaString, Tag, parse_snbt};
|
||||
|
||||
/// An error carrying the translation key and its arguments.
|
||||
@@ -48,6 +47,26 @@ fn get_child<'a>(c: &'a Compound, name: &str) -> Option<&'a Tag> {
|
||||
c.get(&key(name))
|
||||
}
|
||||
|
||||
/// `NbtUtils.compareNbt(expected, actual, true)`.
|
||||
pub fn compare_nbt(expected: &Tag, actual: Option<&Tag>) -> bool {
|
||||
let Some(actual) = actual else { return false };
|
||||
if std::mem::discriminant(expected) != std::mem::discriminant(actual) {
|
||||
return false;
|
||||
}
|
||||
match (expected, actual) {
|
||||
(Tag::Compound(e), Tag::Compound(a)) => {
|
||||
a.len() >= e.len() && e.iter().all(|(k, v)| compare_nbt(v, a.get(k)))
|
||||
}
|
||||
(Tag::List(e), Tag::List(a)) => {
|
||||
if e.is_empty() {
|
||||
return a.is_empty();
|
||||
}
|
||||
a.len() >= e.len() && e.iter().all(|x| a.iter().any(|y| compare_nbt(x, Some(y))))
|
||||
}
|
||||
_ => expected == actual,
|
||||
}
|
||||
}
|
||||
|
||||
// ---- list-like tags ----------------------------------------------------------
|
||||
|
||||
/// The elements of a list or array tag.
|
||||
|
||||
@@ -89,31 +89,6 @@ pub struct Registries {
|
||||
pub trigger_objectives: Vec<String>,
|
||||
/// Objectives with the `trigger` criterion -> holder -> whether its score is locked.
|
||||
pub trigger_scores: BTreeMap<String, BTreeMap<String, bool>>,
|
||||
/// The `ConfiguredFeature` codec for an inline `minecraft:worldgen/feature`
|
||||
/// value (`ResourceOrIdArgument`), where the source has the server's live
|
||||
/// features; None leaves the decode to when the command runs.
|
||||
pub inline_feature: Option<InlineFeatureCheck>,
|
||||
}
|
||||
|
||||
/// A codec check of an inline value: its error message when it does not decode.
|
||||
#[derive(Clone)]
|
||||
pub struct InlineFeatureCheck(
|
||||
std::sync::Arc<dyn Fn(&horizon_world::nbt::Tag) -> Result<(), String> + Send + Sync>,
|
||||
);
|
||||
impl InlineFeatureCheck {
|
||||
pub fn new(
|
||||
check: impl Fn(&horizon_world::nbt::Tag) -> Result<(), String> + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
Self(std::sync::Arc::new(check))
|
||||
}
|
||||
pub fn check(&self, value: &horizon_world::nbt::Tag) -> Result<(), String> {
|
||||
(self.0)(value)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Debug for InlineFeatureCheck {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("InlineFeatureCheck(..)")
|
||||
}
|
||||
}
|
||||
/// The ids and tag ids of one registry.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
||||
@@ -583,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
|
||||
@@ -618,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,
|
||||
@@ -689,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),
|
||||
@@ -751,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,
|
||||
));
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -47,53 +47,6 @@ struct Channel {
|
||||
world: u32,
|
||||
/// The sound file it started and when, to tell another player's copy of it.
|
||||
started: Option<(String, std::time::Instant)>,
|
||||
/// Its sound event plays clearly through a mod's muffle (`Audio::muffle`).
|
||||
unmuffled: bool,
|
||||
}
|
||||
/// The cutoff of the muffle's low-pass at full strength: the world heard through ringing ears.
|
||||
const MUFFLED_CUTOFF: f32 = 300.0;
|
||||
/// The cutoff it sweeps down from as its strength rises.
|
||||
const OPEN_CUTOFF: f32 = 20_000.0;
|
||||
/// How long the muffle takes to glide to a new level: a game tick's step (50 ms) would click.
|
||||
const MUFFLE_GLIDE: f32 = 0.02;
|
||||
/// A mod's muffle (`Audio::muffle`) in the mixer: the other sounds are mixed apart, low-passed by
|
||||
/// two one-pole stages and turned down, then added to the sounds it leaves clear. While it is off
|
||||
/// (never set, or faded out) the sounds mix straight into the output, as without it.
|
||||
#[derive(Default)]
|
||||
struct Muffle {
|
||||
/// `[amount, lowpass]`, each 0 to 1, as the game's tick last set them.
|
||||
target: [f32; 2],
|
||||
/// Where the output is now, gliding to `target`.
|
||||
current: [f32; 2],
|
||||
/// The low-pass stages' state for the left and right output channels.
|
||||
stages: [[f32; 2]; 2],
|
||||
/// The muffled sounds of one buffer, reused.
|
||||
bus: Vec<f32>,
|
||||
}
|
||||
impl Muffle {
|
||||
fn active(&self) -> bool {
|
||||
self.target != [0.0; 2] || self.current.iter().any(|v| *v > 1e-4)
|
||||
}
|
||||
/// Adds `bus` (the muffled sounds) to `data`, low-passed and turned down.
|
||||
fn apply(&mut self, bus: &[f32], data: &mut [f32], channels: usize, rate: u32) {
|
||||
let rate = rate as f32;
|
||||
let glide = 1.0 - (-1.0 / (MUFFLE_GLIDE * rate)).exp();
|
||||
for (frame, muffled) in data.chunks_mut(channels).zip(bus.chunks(channels)) {
|
||||
for (current, target) in self.current.iter_mut().zip(self.target) {
|
||||
*current += (target - *current) * glide;
|
||||
}
|
||||
let [amount, lowpass] = self.current;
|
||||
let gain = 1.0 - amount;
|
||||
let cutoff = OPEN_CUTOFF * (MUFFLED_CUTOFF / OPEN_CUTOFF).powf(lowpass);
|
||||
let a = 1.0 - (-std::f32::consts::TAU * cutoff / rate).exp();
|
||||
for ((out, s), stage) in frame.iter_mut().zip(muffled).zip(&mut self.stages) {
|
||||
stage[0] += (s - stage[0]) * a;
|
||||
stage[1] += (stage[0] - stage[1]) * a;
|
||||
// Dry at no strength, so a muffle that only turns sounds down leaves them as they were.
|
||||
*out += (s + (stage[1] - s) * lowpass) * gain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct Mixer {
|
||||
@@ -112,8 +65,6 @@ struct Mixer {
|
||||
cameras: Vec<([f64; 3], Basis, u32)>,
|
||||
/// Per-source gain/pan/step cache for one buffer, reused across callbacks.
|
||||
voices: Vec<(ChannelId, [f32; 3], f64)>,
|
||||
/// A mod's muffle of the sounds it does not exempt.
|
||||
muffle: Muffle,
|
||||
limiter_peak: f32,
|
||||
/// The output device the stream was opened on (`Library.currentDeviceName`).
|
||||
#[cfg(feature = "audio")]
|
||||
@@ -149,21 +100,6 @@ pub struct Heard {
|
||||
pub range: f32,
|
||||
pub looping: bool,
|
||||
}
|
||||
/// A mod's muffle (`Audio::muffle`): its full strength, and the ticks it fades over and has left.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct MuffleFade {
|
||||
amount: f32,
|
||||
lowpass: f32,
|
||||
ticks: u32,
|
||||
left: u32,
|
||||
}
|
||||
impl MuffleFade {
|
||||
/// `[amount, lowpass]` now: full when set, falling in a straight line to none.
|
||||
fn level(&self) -> [f32; 2] {
|
||||
let left = self.left as f32 / self.ticks as f32;
|
||||
[self.amount * left, self.lowpass * left]
|
||||
}
|
||||
}
|
||||
/// The mixer side of the engine (`Backend`).
|
||||
pub struct MixerBackend {
|
||||
mixer: Arc<Mutex<Mixer>>,
|
||||
@@ -311,8 +247,6 @@ pub struct Audio {
|
||||
pub stopped: std::collections::BTreeMap<String, u32>,
|
||||
/// The volume sliders' `SoundPreviewHandler`.
|
||||
preview: crate::sound_preview::SoundPreview,
|
||||
/// A mod's muffle fading out (`muffle`), advanced by `tick`.
|
||||
muffle: Option<MuffleFade>,
|
||||
/// `SoundEngine.lastSeenDevices`: the system default output last seen.
|
||||
#[cfg(feature = "audio")]
|
||||
default_seen: Option<String>,
|
||||
@@ -505,7 +439,6 @@ impl Backend for MixerBackend {
|
||||
owner: self.owner,
|
||||
world: self.world,
|
||||
started: None,
|
||||
unmuffled: false,
|
||||
},
|
||||
);
|
||||
Some(id)
|
||||
@@ -533,9 +466,6 @@ impl Backend for MixerBackend {
|
||||
fn set_relative(&mut self, channel: ChannelId, relative: bool) {
|
||||
self.with(channel, |c| c.relative = relative);
|
||||
}
|
||||
fn set_unmuffled(&mut self, channel: ChannelId, unmuffled: bool) {
|
||||
self.with(channel, |c| c.unmuffled = unmuffled);
|
||||
}
|
||||
/// `getCompleteBuffer` (cached, else decoded on the decoder thread) or
|
||||
/// `getStream` (decoded on a thread of its own), then attach and
|
||||
/// `alSourcePlay`. A sound that cannot load leaves its source initial,
|
||||
@@ -772,13 +702,6 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
let step = f64::from(*source_rate) / f64::from(rate) * f64::from(c.pitch.max(0.0));
|
||||
voices.push((*id, [left, right, gain], step));
|
||||
}
|
||||
// A mod's muffle: the sounds it doesn't exempt mix into a bus of their own first.
|
||||
let muffling = mixer.muffle.active();
|
||||
let mut bus = std::mem::take(&mut mixer.muffle.bus);
|
||||
if muffling {
|
||||
bus.clear();
|
||||
bus.resize(data.len(), 0.0);
|
||||
}
|
||||
for (id, [left, right, gain], step) in voices.iter().copied() {
|
||||
let Some(c) = mixer.channels.get_mut(&id) else {
|
||||
continue;
|
||||
@@ -788,12 +711,7 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
};
|
||||
let source_channels = source_channels as usize;
|
||||
let frames = samples.len() / source_channels;
|
||||
let out: &mut [f32] = if muffling && !c.unmuffled {
|
||||
&mut bus
|
||||
} else {
|
||||
&mut *data
|
||||
};
|
||||
for frame in out.chunks_mut(channels) {
|
||||
for frame in data.chunks_mut(channels) {
|
||||
let mut i = c.position as usize;
|
||||
if i + 1 >= frames {
|
||||
if c.looping && frames > 1 {
|
||||
@@ -828,14 +746,6 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
}
|
||||
}
|
||||
mixer.voices = voices;
|
||||
if muffling {
|
||||
mixer.muffle.apply(&bus, data, channels, rate);
|
||||
} else {
|
||||
// Off: the next muffle starts from silence, not from the last one's filter.
|
||||
mixer.muffle.current = [0.0; 2];
|
||||
mixer.muffle.stages = [[0.0; 2]; 2];
|
||||
}
|
||||
mixer.muffle.bus = bus;
|
||||
// Limit the whole buffer together, preserving waveform and stereo balance
|
||||
// instead of flattening each overloaded sample; release over 200 ms
|
||||
// (OpenAL Soft's output limiter, `ALC_OUTPUT_LIMITER_SOFT`, releases
|
||||
@@ -1161,13 +1071,12 @@ impl Audio {
|
||||
listener: [0.0; 3],
|
||||
listener_yaw: 0.0,
|
||||
master: 1.0,
|
||||
random: crate::random_source::create(),
|
||||
random: LegacyRandom::new(crate::sound_engine::unique_seed()),
|
||||
source_override: None,
|
||||
capture: None,
|
||||
heard: Default::default(),
|
||||
stopped: Default::default(),
|
||||
preview: Default::default(),
|
||||
muffle: None,
|
||||
#[cfg(feature = "audio")]
|
||||
default_seen: if live { default_output_name() } else { None },
|
||||
#[cfg(feature = "audio")]
|
||||
@@ -1323,13 +1232,12 @@ impl Audio {
|
||||
listener: [0.0; 3],
|
||||
listener_yaw: 0.0,
|
||||
master: self.master,
|
||||
random: crate::random_source::create(),
|
||||
random: LegacyRandom::new(crate::sound_engine::unique_seed()),
|
||||
source_override: None,
|
||||
capture: None,
|
||||
heard: Default::default(),
|
||||
stopped: Default::default(),
|
||||
preview: Default::default(),
|
||||
muffle: None,
|
||||
// The device belongs to the first player's output, which looks after it.
|
||||
#[cfg(feature = "audio")]
|
||||
default_seen: self.default_seen.clone(),
|
||||
@@ -1462,50 +1370,6 @@ impl Audio {
|
||||
}
|
||||
self.sync_volumes();
|
||||
self.engine.tick(paused, ctx, &mut self.backend);
|
||||
// A paused game holds a mod's muffle where it is, as it holds the sounds.
|
||||
if !paused && let Some(fade) = &mut self.muffle {
|
||||
fade.left = fade.left.saturating_sub(1);
|
||||
let level = fade.level();
|
||||
if fade.left == 0 {
|
||||
self.muffle = None;
|
||||
self.engine.set_unmuffled(Vec::new(), &mut self.backend);
|
||||
}
|
||||
self.set_muffle_level(level);
|
||||
}
|
||||
}
|
||||
/// A mod's muffle (`native_muffle_sounds`): `amount` (0 to 1) of every sound's volume taken
|
||||
/// away and `lowpass` (0 to 1) dulling it, from untouched to a 300 Hz low-pass, fading in a
|
||||
/// straight line to none over `ticks` client ticks; the sound events in `exempt` (namespaced,
|
||||
/// `mymod:ear.ringing`) play clearly through it, those already playing too. It replaces any
|
||||
/// muffle in place; `ticks` 0 or no strength ends it. The output's, so in splitscreen every
|
||||
/// player hears it. False for an amount or strength outside 0 to 1.
|
||||
pub fn muffle(&mut self, amount: f32, lowpass: f32, ticks: u32, exempt: &[&str]) -> bool {
|
||||
if !(0.0..=1.0).contains(&amount) || !(0.0..=1.0).contains(&lowpass) {
|
||||
return false;
|
||||
}
|
||||
let on = ticks > 0 && (amount > 0.0 || lowpass > 0.0);
|
||||
let exempt = if on {
|
||||
exempt
|
||||
.iter()
|
||||
.map(|e| crate::sound_events::normalize(e))
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
self.engine.set_unmuffled(exempt, &mut self.backend);
|
||||
self.muffle = on.then_some(MuffleFade {
|
||||
amount,
|
||||
lowpass,
|
||||
ticks,
|
||||
left: ticks,
|
||||
});
|
||||
self.set_muffle_level(self.muffle.map_or([0.0; 2], |fade| fade.level()));
|
||||
true
|
||||
}
|
||||
fn set_muffle_level(&mut self, level: [f32; 2]) {
|
||||
if let Ok(mut m) = self.backend.mixer.lock() {
|
||||
m.muffle.target = level;
|
||||
}
|
||||
}
|
||||
/// The `uiSoundReserve` extra: positional sounds leave
|
||||
/// `sound_engine::HEARD_RESERVE` static channels free (off: vanilla's pool).
|
||||
@@ -1604,14 +1468,6 @@ impl Audio {
|
||||
let id = self.engine.add(instance, false);
|
||||
self.engine.play_delayed(id, ticks);
|
||||
}
|
||||
/// `SoundManager.playDelayed` of an instance the caller made: it starts
|
||||
/// `ticks` sound-engine ticks from now.
|
||||
pub fn play_instance_after(&mut self, instance: SoundInstance, ticks: i32) {
|
||||
*self.heard.entry(instance.identifier.clone()).or_default() += 1;
|
||||
self.sync_volumes();
|
||||
let id = self.engine.add(instance, false);
|
||||
self.engine.play_delayed(id, ticks);
|
||||
}
|
||||
/// `play` for this client only: a sound a packet's handler plays
|
||||
/// (`playLocalSound`), which the guests play from that packet themselves.
|
||||
pub fn play_unshared(&mut self, event: &str, at: Option<[f64; 3]>, volume: f32, pitch: f32) {
|
||||
@@ -1626,7 +1482,7 @@ impl Audio {
|
||||
"minecraft:ui.button.click",
|
||||
1.0,
|
||||
0.25,
|
||||
crate::random_source::create(),
|
||||
SoundEngine::unseeded(),
|
||||
),
|
||||
false,
|
||||
);
|
||||
@@ -1768,7 +1624,7 @@ impl Audio {
|
||||
f64::from(pos[2]) + 0.5,
|
||||
];
|
||||
let event = crate::sound_events::normalize(event);
|
||||
let instance = SoundInstance::for_jukebox_song(&event, at, crate::random_source::create());
|
||||
let instance = SoundInstance::for_jukebox_song(&event, at, SoundEngine::unseeded());
|
||||
self.play_instance(instance, true).0
|
||||
}
|
||||
/// Stop and drop a held instance.
|
||||
@@ -1979,7 +1835,6 @@ mod tests {
|
||||
owner: 0,
|
||||
world: 0,
|
||||
started: None,
|
||||
unmuffled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2400,123 +2255,6 @@ mod tests {
|
||||
assert_eq!(data, [0.5; 2]);
|
||||
}
|
||||
|
||||
/// One second of a `hz` sine at 48 kHz, amplitude 0.3 (looped, it repeats seamlessly).
|
||||
fn sine(hz: f32) -> Vec<f32> {
|
||||
(0..48_000)
|
||||
.map(|i| 0.3 * (std::f32::consts::TAU * hz * i as f32 / 48_000.0).sin())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn rms(data: &[f32]) -> f32 {
|
||||
(data.iter().map(|s| s * s).sum::<f32>() / data.len() as f32).sqrt()
|
||||
}
|
||||
|
||||
/// `seconds` of the mixer's stereo output, in the 10 ms buffers an output stream asks for.
|
||||
fn listen(m: &mut Mixer, seconds: f32) -> Vec<f32> {
|
||||
let mut out = Vec::new();
|
||||
let mut buffer = [0.0; 960];
|
||||
for _ in 0..(seconds * 100.0).round() as usize {
|
||||
mix(m, &mut buffer, 2, 48_000);
|
||||
out.extend_from_slice(&buffer);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_muffle_dulls_and_quietens_every_sound_but_the_exempt_ones() {
|
||||
// A looping tone heard through a muffle of `[amount, lowpass]`, once it has settled.
|
||||
let heard = |hz: f32, muffle: [f32; 2], unmuffled: bool| {
|
||||
let mut m = Mixer::default();
|
||||
let mut c = channel(sine(hz), None, [0.0, 0.0, 2.0], true);
|
||||
c.unmuffled = unmuffled;
|
||||
m.channels.insert(1, c);
|
||||
m.muffle.target = muffle;
|
||||
listen(&mut m, 0.2);
|
||||
listen(&mut m, 0.2)
|
||||
};
|
||||
let plain = |hz: f32| rms(&heard(hz, [0.0; 2], false));
|
||||
// Fully low-passed, a 6 kHz whine all but goes and a 100 Hz hum keeps most of its level.
|
||||
assert!(rms(&heard(6000.0, [0.0, 1.0], false)) < plain(6000.0) * 0.05);
|
||||
assert!(rms(&heard(100.0, [0.0, 1.0], false)) > plain(100.0) * 0.8);
|
||||
// Half the volume taken and no low-pass: half the level, the tone itself untouched.
|
||||
let ducked = rms(&heard(6000.0, [0.5, 0.0], false)) / plain(6000.0);
|
||||
assert!((ducked - 0.5).abs() < 1e-3, "{ducked}");
|
||||
// An exempt sound plays through as if there were no muffle, sample for sample.
|
||||
assert_eq!(
|
||||
heard(6000.0, [0.5, 1.0], true),
|
||||
heard(6000.0, [0.0; 2], false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_faded_muffle_leaves_the_sound_as_it_was() {
|
||||
let mixer = || {
|
||||
let mut m = Mixer::default();
|
||||
m.channels
|
||||
.insert(1, channel(sine(3000.0), None, [2.0, 0.0, 1.0], true));
|
||||
m
|
||||
};
|
||||
let (mut muffled, mut plain) = (mixer(), mixer());
|
||||
muffled.muffle.target = [0.7, 1.0];
|
||||
assert_ne!(listen(&mut muffled, 0.1), listen(&mut plain, 0.1));
|
||||
muffled.muffle.target = [0.0; 2];
|
||||
listen(&mut muffled, 0.5);
|
||||
listen(&mut plain, 0.5);
|
||||
assert_eq!(listen(&mut muffled, 0.1), listen(&mut plain, 0.1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mods_muffle_fades_with_the_game_ticks_and_spares_its_exempt_sounds() {
|
||||
let mut audio = Audio::silent(Path::new("/nonexistent/horizon-test-assets"));
|
||||
audio.engine.registry.add_file(
|
||||
"minecraft",
|
||||
br#"{"ambient.cave":{"sounds":["ambient/cave/cave1"]}}"#,
|
||||
&|_, _| true,
|
||||
);
|
||||
audio.engine.registry.add_file(
|
||||
"hztest",
|
||||
br#"{"ear.ringing":{"sounds":["ear/ringing"]}}"#,
|
||||
&|_, _| true,
|
||||
);
|
||||
// The engine makes its channels; with no output, none loads a file.
|
||||
audio.engine.loaded = true;
|
||||
let marks = |audio: &Audio| {
|
||||
let m = audio.backend.mixer.lock().unwrap();
|
||||
let mut marks: Vec<_> = m
|
||||
.channels
|
||||
.iter()
|
||||
.map(|(&id, c)| (id, c.unmuffled))
|
||||
.collect();
|
||||
marks.sort();
|
||||
marks
|
||||
};
|
||||
let level = |audio: &Audio| audio.backend.mixer.lock().unwrap().muffle.target;
|
||||
let close =
|
||||
|a: [f32; 2], b: [f32; 2]| (a[0] - b[0]).abs() < 1e-6 && (a[1] - b[1]).abs() < 1e-6;
|
||||
// Two sounds already playing, and one exempt sound started under the muffle.
|
||||
audio.play("ambient.cave", None, 1.0, 1.0);
|
||||
audio.play("hztest:ear.ringing", None, 1.0, 1.0);
|
||||
assert!(audio.muffle(0.8, 1.0, 4, &["hztest:ear.ringing"]));
|
||||
audio.play("hztest:ear.ringing", None, 1.0, 1.0);
|
||||
assert_eq!(marks(&audio), [(1, false), (2, true), (3, true)]);
|
||||
assert!(close(level(&audio), [0.8, 1.0]));
|
||||
let ticks = TickContext::default();
|
||||
audio.tick(false, &ticks);
|
||||
assert!(close(level(&audio), [0.6, 0.75]), "{:?}", level(&audio));
|
||||
// A paused game holds it.
|
||||
audio.tick(true, &ticks);
|
||||
assert!(close(level(&audio), [0.6, 0.75]), "{:?}", level(&audio));
|
||||
for _ in 0..3 {
|
||||
audio.tick(false, &ticks);
|
||||
}
|
||||
assert_eq!(level(&audio), [0.0; 2]);
|
||||
// Over: its exemptions go with it.
|
||||
assert_eq!(marks(&audio), [(1, false), (2, false), (3, false)]);
|
||||
// Out of range changes nothing.
|
||||
assert!(!audio.muffle(1.5, 0.0, 10, &[]));
|
||||
assert_eq!(level(&audio), [0.0; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_devices_are_looked_at_again_only_when_the_cards_change() {
|
||||
// No fingerprint (not Linux, or the cards could not be read): the list
|
||||
|
||||
@@ -191,18 +191,13 @@ pub fn discovery_url() -> String {
|
||||
.unwrap_or_else(|| DEFAULT_DISCOVERY.to_owned())
|
||||
}
|
||||
|
||||
pub(crate) fn agent() -> ureq::Agent {
|
||||
fn agent() -> ureq::Agent {
|
||||
ureq::AgentBuilder::new()
|
||||
.timeout(Duration::from_secs(15))
|
||||
.user_agent(&format!("Horizon/{}", horizon_core::release::VERSION))
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Whether `HORIZON_API_DISCOVERY` names another services environment.
|
||||
pub fn discovery_overridden() -> bool {
|
||||
discovery_url() != DEFAULT_DISCOVERY
|
||||
}
|
||||
|
||||
/// The discovery document, fetched once per game (a failure is retried next time).
|
||||
pub(crate) fn discovery() -> Result<Arc<serde_json::Value>, JoinError> {
|
||||
static CACHE: Mutex<Option<Arc<serde_json::Value>>> = Mutex::new(None);
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
//! GPU Culling's Auto mode: the setting switched by what limits the frame, measured here.
|
||||
//!
|
||||
//! GPU Culling moves the walk over the near sections and the listing of their draws from the
|
||||
//! render thread to the GPU. That pays where the CPU is the slower of the two and costs where
|
||||
//! the GPU is, and which a machine is can't be told from its parts: a fast CPU with a modest
|
||||
//! GPU, a slow one with a strong GPU and every mix between depend on the scene, the render
|
||||
//! distance and the resolution. So Auto watches the frames' own CPU and GPU times, and only
|
||||
//! where the CPU is clearly the slower one it tries GPU Culling, measures the frame again, and
|
||||
//! keeps it if the frame got faster. A trial that didn't help is undone and not repeated for
|
||||
//! minutes of play, then for longer, then not until the next start, so a machine or a scene it
|
||||
//! doesn't suit loses a few frames' time a few times, not every half minute.
|
||||
//!
|
||||
//! A verdict is the medians of a window of frames (`WINDOW`), which a few slow frames (a chunk
|
||||
//! load, say) don't move. Nothing here reads the GPU or draws anything: the caller feeds it the
|
||||
//! frames and applies what it answers.
|
||||
|
||||
/// Frames in one verdict.
|
||||
pub const WINDOW: usize = 240;
|
||||
/// The CPU's time must be this many times the GPU's for a window to count as CPU-bound.
|
||||
const CPU_BOUND: f32 = 1.3;
|
||||
/// Below this CPU time, in milliseconds, a frame is fast enough that neither matters.
|
||||
const WORTH_TRYING_MS: f32 = 1.5;
|
||||
/// A trial is kept when the frame's busier side dropped under this share of what it was.
|
||||
const KEEP_BELOW: f32 = 0.97;
|
||||
/// Windows of CPU-bound frames in a row before a trial.
|
||||
const STREAK: u8 = 2;
|
||||
/// Windows left out after a switch, while its effects settle.
|
||||
const SETTLE: u8 = 1;
|
||||
/// How long, in milliseconds of frames watched, after the first and the second trial that didn't
|
||||
/// help before another; after a third there is none.
|
||||
const RETRY_AFTER_MS: [f64; 2] = [5.0 * 60_000.0, 20.0 * 60_000.0];
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum State {
|
||||
/// Off, looking for a CPU-bound frame.
|
||||
Watching,
|
||||
/// On for a trial: left out `SETTLE` windows, then measured against the frame before.
|
||||
Trial,
|
||||
/// The trial made the frame faster: on from here.
|
||||
Kept,
|
||||
}
|
||||
|
||||
/// What Auto has decided and why, a step at a time.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GpuCullingAuto {
|
||||
cpu: Vec<f32>,
|
||||
gpu: Vec<f32>,
|
||||
on: bool,
|
||||
state: State,
|
||||
/// CPU-bound windows in a row.
|
||||
streak: u8,
|
||||
/// The frame's busier side before the trial, in milliseconds.
|
||||
before: f32,
|
||||
/// Windows still left out after a switch.
|
||||
settling: u8,
|
||||
/// The frames watched so far, in milliseconds of their own time.
|
||||
clock_ms: f64,
|
||||
/// When the next trial may start, on that clock.
|
||||
retry_at_ms: f64,
|
||||
/// Trials that didn't help.
|
||||
rejected: usize,
|
||||
}
|
||||
|
||||
impl Default for GpuCullingAuto {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuCullingAuto {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cpu: Vec::with_capacity(WINDOW),
|
||||
gpu: Vec::with_capacity(WINDOW),
|
||||
on: false,
|
||||
state: State::Watching,
|
||||
streak: 0,
|
||||
before: 0.0,
|
||||
settling: 0,
|
||||
clock_ms: 0.0,
|
||||
retry_at_ms: 0.0,
|
||||
rejected: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether Auto has GPU Culling on now.
|
||||
pub fn on(&self) -> bool {
|
||||
self.on
|
||||
}
|
||||
|
||||
/// Where Auto is, for the status: `watching`, `trial` or `kept`.
|
||||
pub fn state(&self) -> &'static str {
|
||||
match self.state {
|
||||
State::Watching => "watching",
|
||||
State::Trial => "trial",
|
||||
State::Kept => "kept",
|
||||
}
|
||||
}
|
||||
|
||||
/// Back to the start: off, with nothing measured (the mode changed, or the world did).
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self::new();
|
||||
}
|
||||
|
||||
/// One frame's CPU time, a GPU time where its timestamps have arrived, and its whole time,
|
||||
/// in milliseconds. Answers the new state when this frame completed a window that changed it.
|
||||
pub fn observe(&mut self, cpu_ms: f32, gpu_ms: Option<f32>, frame_ms: f32) -> Option<bool> {
|
||||
self.clock_ms += f64::from(frame_ms);
|
||||
self.cpu.push(cpu_ms);
|
||||
if let Some(gpu) = gpu_ms {
|
||||
self.gpu.push(gpu);
|
||||
}
|
||||
if self.cpu.len() < WINDOW {
|
||||
return None;
|
||||
}
|
||||
let cpu = median(&mut self.cpu);
|
||||
// A GPU that can't be timed leaves nothing to compare with: Auto stays as it is.
|
||||
let gpu = (self.gpu.len() >= WINDOW / 4).then(|| median(&mut self.gpu));
|
||||
self.cpu.clear();
|
||||
self.gpu.clear();
|
||||
self.verdict(cpu, gpu?)
|
||||
}
|
||||
|
||||
fn verdict(&mut self, cpu: f32, gpu: f32) -> Option<bool> {
|
||||
if self.settling > 0 {
|
||||
self.settling -= 1;
|
||||
return None;
|
||||
}
|
||||
match self.state {
|
||||
State::Kept => None,
|
||||
State::Watching => {
|
||||
if self.clock_ms < self.retry_at_ms {
|
||||
return None;
|
||||
}
|
||||
let bound = cpu >= WORTH_TRYING_MS && cpu > gpu * CPU_BOUND;
|
||||
self.streak = if bound { self.streak + 1 } else { 0 };
|
||||
if self.streak < STREAK {
|
||||
return None;
|
||||
}
|
||||
self.streak = 0;
|
||||
self.before = cpu.max(gpu);
|
||||
self.state = State::Trial;
|
||||
self.settling = SETTLE;
|
||||
self.on = true;
|
||||
Some(true)
|
||||
}
|
||||
State::Trial => {
|
||||
if cpu.max(gpu) < self.before * KEEP_BELOW {
|
||||
self.state = State::Kept;
|
||||
return None;
|
||||
}
|
||||
self.state = State::Watching;
|
||||
self.retry_at_ms = RETRY_AFTER_MS
|
||||
.get(self.rejected)
|
||||
.map_or(f64::INFINITY, |wait| self.clock_ms + wait);
|
||||
self.rejected += 1;
|
||||
self.on = false;
|
||||
Some(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn median(values: &mut [f32]) -> f32 {
|
||||
values.sort_by(f32::total_cmp);
|
||||
values[values.len() / 2]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A window of frames at constant times, each as long as its busier side; the last answer.
|
||||
fn window(auto: &mut GpuCullingAuto, cpu: f32, gpu: f32) -> Option<bool> {
|
||||
(0..WINDOW)
|
||||
.filter_map(|_| auto.observe(cpu, Some(gpu), cpu.max(gpu)))
|
||||
.last()
|
||||
}
|
||||
|
||||
/// A machine whose CPU limits the frame is tried, and keeps GPU Culling when the frame
|
||||
/// gets faster.
|
||||
#[test]
|
||||
fn a_cpu_bound_frame_tries_it_and_keeps_it_when_it_helps() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
assert!(!auto.on());
|
||||
// One CPU-bound window is not yet a verdict.
|
||||
assert_eq!(window(&mut auto, 5.0, 2.0), None);
|
||||
assert_eq!(window(&mut auto, 5.0, 2.0), Some(true));
|
||||
assert!(auto.on());
|
||||
assert_eq!(auto.state(), "trial");
|
||||
// The window after the switch is left out, whatever it shows.
|
||||
assert_eq!(window(&mut auto, 9.0, 9.0), None);
|
||||
// The frame's busier side went from 5.0 to 3.0 ms.
|
||||
assert_eq!(window(&mut auto, 3.0, 2.6), None);
|
||||
assert_eq!(auto.state(), "kept");
|
||||
assert!(auto.on());
|
||||
// Kept stays kept, even if a later scene is GPU-bound.
|
||||
assert_eq!(window(&mut auto, 1.0, 8.0), None);
|
||||
assert!(auto.on());
|
||||
}
|
||||
|
||||
/// Where it didn't help, it is undone, and not tried again for 5 minutes, then 20, then
|
||||
/// not at all: a scene it doesn't suit is not tried every half minute.
|
||||
#[test]
|
||||
fn a_trial_that_does_not_help_is_undone_and_waits_longer_each_time() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
// One window is 240 frames of 5 ms: 1.2 s. A trial, and its rejection:
|
||||
let reject = |auto: &mut GpuCullingAuto| {
|
||||
window(auto, 5.0, 2.0);
|
||||
assert_eq!(window(auto, 5.0, 2.0), Some(true));
|
||||
window(auto, 5.0, 2.0);
|
||||
// The GPU took over the frame: 5.0 ms before, 5.4 after.
|
||||
assert_eq!(window(auto, 2.5, 5.4), Some(false));
|
||||
assert!(!auto.on());
|
||||
assert_eq!(auto.state(), "watching");
|
||||
};
|
||||
reject(&mut auto);
|
||||
// CPU-bound as ever, and left alone for 5 minutes of frames: 250 windows.
|
||||
let mut waited = 0;
|
||||
while window(&mut auto, 5.0, 2.0).is_none() {
|
||||
waited += 1;
|
||||
}
|
||||
assert!((248..=252).contains(&waited), "{waited}");
|
||||
// That window started the second trial; it is rejected too.
|
||||
window(&mut auto, 5.0, 2.0);
|
||||
assert_eq!(window(&mut auto, 2.5, 5.4), Some(false));
|
||||
// Now 20 minutes: 1000 windows.
|
||||
let mut waited = 0;
|
||||
while window(&mut auto, 5.0, 2.0).is_none() {
|
||||
waited += 1;
|
||||
}
|
||||
assert!((998..=1002).contains(&waited), "{waited}");
|
||||
window(&mut auto, 5.0, 2.0);
|
||||
assert_eq!(window(&mut auto, 2.5, 5.4), Some(false));
|
||||
// A third rejection is for good.
|
||||
for _ in 0..5000 {
|
||||
assert_eq!(window(&mut auto, 5.0, 2.0), None);
|
||||
}
|
||||
assert!(!auto.on());
|
||||
}
|
||||
|
||||
/// A GPU-bound machine is never switched: its frame is the GPU's.
|
||||
#[test]
|
||||
fn a_gpu_bound_frame_is_left_alone() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
for _ in 0..20 {
|
||||
assert_eq!(window(&mut auto, 0.5, 3.6), None);
|
||||
}
|
||||
assert!(!auto.on());
|
||||
}
|
||||
|
||||
/// Within the margin neither side limits the frame, and a single CPU-bound window among
|
||||
/// others breaks no streak into a trial.
|
||||
#[test]
|
||||
fn neither_a_balanced_nor_an_occasional_cpu_bound_frame_is_tried() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
for _ in 0..10 {
|
||||
assert_eq!(window(&mut auto, 3.0, 2.6), None);
|
||||
}
|
||||
for _ in 0..10 {
|
||||
assert_eq!(window(&mut auto, 5.0, 2.0), None);
|
||||
assert_eq!(window(&mut auto, 2.0, 2.0), None);
|
||||
}
|
||||
assert!(!auto.on());
|
||||
}
|
||||
|
||||
/// Fast frames aren't worth a trial, bound or not.
|
||||
#[test]
|
||||
fn a_frame_under_the_floor_is_not_tried() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
for _ in 0..10 {
|
||||
assert_eq!(window(&mut auto, 1.2, 0.2), None);
|
||||
}
|
||||
assert!(!auto.on());
|
||||
}
|
||||
|
||||
/// Without GPU times (a GPU that can't be timed) there is nothing to compare.
|
||||
#[test]
|
||||
fn frames_without_gpu_times_decide_nothing() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
for _ in 0..WINDOW * 10 {
|
||||
assert_eq!(auto.observe(8.0, None, 8.0), None);
|
||||
}
|
||||
assert!(!auto.on());
|
||||
}
|
||||
|
||||
/// A spike in a window doesn't move its verdict: the median ignores it.
|
||||
#[test]
|
||||
fn a_few_slow_frames_do_not_make_a_window_cpu_bound() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
for _ in 0..4 {
|
||||
let mut answer = None;
|
||||
for i in 0..WINDOW {
|
||||
// One frame in ten is a 30 ms chunk load, the rest are GPU-bound.
|
||||
let cpu = if i % 10 == 0 { 30.0 } else { 0.8 };
|
||||
answer = answer.or(auto.observe(cpu, Some(3.0), 3.0));
|
||||
}
|
||||
assert_eq!(answer, None);
|
||||
}
|
||||
assert!(!auto.on());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_starts_over() {
|
||||
let mut auto = GpuCullingAuto::new();
|
||||
window(&mut auto, 5.0, 2.0);
|
||||
window(&mut auto, 5.0, 2.0);
|
||||
assert!(auto.on());
|
||||
auto.reset();
|
||||
assert!(!auto.on());
|
||||
assert_eq!(auto.state(), "watching");
|
||||
assert_eq!(window(&mut auto, 5.0, 2.0), None);
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,11 @@ pub struct BanDetails {
|
||||
pub reason_message: Option<String>,
|
||||
}
|
||||
|
||||
/// authlib's `UserProperties`, as far as the title screen and the friends service read it.
|
||||
/// authlib's `UserProperties`, as far as the title screen reads it.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct UserProperties {
|
||||
/// `UserFlag.SERVERS_ALLOWED` (`privileges.multiplayerServer.enabled`).
|
||||
pub servers_allowed: bool,
|
||||
/// `UserFlag.FRIENDS_ENABLED` (`friendsPreferences.friends`).
|
||||
pub friends_enabled: bool,
|
||||
/// `UserFlag.ACCEPT_FRIEND_INVITES` (`friendsPreferences.acceptInvites`).
|
||||
pub accept_friend_invites: bool,
|
||||
/// `bannedScopes.get("MULTIPLAYER")`.
|
||||
pub multiplayer_ban: Option<BanDetails>,
|
||||
}
|
||||
@@ -34,8 +30,6 @@ impl Default for UserProperties {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
servers_allowed: true,
|
||||
friends_enabled: false,
|
||||
accept_friend_invites: false,
|
||||
multiplayer_ban: None,
|
||||
}
|
||||
}
|
||||
@@ -215,13 +209,6 @@ pub fn parse_properties(body: &str) -> Option<UserProperties> {
|
||||
.pointer("/privileges/multiplayerServer/enabled")
|
||||
.and_then(|e| e.as_bool())
|
||||
.unwrap_or(false);
|
||||
let toggle = |name: &str| {
|
||||
v.pointer(&format!("/friendsPreferences/{name}"))
|
||||
.and_then(|f| f.as_str())
|
||||
.is_some_and(|f| f.eq_ignore_ascii_case("ENABLED"))
|
||||
};
|
||||
let friends_enabled = toggle("friends");
|
||||
let accept_friend_invites = toggle("acceptInvites");
|
||||
let multiplayer_ban = v
|
||||
.pointer("/banStatus/bannedScopes/MULTIPLAYER")
|
||||
.filter(|b| b.is_object())
|
||||
@@ -235,8 +222,6 @@ pub fn parse_properties(body: &str) -> Option<UserProperties> {
|
||||
});
|
||||
Some(UserProperties {
|
||||
servers_allowed,
|
||||
friends_enabled,
|
||||
accept_friend_invites,
|
||||
multiplayer_ban,
|
||||
})
|
||||
}
|
||||
@@ -290,24 +275,6 @@ mod tests {
|
||||
assert!(clean.servers_allowed && clean.multiplayer_ban.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn friends_preferences_read_their_two_flags() {
|
||||
let body = r#"{"privileges":{"multiplayerServer":{"enabled":true}},
|
||||
"friendsPreferences":{"friends":"ENABLED","acceptInvites":"DISABLED"}}"#;
|
||||
let p = parse_properties(body).unwrap();
|
||||
assert!(p.friends_enabled);
|
||||
assert!(!p.accept_friend_invites);
|
||||
let other = parse_properties(
|
||||
r#"{"friendsPreferences":{"friends":"DISABLED","acceptInvites":"ENABLED"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!other.friends_enabled);
|
||||
assert!(other.accept_friend_invites);
|
||||
// The offline fallback has neither flag, as `OFFLINE_PROPERTIES` does.
|
||||
assert!(!UserProperties::default().friends_enabled);
|
||||
assert!(!UserProperties::default().accept_friend_invites);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_profile_actions() {
|
||||
let a = parse_profile_actions(
|
||||
@@ -330,7 +297,6 @@ mod tests {
|
||||
properties: UserProperties {
|
||||
servers_allowed: true,
|
||||
multiplayer_ban: Some(ban.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
actions: ProfileActions {
|
||||
forced_name_change: true,
|
||||
@@ -351,7 +317,7 @@ mod tests {
|
||||
let no_servers = AccountStatus {
|
||||
properties: UserProperties {
|
||||
servers_allowed: false,
|
||||
..Default::default()
|
||||
multiplayer_ban: None,
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Biome colours from the pinned biome definitions and colormaps
|
||||
//! (`Biome.getGrassColor`/`getFoliageColor`/..., `BlockTintSources`).
|
||||
use crate::Result;
|
||||
use horizon_client_core::particle_type::ParticleType;
|
||||
use horizon_core::{noise::SimplexNoise, position::BlockPos, random::LegacyRandom};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
@@ -107,31 +106,11 @@ pub struct BiomeInfo {
|
||||
/// The biome's own fog colour attribute; otherwise the dimension's applies.
|
||||
pub fog: Option<u32>,
|
||||
pub water_fog: u32,
|
||||
/// `minecraft:visual/ambient_particles` entries and their probabilities.
|
||||
pub ambient_particles: Vec<(ParticleType, f32)>,
|
||||
/// `BiomeGenerationSettings.features`: the placed feature ids of each
|
||||
/// `GenerationStep.Decoration` step, in order.
|
||||
pub features: Vec<Vec<String>>,
|
||||
/// The biome's environment attributes (`Biome.getAttributes`): its layer in the
|
||||
/// environment attribute stack.
|
||||
pub attributes: horizon_world::environment_attributes::AttributeMap,
|
||||
}
|
||||
impl BiomeInfo {
|
||||
fn ambient_particles(v: &Value) -> Vec<(ParticleType, f32)> {
|
||||
let list = &v["attributes"]["minecraft:visual/ambient_particles"];
|
||||
let entries = list["argument"].as_array().or_else(|| list.as_array());
|
||||
entries
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|entry| {
|
||||
Some((
|
||||
ParticleType::from_name(entry["particle"]["type"].as_str()?)?,
|
||||
entry["probability"].as_f64().unwrap_or(0.0) as f32,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `Biome.getHeightAdjustedTemperature`: the frozen modifier, then cooler
|
||||
/// with height above `sea_level + 17`.
|
||||
pub fn temperature_at(&self, pos: BlockPos, sea_level: i32) -> f32 {
|
||||
@@ -266,7 +245,6 @@ impl BiomeInfo {
|
||||
sky: color(&attributes["minecraft:visual/sky_color"]),
|
||||
fog: color(&attributes["minecraft:visual/fog_color"]),
|
||||
water_fog: color(&attributes["minecraft:visual/water_fog_color"]).unwrap_or(0x050533),
|
||||
ambient_particles: Self::ambient_particles(v),
|
||||
features: v["features"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
@@ -280,11 +258,6 @@ impl BiomeInfo {
|
||||
.collect()
|
||||
})
|
||||
.collect(),
|
||||
attributes: horizon_world::environment_attributes::AttributeMap::parse(attributes)
|
||||
.unwrap_or_else(|e| {
|
||||
log::warn!("biome attributes: {e}");
|
||||
Default::default()
|
||||
}),
|
||||
name,
|
||||
}
|
||||
}
|
||||
@@ -401,17 +374,6 @@ impl Biomes {
|
||||
pub fn get(&self, id: u16) -> &BiomeInfo {
|
||||
&self.list[usize::from(id).min(self.list.len() - 1)]
|
||||
}
|
||||
/// The name of biome `id`, plains (as for an unknown name) when the id is
|
||||
/// past this table: a column a server sent under its earlier, longer
|
||||
/// registry (a proxy's previous server) keeps its ids until the next
|
||||
/// login replaces it. `get` clamps such an id to the last entry instead,
|
||||
/// so for that stale column the colours and this name can come from
|
||||
/// different biomes until the login.
|
||||
pub fn name(&self, id: u16) -> &str {
|
||||
self.list
|
||||
.get(usize::from(id))
|
||||
.map_or("minecraft:plains", |b| b.name.as_str())
|
||||
}
|
||||
pub fn grass(&self, id: u16, x: f64, z: f64) -> u32 {
|
||||
let b = self.get(id);
|
||||
match b.modifier {
|
||||
@@ -601,9 +563,7 @@ mod pack_tests {
|
||||
#[test]
|
||||
fn packs_replace_in_place_and_add_after_the_builtin_biomes() {
|
||||
let base = builtin(&["minecraft:meadow", "minecraft:plains", "minecraft:river"]);
|
||||
let red = json!({"temperature": 2.0, "effects": {"water_color": "#ff0000"},
|
||||
"attributes": {"minecraft:visual/ambient_particles": {
|
||||
"argument": [{"particle": {"type": "minecraft:ash"}, "probability": 0.25}]}}});
|
||||
let red = json!({"temperature": 2.0, "effects": {"water_color": "#ff0000"}});
|
||||
let hot = json!({"effects": {"grass_color": 0x123456}});
|
||||
let files = [
|
||||
("pack:zeta", &hot),
|
||||
@@ -616,9 +576,6 @@ mod pack_tests {
|
||||
assert_eq!(world.id("minecraft:river"), 2);
|
||||
assert_eq!(world.get(2).water, 0xFF0000);
|
||||
assert_eq!(world.get(2).temperature, 2.0);
|
||||
assert_eq!(world.get(2).ambient_particles.len(), 1);
|
||||
assert_eq!(world.get(2).ambient_particles[0].0.name(), "ash");
|
||||
assert_eq!(world.get(2).ambient_particles[0].1, 0.25);
|
||||
// New biomes follow, in registry order.
|
||||
assert_eq!(world.id("minecraft:molten_peaks"), 3);
|
||||
assert_eq!(world.id("pack:zeta"), 4);
|
||||
@@ -643,21 +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)]
|
||||
|
||||
@@ -77,9 +77,6 @@ pub struct BlockEntities {
|
||||
/// The fluid tanks of mod blocks' block entities, by block state id;
|
||||
/// built with `states`, like the containers.
|
||||
mod_fluids: FxHashMap<u32, Arc<ModFluidTanks>>,
|
||||
/// State-aware native factory ownership. Shared across level snapshots;
|
||||
/// an empty slice means the declarations have not yet been compiled.
|
||||
stateful_entities: Arc<[bool]>,
|
||||
/// The energy buffers of mod blocks' block entities, by block state id;
|
||||
/// built with `states`, like the containers.
|
||||
mod_energy: FxHashMap<u32, Arc<ModEnergyBuffer>>,
|
||||
@@ -212,21 +209,6 @@ impl BlockEntities {
|
||||
pub fn mod_fluids(&self, state: u32) -> Option<&Arc<ModFluidTanks>> {
|
||||
self.mod_fluids.get(&state)
|
||||
}
|
||||
pub fn set_stateful_entities(&mut self, states: Vec<bool>) {
|
||||
self.stateful_entities = states.into();
|
||||
}
|
||||
pub fn has_stateful_entity_table(&self) -> bool {
|
||||
!self.stateful_entities.is_empty()
|
||||
}
|
||||
pub fn stateful_entities(&self) -> Arc<[bool]> {
|
||||
self.stateful_entities.clone()
|
||||
}
|
||||
pub fn stateful_entity(&self, state: u32) -> bool {
|
||||
self.stateful_entities
|
||||
.get(state as usize)
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
pub fn set_mod_energy(&mut self, energy: FxHashMap<u32, Arc<ModEnergyBuffer>>) {
|
||||
self.mod_energy = energy;
|
||||
}
|
||||
|
||||
@@ -3,22 +3,20 @@
|
||||
//! decorated pots, conduits, enchanting-table and lectern books and copper
|
||||
//! golem statues, End portals and End gateways. Models are vanilla's baked `LayerDefinitions` layers
|
||||
//! (`entity-models.json`); transforms, animation and textures follow each
|
||||
//! vanilla renderer. Faces carry their tint and world normal: the entity
|
||||
//! shader (and a shader pack) applies `minecraft_mix_light` from the normal, as
|
||||
//! vanilla's does.
|
||||
//! vanilla renderer. Faces are lit like the `entity` shader: the lightmap at
|
||||
//! the block plus `minecraft_mix_light` with the level's two diffuse lights.
|
||||
use crate::{
|
||||
Result,
|
||||
bidi::visual,
|
||||
block_entity_anim::{self, EnchantingBook},
|
||||
breaking::entity_geometry,
|
||||
fast_hash::FxHashMap,
|
||||
font::{Font, Glyph},
|
||||
font::Glyph,
|
||||
gfx::mark_oit,
|
||||
hud::Rect,
|
||||
mesher::{CULL_BACK, Vertex},
|
||||
models::load_visibility,
|
||||
signs::{self, SignData, SignText},
|
||||
text::{Style, Text},
|
||||
world::World,
|
||||
};
|
||||
use glam::{DVec3, Mat4, Quat, Vec3};
|
||||
@@ -130,14 +128,6 @@ fn pack_color(rgb: u32, alpha: u32, brightness: f32) -> u32 {
|
||||
let c = |shift: u32| (((rgb >> shift) & 255) as f32 * brightness) as u32;
|
||||
alpha << 24 | c(0) << 16 | c(8) << 8 | c(16)
|
||||
}
|
||||
/// A world normal as `Vertex::normal` holds it: RGBA8_SNORM, `w` positive so
|
||||
/// the entity shader and a shader pack both shade by it. Block-entity models
|
||||
/// carry no normals of their own (vanilla computes each face's from its
|
||||
/// polygon, then `minecraft_mix_light` uses it in the shader).
|
||||
fn pack_normal(normal: Vec3) -> [i8; 4] {
|
||||
let c = |v: f32| (v.clamp(-1.0, 1.0) * 127.0).round() as i8;
|
||||
[c(normal.x), c(normal.y), c(normal.z), 127]
|
||||
}
|
||||
|
||||
/// Everything a draw call needs besides the geometry.
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -175,7 +165,6 @@ pub fn draw_part(
|
||||
light,
|
||||
lights,
|
||||
glow,
|
||||
..
|
||||
} = paint;
|
||||
for poly in &part.polygons {
|
||||
let pos = poly.map(|v| m.transform_point3(Vec3::new(v[0], v[1], v[2]) / 16.0));
|
||||
@@ -188,21 +177,11 @@ pub fn draw_part(
|
||||
} else {
|
||||
normal
|
||||
};
|
||||
// The colour is the tint alone when the caller shades by the level's
|
||||
// lights: the entity shader applies `minecraft_mix_light` from the
|
||||
// normal, as vanilla does, so a shader pack receives the unshaded
|
||||
// colour Iris gives it (a baked shade would be applied a second time
|
||||
// by the pack's lighting). Other lights (an item preview's
|
||||
// `Lighting.Entry`) are not the shader's to know, so those stay baked
|
||||
// into the colour, as before.
|
||||
let baked = !(lights == diffuse_lights(false) || lights == diffuse_lights(true));
|
||||
let brightness = if baked { diffuse(normal, lights) } else { 1.0 };
|
||||
let color = pack_color(tint, 255, brightness);
|
||||
let normal = if baked { [0; 4] } else { pack_normal(normal) };
|
||||
let color = pack_color(tint, 255, diffuse(normal, lights));
|
||||
for (r, light) in std::iter::once((r, light)).chain(glow.map(|g| (g, FULL_BRIGHT))) {
|
||||
for (v, position) in poly.iter().zip(pos) {
|
||||
out.push(Vertex {
|
||||
normal,
|
||||
normal: [0; 4],
|
||||
pos: position.to_array(),
|
||||
uv: [r.u0 + (r.u1 - r.u0) * v[3], r.v0 + (r.v1 - r.v0) * v[4]],
|
||||
layer: 0,
|
||||
@@ -621,26 +600,30 @@ fn frac(x: f32) -> f32 {
|
||||
x - x.floor()
|
||||
}
|
||||
|
||||
/// `StringSplitter.findLineBreak`: first line, with styled advances.
|
||||
fn first_line(line: &Text, max_width: f32, advance: &dyn Fn(char, &Style) -> f32) -> Text {
|
||||
/// `StringSplitter.splitLines(...).get(0)` for plain text: the first line,
|
||||
/// wrapped at the last space that fits or mid-word when none does.
|
||||
fn first_line(line: &str, max_width: f32, advance: &dyn Fn(char) -> f32) -> String {
|
||||
let mut width = 0.0;
|
||||
let mut visible = false;
|
||||
let mut last_space = None;
|
||||
for (i, (c, style)) in line.chars().enumerate() {
|
||||
for (i, c) in line.char_indices() {
|
||||
if c == '\n' {
|
||||
return line.slice(0, i);
|
||||
return line[..i].to_owned();
|
||||
}
|
||||
if c == ' ' {
|
||||
last_space = Some(i);
|
||||
}
|
||||
let step = advance(c, style);
|
||||
width += step;
|
||||
if visible && width > max_width.max(1.0) {
|
||||
return line.slice(0, last_space.unwrap_or(i));
|
||||
width += advance(c);
|
||||
if width > max_width {
|
||||
let end = match last_space {
|
||||
Some(s) if s > 0 => s,
|
||||
_ if i > 0 => i,
|
||||
// The first char alone is too wide: take that one char.
|
||||
_ => c.len_utf8(),
|
||||
};
|
||||
return line[..end].to_owned();
|
||||
}
|
||||
visible |= step != 0.0;
|
||||
}
|
||||
line.clone()
|
||||
line.to_owned()
|
||||
}
|
||||
|
||||
/// `AbstractSignRenderer.isOutlineVisible`: the camera within 16 blocks of
|
||||
@@ -658,7 +641,7 @@ fn sign_text_near(pos: [i32; 3], camera: DVec3) -> bool {
|
||||
/// `AbstractSignRenderer.submitSignText`: four centred lines, glowing text
|
||||
/// full-bright with an 8x outline in the dark colour. The font colours each
|
||||
/// glyph by its style (`Font.StringRenderOutput`): a line's own colour wins,
|
||||
/// else the sign's. Keep style boundaries for glyphs and decoration effects.
|
||||
/// else the sign's. A line is one call per run of one colour.
|
||||
fn sign_text_calls(
|
||||
scene: &Scene,
|
||||
data: &SignData,
|
||||
@@ -670,43 +653,27 @@ fn sign_text_calls(
|
||||
let line_height = data.line_height();
|
||||
let midpoint = (4.0 * line_height / 2.0).floor();
|
||||
let dark = 0xFF00_0000 | text.dark_color();
|
||||
let (color, outline, light, tier) = if text.glowing {
|
||||
let (color, outline, light) = if text.glowing {
|
||||
let c = text.text_color();
|
||||
(
|
||||
0xFF00_0000 | c,
|
||||
if c == 0 || near { dark } else { 0 },
|
||||
240 | 240 << 8,
|
||||
crate::mesher::LOW,
|
||||
)
|
||||
} else {
|
||||
(dark, 0, vertex_light(packed), 0)
|
||||
};
|
||||
let advance = |c: char, style: &Style| {
|
||||
let g = scene
|
||||
.font
|
||||
.map_or_else(|| (scene.glyph)(c), |f| f.glyph_styled(c, style));
|
||||
g.advance + if style.is_bold() { g.offset } else { 0.0 }
|
||||
(dark, 0, vertex_light(packed))
|
||||
};
|
||||
let advance = |c: char| (scene.glyph)(c).advance;
|
||||
let mut calls: Vec<TextCall> = Vec::with_capacity(text.lines.len());
|
||||
for i in 0..text.lines.len() {
|
||||
// The line that fits, in the order it is drawn (getVisualOrder).
|
||||
let line = text.line_text(i);
|
||||
let shown = if let Some(font) = scene.font {
|
||||
font.split_formatted(&line, data.max_line_width())
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.resolved()
|
||||
.into_owned()
|
||||
} else {
|
||||
first_line(&line.resolved(), data.max_line_width(), &advance)
|
||||
};
|
||||
let line = line.resolved();
|
||||
let flat: String = line.chars().map(|(c, _)| c).collect();
|
||||
let fits = first_line(&flat, data.max_line_width(), &advance);
|
||||
let shown = line.slice(0, fits.chars().count());
|
||||
let shown = visual(&shown);
|
||||
let width = shown
|
||||
.chars()
|
||||
.map(|(c, s)| advance(c, s))
|
||||
.sum::<f32>()
|
||||
.ceil();
|
||||
let width = shown.chars().map(|(c, _)| advance(c)).sum::<f32>().ceil();
|
||||
let x = -((width as i32) / 2) as f32;
|
||||
let y = i as f32 * line_height - midpoint;
|
||||
let first = calls.len();
|
||||
@@ -716,22 +683,19 @@ fn sign_text_calls(
|
||||
.style
|
||||
.color
|
||||
.map_or(color, |rgb| 0xFF00_0000 | (rgb & 0xFF_FFFF));
|
||||
calls.push(TextCall {
|
||||
matrix: m,
|
||||
x: x + pen,
|
||||
y,
|
||||
text: span.text.clone(),
|
||||
style: span.style.clone(),
|
||||
color: run,
|
||||
outline,
|
||||
light,
|
||||
tier,
|
||||
});
|
||||
pen += span
|
||||
.text
|
||||
.chars()
|
||||
.map(|c| advance(c, &span.style))
|
||||
.sum::<f32>();
|
||||
match calls[first..].last_mut() {
|
||||
Some(last) if last.color == run => last.text.push_str(&span.text),
|
||||
_ => calls.push(TextCall {
|
||||
matrix: m,
|
||||
x: x + pen,
|
||||
y,
|
||||
text: span.text.clone(),
|
||||
color: run,
|
||||
outline,
|
||||
light,
|
||||
}),
|
||||
}
|
||||
pen += span.text.chars().map(advance).sum::<f32>();
|
||||
}
|
||||
if calls.len() == first {
|
||||
// A line with no text is still a call, of no glyphs.
|
||||
@@ -740,28 +704,15 @@ fn sign_text_calls(
|
||||
x,
|
||||
y,
|
||||
text: String::new(),
|
||||
style: Style::default(),
|
||||
color,
|
||||
outline,
|
||||
light,
|
||||
tier,
|
||||
});
|
||||
}
|
||||
}
|
||||
calls
|
||||
}
|
||||
|
||||
fn sign_text_animated(data: &SignData) -> bool {
|
||||
[&data.front, &data.back].into_iter().any(|side| {
|
||||
(0..side.lines.len()).any(|i| {
|
||||
side.line_text(i)
|
||||
.resolved()
|
||||
.chars()
|
||||
.any(|(c, s)| c != ' ' && s.obfuscated == Some(true))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// How far the text of an outlined call sits in front of its outline, in
|
||||
/// text units (`Font.DisplayMode.POLYGON_OFFSET`, see `draw_text`).
|
||||
const TEXT_OVER_OUTLINE: f32 = 0.05;
|
||||
@@ -775,131 +726,72 @@ const TEXT_OVER_OUTLINE: f32 = 0.05;
|
||||
/// reader instead (a twentieth of a font pixel, invisible) rather than
|
||||
/// z-fighting with the outline under it.
|
||||
fn draw_text(scene: &Scene, call: &TextCall, out: &mut Vec<Vertex>) {
|
||||
let glyph = |c| {
|
||||
let Some(font) = scene.font else {
|
||||
return (scene.glyph)(c);
|
||||
};
|
||||
let g = font.glyph_styled(c, &call.style);
|
||||
if call.style.obfuscated == Some(true) && c != ' ' {
|
||||
font.random_glyph(&call.style, g.advance)
|
||||
} else {
|
||||
g
|
||||
}
|
||||
};
|
||||
text_vertices(&glyph, scene.font.map(Font::white), call, out);
|
||||
text_vertices(scene.glyph, call, out);
|
||||
}
|
||||
|
||||
/// `BakedSheetGlyph.renderChar` / `Font.PreparedTextBuilder.accept`: the
|
||||
/// `draw_text` with the font's glyph cells and advances from `glyph`: the
|
||||
/// text of `call` as glyph quads in its matrix's plane, 1 unit a font pixel,
|
||||
/// y down as the font's, with its outline when it has one. A native mod's
|
||||
/// block entity draws its text this way (`native_draw_text`).
|
||||
pub fn text_vertices(
|
||||
glyph: &dyn Fn(char) -> Glyph,
|
||||
white: Option<Rect>,
|
||||
call: &TextCall,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
let bold = call.style.is_bold();
|
||||
let mut quad = |corners: [(f32, f32); 4], cell: Rect, z: f32, argb: u32| {
|
||||
let uvs = [
|
||||
[cell.u0, cell.v1],
|
||||
[cell.u1, cell.v1],
|
||||
[cell.u1, cell.v0],
|
||||
[cell.u0, cell.v0],
|
||||
];
|
||||
let color = 0xFF00_0000 | (argb & 255) << 16 | (argb >> 8 & 255) << 8 | (argb >> 16 & 255);
|
||||
for ((x, y), uv) in corners.into_iter().zip(uvs) {
|
||||
out.push(Vertex {
|
||||
normal: [0; 4],
|
||||
pos: call.matrix.transform_point3(Vec3::new(x, y, z)).to_array(),
|
||||
uv,
|
||||
layer: u32::from(call.tier) << 16,
|
||||
color,
|
||||
light: call.light,
|
||||
glint_uv: [0.0; 2],
|
||||
});
|
||||
}
|
||||
};
|
||||
let mut emit = |dx: f32, dy: f32, z: f32, argb: u32, outline: bool| {
|
||||
let mut pen = call.x;
|
||||
for (i, ch) in call.text.chars().enumerate() {
|
||||
pub fn text_vertices(glyph: &dyn Fn(char) -> Glyph, call: &TextCall, out: &mut Vec<Vertex>) {
|
||||
let mut emit = |dx: f32, dy: f32, z: f32, argb: u32| {
|
||||
let mut x = call.x + dx;
|
||||
let y0 = call.y + dy;
|
||||
for ch in call.text.chars() {
|
||||
let g = glyph(ch);
|
||||
let x = pen + dx * g.offset;
|
||||
let y = call.y + dy * g.offset;
|
||||
let advance = g.advance + if bold { g.offset } else { 0.0 };
|
||||
if let Some(cell) = g.rect {
|
||||
let thick = if bold { 0.1 } else { 0.0 };
|
||||
let shear = |v| {
|
||||
if call.style.is_italic() {
|
||||
1.0 - 0.25 * v
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
for copy in 0..=usize::from(bold) {
|
||||
let x0 = x + copy as f32 * g.offset + g.left - thick;
|
||||
let x1 = x + copy as f32 * g.offset + g.right + thick;
|
||||
let top = y + g.top - thick;
|
||||
let bottom = y + g.bottom + thick;
|
||||
quad(
|
||||
[
|
||||
(x0 + shear(g.bottom), bottom),
|
||||
(x1 + shear(g.bottom), bottom),
|
||||
(x1 + shear(g.top), top),
|
||||
(x0 + shear(g.top), top),
|
||||
],
|
||||
cell,
|
||||
z + copy as f32 * 0.001,
|
||||
argb,
|
||||
);
|
||||
// BakedSheetGlyph: the glyph's own box from the pen, not a
|
||||
// fixed 8x8 cell (a Unifont glyph is 0.5 units per texel).
|
||||
let (x0, x1) = (x + g.left, x + g.right);
|
||||
let (top, bottom) = (y0 + g.top, y0 + g.bottom);
|
||||
let corners = [(x0, bottom), (x1, bottom), (x1, top), (x0, top)];
|
||||
let uvs = [
|
||||
[cell.u0, cell.v1],
|
||||
[cell.u1, cell.v1],
|
||||
[cell.u1, cell.v0],
|
||||
[cell.u0, cell.v0],
|
||||
];
|
||||
let c =
|
||||
0xFF00_0000 | (argb & 255) << 16 | (argb >> 8 & 255) << 8 | (argb >> 16 & 255);
|
||||
for ((cx, cy), uv) in corners.into_iter().zip(uvs) {
|
||||
out.push(Vertex {
|
||||
normal: [0; 4],
|
||||
pos: call
|
||||
.matrix
|
||||
.transform_point3(Vec3::new(cx, cy, z))
|
||||
.to_array(),
|
||||
uv,
|
||||
layer: 0,
|
||||
color: c,
|
||||
light: call.light,
|
||||
|
||||
glint_uv: [0.0, 0.0],
|
||||
});
|
||||
}
|
||||
}
|
||||
// `Font.prepare8xTextOutline` discards effects: only the front
|
||||
// text gets underline/strikethrough, including over spaces.
|
||||
if !outline && let Some(white) = white {
|
||||
let x0 = if i == 0 { x - 1.0 } else { x };
|
||||
for (enabled, top) in [
|
||||
(call.style.strikethrough, 3.5),
|
||||
(call.style.underlined, 8.0),
|
||||
] {
|
||||
if enabled == Some(true) {
|
||||
quad(
|
||||
[
|
||||
(x0, y + top + 1.0),
|
||||
(x + advance, y + top + 1.0),
|
||||
(x + advance, y + top),
|
||||
(x0, y + top),
|
||||
],
|
||||
white,
|
||||
z + 0.01,
|
||||
argb,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
pen += advance;
|
||||
x += g.advance;
|
||||
}
|
||||
};
|
||||
if call.outline != 0 {
|
||||
for dx in -1..=1 {
|
||||
for dy in -1..=1 {
|
||||
if dx != 0 || dy != 0 {
|
||||
emit(dx as f32, dy as f32, 0.0, call.outline, true);
|
||||
}
|
||||
}
|
||||
for (dx, dy) in [
|
||||
(-1.0, -1.0),
|
||||
(0.0, -1.0),
|
||||
(1.0, -1.0),
|
||||
(-1.0, 0.0),
|
||||
(1.0, 0.0),
|
||||
(-1.0, 1.0),
|
||||
(0.0, 1.0),
|
||||
(1.0, 1.0),
|
||||
] {
|
||||
emit(dx, dy, 0.0, call.outline);
|
||||
}
|
||||
}
|
||||
emit(
|
||||
0.0,
|
||||
0.0,
|
||||
if call.outline != 0 {
|
||||
TEXT_OVER_OUTLINE
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
call.color,
|
||||
false,
|
||||
);
|
||||
let z = if call.outline != 0 {
|
||||
TEXT_OVER_OUTLINE
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
emit(0.0, 0.0, z, call.color);
|
||||
}
|
||||
|
||||
/// Block-entity fields the level ticks, at one position (vanilla's own field
|
||||
@@ -932,16 +824,12 @@ pub struct TextCall {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub text: String,
|
||||
pub style: Style,
|
||||
/// ARGB.
|
||||
pub color: u32,
|
||||
/// ARGB outline colour, 0 for none.
|
||||
pub outline: u32,
|
||||
/// Horizon packed light (sky*16 | block*16 << 8).
|
||||
pub light: u32,
|
||||
/// HDR emission tier of its glyphs (`mesher::hdr_tier`), 0 for none: glowing sign text
|
||||
/// shines in HDR output and in shader packs (`horizonTier`), as full-bright text does in SDR.
|
||||
pub tier: u8,
|
||||
}
|
||||
|
||||
/// Inputs from the game for one frame of block-entity rendering.
|
||||
@@ -980,8 +868,6 @@ pub struct Scene<'a> {
|
||||
pub skull: &'a dyn Fn([i32; 3]) -> Option<String>,
|
||||
/// Font glyph: atlas cell (none for blank glyphs such as the space), box and advance.
|
||||
pub glyph: &'a dyn Fn(char) -> Glyph,
|
||||
/// Font providers, effects and random glyphs for styled sign text.
|
||||
pub font: Option<&'a Font>,
|
||||
/// Changes whenever `glyph` may answer differently (`Font::epoch`): sign
|
||||
/// text laid out under another value is laid out again.
|
||||
pub glyphs: u64,
|
||||
@@ -1143,7 +1029,6 @@ struct SignTextEntry {
|
||||
/// Game time it was last drawn.
|
||||
used: i64,
|
||||
vertices: Vec<Vertex>,
|
||||
animated: bool,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct StaticKey {
|
||||
@@ -2450,11 +2335,9 @@ impl BlockEntityRenderer {
|
||||
let entry = match signs.entry(pos) {
|
||||
std::collections::hash_map::Entry::Occupied(mut occupied) => {
|
||||
let entry = occupied.get_mut();
|
||||
let changed = entry.data != *data;
|
||||
if (entry.state, entry.packed, entry.near, entry.glyphs)
|
||||
!= (state, packed, near, glyphs)
|
||||
|| changed
|
||||
|| entry.animated
|
||||
|| entry.data != *data
|
||||
{
|
||||
entry.vertices.clear();
|
||||
self.render_one(
|
||||
@@ -2469,10 +2352,7 @@ impl BlockEntityRenderer {
|
||||
&mut entry.vertices,
|
||||
);
|
||||
entry.state = state;
|
||||
if changed {
|
||||
entry.animated = sign_text_animated(data);
|
||||
entry.data.clone_from(data);
|
||||
}
|
||||
entry.data.clone_from(data);
|
||||
entry.packed = packed;
|
||||
entry.near = near;
|
||||
entry.glyphs = glyphs;
|
||||
@@ -2493,7 +2373,6 @@ impl BlockEntityRenderer {
|
||||
&mut vertices,
|
||||
);
|
||||
vacant.insert(SignTextEntry {
|
||||
animated: sign_text_animated(data),
|
||||
state,
|
||||
data: data.clone(),
|
||||
packed,
|
||||
@@ -3733,7 +3612,6 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
// Compare the cached world path with the original model renderer,
|
||||
@@ -3896,105 +3774,6 @@ mod tests {
|
||||
// Straight down: ambient only.
|
||||
assert!((diffuse(-Vec3::Y, lights) - 0.4).abs() < 1e-6);
|
||||
}
|
||||
/// A face's colour carries its tint alone and its normal carries the
|
||||
/// shading: the entity shader applies `minecraft_mix_light` from it, as a
|
||||
/// shader pack's lighting does, so a baked shade would be applied twice.
|
||||
#[test]
|
||||
fn a_face_keeps_its_tint_and_normal_for_the_shader_to_shade() {
|
||||
let quad = |y: f32, z0: f32, z1: f32| {
|
||||
[
|
||||
[0.0, y, z0, 0.0, 0.0],
|
||||
[0.0, y, z1, 0.0, 1.0],
|
||||
[16.0, y, z1, 1.0, 1.0],
|
||||
[16.0, y, z0, 1.0, 0.0],
|
||||
]
|
||||
};
|
||||
// Opposite windings: the first faces up, the second down.
|
||||
let part = ModelPart {
|
||||
pose: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
|
||||
polygons: vec![quad(0.0, 0.0, 16.0), quad(16.0, 16.0, 0.0)],
|
||||
children: vec![],
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
draw_part(
|
||||
&part,
|
||||
"root",
|
||||
Mat4::IDENTITY,
|
||||
&|_, _| {},
|
||||
Paint {
|
||||
sprite: Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 1.0,
|
||||
v1: 1.0,
|
||||
w: 16,
|
||||
h: 16,
|
||||
},
|
||||
tint: 0x123456,
|
||||
light: 0,
|
||||
lights: diffuse_lights(false),
|
||||
glow: None,
|
||||
},
|
||||
&mut out,
|
||||
);
|
||||
assert_eq!(out.len(), 8);
|
||||
// The same tint on both faces: no directional shade baked in (`pack_color`
|
||||
// stores the tint in the vertex colour's BGR byte order).
|
||||
let tint = pack_color(0x12_3456, 255, 1.0);
|
||||
assert!(out.iter().all(|v| v.color == tint));
|
||||
let (up, down) = (&out[0], &out[4]);
|
||||
assert_eq!(up.normal, [0, 127, 0, 127]);
|
||||
assert_eq!(down.normal, [0, -127, 0, 127]);
|
||||
// The shader still gets the difference the old baked colours held.
|
||||
let lights = diffuse_lights(false);
|
||||
let up_normal = Vec3::new(0.0, f32::from(up.normal[1]) / 127.0, 0.0);
|
||||
let down_normal = Vec3::new(0.0, f32::from(down.normal[1]) / 127.0, 0.0);
|
||||
assert!(diffuse(up_normal, lights) > diffuse(down_normal, lights));
|
||||
}
|
||||
/// Lights the entity shader does not know (an item preview's
|
||||
/// `Lighting.Entry`) stay baked into the colour, and `normal.w` 0 tells
|
||||
/// the shader to keep it: `game/item_draw` passes those for block-entity
|
||||
/// items in inventories and hands.
|
||||
#[test]
|
||||
fn lights_other_than_the_levels_stay_baked() {
|
||||
let part = ModelPart {
|
||||
pose: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
|
||||
polygons: vec![[
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 16.0, 0.0, 1.0],
|
||||
[16.0, 0.0, 16.0, 1.0, 1.0],
|
||||
[16.0, 0.0, 0.0, 1.0, 0.0],
|
||||
]],
|
||||
children: vec![],
|
||||
};
|
||||
let lights = (Vec3::X, Vec3::Z);
|
||||
let mut out = Vec::new();
|
||||
draw_part(
|
||||
&part,
|
||||
"root",
|
||||
Mat4::IDENTITY,
|
||||
&|_, _| {},
|
||||
Paint {
|
||||
sprite: Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 1.0,
|
||||
v1: 1.0,
|
||||
w: 16,
|
||||
h: 16,
|
||||
},
|
||||
tint: 0x12_3456,
|
||||
light: 0,
|
||||
lights,
|
||||
glow: None,
|
||||
},
|
||||
&mut out,
|
||||
);
|
||||
let baked = pack_color(0x12_3456, 255, diffuse(Vec3::Y, lights));
|
||||
assert!(out.iter().all(|v| v.color == baked));
|
||||
assert!(out.iter().all(|v| v.normal == [0; 4]));
|
||||
assert_ne!(baked, pack_color(0x12_3456, 255, 1.0));
|
||||
}
|
||||
#[test]
|
||||
fn part_poses_compose_like_model_part() {
|
||||
let part = ModelPart {
|
||||
@@ -4128,7 +3907,6 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let mut call = TextCall {
|
||||
@@ -4136,11 +3914,9 @@ mod tests {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
text: "Hi".into(),
|
||||
style: Style::default(),
|
||||
color: 0xFFFF_FF00,
|
||||
outline: 0xFF33_3300,
|
||||
light: 240 | 240 << 8,
|
||||
tier: 0,
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
draw_text(&scene, &call, &mut out);
|
||||
@@ -4204,7 +3980,6 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let call = TextCall {
|
||||
@@ -4212,11 +3987,9 @@ mod tests {
|
||||
x: 10.0,
|
||||
y: 2.0,
|
||||
text: "ab".into(),
|
||||
style: Style::default(),
|
||||
color: 0xFF00_0000,
|
||||
outline: 0,
|
||||
light: 240 | 240 << 8,
|
||||
tier: 0,
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
draw_text(&scene, &call, &mut out);
|
||||
@@ -4236,83 +4009,6 @@ mod tests {
|
||||
assert_eq!(boxes, [[10.0, 14.0, 2.0, 10.0], [15.0, 19.0, 2.0, 10.0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_sign_quads_follow_vanilla_glyph_and_effect_bounds() {
|
||||
let white = Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 0.1,
|
||||
v1: 0.1,
|
||||
w: 1,
|
||||
h: 1,
|
||||
};
|
||||
let glyph = |c| Glyph {
|
||||
rect: (c != ' ').then_some(white),
|
||||
left: 0.0,
|
||||
right: 4.0,
|
||||
top: 0.0,
|
||||
bottom: 8.0,
|
||||
advance: 5.0,
|
||||
offset: 0.5,
|
||||
};
|
||||
let mut call = TextCall {
|
||||
matrix: Mat4::IDENTITY,
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
text: "A ".into(),
|
||||
color: 0xff123456,
|
||||
outline: 0,
|
||||
light: 240,
|
||||
tier: crate::mesher::LOW,
|
||||
style: Style {
|
||||
bold: Some(true),
|
||||
italic: Some(true),
|
||||
underlined: Some(true),
|
||||
strikethrough: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
text_vertices(&glyph, Some(white), &call, &mut out);
|
||||
// BakedSheetGlyph: bold expands by 0.1, italic shears by 1 - y/4,
|
||||
// the second bold glyph is 0.5 along and 0.001 in front (Unihex).
|
||||
assert_eq!(out.len(), 24);
|
||||
for (got, expected) in out[0].pos.iter().zip([-1.1, 8.1, 0.0]) {
|
||||
assert!((got - expected).abs() < 1e-6);
|
||||
}
|
||||
assert!((out[4].pos[0] - out[0].pos[0] - 0.5).abs() < 1e-6);
|
||||
assert_eq!(out[4].pos[2], 0.001);
|
||||
assert_eq!(out[8].pos, [-1.0, 4.5, 0.01]);
|
||||
assert_eq!(out[12].pos, [-1.0, 9.0, 0.01]);
|
||||
assert_eq!(out[21].pos, [11.0, 9.0, 0.01]); // underline crosses the space
|
||||
assert!(out.iter().all(|v| v.color == 0xff563412 && v.light == 240));
|
||||
call.outline = 0xff010203;
|
||||
out.clear();
|
||||
text_vertices(&glyph, Some(white), &call, &mut out);
|
||||
// Eight outlines contain glyphs only; effects appear once, in front.
|
||||
assert_eq!(out.len(), 8 * 8 + 24);
|
||||
assert!((out[0].pos[0] - -1.6).abs() < 1e-6);
|
||||
assert!(out[64..].iter().all(|v| v.pos[2] >= TEXT_OVER_OUTLINE));
|
||||
// The glow tier reaches bold copies, outlines and decoration quads.
|
||||
assert!(out.iter().all(|v| v.layer >> 16 == 1));
|
||||
call.tier = 0;
|
||||
out.clear();
|
||||
text_vertices(&glyph, Some(white), &call, &mut out);
|
||||
assert!(out.iter().all(|v| v.layer >> 16 == 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_sign_clipping_counts_bold_width_and_preserves_resets() {
|
||||
let line = Text::legacy("§lAB§r東Z");
|
||||
let advance = |_: char, s: &Style| 5.0 + if s.is_bold() { 0.5 } else { 0.0 };
|
||||
let shown = first_line(&line, 16.0, &advance);
|
||||
assert_eq!(shown.plain(), "AB東");
|
||||
assert_eq!(
|
||||
shown.chars().map(|(_, s)| s.is_bold()).collect::<Vec<_>>(),
|
||||
[true, true, false]
|
||||
);
|
||||
}
|
||||
|
||||
/// The front text calls of a sign whose every glyph advances 6.
|
||||
fn front_calls(data: &SignData, near: bool) -> Vec<(String, f32, f32, u32, u32)> {
|
||||
let world = World::new(BlockStateId(0), 1);
|
||||
@@ -4341,7 +4037,6 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
sign_text_calls(&scene, data, &data.front, Mat4::IDENTITY, 0, near)
|
||||
@@ -4481,9 +4176,7 @@ mod tests {
|
||||
..changed.clone()
|
||||
};
|
||||
let wide = std::cell::Cell::new(6.0);
|
||||
let glyph_reads = std::cell::Cell::new(0);
|
||||
let glyph = |c: char| {
|
||||
glyph_reads.set(glyph_reads.get() + 1);
|
||||
let u = f32::from(c as u8 % 16) / 16.0;
|
||||
let rect = Rect {
|
||||
u0: u,
|
||||
@@ -4526,7 +4219,6 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let check = |scene: &Scene, pos: [i32; 3], state: u32| {
|
||||
@@ -4570,23 +4262,6 @@ mod tests {
|
||||
wide.set(5.0);
|
||||
scene.glyphs = 1;
|
||||
check(&scene, pos, 1);
|
||||
// With the same game time and data, plain text reuses its vertices,
|
||||
// but obfuscated text must ask the font for fresh glyphs each frame.
|
||||
let mut vertices = Vec::new();
|
||||
let reads = glyph_reads.get();
|
||||
renderer.render(&scene, [(pos, 1)].iter(), &mut vertices);
|
||||
assert_eq!(glyph_reads.get(), reads);
|
||||
let animated = SignData {
|
||||
front: text("§kSecret", false),
|
||||
..Default::default()
|
||||
};
|
||||
let animated_sign = |_| Some(&animated);
|
||||
scene.sign = &animated_sign;
|
||||
renderer.render(&scene, [(pos, 0)].iter(), &mut vertices);
|
||||
let reads = glyph_reads.get();
|
||||
renderer.render(&scene, [(pos, 0)].iter(), &mut vertices);
|
||||
assert!(glyph_reads.get() > reads);
|
||||
scene.sign = &first_sign;
|
||||
// Signs out of view for a while are dropped.
|
||||
scene.game_time = 2 * SIGN_TEXT_KEEP;
|
||||
check(&scene, pos, 0);
|
||||
@@ -4594,23 +4269,11 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn first_line_wraps_on_bytes_without_splitting_a_char() {
|
||||
let advance = |c: char, _: &Style| if c == ' ' { 4.0 } else { 6.0 };
|
||||
assert_eq!(
|
||||
first_line(&Text::literal("ab cd ef"), 13.0, &advance).plain(),
|
||||
"ab"
|
||||
);
|
||||
assert_eq!(
|
||||
first_line(&Text::literal("abcdef"), 13.0, &advance).plain(),
|
||||
"ab"
|
||||
);
|
||||
assert_eq!(
|
||||
first_line(&Text::literal("\u{6771}"), 3.0, &advance).plain(),
|
||||
"\u{6771}"
|
||||
);
|
||||
assert_eq!(
|
||||
first_line(&Text::literal("ab\ncd"), 100.0, &advance).plain(),
|
||||
"ab"
|
||||
);
|
||||
let advance = |c: char| if c == ' ' { 4.0 } else { 6.0 };
|
||||
assert_eq!(first_line("ab cd ef", 13.0, &advance), "ab");
|
||||
assert_eq!(first_line("abcdef", 13.0, &advance), "ab");
|
||||
assert_eq!(first_line("\u{6771}", 3.0, &advance), "\u{6771}");
|
||||
assert_eq!(first_line("ab\ncd", 100.0, &advance), "ab");
|
||||
}
|
||||
/// `rendertype_end_portal` culls back faces, and `AbstractEndPortalRenderer`
|
||||
/// submits the unit cube's two Y faces. Both are wound outward, so of each
|
||||
@@ -4714,7 +4377,6 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let mut verts = Vec::new();
|
||||
@@ -4801,7 +4463,6 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
// A closed single chest of `block` drawn by `renderer`: positions and UVs.
|
||||
|
||||
@@ -358,7 +358,6 @@ fn batched_callbacks_preserve_entity_ranges_and_allow_immediate_culling() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
// Reverse local-position order to check that cache indexing does not
|
||||
@@ -448,7 +447,6 @@ fn static_cache_qualifies_local_positions_by_section_and_drops_old_entries() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
@@ -504,7 +502,6 @@ fn gpu_static_cache_uses_section_local_chests_and_keeps_visibility_separate() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([100, 70, -32], 0)];
|
||||
@@ -692,7 +689,6 @@ fn sign_text_cache_can_be_disabled_and_reenabled_without_changing_output() {
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -747,7 +743,6 @@ fn shulker_cache_tracks_settled_poses_and_does_not_freeze_opening_animation() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -850,7 +845,6 @@ fn shulker_cache_covers_all_colors_and_facing_directions() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
@@ -906,7 +900,6 @@ fn cached_bells_keep_shaking_until_the_animation_ends() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -969,7 +962,6 @@ fn cached_closed_chests_match_the_direct_model_and_rebuild_when_opening() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -1001,9 +993,7 @@ fn cached_closed_chests_match_the_direct_model_and_rebuild_when_opening() {
|
||||
scene.nether = true;
|
||||
let nether_closed = render_cached(&scene);
|
||||
assert_same_vertices(&nether_closed, &render_direct(&renderer, &scene, &blocks));
|
||||
// The nether is the entity shader's flag now (`entity_light`'s `l1` and
|
||||
// `globals.time.y`), not a baked colour, so the cached geometry serves both.
|
||||
assert_same_vertices(&closed_again, &nether_closed);
|
||||
assert_vertices_differ(&closed_again, &nether_closed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1068,7 +1058,6 @@ fn closed_chest_cache_covers_all_materials_and_atlas_reloads() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let pos = [0, 64, 0];
|
||||
@@ -1158,7 +1147,6 @@ fn double_chest_cache_bypasses_both_halves_while_either_lid_is_open() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let pair = [([0, 64, 0], 0), ([1, 64, 0], 1)];
|
||||
@@ -1235,7 +1223,6 @@ fn decorated_pot_cache_tracks_sherds_and_bypasses_wobble_animation() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -1315,7 +1302,6 @@ fn banner_cache_keeps_the_pole_static_while_the_flag_and_patterns_update() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -1383,7 +1369,6 @@ fn cached_wall_banner_poles_use_the_wall_model() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let pos = [0, 64, 0];
|
||||
@@ -1432,7 +1417,6 @@ fn copper_statue_cache_rebuilds_when_its_pose_state_changes() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
@@ -1539,7 +1523,6 @@ fn copper_statue_cache_covers_weathering_waxing_and_all_poses() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
font: None,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
|
||||
@@ -37,37 +37,6 @@ pub fn potion_name_key(item: &str, potion: Option<&str>) -> String {
|
||||
let suffix = potion.and_then(self::potion).map_or("empty", |p| p.name);
|
||||
format!("item.minecraft.{prefix}.effect.{suffix}")
|
||||
}
|
||||
/// `PotionItem.getName` and `TippedArrowItem.getName`: a potion or a tipped
|
||||
/// arrow is named by its contents (`PotionContents.getName`: their own
|
||||
/// `custom_name`, else the potion's name, else `empty`), whatever its
|
||||
/// `item_name`. None for other items, and for a stack whose contents were
|
||||
/// taken off (`!minecraft:potion_contents`), which takes the item's name.
|
||||
pub fn contents_name_key(stack: &ItemStack) -> Option<String> {
|
||||
use horizon_world::nbt::Tag;
|
||||
let item = &*stack.item;
|
||||
let named = matches!(
|
||||
item,
|
||||
"minecraft:potion"
|
||||
| "minecraft:splash_potion"
|
||||
| "minecraft:lingering_potion"
|
||||
| "minecraft:tipped_arrow"
|
||||
);
|
||||
let data = stack.data();
|
||||
if !named || data.is_some_and(|d| d.other("!minecraft:potion_contents").is_some()) {
|
||||
return None;
|
||||
}
|
||||
let prefix = item.trim_start_matches("minecraft:");
|
||||
let own = data
|
||||
.and_then(|d| d.potion_extra.iter().find(|(k, _)| k == "custom_name"))
|
||||
.and_then(|(_, name)| match name {
|
||||
Tag::String(s) => Some(s.to_string_lossy()),
|
||||
_ => None,
|
||||
});
|
||||
Some(match own {
|
||||
Some(suffix) => format!("item.minecraft.{prefix}.effect.{suffix}"),
|
||||
None => potion_name_key(item, stack.potion()),
|
||||
})
|
||||
}
|
||||
/// `PotionContents.getAllEffects`: the potion's effects, then the stack's
|
||||
/// `custom_effects` (id, duration, amplifier). Unknown effect ids are dropped.
|
||||
pub fn contents_effects(stack: &ItemStack) -> Vec<(&'static str, i32, i32)> {
|
||||
@@ -500,40 +469,6 @@ mod tests {
|
||||
potion_name_key("minecraft:splash_potion", Some("minecraft:long_swiftness")),
|
||||
"item.minecraft.splash_potion.effect.swiftness"
|
||||
);
|
||||
// A potion or a tipped arrow is named by its contents, over its
|
||||
// item_name; one whose contents were taken off is not.
|
||||
use horizon_world::nbt::Tag;
|
||||
let mut swift =
|
||||
ItemStack::new("minecraft:potion", 1).with_potion("minecraft:long_swiftness");
|
||||
swift
|
||||
.data_mut()
|
||||
.set_other("minecraft:item_name", Some(Tag::String("Bottle".into())));
|
||||
assert_eq!(
|
||||
contents_name_key(&swift).as_deref(),
|
||||
Some("item.minecraft.potion.effect.swiftness")
|
||||
);
|
||||
assert_eq!(
|
||||
contents_name_key(&ItemStack::new("minecraft:splash_potion", 1)).as_deref(),
|
||||
Some("item.minecraft.splash_potion.effect.empty")
|
||||
);
|
||||
let mut own = ItemStack::new("minecraft:tipped_arrow", 1).with_potion("minecraft:poison");
|
||||
own.data_mut()
|
||||
.potion_extra
|
||||
.push(("custom_name".into(), Tag::String("luck".into())));
|
||||
assert_eq!(
|
||||
contents_name_key(&own).as_deref(),
|
||||
Some("item.minecraft.tipped_arrow.effect.luck")
|
||||
);
|
||||
let mut taken = ItemStack::new("minecraft:potion", 1);
|
||||
taken.data_mut().set_other(
|
||||
"!minecraft:potion_contents",
|
||||
Some(Tag::compound(Vec::<(&str, Tag)>::new())),
|
||||
);
|
||||
assert_eq!(contents_name_key(&taken), None);
|
||||
assert_eq!(
|
||||
contents_name_key(&ItemStack::new("minecraft:stone", 1)),
|
||||
None
|
||||
);
|
||||
assert_eq!(format_duration(9600), "08:00");
|
||||
assert_eq!(format_duration(450), "00:22");
|
||||
assert_eq!(format_duration(140), "00:07");
|
||||
|
||||
@@ -357,24 +357,6 @@ impl Light {
|
||||
}
|
||||
/// The smallest form of packed light per block.
|
||||
pub fn packed(light: Box<[u8; 4096]>) -> Self {
|
||||
match Self::classify(&light) {
|
||||
(false, false, first) => Self::Uniform(first),
|
||||
(true, false, first) => Self::Sky(Self::pack_layer(&light, 4), first & 15),
|
||||
(false, true, first) => Self::Block(Self::pack_layer(&light, 0), first >> 4),
|
||||
(true, true, _) => Self::Full(light),
|
||||
}
|
||||
}
|
||||
/// The smallest form of packed light per block, allocating a 4 KiB box only
|
||||
/// when both sky and block light vary within the section.
|
||||
pub fn from_slice(light: &[u8; 4096]) -> Self {
|
||||
match Self::classify(light) {
|
||||
(false, false, first) => Self::Uniform(first),
|
||||
(true, false, first) => Self::Sky(Self::pack_layer(light, 4), first & 15),
|
||||
(false, true, first) => Self::Block(Self::pack_layer(light, 0), first >> 4),
|
||||
(true, true, _) => Self::Full(Box::new(*light)),
|
||||
}
|
||||
}
|
||||
fn classify(light: &[u8; 4096]) -> (bool, bool, u8) {
|
||||
let first = light[0];
|
||||
let (mut sky_varies, mut block_varies) = (false, false);
|
||||
for &v in light.iter() {
|
||||
@@ -382,14 +364,19 @@ impl Light {
|
||||
sky_varies |= d & 0xF0 != 0;
|
||||
block_varies |= d & 15 != 0;
|
||||
}
|
||||
(sky_varies, block_varies, first)
|
||||
}
|
||||
fn pack_layer(light: &[u8; 4096], shift: u32) -> Box<[u8; 2048]> {
|
||||
let mut layer = Box::new([0u8; 2048]);
|
||||
for (out, pair) in layer.iter_mut().zip(light.as_chunks::<2>().0) {
|
||||
*out = (pair[0] >> shift & 15) | (pair[1] >> shift & 15) << 4;
|
||||
let layer = |shift: u32| {
|
||||
let mut layer = Box::new([0u8; 2048]);
|
||||
for (out, pair) in layer.iter_mut().zip(light.as_chunks::<2>().0) {
|
||||
*out = (pair[0] >> shift & 15) | (pair[1] >> shift & 15) << 4;
|
||||
}
|
||||
layer
|
||||
};
|
||||
match (sky_varies, block_varies) {
|
||||
(false, false) => Self::Uniform(first),
|
||||
(true, false) => Self::Sky(layer(4), first & 15),
|
||||
(false, true) => Self::Block(layer(0), first >> 4),
|
||||
(true, true) => Self::Full(light),
|
||||
}
|
||||
layer
|
||||
}
|
||||
/// The packed light of blocks `at..at + out.len()`, into `out`.
|
||||
#[inline]
|
||||
@@ -589,7 +576,7 @@ pub struct Chunk {
|
||||
/// Block entities read from a save, moved into the world on arrival.
|
||||
pub block_entities: Vec<((i32, i32, i32), block_entity::BlockEntity)>,
|
||||
/// `CustomName` of the container block entities (shown as the screen title).
|
||||
pub block_names: Vec<((i32, i32, i32), Tag)>,
|
||||
pub block_names: Vec<((i32, i32, i32), String)>,
|
||||
/// Generation postprocessing marks (ProtoChunk.postProcessing) as world
|
||||
/// positions, applied once by LevelChunk.postProcessGeneration.
|
||||
pub postprocess: Vec<[i32; 3]>,
|
||||
@@ -633,14 +620,6 @@ impl Chunk {
|
||||
entities: Vec::new(),
|
||||
}
|
||||
}
|
||||
/// The original `structures.starts`, including starts with unknown pieces.
|
||||
pub fn saved_structure_starts(&self) -> Option<&horizon_world::nbt::Compound> {
|
||||
let structures = self.saved.as_ref()?.root.get(&"structures".into())?;
|
||||
match structures.get("starts")? {
|
||||
horizon_world::nbt::Tag::Compound(starts) => Some(starts),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// The minimum block Y of the column.
|
||||
#[inline]
|
||||
pub fn min_y(&self) -> i32 {
|
||||
|
||||
@@ -59,9 +59,6 @@ pub enum BlockEntity {
|
||||
CreakingHeart(Box<CreakingHeart>),
|
||||
/// `BannerBlockEntity`: the pattern layers.
|
||||
Banner(Banner),
|
||||
/// `CopperGolemStatueBlockEntity`: its own components, which hold the
|
||||
/// custom name of the golem it was (`createStatue`).
|
||||
CopperGolemStatue(Compound),
|
||||
/// `CrafterBlockEntity`.
|
||||
Crafter(Crafter),
|
||||
/// `SpawnerBlockEntity` (`crate::mobs::spawner`).
|
||||
@@ -83,8 +80,6 @@ pub enum BlockEntity {
|
||||
/// `DaylightDetectorBlockEntity`: no data, only the ticker
|
||||
/// (`DaylightDetectorBlock.tickEntity`).
|
||||
DaylightDetector,
|
||||
/// `BellBlockEntity`: saved identity only; ringing state is kept separately.
|
||||
Bell,
|
||||
/// Suspicious sand and gravel: saved loot and the transient brushed face.
|
||||
Brushable(Box<Brushable>),
|
||||
/// A shelf holds three displayed item stacks.
|
||||
@@ -454,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 {
|
||||
@@ -515,11 +507,6 @@ pub struct Info {
|
||||
pub pos: [f64; 3],
|
||||
pub source: Option<Who>,
|
||||
pub owner: Option<Who>,
|
||||
/// `uuid`: the source's saved identity (`source`), taken when the event
|
||||
/// was posted. `source` is the live entity and comes back `None` on load.
|
||||
pub source_uuid: Option<[i32; 4]>,
|
||||
/// `projectileOwnerUuid` (`projectile_owner`), likewise beside `owner`.
|
||||
pub owner_uuid: Option<[i32; 4]>,
|
||||
}
|
||||
/// `VibrationSelector`: the closest, then highest-frequency candidate of a tick.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
|
||||
@@ -208,7 +208,7 @@ where
|
||||
Ok(resolve_world_movement(
|
||||
self,
|
||||
MovementRequest {
|
||||
entity: context.entity().unwrap_or(EntityIdentity(0)),
|
||||
entity: EntityIdentity(0),
|
||||
context,
|
||||
bounds,
|
||||
movement,
|
||||
@@ -279,7 +279,7 @@ impl SceneCollisions<'_, '_> {
|
||||
Ok(resolve_world_movement(
|
||||
self,
|
||||
MovementRequest {
|
||||
entity: context.entity().unwrap_or(EntityIdentity(0)),
|
||||
entity: EntityIdentity(0),
|
||||
context,
|
||||
bounds,
|
||||
movement,
|
||||
@@ -300,7 +300,6 @@ impl SceneCollisions<'_, '_> {
|
||||
CollisionContext::for_entity(
|
||||
CollisionEntity {
|
||||
identity: EntityIdentity(0),
|
||||
kind: horizon_world::collision_context::EntityContextKind::Living,
|
||||
y,
|
||||
descending: false,
|
||||
main_hand: Some(CollisionItem::EMPTY),
|
||||
@@ -336,58 +335,6 @@ where
|
||||
{
|
||||
no_collision_in(world, bounds, true)
|
||||
}
|
||||
/// `CollisionGetter.noCollision(entity, bounds)`: retain the real entity snapshot.
|
||||
pub fn no_collision_with_context<W, E>(
|
||||
world: &mut W,
|
||||
bounds: Aabb,
|
||||
context: CollisionContext<'_>,
|
||||
) -> bool
|
||||
where
|
||||
W: BlockCollisionWorld<Error = E> + EntityMovementWorld<Error = E>,
|
||||
{
|
||||
no_world_collision(
|
||||
world,
|
||||
CollisionCheckRequest {
|
||||
entity: context.entity().unwrap_or(EntityIdentity(0)),
|
||||
context,
|
||||
bounds,
|
||||
candidate_budget: 1_000_000,
|
||||
},
|
||||
)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
/// `Level.loadedAndEntityCanStandOn(pos, projectile)` with the projectile's
|
||||
/// feet at `entity_y` (`loadedAndEntityCanStandOnFace` with `UP`):
|
||||
/// `Block.isFaceFull(state.getCollisionShape(level, pos,
|
||||
/// CollisionContext.of(projectile)), UP)`. A projectile is not living and never
|
||||
/// falls (it moves by `setPos`), so powder snow and liquids hold nothing up
|
||||
/// and only scaffolding reads the height. An unloaded or out-of-world block
|
||||
/// reads as air.
|
||||
pub fn projectile_can_stand_on(
|
||||
world: &World,
|
||||
binding: &BlockCollisionRegistry<'_>,
|
||||
pos: BlockPos,
|
||||
entity_y: f64,
|
||||
) -> bool {
|
||||
use horizon_core::geometry::{Direction, ShapeOp};
|
||||
let mut scene = SceneCollisions {
|
||||
world,
|
||||
binding,
|
||||
subject: None,
|
||||
};
|
||||
let state = world.block(pos.x, pos.y, pos.z);
|
||||
let context = CollisionContext::position(entity_y);
|
||||
binding
|
||||
.collision_shape_with_context(&mut scene, state, pos, &context)
|
||||
.is_ok_and(|shape| {
|
||||
shape.is_block()
|
||||
|| shape.face_shape(Direction::Up).is_ok_and(|face| {
|
||||
VoxelShape::block()
|
||||
.join_is_not_empty(&face, ShapeOp::XOR)
|
||||
.is_ok_and(|differs| !differs)
|
||||
})
|
||||
})
|
||||
}
|
||||
fn no_collision_in<W, E>(world: &mut W, bounds: Aabb, fluids: bool) -> bool
|
||||
where
|
||||
W: BlockCollisionWorld<Error = E> + EntityMovementWorld<Error = E>,
|
||||
|
||||
@@ -19,75 +19,26 @@ pub struct ModMenuType {
|
||||
pub slots: usize,
|
||||
pub data_slots: usize,
|
||||
pub results: Vec<usize>,
|
||||
/// its crafting grid, if it has one (`crafting`).
|
||||
pub crafting: Option<ModCrafting>,
|
||||
/// its linked slots, if it has any (`links`).
|
||||
pub links: Option<ModLinks>,
|
||||
}
|
||||
|
||||
/// a mod menu type's linked slots: `count` container slots from `from` on,
|
||||
/// which show the slots of the containers a native mod links them to
|
||||
/// (`native_menu_links`) instead of the block entity's.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ModLinks {
|
||||
pub from: usize,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
impl ModLinks {
|
||||
/// The linked container slots.
|
||||
pub fn slots(&self) -> std::ops::Range<usize> {
|
||||
self.from..self.from + self.count
|
||||
}
|
||||
}
|
||||
|
||||
/// a mod menu type's crafting grid: `size`x`size` container slots from
|
||||
/// `grid` on, row by row, and the slot showing what they craft. The block
|
||||
/// entity holds the grid; the result is the menu's.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ModCrafting {
|
||||
pub grid: usize,
|
||||
pub size: usize,
|
||||
pub result: usize,
|
||||
}
|
||||
|
||||
impl ModCrafting {
|
||||
/// The grid's container slots.
|
||||
pub fn grid_slots(&self) -> std::ops::Range<usize> {
|
||||
self.grid..self.grid + self.size * self.size
|
||||
}
|
||||
}
|
||||
|
||||
static MOD_MENU_TYPES: std::sync::OnceLock<Vec<ModMenuType>> = std::sync::OnceLock::new();
|
||||
|
||||
/// One menu type as [`register_mod_menu_types`] takes it: `(id, slots, data
|
||||
/// slots, results, crafting, links)`.
|
||||
pub type ModMenuTypeEntry = (
|
||||
String,
|
||||
usize,
|
||||
usize,
|
||||
Vec<usize>,
|
||||
Option<ModCrafting>,
|
||||
Option<ModLinks>,
|
||||
);
|
||||
|
||||
/// registers the content mods' menu types, a list set once per process
|
||||
/// (false when it already was).
|
||||
pub fn register_mod_menu_types(menus: impl IntoIterator<Item = ModMenuTypeEntry>) -> bool {
|
||||
/// registers the content mods' menu types, `(id, slots, data slots,
|
||||
/// results)`, a list set once per process (false when it already was).
|
||||
pub fn register_mod_menu_types(
|
||||
menus: impl IntoIterator<Item = (String, usize, usize, Vec<usize>)>,
|
||||
) -> bool {
|
||||
if MOD_MENU_TYPES.get().is_some() {
|
||||
return false;
|
||||
}
|
||||
let menus =
|
||||
menus.into_iter().map(
|
||||
|(id, slots, data_slots, results, crafting, links)| ModMenuType {
|
||||
id: Box::leak(id.into_boxed_str()),
|
||||
slots,
|
||||
data_slots,
|
||||
results,
|
||||
crafting,
|
||||
links,
|
||||
},
|
||||
);
|
||||
let menus = menus
|
||||
.into_iter()
|
||||
.map(|(id, slots, data_slots, results)| ModMenuType {
|
||||
id: Box::leak(id.into_boxed_str()),
|
||||
slots,
|
||||
data_slots,
|
||||
results,
|
||||
});
|
||||
MOD_MENU_TYPES.set(menus.collect()).is_ok()
|
||||
}
|
||||
|
||||
@@ -441,15 +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: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,
|
||||
@@ -515,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"),
|
||||
@@ -539,7 +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::StructureBlock(_) => name == "minecraft:structure_block",
|
||||
Self::TestBlock(_) => name == "minecraft:test_block",
|
||||
Self::TestInstanceBlock(_) => name == "minecraft:test_instance_block",
|
||||
@@ -567,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",
|
||||
@@ -578,7 +520,6 @@ impl BlockEntity {
|
||||
Self::Skull(_) => "minecraft:skull",
|
||||
Self::EndGateway(_) => "minecraft:end_gateway",
|
||||
Self::DaylightDetector => "minecraft:daylight_detector",
|
||||
Self::Bell => "minecraft:bell",
|
||||
Self::Brushable(_) => "minecraft:brushable_block",
|
||||
Self::Campfire(_) => "minecraft:campfire",
|
||||
Self::ChiseledBookshelf(_) => "minecraft:chiseled_bookshelf",
|
||||
@@ -613,12 +554,11 @@ impl BlockEntity {
|
||||
| Self::TestBlock(_)
|
||||
| Self::TestInstanceBlock(_)
|
||||
| Self::DaylightDetector
|
||||
| Self::Bell
|
||||
| Self::Brushable(_) => Vec::new(),
|
||||
Self::Mod(m) => m.items.iter().flatten().cloned().collect(),
|
||||
Self::Lectern(l) => l.book.iter().cloned().collect(),
|
||||
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(),
|
||||
@@ -635,7 +575,6 @@ impl BlockEntity {
|
||||
| Self::Lectern(_)
|
||||
| Self::Sculk(_)
|
||||
| Self::Banner(_)
|
||||
| Self::CopperGolemStatue(_)
|
||||
| Self::EndGateway(_)
|
||||
| Self::Skull(_)
|
||||
| Self::PotentSulfur(_)
|
||||
@@ -644,7 +583,6 @@ impl BlockEntity {
|
||||
| Self::TestBlock(_)
|
||||
| Self::TestInstanceBlock(_)
|
||||
| Self::DaylightDetector
|
||||
| Self::Bell
|
||||
| Self::Brushable(_) => &[],
|
||||
Self::Jukebox(j) => &j.items,
|
||||
Self::Crafter(c) => &c.items,
|
||||
@@ -671,7 +609,6 @@ impl BlockEntity {
|
||||
| Self::Lectern(_)
|
||||
| Self::Sculk(_)
|
||||
| Self::Banner(_)
|
||||
| Self::CopperGolemStatue(_)
|
||||
| Self::EndGateway(_)
|
||||
| Self::Skull(_)
|
||||
| Self::PotentSulfur(_)
|
||||
@@ -680,7 +617,6 @@ impl BlockEntity {
|
||||
| Self::TestBlock(_)
|
||||
| Self::TestInstanceBlock(_)
|
||||
| Self::DaylightDetector
|
||||
| Self::Bell
|
||||
| Self::Brushable(_) => &mut [],
|
||||
Self::Jukebox(j) => &mut j.items,
|
||||
Self::Crafter(c) => &mut c.items,
|
||||
|
||||
@@ -83,13 +83,6 @@ fn choice_description(opt: Opt, pad: &PadSettings) -> Option<&'static str> {
|
||||
GlyphStyle::PlayStation => "Always shows PlayStation buttons.",
|
||||
GlyphStyle::Nintendo => "Always shows Nintendo buttons.",
|
||||
},
|
||||
Opt::PadNintendoButtons => {
|
||||
if pad.nintendo_letters {
|
||||
"A, on the right, confirms and jumps and B goes back, as on a Switch."
|
||||
} else {
|
||||
"B, at the bottom, confirms and jumps and A goes back, as on other controllers."
|
||||
}
|
||||
}
|
||||
Opt::PadKeyboard => match pad.keyboard {
|
||||
gamepad::TextKeyboard::Auto => {
|
||||
"Opens Steam's keyboard on a Steam Deck under Steam, and Horizon's otherwise."
|
||||
@@ -191,10 +184,6 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
|
||||
Opt::PadGameplayGuide,
|
||||
"Show the buttons for what you can do, while you play with a controller.",
|
||||
),
|
||||
option(
|
||||
Opt::PadCraftOnServers,
|
||||
"Lets LT + A craft on servers other than your own worlds. Some servers count it as a macro.",
|
||||
),
|
||||
option(
|
||||
Opt::PadTriggerThreshold,
|
||||
"How far a trigger is pulled before it counts as pressed.",
|
||||
@@ -272,10 +261,6 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
|
||||
Opt::PadGlyphs,
|
||||
"Follow the controller, or always show one family's buttons.",
|
||||
),
|
||||
option(
|
||||
Opt::PadNintendoButtons,
|
||||
"Whether a Nintendo controller's A and B work by where they sit or by their letters.",
|
||||
),
|
||||
info(Info::Using, "The buttons menus show right now."),
|
||||
],
|
||||
),
|
||||
@@ -348,7 +333,6 @@ impl Item {
|
||||
Item::Option(Opt::PadAdaptiveTriggers) => Some(c.adaptive_triggers),
|
||||
Item::Option(Opt::PadInvertX) => Some(c.invert_x),
|
||||
Item::Option(Opt::PadInvertY) => Some(c.invert_y),
|
||||
Item::Option(Opt::PadCraftOnServers) => Some(c.craft_on_servers),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1348,7 +1332,6 @@ mod tests {
|
||||
Opt::PadLookDeadzone,
|
||||
Opt::PadTriggerThreshold,
|
||||
Opt::PadGlyphs,
|
||||
Opt::PadNintendoButtons,
|
||||
Opt::PadKeyboard,
|
||||
Opt::PadPointerSpeed,
|
||||
Opt::PadScrollSpeed,
|
||||
@@ -1446,53 +1429,6 @@ mod tests {
|
||||
assert!((s.controller.move_deadzone - before - 0.01).abs() < 0.001);
|
||||
}
|
||||
|
||||
/// The Device page's Nintendo Buttons row sits with the glyphs, steps between its two
|
||||
/// values rather than ticking a box, and what it sets is what the controller file keeps.
|
||||
#[test]
|
||||
fn the_nintendo_buttons_row_flips_letters_and_the_file_keeps_it() {
|
||||
let (page, group) = rows()
|
||||
.into_iter()
|
||||
.flat_map(|(page, groups)| groups.into_iter().map(move |g| (page, g)))
|
||||
.find(|(_, (_, rows))| {
|
||||
rows.iter()
|
||||
.any(|r| r.item == Item::Option(Opt::PadNintendoButtons))
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!((page, group.0), (Page::Device, "Glyphs"));
|
||||
let mut s = Settings::default();
|
||||
let item = Item::Option(Opt::PadNintendoButtons);
|
||||
assert!(item.steps(&s));
|
||||
s.controller.nintendo_letters = false;
|
||||
let mut ui = ControllerUi {
|
||||
focus: Some(Target::Item(item)),
|
||||
..ControllerUi::default()
|
||||
};
|
||||
// Vanilla's caption and value template; Horizon's own words come bundled.
|
||||
struct Vanilla;
|
||||
impl lang::ResourceSource for Vanilla {
|
||||
fn read(&self, path: &str) -> Option<Vec<u8>> {
|
||||
(path == "assets/minecraft/lang/en_us.json")
|
||||
.then(|| br#"{"options.generic_value": "%s: %s"}"#.to_vec())
|
||||
}
|
||||
}
|
||||
let value = |ui: &ControllerUi, s: &Settings| {
|
||||
lang::scoped(lang::Language::load(&[&Vanilla], "en_us"), || {
|
||||
ui.parts(item, s)
|
||||
})
|
||||
.0
|
||||
};
|
||||
assert_eq!(value(&ui, &s).0, "Nintendo Buttons");
|
||||
assert_eq!(value(&ui, &s).1.as_deref(), Some("By Position"));
|
||||
let actions = key(&mut ui, &mut s, K::Enter);
|
||||
assert!(actions.contains(&Action::Changed), "{actions:?}");
|
||||
assert!(s.controller.nintendo_letters);
|
||||
assert_eq!(value(&ui, &s).1.as_deref(), Some("By Letter"));
|
||||
assert!(PadSettings::parse(&s.controller.serialize()).nintendo_letters);
|
||||
key(&mut ui, &mut s, K::Enter);
|
||||
assert!(!s.controller.nintendo_letters);
|
||||
assert!(!PadSettings::parse(&s.controller.serialize()).nintendo_letters);
|
||||
}
|
||||
|
||||
/// What the screen draws at the bottom of the page changes with the choice, in every one of
|
||||
/// these settings.
|
||||
#[test]
|
||||
@@ -1502,7 +1438,6 @@ mod tests {
|
||||
for (opt, choices) in [
|
||||
(Opt::PadGameplayGuide, 3),
|
||||
(Opt::PadGlyphs, 4),
|
||||
(Opt::PadNintendoButtons, 2),
|
||||
(Opt::PadKeyboard, 3),
|
||||
(Opt::PadLightbar, 3),
|
||||
] {
|
||||
@@ -1535,7 +1470,6 @@ mod tests {
|
||||
for opt in [
|
||||
Opt::PadGameplayGuide,
|
||||
Opt::PadGlyphs,
|
||||
Opt::PadNintendoButtons,
|
||||
Opt::PadKeyboard,
|
||||
Opt::PadLightbar,
|
||||
] {
|
||||
|
||||
@@ -141,8 +141,7 @@ impl DataPack {
|
||||
pub fn discover(world: &Path) -> Vec<DataPack> {
|
||||
discover_folder(&world.join("datapacks"))
|
||||
}
|
||||
/// `FolderRepositorySource.loadPacks`: packs in Create World's temporary
|
||||
/// folder or a world's data-pack directory, with required folder metadata.
|
||||
/// Packs in Create World's temporary pack folder, before a world exists.
|
||||
pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
return Vec::new();
|
||||
@@ -151,7 +150,7 @@ pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
|
||||
for e in entries.flatten() {
|
||||
let name = e.file_name().to_string_lossy().into_owned();
|
||||
let path = e.path();
|
||||
if path.is_dir() && folder_metadata_present(&path) {
|
||||
if path.is_dir() && path.join("pack.mcmeta").exists() {
|
||||
packs.push(DataPack {
|
||||
id: format!("file/{name}"),
|
||||
source: Source::Dir(path),
|
||||
@@ -169,22 +168,6 @@ pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
|
||||
packs.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
packs
|
||||
}
|
||||
/// `Pack.readPackMetadata`: even its fallback codec requires a pack section
|
||||
/// and description. Keep this folder gate separate from format compatibility
|
||||
/// and the existing interpretation of the description component.
|
||||
fn folder_metadata_present(path: &Path) -> bool {
|
||||
let Some(meta) = std::fs::read(path.join("pack.mcmeta"))
|
||||
.ok()
|
||||
.and_then(|bytes| crate::packs::parse_meta(&bytes))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
meta.as_object()
|
||||
.and_then(|root| root.get("pack"))
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.and_then(|pack| pack.get("description"))
|
||||
.is_some_and(|description| !description.is_null())
|
||||
}
|
||||
/// The resources root's built-in feature packs (`ServerPacksSource`
|
||||
/// scanning the game's `data/minecraft/datapacks`): every folder with a
|
||||
/// `pack.mcmeta`, a feature pack (`PackSource.FEATURE`). Off until a world
|
||||
@@ -278,43 +261,6 @@ pub fn screen_packs(dir: &Path, resources: &Path) -> Vec<crate::pack_screen::Pac
|
||||
}
|
||||
infos
|
||||
}
|
||||
/// The folder of a game folder whose packs every new world there starts with: a modpack's profile
|
||||
/// keeps the packs of its data-only jars here (`horizon-port-pack`). Each world gets its own copy,
|
||||
/// as Create World's packs do, so a world keeps working when the folder changes. Vanilla has no
|
||||
/// such folder, and without one nothing here happens.
|
||||
pub const NEW_WORLD_PACKS: &str = "new-world-datapacks";
|
||||
|
||||
/// Create World's temporary pack folder holding `game_dir`'s [`NEW_WORLD_PACKS`], and their ids,
|
||||
/// to show them selected in the Data Packs screen. `None` when there are none.
|
||||
pub fn seed_create_world(game_dir: &Path) -> Option<(PathBuf, Vec<String>)> {
|
||||
let from = game_dir.join(NEW_WORLD_PACKS);
|
||||
let ids: Vec<String> = discover_folder(&from).into_iter().map(|p| p.id).collect();
|
||||
if ids.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let seeded = temp_folder()
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|dir| copy(&from, &dir).map(|()| dir));
|
||||
match seeded {
|
||||
Ok(dir) => Some((dir, ids)),
|
||||
Err(e) => {
|
||||
log::warn!("Horizon: data packs for new worlds: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A world about to be created without Create World (`--world`, the control socket) gets
|
||||
/// `game_dir`'s [`NEW_WORLD_PACKS`]. The game turns on every pack it finds in a world.
|
||||
pub fn install_new_world_packs(game_dir: &Path, world: &Path) -> Result<(), String> {
|
||||
let from = game_dir.join(NEW_WORLD_PACKS);
|
||||
let exists = |name: &str| world.join(name).exists();
|
||||
if !from.is_dir() || exists("level.dat") || exists("level.dat_old") {
|
||||
return Ok(());
|
||||
}
|
||||
copy(&from, &world.join("datapacks"))
|
||||
}
|
||||
|
||||
/// A new empty folder for Create World's packs
|
||||
/// (`Files.createTempDirectory("mcworld-")`).
|
||||
pub fn temp_folder() -> std::io::Result<PathBuf> {
|
||||
@@ -662,7 +608,7 @@ pub struct DataStore {
|
||||
/// `item_modifier` files, for `/item modify`.
|
||||
pub item_modifiers: BTreeMap<String, serde_json::Value>,
|
||||
/// The content tables' raw files from the packs after the built-in one:
|
||||
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `dialog/`, `damage_type/`, `data_maps/`, `enchantment/` and `tags/…/` files, by
|
||||
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `dialog/`, `damage_type/`, `data_maps/` and `tags/…/` files, by
|
||||
/// `<kind-dir>/<ns>:<path>`, each id's files in pack order. The tables
|
||||
/// merge them over what they read from the resource root.
|
||||
pub files: BTreeMap<String, Vec<Vec<u8>>>,
|
||||
@@ -671,7 +617,7 @@ pub struct DataStore {
|
||||
}
|
||||
/// `<kind-dir>/<ns>:<path>` for a pack file the content tables read:
|
||||
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `trade_set/`,
|
||||
/// `villager_trade/`, `dialog/`, `damage_type/`, `data_maps/`, `enchantment/` or `tags/<sub>/`, where the id keeps the sub
|
||||
/// `villager_trade/`, `dialog/`, `damage_type/`, `data_maps/` or `tags/<sub>/`, where the id keeps the sub
|
||||
/// path so a nested tag kind stays addressable.
|
||||
fn content_id(file: &str) -> Option<String> {
|
||||
let rest = file.strip_prefix("data/")?;
|
||||
@@ -679,7 +625,7 @@ fn content_id(file: &str) -> Option<String> {
|
||||
let (kind, rest) = rest.split_once('/')?;
|
||||
let kind = match kind {
|
||||
"recipe" | "recipe_filter" | "loot_table" | "advancement" | "trade_set"
|
||||
| "villager_trade" | "dialog" | "damage_type" | "data_maps" | "enchantment" => kind,
|
||||
| "villager_trade" | "dialog" | "damage_type" | "data_maps" => kind,
|
||||
// A tag id is `{ns}:{path-under-tags}` (`tags/worldgen/biome/
|
||||
// test:sub/mytag`), the whole path after the namespace, so nested
|
||||
// registries keep their directory (`TagLoader`'s id shape).
|
||||
@@ -713,7 +659,6 @@ impl DataStore {
|
||||
"damage_type",
|
||||
"data_maps",
|
||||
"dialog",
|
||||
"enchantment",
|
||||
"function",
|
||||
"item_modifier",
|
||||
"loot_table",
|
||||
@@ -769,55 +714,15 @@ impl DataStore {
|
||||
}
|
||||
s
|
||||
}
|
||||
/// `ServerFunctionLibrary.getTag`: only valid function tags expand
|
||||
/// (nested tags included, each function once).
|
||||
/// `FunctionManager.getTag`: the functions a tag expands to (nested tags
|
||||
/// included, each function once).
|
||||
pub fn tag_functions(&self, id: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut seen_fns = std::collections::HashSet::new();
|
||||
let mut seen_tags = std::collections::HashSet::new();
|
||||
let invalid = self.invalid_function_tags(id);
|
||||
self.expand(id, &mut out, &mut seen_fns, &mut seen_tags, &invalid);
|
||||
self.expand(id, &mut out, &mut seen_fns, &mut seen_tags);
|
||||
out
|
||||
}
|
||||
/// `TagLoader.tryBuildTag`: missing required members invalidate their tag and
|
||||
/// required parents. Optional invalid children contribute nothing.
|
||||
/// Resolve against the current accepted function map, without caching a
|
||||
/// candidate store or changing the existing expansion's cycle handling.
|
||||
fn invalid_function_tags<'a>(&'a self, id: &'a str) -> std::collections::HashSet<&'a str> {
|
||||
let mut invalid = std::collections::HashSet::new();
|
||||
let mut parents: std::collections::HashMap<&str, Vec<&str>> =
|
||||
std::collections::HashMap::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut pending = vec![id];
|
||||
while let Some(tag) = pending.pop() {
|
||||
if !seen.insert(tag) {
|
||||
continue;
|
||||
}
|
||||
let Some(entries) = self.function_tags.get(tag) else {
|
||||
invalid.insert(tag);
|
||||
continue;
|
||||
};
|
||||
for (entry, required) in entries {
|
||||
if let Some(child) = entry.strip_prefix('#') {
|
||||
pending.push(child);
|
||||
if *required {
|
||||
parents.entry(child).or_default().push(tag);
|
||||
}
|
||||
} else if *required && !self.functions.contains_key(entry) {
|
||||
invalid.insert(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
pending.extend(invalid.iter().copied());
|
||||
while let Some(child) = pending.pop() {
|
||||
for &parent in parents.get(child).into_iter().flatten() {
|
||||
if invalid.insert(parent) {
|
||||
pending.push(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
invalid
|
||||
}
|
||||
/// Depth-first walk keeping first-seen order. Each tag is expanded at most
|
||||
/// once, so reference cycles end and nesting depth is not capped.
|
||||
fn expand<'a>(
|
||||
@@ -826,19 +731,18 @@ impl DataStore {
|
||||
out: &mut Vec<String>,
|
||||
seen_fns: &mut std::collections::HashSet<&'a str>,
|
||||
seen_tags: &mut std::collections::HashSet<&'a str>,
|
||||
invalid: &std::collections::HashSet<&'a str>,
|
||||
) {
|
||||
if invalid.contains(id) || !seen_tags.insert(id) {
|
||||
if !seen_tags.insert(id) {
|
||||
return;
|
||||
}
|
||||
let Some(entries) = self.function_tags.get(id) else {
|
||||
return;
|
||||
};
|
||||
for (e, _) in entries {
|
||||
for (e, required) in entries {
|
||||
match e.strip_prefix('#') {
|
||||
Some(inner) => self.expand(inner, out, seen_fns, seen_tags, invalid),
|
||||
Some(inner) => self.expand(inner, out, seen_fns, seen_tags),
|
||||
None => {
|
||||
if self.functions.contains_key(e) && seen_fns.insert(e) {
|
||||
if (self.functions.contains_key(e) || *required) && seen_fns.insert(e) {
|
||||
out.push(e.clone());
|
||||
}
|
||||
}
|
||||
@@ -1020,97 +924,6 @@ pub fn worldgen_overrides(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn folder_metadata_is_required_before_contents_are_discovered() {
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("horizon-folder-metadata-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
for (name, metadata) in [
|
||||
("broken", Some("{")),
|
||||
("root_empty", Some("{}")),
|
||||
("root_array", Some("[]")),
|
||||
("pack_empty", Some(r#"{"pack":{}}"#)),
|
||||
("pack_string", Some(r#"{"pack":"bad"}"#)),
|
||||
("missing", None),
|
||||
(
|
||||
"full",
|
||||
Some(
|
||||
r#"{"pack":{"description":"full","pack_format":121,"min_format":[121,0],"max_format":[121,0]}}"#,
|
||||
),
|
||||
),
|
||||
(
|
||||
"description",
|
||||
Some(r#"{"pack":{"description":"description only"}}"#),
|
||||
),
|
||||
("numeric", Some(r#"{"pack":{"description":42}}"#)),
|
||||
("null", Some(r#"{"pack":{"description":null}}"#)),
|
||||
("bom", Some("\u{feff}{\"pack\":{\"description\":\"BOM\"}}")),
|
||||
(
|
||||
"comment",
|
||||
Some("{\"pack\":{\"description\":\"comment\"}}\n// trailing"),
|
||||
),
|
||||
] {
|
||||
let dir = root.join(name);
|
||||
std::fs::create_dir_all(dir.join("data/h146/function")).unwrap();
|
||||
std::fs::write(
|
||||
dir.join(format!("data/h146/function/{name}.mcfunction")),
|
||||
"scoreboard players set admitted h146 1\n",
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(metadata) = metadata {
|
||||
std::fs::write(dir.join("pack.mcmeta"), metadata).unwrap();
|
||||
}
|
||||
}
|
||||
let packs = discover_folder(&root);
|
||||
assert_eq!(
|
||||
packs.iter().map(|p| p.id.as_str()).collect::<Vec<_>>(),
|
||||
[
|
||||
"file/bom",
|
||||
"file/comment",
|
||||
"file/description",
|
||||
"file/full",
|
||||
"file/numeric"
|
||||
]
|
||||
);
|
||||
let store = DataStore::load(&packs.iter().collect::<Vec<_>>());
|
||||
assert_eq!(
|
||||
store
|
||||
.functions
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>(),
|
||||
[
|
||||
"h146:bom",
|
||||
"h146:comment",
|
||||
"h146:description",
|
||||
"h146:full",
|
||||
"h146:numeric"
|
||||
]
|
||||
);
|
||||
let numeric = packs.iter().find(|p| p.id == "file/numeric").unwrap();
|
||||
let raw: serde_json::Value =
|
||||
serde_json::from_slice(&numeric.read("pack.mcmeta").unwrap()).unwrap();
|
||||
assert_eq!(raw["pack"]["description"], 42);
|
||||
// Builtin and feature construction still have their independent paths.
|
||||
let resources = root.join("resources");
|
||||
let feature = resources.join("data/minecraft/datapacks/feature");
|
||||
std::fs::create_dir_all(&feature).unwrap();
|
||||
std::fs::write(
|
||||
feature.join("pack.mcmeta"),
|
||||
r#"{"pack":{"description":"feature"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
feature_packs(&resources)
|
||||
.iter()
|
||||
.map(|p| p.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["feature"]
|
||||
);
|
||||
assert_eq!(DataPack::builtin(&resources).id, "vanilla");
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_map_files_are_content_files_keyed_by_map_namespace_and_registry() {
|
||||
assert_eq!(
|
||||
@@ -1138,35 +951,6 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_game_folder_s_new_world_packs_go_into_new_worlds_only() {
|
||||
let root = std::env::temp_dir().join(format!("hz-new-world-packs-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
let (game, world, old) = (root.join("game"), root.join("new"), root.join("old"));
|
||||
assert!(seed_create_world(&game).is_none());
|
||||
assert_eq!(install_new_world_packs(&game, &world), Ok(()));
|
||||
assert!(!world.join("datapacks").exists());
|
||||
for pack in ["void", "islands"] {
|
||||
let dir = game.join(NEW_WORLD_PACKS).join(pack);
|
||||
std::fs::create_dir_all(dir.join("data/v/function")).unwrap();
|
||||
std::fs::write(dir.join("pack.mcmeta"), r#"{"pack":{"description":""}}"#).unwrap();
|
||||
std::fs::write(dir.join("data/v/function/a.mcfunction"), "say a").unwrap();
|
||||
}
|
||||
let (temp, ids) = seed_create_world(&game).unwrap();
|
||||
assert_eq!(ids, ["file/islands", "file/void"]);
|
||||
assert!(temp.join("void/data/v/function/a.mcfunction").is_file());
|
||||
let _ = std::fs::remove_dir_all(&temp);
|
||||
install_new_world_packs(&game, &world).unwrap();
|
||||
let ids: Vec<String> = discover(&world).into_iter().map(|p| p.id).collect();
|
||||
assert_eq!(ids, ["file/islands", "file/void"]);
|
||||
// A world that exists already keeps the packs it has.
|
||||
std::fs::create_dir_all(&old).unwrap();
|
||||
std::fs::write(old.join("level.dat"), "").unwrap();
|
||||
install_new_world_packs(&game, &old).unwrap();
|
||||
assert!(discover(&old).is_empty());
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enabled_packs_override_worldgen_in_order() {
|
||||
let dir = std::env::temp_dir().join(format!("hz-datapack-worldgen-{}", std::process::id()));
|
||||
@@ -1177,7 +961,7 @@ mod tests {
|
||||
std::fs::write(path, t).unwrap();
|
||||
};
|
||||
for pack in ["a", "b", "off"] {
|
||||
write(pack, "pack.mcmeta", r#"{"pack":{"description":"fixture"}}"#);
|
||||
write(pack, "pack.mcmeta", "{}");
|
||||
write(pack, "data/minecraft/worldgen/noise/cave.json", pack);
|
||||
write(pack, "data/p/structure/room/a.nbt", pack);
|
||||
write(
|
||||
@@ -1348,20 +1132,6 @@ mod tests {
|
||||
#[test]
|
||||
fn tag_resolution_dedupes_fast_ends_cycles_and_nests_deeply() {
|
||||
let mut store = DataStore::default();
|
||||
// Every referenced function exists; missing required ones invalidate a tag.
|
||||
for id in (0..25_000)
|
||||
.map(|i| format!("t:f{i}"))
|
||||
.chain(["t:x", "t:y", "t:deep"].map(str::to_owned))
|
||||
{
|
||||
store.functions.insert(
|
||||
id.clone(),
|
||||
Function {
|
||||
id,
|
||||
lines: Vec::new(),
|
||||
parameters: Vec::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
let big: Vec<(String, bool)> = (0..50_000)
|
||||
.map(|i| (format!("t:f{}", i % 25_000), true))
|
||||
.collect();
|
||||
@@ -1446,13 +1216,13 @@ mod tests {
|
||||
write(
|
||||
"fns",
|
||||
"pack.mcmeta",
|
||||
r#"{"pack":{"description":"fixture","min_format":121,"max_format":121}}"#,
|
||||
r#"{"pack":{"min_format":121,"max_format":121}}"#,
|
||||
);
|
||||
write("fns", "data/x/function/f.mcfunction", "say hi");
|
||||
write(
|
||||
"off",
|
||||
"pack.mcmeta",
|
||||
r#"{"pack":{"description":"fixture","min_format":121,"max_format":121}}"#,
|
||||
r#"{"pack":{"min_format":121,"max_format":121}}"#,
|
||||
);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut zip = zip::ZipWriter::new(std::fs::File::create(dir.join("old.zip")).unwrap());
|
||||
|
||||
@@ -33,10 +33,7 @@ struct Globals {
|
||||
hdr_windows: array<vec4<f32>, 2>,
|
||||
// HDR lighting: x how strongly the sky lights the world now (`Sky::sky_light_factor`),
|
||||
// y what the sky and fog get: the same, but none under water.
|
||||
// w: apply the no-pack HDR gain; HDR_WRITE_LIGHT independently keeps float alpha as coverage.
|
||||
hdr_light: vec4<f32>,
|
||||
hdr_settings: vec4<f32>,
|
||||
hdr_shares: vec4<f32>,
|
||||
}
|
||||
@group(0) @binding(0) var<uniform> globals: Globals;
|
||||
@group(0) @binding(1) var atlas: texture_2d<f32>;
|
||||
@@ -279,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> {
|
||||
@@ -374,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));
|
||||
@@ -431,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,
|
||||
@@ -452,12 +424,7 @@ fn celestial_color(in: Out) -> vec4<f32> {
|
||||
let y = brightness(added);
|
||||
// Only what's past the tier's window takes the mark: a soft edge keeps the sky's light,
|
||||
// or it would come out darker than the sky around it.
|
||||
let full = y >= globals.hdr_windows[1][clamp(tier, 1u, 4u) - 1u];
|
||||
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 {
|
||||
return vec4(hdr_written(added, select(1.0, hdr_mark(tier, y), full), globals.hdr_settings, globals.hdr_shares), color.a);
|
||||
}
|
||||
if HDR_WRITE_LIGHT { return vec4(added, color.a); }
|
||||
if !full { return vec4(added, 0.0); }
|
||||
if y < globals.hdr_windows[1][clamp(tier, 1u, 4u) - 1u] { return vec4(added, 0.0); }
|
||||
return vec4(added, 1.0 - hdr_mark(tier, y));
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -5,7 +5,7 @@ use crate::{
|
||||
lang,
|
||||
options_ui::{Event, Page},
|
||||
settings::Settings,
|
||||
settings_shell::{self as shell, Group, Output, Row, Screen, Sheet, Value},
|
||||
settings_shell::{Group, Output, Row, Screen, Sheet, Value},
|
||||
};
|
||||
|
||||
const TITLE: &str = "horizon.pages.extras";
|
||||
@@ -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,
|
||||
@@ -232,66 +228,4 @@ mod tests {
|
||||
assert!(s.extras.sort);
|
||||
assert_eq!(screen.wheel_step(&mut s, ExtraRow::Zoom(0), true), None);
|
||||
}
|
||||
|
||||
/// Extras has no Undo: a change leaves none to press or reach.
|
||||
#[test]
|
||||
fn a_change_on_extras_leaves_no_undo() {
|
||||
use crate::settings_shell::{Columns, Target};
|
||||
use winit::keyboard::KeyCode as K;
|
||||
let mut s = Settings::default();
|
||||
let mut shell = Shell::<ExtrasScreen>::default();
|
||||
let at = shell.point_at(&s, SIZE, ExtraRow::Item(Item::Sort));
|
||||
assert!(
|
||||
!shell
|
||||
.click(&mut s, at, SIZE, false, false, false)
|
||||
.is_empty()
|
||||
);
|
||||
assert!(s.extras.sort && !shell.changed());
|
||||
let cols = Columns::of(SIZE.0, SIZE.1).with_note_lines(NOTE_LINES);
|
||||
let [x, y, ..] = cols.button(1);
|
||||
assert!(
|
||||
shell
|
||||
.click(&mut s, (x + 2.0, y + 2.0), SIZE, false, false, false)
|
||||
.is_empty()
|
||||
);
|
||||
assert!(s.extras.sort);
|
||||
let reached = (0..100).any(|_| {
|
||||
shell.key(&mut s, K::Tab, None, false, SIZE);
|
||||
shell.focused() == Some(Target::Undo)
|
||||
});
|
||||
assert!(!reached);
|
||||
}
|
||||
|
||||
/// A page with no page list has Done under the search field: Down goes to the first row
|
||||
/// instead, from the keys and, as the frame declares it, from a controller's focus.
|
||||
#[test]
|
||||
fn down_from_the_search_field_lands_on_the_first_row() {
|
||||
use crate::{hud::HudBatch, settings_shell::Columns, ui};
|
||||
use winit::keyboard::KeyCode as K;
|
||||
let atlas = crate::hud::HudAtlas::build(std::path::Path::new("/nonexistent")).unwrap();
|
||||
let mut s = Settings::default();
|
||||
let mut shell = Shell::<ExtrasScreen>::default();
|
||||
let mut batch = HudBatch::new([1708, 960]);
|
||||
shell.draw(&s, &mut batch, &atlas, (0.0, 0.0), false, None);
|
||||
crate::widgets::end_frame(1);
|
||||
let size = (batch.width, batch.height);
|
||||
let cols = Columns::of(size.0, size.1).with_note_lines(NOTE_LINES);
|
||||
// The Mods group's note can't take the focus: the first item can.
|
||||
let first = ExtraRow::Item(Item::OwnNameTag);
|
||||
let at = shell.point_at(&s, size, first);
|
||||
let [x, y, w, h] = crate::widgets::linked_step(cols.search(), true).unwrap();
|
||||
assert!(ui::hovered(at, x, y, w, h));
|
||||
assert_eq!(crate::widgets::linked_step(cols.search(), false), None);
|
||||
let [x, y, w, h] = cols.search();
|
||||
shell.click(
|
||||
&mut s,
|
||||
(x + w / 2.0, y + h / 2.0),
|
||||
size,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
shell.key(&mut s, K::ArrowDown, None, false, size);
|
||||
assert_eq!(shell.focused_row(), Some(first));
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ use crate::{
|
||||
environment::Sky,
|
||||
world::World,
|
||||
};
|
||||
use horizon_core::random::{LegacyRandom, RandomSource};
|
||||
use std::path::Path;
|
||||
|
||||
/// `FogType` of the camera position.
|
||||
@@ -65,7 +64,7 @@ impl FogTables {
|
||||
}
|
||||
|
||||
/// Fog state that evolves over time.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct FogState {
|
||||
/// `LocalPlayer.waterVisionTime` (0..600).
|
||||
pub water_vision_time: i32,
|
||||
@@ -73,28 +72,24 @@ pub struct FogState {
|
||||
pub rain_fog_multiplier: f32,
|
||||
/// `LightmapRenderStateExtractor.blockLightFlicker`.
|
||||
pub block_light_flicker: f32,
|
||||
/// `LightmapRenderStateExtractor.random`: `RandomSource.create()`.
|
||||
flicker_random: LegacyRandom,
|
||||
}
|
||||
impl Default for FogState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
water_vision_time: 0,
|
||||
rain_fog_multiplier: 0.0,
|
||||
block_light_flicker: 0.0,
|
||||
flicker_random: crate::random_source::create(),
|
||||
}
|
||||
}
|
||||
flicker_seed: u64,
|
||||
}
|
||||
impl FogState {
|
||||
fn next_float(&mut self) -> f32 {
|
||||
// splitmix64: the vanilla source is unseeded `RandomSource.create()`.
|
||||
self.flicker_seed = self.flicker_seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
let mut z = self.flicker_seed;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
((z ^ (z >> 31)) >> 40) as f32 / (1u32 << 24) as f32
|
||||
}
|
||||
/// `LightmapRenderStateExtractor.tick`: the torch light flickers.
|
||||
pub fn tick_flicker(&mut self) {
|
||||
let r = &mut self.flicker_random;
|
||||
let (a, b, c, d) = (
|
||||
r.next_float(),
|
||||
r.next_float(),
|
||||
r.next_float(),
|
||||
r.next_float(),
|
||||
self.next_float(),
|
||||
self.next_float(),
|
||||
self.next_float(),
|
||||
self.next_float(),
|
||||
);
|
||||
self.block_light_flicker += (a - b) * c * d * 0.1;
|
||||
self.block_light_flicker *= 0.9;
|
||||
@@ -755,7 +750,7 @@ mod tests {
|
||||
rain_fog_multiplier: 0.1,
|
||||
..FogState::default()
|
||||
};
|
||||
let mut settled = start.clone();
|
||||
let mut settled = start;
|
||||
settle_rain_fog(&mut settled, 0.5, 15, true);
|
||||
assert_eq!(settled.rain_fog_multiplier, 0.5);
|
||||
let mut eased = start;
|
||||
|
||||
@@ -126,16 +126,11 @@ struct Cache {
|
||||
random: horizon_core::random::LegacyRandom,
|
||||
}
|
||||
pub struct Font {
|
||||
/// The atlas rects of every texture, by resource name: a bitmap provider's
|
||||
/// `file` may name any texture, not only one under `textures/font`.
|
||||
/// The atlas rects of the sheets under `textures/font`.
|
||||
sheets: HashMap<String, Rect>,
|
||||
missing: Glyph,
|
||||
white: Rect,
|
||||
root: PathBuf,
|
||||
/// The game's own resources, below a pack overlay: a font file the overlay
|
||||
/// lacks (a pack adds providers over vanilla's, or the overlay left a
|
||||
/// directory out) is read from here, as vanilla's resource stack does.
|
||||
fallback: Option<PathBuf>,
|
||||
/// Atlas size and the reserved dynamic region (y, height).
|
||||
atlas: (u32, u32),
|
||||
region: (u32, u32),
|
||||
@@ -204,13 +199,6 @@ fn parse_hex(text: &[u8], out: &mut HashMap<u32, HexGlyph>) {
|
||||
out.insert(codepoint, HexGlyph { rows, left, right });
|
||||
}
|
||||
}
|
||||
/// A resource file's bytes, from `root` or, when the overlay lacks it, from
|
||||
/// the game's own resources below (`defaults`).
|
||||
fn read_resource(root: &Path, defaults: Option<&Path>, path: &str) -> Option<Vec<u8>> {
|
||||
std::fs::read(root.join(path))
|
||||
.ok()
|
||||
.or_else(|| defaults.and_then(|d| std::fs::read(d.join(path)).ok()))
|
||||
}
|
||||
fn read_json(root: &Path, path: &str) -> Option<serde_json::Value> {
|
||||
serde_json::from_slice(&std::fs::read(root.join(path)).ok()?).ok()
|
||||
}
|
||||
@@ -293,7 +281,6 @@ fn hex_advance(h: &HexGlyph) -> f32 {
|
||||
/// pixels, copied into the atlas when first drawn.
|
||||
fn load_bitmap(
|
||||
root: &Path,
|
||||
defaults: Option<&Path>,
|
||||
p: &serde_json::Value,
|
||||
sheets: &HashMap<String, Rect>,
|
||||
pixels: &[u8],
|
||||
@@ -303,36 +290,22 @@ fn load_bitmap(
|
||||
let mut out = HashMap::new();
|
||||
let file = p["file"].as_str().unwrap_or("");
|
||||
let (ns, name) = file.split_once(':').unwrap_or(("minecraft", file));
|
||||
let name = name.trim_end_matches(".png");
|
||||
// The atlas names the default namespace's textures by their path and every
|
||||
// other namespace's as `namespace:path` (HudAtlas::build).
|
||||
let atlas_name = if ns == "minecraft" {
|
||||
name.to_owned()
|
||||
} else {
|
||||
format!("{ns}:{name}")
|
||||
};
|
||||
let in_atlas = sheets.get(&atlas_name).copied();
|
||||
let in_atlas = (ns == "minecraft")
|
||||
.then(|| sheets.get(name.trim_end_matches(".png")).copied())
|
||||
.flatten();
|
||||
// The sheet: its size, and its pixels when they are not in the atlas.
|
||||
let (sw, sh, sheet) = match in_atlas {
|
||||
Some(rect) => {
|
||||
let (w, h) = rect.texels(atlas);
|
||||
(w, h, None)
|
||||
}
|
||||
None => {
|
||||
let path = resource_path(file, "textures", "");
|
||||
let sheet = crate::hud::decode(&root.join(&path)).or_else(|first| {
|
||||
defaults
|
||||
.and_then(|d| crate::hud::decode(&d.join(&path)).ok())
|
||||
.ok_or(first)
|
||||
});
|
||||
match sheet {
|
||||
Ok((w, h, rgba)) => (w, h, Some(Arc::new((w, rgba)))),
|
||||
Err(_) => {
|
||||
log::warn!("font sheet {file} could not be read");
|
||||
return out;
|
||||
}
|
||||
None => match crate::hud::decode(&root.join(resource_path(file, "textures", ""))) {
|
||||
Ok((w, h, rgba)) => (w, h, Some(Arc::new((w, rgba)))),
|
||||
Err(_) => {
|
||||
log::warn!("font sheet {file} could not be read");
|
||||
return out;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
let rows: Vec<Vec<char>> = p["chars"]
|
||||
.as_array()
|
||||
@@ -409,13 +382,11 @@ fn load_bitmap(
|
||||
}
|
||||
/// `TrueTypeGlyphProviderDefinition.load`: a TrueType file (not CFF) under
|
||||
/// `font/`, its Unicode characters but the `skip` ones.
|
||||
fn load_true_type(root: &Path, defaults: Option<&Path>, p: &serde_json::Value) -> Option<TrueType> {
|
||||
fn load_true_type(root: &Path, p: &serde_json::Value) -> Option<TrueType> {
|
||||
let file = p["file"].as_str()?;
|
||||
let path = resource_path(file, "font", "");
|
||||
let Some(data) = read_resource(root, defaults, &path) else {
|
||||
log::warn!("font file {file} could not be read");
|
||||
return None;
|
||||
};
|
||||
let data = std::fs::read(root.join(resource_path(file, "font", "")))
|
||||
.map_err(|e| log::warn!("font file {file}: {e}"))
|
||||
.ok()?;
|
||||
// FT_Get_Font_Format must say TrueType: CFF outlines ("OTTO") are refused.
|
||||
if data.starts_with(b"OTTO") {
|
||||
log::warn!("font file {file} is not in TTF format");
|
||||
@@ -515,16 +486,13 @@ impl TrueType {
|
||||
)
|
||||
}
|
||||
}
|
||||
fn load_hex(root: &Path, defaults: Option<&Path>, p: &serde_json::Value) -> HashMap<u32, HexGlyph> {
|
||||
fn load_hex(root: &Path, p: &serde_json::Value) -> HashMap<u32, HexGlyph> {
|
||||
let mut glyphs = HashMap::new();
|
||||
let Some(file) = p["hex_file"].as_str() else {
|
||||
return glyphs;
|
||||
};
|
||||
let path = resource_path(file, "", "").replace("//", "/");
|
||||
let Some(zip) = Zip::open(&root.join(&path))
|
||||
.ok()
|
||||
.or_else(|| defaults.and_then(|d| Zip::open(&d.join(&path)).ok()))
|
||||
else {
|
||||
let path = root.join(resource_path(file, "", "").replace("//", "/"));
|
||||
let Ok(zip) = Zip::open(&path) else {
|
||||
return glyphs;
|
||||
};
|
||||
let names: Vec<String> = zip
|
||||
@@ -635,24 +603,7 @@ impl Font {
|
||||
region: (u32, u32),
|
||||
white: Rect,
|
||||
) -> Self {
|
||||
Self::load_with(root, None, sprites, pixels, atlas, region, white, false)
|
||||
}
|
||||
/// [`Font::load`] over a pack overlay: a sheet, TrueType file or Unihex
|
||||
/// file the overlay lacks is read from `defaults`, the game's own
|
||||
/// resources, as vanilla's resource stack does. (Definitions stay the
|
||||
/// overlay's: a pack's filter may hide one from a lower pack.)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn load_over(
|
||||
root: &Path,
|
||||
defaults: &Path,
|
||||
sprites: &HashMap<String, Rect>,
|
||||
pixels: &[u8],
|
||||
atlas: (u32, u32),
|
||||
region: (u32, u32),
|
||||
white: Rect,
|
||||
) -> Self {
|
||||
let fallback = (root != defaults).then_some(defaults);
|
||||
Self::load_with(root, fallback, sprites, pixels, atlas, region, white, false)
|
||||
Self::load_with(root, sprites, pixels, atlas, region, white, false)
|
||||
}
|
||||
/// A server measures text but never draws it. Decode bitmap sheets only
|
||||
/// to obtain advances, then release their pixels; never rasterize glyphs.
|
||||
@@ -665,21 +616,10 @@ impl Font {
|
||||
w: 1,
|
||||
h: 1,
|
||||
};
|
||||
Self::load_with(
|
||||
root,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
&[],
|
||||
(1, 1),
|
||||
(0, 0),
|
||||
white,
|
||||
true,
|
||||
)
|
||||
Self::load_with(root, &HashMap::new(), &[], (1, 1), (0, 0), white, true)
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn load_with(
|
||||
root: &Path,
|
||||
fallback: Option<&Path>,
|
||||
sprites: &HashMap<String, Rect>,
|
||||
pixels: &[u8],
|
||||
atlas: (u32, u32),
|
||||
@@ -687,7 +627,11 @@ impl Font {
|
||||
white: Rect,
|
||||
metrics_only: bool,
|
||||
) -> Self {
|
||||
let sheets = sprites.clone();
|
||||
let sheets = sprites
|
||||
.iter()
|
||||
.filter(|(k, _)| k.starts_with("font/"))
|
||||
.map(|(k, v)| (k.clone(), *v))
|
||||
.collect();
|
||||
let font = Self {
|
||||
sheets,
|
||||
missing: Glyph {
|
||||
@@ -701,7 +645,6 @@ impl Font {
|
||||
},
|
||||
white,
|
||||
root: root.to_owned(),
|
||||
fallback: fallback.map(|p| p.to_owned()),
|
||||
atlas,
|
||||
region,
|
||||
metrics_only,
|
||||
@@ -713,8 +656,11 @@ impl Font {
|
||||
pending: Vec::new(),
|
||||
full: false,
|
||||
},
|
||||
// `Font.random`: `RandomSource.create()`.
|
||||
random: crate::random_source::create(),
|
||||
random: horizon_core::random::LegacyRandom::new(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_nanos() as i64),
|
||||
),
|
||||
}),
|
||||
generation: std::cell::Cell::new(next_generation()),
|
||||
uniform: false,
|
||||
@@ -752,7 +698,6 @@ impl Font {
|
||||
Filter::of(p),
|
||||
load_bitmap(
|
||||
&self.root,
|
||||
self.fallback.as_deref(),
|
||||
p,
|
||||
&self.sheets,
|
||||
pixels,
|
||||
@@ -763,7 +708,7 @@ impl Font {
|
||||
"unihex" => Some(Provider::Unihex(Filter::of(p), p.clone(), None)),
|
||||
"ttf" => Some(Provider::TrueType(
|
||||
Filter::of(p),
|
||||
Box::new(load_true_type(&self.root, self.fallback.as_deref(), p)?),
|
||||
Box::new(load_true_type(&self.root, p)?),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
@@ -923,8 +868,7 @@ impl Font {
|
||||
}
|
||||
}
|
||||
Provider::Unihex(filter, def, glyphs) if filter.passes(uniform, japanese) => {
|
||||
let glyphs = glyphs
|
||||
.get_or_insert_with(|| load_hex(&self.root, self.fallback.as_deref(), def));
|
||||
let glyphs = glyphs.get_or_insert_with(|| load_hex(&self.root, def));
|
||||
if let Some(h) = glyphs.get(&u32::from(c)).copied()
|
||||
&& !(non_fishy && fishy_advance(hex_advance(&h)))
|
||||
{
|
||||
@@ -1082,8 +1026,7 @@ impl Font {
|
||||
}
|
||||
}
|
||||
Provider::Unihex(filter, def, glyphs) if filter.passes(uniform, japanese) => {
|
||||
let glyphs = glyphs
|
||||
.get_or_insert_with(|| load_hex(&self.root, self.fallback.as_deref(), def));
|
||||
let glyphs = glyphs.get_or_insert_with(|| load_hex(&self.root, def));
|
||||
if let Some(h) = glyphs.get(&u32::from(c)) {
|
||||
return Some(hex_advance(h));
|
||||
}
|
||||
@@ -1569,52 +1512,6 @@ mod tests {
|
||||
assert!(texels.iter().any(|t| t[0] == 255));
|
||||
}
|
||||
|
||||
/// A bitmap provider's `file` can name any texture. When the atlas has that
|
||||
/// texture (HUD packs keep their glyph sheets outside `textures/font`), the
|
||||
/// glyphs are read from it instead of being copied into the small run-time
|
||||
/// region one by one; the region then cannot fill up and turn later glyphs
|
||||
/// into the missing one, whose advance shifts the rest of a HUD line.
|
||||
#[test]
|
||||
fn a_bitmap_sheet_outside_font_is_read_from_the_atlas() {
|
||||
let sheet = png(16, 8, |x, _| x < 2 || (8..14).contains(&x));
|
||||
let (_, _, pixels) = crate::hud::decode_bytes(&sheet).unwrap();
|
||||
let rect = Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 1.0,
|
||||
v1: 1.0,
|
||||
w: 16,
|
||||
h: 8,
|
||||
};
|
||||
for (ns, file, key) in [
|
||||
("minecraft", "minecraft:hud/sheet.png", "hud/sheet"),
|
||||
(
|
||||
"hudengine",
|
||||
"hudengine:font/sheet.png",
|
||||
"hudengine:font/sheet",
|
||||
),
|
||||
] {
|
||||
let def = format!(
|
||||
r#"{{"providers":[{{"type":"bitmap","file":"{file}","ascent":7,"chars":["ab"]}}]}}"#
|
||||
);
|
||||
// The image is only in the atlas: the pack's copy is not on disk.
|
||||
let root = root(
|
||||
ns,
|
||||
&[("assets/minecraft/font/default.json", def.as_bytes())],
|
||||
);
|
||||
let sprites = HashMap::from([(key.to_string(), rect)]);
|
||||
let font = Font::load(&root, &sprites, &pixels, (16, 8), (0, 8), white());
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
assert_eq!(font.glyph('a').advance, 3.0, "{ns}");
|
||||
assert_eq!(font.glyph('b').advance, 7.0, "{ns}");
|
||||
assert!(font.glyph('a').rect.is_some(), "{ns}");
|
||||
assert!(
|
||||
font.take_patches().is_empty(),
|
||||
"{ns}: the atlas sheet is not copied into the run-time region"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styles_pick_their_font_and_unknown_fonts_draw_missing_glyphs() {
|
||||
let sheet = png(16, 8, |x, _| x < 2 || (8..14).contains(&x));
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -879,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()),
|
||||
@@ -1048,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;
|
||||
@@ -1092,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()),
|
||||
@@ -1320,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()),
|
||||
|
||||
@@ -67,7 +67,6 @@ fn copy(mesh: &SectionMesh) -> SectionMesh {
|
||||
sides: mesh.sides,
|
||||
translucent: mesh.translucent.clone(),
|
||||
packed: mesh.packed.clone(),
|
||||
packed_far: mesh.packed_far,
|
||||
solid_tints: Vec::new(),
|
||||
visibility: mesh.visibility,
|
||||
block_entities: mesh.block_entities.clone(),
|
||||
|
||||
@@ -1,436 +0,0 @@
|
||||
//! A resource pack's `core/sky` and `core/stars` shaders: the overworld's sky disc and stars drawn
|
||||
//! with the pack's GLSL (`SkyRenderer.renderSkyDisc`, `renderStars`), as a pack that adds a haze
|
||||
//! to the horizon or tints the night sky expects. Without one the sky draws natively (`fs_sky` in
|
||||
//! terrain.wgsl, stars.rs).
|
||||
|
||||
use glam::Mat4;
|
||||
use horizon_shaders::{CoreProgram, Glue};
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
/// `DefaultVertexFormat.POSITION`: the disc's and the stars' vertices.
|
||||
pub(crate) const GLUE: Glue = Glue {
|
||||
inputs: &[("Position", "vec3")],
|
||||
prelude: "layout(location = 0) in vec3 hz_position;",
|
||||
setup: "Position = hz_position;",
|
||||
// 26.3's ProjMat already has wgpu's reversed 0 to 1 depth (Projection: zZeroToOne).
|
||||
finish: "",
|
||||
vertex_id: "int(gl_VertexIndex)",
|
||||
};
|
||||
|
||||
/// Each uniform block's offset in the one buffer (`core_shader::BLOCKS` order).
|
||||
const BLOCK_OFFSETS: [u64; 4] = [0, 256, 512, 768];
|
||||
const BLOCK_SIZES: [u64; 4] = [48, 64, 160, 48];
|
||||
|
||||
/// `SkyRenderer.buildSkyDisc(16)`: a fan around the point 16 above the camera, out to 512 every
|
||||
/// 45 degrees, as a triangle list.
|
||||
pub(crate) fn disc_triangles() -> Vec<[f32; 3]> {
|
||||
let rim: Vec<[f32; 3]> = (-180..=180)
|
||||
.step_by(45)
|
||||
.map(|i: i32| {
|
||||
let a = (i as f32).to_radians();
|
||||
[512.0 * a.cos(), 16.0, 512.0 * a.sin()]
|
||||
})
|
||||
.collect();
|
||||
rim.windows(2)
|
||||
.flat_map(|edge| [[0.0, 16.0, 0.0], edge[0], edge[1]])
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// One of the sky's pack pipelines with its vertices and uniforms.
|
||||
struct Part {
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
group: wgpu::BindGroup,
|
||||
uniform: wgpu::Buffer,
|
||||
vertices: wgpu::Buffer,
|
||||
count: u32,
|
||||
}
|
||||
|
||||
impl Part {
|
||||
fn new(
|
||||
device: &wgpu::Device,
|
||||
format: wgpu::TextureFormat,
|
||||
program: &CoreProgram,
|
||||
name: &str,
|
||||
blend: Option<wgpu::BlendState>,
|
||||
triangles: &[[f32; 3]],
|
||||
) -> Option<Self> {
|
||||
// A pack's shader is untrusted: the backend failing to compile it is an internal error.
|
||||
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let internal = device.push_error_scope(wgpu::ErrorFilter::Internal);
|
||||
let module = |stage, module: &naga::Module| {
|
||||
device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some(&format!("Resource pack {name} ({stage})")),
|
||||
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module.clone())),
|
||||
})
|
||||
};
|
||||
let vertex = module("vertex", &program.vertex);
|
||||
let fragment = module("fragment", &program.fragment);
|
||||
// Vanilla's SKY and STARS pipelines have no samplers: a pack that reads one is not used.
|
||||
let entries: Vec<wgpu::BindGroupLayoutEntry> = (0..4)
|
||||
.map(|binding| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: wgpu::BufferSize::new(BLOCK_SIZES[binding as usize]),
|
||||
},
|
||||
count: None,
|
||||
})
|
||||
.collect();
|
||||
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some(name),
|
||||
entries: &entries,
|
||||
});
|
||||
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some(name),
|
||||
size: 1024,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let resources: Vec<wgpu::BindGroupEntry> = (0..4)
|
||||
.map(|i| wgpu::BindGroupEntry {
|
||||
binding: i as u32,
|
||||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||
buffer: &uniform,
|
||||
offset: BLOCK_OFFSETS[i],
|
||||
size: wgpu::BufferSize::new(BLOCK_SIZES[i]),
|
||||
}),
|
||||
})
|
||||
.collect();
|
||||
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some(name),
|
||||
layout: &layout,
|
||||
entries: &resources,
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some(name),
|
||||
bind_group_layouts: &[Some(&layout)],
|
||||
..Default::default()
|
||||
});
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(name),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &vertex,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[Some(wgpu::VertexBufferLayout {
|
||||
array_stride: 12,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &wgpu::vertex_attr_array![0 => Float32x3],
|
||||
})],
|
||||
},
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
// The sky is drawn first, behind everything: no depth test, no depth write.
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: Some(false),
|
||||
depth_compare: Some(wgpu::CompareFunction::Always),
|
||||
stencil: Default::default(),
|
||||
bias: Default::default(),
|
||||
}),
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &fragment,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let internal = pollster::block_on(internal.pop());
|
||||
if let Some(e) = internal.or(pollster::block_on(validation.pop())) {
|
||||
log::warn!("the resource pack's {name} shader is not used: {e}");
|
||||
return None;
|
||||
}
|
||||
let vertices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some(name),
|
||||
contents: bytemuck::cast_slice(triangles),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
Some(Self {
|
||||
pipeline,
|
||||
group,
|
||||
uniform,
|
||||
vertices,
|
||||
count: triangles.len() as u32,
|
||||
})
|
||||
}
|
||||
|
||||
fn draw(&self, pass: &mut wgpu::RenderPass<'_>) {
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
pass.set_vertex_buffer(0, self.vertices.slice(..));
|
||||
pass.draw(0..self.count, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
/// The packs' sky disc and stars pipelines, each present when a pack replaces that shader.
|
||||
pub(super) struct CoreSky {
|
||||
disc: Option<Part>,
|
||||
stars: Option<Part>,
|
||||
}
|
||||
|
||||
impl CoreSky {
|
||||
/// `None` when neither shader is replaced or neither builds.
|
||||
pub(super) fn new(
|
||||
device: &wgpu::Device,
|
||||
format: wgpu::TextureFormat,
|
||||
disc: Option<&CoreProgram>,
|
||||
stars: Option<&CoreProgram>,
|
||||
) -> Option<Self> {
|
||||
let disc = disc.and_then(|program| {
|
||||
// RenderPipelines.SKY: ColorTargetState.DEFAULT, no blending.
|
||||
Part::new(device, format, program, "sky", None, &disc_triangles())
|
||||
});
|
||||
let stars = stars.and_then(|program| {
|
||||
// RenderPipelines.STARS: BlendFunction.OVERLAY (src alpha and one; alpha one, zero).
|
||||
let overlay = wgpu::BlendState {
|
||||
color: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::SrcAlpha,
|
||||
dst_factor: wgpu::BlendFactor::One,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
alpha: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::One,
|
||||
dst_factor: wgpu::BlendFactor::Zero,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
};
|
||||
let triangles = super::stars::star_triangles();
|
||||
Part::new(device, format, program, "stars", Some(overlay), &triangles)
|
||||
});
|
||||
(disc.is_some() || stars.is_some()).then_some(Self { disc, stars })
|
||||
}
|
||||
|
||||
pub(super) fn draws_disc(&self) -> bool {
|
||||
self.disc.is_some()
|
||||
}
|
||||
|
||||
pub(super) fn draws_stars(&self) -> bool {
|
||||
self.stars.is_some()
|
||||
}
|
||||
|
||||
/// Uploads the disc's and the stars' blocks: `sky` and `stars` from [`uniforms`].
|
||||
pub(super) fn prepare(
|
||||
&self,
|
||||
uploads: &mut super::upload::Uploads,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
sky: &[u8],
|
||||
stars: Option<&[u8]>,
|
||||
) {
|
||||
if let Some(disc) = &self.disc {
|
||||
uploads.buffer(encoder, &disc.uniform, 0, sky);
|
||||
}
|
||||
if let (Some(part), Some(data)) = (&self.stars, stars) {
|
||||
uploads.buffer(encoder, &part.uniform, 0, data);
|
||||
}
|
||||
}
|
||||
|
||||
/// The disc, in place of the native sky colour. The caller sets its own group 0 again after.
|
||||
pub(super) fn draw_disc(&self, pass: &mut wgpu::RenderPass<'_>) {
|
||||
if let Some(disc) = &self.disc {
|
||||
disc.draw(pass);
|
||||
}
|
||||
}
|
||||
|
||||
/// The stars, in place of the native ones. The caller sets its own group 0 again after.
|
||||
pub(super) fn draw_stars(&self, pass: &mut wgpu::RenderPass<'_>) {
|
||||
if let Some(stars) = &self.stars {
|
||||
stars.draw(pass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The std140 blocks for one sky draw: `Globals`, `Projection` (`projection`, far enough for the
|
||||
/// disc's 512-block rim), `DynamicTransforms` (`model_view`: the camera's rotation, times the
|
||||
/// stars' rotation for them; `color`: the sky colour, or the star brightness in every channel)
|
||||
/// and `Fog` (`fog_color`, `fog`: environmental start and end, render distance start and end,
|
||||
/// sky end, clouds end).
|
||||
pub(super) fn uniforms(
|
||||
globals: [u8; 48],
|
||||
projection: Mat4,
|
||||
model_view: Mat4,
|
||||
color: [f32; 4],
|
||||
fog_color: [f32; 4],
|
||||
fog: [f32; 6],
|
||||
) -> Vec<u8> {
|
||||
let mut data = vec![0f32; 256];
|
||||
data[64..80].copy_from_slice(&projection.to_cols_array());
|
||||
let transforms = 128;
|
||||
data[transforms..transforms + 16].copy_from_slice(&model_view.to_cols_array());
|
||||
data[transforms + 16..transforms + 32].copy_from_slice(&Mat4::IDENTITY.to_cols_array());
|
||||
data[transforms + 32..transforms + 36].copy_from_slice(&color);
|
||||
data[192..196].copy_from_slice(&fog_color);
|
||||
data[196..202].copy_from_slice(&fog);
|
||||
let mut bytes: Vec<u8> = bytemuck::cast_slice(&data).to_vec();
|
||||
bytes[..48].copy_from_slice(&globals);
|
||||
bytes
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// `buildSkyDisc(16)`: eight triangles from the point above the camera to the rim, which
|
||||
/// starts and ends at x -512 (cos 180°) and passes through z -512, x 512 and z 512.
|
||||
#[test]
|
||||
fn the_disc_is_vanillas_fan_as_triangles() {
|
||||
let triangles = disc_triangles();
|
||||
assert_eq!(triangles.len(), 24);
|
||||
assert!(triangles.chunks(3).all(|t| t[0] == [0.0, 16.0, 0.0]));
|
||||
let rim: Vec<[f32; 3]> = triangles.chunks(3).map(|t| t[1]).collect();
|
||||
let near = |a: [f32; 3], b: [f32; 3]| a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-3);
|
||||
assert!(near(rim[0], [-512.0, 16.0, 0.0]));
|
||||
assert!(near(rim[2], [0.0, 16.0, -512.0]));
|
||||
assert!(near(rim[4], [512.0, 16.0, 0.0]));
|
||||
assert!(near(rim[6], [0.0, 16.0, 512.0]));
|
||||
assert!(near(triangles[23], [-512.0, 16.0, 0.0]));
|
||||
}
|
||||
|
||||
/// The prepared resources (`HORIZON_RESOURCES`, or `.local/horizon-client/<version>`), where
|
||||
/// vanilla's sky shader and its includes are.
|
||||
fn resources() -> std::path::PathBuf {
|
||||
std::env::var_os("HORIZON_RESOURCES")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|| {
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client")
|
||||
.join(horizon_core::VANILLA_VERSION)
|
||||
})
|
||||
}
|
||||
|
||||
/// Vanilla's `sky.vsh` with a pack's fragment shader that paints `ColorModulator` (the sky
|
||||
/// colour) where the disc is: looking at the horizon, the top half of the target is the
|
||||
/// disc, the bottom half stays clear.
|
||||
#[test]
|
||||
fn a_pack_sky_shader_paints_the_disc_above_the_horizon() {
|
||||
let root = resources();
|
||||
let Ok(vertex) =
|
||||
std::fs::read_to_string(root.join("assets/minecraft/shaders/core/sky.vsh"))
|
||||
else {
|
||||
eprintln!("skipping: no prepared resources at {}", root.display());
|
||||
return;
|
||||
};
|
||||
let include = |id: &str| {
|
||||
let (namespace, path) = id.split_once(':')?;
|
||||
std::fs::read_to_string(root.join(format!("assets/{namespace}/{path}"))).ok()
|
||||
};
|
||||
let fragment = "#version 330\n#include <minecraft:dynamictransforms.glsl>\n\
|
||||
layout(location = 0) out vec4 fragColor;\n\
|
||||
void main() {\n fragColor = ColorModulator;\n}\n";
|
||||
let program = horizon_shaders::translate_core(&vertex, fragment, &[], &include, &GLUE)
|
||||
.expect("the sky shaders translate");
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let Some((device, queue)) =
|
||||
pollster::block_on(instance.request_adapter(&Default::default()))
|
||||
.ok()
|
||||
.and_then(|adapter| {
|
||||
pollster::block_on(adapter.request_device(&Default::default())).ok()
|
||||
})
|
||||
else {
|
||||
eprintln!("no GPU: skipping");
|
||||
return;
|
||||
};
|
||||
const SIZE: u32 = 64;
|
||||
let format = wgpu::TextureFormat::Rgba8Unorm;
|
||||
let sky = CoreSky::new(&device, format, Some(&program), None).expect("the sky pipeline");
|
||||
let projection = crate::gfx::world_projection(90.0, 1.0, 2048.0);
|
||||
let data = uniforms(
|
||||
[0; 48],
|
||||
projection,
|
||||
Mat4::IDENTITY,
|
||||
[0.25, 0.5, 1.0, 1.0],
|
||||
[0.0; 4],
|
||||
[1e6; 6],
|
||||
);
|
||||
queue.write_buffer(&sky.disc.as_ref().unwrap().uniform, 0, &data);
|
||||
let target = |format, usage| {
|
||||
device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: SIZE,
|
||||
height: SIZE,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | usage,
|
||||
view_formats: &[],
|
||||
})
|
||||
};
|
||||
let color = target(format, wgpu::TextureUsages::COPY_SRC);
|
||||
let depth = target(
|
||||
wgpu::TextureFormat::Depth32Float,
|
||||
wgpu::TextureUsages::empty(),
|
||||
);
|
||||
let read = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: u64::from(SIZE * SIZE * 4),
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
{
|
||||
let color_view = color.create_view(&Default::default());
|
||||
let depth_view = depth.create_view(&Default::default());
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: None,
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &color_view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &depth_view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(0.0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
sky.draw_disc(&mut pass);
|
||||
}
|
||||
encoder.copy_texture_to_buffer(
|
||||
color.as_image_copy(),
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &read,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(SIZE * 4),
|
||||
rows_per_image: Some(SIZE),
|
||||
},
|
||||
},
|
||||
color.size(),
|
||||
);
|
||||
queue.submit([encoder.finish()]);
|
||||
read.slice(..)
|
||||
.map_async(wgpu::MapMode::Read, |r| r.unwrap());
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
let pixels = read.slice(..).get_mapped_range().unwrap().to_vec();
|
||||
let at = |x: usize, y: usize| pixels[(y * 64 + x) * 4..(y * 64 + x) * 4 + 4].to_vec();
|
||||
// Row 0 is the top: the disc, in the sky colour; the bottom row is below the horizon.
|
||||
let top = at(32, 4);
|
||||
assert!(
|
||||
top.iter()
|
||||
.zip([64u8, 128, 255, 255])
|
||||
.all(|(&p, w)| p.abs_diff(w) <= 1),
|
||||
"{top:?}"
|
||||
);
|
||||
assert_eq!(at(32, 60), vec![0, 0, 0, 0]);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
//! second pass, draws the sections whose box was seen; the others' draws have
|
||||
//! an instance count of 0.
|
||||
//!
|
||||
//! The picture stays the one without culling: the boxes around the mesh's
|
||||
//! geometry (padded by a block) are drawn front faces
|
||||
//! The picture stays the one without culling: the boxes (grown by a block,
|
||||
//! the farthest a block model reaches out of its cell) are drawn front faces
|
||||
//! only, so a fragment is the box's nearest point on its pixel, and the
|
||||
//! fragment shader runs only where the depth test passes (forced early), so a
|
||||
//! section is left out only where all of its fragments would fail the depth
|
||||
@@ -45,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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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).
|
||||
@@ -367,7 +365,6 @@ impl Flat {
|
||||
terrain_layout: &wgpu::BindGroupLayout,
|
||||
page_vertices: u64,
|
||||
min_sections: usize,
|
||||
indirect_execution: bool,
|
||||
pipeline: impl Fn(
|
||||
&wgpu::PipelineLayout,
|
||||
&str,
|
||||
@@ -492,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),
|
||||
@@ -654,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 {
|
||||
@@ -763,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 {
|
||||
|
||||
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,14 +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"),
|
||||
include_str!("../terrain.wgsl").to_owned() + include_str!("../visbuffer.wgsl"),
|
||||
),
|
||||
(
|
||||
"improved transparency world border",
|
||||
|
||||
@@ -50,10 +50,8 @@ impl Pending {
|
||||
}
|
||||
}
|
||||
|
||||
/// Small, its meshes boxed: `draw_frame` walks every tile each frame (thousands at long far
|
||||
/// distances), and that walk is bound by the memory it reads.
|
||||
pub(super) struct Tile {
|
||||
pub mesh: Box<SectionGpu>,
|
||||
pub mesh: SectionGpu,
|
||||
bounds: Bounds,
|
||||
/// When it began to fade in; none while the rest of its job uploads.
|
||||
shown: Option<Instant>,
|
||||
@@ -64,7 +62,7 @@ pub(super) struct Tile {
|
||||
/// Its mesh before near chunks took some of its columns, drawn in its
|
||||
/// place until they have faded in (from the job's end, the instant), so
|
||||
/// no hole shows through them.
|
||||
previous: Option<Box<(SectionGpu, Option<Instant>)>>,
|
||||
previous: Option<(SectionGpu, Option<Instant>)>,
|
||||
/// Its slot in occlusion culling's results (`cull::TILE`), kept while
|
||||
/// meshes of the same tile replace one another.
|
||||
pub cull_slot: u32,
|
||||
@@ -157,12 +155,10 @@ pub(super) struct Visible {
|
||||
impl Tile {
|
||||
/// The mesh drawn this frame: the previous one while it stands in.
|
||||
pub fn drawn(&self) -> &SectionGpu {
|
||||
self.previous
|
||||
.as_deref()
|
||||
.map_or(&self.mesh, |(mesh, _)| mesh)
|
||||
self.previous.as_ref().map_or(&self.mesh, |(mesh, _)| mesh)
|
||||
}
|
||||
pub fn drawn_mut(&mut self) -> &mut SectionGpu {
|
||||
match self.previous.as_deref_mut() {
|
||||
match &mut self.previous {
|
||||
Some((mesh, _)) => mesh,
|
||||
None => &mut self.mesh,
|
||||
}
|
||||
@@ -181,7 +177,7 @@ impl Tile {
|
||||
}
|
||||
/// Both meshes it holds in the arena: its own and the stand-in, if any.
|
||||
pub fn meshes_mut(&mut self) -> impl Iterator<Item = &mut SectionGpu> {
|
||||
std::iter::once(&mut *self.mesh).chain(self.previous.as_deref_mut().map(|(mesh, _)| mesh))
|
||||
std::iter::once(&mut self.mesh).chain(self.previous.as_mut().map(|(mesh, _)| mesh))
|
||||
}
|
||||
pub fn visible(
|
||||
&self,
|
||||
@@ -218,12 +214,12 @@ impl Renderer {
|
||||
let (shown, previous) = match self.lods.remove(&key) {
|
||||
Some(old) if old.dying.is_none() => match old.previous {
|
||||
Some(previous) => {
|
||||
self.release_mesh(*old.mesh);
|
||||
self.release_mesh(old.mesh);
|
||||
(old.shown, Some(previous))
|
||||
}
|
||||
None if keep => (old.shown, Some(Box::new((*old.mesh, None)))),
|
||||
None if keep => (old.shown, Some((old.mesh, None))),
|
||||
None => {
|
||||
self.release_mesh(*old.mesh);
|
||||
self.release_mesh(old.mesh);
|
||||
(old.shown, None)
|
||||
}
|
||||
},
|
||||
@@ -244,7 +240,7 @@ impl Renderer {
|
||||
self.lods.insert(
|
||||
key,
|
||||
Tile {
|
||||
mesh: Box::new(mesh),
|
||||
mesh,
|
||||
bounds,
|
||||
shown,
|
||||
dying: None,
|
||||
@@ -255,7 +251,7 @@ impl Renderer {
|
||||
);
|
||||
}
|
||||
None => {
|
||||
if let Some((previous, _)) = previous.map(|p| *p) {
|
||||
if let Some((previous, _)) = previous {
|
||||
self.release_mesh(previous);
|
||||
}
|
||||
self.lod_cull_free.extend(old_slot);
|
||||
@@ -264,8 +260,8 @@ impl Renderer {
|
||||
}
|
||||
|
||||
fn release_tile(&mut self, tile: Tile) {
|
||||
self.release_mesh(*tile.mesh);
|
||||
if let Some((previous, _)) = tile.previous.map(|p| *p) {
|
||||
self.release_mesh(tile.mesh);
|
||||
if let Some((previous, _)) = tile.previous {
|
||||
self.release_mesh(previous);
|
||||
}
|
||||
}
|
||||
@@ -341,10 +337,8 @@ impl Renderer {
|
||||
let stood_in: Vec<_> = self
|
||||
.lods
|
||||
.values_mut()
|
||||
.filter_map(|tile| match tile.previous.as_deref() {
|
||||
Some((_, Some(since))) if start - *since >= FADE => {
|
||||
tile.previous.take().map(|p| *p)
|
||||
}
|
||||
.filter_map(|tile| match tile.previous {
|
||||
Some((_, Some(since))) if start - since >= FADE => tile.previous.take(),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
@@ -446,7 +440,7 @@ impl Renderer {
|
||||
if tile.shown.is_none() {
|
||||
tile.shown = Some(if over_replaced(key) { start } else { whole });
|
||||
}
|
||||
if let Some((_, since)) = tile.previous.as_deref_mut() {
|
||||
if let Some((_, since)) = &mut tile.previous {
|
||||
since.get_or_insert(start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,23 +49,9 @@ struct HorizonMaterialOut {
|
||||
@location(14) @interpolate(flat) normal: vec3<f32>,
|
||||
}
|
||||
fn horizon_material_vertex(position: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f32>,
|
||||
light: vec2<u32>, origin: vec4<f32>) -> HorizonMaterialOut {
|
||||
let pos = horizon_material_displace(position + origin.xyz, uv, layer, color, light);
|
||||
var out = horizon_material_out(pos, uv, layer, color, vec2<f32>(light));
|
||||
// The section's fade-in (`RenderSection.getVisibility`), as how far it is from whole, in the
|
||||
// high 16 bits of `hdr_code`: no varying of its own, and 0 (whole) where nothing fades. It is
|
||||
// the same for the whole section, so the flat varying loses nothing.
|
||||
let instance = horizon_terrain_instance_visibility(origin.w);
|
||||
out.hdr_code |= u32(round((1.0 - instance) * 65535.0)) << 16u;
|
||||
return out;
|
||||
}
|
||||
// terrain.wgsl's fade-in (terrain.vsh's chunkVisibility): whole within 16 blocks, the section's
|
||||
// own visibility from 32 out, toward the fog colour, alpha kept. The distance blend runs here on
|
||||
// the interpolated distance, as vanilla's interpolated `visibility`, not once per triangle.
|
||||
fn horizon_material_faded(in: HorizonMaterialOut, color: vec4<f32>) -> vec4<f32> {
|
||||
let instance = 1.0 - f32(in.hdr_code >> 16u) / 65535.0;
|
||||
let visibility = mix(1.0, instance, clamp((in.spherical - 16.0) / 16.0, 0.0, 1.0));
|
||||
return vec4(mix(horizon_terrain_globals.fog_color.rgb, color.rgb, visibility), color.a);
|
||||
light: vec2<u32>, origin: vec3<f32>) -> HorizonMaterialOut {
|
||||
let pos = horizon_material_displace(position + origin, uv, layer, color, light);
|
||||
return horizon_material_out(pos, uv, layer, color, vec2<f32>(light));
|
||||
}
|
||||
// `light`: sky, block, 16 per level.
|
||||
fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f32>,
|
||||
@@ -108,7 +94,7 @@ fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f
|
||||
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec2<u32>,
|
||||
@location(5) origin: vec4<f32>) -> HorizonMaterialOut {
|
||||
return horizon_material_vertex(position, horizon_terrain_terrain_uv(uv, origin.w), layer,
|
||||
color, light, origin);
|
||||
color, light, origin.xyz);
|
||||
}
|
||||
@vertex fn horizon_material_vs_flat(
|
||||
@builtin(vertex_index) vertex: u32,
|
||||
@@ -117,7 +103,7 @@ fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f
|
||||
) -> HorizonMaterialOut {
|
||||
let origin = horizon_terrain_slot_origins[horizon_terrain_block_slots[vertex >> 8u]];
|
||||
return horizon_material_vertex(position, horizon_terrain_terrain_uv(uv, origin.w), layer,
|
||||
color, light, origin);
|
||||
color, light, origin.xyz);
|
||||
}
|
||||
// The prelude's `block_normal_map` and `block_specular_map`: the resource packs' LabPBR maps
|
||||
// in the pack's frame group (3), one array each, a blank 1x1 without them.
|
||||
@@ -195,11 +181,9 @@ struct HorizonMaterialSolid {
|
||||
// What `fs_solid` leaves in the frame's alpha for HDR output, from the sprite before the pack's
|
||||
// material: the pack decides the colour, Horizon still knows which pixels are light sources.
|
||||
fn horizon_material_mark(in: HorizonMaterialOut, texel: vec4<f32>) -> f32 {
|
||||
if horizon_terrain_HDR_WRITE_LIGHT { return 1.0; }
|
||||
let code = in.hdr_code & 15u;
|
||||
if code > 0u {
|
||||
let whole = code > 4u;
|
||||
let tier = select(code, code - 4u, whole);
|
||||
if in.hdr_code > 0u {
|
||||
let whole = in.hdr_code > 4u;
|
||||
let tier = select(in.hdr_code, in.hdr_code - 4u, whole);
|
||||
let l = in.layer & 0x7fffu;
|
||||
let t = textureLoad(horizon_terrain_emission_thresholds,
|
||||
vec2(i32(l % horizon_terrain_SPRITE_SPLIT), i32(l / horizon_terrain_SPRITE_SPLIT)), 0);
|
||||
@@ -218,7 +202,7 @@ fn horizon_material_mark(in: HorizonMaterialOut, texel: vec4<f32>) -> f32 {
|
||||
return horizon_material_solid(in);
|
||||
}
|
||||
fn horizon_material_solid(in: HorizonMaterialOut) -> HorizonMaterialSolid {
|
||||
let color = horizon_material_faded(in, horizon_material_color(in, false));
|
||||
let color = horizon_material_color(in, false);
|
||||
let texel = horizon_terrain_sprite(in.uv, in.layer & 0x7fffu);
|
||||
// Bit 15 of the layer marks opaque leaves (forceOpaque): no alpha cutout.
|
||||
if color.a < 0.5 && (in.layer & 0x8000u) == 0u { discard; }
|
||||
@@ -232,7 +216,7 @@ fn horizon_material_solid(in: HorizonMaterialOut) -> HorizonMaterialSolid {
|
||||
return out;
|
||||
}
|
||||
@fragment fn horizon_material_fs_translucent(in: HorizonMaterialOut) -> @location(0) vec4<f32> {
|
||||
let color = horizon_material_faded(in, horizon_material_color(in, true));
|
||||
let color = horizon_material_color(in, true);
|
||||
if color.a < 0.01 { discard; }
|
||||
return horizon_material_fogged(color, in.spherical, in.cylindrical);
|
||||
}
|
||||
@@ -310,8 +294,7 @@ pub struct NativeMaterial {
|
||||
/// every name it declares prefixed.
|
||||
fn vanilla_terrain() -> String {
|
||||
prefixed(
|
||||
&(include_str!("../hdr_light.wgsl").to_owned()
|
||||
+ section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)")),
|
||||
section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)"),
|
||||
PREFIX,
|
||||
)
|
||||
}
|
||||
@@ -433,10 +416,6 @@ impl NativeMaterial {
|
||||
];
|
||||
constants.extend(sprite_names.into_iter().zip(shape.sprites));
|
||||
constants.push(("horizon_material_fog", f64::from(u8::from(fog))));
|
||||
constants.push((
|
||||
"horizon_terrain_HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(shape.format == super::LINEAR_FRAME)),
|
||||
));
|
||||
constants.push((
|
||||
"horizon_displace_distance",
|
||||
displaced.map_or(0.0, |d| f64::from(d.distance)),
|
||||
@@ -585,39 +564,6 @@ impl NativeMaterial {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn coverage_test_shader(api: horizon_shader_api::Api) -> String {
|
||||
use horizon_shader_api::{MANIFEST, Pack, StageKind, check};
|
||||
let manifest = format!(
|
||||
"schema = 1\nid = \"coverage\"\nname = \"Coverage\"\nversion = \"1\"\napi = \"{api}\"\n[material]\nfile = \"material.wgsl\"\n[displacement]\n"
|
||||
);
|
||||
let material = "fn material(input: MaterialInput) -> Surface { return Surface(vec4<f32>(1.4, 0.3, 0.1, 0.7)); }\nfn displacement(input: DisplacementInput) -> f32 { return 0.0; }\n";
|
||||
let files = [(MANIFEST, manifest.as_str()), ("material.wgsl", material)]
|
||||
.map(|(n, text)| (n.to_owned(), text.as_bytes().to_vec()))
|
||||
.into();
|
||||
let report = check(&Pack::from_files(files).unwrap()).unwrap();
|
||||
let stage = report
|
||||
.stages
|
||||
.iter()
|
||||
.find(|s| s.kind == StageKind::Material)
|
||||
.unwrap();
|
||||
shader_source(&stage.source, &[], &[], (false, true), api)
|
||||
+ r#"
|
||||
override TEST_TIER: u32 = 0u;
|
||||
override TEST_OVERLAY: u32 = 0u;
|
||||
@vertex fn test_vertex(@builtin(vertex_index) i: u32) -> HorizonMaterialOut {
|
||||
var o: HorizonMaterialOut;
|
||||
o.clip = vec4(vec2(f32((i << 1u) & 2u), f32(i & 2u)) * 2.0 - 1.0, 0.0, 1.0);
|
||||
o.uv = vec2(0.5);
|
||||
o.tint = vec4(1.0);
|
||||
o.lightmap = vec3(1.0);
|
||||
o.hdr_code = TEST_TIER;
|
||||
o.base = vec3(1.0);
|
||||
return o;
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -636,25 +582,6 @@ mod tests {
|
||||
report.stages.remove(0)
|
||||
}
|
||||
|
||||
/// The fade-in packed above the HDR code: a section a quarter visible (`section_instance`
|
||||
/// with step 1: mantissa `1 - visibility` = 0.75) keeps its visibility through the code, and
|
||||
/// a fragment 24 blocks away, half-way into the 16-block blend, is 1 - (1 - 0.25) / 2 = 0.625
|
||||
/// visible; the code's low nibble survives.
|
||||
#[test]
|
||||
fn the_fade_in_rides_the_hdr_code() {
|
||||
let w = f32::from_bits(1.0f32.to_bits() | (0.75 * 8_388_608.0) as u32);
|
||||
// `horizon_material_vertex`.
|
||||
let instance = 1.0 - (w.to_bits() & 0x7fffff) as f32 / 8_388_608.0;
|
||||
let code = 7u32 | (((1.0 - instance) * 65535.0).round() as u32) << 16;
|
||||
// `horizon_material_faded` and `horizon_material_mark`.
|
||||
let unpacked = 1.0 - (code >> 16) as f32 / 65535.0;
|
||||
assert!((unpacked - 0.25).abs() < 1.0 / 65535.0);
|
||||
let blend = ((24.0f32 - 16.0) / 16.0).clamp(0.0, 1.0);
|
||||
let visibility = 1.0 + (unpacked - 1.0) * blend;
|
||||
assert!((visibility - 0.625).abs() < 1.0 / 65535.0);
|
||||
assert_eq!(code & 15, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_terrain_takes_a_prefix_and_validates_with_a_pack() {
|
||||
let terrain = vanilla_terrain();
|
||||
|
||||
@@ -90,27 +90,6 @@ impl NativePack {
|
||||
report: Report,
|
||||
constants: &[(String, f64)],
|
||||
lights: super::block_light::LightTable,
|
||||
) -> Result<(Self, Vec<String>), String> {
|
||||
// A backend may refuse a layout or pipeline the pack asks for (a stage's bindings
|
||||
// past its sampled-texture limit, say, as OpenGL's 16 was before #10095): the pack
|
||||
// is refused with that error instead of a validation panic in the renderer.
|
||||
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let built = Self::build(device, queue, terrain, report, constants, lights);
|
||||
let refused = pollster::block_on(scope.pop());
|
||||
match (built, refused) {
|
||||
(Ok(built), None) => Ok(built),
|
||||
(Ok(_), Some(e)) => Err(e.to_string()),
|
||||
(Err(e), _) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn build(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
terrain: &TerrainShape<'_>,
|
||||
report: Report,
|
||||
constants: &[(String, f64)],
|
||||
lights: super::block_light::LightTable,
|
||||
) -> Result<(Self, Vec<String>), String> {
|
||||
let (globals, format) = (terrain.globals, terrain.format);
|
||||
let textures = super::native_post::pack_textures(device, queue, &report);
|
||||
@@ -299,90 +278,9 @@ impl NativePack {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{NativePack, TerrainShape, chosen_shadow};
|
||||
use super::chosen_shadow;
|
||||
use horizon_shader_api::manifest::{OptionKind, PackOption, Shadow};
|
||||
|
||||
/// A device with OpenGL's minimum 16 sampled textures per stage refuses a pack whose
|
||||
/// sky adds the terrain's 10 to the frame's 9: that is the pack's error to report, not
|
||||
/// a validation panic (#10095).
|
||||
#[test]
|
||||
fn a_pack_the_backend_refuses_is_an_error_not_a_panic() {
|
||||
use horizon_shader_api::{MANIFEST, Pack, check};
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
|
||||
return;
|
||||
};
|
||||
let mut limits = adapter.limits();
|
||||
limits.max_sampled_textures_per_shader_stage = 16;
|
||||
let Ok((device, queue)) =
|
||||
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: limits,
|
||||
..Default::default()
|
||||
}))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// The terrain's group 0: its uniform and the 10 sampled textures (_terrain.wgsl_'s
|
||||
// 8 sprite arrays, the lightmap and the emission thresholds).
|
||||
let entry = |binding, ty| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty,
|
||||
count: None,
|
||||
};
|
||||
let mut entries = vec![entry(
|
||||
0,
|
||||
wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
)];
|
||||
entries.extend((1..=10).map(|binding| {
|
||||
entry(
|
||||
binding,
|
||||
wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
)
|
||||
}));
|
||||
let globals = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: None,
|
||||
entries: &entries,
|
||||
});
|
||||
let empty = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: None,
|
||||
entries: &[],
|
||||
});
|
||||
let terrain = TerrainShape {
|
||||
globals: &globals,
|
||||
flat: None,
|
||||
sprites: [1.0, 2048.0],
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
sprite_tags: &[],
|
||||
sprite_materials: &[],
|
||||
entities: &empty,
|
||||
weather_groups: [&empty, &empty],
|
||||
pbr: None,
|
||||
};
|
||||
let manifest = "schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"0.1\"\n\
|
||||
[sky]\nfile = \"sky.wgsl\"\n";
|
||||
let files = [
|
||||
(MANIFEST, manifest),
|
||||
(
|
||||
"sky.wgsl",
|
||||
"fn sky(input: SkyInput) -> vec4<f32> { return input.color; }\n",
|
||||
),
|
||||
]
|
||||
.map(|(n, t)| (n.to_owned(), t.as_bytes().to_vec()))
|
||||
.into();
|
||||
let report = check(&Pack::from_files(files).unwrap()).unwrap();
|
||||
let built = NativePack::new(&device, &queue, &terrain, report, &[], Default::default());
|
||||
assert!(built.is_err(), "the pack should be refused");
|
||||
}
|
||||
|
||||
fn option(id: &str, kind: OptionKind) -> PackOption {
|
||||
PackOption {
|
||||
id: id.into(),
|
||||
|
||||
@@ -73,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>,
|
||||
|
||||
@@ -512,8 +512,7 @@ fn shader_source(
|
||||
format!(
|
||||
"{head}// Horizon's terrain (terrain.wgsl)\n{}{solid}{}{displace}{ENTRIES}",
|
||||
prefixed(
|
||||
&(include_str!("../hdr_light.wgsl").to_owned()
|
||||
+ section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)")),
|
||||
section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)"),
|
||||
PREFIX
|
||||
),
|
||||
super::native_post::line_helpers(api)
|
||||
|
||||
@@ -30,7 +30,6 @@ pub struct NativeSky {
|
||||
/// terrain.wgsl's sky with every name it declares (and `globals`) prefixed.
|
||||
fn vanilla_sky() -> String {
|
||||
let text = [
|
||||
include_str!("../hdr_light.wgsl"),
|
||||
section(TERRAIN, "struct Globals {", "@group(0) @binding(1)"),
|
||||
section(TERRAIN, "fn linear_fog(", "fn apply_fog("),
|
||||
section(TERRAIN, "struct SkyOut {", "// rendertype_lines"),
|
||||
|
||||
@@ -192,7 +192,7 @@ impl Oit {
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Horizon improved transparency"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
(super::terrain_shader()
|
||||
(include_str!("../terrain.wgsl").to_owned()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_world.wgsl"))
|
||||
.into(),
|
||||
@@ -253,14 +253,7 @@ impl Oit {
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
let constants = [
|
||||
("SPRITE_ARRAYS", sprites[0]),
|
||||
("SPRITE_SPLIT", sprites[1]),
|
||||
(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
),
|
||||
];
|
||||
let constants = [("SPRITE_ARRAYS", sprites[0]), ("SPRITE_SPLIT", sprites[1])];
|
||||
let add = wgpu::BlendOperation::Add;
|
||||
let depth = |write| {
|
||||
Some(wgpu::DepthStencilState {
|
||||
@@ -471,10 +464,7 @@ impl Oit {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &cloud_shader,
|
||||
entry_point: Some(fragment),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &constants[2..],
|
||||
..Default::default()
|
||||
},
|
||||
compilation_options: Default::default(),
|
||||
targets: &stage_targets(stage),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
@@ -498,14 +488,7 @@ impl Oit {
|
||||
Stage::Accumulate,
|
||||
),
|
||||
];
|
||||
let features = Features::new(
|
||||
device,
|
||||
&layouts,
|
||||
features[0],
|
||||
features[1],
|
||||
features[2],
|
||||
format,
|
||||
);
|
||||
let features = Features::new(device, &layouts, features[0], features[1], features[2]);
|
||||
Self {
|
||||
uniform,
|
||||
layouts,
|
||||
@@ -929,7 +912,7 @@ mod tests {
|
||||
#[test]
|
||||
fn the_terrain_shader_validates() {
|
||||
validate(
|
||||
&(crate::gfx::terrain_shader()
|
||||
&(include_str!("../terrain.wgsl").to_owned()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_world.wgsl")),
|
||||
&[
|
||||
|
||||
@@ -55,7 +55,6 @@ impl Features {
|
||||
entity: &wgpu::BindGroupLayout,
|
||||
weather: &wgpu::BindGroupLayout,
|
||||
border: &wgpu::BindGroupLayout,
|
||||
format: wgpu::TextureFormat,
|
||||
) -> Self {
|
||||
let entity_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Horizon improved transparency features"),
|
||||
@@ -114,15 +113,6 @@ impl Features {
|
||||
bind_group_layouts: &[groups[0], groups[1], Some(&layouts[stage as usize])],
|
||||
..Default::default()
|
||||
});
|
||||
let fragment = if kind.label == "text" && stage == Stage::Accumulate {
|
||||
"fs_oit_text_accumulate".to_owned()
|
||||
} else {
|
||||
format!("fs_oit_{}_{suffix}", kind.name)
|
||||
};
|
||||
let constants = [(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
)];
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(&format!("OIT {} {suffix}", kind.label)),
|
||||
layout: Some(&layout),
|
||||
@@ -146,15 +136,8 @@ impl Features {
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module,
|
||||
entry_point: Some(&fragment),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: if kind.name == "border" {
|
||||
&[]
|
||||
} else {
|
||||
&constants
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
entry_point: Some(&format!("fs_oit_{}_{suffix}", kind.name)),
|
||||
compilation_options: Default::default(),
|
||||
targets: &stage_targets(stage),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
|
||||
@@ -728,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)",
|
||||
@@ -757,7 +750,6 @@ impl PackAtlas {
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,6 @@ fn terrain_fade_reaches_pack_pixels_through_real_instances() {
|
||||
flat,
|
||||
geometry,
|
||||
origin,
|
||||
0x80ffffff,
|
||||
);
|
||||
let fade = if requested { expected } else { 1.0 };
|
||||
let expected = [
|
||||
@@ -183,96 +182,6 @@ fn terrain_fade_reaches_pack_pixels_through_real_instances() {
|
||||
assert_eq!(count, 98);
|
||||
}
|
||||
|
||||
/// `horizonTier`: the HDR tier the mesher marks a light source's faces with (`mesher::emitting`),
|
||||
/// read by a pack's terrain program, flat or instanced.
|
||||
#[test]
|
||||
fn terrain_tier_reaches_pack_pixels() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = match pollster::block_on(instance.request_adapter(&Default::default())) {
|
||||
Ok(adapter) => adapter,
|
||||
Err(error) => {
|
||||
eprintln!("SKIP terrain tier GPU control: no adapter: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&Default::default())).unwrap();
|
||||
let array = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: 2,
|
||||
height: 2,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
let atlas = PackAtlas::new(&device, &mut encoder, &[array], 1, 1, 2).unwrap();
|
||||
queue.submit([encoder.finish()]);
|
||||
let folder = ProbePack(
|
||||
std::env::temp_dir().join(format!("horizon-terrain-tier-{}", std::process::id())),
|
||||
);
|
||||
std::fs::create_dir_all(folder.0.join("shaders")).unwrap();
|
||||
std::fs::write(
|
||||
folder.0.join("shaders/gbuffers_terrain.vsh"),
|
||||
"#version 330\nin float horizonTier;\nout vec4 observation;\n\
|
||||
void main() {\nobservation = vec4(horizonTier, 0.0, 0.0, 0.0);\n\
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n}",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
folder.0.join("shaders/gbuffers_terrain.fsh"),
|
||||
"#version 330\nin vec4 observation;\nlayout(location = 0) out uvec4 pixel;\n\
|
||||
void main() { pixel = floatBitsToUint(observation); }",
|
||||
)
|
||||
.unwrap();
|
||||
let pack = horizon_shaders::Pack::open(&folder.0).unwrap();
|
||||
let mut macros = horizon_shaders::Macros::standard();
|
||||
let (properties, _) = horizon_shaders::read_properties(&pack, &mut macros);
|
||||
use crate::mesher::{BRIGHT, LOW, VERY_BRIGHT, WHOLE, emitting};
|
||||
// No light, low, very bright, and bright all over (4 more).
|
||||
let cases = [
|
||||
(0, 0.0),
|
||||
(LOW, 1.0),
|
||||
(VERY_BRIGHT, 4.0),
|
||||
(BRIGHT | WHOLE, 7.0),
|
||||
];
|
||||
for flat in [false, true] {
|
||||
let post = horizon_shaders::load_post(
|
||||
&pack,
|
||||
"",
|
||||
&properties,
|
||||
¯os,
|
||||
&horizon_shaders::GameInputs {
|
||||
terrain: Some(atlas.terrain_inputs(flat)),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let program = post
|
||||
.terrain
|
||||
.as_ref()
|
||||
.unwrap_or_else(|| panic!("{:?}", post.notes));
|
||||
for (tier, expected) in cases {
|
||||
let origin = section_instance([0.0; 3], 0, 1.0);
|
||||
let pixel = sample(
|
||||
&device,
|
||||
&queue,
|
||||
&atlas,
|
||||
program.program.as_ref().unwrap(),
|
||||
flat,
|
||||
false,
|
||||
origin,
|
||||
emitting(0xffff_ffff, tier),
|
||||
);
|
||||
assert_eq!(pixel[0], expected, "tier {tier} flat={flat}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sample(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
@@ -281,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 {
|
||||
@@ -301,7 +209,7 @@ 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),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1635,44 +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);
|
||||
}
|
||||
}
|
||||
@@ -82,9 +82,6 @@ pub(super) fn format(f: Format, packed_float: bool) -> wgpu::TextureFormat {
|
||||
_ => rgba,
|
||||
};
|
||||
match (f.kind, f.bits) {
|
||||
(Kind::Unorm, 10) => T::Rgb10a2Unorm,
|
||||
(Kind::Uint, 10) => T::Rgb10a2Uint,
|
||||
(Kind::Snorm, 16) => pick(T::R16Snorm, T::Rg16Snorm, T::Rgba16Snorm),
|
||||
(Kind::Int, 8) => pick(T::R8Sint, T::Rg8Sint, T::Rgba8Sint),
|
||||
(Kind::Int, 16) => pick(T::R16Sint, T::Rg16Sint, T::Rgba16Sint),
|
||||
(Kind::Int, _) => pick(T::R32Sint, T::Rg32Sint, T::Rgba32Sint),
|
||||
|
||||
@@ -52,7 +52,6 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
//! Paged maps (`paged_maps.rs`) through the translator's `hz_pagedTexture` and the pass's
|
||||
//! binding: a pack reads the map's texels at the colour's coordinates.
|
||||
use super::*;
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
/// The atlas the fixture's coordinates are in, and the one mapped texture's place.
|
||||
const ATLAS: (u32, u32) = (64, 32);
|
||||
const SPRITE: [u32; 4] = [20, 4, 20, 20];
|
||||
|
||||
/// The fixture's normal map texel at texel (x, y) of its texture.
|
||||
fn texel(x: u32, y: u32) -> [u8; 4] {
|
||||
[x as u8 * 10, y as u8 * 10, 7, 255]
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a host GPU; missing adapter is a failure"]
|
||||
fn a_pack_reads_paged_maps_at_the_colours_coordinates() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: adapter.limits(),
|
||||
..Default::default()
|
||||
}))
|
||||
.unwrap();
|
||||
let root = std::env::temp_dir().join(format!("horizon-paged-pack-{}", std::process::id()));
|
||||
std::fs::create_dir_all(root.join("shaders")).unwrap();
|
||||
std::fs::write(
|
||||
root.join("shaders/gbuffers_terrain.vsh"),
|
||||
"#version 120\nvoid main(){gl_Position=gl_Vertex;}",
|
||||
)
|
||||
.unwrap();
|
||||
// Each pixel reads one atlas texel's centre: in the texture (on four pages), beside it,
|
||||
// on a page without maps, and the specular map, which no texture has.
|
||||
std::fs::write(
|
||||
root.join("shaders/gbuffers_terrain.fsh"),
|
||||
r#"#version 120
|
||||
uniform sampler2D normals;
|
||||
uniform sampler2D specular;
|
||||
vec2 at(float x, float y) { return (vec2(x, y) + 0.5) / vec2(64.0, 32.0); }
|
||||
void main() {
|
||||
int x = int(floor(gl_FragCoord.x));
|
||||
if (x == 0) gl_FragData[0] = texture2D(normals, at(20.0, 4.0));
|
||||
else if (x == 1) gl_FragData[0] = texture2D(normals, at(39.0, 23.0));
|
||||
else if (x == 2) gl_FragData[0] = texture2D(normals, at(31.0, 15.0), 2.0);
|
||||
else if (x == 3) gl_FragData[0] = texture2DLod(normals, at(32.0, 16.0), 3.0);
|
||||
else if (x == 4) gl_FragData[0] = texture2D(normals, at(19.0, 4.0));
|
||||
else if (x == 5) gl_FragData[0] = texture2D(normals, at(50.0, 30.0));
|
||||
else if (x == 6) gl_FragData[0] = texture2D(specular, at(25.0, 10.0));
|
||||
else gl_FragData[0] = texture2D(normals, at(33.0, 5.0));
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let [x0, y0, w, h] = SPRITE;
|
||||
let normal: Vec<u8> = (0..w * h).flat_map(|i| texel(i % w, i / w)).collect();
|
||||
let [normals, specular] = crate::paged_maps::build(
|
||||
ATLAS,
|
||||
&[crate::paged_maps::SpriteMaps {
|
||||
rect: SPRITE,
|
||||
maps: [Some(normal), None],
|
||||
}],
|
||||
16384,
|
||||
);
|
||||
assert!(specular.is_none());
|
||||
let normals = normals.unwrap();
|
||||
let map = device
|
||||
.create_texture_with_data(
|
||||
&queue,
|
||||
&wgpu::TextureDescriptor {
|
||||
label: Some("Fixture paged normal map"),
|
||||
size: wgpu::Extent3d {
|
||||
width: normals.width,
|
||||
height: normals.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
},
|
||||
wgpu::util::TextureDataOrder::LayerMajor,
|
||||
&normals.pixels,
|
||||
)
|
||||
.create_view(&Default::default());
|
||||
let pack = horizon_shaders::Pack::open(&root).unwrap();
|
||||
let macros = horizon_shaders::Macros::standard();
|
||||
let properties = horizon_shaders::Properties::parse("alphaTest.gbuffers_terrain=off", ¯os);
|
||||
let inputs = horizon_shaders::GameInputs {
|
||||
terrain: Some(horizon_shaders::GbuffersInputs {
|
||||
body: "vec2 p = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); hz_Vertex=vec4(p*2.0-1.0,0.0,1.0);".into(),
|
||||
paged_maps: true,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let post = horizon_shaders::load_post(&pack, "", &properties, ¯os, &inputs);
|
||||
assert!(post.terrain.is_some(), "{:?}", post.notes);
|
||||
let world = World {
|
||||
sprite_rects: &[],
|
||||
atlas: None,
|
||||
material_maps: None,
|
||||
lightmap: map.clone(),
|
||||
terrain: Some(Geometry {
|
||||
buffers: &[],
|
||||
table: None,
|
||||
depth_write: true,
|
||||
atlas: None,
|
||||
material_maps: false,
|
||||
ids: None,
|
||||
cull: false,
|
||||
}),
|
||||
block_ids: &[],
|
||||
celestial: None,
|
||||
gui_size: [1, 1],
|
||||
clouds: None,
|
||||
weather: false,
|
||||
hand: false,
|
||||
};
|
||||
let mut post = PackPost::new(
|
||||
&device,
|
||||
&queue,
|
||||
&adapter,
|
||||
post,
|
||||
wgpu::TextureFormat::Rgba8Unorm,
|
||||
world,
|
||||
&Default::default(),
|
||||
)
|
||||
.expect("the pack runs");
|
||||
let mut pass = post
|
||||
.terrain
|
||||
.take()
|
||||
.unwrap_or_else(|| panic!("terrain pipeline must compile: {:?}", post.notes));
|
||||
// As the entity passes are (`paged`), with the maps the renderer hands over.
|
||||
pass.paged = true;
|
||||
post.set_paged_maps([Some(map), None]);
|
||||
post.copied |= 1;
|
||||
post.resize(&device, [8, 1]);
|
||||
let read = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: 256,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
{
|
||||
let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Initialize fixture depth"),
|
||||
color_attachments: &[],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &post.targets.as_ref().unwrap().depth,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
let group = post.bind(&device, &queue, &mut encoder, &mut pass, [8, 1]);
|
||||
{
|
||||
let mut render = post.begin(&mut encoder, &pass);
|
||||
render.set_pipeline(pass.render());
|
||||
render.set_bind_group(0, &group, &[]);
|
||||
if pass.draw {
|
||||
post.draws.bind(&mut render, "TERRAIN_SOLID");
|
||||
}
|
||||
render.draw(0..3, 0..1);
|
||||
}
|
||||
let output = post.targets.as_ref().unwrap().color[0].as_ref().unwrap()[0].texture();
|
||||
encoder.copy_texture_to_buffer(
|
||||
output.as_image_copy(),
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &read,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
bytes_per_row: Some(256),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: 8,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
queue.submit([encoder.finish()]);
|
||||
read.slice(..).map_async(wgpu::MapMode::Read, |_| {});
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
let data = read.slice(..).get_mapped_range().unwrap();
|
||||
let local = |x: u32, y: u32| texel(x - x0, y - y0);
|
||||
let expected = [
|
||||
local(20, 4),
|
||||
local(39, 23),
|
||||
local(31, 15),
|
||||
local(32, 16),
|
||||
crate::sprites::FLAT_NORMAL,
|
||||
crate::sprites::FLAT_NORMAL,
|
||||
crate::sprites::NO_SPECULAR,
|
||||
local(33, 5),
|
||||
];
|
||||
for (i, e) in expected.iter().enumerate() {
|
||||
assert_eq!(&data[i * 4..i * 4 + 4], e, "pixel {i}");
|
||||
}
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
@@ -61,9 +61,6 @@ mod shadow_faces_tests;
|
||||
#[cfg(test)]
|
||||
mod r11f_image_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod work_group_fold_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod forced_flip_tests;
|
||||
|
||||
@@ -433,9 +430,6 @@ struct Pass {
|
||||
/// What `texture` reads instead of the block atlas.
|
||||
atlas: Option<wgpu::TextureView>,
|
||||
material_maps: bool,
|
||||
/// Draws the shared atlas's geometry: `normals` and `specular` are its paged maps
|
||||
/// (`PackPost::set_paged_maps`), as its programs were translated to read them.
|
||||
paged: bool,
|
||||
/// Reads `mc_Entity`'s table (`PackPost::block_ids`).
|
||||
ids: bool,
|
||||
/// Bind groups by the sides read (bits 0–15), the buffers whose mip chain is current (bits
|
||||
@@ -680,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,
|
||||
@@ -2187,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(),
|
||||
@@ -2328,7 +2319,6 @@ fn build_compute(
|
||||
draw: false,
|
||||
atlas: None,
|
||||
material_maps: false,
|
||||
paged: false,
|
||||
ids: false,
|
||||
groups: HashMap::new(),
|
||||
portal_groups: HashMap::new(),
|
||||
@@ -2367,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.
|
||||
///
|
||||
@@ -2464,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;
|
||||
@@ -2803,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" {
|
||||
@@ -2822,15 +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))
|
||||
}
|
||||
})
|
||||
});
|
||||
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))
|
||||
@@ -2873,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| {
|
||||
@@ -2887,7 +2857,7 @@ impl PackPost {
|
||||
ids: None,
|
||||
cull: false,
|
||||
};
|
||||
paged(build(p, Some(&g)))
|
||||
build(p, Some(&g))
|
||||
});
|
||||
let weather = post
|
||||
.weather
|
||||
@@ -3326,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),
|
||||
@@ -3449,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(),
|
||||
@@ -3688,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()
|
||||
@@ -3794,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]);
|
||||
}
|
||||
@@ -6304,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;
|
||||
@@ -6377,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;
|
||||
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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);\
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
//! A `workGroups` over wgpu's 65535 per dimension runs, and its programs read the work group
|
||||
//! built-ins of the dispatch they declared. The programs are original.
|
||||
use super::destination_tests::Fixture;
|
||||
use super::*;
|
||||
use std::io::{Cursor, Write};
|
||||
|
||||
fn pack() -> Post {
|
||||
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
let mut add = |name: &str, source: &str| {
|
||||
zip.start_file(
|
||||
format!("shaders/{name}"),
|
||||
zip::write::SimpleFileOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
zip.write_all(source.as_bytes()).unwrap();
|
||||
};
|
||||
// 70000 x 1 x 2 groups; the last 64 of the second layer each paint one pixel, green when
|
||||
// the built-ins agree with that dispatch.
|
||||
add(
|
||||
"composite.csh",
|
||||
"#version 430\nlayout(local_size_x=1) in;\nconst ivec3 workGroups=ivec3(70000,1,2);\n\
|
||||
layout(rgba8) writeonly uniform image2D colorimg0;\n\
|
||||
void main() {\n uint first = 70000u - 64u;\n\
|
||||
if (gl_WorkGroupID.z != 1u || gl_WorkGroupID.x < first) return;\n\
|
||||
uint k = gl_WorkGroupID.x - first;\n\
|
||||
bool ok = gl_WorkGroupID.y == 0u && gl_NumWorkGroups == uvec3(70000u, 1u, 2u)\n\
|
||||
&& gl_GlobalInvocationID == gl_WorkGroupID;\n\
|
||||
imageStore(colorimg0, ivec2(k % 8u, k / 8u), ok ? vec4(0, 1, 0, 1) : vec4(1, 0, 0, 1));\n}\n",
|
||||
);
|
||||
add(
|
||||
"final.vsh",
|
||||
"#version 120\nvarying vec2 uv;\nvoid main() { uv=gl_MultiTexCoord0.xy; gl_Position=ftransform(); }\n",
|
||||
);
|
||||
add(
|
||||
"final.fsh",
|
||||
"#version 330 compatibility\nuniform sampler2D colortex0;\nin vec2 uv;\n\
|
||||
void main() { gl_FragColor = texelFetch(colortex0, ivec2(gl_FragCoord.xy), 0); }\n",
|
||||
);
|
||||
let pack =
|
||||
horizon_shaders::Pack::from_zip_bytes("original-fold", zip.finish().unwrap().into_inner())
|
||||
.unwrap();
|
||||
let mut macros = horizon_shaders::Macros::standard();
|
||||
let (properties, _) = horizon_shaders::read_properties(&pack, &mut macros);
|
||||
horizon_shaders::load_post(&pack, "", &properties, ¯os, &Default::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dispatch_over_the_limit_reads_its_declared_work_groups() {
|
||||
let Some(gpu) = Fixture::new() else { return };
|
||||
let post = pack();
|
||||
assert!(post.notes.is_empty(), "{:?}", post.notes);
|
||||
let mut post = gpu.build(post);
|
||||
let pixels = gpu.render(&mut post, [8, 8]);
|
||||
for (k, got) in pixels.iter().enumerate() {
|
||||
assert_eq!(*got, [0, 255, 0, 255], "pixel {k}; notes {:?}", post.notes);
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,6 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: true,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +131,6 @@ pub fn celestial_inputs() -> horizon_shaders::GbuffersInputs {
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
far_depth: true,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -355,25 +355,6 @@ pub const NAMES: &[&str] = &[
|
||||
"hasSkylight",
|
||||
"hasCeiling",
|
||||
"cloudTime",
|
||||
// Horizon's own: Horizon Extras' dynamic lights (`dynamic_light.rs`), nearest first: how
|
||||
// many, and each one's camera-relative position and light level (zeros past the count).
|
||||
"horizonDynamicLightCount",
|
||||
"horizonDynamicLight0",
|
||||
"horizonDynamicLight1",
|
||||
"horizonDynamicLight2",
|
||||
"horizonDynamicLight3",
|
||||
"horizonDynamicLight4",
|
||||
"horizonDynamicLight5",
|
||||
"horizonDynamicLight6",
|
||||
"horizonDynamicLight7",
|
||||
"horizonDynamicLight8",
|
||||
"horizonDynamicLight9",
|
||||
"horizonDynamicLight10",
|
||||
"horizonDynamicLight11",
|
||||
"horizonDynamicLight12",
|
||||
"horizonDynamicLight13",
|
||||
"horizonDynamicLight14",
|
||||
"horizonDynamicLight15",
|
||||
"isElytraFlying",
|
||||
"cloudHeight",
|
||||
];
|
||||
@@ -405,8 +386,6 @@ pub struct FrameUniforms {
|
||||
center_depth: Option<f32>,
|
||||
/// The pack's shadow settings (7.1).
|
||||
pub shadow: Shadow,
|
||||
/// The pack's smoothing half-lives in ticks (`horizon_shaders::HALF_LIVES`).
|
||||
pub half_lives: [Option<f32>; 4],
|
||||
}
|
||||
|
||||
impl Default for FrameUniforms {
|
||||
@@ -424,7 +403,6 @@ impl Default for FrameUniforms {
|
||||
center_depth_raw: None,
|
||||
center_depth: None,
|
||||
shadow: Shadow::default(),
|
||||
half_lives: [None; 4],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -737,7 +715,6 @@ impl FrameUniforms {
|
||||
*self = Self {
|
||||
atlas: self.atlas,
|
||||
shadow: std::mem::take(&mut self.shadow),
|
||||
half_lives: self.half_lives,
|
||||
..Self::default()
|
||||
};
|
||||
}
|
||||
@@ -802,13 +779,10 @@ impl FrameUniforms {
|
||||
// a tenth of a second a tick: Iris 1.11.7 smooths that slowly (measured frame by frame
|
||||
// with a probe pack: wetness 60 s rising, 20 s falling; eyeBrightnessSmooth 1 s;
|
||||
// centerDepthSmooth 0.1 s).
|
||||
// The pack's `wetnessHalflife`/`drynessHalflife` when it sets them (OptiFine's defaults
|
||||
// otherwise), as for `eyeBrightnessHalflife` and `centerDepthHalflife` below.
|
||||
let [wet, dry, eye_half, depth_half] = self.half_lives;
|
||||
let half_life = if inputs.rain > self.wetness {
|
||||
wet.unwrap_or(600.0)
|
||||
600.0
|
||||
} else {
|
||||
dry.unwrap_or(200.0)
|
||||
200.0
|
||||
};
|
||||
self.wetness = smooth(self.wetness, inputs.rain, half_life * SECONDS_A_TICK, dt);
|
||||
let eye = inputs.eye_light.map(|l| (l * 16) as f32);
|
||||
@@ -817,18 +791,13 @@ impl FrameUniforms {
|
||||
self.eye_smooth = eye;
|
||||
}
|
||||
for (smoothed, target) in self.eye_smooth.iter_mut().zip(eye) {
|
||||
*smoothed = smooth(
|
||||
*smoothed,
|
||||
target,
|
||||
eye_half.unwrap_or(10.0) * SECONDS_A_TICK,
|
||||
dt,
|
||||
);
|
||||
*smoothed = smooth(*smoothed, target, 10.0 * SECONDS_A_TICK, dt);
|
||||
}
|
||||
// `centerDepthHalflife`'s default, 1 tick (OptiFine's shaders.txt); the first value
|
||||
// read back is taken as it is.
|
||||
if let Some(raw) = self.center_depth_raw {
|
||||
self.center_depth = Some(match self.center_depth {
|
||||
Some(v) => smooth(v, raw, depth_half.unwrap_or(1.0) * SECONDS_A_TICK, dt),
|
||||
Some(v) => smooth(v, raw, SECONDS_A_TICK, dt),
|
||||
None => raw,
|
||||
});
|
||||
}
|
||||
@@ -960,12 +929,6 @@ impl FrameUniforms {
|
||||
"gbufferProjectionInverse" => m4(projection_inverse),
|
||||
"gbufferPreviousModelView" => m4(previous_view),
|
||||
"gbufferPreviousProjection" => m4(previous_projection),
|
||||
"horizonDynamicLightCount" => Value::i(&[frame.dynamic_lights.len() as i32]),
|
||||
n if dynamic_light_index(n).is_some() => Value::f(
|
||||
&(dynamic_light_index(n).and_then(|k| frame.dynamic_lights.get(k)))
|
||||
.copied()
|
||||
.unwrap_or([0.0; 4]),
|
||||
),
|
||||
"heldItemId" => Value::i(&[inputs.held_items[0]]),
|
||||
"heldItemId2" => Value::i(&[inputs.held_items[1]]),
|
||||
"heldBlockLightValue" => Value::i(&[inputs.held_light[0]]),
|
||||
@@ -1272,12 +1235,10 @@ mod tests {
|
||||
format!(r#"{{"attributes": {attributes}}}"#),
|
||||
)
|
||||
.unwrap();
|
||||
let environment = crate::environment::Environment::load_dimension(&root, name).unwrap();
|
||||
let environment =
|
||||
crate::environment::Environment::load_dimension(&root, name, false).unwrap();
|
||||
std::fs::remove_dir_all(&root).unwrap();
|
||||
environment.sample(&crate::environment::Sample {
|
||||
day_time: 6000,
|
||||
..Default::default()
|
||||
})
|
||||
environment.sample(6000, None, None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1824,29 +1785,6 @@ pub(super) mod initial_history_tests {
|
||||
assert_eq!(crate::clouds::drift(256 * 400 + 3, 0.25, 256), 3.25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_dynamic_lights_reach_the_pack_nearest_first() {
|
||||
let hud = HudBatch::new([800, 450]);
|
||||
let mut f = frame(&hud);
|
||||
let lights = [[1.0, 2.0, 3.0, 14.0], [-4.0, 0.5, 6.0, 7.0]];
|
||||
f.dynamic_lights = &lights;
|
||||
let mut u = FrameUniforms::default();
|
||||
u.update(&f, &PackInputs::default(), [800, 450]);
|
||||
let count = index("horizonDynamicLightCount").unwrap();
|
||||
assert_eq!(int(&u, count), 2);
|
||||
assert_eq!(float(&u, "horizonDynamicLight0")[..4], lights[0]);
|
||||
assert_eq!(float(&u, "horizonDynamicLight1")[..4], lights[1]);
|
||||
// Past the count, and the last of the sixteen: zeros.
|
||||
assert_eq!(float(&u, "horizonDynamicLight2")[..4], [0.0; 4]);
|
||||
assert_eq!(float(&u, "horizonDynamicLight15")[..4], [0.0; 4]);
|
||||
assert_eq!(index("horizonDynamicLight16"), None);
|
||||
// Off (no lights): none.
|
||||
f.dynamic_lights = &[];
|
||||
u.update(&f, &PackInputs::default(), [800, 450]);
|
||||
assert_eq!(int(&u, count), 0);
|
||||
assert_eq!(float(&u, "horizonDynamicLight0")[..4], [0.0; 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environmental_smoothing_starts_at_the_first_inputs_without_resetting_each_frame() {
|
||||
let hud = HudBatch::new([800, 450]);
|
||||
@@ -2232,33 +2170,22 @@ pub(super) mod initial_history_tests {
|
||||
let hud = HudBatch::new([800, 450]);
|
||||
let f = frame(&hud);
|
||||
let mut u = FrameUniforms::default();
|
||||
// A scripted clock: the next frame comes exactly `seconds` after the last, however long
|
||||
// the test itself takes (a wall-clock gap of a millisecond moved centerDepthSmooth by
|
||||
// more than the tolerance on a busy CI runner).
|
||||
let mut counter = 1;
|
||||
let mut elapsed = 0.0;
|
||||
let mut after = |input: &mut PackInputs, seconds: f32| {
|
||||
counter += 1;
|
||||
elapsed += seconds;
|
||||
input.frame_timing = Some(FrameTiming {
|
||||
counter,
|
||||
elapsed,
|
||||
delta: seconds,
|
||||
});
|
||||
// The next frame comes `seconds` after the last (and a few microseconds of the test).
|
||||
let after = |u: &mut FrameUniforms, seconds: f32| {
|
||||
u.last = Some(std::time::Instant::now() - std::time::Duration::from_secs_f32(seconds));
|
||||
};
|
||||
let mut input = PackInputs {
|
||||
eye_light: [0, 15],
|
||||
frame_timing: Some(FrameTiming::nth(1, 0.0)),
|
||||
..Default::default()
|
||||
};
|
||||
u.center_depth_raw = Some(1.0);
|
||||
u.update(&f, &input, [800, 450]);
|
||||
input.eye_light = [0, 0];
|
||||
u.center_depth_raw = Some(0.0);
|
||||
after(&mut input, 0.1);
|
||||
after(&mut u, 0.1);
|
||||
u.update(&f, &input, [800, 450]);
|
||||
assert!((float(&u, "centerDepthSmooth")[0] - 0.5).abs() < 1e-3);
|
||||
after(&mut input, 0.6);
|
||||
after(&mut u, 0.6);
|
||||
u.update(&f, &input, [800, 450]);
|
||||
// 240 falling toward 0 for 0.7 s: 147.8, read as 147.
|
||||
match u.values[index("eyeBrightnessSmooth").unwrap()] {
|
||||
@@ -2266,46 +2193,11 @@ pub(super) mod initial_history_tests {
|
||||
_ => panic!("integer uniform expected"),
|
||||
}
|
||||
input.rain = 1.0;
|
||||
after(&mut input, 60.0);
|
||||
after(&mut u, 60.0);
|
||||
u.update(&f, &input, [800, 450]);
|
||||
assert!((float(&u, "wetness")[0] - 0.5).abs() < 1e-3);
|
||||
input.rain = 0.0;
|
||||
after(&mut input, 20.0);
|
||||
u.update(&f, &input, [800, 450]);
|
||||
assert!((float(&u, "wetness")[0] - 0.25).abs() < 1e-3);
|
||||
}
|
||||
|
||||
/// A pack's `const float wetnessHalflife = 30.0;` (and its dryness) replace the defaults:
|
||||
/// wetness reaches half the rain in 3 s instead of 60 s.
|
||||
#[test]
|
||||
fn a_pack_s_half_lives_replace_the_defaults() {
|
||||
let hud = HudBatch::new([800, 450]);
|
||||
let f = frame(&hud);
|
||||
let mut u = FrameUniforms {
|
||||
half_lives: [Some(30.0), Some(10.0), None, None],
|
||||
..Default::default()
|
||||
};
|
||||
// A scripted clock, as in `smoothed_uniforms_take_iris_half_lives`: the wall clock's gap
|
||||
// between two updates is not exact on a busy runner.
|
||||
let mut input = PackInputs {
|
||||
frame_timing: Some(FrameTiming::nth(1, 0.0)),
|
||||
..Default::default()
|
||||
};
|
||||
u.update(&f, &input, [800, 450]);
|
||||
input.rain = 1.0;
|
||||
input.frame_timing = Some(FrameTiming {
|
||||
counter: 2,
|
||||
elapsed: 3.0,
|
||||
delta: 3.0,
|
||||
});
|
||||
u.update(&f, &input, [800, 450]);
|
||||
assert!((float(&u, "wetness")[0] - 0.5).abs() < 1e-3);
|
||||
input.rain = 0.0;
|
||||
input.frame_timing = Some(FrameTiming {
|
||||
counter: 3,
|
||||
elapsed: 4.0,
|
||||
delta: 1.0,
|
||||
});
|
||||
after(&mut u, 20.0);
|
||||
u.update(&f, &input, [800, 450]);
|
||||
assert!((float(&u, "wetness")[0] - 0.25).abs() < 1e-3);
|
||||
}
|
||||
@@ -2642,12 +2534,6 @@ pub(super) mod initial_history_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// `horizonDynamicLight<k>`'s `k`, for the lights `Frame::dynamic_lights` holds.
|
||||
fn dynamic_light_index(name: &str) -> Option<usize> {
|
||||
let k: usize = name.strip_prefix("horizonDynamicLight")?.parse().ok()?;
|
||||
(k < crate::dynamic_light::MAX).then_some(k)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "pack_uniforms/ftransform_test.rs"]
|
||||
mod ftransform_test;
|
||||
@@ -46,7 +46,6 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
}),
|
||||
item_id_declared: false,
|
||||
far_depth: false,
|
||||
paged_maps: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -274,13 +274,7 @@ impl Panorama {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_panorama"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
)],
|
||||
..Default::default()
|
||||
},
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: None,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// CubeMap's position-only cube and panorama.vsh/fsh: sample its untransformed direction.
|
||||
override HDR_WRITE_LIGHT: bool = false;
|
||||
struct Projection { view_proj: mat4x4<f32> }
|
||||
@group(0) @binding(0) var<uniform> projection: Projection;
|
||||
@group(0) @binding(1) var panorama: texture_cube<f32>;
|
||||
@@ -27,6 +26,5 @@ struct Out {
|
||||
return out;
|
||||
}
|
||||
@fragment fn fs_panorama(in: Out) -> @location(0) vec4<f32> {
|
||||
let color = textureSample(panorama, panorama_sampler, in.direction);
|
||||
return vec4(color.rgb, select(color.a, 1.0, HDR_WRITE_LIGHT));
|
||||
return textureSample(panorama, panorama_sampler, in.direction);
|
||||
}
|
||||
@@ -124,10 +124,6 @@ impl<T> Resident<T> {
|
||||
pub fn get_mut(&mut self, slot: u32) -> &mut T {
|
||||
&mut self.items[slot as usize].1
|
||||
}
|
||||
/// Padded mesh bounds of the section in `slot`, relative to its origin.
|
||||
pub fn bounds(&self, slot: u32) -> Option<[Vec3; 2]> {
|
||||
self.bounds[slot as usize]
|
||||
}
|
||||
/// Sections from now on count from `min_y` (a level of another column).
|
||||
pub fn set_min_y(&mut self, min_y: i32) {
|
||||
self.min_y = min_y;
|
||||
@@ -439,18 +435,17 @@ impl<T> Resident<T> {
|
||||
if list.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let ox = f64::from(column.x * 16) - camera.x;
|
||||
let oz = f64::from(column.z * 16) - camera.z;
|
||||
let min_x = ox as f32;
|
||||
let min_z = oz as f32;
|
||||
let low_oy = (f64::from(self.min_y + column.low * 16) - camera.y) as f32;
|
||||
let high_oy = (f64::from(self.min_y + column.high * 16) - camera.y) as f32;
|
||||
let min = Vec3::new(min_x, low_oy, min_z);
|
||||
let max = Vec3::new(min_x + 16.0, high_oy + 16.0, min_z + 16.0);
|
||||
let min = origin((column.x, column.low, column.z), self.min_y, camera).as_vec3();
|
||||
let top = origin((column.x, column.high, column.z), self.min_y, camera).as_vec3();
|
||||
let max = top + Vec3::splat(16.0);
|
||||
let inside = frustum.contains(min, max);
|
||||
if !inside && !frustum.intersects(min, max) {
|
||||
continue;
|
||||
}
|
||||
let ox = f64::from(column.x * 16) - camera.x;
|
||||
let oz = f64::from(column.z * 16) - camera.z;
|
||||
let min_x = ox as f32;
|
||||
let min_z = oz as f32;
|
||||
let dx8 = ox + 8.0;
|
||||
let dz8 = oz + 8.0;
|
||||
let xz_sq = dx8 * dx8 + dz8 * dz8;
|
||||
|
||||
@@ -46,9 +46,6 @@ pub fn star_triangles() -> Vec<[f32; 3]> {
|
||||
struct Uniform {
|
||||
clip_from_sky: [[f32; 4]; 4],
|
||||
color: [f32; 4],
|
||||
hdr_settings: [f32; 4],
|
||||
hdr_shares: [f32; 4],
|
||||
hdr_light: [f32; 4],
|
||||
}
|
||||
|
||||
pub(super) struct Stars {
|
||||
@@ -96,9 +93,7 @@ impl Stars {
|
||||
});
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Stars shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
(include_str!("../hdr_light.wgsl").to_owned() + include_str!("stars.wgsl")).into(),
|
||||
),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("stars.wgsl").into()),
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Stars layout"),
|
||||
@@ -134,13 +129,7 @@ impl Stars {
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_stars"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &[(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
)],
|
||||
..Default::default()
|
||||
},
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
// RenderPipelines.STARS: the celestial blend (src alpha, one), with the
|
||||
@@ -176,15 +165,11 @@ impl Stars {
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
clip_from_sky: Mat4,
|
||||
brightness: f32,
|
||||
globals: &super::Globals,
|
||||
) {
|
||||
let b = brightness.clamp(0.0, 1.0);
|
||||
let uniform = Uniform {
|
||||
clip_from_sky: clip_from_sky.to_cols_array_2d(),
|
||||
color: [b; 4],
|
||||
hdr_settings: globals.hdr_settings,
|
||||
hdr_shares: globals.hdr_shares,
|
||||
hdr_light: globals.hdr_light,
|
||||
};
|
||||
uploads.buffer(encoder, &self.uniform, 0, bytemuck::bytes_of(&uniform));
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@ struct Stars {
|
||||
clip_from_sky: mat4x4<f32>,
|
||||
// starBrightness in every channel (RenderSystem.setShaderColor).
|
||||
color: vec4<f32>,
|
||||
hdr_settings: vec4<f32>,
|
||||
hdr_shares: vec4<f32>,
|
||||
hdr_light: vec4<f32>,
|
||||
}
|
||||
@group(0) @binding(0) var<uniform> stars: Stars;
|
||||
|
||||
@@ -19,9 +16,5 @@ struct Stars {
|
||||
// fully lifted wherever they show at all. Nothing else reads the frame's alpha.
|
||||
@fragment fn fs_stars() -> @location(0) vec4<f32> {
|
||||
let b = stars.color.a;
|
||||
if HDR_WRITE_LIGHT && stars.hdr_light.w != 0.0 {
|
||||
return vec4(hdr_written(stars.color.rgb * b, 0.175, stars.hdr_settings, stars.hdr_shares), b);
|
||||
}
|
||||
if HDR_WRITE_LIGHT { return vec4(stars.color.rgb * b, b); }
|
||||
return vec4(stars.color.rgb * b, 0.175);
|
||||
}
|
||||
@@ -8,10 +8,6 @@ pub(super) fn pipelines(
|
||||
) -> [wgpu::RenderPipeline; 3] {
|
||||
std::array::from_fn(|i| {
|
||||
let mut constants = sprite_constants.to_vec();
|
||||
constants.push((
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(format == super::LINEAR_FRAME)),
|
||||
));
|
||||
constants.push(("NATIVE_MODEL_ALPHA_CUTOFF", [0.0, 0.1, 0.5][i]));
|
||||
// The ordinary Vertex ABI is unchanged. This material's private payload uses
|
||||
// normal's four bytes for a float z, and glint UV for float normal x/y.
|
||||
|
||||
@@ -65,18 +65,13 @@ impl Visbuffer {
|
||||
let resolve_constants: Vec<(&str, f64)> = constants
|
||||
.iter()
|
||||
.copied()
|
||||
.chain([
|
||||
("VIS_BLOCKS_PER_PAGE", blocks_per_page as f64),
|
||||
(
|
||||
"HDR_WRITE_LIGHT",
|
||||
f64::from(u8::from(color == super::LINEAR_FRAME)),
|
||||
),
|
||||
])
|
||||
.chain([("VIS_BLOCKS_PER_PAGE", blocks_per_page as f64)])
|
||||
.collect();
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Horizon visibility buffer"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
(super::terrain_shader() + include_str!("../visbuffer.wgsl")).into(),
|
||||
(include_str!("../terrain.wgsl").to_owned() + include_str!("../visbuffer.wgsl"))
|
||||
.into(),
|
||||
),
|
||||
});
|
||||
let fragment = wgpu::ShaderStages::FRAGMENT;
|
||||
|
||||
@@ -55,13 +55,6 @@ pub struct Post {
|
||||
sampler: wgpu::Sampler,
|
||||
copy: wgpu::RenderPipeline,
|
||||
fxaa: wgpu::RenderPipeline,
|
||||
/// Contrast-adaptive sharpening of TAA's upscaled picture: the jittered frames are
|
||||
/// spread over more output pixels, and the blend softens edges.
|
||||
sharpen: wgpu::RenderPipeline,
|
||||
/// The strength `sharpen` was built with (`set_sharpness`), and what rebuilding it takes.
|
||||
sharpness: f32,
|
||||
shader: wgpu::ShaderModule,
|
||||
pipeline_layout: wgpu::PipelineLayout,
|
||||
format: wgpu::TextureFormat,
|
||||
/// World resolution relative to the window (0.5..=2.0).
|
||||
pub scale: f32,
|
||||
@@ -79,7 +72,6 @@ pub struct Post {
|
||||
/// The last TAA frame's unjittered `view_proj` and camera, `None` once a frame ran without it.
|
||||
taa_previous: Option<(DMat4, DVec3)>,
|
||||
taa_this_frame: TaaUniform,
|
||||
light_key: Option<(bool, [f32; 4], [f32; 4], [[f32; 4]; 2])>,
|
||||
}
|
||||
|
||||
impl Post {
|
||||
@@ -240,12 +232,8 @@ impl Post {
|
||||
..Default::default()
|
||||
});
|
||||
Self {
|
||||
shader: shader.clone(),
|
||||
pipeline_layout: pipeline_layout.clone(),
|
||||
copy: pipeline("fs_copy", &pipeline_layout, format),
|
||||
fxaa: pipeline("fs_fxaa", &pipeline_layout, format),
|
||||
sharpen: sharpen_pipeline(device, &shader, &pipeline_layout, format, 0.85),
|
||||
sharpness: 0.85,
|
||||
layout,
|
||||
sampler,
|
||||
format,
|
||||
@@ -259,28 +247,6 @@ impl Post {
|
||||
taa_frame: 0,
|
||||
taa_previous: None,
|
||||
taa_this_frame: bytemuck::Zeroable::zeroed(),
|
||||
light_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
// History stores baked light, so a change must reject it before jitter records the next frame.
|
||||
pub(super) fn baked_light(
|
||||
&mut self,
|
||||
gain: bool,
|
||||
settings: [f32; 4],
|
||||
shares: [f32; 4],
|
||||
windows: [[f32; 4]; 2],
|
||||
) {
|
||||
// These uniforms do not change baked colour without gain. Keep compatibility
|
||||
// history stable when only its final display settings change.
|
||||
let key = Some(if gain {
|
||||
(true, settings, shares, windows)
|
||||
} else {
|
||||
(false, [0.0; 4], [0.0; 4], [[0.0; 4]; 2])
|
||||
});
|
||||
if self.light_key != key {
|
||||
self.taa_previous = None;
|
||||
self.light_key = key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,21 +474,6 @@ impl Post {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Upscaling Sharpness setting, 0..=1: rebuilds the sharpen pipeline when it changed.
|
||||
pub fn set_sharpness(&mut self, device: &wgpu::Device, sharpness: f32) {
|
||||
let sharpness = sharpness.clamp(0.0, 1.0);
|
||||
if sharpness != self.sharpness {
|
||||
self.sharpen = sharpen_pipeline(
|
||||
device,
|
||||
&self.shader,
|
||||
&self.pipeline_layout,
|
||||
self.format,
|
||||
sharpness,
|
||||
);
|
||||
self.sharpness = sharpness;
|
||||
}
|
||||
}
|
||||
|
||||
/// The size of the world's own target, when it has one.
|
||||
pub fn size(&self) -> Option<[u32; 2]> {
|
||||
self.target.as_ref().map(|t| t.size)
|
||||
@@ -584,12 +535,7 @@ impl Post {
|
||||
pass.set_bind_group(0, &taa.resolve[i], &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
drop(pass);
|
||||
let pipeline = if self.upscale > 1.0 {
|
||||
&self.sharpen
|
||||
} else {
|
||||
&self.copy
|
||||
};
|
||||
self.draw_onto(encoder, screen, &taa.show[i], pipeline, timing);
|
||||
self.finish_with(encoder, screen, &taa.show[i], timing);
|
||||
}
|
||||
|
||||
/// Draw what `group` binds (`bind`) onto `screen`.
|
||||
@@ -599,23 +545,6 @@ impl Post {
|
||||
screen: &wgpu::TextureView,
|
||||
group: &wgpu::BindGroup,
|
||||
timing: Option<&mut super::pack_timing::PassTiming>,
|
||||
) {
|
||||
let pipeline = if self.antialias {
|
||||
&self.fxaa
|
||||
} else {
|
||||
&self.copy
|
||||
};
|
||||
self.draw_onto(encoder, screen, group, pipeline, timing);
|
||||
}
|
||||
|
||||
/// Draw what `group` binds onto `screen` through `pipeline`.
|
||||
fn draw_onto(
|
||||
&self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
screen: &wgpu::TextureView,
|
||||
group: &wgpu::BindGroup,
|
||||
pipeline: &wgpu::RenderPipeline,
|
||||
timing: Option<&mut super::pack_timing::PassTiming>,
|
||||
) {
|
||||
let attachments = [Some(wgpu::RenderPassColorAttachment {
|
||||
view: screen,
|
||||
@@ -632,51 +561,16 @@ impl Post {
|
||||
color_attachments: &attachments,
|
||||
..Default::default()
|
||||
});
|
||||
pass.set_pipeline(pipeline);
|
||||
pass.set_pipeline(if self.antialias {
|
||||
&self.fxaa
|
||||
} else {
|
||||
&self.copy
|
||||
});
|
||||
pass.set_bind_group(0, group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
/// `fs_sharpen` at `sharpness` (its override), drawing onto `format`.
|
||||
fn sharpen_pipeline(
|
||||
device: &wgpu::Device,
|
||||
shader: &wgpu::ShaderModule,
|
||||
layout: &wgpu::PipelineLayout,
|
||||
format: wgpu::TextureFormat,
|
||||
sharpness: f32,
|
||||
) -> wgpu::RenderPipeline {
|
||||
let constants = [("sharpness", f64::from(sharpness))];
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Horizon sharpen"),
|
||||
layout: Some(layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: shader,
|
||||
entry_point: Some("vs_post"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: shader,
|
||||
entry_point: Some("fs_sharpen"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &constants,
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// This frame's clip space to the previous frame's, for camera-relative projections:
|
||||
/// a point `camera - previous_camera` further from the previous camera than from this one.
|
||||
pub(super) fn reprojection(
|
||||
@@ -753,60 +647,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn baked_light_changes_and_returning_from_a_pack_reject_taa_history() {
|
||||
let Some((device, _)) = test_device() else {
|
||||
eprintln!("TAA baked-light GPU skipped: no test device");
|
||||
return;
|
||||
};
|
||||
let mut post = Post::new(&device, wgpu::TextureFormat::Rgba16Float);
|
||||
post.taa = true;
|
||||
if !post.supports_taa() {
|
||||
eprintln!("TAA baked-light GPU skipped: TAA unsupported");
|
||||
return;
|
||||
}
|
||||
let key = (
|
||||
[5.0, 4.0, 1.0, 0.0],
|
||||
[0.2, 0.5, 0.6, 1.0],
|
||||
[[0.1; 4], [0.4; 4]],
|
||||
);
|
||||
for gain in [true, false, true] {
|
||||
post.baked_light(gain, key.0, key.1, key.2);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 1.0);
|
||||
post.baked_light(gain, key.0, key.1, key.2);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 0.0);
|
||||
}
|
||||
for field in 0..16 {
|
||||
let mut changed = key;
|
||||
match field {
|
||||
0..=3 => changed.0[field] += 0.01,
|
||||
4..=7 => changed.1[field - 4] += 0.01,
|
||||
_ => changed.2[(field - 8) / 4][(field - 8) % 4] += 0.01,
|
||||
}
|
||||
post.baked_light(true, changed.0, changed.1, changed.2);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 1.0);
|
||||
}
|
||||
post.baked_light(false, key.0, key.1, key.2);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 1.0);
|
||||
post.baked_light(false, [0.0; 4], [0.0; 4], [[0.0; 4]; 2]);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 0.0);
|
||||
// A pack disables TAA even when the light settings stay the same. Its first
|
||||
// world frame after the pack is dropped must not reuse the pre-pack history.
|
||||
post.jitter([32, 32], false, Mat4::IDENTITY, DVec3::ZERO);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 1.0);
|
||||
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
|
||||
assert_eq!(post.taa_this_frame.jitter[2], 0.0);
|
||||
eprintln!(
|
||||
"TAA baked-light GPU checked gain, all settings/shares/windows and the first frame after a pack"
|
||||
);
|
||||
}
|
||||
|
||||
fn test_device() -> Option<(wgpu::Device, wgpu::Queue)> {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(super) fn pipeline(
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: shader,
|
||||
entry_point: Some("fs_entity_unmarked"),
|
||||
entry_point: Some("fs_entity"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
@@ -67,7 +67,7 @@ struct Out { @builtin(position) pos:vec4<f32>, @location(0) color:vec4<f32> }
|
||||
@vertex fn vs_entity(@location(0) pos:vec3<f32>, @location(3) color:vec4<f32>)->Out {
|
||||
return Out(vec4(pos,1.0),color);
|
||||
}
|
||||
@fragment fn fs_entity_unmarked(v:Out)->@location(0) vec4<f32> { return v.color; }
|
||||
@fragment fn fs_entity(v:Out)->@location(0) vec4<f32> { return v.color; }
|
||||
"#
|
||||
.into(),
|
||||
),
|
||||
|
||||
@@ -532,54 +532,6 @@ pub fn smooth_gradients_offered() -> bool {
|
||||
SMOOTH_GRADIENTS_OFFERED.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// GPU Culling's setting: off, on where it is offered, or Auto, which has it on only where the
|
||||
/// CPU limits the frame and it makes the frame faster (`autotune::GpuCullingAuto`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum GpuCullingMode {
|
||||
#[default]
|
||||
Off,
|
||||
On,
|
||||
Auto,
|
||||
}
|
||||
impl GpuCullingMode {
|
||||
pub const ALL: [GpuCullingMode; 3] = [
|
||||
GpuCullingMode::Off,
|
||||
GpuCullingMode::On,
|
||||
GpuCullingMode::Auto,
|
||||
];
|
||||
/// The value in `horizon-graphics.txt`: `true` and `false` as the switch wrote them.
|
||||
pub fn key(self) -> &'static str {
|
||||
match self {
|
||||
Self::Off => "false",
|
||||
Self::On => "true",
|
||||
Self::Auto => "auto",
|
||||
}
|
||||
}
|
||||
/// The word `horizon-ctl gpu-culling` takes.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Off => "off",
|
||||
Self::On => "on",
|
||||
Self::Auto => "auto",
|
||||
}
|
||||
}
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Off => "Off",
|
||||
Self::On => "On",
|
||||
Self::Auto => "Auto",
|
||||
}
|
||||
}
|
||||
fn parse(s: &str, default: Self) -> Self {
|
||||
match s {
|
||||
"true" | "on" => Self::On,
|
||||
"false" | "off" => Self::Off,
|
||||
"auto" => Self::Auto,
|
||||
_ => default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether GPU Culling (`Graphics::gpu_culling`) can be switched on here, and why not: set when a
|
||||
/// renderer starts, and again when its pass can't be set up or its buffers stop fitting in video
|
||||
/// memory. Where it isn't offered the settings grey its switch out with the reason, and the saved
|
||||
@@ -720,9 +672,8 @@ pub struct Graphics {
|
||||
/// Near solid terrain culled, and its draws listed, on the GPU (`gfx::gpu_culling`), where it
|
||||
/// is offered (`gpu_culling_offer`). Off by default (GPU Culling, Performance graphics); it
|
||||
/// applies at once, and no preset sets it. Every save writes it, so a default turned on later
|
||||
/// needs a new key, as `batchTranslucent` did. Auto turns it on only where the CPU limits the
|
||||
/// frame and it makes the frame faster (`autotune`).
|
||||
pub gpu_culling: GpuCullingMode,
|
||||
/// needs a new key, as `batchTranslucent` did.
|
||||
pub gpu_culling: bool,
|
||||
/// Skip drawing entities hidden behind opaque terrain, synchronously or asynchronously.
|
||||
pub entity_culling: EntityCullingMode,
|
||||
/// Minimum entity population for coarse AABB culling; zero disables it.
|
||||
@@ -733,8 +684,6 @@ pub struct Graphics {
|
||||
/// vanilla's fancy leaves. Ignored with a resource pack that may replace
|
||||
/// the vanilla leaf models ([`Graphics::leaf_culling_effective`]).
|
||||
pub leaf_culling: LeafCullingMode,
|
||||
/// Draw regular ice opaque in the solid pass, without its see-through texture.
|
||||
pub opaque_ice: bool,
|
||||
/// Cache settled shulker-box models.
|
||||
pub cache_shulker_boxes: bool,
|
||||
/// Cache sign-text geometry.
|
||||
@@ -776,8 +725,6 @@ pub struct Graphics {
|
||||
pub taa: bool,
|
||||
/// TAA upscaling; anything but `Native` comes with `taa` on.
|
||||
pub upscaling: Upscaling,
|
||||
/// How much the upscaled picture is sharpened, 0..=100 (percent of the most).
|
||||
pub upscaling_sharpness: u32,
|
||||
/// F2 screenshots at this many times the game's resolution: 1, 2, 4 or 8.
|
||||
pub screenshot_quality: u32,
|
||||
/// Frame generation (Horizon setting), off by default.
|
||||
@@ -837,12 +784,11 @@ impl Default for Graphics {
|
||||
far_detail: FarDetail::Full,
|
||||
blue_map: crate::bluemap::Settings::default(),
|
||||
cave_culling: true,
|
||||
gpu_culling: GpuCullingMode::Off,
|
||||
gpu_culling: false,
|
||||
entity_culling: EntityCullingMode::Off,
|
||||
entity_culling_precheck: 64,
|
||||
particle_culling: ParticleCullingMode::Off,
|
||||
leaf_culling: LeafCullingMode::Off,
|
||||
opaque_ice: false,
|
||||
cache_shulker_boxes: false,
|
||||
// Sign text has used its renderer cache independently of the new model caches.
|
||||
cache_signs: true,
|
||||
@@ -867,7 +813,6 @@ impl Default for Graphics {
|
||||
fxaa: false,
|
||||
taa: false,
|
||||
upscaling: Upscaling::Native,
|
||||
upscaling_sharpness: 85,
|
||||
screenshot_quality: 1,
|
||||
frame_generation: FrameGeneration::Off,
|
||||
hdr: false,
|
||||
@@ -937,14 +882,13 @@ impl Graphics {
|
||||
}
|
||||
"blueMapAutoDetect" => g.blue_map.auto_detect = flag(g.blue_map.auto_detect),
|
||||
"caveCulling" => g.cave_culling = flag(g.cave_culling),
|
||||
"gpuCulling" => g.gpu_culling = GpuCullingMode::parse(value, g.gpu_culling),
|
||||
"gpuCulling" => g.gpu_culling = flag(g.gpu_culling),
|
||||
"entityCulling" => g.entity_culling = EntityCullingMode::parse(value),
|
||||
"entityCullingPrecheck" => {
|
||||
g.entity_culling_precheck = number(g.entity_culling_precheck).min(512)
|
||||
}
|
||||
"particleCulling" => g.particle_culling = ParticleCullingMode::parse(value),
|
||||
"leafCulling" => g.leaf_culling = LeafCullingMode::parse(value),
|
||||
"opaqueIce" => g.opaque_ice = flag(g.opaque_ice),
|
||||
"blockEntityCache" => {
|
||||
let enabled = flag(false);
|
||||
g.cache_shulker_boxes = enabled;
|
||||
@@ -991,9 +935,6 @@ impl Graphics {
|
||||
"fxaa" => g.fxaa = flag(g.fxaa),
|
||||
"taa" => g.taa = flag(g.taa),
|
||||
"upscaling" => g.upscaling = Upscaling::parse(value),
|
||||
"upscalingSharpness" => {
|
||||
g.upscaling_sharpness = number(g.upscaling_sharpness).min(100)
|
||||
}
|
||||
"screenshotQuality" => {
|
||||
g.screenshot_quality = Some(number(0))
|
||||
.filter(|q| SCREENSHOT_QUALITIES.contains(q))
|
||||
@@ -1039,7 +980,7 @@ impl Graphics {
|
||||
}
|
||||
pub fn serialize(&self) -> String {
|
||||
let mut text = format!(
|
||||
"preset:{}\nlodDistance:{}\nfarDetail:{}\ncaveCulling:{}\nentityCulling:{}\nentityCullingPrecheck:{}\nparticleCulling:{}\nleafCulling:{}\nopaqueIce:{}\nblockEntityCacheShulkerBoxes:{}\nblockEntityCacheSigns:{}\nblockEntityCacheDecoratedPots:{}\nblockEntityCacheBells:{}\nblockEntityCacheChests:{}\nblockEntityCacheBanners:{}\nblockEntityCacheCopperGolemStatues:{}\nchunkUpdates:{}\nuploadBudgetKiB:{}\ntextureBudgetMiB:{}\nsky:{}\nfog:{}\nweather:{}\nanimatedTextures:{}\nditherCutoutMips:{}\npresentMode:{}\nframePacing:{}\nworkers:{}\noverlay:{}\nresolutionScale:{}\nfxaa:{}\ntaa:{}\nupscaling:{}\nupscalingSharpness:{}\nscreenshotQuality:{}\nframeGeneration:{}\nhdr:{}\nhdrPaperWhite:{}\nhdrPeak:{}\nhdrBlackLevel:{}\nhdrUiBrightness:{}\nhdrLighting:{}\nhdrKey:{}\nbatteryMode:{}\nshadowCache:{}\ngraphicsApi:{}\npickedShaderPack:{}\nbatchTranslucent:{}\nhdrSmoothGradients:{}\ngpuCulling:{}\n",
|
||||
"preset:{}\nlodDistance:{}\nfarDetail:{}\ncaveCulling:{}\nentityCulling:{}\nentityCullingPrecheck:{}\nparticleCulling:{}\nleafCulling:{}\nblockEntityCacheShulkerBoxes:{}\nblockEntityCacheSigns:{}\nblockEntityCacheDecoratedPots:{}\nblockEntityCacheBells:{}\nblockEntityCacheChests:{}\nblockEntityCacheBanners:{}\nblockEntityCacheCopperGolemStatues:{}\nchunkUpdates:{}\nuploadBudgetKiB:{}\ntextureBudgetMiB:{}\nsky:{}\nfog:{}\nweather:{}\nanimatedTextures:{}\nditherCutoutMips:{}\npresentMode:{}\nframePacing:{}\nworkers:{}\noverlay:{}\nresolutionScale:{}\nfxaa:{}\ntaa:{}\nupscaling:{}\nscreenshotQuality:{}\nframeGeneration:{}\nhdr:{}\nhdrPaperWhite:{}\nhdrPeak:{}\nhdrBlackLevel:{}\nhdrUiBrightness:{}\nhdrLighting:{}\nhdrKey:{}\nbatteryMode:{}\nshadowCache:{}\ngraphicsApi:{}\npickedShaderPack:{}\nbatchTranslucent:{}\nhdrSmoothGradients:{}\ngpuCulling:{}\n",
|
||||
self.preset.name(),
|
||||
self.lod_distance,
|
||||
self.far_detail.name(),
|
||||
@@ -1048,7 +989,6 @@ impl Graphics {
|
||||
self.entity_culling_precheck,
|
||||
self.particle_culling.name(),
|
||||
self.leaf_culling.name(),
|
||||
bool_text(self.opaque_ice),
|
||||
bool_text(self.cache_shulker_boxes),
|
||||
bool_text(self.cache_signs),
|
||||
bool_text(self.cache_decorated_pots),
|
||||
@@ -1079,7 +1019,6 @@ impl Graphics {
|
||||
bool_text(self.fxaa),
|
||||
bool_text(self.taa),
|
||||
self.upscaling.name(),
|
||||
self.upscaling_sharpness,
|
||||
self.screenshot_quality,
|
||||
self.frame_generation.name(),
|
||||
bool_text(self.hdr),
|
||||
@@ -1095,7 +1034,7 @@ impl Graphics {
|
||||
self.shader_pack,
|
||||
bool_text(self.translucent_batching),
|
||||
bool_text(self.hdr_smooth_gradients),
|
||||
self.gpu_culling.key(),
|
||||
bool_text(self.gpu_culling),
|
||||
);
|
||||
text += &self.blue_map.serialize();
|
||||
text
|
||||
@@ -1235,12 +1174,11 @@ mod tests {
|
||||
servers: Vec::new(),
|
||||
},
|
||||
cave_culling: false,
|
||||
gpu_culling: GpuCullingMode::On,
|
||||
gpu_culling: true,
|
||||
entity_culling: EntityCullingMode::Synchronous,
|
||||
entity_culling_precheck: 128,
|
||||
particle_culling: ParticleCullingMode::Asynchronous,
|
||||
leaf_culling: LeafCullingMode::SolidAggressive,
|
||||
opaque_ice: true,
|
||||
cache_shulker_boxes: true,
|
||||
cache_signs: true,
|
||||
cache_decorated_pots: true,
|
||||
@@ -1264,7 +1202,6 @@ mod tests {
|
||||
fxaa: true,
|
||||
taa: true,
|
||||
upscaling: Upscaling::Balanced,
|
||||
upscaling_sharpness: 40,
|
||||
screenshot_quality: 4,
|
||||
frame_generation: FrameGeneration::Fsr31,
|
||||
hdr: true,
|
||||
@@ -1326,32 +1263,8 @@ mod tests {
|
||||
assert!(!Graphics::parse("hdr:true\nhdrSmoothGradients:false\n").wide_frame());
|
||||
assert!(Graphics::parse("translucentBatching:false\n").translucent_batching);
|
||||
// GPU Culling is opt-in: only the file or its switch turns it on.
|
||||
assert_eq!(Graphics::default().gpu_culling, GpuCullingMode::Off);
|
||||
assert_eq!(
|
||||
Graphics::parse("gpuCulling:true\n").gpu_culling,
|
||||
GpuCullingMode::On
|
||||
);
|
||||
// Auto is a third value of the same key. A file from before it reads as before, and an
|
||||
// older Horizon reading `auto` finds a value it doesn't know and keeps Off.
|
||||
assert_eq!(
|
||||
Graphics::parse("gpuCulling:auto\n").gpu_culling,
|
||||
GpuCullingMode::Auto
|
||||
);
|
||||
assert_eq!(
|
||||
Graphics::parse("gpuCulling:false\n").gpu_culling,
|
||||
GpuCullingMode::Off
|
||||
);
|
||||
assert_eq!(
|
||||
Graphics::parse("gpuCulling:maybe\n").gpu_culling,
|
||||
GpuCullingMode::Off
|
||||
);
|
||||
for mode in GpuCullingMode::ALL {
|
||||
let g = Graphics {
|
||||
gpu_culling: mode,
|
||||
..Graphics::default()
|
||||
};
|
||||
assert_eq!(Graphics::parse(&g.serialize()).gpu_culling, mode);
|
||||
}
|
||||
assert!(!Graphics::default().gpu_culling);
|
||||
assert!(Graphics::parse("gpuCulling:true\n").gpu_culling);
|
||||
// 0 is Auto; anything else is a peak the slider reaches.
|
||||
assert_eq!(Graphics::parse("hdrPeak:0\n").hdr_peak, 0);
|
||||
assert_eq!(Graphics::parse("hdrPeak:50\n").hdr_peak, 200);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -33,11 +33,6 @@ struct Params {
|
||||
@group(0) @binding(2) var<uniform> params: Params;
|
||||
// Horizon's logo, 1 inside, 0 outside, for calibration's test pictures.
|
||||
@group(0) @binding(3) var logo: texture_2d<f32>;
|
||||
// The float frame with HUD coverage (Hdr::layered): the frame keeps the world as light
|
||||
// directly from its writers; the HUD draws on a picture of its own (`hud`, fs_keep), and `coverage` says how
|
||||
// much of the world it hides there (gfx/hud_coverage.rs).
|
||||
@group(0) @binding(4) var coverage: texture_2d<f32>;
|
||||
@group(0) @binding(5) var hud: texture_2d<f32>;
|
||||
|
||||
// Light sources are lifted evenly rather than by a highlight curve: vanilla's
|
||||
// textures were drawn for SDR, so a curve on their brightest pixels blows out
|
||||
@@ -154,28 +149,80 @@ fn calibration_nits(p: vec2<f32>, size: vec2<f32>) -> f32 {
|
||||
return vec4(0.0);
|
||||
}
|
||||
|
||||
fn lit(f: vec3<f32>, a: f32, shows: bool, eased: bool) -> Lit {
|
||||
return hdr_mark_light(f, a, shows, eased, vec4(params.peak, params.scene, params.world, params.debug), params.share);
|
||||
}
|
||||
|
||||
// Calibration: above its controls the test picture, straight in nits, past
|
||||
// paper white, the lift and the black level.
|
||||
fn calibrating(pos: vec4<f32>) -> bool {
|
||||
@fragment fn fs_hdr(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
let at = vec2<i32>(pos.xy);
|
||||
// Calibration: above its controls the test picture, straight in nits, past
|
||||
// paper white, the lift and the black level.
|
||||
let size = vec2<f32>(textureDimensions(frame));
|
||||
return params.calibration.x > 0.5 && pos.y < size.y * params.calibration.z;
|
||||
}
|
||||
|
||||
fn calibration_picture(pos: vec4<f32>) -> vec4<f32> {
|
||||
let nits = vec3(calibration_nits(pos.xy, vec2<f32>(textureDimensions(frame))));
|
||||
if params.pq > 0.5 {
|
||||
return vec4(pq(nits), 1.0);
|
||||
if params.calibration.x > 0.5 && pos.y < size.y * params.calibration.z {
|
||||
let nits = vec3(calibration_nits(pos.xy, size));
|
||||
if params.pq > 0.5 {
|
||||
return vec4(pq(nits), 1.0);
|
||||
}
|
||||
return vec4(nits / 80.0, 1.0);
|
||||
}
|
||||
return vec4(nits / 80.0, 1.0);
|
||||
}
|
||||
|
||||
// Light in units of paper white, as the swapchain takes it: HDR10 or scRGB, the black level
|
||||
// lifted.
|
||||
fn display(light: vec3<f32>) -> vec4<f32> {
|
||||
let f = frame_texel(textureLoad(frame, at, 0));
|
||||
// What covers the world here (Hdr::keep_world): nothing at 0, the hand at
|
||||
// 1/255 (Hdr::mark_hand: drawn at the world's brightness), else the HUD or a
|
||||
// menu, by how much.
|
||||
let covered = f.a > 0.0;
|
||||
let hand = covered && f.a < 1.5 / 255.0;
|
||||
let ui = select(f.a, 0.0, hand);
|
||||
let w = frame_texel(textureLoad(world, at, 0));
|
||||
// The world's alpha is below 0.75 on light sources, the sun and the moon
|
||||
// (terrain.wgsl, entity.wgsl): only those shine, and only where nothing was
|
||||
// drawn over them since: grey text over the white sun still looks like the sun
|
||||
// darkened, but it raised the frame's alpha. Its band of 0.175 is the tier, the
|
||||
// lower the brighter, and where in the band how much (hdr_mark). The hand
|
||||
// writes its own alpha there (Hdr::hand_light_view): 1, or a light source it holds.
|
||||
var lifted = false;
|
||||
let shows = params.world > 0.5 && params.peak > 1.0
|
||||
&& (hand || (!covered && shows_world(f.rgb, w.rgb)));
|
||||
// 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.rgb, vec3(1.0)), f.rgb, shows && w.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 && w.a >= 0.75 && w.a < 1.0 {
|
||||
let top = max(light.r, max(light.g, light.b));
|
||||
let gain = 1.0 + (params.scene - 1.0) * (1.0 - w.a) / 0.24;
|
||||
let v = top * gain;
|
||||
let knee = max(0.6 * params.peak, 1.0);
|
||||
var out = v;
|
||||
if v > knee && params.peak > knee {
|
||||
out = knee + (params.peak - knee) * (1.0 - exp(-(v - knee) / (params.peak - knee)));
|
||||
}
|
||||
light *= select(gain, out / max(top, 1e-4), v > knee);
|
||||
}
|
||||
if shows && w.a < 0.75 {
|
||||
let band = u32(clamp(floor((w.a + 0.005) / 0.175), 0.0, 3.0));
|
||||
let tier = 3u - band;
|
||||
let lift = 1.0 - clamp((w.a - 0.175 * f32(band)) / 0.165, 0.0, 1.0);
|
||||
if lift > 0.0 {
|
||||
let top = max(light.r, max(light.g, light.b));
|
||||
let gain = min(1.0 + (params.peak - 1.0) * params.share[tier] * lift, params.peak / max(top, 1e-4));
|
||||
light *= max(gain, 1.0);
|
||||
lifted = true;
|
||||
}
|
||||
}
|
||||
// HORIZON_HDR_DEBUG=1: what isn't lifted turns grey, what is keeps its
|
||||
// colour with a magenta tint, so a lift shows on any colour.
|
||||
if params.debug > 0.5 && params.world > 0.5 {
|
||||
if lifted {
|
||||
light *= vec3(1.0, 0.6, 1.0);
|
||||
} else {
|
||||
light = vec3(dot(light, vec3(0.2126, 0.7152, 0.0722)));
|
||||
}
|
||||
}
|
||||
light = min(light, vec3(params.peak));
|
||||
// The HUD and menus at their own brightness, by how much they cover.
|
||||
light *= mix(1.0, params.ui, ui);
|
||||
let scrgb = light * params.paper;
|
||||
if params.pq > 0.5 {
|
||||
let e = pq(BT709_TO_BT2020 * scrgb * 80.0);
|
||||
@@ -186,107 +233,3 @@ fn display(light: vec3<f32>) -> vec4<f32> {
|
||||
}
|
||||
return vec4(scrgb, 1.0);
|
||||
}
|
||||
|
||||
@fragment fn fs_hdr(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
if calibrating(pos) {
|
||||
return calibration_picture(pos);
|
||||
}
|
||||
let at = vec2<i32>(pos.xy);
|
||||
let f = frame_texel(textureLoad(frame, at, 0));
|
||||
// What covers the world here (Hdr::keep_world): nothing at 0, the hand at
|
||||
// 1/255 (Hdr::mark_hand: drawn at the world's brightness), else the HUD or a
|
||||
// menu, by how much.
|
||||
let covered = f.a > 0.0;
|
||||
let hand = covered && f.a < 1.5 / 255.0;
|
||||
let ui = select(f.a, 0.0, hand);
|
||||
let w = frame_texel(textureLoad(world, at, 0));
|
||||
// The world's marks lift only where nothing was drawn over them since: grey text over
|
||||
// the white sun still looks like the sun darkened, but it raised the frame's alpha. The
|
||||
// hand writes its own alpha there (Hdr::hand_light_view): 1, or a light source it holds.
|
||||
let shows = params.world > 0.5 && params.peak > 1.0
|
||||
&& (hand || (!covered && shows_world(f.rgb, w.rgb)));
|
||||
var light = lit(f.rgb, w.a, shows, true).light;
|
||||
// The HUD and menus at their own brightness, by how much they cover.
|
||||
light *= mix(1.0, params.ui, ui);
|
||||
return display(light);
|
||||
}
|
||||
|
||||
// The tone curve's knee: up to it light stays as it is. Where HDR lighting eased into the peak
|
||||
// before the curve (`lit`), 60% of it, so bright textures keep their detail; at least paper white.
|
||||
fn tone_knee(headroom: f32) -> f32 {
|
||||
return max(0.6 * headroom, 1.0);
|
||||
}
|
||||
|
||||
// The tone curve on light whose brightest channel is `top`, in units of paper white `paper`
|
||||
// nits: what to scale the light by, so it ends at most `headroom` (above 1). ITU-R BT.2390's
|
||||
// EETF in PQ, without the black level lift (`display` does that): the same up to its knee, then
|
||||
// a Hermite spline that reaches the peak level, at the source's peak. That peak is where the knee
|
||||
// lands at `tone_knee`, PQ's 10 000 nits at most; light past it shows at the peak. The brightest
|
||||
// channel sets the scale for all three, so light keeps its hue as it eases (lava stays orange)
|
||||
// and no channel passes the peak.
|
||||
fn tone_scale(top: f32, headroom: f32, paper: f32) -> f32 {
|
||||
if top <= tone_knee(headroom) {
|
||||
return 1.0;
|
||||
}
|
||||
let peak = pq(vec3(headroom * paper)).x;
|
||||
let source = min(3.0 * peak - 2.0 * pq(vec3(tone_knee(headroom) * paper)).x, 1.0);
|
||||
let e = pq(vec3(top * paper)).x / source;
|
||||
let max_lum = peak / source;
|
||||
let ks = 1.5 * max_lum - 0.5;
|
||||
if e >= 1.0 {
|
||||
return headroom / top;
|
||||
}
|
||||
if e <= ks {
|
||||
return 1.0;
|
||||
}
|
||||
let t = (e - ks) / (1.0 - ks);
|
||||
let t2 = t * t;
|
||||
let t3 = t2 * t;
|
||||
let p = (2.0 * t3 - 3.0 * t2 + 1.0) * ks + (t3 - 2.0 * t2 + t) * (1.0 - ks) + (3.0 * t2 - 2.0 * t3) * max_lum;
|
||||
return pq_nits(vec3(p * source)).x / paper / top;
|
||||
}
|
||||
|
||||
// Hdr::tonemap, after the post effects and before the camera overlays (docs/design/
|
||||
// hdr-linear-light.md, `[tonemap]`): the light (`world` holds the frame) eased into the peak, in
|
||||
// gamma, where scaling the light by s scales the stored colour by s^(1/2.2). Light up to the
|
||||
// knee stays exactly as stored. With no headroom, a clip at white.
|
||||
@fragment fn fs_tonemap(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
let g = textureLoad(world, vec2<i32>(pos.xy), 0).rgb;
|
||||
if params.peak <= 1.0 {
|
||||
return vec4(min(g, vec3(1.0)), 1.0);
|
||||
}
|
||||
let top = pow(max(max(g.r, max(g.g, g.b)), 0.0), 2.2);
|
||||
let s = tone_scale(top, params.peak, params.paper * 80.0);
|
||||
return vec4(select(g * pow(s, 1.0 / 2.2), g, s == 1.0), 1.0);
|
||||
}
|
||||
|
||||
// Hdr::keep_world: the HUD's backdrop, the world clipped to white with alpha 0, the frame as
|
||||
// the HUD found it before.
|
||||
@fragment fn fs_keep(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
let w = textureLoad(frame, vec2<i32>(pos.xy), 0).rgb;
|
||||
return vec4(clamp(w, vec3(0.0), vec3(1.0)), 0.0);
|
||||
}
|
||||
|
||||
// The HUD over the world in gamma, the space the HUD blends in: its own part, what its backdrop
|
||||
// B doesn't explain (H = P - (1 - c) B, exact for the layering blends), at UI Brightness, over
|
||||
// (1 - c) of the world, light above white included. Where the world is at or below white, at
|
||||
// 100%, that is the HUD's picture as drawn.
|
||||
fn layered(at: vec2<i32>) -> vec3<f32> {
|
||||
let w = max(textureLoad(frame, at, 0).rgb, vec3(0.0));
|
||||
let p = clamp(textureLoad(hud, at, 0).rgb, vec3(0.0), vec3(1.0));
|
||||
let c = textureLoad(coverage, at, 0).r;
|
||||
let h = max(p - (1.0 - c) * min(w, vec3(1.0)), vec3(0.0));
|
||||
return h * pow(params.ui, 1.0 / 2.2) + (1.0 - c) * w;
|
||||
}
|
||||
|
||||
// Hdr::composite: the picture so far, stored as the frame stores light, for the menu blur.
|
||||
@fragment fn fs_composite(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
return vec4(layered(vec2<i32>(pos.xy)), 1.0);
|
||||
}
|
||||
|
||||
@fragment fn fs_hdr_layered(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
if calibrating(pos) {
|
||||
return calibration_picture(pos);
|
||||
}
|
||||
return display(min(pow(layered(vec2<i32>(pos.xy)), vec3(2.2)), vec3(params.peak)));
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
override HDR_WRITE_LIGHT: bool = false;
|
||||
|
||||
// The light of colour `f` under the world's mark `a` (the alpha the world left, hdr_mark) where
|
||||
// the world `shows`, else the colour clipped at white; and whether a light source lifted it.
|
||||
// `eased` for the last pass, which eases it into the peak itself; the layered frame leaves that
|
||||
// to the tone curve (fs_tonemap).
|
||||
struct Lit {
|
||||
light: vec3<f32>,
|
||||
lifted: bool,
|
||||
};
|
||||
|
||||
fn hdr_mark_light(f: vec3<f32>, a: f32, shows: bool, eased: bool, settings: vec4<f32>, shares: vec4<f32>) -> Lit {
|
||||
var lifted = false;
|
||||
// Light over 1 (a float frame's) shows only where the world does and carries
|
||||
// no mark (alpha 1), up to the peak: light written as light. Marked pixels clip
|
||||
// at white before their lift, as on a unorm frame, so light sources, the sun and
|
||||
// HDR lighting look as they do there; so do the HUD, menus and HDR Lighting off.
|
||||
let c = select(min(f, vec3(1.0)), f, shows && a == 1.0);
|
||||
// Gamma 2.2, as SDR displays and the compositors' SDR paths decode it.
|
||||
var light = pow(c, vec3(2.2));
|
||||
// HDR lighting: alpha between 0.76 and 1 is how much of it the surface gets. Vanilla's
|
||||
// light is albedo times lightmap, so this is the lightmap going past 1. It eases into the
|
||||
// peak from 60% of it, so bright textures keep their detail. Unmarked light (alpha 1)
|
||||
// gets no gain and no knee: the clamp to the peak below is all it needs here.
|
||||
if shows && a >= 0.75 && a < 1.0 {
|
||||
let top = max(light.r, max(light.g, light.b));
|
||||
let gain = 1.0 + (settings.y - 1.0) * (1.0 - a) / 0.24;
|
||||
let v = top * gain;
|
||||
let knee = max(0.6 * settings.x, 1.0);
|
||||
var out = v;
|
||||
if eased && v > knee && settings.x > knee {
|
||||
out = knee + (settings.x - knee) * (1.0 - exp(-(v - knee) / (settings.x - knee)));
|
||||
}
|
||||
light *= select(gain, out / max(top, 1e-4), eased && v > knee);
|
||||
}
|
||||
// The world's alpha is below 0.75 on light sources, the sun and the moon
|
||||
// (terrain.wgsl, entity.wgsl): its band of 0.175 is the tier, the lower the
|
||||
// brighter, and where in the band how much (hdr_mark).
|
||||
if shows && a < 0.75 {
|
||||
let band = u32(clamp(floor((a + 0.005) / 0.175), 0.0, 3.0));
|
||||
let tier = 3u - band;
|
||||
let lift = 1.0 - clamp((a - 0.175 * f32(band)) / 0.165, 0.0, 1.0);
|
||||
if lift > 0.0 {
|
||||
let top = max(light.r, max(light.g, light.b));
|
||||
var gain = 1.0 + (settings.x - 1.0) * shares[tier] * lift;
|
||||
if eased {
|
||||
gain = min(gain, settings.x / max(top, 1e-4));
|
||||
}
|
||||
light *= max(gain, 1.0);
|
||||
lifted = true;
|
||||
}
|
||||
}
|
||||
// HORIZON_HDR_DEBUG=1: what isn't lifted turns grey, what is keeps its
|
||||
// colour with a magenta tint, so a lift shows on any colour.
|
||||
if settings.w > 0.5 && settings.z > 0.5 {
|
||||
if lifted {
|
||||
light *= vec3(1.0, 0.6, 1.0);
|
||||
} else {
|
||||
light = vec3(dot(light, vec3(0.2126, 0.7152, 0.0722)));
|
||||
}
|
||||
}
|
||||
if eased {
|
||||
light = min(light, vec3(settings.x));
|
||||
}
|
||||
return Lit(light, lifted);
|
||||
}
|
||||
|
||||
// Marks are decoded at the precision of the original 8-bit frame, even in a float target.
|
||||
fn hdr_quantized_mark(mark: f32) -> f32 {
|
||||
return round(saturate(mark) * 255.0) / 255.0;
|
||||
}
|
||||
|
||||
fn hdr_written(color: vec3<f32>, mark: f32, settings: vec4<f32>, shares: vec4<f32>) -> vec3<f32> {
|
||||
let shows = settings.z > 0.5 && settings.x > 1.0;
|
||||
return pow(hdr_mark_light(max(color, vec3(0.0)), hdr_quantized_mark(mark), shows, false, settings, shares).light, vec3(1.0 / 2.2));
|
||||
}
|
||||
@@ -100,18 +100,12 @@ pub struct HudAtlas {
|
||||
/// A resource pack's own `core/rendertype_lines` shader for the block outline, translated;
|
||||
/// none draws the outline natively.
|
||||
pub lines_shader: Option<std::sync::Arc<horizon_shaders::CoreProgram>>,
|
||||
/// The packs' `core/sky` and `core/stars`, when a pack replaces them.
|
||||
pub sky_shader: Option<std::sync::Arc<horizon_shaders::CoreProgram>>,
|
||||
pub stars_shader: Option<std::sync::Arc<horizon_shaders::CoreProgram>>,
|
||||
/// The packs' OptiFine random entity textures, whose variants this atlas packs.
|
||||
pub random_textures: horizon_optifine::random_textures::RandomTextures,
|
||||
/// Their emissive overlays (`_e`), packed here too.
|
||||
pub emissive: std::sync::Arc<horizon_optifine::emissive::Emissive>,
|
||||
/// ETF's blinking frames (`_blink`), packed here too.
|
||||
pub blinks: horizon_optifine::blink::Blinks,
|
||||
/// Of the shared atlas's textures, their LabPBR normal and specular maps as paged maps
|
||||
/// (`paged_maps.rs`), read while loading for shader packs (`sprites::labpbr_maps`).
|
||||
pub material_maps: std::sync::Arc<[Option<crate::paged_maps::PagedMap>; 2]>,
|
||||
white: Rect,
|
||||
}
|
||||
/// Atlas rows reserved for glyphs rasterized at run time.
|
||||
@@ -537,49 +531,6 @@ fn cit_sprite(root: &Path, id: &str) -> Option<(PackedImage, Option<TextureFlags
|
||||
let flags = mcmeta.as_deref().and_then(TextureFlags::from_mcmeta);
|
||||
Some(((name.to_owned(), w, h, pixels), flags, animation.is_some()))
|
||||
}
|
||||
/// The texture of every `bitmap` font provider. A provider's `file` may name
|
||||
/// any texture, so a pack's glyph sheet need not live under `textures/font`.
|
||||
fn bitmap_font_textures(root: &Path) -> Vec<PathBuf> {
|
||||
fn jsons(dir: &Path, out: &mut Vec<PathBuf>) {
|
||||
for entry in fs::read_dir(dir).into_iter().flatten().flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
jsons(&path, out);
|
||||
} else if path.extension().is_some_and(|e| e == "json") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
let assets = root.join("assets");
|
||||
let mut defs = Vec::new();
|
||||
for ns in fs::read_dir(&assets).into_iter().flatten().flatten() {
|
||||
jsons(&ns.path().join("font"), &mut defs);
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
for def in defs {
|
||||
let Ok(bytes) = fs::read(&def) else { continue };
|
||||
let bytes = bytes.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(&bytes);
|
||||
let Ok(json) = serde_json::from_slice::<serde_json::Value>(bytes) else {
|
||||
continue;
|
||||
};
|
||||
for p in json["providers"].as_array().into_iter().flatten() {
|
||||
if p["type"] != "bitmap" {
|
||||
continue;
|
||||
}
|
||||
let Some(file) = p["file"].as_str() else {
|
||||
continue;
|
||||
};
|
||||
let (ns, path) = file.split_once(':').unwrap_or(("minecraft", file));
|
||||
let path = assets.join(ns).join("textures").join(path);
|
||||
if path.is_file() && seen.insert(path.clone()) {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Folders of `assets/minecraft/textures/` that [`HudAtlas::pack`] walks for
|
||||
/// the full atlas. A pack may add others through `atlases/blocks.json`.
|
||||
const WALKED_TEXTURE_FOLDERS: &[&str] = &[
|
||||
@@ -894,10 +845,6 @@ impl HudAtlas {
|
||||
&[],
|
||||
&crate::gfx::core_lines::GLUE,
|
||||
);
|
||||
let sky =
|
||||
|name| pack_text::load_core(root, defaults, name, &[], &crate::gfx::core_sky::GLUE);
|
||||
atlas.sky_shader = sky("sky");
|
||||
atlas.stars_shader = sky("stars");
|
||||
Ok(atlas)
|
||||
}
|
||||
/// Empty slots written at run time: map textures (game/maps.rs), the
|
||||
@@ -977,7 +924,6 @@ impl HudAtlas {
|
||||
revision: Self::next_revision(),
|
||||
pixels: Vec::new(),
|
||||
packed: None,
|
||||
material_maps: Default::default(),
|
||||
galactic_widths: [0; 128],
|
||||
width: 1,
|
||||
height: 1,
|
||||
@@ -992,8 +938,6 @@ impl HudAtlas {
|
||||
glint_armor: (Vec::new(), 1, 1),
|
||||
text_shader: None,
|
||||
lines_shader: None,
|
||||
sky_shader: None,
|
||||
stars_shader: None,
|
||||
random_textures: Default::default(),
|
||||
emissive: Arc::default(),
|
||||
blinks: Default::default(),
|
||||
@@ -1042,7 +986,6 @@ impl HudAtlas {
|
||||
(glyph_top, height - glyph_top),
|
||||
white,
|
||||
),
|
||||
material_maps: Default::default(),
|
||||
galactic_widths: galactic_widths(&sprites, &pixels, width, height),
|
||||
pixels,
|
||||
packed: None,
|
||||
@@ -1059,8 +1002,6 @@ impl HudAtlas {
|
||||
white,
|
||||
text_shader: None,
|
||||
lines_shader: None,
|
||||
sky_shader: None,
|
||||
stars_shader: None,
|
||||
random_textures: Default::default(),
|
||||
emissive: Default::default(),
|
||||
blinks: Default::default(),
|
||||
@@ -1161,18 +1102,6 @@ impl HudAtlas {
|
||||
for folder in folders {
|
||||
walk(&textures.join(folder), &mut files);
|
||||
}
|
||||
// A bitmap provider's `file` may be outside the walked folders (HUD
|
||||
// packs keep their glyph sheets under their own namespace or path).
|
||||
// Pack those sheets too, so Font reads them from the atlas instead of
|
||||
// copying every glyph into the small run-time region.
|
||||
if folders.contains(&"font") {
|
||||
let mut seen: HashSet<PathBuf> = files.iter().cloned().collect();
|
||||
files.extend(
|
||||
bitmap_font_textures(root)
|
||||
.into_iter()
|
||||
.filter(|p| seen.insert(p.clone())),
|
||||
);
|
||||
}
|
||||
// Content mods' entity textures: their equipment assets name them as
|
||||
// `<ns>:<path>`, and native mods draw their block entities with them
|
||||
// (`native_draw_quad`: `<ns>:entity/...`); and their mob effects'
|
||||
@@ -1208,21 +1137,9 @@ impl HudAtlas {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The title panorama and the New World preset pictures load on their own. The
|
||||
// Realms world-slot pictures are drawn from this atlas (vanilla draws their
|
||||
// textures full size): the three faces and `presets/isles.png` stay, costing
|
||||
// one 1024-tall shelf.
|
||||
files.retain(|p| {
|
||||
let s = p.to_string_lossy().replace('\\', "/");
|
||||
let slot_picture = [
|
||||
"textures/gui/title/background/panorama_0.png",
|
||||
"textures/gui/title/background/panorama_2.png",
|
||||
"textures/gui/title/background/panorama_3.png",
|
||||
"textures/gui/presets/isles.png",
|
||||
]
|
||||
.iter()
|
||||
.any(|kept| s.ends_with(kept));
|
||||
slot_picture || (!s.contains("title/background") && !s.contains("presets"))
|
||||
let s = p.to_string_lossy();
|
||||
!s.contains("title/background") && !s.contains("presets")
|
||||
});
|
||||
// OptiFine random entity variants, under the names their scan gives them (those
|
||||
// outside `textures/` are `<ns>:optifine/random/...`).
|
||||
@@ -1297,87 +1214,61 @@ impl HudAtlas {
|
||||
extra.sort();
|
||||
files.extend(extra);
|
||||
}
|
||||
// A texture's LabPBR maps, as decoded (width, height, RGBA8), for shader packs.
|
||||
type Maps = [Option<(u32, u32, Vec<u8>)>; 2];
|
||||
let read_maps = full && crate::sprites::labpbr_maps();
|
||||
type Decoded = (
|
||||
PackedImage,
|
||||
Option<TextureFlags>,
|
||||
Option<(u32, u32)>,
|
||||
bool,
|
||||
Option<Box<Maps>>,
|
||||
);
|
||||
let decode_one =
|
||||
|path: &PathBuf| -> Option<Decoded> {
|
||||
let name = if let Some(name) = named.get(path) {
|
||||
name.clone()
|
||||
} else {
|
||||
match path.strip_prefix(&textures) {
|
||||
Ok(rel) => rel.with_extension("").to_string_lossy().replace('\\', "/"),
|
||||
Err(_) => {
|
||||
let rel = path.strip_prefix(&assets).ok()?.with_extension("");
|
||||
let rel = rel.to_string_lossy().replace('\\', "/");
|
||||
let (namespace, rest) = rel.split_once("/textures/")?;
|
||||
format!("{namespace}:{rest}")
|
||||
}
|
||||
type Decoded = (PackedImage, Option<TextureFlags>, Option<(u32, u32)>, bool);
|
||||
let decode_one = |path: &PathBuf| -> Option<Decoded> {
|
||||
let name = if let Some(name) = named.get(path) {
|
||||
name.clone()
|
||||
} else {
|
||||
match path.strip_prefix(&textures) {
|
||||
Ok(rel) => rel.with_extension("").to_string_lossy().replace('\\', "/"),
|
||||
Err(_) => {
|
||||
let rel = path.strip_prefix(&assets).ok()?.with_extension("");
|
||||
let rel = rel.to_string_lossy().replace('\\', "/");
|
||||
let (namespace, rest) = rel.split_once("/textures/")?;
|
||||
format!("{namespace}:{rest}")
|
||||
}
|
||||
};
|
||||
// LabPBR's maps beside a texture are the world's (sprites.rs), not images to draw.
|
||||
if let Some(base) = name.strip_suffix("_n").or_else(|| name.strip_suffix("_s"))
|
||||
&& textures.join(format!("{base}.png")).is_file()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let image = decode(path)
|
||||
.map_err(|e| log::warn!("skipped GUI texture {}: {e}", path.display()))
|
||||
.ok()?;
|
||||
let mcmeta = horizon_core::prepared_files::read_to_string(
|
||||
&path.with_extension("png.mcmeta"),
|
||||
)
|
||||
.ok();
|
||||
let animation = animation_of(mcmeta.as_deref());
|
||||
// An animated strip keeps its first frame.
|
||||
let frames =
|
||||
animation.is_some() || name.starts_with("block/") || name.starts_with("item/");
|
||||
let (w, h, pixels) = if frames {
|
||||
first_frame(image, animation.as_ref())
|
||||
} else {
|
||||
image
|
||||
};
|
||||
// The default texture's size, when a pack's differs from it.
|
||||
let gui_size = default_textures
|
||||
.as_ref()
|
||||
.and_then(|d| png_size(&d.join(format!("{name}.png"))))
|
||||
.map(|(dw, dh)| {
|
||||
let side = dw.min(dh);
|
||||
if frames { (side, side) } else { (dw, dh) }
|
||||
})
|
||||
.filter(|&size| size != (w, h));
|
||||
let flags = mcmeta.as_deref().and_then(TextureFlags::from_mcmeta);
|
||||
let animated = frames && animation.is_some();
|
||||
let (gw, gh) = gui_size.unwrap_or((w, h));
|
||||
let maps = read_maps
|
||||
.then(|| {
|
||||
let stem = path.file_stem()?.to_str()?;
|
||||
let maps: Maps = ["_n", "_s"].map(|suffix| {
|
||||
decode(&path.with_file_name(format!("{stem}{suffix}.png"))).ok()
|
||||
});
|
||||
maps.iter().any(Option::is_some).then(|| Box::new(maps))
|
||||
})
|
||||
.flatten();
|
||||
// The Realms world-slot pictures are 1024-tall GUI textures drawn as
|
||||
// they are; the cap that keeps other GUI textures small does not
|
||||
// apply to them.
|
||||
let tall_picture = matches!(
|
||||
name.as_str(),
|
||||
"gui/title/background/panorama_0"
|
||||
| "gui/title/background/panorama_2"
|
||||
| "gui/title/background/panorama_3"
|
||||
| "gui/presets/isles"
|
||||
);
|
||||
(gw <= 1024 && (gh <= 512 || tall_picture || name.starts_with("font/")))
|
||||
.then_some(((name, w, h, pixels), flags, gui_size, animated, maps))
|
||||
};
|
||||
// LabPBR's maps beside a texture are the world's (sprites.rs), not images to draw.
|
||||
if let Some(base) = name.strip_suffix("_n").or_else(|| name.strip_suffix("_s"))
|
||||
&& textures.join(format!("{base}.png")).is_file()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let image = decode(path)
|
||||
.map_err(|e| log::warn!("skipped GUI texture {}: {e}", path.display()))
|
||||
.ok()?;
|
||||
let mcmeta =
|
||||
horizon_core::prepared_files::read_to_string(&path.with_extension("png.mcmeta"))
|
||||
.ok();
|
||||
let animation = animation_of(mcmeta.as_deref());
|
||||
// An animated strip keeps its first frame.
|
||||
let frames =
|
||||
animation.is_some() || name.starts_with("block/") || name.starts_with("item/");
|
||||
let (w, h, pixels) = if frames {
|
||||
first_frame(image, animation.as_ref())
|
||||
} else {
|
||||
image
|
||||
};
|
||||
// The default texture's size, when a pack's differs from it.
|
||||
let gui_size = default_textures
|
||||
.as_ref()
|
||||
.and_then(|d| png_size(&d.join(format!("{name}.png"))))
|
||||
.map(|(dw, dh)| {
|
||||
let side = dw.min(dh);
|
||||
if frames { (side, side) } else { (dw, dh) }
|
||||
})
|
||||
.filter(|&size| size != (w, h));
|
||||
let flags = mcmeta.as_deref().and_then(TextureFlags::from_mcmeta);
|
||||
let animated = frames && animation.is_some();
|
||||
let (gw, gh) = gui_size.unwrap_or((w, h));
|
||||
(gw <= 1024 && (gh <= 512 || name.starts_with("font/"))).then_some((
|
||||
(name, w, h, pixels),
|
||||
flags,
|
||||
gui_size,
|
||||
animated,
|
||||
))
|
||||
};
|
||||
let threads = std::thread::available_parallelism().map_or(2, |n| n.get());
|
||||
let chunk = files.len().div_ceil(threads).max(1);
|
||||
let decoded: Vec<_> = std::thread::scope(|scope| {
|
||||
@@ -1396,11 +1287,7 @@ impl HudAtlas {
|
||||
let mut gui_sizes = HashMap::new();
|
||||
let mut animated: Vec<String> = Vec::new();
|
||||
let mut images: Vec<PackedImage> = Vec::with_capacity(decoded.len());
|
||||
let mut maps: HashMap<String, Box<Maps>> = HashMap::new();
|
||||
for (image, image_flags, gui_size, is_animated, image_maps) in decoded {
|
||||
if let Some(m) = image_maps {
|
||||
maps.insert(image.0.clone(), m);
|
||||
}
|
||||
for (image, image_flags, gui_size, is_animated) in decoded {
|
||||
if let Some(f) = image_flags {
|
||||
flags.insert(image.0.clone(), f);
|
||||
}
|
||||
@@ -1575,34 +1462,11 @@ impl HudAtlas {
|
||||
}
|
||||
sprite_animations.push((rect, animation));
|
||||
}
|
||||
// Each mapped texture's maps at its size in the atlas, paged.
|
||||
let material_maps = if maps.is_empty() {
|
||||
Default::default()
|
||||
} else {
|
||||
let mapped: Vec<crate::paged_maps::SpriteMaps> = maps
|
||||
.iter()
|
||||
.filter_map(|(name, m)| {
|
||||
let rect = sprites.get(name)?;
|
||||
let (tw, th) = rect.texels((width, height));
|
||||
let x = (rect.u0 * width as f32).round() as u32;
|
||||
let y = (rect.v0 * height as f32).round() as u32;
|
||||
Some(crate::paged_maps::SpriteMaps {
|
||||
rect: [x, y, tw, th],
|
||||
maps: m.each_ref().map(|map| {
|
||||
let (mw, mh, pixels) = map.as_ref()?;
|
||||
crate::paged_maps::fit(pixels, (*mw, *mh), (tw, th))
|
||||
}),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
crate::paged_maps::build((width, height), &mapped, MAX_ATLAS_SIZE)
|
||||
};
|
||||
let white = sprites["horizon/white"];
|
||||
let glint = Self::glint_texture(root, defaults, "enchanted_glint_item");
|
||||
let glint_armor = Self::glint_texture(root, defaults, "enchanted_glint_armor");
|
||||
let font = crate::font::Font::load_over(
|
||||
let font = crate::font::Font::load(
|
||||
root,
|
||||
defaults,
|
||||
&sprites,
|
||||
&pixels,
|
||||
(width, height),
|
||||
@@ -1611,7 +1475,6 @@ impl HudAtlas {
|
||||
);
|
||||
Ok(Self {
|
||||
revision: Self::next_revision(),
|
||||
material_maps: std::sync::Arc::new(material_maps),
|
||||
galactic_widths: galactic_widths(&sprites, &pixels, width, height),
|
||||
pixels,
|
||||
packed: None,
|
||||
@@ -1627,8 +1490,6 @@ impl HudAtlas {
|
||||
glint_armor,
|
||||
text_shader: None,
|
||||
lines_shader: None,
|
||||
sky_shader: None,
|
||||
stars_shader: None,
|
||||
random_textures,
|
||||
emissive,
|
||||
blinks,
|
||||
@@ -2518,19 +2379,17 @@ impl HudUniform {
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::gfx::hud_coverage::{self, Pipeline};
|
||||
|
||||
pub struct HudRenderer {
|
||||
pub sprite_animations: Vec<(Rect, crate::sprites::SpriteAnimation, (usize, usize, u32))>,
|
||||
/// The shared atlas texture, for run-time glyph uploads.
|
||||
pub atlas_texture: Option<wgpu::Texture>,
|
||||
pipeline: Pipeline,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
/// `RenderPipelines.CROSSHAIR`.
|
||||
invert_pipeline: Pipeline,
|
||||
additive_pipeline: Pipeline,
|
||||
depth_pipeline: Pipeline,
|
||||
vignette_pipeline: Pipeline,
|
||||
end_portal_pipeline: Pipeline,
|
||||
invert_pipeline: wgpu::RenderPipeline,
|
||||
additive_pipeline: wgpu::RenderPipeline,
|
||||
depth_pipeline: wgpu::RenderPipeline,
|
||||
vignette_pipeline: wgpu::RenderPipeline,
|
||||
end_portal_pipeline: wgpu::RenderPipeline,
|
||||
/// The layout of `group`, which the CIT glint pipelines share.
|
||||
layout: wgpu::BindGroupLayout,
|
||||
group: wgpu::BindGroup,
|
||||
@@ -2540,7 +2399,6 @@ pub struct HudRenderer {
|
||||
buffer: wgpu::Buffer,
|
||||
capacity: u64,
|
||||
format: wgpu::TextureFormat,
|
||||
coverage: bool,
|
||||
atlas_view: wgpu::TextureView,
|
||||
/// The resource pack's text shader, when it has one that works.
|
||||
pack_text: Option<pack_text::PackText>,
|
||||
@@ -2555,7 +2413,6 @@ impl HudRenderer {
|
||||
view: &wgpu::TextureView,
|
||||
glint_view: &wgpu::TextureView,
|
||||
glint_sampler: &wgpu::Sampler,
|
||||
coverage: bool,
|
||||
) -> Result<Self> {
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Horizon HUD"),
|
||||
@@ -2668,7 +2525,7 @@ impl HudRenderer {
|
||||
let attributes =
|
||||
wgpu::vertex_attr_array![0=>Float32x2,1=>Float32x2,2=>Unorm8x4,3=>Float32,4=>Float32x2];
|
||||
let create = |label, blend, fragment_entry, depth: bool| {
|
||||
let desc = wgpu::RenderPipelineDescriptor {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
@@ -2702,9 +2559,7 @@ impl HudRenderer {
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
};
|
||||
let source = wgpu::ShaderSource::Wgsl(include_str!("hud.wgsl").into());
|
||||
Pipeline::new(device, &desc, source, coverage).expect("the HUD's coverage")
|
||||
})
|
||||
};
|
||||
let pipeline = create("HUD", wgpu::BlendState::ALPHA_BLENDING, "fs_main", false);
|
||||
let depth_pipeline = create(
|
||||
@@ -2795,7 +2650,6 @@ impl HudRenderer {
|
||||
buffer,
|
||||
capacity,
|
||||
format,
|
||||
coverage,
|
||||
atlas_view: view.clone(),
|
||||
pack_text: None,
|
||||
zoom: 1.0,
|
||||
@@ -2810,7 +2664,7 @@ impl HudRenderer {
|
||||
textures: Option<&cit_glint::GlintTextures>,
|
||||
) {
|
||||
self.cit_glint = textures.and_then(|t| {
|
||||
cit_glint::CitGlintPass::new(device, queue, self.format, &self.layout, t, self.coverage)
|
||||
cit_glint::CitGlintPass::new(device, queue, self.format, &self.layout, t)
|
||||
});
|
||||
}
|
||||
/// Draw the GUI's text with `shader` (`HudAtlas::text_shader`), or natively.
|
||||
@@ -2820,7 +2674,7 @@ impl HudRenderer {
|
||||
shader: Option<&horizon_shaders::CoreProgram>,
|
||||
) {
|
||||
self.pack_text = shader.and_then(|shader| {
|
||||
pack_text::PackText::new(device, self.format, &self.atlas_view, shader, self.coverage)
|
||||
pack_text::PackText::new(device, self.format, &self.atlas_view, shader)
|
||||
});
|
||||
}
|
||||
pub fn animate_sprites(
|
||||
@@ -2969,7 +2823,6 @@ impl HudRenderer {
|
||||
self.draw_range(
|
||||
encoder,
|
||||
target,
|
||||
None,
|
||||
depth,
|
||||
batch,
|
||||
size,
|
||||
@@ -2983,7 +2836,6 @@ impl HudRenderer {
|
||||
&self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
target: &wgpu::TextureView,
|
||||
coverage: Option<&wgpu::TextureView>,
|
||||
depth: &wgpu::TextureView,
|
||||
batch: &HudBatch,
|
||||
size: [u32; 2],
|
||||
@@ -2993,7 +2845,7 @@ impl HudRenderer {
|
||||
if vertices.is_empty() || size.contains(&0) {
|
||||
return;
|
||||
}
|
||||
let mut attachments = vec![Some(wgpu::RenderPassColorAttachment {
|
||||
let attachments = [Some(wgpu::RenderPassColorAttachment {
|
||||
view: target,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
@@ -3002,9 +2854,6 @@ impl HudRenderer {
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})];
|
||||
if let Some(view) = coverage {
|
||||
attachments.push(Some(hud_coverage::attachment(view)));
|
||||
}
|
||||
let runs = batch.runs_in(vertices);
|
||||
let mut at = 0;
|
||||
while at < runs.len() {
|
||||
@@ -3056,17 +2905,16 @@ impl HudRenderer {
|
||||
Blend::EndPortal => &self.end_portal_pipeline,
|
||||
Blend::CitGlint(blend) => {
|
||||
if let Some(cit) = &self.cit_glint {
|
||||
cit.draw(&mut pass, *blend, range.clone(), coverage.is_some());
|
||||
cit.draw(&mut pass, *blend, range.clone());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let pipeline = pipeline.get(coverage.is_some());
|
||||
match &self.pack_text {
|
||||
Some(pack_text) if *blend == Blend::Alpha => {
|
||||
for (part, text) in pack_text::text_runs(&batch.vertices, range.clone()) {
|
||||
if text {
|
||||
pack_text.draw(&mut pass, part, coverage.is_some());
|
||||
pack_text.draw(&mut pass, part);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
} else {
|
||||
pass.set_pipeline(pipeline);
|
||||
@@ -3085,10 +2933,6 @@ impl HudRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "hud/coverage_tests.rs"]
|
||||
mod coverage_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -4047,30 +3891,6 @@ texture=wide",
|
||||
assert_eq!(((r.w, r.h), r.texels(size)), (gui, texels), "{name}");
|
||||
}
|
||||
}
|
||||
/// A bitmap font's `file` may be any texture, not only one under
|
||||
/// `textures/font`; the atlas packs it so the font can read its glyphs
|
||||
/// from there instead of copying them into the run-time region.
|
||||
#[test]
|
||||
fn a_bitmap_font_sheet_outside_font_is_packed() {
|
||||
let root = std::env::temp_dir().join(format!("horizon-font-pack-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
write_png(&root, "hud/sheet", 16, 8);
|
||||
let def = root.join("assets/minecraft/font/default.json");
|
||||
fs::create_dir_all(def.parent().unwrap()).unwrap();
|
||||
fs::write(
|
||||
&def,
|
||||
r#"{"providers":[{"type":"bitmap","file":"minecraft:hud/sheet.png","ascent":7,"chars":["ab"]}]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let atlas = HudAtlas::build(&root).unwrap();
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
let rect = atlas
|
||||
.sprites
|
||||
.get("hud/sheet")
|
||||
.expect("the font sheet is packed");
|
||||
assert_eq!(rect.texels((atlas.width, atlas.height)), (16, 8));
|
||||
}
|
||||
|
||||
/// A content mod's GUI textures join the atlas under its namespace;
|
||||
/// its other textures and vanilla's names stay as they were.
|
||||
#[test]
|
||||
@@ -4190,52 +4010,6 @@ texture=wide",
|
||||
.unwrap();
|
||||
root
|
||||
}
|
||||
/// A pack layer that adds a font provider over the game's own resources:
|
||||
/// its `font/default.json` names `minecraft:font/test.png`, which only the
|
||||
/// game's root holds (`HudAtlas::build_over`'s `defaults`).
|
||||
fn pack_font_over_defaults(name: &str) -> (PathBuf, PathBuf) {
|
||||
let base = std::env::temp_dir().join(format!(
|
||||
"horizon-font-fallback-{name}-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let pack = base.join("pack");
|
||||
let vanilla = base.join("vanilla");
|
||||
fs::create_dir_all(pack.join("assets/minecraft/font")).unwrap();
|
||||
fs::create_dir_all(vanilla.join("assets/minecraft/textures/font")).unwrap();
|
||||
fs::write(
|
||||
pack.join("assets/minecraft/font/default.json"),
|
||||
r#"{"providers":[{"type":"bitmap","file":"minecraft:font/test.png","ascent":7,"chars":["ab"]}]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let mut pixels = vec![0u8; 16 * 8 * 4];
|
||||
for y in 0..8 {
|
||||
for x in (0..3).chain(8..13) {
|
||||
pixels[(y * 16 + x) * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
let mut encoder = png::Encoder::new(
|
||||
fs::File::create(vanilla.join("assets/minecraft/textures/font/test.png")).unwrap(),
|
||||
16,
|
||||
8,
|
||||
);
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
encoder
|
||||
.write_header()
|
||||
.unwrap()
|
||||
.write_image_data(&pixels)
|
||||
.unwrap();
|
||||
(pack, vanilla)
|
||||
}
|
||||
#[test]
|
||||
fn a_pack_overlay_reads_font_sheets_from_the_game_below_it() {
|
||||
let (pack, vanilla) = pack_font_over_defaults("sheets");
|
||||
let atlas = HudAtlas::build_over(&pack, &vanilla).unwrap();
|
||||
let glyph = atlas.font.glyph('a');
|
||||
let _ = fs::remove_dir_all(pack.parent().unwrap());
|
||||
assert!(glyph.rect.is_some(), "text draws, not the missing glyph");
|
||||
assert!(glyph.advance > 0.0);
|
||||
}
|
||||
#[test]
|
||||
fn the_fonts_only_atlas_measures_text_like_the_full_one() {
|
||||
let root = two_glyph_font("atlas");
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
//! vanilla's glint's is. Without enchantment rules nothing here is made.
|
||||
|
||||
use super::HudVertex;
|
||||
use crate::gfx::hud_coverage::Pipeline;
|
||||
use horizon_optifine::cit::{Blend, Factor, Kind, Rule};
|
||||
use std::path::Path;
|
||||
|
||||
@@ -231,7 +230,7 @@ pub fn blend_state(blend: Option<Blend>) -> wgpu::BlendState {
|
||||
/// pipeline per blend.
|
||||
pub(super) struct CitGlintPass {
|
||||
group: wgpu::BindGroup,
|
||||
pipelines: Vec<Pipeline>,
|
||||
pipelines: Vec<wgpu::RenderPipeline>,
|
||||
}
|
||||
|
||||
impl CitGlintPass {
|
||||
@@ -242,7 +241,6 @@ impl CitGlintPass {
|
||||
format: wgpu::TextureFormat,
|
||||
hud: &wgpu::BindGroupLayout,
|
||||
textures: &GlintTextures,
|
||||
coverage: bool,
|
||||
) -> Option<Self> {
|
||||
use wgpu::util::DeviceExt;
|
||||
if textures.is_empty() {
|
||||
@@ -383,7 +381,7 @@ impl CitGlintPass {
|
||||
.blends
|
||||
.iter()
|
||||
.map(|&blend| {
|
||||
let desc = wgpu::RenderPipelineDescriptor {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("CIT glint"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
@@ -411,9 +409,7 @@ impl CitGlintPass {
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
};
|
||||
let source = wgpu::ShaderSource::Wgsl(include_str!("../hud.wgsl").into());
|
||||
Pipeline::new(device, &desc, source, coverage).expect("the CIT glint's coverage")
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Some(Self { group, pipelines })
|
||||
@@ -425,10 +421,9 @@ impl CitGlintPass {
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
blend: u8,
|
||||
vertices: std::ops::Range<u32>,
|
||||
coverage: bool,
|
||||
) {
|
||||
if let Some(pipeline) = self.pipelines.get(usize::from(blend)) {
|
||||
pass.set_pipeline(pipeline.get(coverage));
|
||||
pass.set_pipeline(pipeline);
|
||||
pass.set_bind_group(1, &self.group, &[]);
|
||||
pass.draw(vertices, 0..1);
|
||||
}
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
use super::*;
|
||||
use crate::gfx::hud_coverage;
|
||||
|
||||
struct Gpu {
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
hud: HudRenderer,
|
||||
}
|
||||
|
||||
impl Gpu {
|
||||
fn new() -> Option<Self> {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
|
||||
if !hud_coverage::supported(
|
||||
adapter.limits().max_color_attachments,
|
||||
adapter.get_downlevel_capabilities().flags,
|
||||
adapter.get_texture_format_features(hud_coverage::FORMAT),
|
||||
) {
|
||||
eprintln!("skipping HUD coverage: adapter lacks independent blending or R8 rendering");
|
||||
return None;
|
||||
}
|
||||
let (device, queue) =
|
||||
pollster::block_on(adapter.request_device(&Default::default())).ok()?;
|
||||
let atlas = device.create_texture_with_data(
|
||||
&queue,
|
||||
&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: 2,
|
||||
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,
|
||||
&[255, 255, 255, 255, 255, 255, 255, 0],
|
||||
);
|
||||
let view = atlas.create_view(&Default::default());
|
||||
let hud = HudRenderer::new(
|
||||
&device,
|
||||
wgpu::TextureFormat::Rgba16Float,
|
||||
&view,
|
||||
&view,
|
||||
&device.create_sampler(&Default::default()),
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
Some(Self { device, queue, hud })
|
||||
}
|
||||
|
||||
fn texture(&self, format: wgpu::TextureFormat) -> wgpu::Texture {
|
||||
self.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: 4,
|
||||
height: 2,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
|
||||
fn draw(&self, batch: &HudBatch, initial: f64, coverage: bool) -> (Vec<u8>, Vec<u8>) {
|
||||
let frame = self.texture(wgpu::TextureFormat::Rgba16Float);
|
||||
let mask = self.texture(hud_coverage::FORMAT);
|
||||
let depth = self.texture(wgpu::TextureFormat::Depth32Float);
|
||||
let [frame_view, mask_view, depth_view] =
|
||||
[&frame, &mask, &depth].map(|t| t.create_view(&Default::default()));
|
||||
self.queue
|
||||
.write_buffer(&self.hud.buffer, 0, bytemuck::cast_slice(&batch.vertices));
|
||||
self.queue.write_buffer(
|
||||
&self.hud.uniform,
|
||||
0,
|
||||
bytemuck::bytes_of(&HudUniform::new([4.0, 2.0, 1.0, 0.75], batch.end_portal, 0)),
|
||||
);
|
||||
let mut encoder = self.device.create_command_encoder(&Default::default());
|
||||
for (view, value) in [(&frame_view, 0.3), (&mask_view, initial)] {
|
||||
let mut attachment = hud_coverage::attachment(view);
|
||||
attachment.ops.load = wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: value,
|
||||
g: value,
|
||||
b: value,
|
||||
a: value,
|
||||
});
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(attachment)],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
self.hud.draw_range(
|
||||
&mut encoder,
|
||||
&frame_view,
|
||||
coverage.then_some(&mask_view),
|
||||
&depth_view,
|
||||
batch,
|
||||
[4, 2],
|
||||
0..batch.vertices.len() as u32,
|
||||
None,
|
||||
);
|
||||
let buffers = [&frame, &mask].map(|t| {
|
||||
let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: 512,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
encoder.copy_texture_to_buffer(
|
||||
t.as_image_copy(),
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buffer,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(256),
|
||||
rows_per_image: None,
|
||||
},
|
||||
},
|
||||
t.size(),
|
||||
);
|
||||
buffer
|
||||
});
|
||||
self.queue.submit([encoder.finish()]);
|
||||
let [color, mask] = buffers.map(|buffer| {
|
||||
buffer.map_async(wgpu::MapMode::Read, .., |r| r.unwrap());
|
||||
self.device
|
||||
.poll(wgpu::PollType::wait_indefinitely())
|
||||
.unwrap();
|
||||
buffer.get_mapped_range(..).unwrap().to_vec()
|
||||
});
|
||||
(
|
||||
color
|
||||
.chunks(256)
|
||||
.flat_map(|r| r[..32].iter().copied())
|
||||
.collect(),
|
||||
mask.chunks(256)
|
||||
.flat_map(|r| r[..4].iter().copied())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn check(&self, batch: &HudBatch, initial: f64, expected: [u8; 4]) {
|
||||
let (plain, _) = self.draw(batch, initial, false);
|
||||
let (color, mask) = self.draw(batch, initial, true);
|
||||
assert_eq!(color, plain, "coverage changed the primary colour or alpha");
|
||||
for (got, want) in mask.iter().zip(expected.repeat(2)) {
|
||||
assert!(
|
||||
got.abs_diff(want) <= 1,
|
||||
"coverage {mask:?}, expected {expected:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn quad(batch: &mut HudBatch, blend: Blend, color: u32, x: f32, uv: f32) {
|
||||
let start = batch.vertices.len();
|
||||
batch.quad(
|
||||
[[x, 0.0], [x + 2.0, 0.0], [x + 2.0, 2.0], [x, 2.0]],
|
||||
[[uv, 0.5]; 4],
|
||||
color,
|
||||
);
|
||||
batch.blend_since(start, blend);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_follows_hud_blends_discard_scissor_and_depth_without_changing_colour() {
|
||||
let Some(gpu) = Gpu::new() else {
|
||||
return;
|
||||
};
|
||||
for (blend, color, initial, count, expected) in [
|
||||
(Blend::Alpha, rgba(255, 255, 255, 128), 0.0, 2, 192),
|
||||
(Blend::Additive, rgba(255, 255, 255, 128), 0.3, 1, 77),
|
||||
(Blend::Invert, rgba(255, 255, 255, 32), 0.0, 1, 255),
|
||||
(Blend::EndPortal, rgba(255, 255, 255, 255), 0.0, 1, 255),
|
||||
(Blend::Vignette, rgba(102, 102, 102, 255), 0.0, 1, 102),
|
||||
] {
|
||||
let mut batch = HudBatch::new([4, 2]);
|
||||
for _ in 0..count {
|
||||
quad(&mut batch, blend, color, 0.0, 0.25);
|
||||
}
|
||||
// Crosshair fragments with zero texture alpha discard; EndPortal has no discard.
|
||||
if blend == Blend::Invert {
|
||||
quad(&mut batch, blend, color, 2.0, 0.75);
|
||||
}
|
||||
gpu.check(
|
||||
&batch,
|
||||
initial,
|
||||
[
|
||||
expected,
|
||||
expected,
|
||||
(initial * 255.0).round() as u8,
|
||||
(initial * 255.0).round() as u8,
|
||||
],
|
||||
);
|
||||
}
|
||||
let mut batch = HudBatch::new([4, 2]);
|
||||
for depth in [0.25, 0.75] {
|
||||
quad(
|
||||
&mut batch,
|
||||
Blend::Alpha,
|
||||
rgba(255, 255, 255, 128),
|
||||
0.0,
|
||||
0.25,
|
||||
);
|
||||
let start = batch.vertices.len() - 6;
|
||||
for vertex in &mut batch.vertices[start..] {
|
||||
vertex.depth = depth;
|
||||
}
|
||||
}
|
||||
batch.blends.push((0..12, Blend::Depth([1, 0, 2, 2])));
|
||||
gpu.check(&batch, 0.0, [0, 128, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_preserves_each_cit_blend_and_its_destination_alpha() {
|
||||
let Some(mut gpu) = Gpu::new() else {
|
||||
return;
|
||||
};
|
||||
use horizon_optifine::cit::parse_blend;
|
||||
for (name, expected) in [
|
||||
("alpha", 128),
|
||||
("add", 0),
|
||||
("dodge", 0),
|
||||
("glint", 0),
|
||||
("burn", 102),
|
||||
("multiply", 26),
|
||||
("overlay", 51),
|
||||
("screen", 255),
|
||||
("subtract", 255),
|
||||
("replace", 255),
|
||||
("0 768", 153),
|
||||
("774 0", 153),
|
||||
("770 771 772 773", 128),
|
||||
] {
|
||||
let textures = cit_glint::GlintTextures {
|
||||
width: 1,
|
||||
height: 1,
|
||||
pixels: vec![102, 102, 102, 128],
|
||||
slots: vec![cit_glint::Slot {
|
||||
rgba: [1.0; 4],
|
||||
blur: false,
|
||||
blend: 0,
|
||||
}],
|
||||
blends: vec![parse_blend(name).unwrap()],
|
||||
..Default::default()
|
||||
};
|
||||
gpu.hud
|
||||
.set_cit_glints(&gpu.device, &gpu.queue, Some(&textures));
|
||||
let mut batch = HudBatch::new([4, 2]);
|
||||
quad(
|
||||
&mut batch,
|
||||
Blend::CitGlint(0),
|
||||
rgba(0, 0, 0, 255),
|
||||
0.0,
|
||||
0.25,
|
||||
);
|
||||
quad(
|
||||
&mut batch,
|
||||
Blend::CitGlint(0),
|
||||
rgba(0, 0, 0, 255),
|
||||
2.0,
|
||||
0.75,
|
||||
);
|
||||
gpu.check(&batch, 0.0, [expected, expected, 0, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_uses_pack_texts_final_alpha_and_discard() {
|
||||
let Some(mut gpu) = Gpu::new() else {
|
||||
return;
|
||||
};
|
||||
// A translated-style struct result and early return; no dependency on game resources.
|
||||
let program = horizon_shaders::CoreProgram {
|
||||
vertex: naga::front::wgsl::parse_str("@vertex fn main(@location(0) p: vec2<f32>) -> @builtin(position) vec4<f32> { return vec4(p.x / 2.0 - 1.0, 1.0 - p.y, 0.0, 1.0); }").unwrap(),
|
||||
fragment: naga::front::wgsl::parse_str("struct Result { @location(0) color: vec4<f32> } @fragment fn main(@builtin(position) p: vec4<f32>) -> Result { if p.x > 2.0 { discard; } return Result(vec4(0.4, 0.2, 0.1, 0.5)); }").unwrap(),
|
||||
};
|
||||
gpu.hud.set_text_shader(&gpu.device, Some(&program));
|
||||
assert!(gpu.hud.pack_text.is_some());
|
||||
let mut batch = HudBatch::new([4, 2]);
|
||||
for x in [0.0, 2.0, 0.0, 2.0] {
|
||||
quad(&mut batch, Blend::Alpha, rgba(255, 255, 255, 255), x, 0.25);
|
||||
}
|
||||
for v in &mut batch.vertices {
|
||||
v.text = 1;
|
||||
}
|
||||
gpu.check(&batch, 0.0, [191, 191, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pack_text_shader_without_a_colour_output_is_dropped_not_wrapped() {
|
||||
let Some(mut gpu) = Gpu::new() else {
|
||||
return;
|
||||
};
|
||||
let program = horizon_shaders::CoreProgram {
|
||||
vertex: naga::front::wgsl::parse_str("@vertex fn main(@location(0) p: vec2<f32>) -> @builtin(position) vec4<f32> { return vec4(p, 0.0, 1.0); }").unwrap(),
|
||||
fragment: naga::front::wgsl::parse_str("@fragment fn main() -> @location(1) vec4<f32> { return vec4(1.0); }").unwrap(),
|
||||
};
|
||||
gpu.hud.set_text_shader(&gpu.device, Some(&program));
|
||||
assert!(gpu.hud.pack_text.is_none());
|
||||
}
|
||||
@@ -8,7 +8,6 @@ use std::sync::Arc;
|
||||
use horizon_shaders::{CoreProgram, Glue};
|
||||
|
||||
use super::{HudBatch, HudVertex};
|
||||
use crate::gfx::hud_coverage::Pipeline;
|
||||
|
||||
/// GUI text's vertex format (`POSITION_TEX_COLOR`) from the HUD's vertices. A glyph is two
|
||||
/// triangles here, its corners top left, top right, bottom right, top left, bottom right, bottom
|
||||
@@ -82,7 +81,7 @@ const BLOCK_SIZES: [u64; 4] = [48, 64, 160, 48];
|
||||
|
||||
/// The pipeline and its resources.
|
||||
pub(super) struct PackText {
|
||||
pipeline: Pipeline,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
group: wgpu::BindGroup,
|
||||
uniform: wgpu::Buffer,
|
||||
}
|
||||
@@ -93,7 +92,6 @@ impl PackText {
|
||||
format: wgpu::TextureFormat,
|
||||
atlas: &wgpu::TextureView,
|
||||
program: &CoreProgram,
|
||||
coverage: bool,
|
||||
) -> Option<Self> {
|
||||
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let module = |label, module: &naga::Module| {
|
||||
@@ -181,7 +179,7 @@ impl PackText {
|
||||
..Default::default()
|
||||
});
|
||||
let attributes = wgpu::vertex_attr_array![0=>Float32x2,1=>Float32x2,2=>Unorm8x4];
|
||||
let desc = wgpu::RenderPipelineDescriptor {
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Resource pack text"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
@@ -210,17 +208,11 @@ impl PackText {
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
};
|
||||
let source = wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(program.fragment.clone()));
|
||||
let pipeline = Pipeline::new(device, &desc, source, coverage);
|
||||
});
|
||||
if let Some(e) = pollster::block_on(scope.pop()) {
|
||||
log::warn!("the resource pack's text shader is not used: {e}");
|
||||
return None;
|
||||
}
|
||||
let Some(pipeline) = pipeline else {
|
||||
log::warn!("the resource pack's text shader is not used: it has no colour output");
|
||||
return None;
|
||||
};
|
||||
Some(Self {
|
||||
pipeline,
|
||||
group,
|
||||
@@ -242,13 +234,8 @@ impl PackText {
|
||||
|
||||
/// Draw `range` (whole glyph quads) with the pack's shader; the caller sets its own
|
||||
/// pipeline and bind group again after.
|
||||
pub(super) fn draw(
|
||||
&self,
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
range: std::ops::Range<u32>,
|
||||
coverage: bool,
|
||||
) {
|
||||
pass.set_pipeline(self.pipeline.get(coverage));
|
||||
pub(super) fn draw(&self, pass: &mut wgpu::RenderPass<'_>, range: std::ops::Range<u32>) {
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
pass.draw(range.clone(), range.start..range.start + 1);
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
//! The HUD kit: small immediate-mode pieces Horizon's own overlays share, so they fit around
|
||||
//! vanilla's HUD and look alike (`docs/ui-kit.md`). Nothing here reaches gameplay; the game
|
||||
//! builds the inputs (`Game::hud_room`) and the overlays draw with them.
|
||||
//!
|
||||
//! - `room`: what of a view an overlay may use, clear of the boss bars.
|
||||
//! - `plate`: the box behind a notice's text.
|
||||
//!
|
||||
//! Vanilla's own display lists (`hud_layout`, `screen_draw`) never use the kit: they stay
|
||||
//! exactly vanilla's.
|
||||
pub mod plate;
|
||||
pub mod room;
|
||||
@@ -1,64 +0,0 @@
|
||||
//! Plates: the boxes Horizon's HUD overlays draw behind their text, one place for each look so
|
||||
//! overlays that mean the same thing look the same.
|
||||
//!
|
||||
//! A plate is drawn first, then the overlay's text and glyphs over it. Only the kinds the
|
||||
//! overlays use today are here; each keeps the colours its overlay always had. Today that is
|
||||
//! one, shared by the controller notice and splitscreen's prompts.
|
||||
//!
|
||||
//! ```
|
||||
//! use horizon_client_render::hud_kit::plate::Plate;
|
||||
//!
|
||||
//! // A notice's box: a dark fill and a faint 1 px edge inside its area.
|
||||
//! assert_eq!(Plate::Notice.fill(), 0xD200_0000);
|
||||
//! assert_eq!(Plate::Notice.edge(), Some(0x5AFF_FFFF));
|
||||
//! ```
|
||||
//!
|
||||
//! In a draw: `plate::draw(batch, atlas, Plate::Notice, [x, y, w, h])`, the area in GUI px.
|
||||
|
||||
use crate::hud::{HudAtlas, HudBatch, rgba};
|
||||
|
||||
/// A plate's look.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Plate {
|
||||
/// Something to read now: controller notices and splitscreen's prompts. A dark fill with a
|
||||
/// faint edge.
|
||||
Notice,
|
||||
}
|
||||
|
||||
impl Plate {
|
||||
/// The fill colour as `0xAARRGGBB`.
|
||||
pub fn fill(self) -> u32 {
|
||||
match self {
|
||||
Self::Notice => 0xD200_0000,
|
||||
}
|
||||
}
|
||||
|
||||
/// The 1 GUI px edge's colour as `0xAARRGGBB`, drawn inside the area; `None` for none.
|
||||
pub fn edge(self) -> Option<u32> {
|
||||
match self {
|
||||
Self::Notice => Some(0x5AFF_FFFF),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `[a, r, g, b]` of `0xAARRGGBB` as the batch's colour.
|
||||
fn colour(argb: u32) -> u32 {
|
||||
let [a, r, g, b] = argb.to_be_bytes();
|
||||
rgba(r, g, b, a)
|
||||
}
|
||||
|
||||
/// Draws `plate` over `[x, y, w, h]` (GUI px): its fill, then its edge inside the area.
|
||||
pub fn draw(batch: &mut HudBatch, atlas: &HudAtlas, plate: Plate, [x, y, w, h]: [f32; 4]) {
|
||||
batch.fill(atlas, x, y, w, h, colour(plate.fill()));
|
||||
if let Some(edge) = plate.edge() {
|
||||
let edge = colour(edge);
|
||||
for [ex, ey, ew, eh] in [
|
||||
[x, y, w, 1.0],
|
||||
[x, y + h - 1.0, w, 1.0],
|
||||
[x, y, 1.0, h],
|
||||
[x + w - 1.0, y, 1.0, h],
|
||||
] {
|
||||
batch.fill(atlas, ex, ey, ew, eh, edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
//! Room: what of a view Horizon's own HUD overlays may use. Vanilla's HUD elements are
|
||||
//! keep-outs; an overlay asks the room where it fits instead of assuming a fixed spot, so it
|
||||
//! doesn't cover them. Today the only keep-out is the boss bars at the top centre.
|
||||
//!
|
||||
//! The room is built once a frame from what the HUD shows (`Game::hud_room` in gameplay). With
|
||||
//! nothing showing it is empty, and every overlay stays exactly where it always was.
|
||||
//!
|
||||
//! All sizes are GUI px, from the view's top left. An area is `[x, y, w, h]`.
|
||||
//!
|
||||
//! ```
|
||||
//! use horizon_client_render::hud_kit::room::{Keep, Room};
|
||||
//! use horizon_client_render::hud_overlays::boss_bars_bottom;
|
||||
//!
|
||||
//! // A centred notice wants y = 18.
|
||||
//! let empty = Room::new(320.0, 240.0);
|
||||
//! assert_eq!(empty.below_top(18.0), 18.0);
|
||||
//! // One boss bar ends at y 17: no overlap, so the notice stays.
|
||||
//! let one = Room::new(320.0, 240.0).with(Keep::BossBars, [69.0, 0.0, 182.0, 17.0]);
|
||||
//! assert_eq!(one.below_top(18.0), 18.0);
|
||||
//! // Two end at y 36: the notice goes below them, a gap past.
|
||||
//! let bars = boss_bars_bottom(240, 2) as f32;
|
||||
//! let two = Room::new(320.0, 240.0).with(Keep::BossBars, [69.0, 0.0, 182.0, bars]);
|
||||
//! assert_eq!(two.below_top(18.0), bars + 2.0);
|
||||
//! ```
|
||||
|
||||
/// An area in GUI px: `[x, y, w, h]` from the view's top left.
|
||||
pub type Area = [f32; 4];
|
||||
|
||||
/// The gap, in GUI px, an overlay leaves below a keep-out it moved past.
|
||||
pub const GAP: f32 = 2.0;
|
||||
|
||||
/// A HUD element Horizon's overlays keep clear of.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Keep {
|
||||
/// The boss bars and their names at the top centre (`hud_overlays::boss_bars_bottom`).
|
||||
BossBars,
|
||||
}
|
||||
|
||||
/// A view and the keep-outs its HUD shows this frame.
|
||||
///
|
||||
/// Invariants: the view's size is what the overlays draw in (the `HudBatch`'s size), and an
|
||||
/// empty keep-out (no width or height) is never stored, so a room built from hidden elements
|
||||
/// equals `Room::new`.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Room {
|
||||
width: f32,
|
||||
height: f32,
|
||||
keeps: Vec<(Keep, Area)>,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
/// A `width` x `height` GUI px view with nothing to keep clear of.
|
||||
pub fn new(width: f32, height: f32) -> Self {
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
keeps: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// This room with `area` kept clear of as `keep`; an empty area is ignored.
|
||||
pub fn with(mut self, keep: Keep, area: Area) -> Self {
|
||||
self.add(keep, area);
|
||||
self
|
||||
}
|
||||
|
||||
/// Keeps clear of `area` as `keep`; an empty area is ignored.
|
||||
pub fn add(&mut self, keep: Keep, area: Area) {
|
||||
if area[2] > 0.0 && area[3] > 0.0 {
|
||||
self.keeps.push((keep, area));
|
||||
}
|
||||
}
|
||||
|
||||
/// The view's width in GUI px.
|
||||
pub fn width(&self) -> f32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
/// The view's height in GUI px.
|
||||
pub fn height(&self) -> f32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
/// The keep-outs, in the order they were added.
|
||||
pub fn keeps(&self) -> impl Iterator<Item = (Keep, Area)> + '_ {
|
||||
self.keeps.iter().copied()
|
||||
}
|
||||
|
||||
/// How far, in GUI px from the view's top, the keep-outs at the top reach down: the boss
|
||||
/// bars' bottom, or 0 with none.
|
||||
pub fn top(&self) -> f32 {
|
||||
self.keeps()
|
||||
.map(|(Keep::BossBars, [_, y, _, h])| y + h)
|
||||
.fold(0.0, f32::max)
|
||||
}
|
||||
|
||||
/// Where a centred overlay that wants its top at `wanted` (GUI px) goes: at `wanted` unless
|
||||
/// a keep-out reaches past it (`top() > wanted`, a real overlap; touching isn't one), else
|
||||
/// `GAP` below that keep-out.
|
||||
pub fn below_top(&self, wanted: f32) -> f32 {
|
||||
let top = self.top();
|
||||
if top > wanted { top + GAP } else { wanted }
|
||||
}
|
||||
|
||||
/// The view below the top keep-outs, with `GAP` past them: where a centred overlay may go.
|
||||
/// The whole view when nothing is kept clear of.
|
||||
pub fn usable(&self) -> Area {
|
||||
let top = self.top();
|
||||
let top = if top > 0.0 { top + GAP } else { 0.0 };
|
||||
[0.0, top, self.width, (self.height - top).max(0.0)]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn an_empty_room_is_the_whole_view_and_moves_nothing() {
|
||||
let room = Room::new(320.0, 240.0).with(Keep::BossBars, [69.0, 0.0, 182.0, 0.0]);
|
||||
assert_eq!(room, Room::new(320.0, 240.0));
|
||||
assert_eq!(room.usable(), [0.0, 0.0, 320.0, 240.0]);
|
||||
assert_eq!(room.below_top(18.0), 18.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_overlay_moves_only_when_the_bars_reach_past_it() {
|
||||
let bars =
|
||||
|bottom: f32| Room::new(500.0, 300.0).with(Keep::BossBars, [159.0, 0.0, 182.0, bottom]);
|
||||
// Ending above or at its top: no overlap.
|
||||
assert_eq!(bars(17.0).below_top(18.0), 18.0);
|
||||
assert_eq!(bars(18.0).below_top(18.0), 18.0);
|
||||
// Past it: below, a gap away.
|
||||
assert_eq!(bars(36.0).below_top(18.0), 38.0);
|
||||
assert_eq!(bars(36.0).usable(), [0.0, 38.0, 500.0, 262.0]);
|
||||
}
|
||||
}
|
||||
@@ -571,41 +571,6 @@ pub fn boss_overlay(w: i32, h: i32, bars: &[BossBar], font: &Font) -> Vec<GuiDra
|
||||
out
|
||||
}
|
||||
|
||||
/// `BossHealthOverlay`'s first bar's top edge, in GUI px (its name sits 9 px above).
|
||||
pub const BOSS_BAR_TOP: i32 = 12;
|
||||
/// `BossHealthOverlay`'s step from one bar to the next, in GUI px (`10 + 9`).
|
||||
pub const BOSS_BAR_STEP: i32 = 10 + 9;
|
||||
/// A boss bar's sprite height, in GUI px.
|
||||
pub const BOSS_BAR_HEIGHT: i32 = 5;
|
||||
/// A boss bar's sprite width, in GUI px; it is centred on the view.
|
||||
pub const BOSS_BAR_WIDTH: i32 = 182;
|
||||
|
||||
/// How many of `n` boss bars `BossHealthOverlay.extractRenderState` draws on a view `h` GUI px
|
||||
/// tall: it stops once the next bar's top reaches a third of the height (integer division,
|
||||
/// as vanilla's `guiHeight() / 3`), but always draws the first.
|
||||
pub fn boss_bars_drawn(h: i32, n: usize) -> usize {
|
||||
let mut drawn = 0;
|
||||
let mut y = BOSS_BAR_TOP;
|
||||
while drawn < n {
|
||||
drawn += 1;
|
||||
y += BOSS_BAR_STEP;
|
||||
if y >= h / 3 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
drawn
|
||||
}
|
||||
|
||||
/// The bottom edge, in GUI px from the view's top, of the last of `n` boss bars a view `h` GUI
|
||||
/// px tall shows (`boss_overlay`'s layout); 0 when there are none. Names sit above their bar,
|
||||
/// so nothing of the overlay reaches below this.
|
||||
pub fn boss_bars_bottom(h: i32, n: usize) -> i32 {
|
||||
match boss_bars_drawn(h, n) {
|
||||
0 => 0,
|
||||
k => BOSS_BAR_TOP + BOSS_BAR_STEP * (k as i32 - 1) + BOSS_BAR_HEIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
/// `JumpableVehicleBar.extractBackground`: `cooldown` is the mount's jump
|
||||
/// cooldown and `scale` the player's `getJumpRidingScale()`.
|
||||
pub fn jump_bar(w: i32, h: i32, cooldown: i32, scale: f32) -> Vec<GuiDraw> {
|
||||
@@ -1416,64 +1381,3 @@ mod text_position_tests {
|
||||
assert_eq!(relative, Some(-4));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod boss_bar_room_tests {
|
||||
use super::*;
|
||||
|
||||
/// `BossHealthOverlay.extractRenderState` worked by hand: bars at y 12, 31, 50, 69, 88...,
|
||||
/// each 5 tall, until the next top reaches `guiHeight() / 3`.
|
||||
#[test]
|
||||
fn boss_bars_end_where_vanillas_overlay_stops() {
|
||||
assert_eq!(boss_bars_bottom(300, 0), 0);
|
||||
assert_eq!(boss_bars_bottom(300, 1), 17);
|
||||
assert_eq!(boss_bars_bottom(300, 2), 36);
|
||||
assert_eq!(boss_bars_bottom(300, 3), 55);
|
||||
// A third of 300 is 100: the fifth bar is the last (its next top would be 107).
|
||||
assert_eq!(boss_bars_drawn(300, 9), 5);
|
||||
assert_eq!(boss_bars_bottom(300, 9), 93);
|
||||
// A short view still draws the first.
|
||||
assert_eq!(boss_bars_bottom(60, 3), 17);
|
||||
// `guiHeight() / 3` is integer division: 152 / 3 = 50 stops at two, 153 / 3 = 51 not.
|
||||
assert_eq!(boss_bars_drawn(152, 3), 2);
|
||||
assert_eq!(boss_bars_drawn(153, 3), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boss_bars_bottom_is_below_every_drawn_bar() {
|
||||
// The missing-glyph metrics: no game assets needed.
|
||||
let font = Font::load(
|
||||
std::path::Path::new("/nonexistent-horizon-hud-test-resources"),
|
||||
&std::collections::HashMap::new(),
|
||||
&[],
|
||||
(1, 1),
|
||||
(0, 0),
|
||||
crate::hud::Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 1.0,
|
||||
v1: 1.0,
|
||||
w: 1,
|
||||
h: 1,
|
||||
},
|
||||
);
|
||||
let bars: Vec<BossBar> = (0..6)
|
||||
.map(|_| BossBar {
|
||||
name: Text::literal(""),
|
||||
progress: 1.0,
|
||||
color: 0,
|
||||
overlay: 0,
|
||||
})
|
||||
.collect();
|
||||
for h in [60, 120, 152, 153, 240, 300, 600] {
|
||||
let bottom = boss_overlay(400, h, &bars, &font)
|
||||
.iter()
|
||||
.filter_map(|d| match d {
|
||||
GuiDraw::Sprite { y, h, .. } => Some(y + h),
|
||||
_ => None,
|
||||
})
|
||||
.max();
|
||||
assert_eq!(bottom, Some(boss_bars_bottom(h, bars.len())), "height {h}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ pub mod assets;
|
||||
pub mod atlas_sources;
|
||||
pub mod audio;
|
||||
pub mod auth;
|
||||
pub mod autotune;
|
||||
pub mod ban_notice;
|
||||
pub mod bidi;
|
||||
pub mod biome;
|
||||
@@ -55,7 +54,6 @@ pub mod graphics_style;
|
||||
pub mod graphics_ui;
|
||||
pub mod hdr_calibration;
|
||||
pub mod hud;
|
||||
pub mod hud_kit;
|
||||
pub mod hud_layout;
|
||||
pub mod hud_overlays;
|
||||
pub mod inventory_effects;
|
||||
@@ -86,7 +84,6 @@ pub mod pack_screen;
|
||||
pub mod packs;
|
||||
pub mod pad_cursor;
|
||||
pub mod pad_glyph;
|
||||
pub mod paged_maps;
|
||||
pub mod par;
|
||||
pub mod particles;
|
||||
pub mod poi_sections;
|
||||
@@ -97,7 +94,6 @@ pub mod preedit;
|
||||
pub mod providers;
|
||||
pub mod pvp;
|
||||
pub mod pvp_hud;
|
||||
pub mod random_source;
|
||||
pub mod screen_draw;
|
||||
pub mod screen_effects;
|
||||
pub mod sculk;
|
||||
@@ -110,7 +106,6 @@ pub mod shader_pack_list;
|
||||
pub mod shader_ui;
|
||||
pub mod signs;
|
||||
pub mod skins;
|
||||
pub mod social;
|
||||
pub mod sound_engine;
|
||||
pub mod sound_events;
|
||||
pub mod sound_preview;
|
||||
@@ -130,7 +125,6 @@ pub mod test_block;
|
||||
pub mod test_instance_block;
|
||||
pub mod testgen;
|
||||
pub mod title_edition;
|
||||
pub mod touch;
|
||||
pub mod trims;
|
||||
pub mod ui;
|
||||
pub mod vault;
|
||||
|
||||
@@ -653,17 +653,15 @@ fn pack_lighting(
|
||||
sky_light: bool,
|
||||
) -> Vec<Light> {
|
||||
let open = if sky_light { 0xF0 } else { 0x00 };
|
||||
let mut data = [open; 4096];
|
||||
(0..sections)
|
||||
.map(|si| {
|
||||
if si * 16 >= h {
|
||||
return Light::Uniform(open);
|
||||
}
|
||||
let mut data = Box::new([open; 4096]);
|
||||
for ly in 0..16 {
|
||||
let y = si * 16 + ly;
|
||||
if y >= h {
|
||||
let out_row = ly << 8;
|
||||
data[out_row..out_row + 256].fill(open);
|
||||
continue;
|
||||
}
|
||||
let y_offset = y * (W * W);
|
||||
@@ -676,7 +674,7 @@ fn pack_lighting(
|
||||
}
|
||||
}
|
||||
}
|
||||
Light::from_slice(&data)
|
||||
Light::packed(data)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ use crate::{
|
||||
};
|
||||
pub mod cache;
|
||||
mod mesh;
|
||||
pub mod packed;
|
||||
mod samples;
|
||||
use horizon_world::registry::BlockStateId;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
@@ -561,8 +559,7 @@ pub(crate) fn state_kinds(facts: &BlockFacts) -> Vec<u8> {
|
||||
}
|
||||
/// Blocks that fill their cell but neither draw solid nor collide as a full
|
||||
/// block: far terrain draws them as blocks, or patches of powder snow leave
|
||||
/// holes in mountains (and paths and fields dips). Slabs use the same
|
||||
/// full-cell approximation as stairs, so distant slab builds remain visible.
|
||||
/// holes in mountains (and paths and fields dips).
|
||||
fn nearly_full(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
@@ -571,7 +568,6 @@ fn nearly_full(name: &str) -> bool {
|
||||
| "minecraft:farmland"
|
||||
| "minecraft:honey_block"
|
||||
) || name.ends_with("_stairs")
|
||||
|| name.ends_with("_slab")
|
||||
|| name.contains("fence")
|
||||
|| name.contains("_wall")
|
||||
|| name.contains("bars")
|
||||
@@ -958,7 +954,7 @@ type Reduced = FxHashMap<(i32, i32), Arc<ReducedColumns>>;
|
||||
/// every tile's inputs from it (gathering on the render thread stalled the
|
||||
/// frame, tens of milliseconds for a large job on a 4-thread machine).
|
||||
struct Snapshot {
|
||||
samples: samples::Samples,
|
||||
samples: FxHashMap<TileKey, Arc<Vec<Column>>>,
|
||||
saved: Saved,
|
||||
reduced: Reduced,
|
||||
/// The plan's tiles, to tell terrain not drawn yet from terrain out of
|
||||
@@ -1268,8 +1264,6 @@ pub struct Lod {
|
||||
/// Stale tiles `refresh`ed again while sampled: that sample may be older.
|
||||
restale: FxHashSet<TileKey>,
|
||||
active: FxHashSet<TileKey>,
|
||||
/// The active tiles' `ready` samples, as mesh jobs read them (`sync_sample`).
|
||||
mesh_samples: samples::Samples,
|
||||
/// Tiles needing a mesh, with the revision their job must carry.
|
||||
dirty: FxHashMap<TileKey, u64>,
|
||||
/// The newest revision of each active tile's mesh.
|
||||
@@ -1420,13 +1414,7 @@ impl Lod {
|
||||
&edges,
|
||||
&input.finer,
|
||||
);
|
||||
// Sorts the quads by facing side, and clears
|
||||
// the compact layout: a far tile has no tints
|
||||
// for `mesher::packed`, so its own records
|
||||
// (`lod::packed`) are laid out here, from the
|
||||
// quads in the order the sort left them.
|
||||
mesh.sort_solid();
|
||||
crate::lod::packed::lay_out(&mut mesh);
|
||||
let [ox, _, oz] = job.key.origin();
|
||||
let size = job.key.size();
|
||||
mesh.keep = keep_finer
|
||||
@@ -1660,7 +1648,6 @@ impl Lod {
|
||||
ready: FxHashMap::default(),
|
||||
complete: FxHashMap::default(),
|
||||
active: FxHashSet::default(),
|
||||
mesh_samples: samples::Samples::default(),
|
||||
dirty: FxHashMap::default(),
|
||||
mesh_version: FxHashMap::default(),
|
||||
shown_version: FxHashMap::default(),
|
||||
@@ -1939,11 +1926,9 @@ impl Lod {
|
||||
if active != was_active {
|
||||
if active {
|
||||
self.active.insert(node);
|
||||
self.sync_sample(node);
|
||||
self.removed_pending.remove(&node);
|
||||
} else {
|
||||
self.active.remove(&node);
|
||||
self.sync_sample(node);
|
||||
self.removed_pending.insert(node);
|
||||
self.cancel_mesh(node);
|
||||
self.mesh_version.remove(&node);
|
||||
@@ -2216,7 +2201,6 @@ impl Lod {
|
||||
}
|
||||
}
|
||||
for key in modified {
|
||||
self.sync_sample(key);
|
||||
self.mark_data_changed(key);
|
||||
}
|
||||
}
|
||||
@@ -2278,7 +2262,6 @@ impl Lod {
|
||||
return;
|
||||
}
|
||||
let existed = self.ready.insert(key, Arc::new(samples)).is_some();
|
||||
self.sync_sample(key);
|
||||
self.refresh_ready(key, existed);
|
||||
}
|
||||
}
|
||||
@@ -2513,8 +2496,6 @@ impl Lod {
|
||||
.collect();
|
||||
self.plan = plan;
|
||||
self.ready.retain(|key, _| self.plan.nodes.contains(key));
|
||||
let nodes = &self.plan.nodes;
|
||||
self.mesh_samples.retain(|key| nodes.contains(key));
|
||||
// Queued tiles the view left are not sampled (a running one is
|
||||
// dropped when it finishes, `finish_sample`).
|
||||
drop_queued_samples(&self.shared, &mut self.pending, |key| {
|
||||
@@ -2582,7 +2563,6 @@ impl Lod {
|
||||
.retain(|key| !self.active.contains(key));
|
||||
let changed: Vec<_> = old.symmetric_difference(&self.active).copied().collect();
|
||||
for &key in &changed {
|
||||
self.sync_sample(key);
|
||||
if old.contains(&key) && !self.active.contains(&key) {
|
||||
self.cancel_mesh(key);
|
||||
self.removed_pending.insert(key);
|
||||
@@ -2868,10 +2848,12 @@ impl Lod {
|
||||
if keys.is_empty() {
|
||||
return;
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
self.check_mesh_samples();
|
||||
let snapshot = Arc::new(Snapshot {
|
||||
samples: self.mesh_samples.clone(),
|
||||
samples: self
|
||||
.active
|
||||
.iter()
|
||||
.filter_map(|key| self.ready.get(key).map(|columns| (*key, columns.clone())))
|
||||
.collect(),
|
||||
saved: self.saved.clone(),
|
||||
reduced: self.reduced.clone(),
|
||||
nodes: self.plan.nodes.clone(),
|
||||
@@ -2904,31 +2886,6 @@ impl Lod {
|
||||
self.outstanding_meshes += count;
|
||||
self.shared.mesh_wake.notify_all();
|
||||
}
|
||||
/// Brings `mesh_samples` up to date for `key` after its `active` or `ready` changed.
|
||||
fn sync_sample(&mut self, key: TileKey) {
|
||||
let samples = self.ready.get(&key).filter(|_| self.active.contains(&key));
|
||||
self.mesh_samples.set(key, samples);
|
||||
}
|
||||
/// `mesh_samples` holds every active tile's ready samples and nothing else (debug builds).
|
||||
#[cfg(debug_assertions)]
|
||||
fn check_mesh_samples(&self) {
|
||||
let kept = self.mesh_samples.iter().count();
|
||||
let mut want = 0;
|
||||
for key in &self.active {
|
||||
if let Some(columns) = self.ready.get(key) {
|
||||
want += 1;
|
||||
let got = self.mesh_samples.get(key);
|
||||
assert!(
|
||||
got.is_some_and(|got| Arc::ptr_eq(got, columns)),
|
||||
"mesh samples of {key:?} out of date"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
kept, want,
|
||||
"mesh samples keep tiles no longer active or ready"
|
||||
);
|
||||
}
|
||||
fn finer_of(&self, key: TileKey) -> Vec<TileKey> {
|
||||
if !self.plan.split.contains(&key) {
|
||||
return Vec::new();
|
||||
@@ -4020,73 +3977,6 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
#[ignore = "requires prepared models/textures in HORIZON_TEST_RESOURCES"]
|
||||
fn slab_materials_survive_chunk_summaries_and_draw_far_faces() -> crate::Result<()> {
|
||||
use crate::{assets::Assets, biome::Biomes, light::LightTables, mesher::MeshTables};
|
||||
let root = std::path::PathBuf::from(
|
||||
std::env::var_os("HORIZON_TEST_RESOURCES").expect("HORIZON_TEST_RESOURCES"),
|
||||
);
|
||||
let assets = Arc::new(Assets::load(&root)?);
|
||||
let render = Arc::new(assets.reload(&root)?);
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let context = WorkerContext {
|
||||
light: Arc::new(LightTables::load(&root, assets.registry, &facts)?),
|
||||
tables: Arc::new(MeshTables::new(
|
||||
&assets,
|
||||
&render,
|
||||
&facts,
|
||||
crate::models::load_visibility(&root, assets.registry)?,
|
||||
)?),
|
||||
assets: assets.clone(),
|
||||
render,
|
||||
biomes: Arc::new(Biomes::load(&root)?),
|
||||
};
|
||||
let kinds = state_kinds(&facts);
|
||||
let mut chunk = Chunk::empty(0, 0);
|
||||
let mut mesher = mesh::Mesher::default();
|
||||
let mut materials = FxHashSet::default();
|
||||
// Real top, bottom, double and waterlogged states: a waterlogged slab
|
||||
// must keep its material rather than become a column of plain water.
|
||||
for (i, f) in facts.states.iter().enumerate() {
|
||||
if !f.name.ends_with("_slab") {
|
||||
continue;
|
||||
}
|
||||
materials.insert(f.name.as_ref());
|
||||
let state = BlockStateId(i as u32);
|
||||
chunk.set(3, 90, 3, state.0);
|
||||
let columns = summarize(&chunk, &kinds);
|
||||
let column = &columns[3 * 16 + 3];
|
||||
assert_eq!(column.spans.len(), 1, "{} state {i}", f.name);
|
||||
assert_eq!(column.spans[0].state, state, "{} state {i}", f.name);
|
||||
assert_eq!((column.spans[0].min, column.spans[0].max), (90, 91));
|
||||
assert_eq!(Column::reduced(&columns, 0, 0, 4).spans, column.spans);
|
||||
let mesh = mesher.mesh(
|
||||
TileKey {
|
||||
x: 0,
|
||||
z: 0,
|
||||
level: 0,
|
||||
},
|
||||
&columns.iter().collect::<Vec<_>>(),
|
||||
&FxHashSet::default(),
|
||||
None,
|
||||
&context,
|
||||
&std::array::from_fn(|_| Vec::new()),
|
||||
&[],
|
||||
);
|
||||
// Like stairs, far slabs use the existing full-cell approximation.
|
||||
assert_eq!(mesh.solid.len(), 6 * 4, "{} state {i}", f.name);
|
||||
assert!(mesh.translucent.is_empty(), "{} state {i}", f.name);
|
||||
}
|
||||
for name in [
|
||||
"minecraft:smooth_stone_slab",
|
||||
"minecraft:oak_slab",
|
||||
"minecraft:cut_copper_slab",
|
||||
] {
|
||||
assert!(materials.contains(name), "missing fixture {name}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
#[ignore = "requires prepared models/textures in HORIZON_TEST_RESOURCES"]
|
||||
fn real_mesh_clips_near_chunks_and_preserves_water_and_edited_surfaces() -> crate::Result<()> {
|
||||
use crate::{
|
||||
assets::Assets, biome::Biomes, light::LightTables, mesher::MeshTables,
|
||||
@@ -4886,7 +4776,6 @@ mod tests {
|
||||
lod.active.insert(key);
|
||||
lod.ready
|
||||
.insert(key, Arc::new(vec![Column::default(); CELLS * CELLS]));
|
||||
lod.sync_sample(key);
|
||||
lod.mark_mesh(key);
|
||||
lod.set_near_loading(true);
|
||||
lod.dispatch_meshes([0, 0]);
|
||||
|
||||
@@ -1386,52 +1386,6 @@ mod tests {
|
||||
let p = |i: usize| glam::Vec3::from(q[i].pos);
|
||||
(p(1) - p(0)).cross(p(2) - p(0))
|
||||
}
|
||||
/// Every solid quad the mesher emits packs into a far record, and the
|
||||
/// record gives the quad back bit for bit (`lod::packed`).
|
||||
///
|
||||
/// `lod::packed`'s own tests check its records against its own copy of what
|
||||
/// `emit_quad` does, which would pass just as well if the two had drifted
|
||||
/// apart. This checks them against the real mesher, over terrain that makes
|
||||
/// faces of every axis and winding: a hill of varied heights, a material
|
||||
/// boundary, a light boundary and water.
|
||||
#[test]
|
||||
fn every_quad_the_mesher_emits_packs_and_comes_back() {
|
||||
let mut columns = columns();
|
||||
for (i, column) in columns.iter_mut().enumerate() {
|
||||
let (x, z) = (i % CELLS, i / CELLS);
|
||||
let top = 4 + (x as i16 % 5) * 2 + (z as i16 % 3);
|
||||
let state = if (x + z) % 7 == 0 { 3 } else { 1 };
|
||||
let light = if x % 4 == 0 { 16 } else { 240 };
|
||||
column.spans = vec![
|
||||
Span {
|
||||
light,
|
||||
..span(0, top, state)
|
||||
},
|
||||
// Water over the lower ground, for the fluid faces too.
|
||||
span(top, 12.max(top + 1), 2),
|
||||
]
|
||||
.into();
|
||||
}
|
||||
let mesh = mesh(&columns);
|
||||
let quads = mesh.solid.as_chunks::<4>().0;
|
||||
assert!(
|
||||
quads.len() > 50,
|
||||
"{} quads is too few to say much",
|
||||
quads.len()
|
||||
);
|
||||
for (i, quad) in quads.iter().enumerate() {
|
||||
let words = crate::lod::packed::pack(quad)
|
||||
.unwrap_or_else(|| panic!("quad {i} of {} did not pack", quads.len()));
|
||||
let back = crate::lod::packed::corners(words)
|
||||
.unwrap_or_else(|| panic!("quad {i}'s record names no face"));
|
||||
assert_eq!(
|
||||
bytemuck::cast::<_, [[u32; 6]; 4]>(back),
|
||||
bytemuck::cast::<_, [[u32; 6]; 4]>(*quad),
|
||||
"quad {i} came back changed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_solid_cube_merges_to_six_faces_with_block_scaled_uvs() {
|
||||
let mut columns = columns();
|
||||
|
||||
Loaded 100 of 1233 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user