Compare commits

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

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

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

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

No files matched your search

+9 -9
View File
@@ -152,23 +152,23 @@ state stays separate from the library tests. This compiles gameplay twice for ex
| Subsystem | Files |
|---|---|
| Game loop, tick, frame | `game.rs`: `fn run`, `fn tick`, `fn tick_world`, `fn render`; G: `window_handler.rs` (`impl ApplicationHandler for Game`: `window_event`, `about_to_wait`); L: `clock.rs`, `spans.rs`; `horizon-client-input/src/window.rs` |
| Rendering | L: `gfx.rs` (`Renderer::new`, `Renderer::render`), `gfx/` (cull, occlusion, translucency, upload, timing, lod, weather, stars, end_sky), `*.wgsl`, `particles.rs`, `block_entity_render.rs`, `item_render.rs`, `fog.rs`, `clouds.rs`, `camera.rs`, `first_person.rs`; G: `render_tick.rs`, `item_draw.rs`, `screen_backgrounds.rs` (panorama, menu blur, border wall) |
| Chunks, streaming, light | L: `streaming.rs` (worker pool: generate, light, mesh), `world.rs`, `chunk.rs`, `chunk_map.rs`, `light.rs`, `mesher.rs` + `mesher/`, `vanillagen.rs` (binds horizon-world's generator), `lod.rs` + `lod/`, `server_terrain.rs` (a server's kept chunks), `bluemap.rs` + `bluemap_detect.rs` (far terrain from its web map), `ticket_levels.rs`; G: `world_loading.rs`, `world_entry.rs`, `postprocess.rs`; game.rs `fn update_world` |
| Game loop, tick, frame | `game.rs`: `fn run`, `impl ApplicationHandler for Game` (`window_event`, `about_to_wait`), `fn tick`, `fn tick_world`, `fn render`; L: `clock.rs`, `spans.rs`; `horizon-client-input/src/window.rs` |
| Rendering | L: `gfx.rs` (`Renderer::new`, `Renderer::render`), `gfx/` (cull, occlusion, translucency, upload, timing, lod, weather, stars, end_sky), `*.wgsl`, `particles.rs`, `block_entity_render.rs`, `item_render.rs`, `fog.rs`, `clouds.rs`, `camera.rs`, `first_person.rs`; G: `render_tick.rs`, `item_draw.rs` |
| Chunks, streaming, light | L: `streaming.rs` (worker pool: generate, light, mesh), `world.rs`, `chunk.rs`, `chunk_map.rs`, `light.rs`, `mesher.rs` + `mesher/`, `vanillagen.rs` (binds horizon-world's generator), `lod.rs` + `lod/`, `server_terrain.rs` (a server's kept chunks), `bluemap.rs` + `bluemap_detect.rs` (far terrain from its web map), `ticket_levels.rs`; G: `world_loading.rs`, `postprocess.rs`; game.rs `fn update_world` |
| World generation | `crates/horizon-world/src/worldgen/` (`generator*.rs`, `feature_graph.rs`, `structure/`, `carver/`) |
| Block behaviour | `crates/horizon-world/src/level/` (`mod.rs`, `tick.rs`, `hooks.rs`, `redstone.rs`, `place.rs`, `interact.rs`); game.rs `fn tick_blocks`, `fn process_level_events` |
| Mobs | L: `mobs.rs` (types), `mobs/` (`ai.rs`, `goals.rs`, `brain.rs`, `*_brain.rs`, `nav.rs`, `path.rs`, `species.rs`, `spawning.rs`, `render.rs`, `anim.rs`); G: `creatures.rs` (ticking, spawning, mob events), one file per mob family (`dragon.rs`, `wither.rs`, `villagers.rs`, `piglins.rs`, `golems.rs`...), `natural_spawning.rs` |
| Other entities | L: `entities.rs`, `projectiles.rs`, `arrows.rs`; G: `boats.rs`, `minecarts.rs`, `mounts.rs`, `armor_stands.rs`, `paintings.rs`, `frames.rs`, `fireworks.rs`, `tnt.rs`, `explosion.rs` |
| Player actions | game.rs (`fn use_hand`, `fn tick_mining`, `fn break_block`); G: `item_actions.rs` (`fn use_item`), `melee.rs` (`fn attack`), `hand.rs`, `combat.rs`, `consume.rs`, `using_item.rs` (an item in use, tick by tick), `potions.rs`, `shield.rs`, `crossbow.rs`, `trident.rs`, `fishing.rs`; L: `motion.rs` + `motion/` (movement), `collisions.rs`; crate `horizon-items`: `items.rs`, `item_data.rs`, `enchant.rs`, `blocks.rs` |
| Inventory and containers | L: `menu.rs` (`AbstractContainerMenu.clicked`), `container_screen.rs`, `containers.rs`, `creative.rs`, `crafting.rs`, `recipe_book*.rs`, `stations.rs`, `merchant.rs`; game.rs `fn menu_ops`, `fn click_inventory`; G: `stations.rs`, `trading.rs`, `recipe_book.rs`, `machines.rs`, `inventory_input.rs` (keys and clicks over a container), `inventory_slots.rs` (slot layout and drawing) |
| Player actions | game.rs (`fn use_hand`, `fn use_item`, `fn attack`, `fn tick_mining`, `fn break_block`); G: `hand.rs`, `combat.rs`, `consume.rs`, `potions.rs`, `shield.rs`, `crossbow.rs`, `trident.rs`, `fishing.rs`; L: `motion.rs` + `motion/` (movement), `collisions.rs`; crate `horizon-items`: `items.rs`, `item_data.rs`, `enchant.rs`, `blocks.rs` |
| Inventory and containers | L: `menu.rs` (`AbstractContainerMenu.clicked`), `container_screen.rs`, `containers.rs`, `creative.rs`, `crafting.rs`, `recipe_book*.rs`, `stations.rs`, `merchant.rs`; game.rs `fn menu_ops`, `fn click_inventory`; G: `stations.rs`, `trading.rs`, `recipe_book.rs`, `machines.rs` |
| Commands | parse: crate `horizon-command` (`args.rs`, `selector.rs`, `tree_*.rs`, `vanilla.rs`); run: G: `commands.rs` (dispatch) + `commands/*.rs` (`admin`, `execute`, `items`, `players`, `world_cmds`...); L: `scoreboard_cmd.rs`, `data_cmd.rs`; `horizon-client-core`: `nbt_path.rs`, `scoreboard.rs`; G: `functions.rs`, `command_blocks.rs` |
| UI screens | `Screen` enum in game.rs; L: `title.rs` + `title/`, `options_ui.rs`, `multiplayer.rs`, `pack_screen.rs`, `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
-4
View File
@@ -61,10 +61,6 @@ another project's source may also be read and used, **only when that project's l
"source available" licenses, all rights reserved (OptiFine), and any project with no license or an unclear one.
Don't read, copy, translate or paraphrase their source, and don't send code an AI tool wrote from it. Being able
to download a mod or a pack does not give you the right to read its files.
- **Java's own library is GPL too.** OpenJDK's source (`java.lang.Math`, `StrictMath`, `FdLibm`, `HashMap`, the
sorts, `Double.toString`, HotSpot's intrinsics...) is GPL-2.0, so it is off limits like the rest. Reproduce Java's
behaviour from its API documentation (the Javadoc) and from what a JVM returns, or from a permissive original such
as Sun's fdlibm C sources (see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)); see #6741.
- **Check the license yourself.** Read the LICENSE file at the commit you use and the header of each file you take:
some repositories mix licenses, and a file under another license is off limits. A license that changed: the one at
your commit counts. In doubt, don't: ask in #contributors first.
Generated
-2
View File
@@ -1588,7 +1588,6 @@ dependencies = [
"horizon-mod-api-gen",
"horizon-narrator",
"horizon-optifine",
"horizon-polytone",
"horizon-protocol",
"horizon-shader-api",
"horizon-shaders",
@@ -1776,7 +1775,6 @@ dependencies = [
name = "horizon-polytone"
version = "0.1.0"
dependencies = [
"log",
"regex",
"serde_json",
]
+2 -63
View File
@@ -1,36 +1,5 @@
# Third-party notices
## Code removed for its license
Horizon once shipped three pieces of code under MIT that came from projects whose licenses don't allow that. All
three are gone from main. They are listed here for anyone who holds an older commit, tag or release: in those
versions, that code is not covered by Horizon's MIT license. It stays under its authors' copyright and its original
license.
- **Shader packs written from Iris's source.** `crates/horizon-shaderpack` and the render stages that used it were
written from the source of [Iris](https://github.com/IrisShaders/Iris) (LGPL-3.0, copyright the Iris contributors).
They were added from 15 October 2026 (`b22583d057`), shipped in the game releases `v2026.10.16.02` to `v0.2.46`,
and were removed on 24 October 2026 ([#1561](https://github.com/horizon-rust/horizon/pull/1561)) after a developer
of Iris showed us. Shader packs came back in `crates/horizon-shaders`, first written from a description of the
pack format.
- **Math functions translated from OpenJDK.** In `crates/horizon-core/src/jmath.rs`, `fdlibm::{log, log10, cbrt,
hypot}` were translated from OpenJDK's `java/lang/FdLibm.java` and `round_f32` from its `java/lang/Math.java`
(GPL-2.0 with the Classpath Exception), and `aarch64::{sin, cos}` from HotSpot's `macroAssembler_aarch64_trig.cpp`
(GPL-2.0); copyright Oracle and/or its affiliates, and Cavium for that file. The `fdlibm` and `aarch64` functions
were added on 13 October 2026 (`558d140fca`, first released in `v2026.10.13`). `round_f32` was added on 14 October
2026 as `java_round` in `crates/horizon-client/src/mipmap.rs` (`95db0fce9d`) and moved into `jmath.rs` on 3
November ([#7770](https://github.com/horizon-rust/horizon/pull/7770)). All of them were replaced in
[#11944](https://github.com/horizon-rust/horizon/pull/11944) by code written without OpenJDK's sources, from Sun's
fdlibm C sources, the Java SE API documentation and a description of the behaviour, and checked against a JVM
([#6741](https://github.com/horizon-rust/horizon/issues/6741)).
- **Java's `HashMap` translated from OpenJDK.** `crates/horizon-world/src/java_hash_map.rs`, which reproduces the
iteration order of `java.util.HashMap`, was translated from OpenJDK's `java/util/HashMap.java` (GPL-2.0 with the
Classpath Exception; copyright Oracle and/or its affiliates). It was added on 7 October 2026 (`118e93ea51` and
`194db0dbb4`, first released in `v2026.10.12`). It was replaced in
[#12043](https://github.com/horizon-rust/horizon/pull/12043) by code written without OpenJDK's sources, from a
description of the behaviour, the Java SE API documentation and the textbook red-black tree, and checked against a
JVM ([#6741](https://github.com/horizon-rust/horizon/issues/6741)).
## Catnip / Ponder
`crates/horizon-client-render/src/fluid_render.rs` translates the still-fluid face tiling and
@@ -46,11 +15,8 @@ original classes, game shaders, textures or prepared resources are distributed h
`third_party/wgpu-hal` is [wgpu-hal](https://github.com/gfx-rs/wgpu) 30.0.1 by the gfx-rs
developers, with upstream fixes applied
([gfx-rs/wgpu#10124](https://github.com/gfx-rs/wgpu/pull/10124),
[gfx-rs/wgpu#10714](https://github.com/gfx-rs/wgpu/pull/10714)). Its GL backend also reports
the context's sampled textures and samplers per shader stage (usually 32 on desktop GL,
where WebGL2's minimum 16 was reported before), so a pipeline layout the driver accepts is
not refused. It is licensed under MIT or Apache-2.0; both licenses are in that folder
(`LICENSE.MIT`, `LICENSE.APACHE`).
[gfx-rs/wgpu#10714](https://github.com/gfx-rs/wgpu/pull/10714)). It is licensed under MIT or
Apache-2.0; both licenses are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
## naga
@@ -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).
+14 -55
View File
@@ -917,46 +917,6 @@ impl NumericProperty {
/// (`None` removes a prototype component).
pub type Components = BTreeMap<String, Value>;
/// A stack's components read in place: its patch first, then its item's defaults. Reading one copies
/// nothing, where [`ItemModels::components`] clones the defaults for a stack with a patch (an enchanted
/// sword's are a few dozen JSON values, on every draw of it).
#[derive(Clone, Copy)]
pub struct ComponentsView<'a> {
prototype: &'a Components,
patch: &'a BTreeMap<String, Option<Value>>,
}
impl<'a> ComponentsView<'a> {
pub fn get(&self, key: &str) -> Option<&'a Value> {
match self.patch.get(key) {
Some(patched) => patched.as_ref(),
None => self.prototype.get(key),
}
}
pub fn contains_key(&self, key: &str) -> bool {
self.get(key).is_some()
}
/// Components with no patch: `components` as they are.
pub fn of(components: &'a Components) -> Self {
static NO_PATCH: BTreeMap<String, Option<Value>> = BTreeMap::new();
Self {
prototype: components,
patch: &NO_PATCH,
}
}
/// The merged map, for what needs one whole.
pub fn to_map(&self) -> Components {
let mut out = self.prototype.clone();
for (key, value) in self.patch {
match value {
Some(v) => out.insert(key.clone(), v.clone()),
None => out.remove(key),
};
}
out
}
}
/// An item stack as the resolver reads it.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct StackView {
@@ -1127,15 +1087,14 @@ impl ItemModels {
if stack.patch.is_empty() {
return std::borrow::Cow::Borrowed(prototype);
}
std::borrow::Cow::Owned(self.components_view(stack).to_map())
}
/// The stack's components, read in place (see [`ComponentsView`]).
pub fn components_view<'a>(&'a self, stack: &'a StackView) -> ComponentsView<'a> {
static NONE: Components = BTreeMap::new();
ComponentsView {
prototype: self.prototypes.get(&stack.item).unwrap_or(&NONE),
patch: &stack.patch,
let mut out = prototype.clone();
for (key, value) in &stack.patch {
match value {
Some(v) => out.insert(key.clone(), v.clone()),
None => out.remove(key),
};
}
std::borrow::Cow::Owned(out)
}
/// `ItemModelResolver.updateForTopItem`.
pub fn resolve(&self, stack: &StackView, ctx: &Context, env: &dyn ItemEnv) -> ResolvedItem {
@@ -1153,7 +1112,7 @@ impl ItemModels {
ctx: &Context,
env: &dyn ItemEnv,
) {
let components = self.components_view(stack);
let components = self.components(stack);
let Some(Value::String(model_id)) = components.get("minecraft:item_model") else {
return;
};
@@ -1163,7 +1122,7 @@ impl ItemModels {
let mut state = Resolve {
models: self,
stack,
components,
components: &components,
ctx,
env,
};
@@ -1181,7 +1140,7 @@ impl ItemModels {
self.properties(stack).2
}
fn properties(&self, stack: &StackView) -> (bool, bool, f32) {
match self.components_view(stack).get("minecraft:item_model") {
match self.components(stack).get("minecraft:item_model") {
Some(Value::String(id)) => identifier(id)
.ok()
.and_then(|id| self.definitions.get(&id))
@@ -1200,7 +1159,7 @@ impl ItemModels {
struct Resolve<'a> {
models: &'a ItemModels,
stack: &'a StackView,
components: ComponentsView<'a>,
components: &'a Components,
ctx: &'a Context<'a>,
env: &'a dyn ItemEnv,
}
@@ -1496,9 +1455,9 @@ impl Resolve<'_> {
fn numeric(&self, property: &NumericProperty) -> f32 {
let owner = self.ctx.owner;
match property {
NumericProperty::BundleFullness => self
.env
.bundle_fullness(self.stack, &self.components.to_map()),
NumericProperty::BundleFullness => {
self.env.bundle_fullness(self.stack, self.components)
}
NumericProperty::Compass {
wobble,
target,
@@ -63,7 +63,7 @@ pub(super) fn decode(m: &Map<String, Value>) -> Result<Value> {
/// with a matching level. An empty predicate list still needs the component.
pub(super) fn matches(
predicate: &Value,
components: ComponentsView<'_>,
components: &Components,
env: &dyn ItemEnv,
) -> Option<bool> {
let (id, expected) = predicate.as_object()?.iter().next()?;
@@ -143,17 +143,13 @@ mod tests {
json!({"minecraft:density":0}),
)]);
assert_eq!(
matches(
&json!({"minecraft:enchantments":[{}]}),
ComponentsView::of(&components),
&NoEnv
),
matches(&json!({"minecraft:enchantments":[{}]}), &components, &NoEnv),
Some(true)
);
assert_eq!(
matches(
&json!({"minecraft:enchantments":[{"enchantments":"minecraft:density"}]}),
ComponentsView::of(&components),
&components,
&NoEnv
),
Some(false)
@@ -170,21 +166,12 @@ mod tests {
"minecraft:enchantments".into(),
json!({"minecraft:density":3}),
)]);
assert_eq!(
matches(&predicate, ComponentsView::of(&components), &Tags),
Some(true)
);
assert_eq!(
matches(&predicate, ComponentsView::of(&components), &NoEnv),
Some(false)
);
assert_eq!(matches(&predicate, &components, &Tags), Some(true));
assert_eq!(matches(&predicate, &components, &NoEnv), Some(false));
components.insert(
"minecraft:enchantments".into(),
json!({"minecraft:density":2}),
);
assert_eq!(
matches(&predicate, ComponentsView::of(&components), &Tags),
Some(false)
);
assert_eq!(matches(&predicate, &components, &Tags), Some(false));
}
}
@@ -1,5 +1,4 @@
use super::*;
use serde_json::json;
#[test]
fn identifier_adds_default_namespace() {
@@ -112,49 +111,3 @@ fn custom_condition_validates_its_name_and_both_branches() {
assert!(matches!(node("example:charged", empty).unwrap(),
ItemModelNode::Condition { property: ConditionProperty::Custom(name), .. } if name == "example:charged"));
}
#[test]
fn a_patched_stack_reads_its_patch_over_its_items_defaults() {
let mut models = ItemModels::default();
models.prototypes.insert(
"minecraft:diamond_sword".into(),
Components::from_iter([
(
"minecraft:item_model".into(),
json!("minecraft:diamond_sword"),
),
("minecraft:max_damage".into(), json!(1561)),
("minecraft:rarity".into(), json!("common")),
]),
);
let mut stack = StackView::new("minecraft:diamond_sword", 1);
stack.patch.insert(
"minecraft:enchantments".into(),
Some(json!({"minecraft:sharpness": 5})),
);
stack
.patch
.insert("minecraft:rarity".into(), Some(json!("rare")));
stack.patch.insert("minecraft:max_damage".into(), None);
let view = models.components_view(&stack);
assert_eq!(view.get("minecraft:rarity"), Some(&json!("rare")));
assert_eq!(view.get("minecraft:max_damage"), None, "a removed default");
assert_eq!(
view.get("minecraft:item_model"),
Some(&json!("minecraft:diamond_sword"))
);
assert!(view.contains_key("minecraft:enchantments"));
assert!(!view.contains_key("minecraft:custom_name"));
// Every key reads as the merged map has it.
let merged = models.components(&stack);
for key in [
"minecraft:rarity",
"minecraft:max_damage",
"minecraft:item_model",
"minecraft:enchantments",
"minecraft:custom_name",
] {
assert_eq!(view.get(key), merged.get(key), "{key}");
}
assert_eq!(view.to_map(), *merged);
}
+2 -2
View File
@@ -290,12 +290,12 @@ pub fn normalize_quaternion(q: [f32; 4]) -> [f32; 4] {
[x * inv, y * inv, z * inv, w * inv]
}
/// JOML `Math.sin(float)` without fast math.
pub fn joml_sin(rad: f32) -> f32 {
fn joml_sin(rad: f32) -> f32 {
f64::from(rad).sin() as f32
}
/// JOML `Math.cosFromSin(float, float)` without fast math: the magnitude from
/// the sine, the sign from the angle's quadrant (exact zeros at right angles).
pub fn cos_from_sin(sin: f32, angle: f32) -> f32 {
fn cos_from_sin(sin: f32, angle: f32) -> f32 {
let cos = (1.0 - sin * sin).sqrt();
let tau = (std::f64::consts::PI * 2.0) as f32;
let a = angle + std::f32::consts::FRAC_PI_2;
@@ -217,10 +217,9 @@ pub fn stack(tag: &Tag) -> Option<Tag> {
Some(Tag::compound(out))
}
/// `ItemStackTemplate.CODEC`: an item id or `{id, count?, components?}`.
/// Encoding uses the primary record form, including for a bare item id.
fn template(tag: &Tag) -> Option<Tag> {
match tag {
Tag::String(_) => Some(Tag::compound([("id", tag.clone())])),
Tag::String(_) => Some(tag.clone()),
Tag::Compound(_) => {
let mut out = vec![("id", string(&text(tag.get("id"))?))];
if let Some(count) = int(tag.get("count")).filter(|c| *c != 1) {
@@ -1012,69 +1011,3 @@ mod tests {
assert_eq!(items[1].get("count"), Some(&Tag::Int(5)));
}
}
#[cfg(test)]
mod pot_template_tests {
use super::*;
use horizon_world::nbt::parse_snbt;
#[test]
fn decorated_pot_string_template_load_save_uses_item_compound() {
let parse = |s: &str| parse_snbt(s).expect("valid input NBT").0;
let input = |front: &str| {
let mut tag = parse(
r#"{id:"minecraft:decorated_pot",x:4,y:200,z:4,sherds:{back:{id:"minecraft:brick"},right:{id:"minecraft:archer_pottery_sherd"}},item:{id:"minecraft:wheat",count:3,components:{"minecraft:custom_name":"H290 item"}}}"#,
);
let Tag::Compound(root) = &mut tag else {
unreachable!()
};
let Some(Tag::Compound(sides)) = root.get_mut(&"sherds".into()) else {
unreachable!()
};
sides.insert("front".into(), parse(front));
tag
};
let other_fields = |loaded: &Tag, saved: &Tag| {
assert_eq!(saved.get("id"), loaded.get("id"));
for key in ["x", "y", "z", "item"] {
assert_eq!(saved.get(key), loaded.get(key), "{key}");
}
let loaded_sides = loaded.get("sherds").unwrap();
let saved_sides = saved.get("sherds").unwrap();
for side in ["back", "right"] {
assert_eq!(saved_sides.get(side), loaded_sides.get(side), "{side}");
}
assert_eq!(saved_sides.get("left"), None);
};
let plain = r#"{id:"minecraft:angler_pottery_sherd"}"#;
let defaults = r#"{id:"minecraft:angler_pottery_sherd",count:1,components:{}}"#;
let rich = r#"{id:"minecraft:angler_pottery_sherd",count:3,components:{"minecraft:custom_data":{h290:1},"minecraft:custom_name":"H290 sherd"}}"#;
// Actual canonical load/save entry, not the private template helper.
for (front, expected) in [(plain, plain), (defaults, plain), (rich, rich)] {
let loaded = input(front);
let original = loaded.clone();
let saved = canonical(&loaded).expect("actual decorated-pot codec");
other_fields(&loaded, &saved);
assert_eq!(
saved.get("sherds").unwrap().get("front"),
Some(&parse(expected))
);
assert_eq!(canonical(&saved).as_ref(), Some(&saved));
assert_eq!(loaded, original, "read-only canonicalization");
}
println!(
"H290 actual canonical compound/default/count/components, other sides/item/name and idempotency controls admitted"
);
let loaded = input(r#""minecraft:angler_pottery_sherd""#);
let original = loaded.clone();
let saved = canonical(&loaded).expect("actual decorated-pot codec");
other_fields(&loaded, &saved);
assert_eq!(loaded, original, "read-only canonicalization");
assert_eq!(
saved.get("sherds").unwrap().get("front"),
Some(&parse(plain)),
"string template must save the primary item compound",
);
assert_eq!(canonical(&saved).as_ref(), Some(&saved));
}
}
+3 -41
View File
@@ -412,11 +412,10 @@ pub fn to_text_with(
.collect()
})
.unwrap_or_default();
// TranslatableContents.decompose: Language.getOrDefault(key, fallback), so the
// fallback is the template itself, never looked up as another key.
let translated = Text::translate(key, &args);
match o.get("fallback").and_then(Value::as_str) {
Some(fallback) => Text::translate_with_fallback(key, fallback, &args),
None => Text::translate(key, &args),
Some(fallback) if translated.plain() == key => Text::translate(fallback, &args),
_ => translated,
}
} else if let Some(sel) = o.get("selector").and_then(Value::as_str) {
resolve(sel)
@@ -514,43 +513,6 @@ pub fn name_text(tag: &Tag) -> Text {
#[cfg(test)]
mod tests {
use super::*;
use crate::lang::{self, Language, ResourceSource};
struct Mem(std::collections::HashMap<String, Vec<u8>>);
impl ResourceSource for Mem {
fn read(&self, path: &str) -> Option<Vec<u8>> {
self.0.get(path).cloned()
}
}
/// TranslatableContents.decompose: Language.getOrDefault(key, fallback). A missing key
/// shows the fallback as written, even when the fallback is another key the language has.
#[test]
fn a_translatable_fallback_stays_literal() {
let pack = Mem([(
"assets/minecraft/lang/en_us.json".to_owned(),
br#"{"real.key": "Translated", "greet": "Hello %s"}"#.to_vec(),
)]
.into());
let ((), _) = lang::scoped(Language::load(&[&pack], "en_us"), || {
let parse = |v: Value| to_text(&v, &mut |_| Text::empty()).plain();
let missing = serde_json::json!({"translate": "missing.key", "fallback": "real.key"});
assert_eq!(parse(missing), "real.key");
// The fallback is a template: its arguments are filled in.
let args = serde_json::json!({"translate": "missing.key", "fallback": "Hi %s", "with": ["Steve"]});
assert_eq!(parse(args), "Hi Steve");
// A key the language has wins over its fallback.
let found =
serde_json::json!({"translate": "greet", "fallback": "real.key", "with": ["Alex"]});
assert_eq!(parse(found), "Hello Alex");
// A component built with the constructor keeps its meaning through its JSON.
let built = Text::translate_with_fallback("missing.key", "real.key", &[]);
let again = to_text(&built.component(), &mut |_| Text::empty());
assert_eq!(again.plain(), built.plain());
assert_eq!(again.component(), built.component());
});
}
/// SnbtGrammar: a compound or list may end with one comma; two in a row
/// stay an error.
#[test]
-18
View File
@@ -46,21 +46,6 @@ thread_local! {
}
/// The current language's `bidirectional`, read for every drawn string.
static BIDIRECTIONAL: AtomicBool = AtomicBool::new(false);
/// Counts the languages set (`set`): another count is another language, so text looked up
/// before it may read otherwise.
static GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
thread_local! {
/// Counts the scopes this thread entered and left (`scoped`), which no other thread sees.
static SCOPE_GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
/// Which language text looks up in now, on this thread, as a count that changes whenever that
/// can: both counts only grow, so their sum changes when either does.
pub fn generation() -> u64 {
GENERATION.load(Ordering::Relaxed) + SCOPE_GENERATION.get()
}
fn scope_changed() {
SCOPE_GENERATION.set(SCOPE_GENERATION.get() + 1);
}
/// A source of resource files: the prepared root plus packs, highest priority last.
pub trait ResourceSource {
@@ -539,7 +524,6 @@ pub fn available(sources: &[&dyn ResourceSource]) -> Vec<LanguageInfo> {
pub fn set(language: Language) {
BIDIRECTIONAL.store(language.bidirectional, Ordering::Relaxed);
*CURRENT.write().unwrap_or_else(|e| e.into_inner()) = Some(Arc::new(language));
GENERATION.fetch_add(1, Ordering::Relaxed);
}
pub fn current() -> Option<Arc<Language>> {
if let Some(language) = SCOPE.with(|s| s.borrow().as_ref().map(|s| s.language.clone())) {
@@ -559,7 +543,6 @@ pub fn scoped<R>(language: Language, f: impl FnOnce() -> R) -> (R, Vec<String>)
impl Drop for Restore {
fn drop(&mut self) {
SCOPE.with(|s| *s.borrow_mut() = self.0.take());
scope_changed();
}
}
let outer = SCOPE.with(|s| {
@@ -569,7 +552,6 @@ pub fn scoped<R>(language: Language, f: impl FnOnce() -> R) -> (R, Vec<String>)
})
});
let _restore = Restore(outer);
scope_changed();
let result = f();
let missed = SCOPE.with(|s| {
s.borrow_mut()
+20 -1
View File
@@ -1,7 +1,6 @@
//! NBT paths (`NbtPathArgument`): `foo.bar[0]`, `[]`, `list[{id:1}]`,
//! `{root:match}`, with get, set, insert and remove on a tag tree, and the
//! partial NBT comparison (`NbtUtils.compareNbt`) selectors and conditions use.
pub use horizon_world::nbt::compare_nbt;
use horizon_world::nbt::{Compound, JavaString, Tag, parse_snbt};
/// An error carrying the translation key and its arguments.
@@ -48,6 +47,26 @@ fn get_child<'a>(c: &'a Compound, name: &str) -> Option<&'a Tag> {
c.get(&key(name))
}
/// `NbtUtils.compareNbt(expected, actual, true)`.
pub fn compare_nbt(expected: &Tag, actual: Option<&Tag>) -> bool {
let Some(actual) = actual else { return false };
if std::mem::discriminant(expected) != std::mem::discriminant(actual) {
return false;
}
match (expected, actual) {
(Tag::Compound(e), Tag::Compound(a)) => {
a.len() >= e.len() && e.iter().all(|(k, v)| compare_nbt(v, a.get(k)))
}
(Tag::List(e), Tag::List(a)) => {
if e.is_empty() {
return a.is_empty();
}
a.len() >= e.len() && e.iter().all(|x| a.iter().any(|y| compare_nbt(x, Some(y))))
}
_ => expected == actual,
}
}
// ---- list-like tags ----------------------------------------------------------
/// The elements of a list or array tag.
@@ -89,31 +89,6 @@ pub struct Registries {
pub trigger_objectives: Vec<String>,
/// Objectives with the `trigger` criterion -> holder -> whether its score is locked.
pub trigger_scores: BTreeMap<String, BTreeMap<String, bool>>,
/// The `ConfiguredFeature` codec for an inline `minecraft:worldgen/feature`
/// value (`ResourceOrIdArgument`), where the source has the server's live
/// features; None leaves the decode to when the command runs.
pub inline_feature: Option<InlineFeatureCheck>,
}
/// A codec check of an inline value: its error message when it does not decode.
#[derive(Clone)]
pub struct InlineFeatureCheck(
std::sync::Arc<dyn Fn(&horizon_world::nbt::Tag) -> Result<(), String> + Send + Sync>,
);
impl InlineFeatureCheck {
pub fn new(
check: impl Fn(&horizon_world::nbt::Tag) -> Result<(), String> + Send + Sync + 'static,
) -> Self {
Self(std::sync::Arc::new(check))
}
pub fn check(&self, value: &horizon_world::nbt::Tag) -> Result<(), String> {
(self.0)(value)
}
}
impl std::fmt::Debug for InlineFeatureCheck {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("InlineFeatureCheck(..)")
}
}
/// The ids and tag ids of one registry.
#[derive(Clone, Debug, Default)]
+1 -8
View File
@@ -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,
));
-1
View File
@@ -23,7 +23,6 @@ horizon-narrator = { path = "../horizon-narrator" }
# OptiFine-format pack files: random entity textures (hud.rs), connected textures
# (connected.rs) and custom item texture rules (cit.rs).
horizon-optifine = { path = "../horizon-optifine" }
horizon-polytone = { path = "../horizon-polytone" }
horizon-protocol = { path = "../horizon-protocol" }
horizon-shader-api = { path = "../horizon-shader-api" }
horizon-shaders = { path = "../horizon-shaders" }
@@ -32,11 +32,6 @@ fn pack(properties: &str, shadow: Option<&str>, reads_shadow: bool) -> horizon_s
write("final.fsh", if reads_shadow { SHADOW_READ } else { COLOUR });
write("gbuffers_entities.vsh", VERTEX);
write("gbuffers_entities.fsh", COLOUR);
// Terrain programs keep the load free of "nothing draws it" notes; no terrain is drawn.
for terrain in ["gbuffers_terrain", "gbuffers_water"] {
write(&format!("{terrain}.vsh"), VERTEX);
write(&format!("{terrain}.fsh"), COLOUR);
}
if let Some(shadow) = shadow {
write("shadow.vsh", VERTEX);
write("shadow.fsh", shadow);
@@ -245,7 +245,6 @@ fn replay(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
settings.fxaa,
settings.taa,
settings.upscale,
0.85,
);
let mut notes = Vec::new();
let mut note = |what: &str, result: horizon_client_render::Result<()>| {
@@ -661,7 +660,6 @@ fn remesh(
blend_radius,
smooth_lighting,
cutout_leaves,
opaque_ice: false,
translucent_layers: &[],
leaf_culling,
};
@@ -50,14 +50,10 @@ fn pack(renderer: &mut Renderer, folder: &Path, valid: bool) {
std::fs::create_dir_all(&shaders).unwrap();
let vertex = "#version 120\nvarying vec2 uv; void main(){gl_Position=ftransform();uv=gl_MultiTexCoord0.xy;}\n";
let fragment = "#version 120\nuniform int entityId; uniform int blockEntityId; uniform vec4 entityColor; void main(){gl_FragData[0]=vec4(float(entityId)/255.0,entityColor.a,float(blockEntityId)/255.0,1.0);}\n";
// The terrain programs keep the load free of "nothing draws it" notes; this
// fixture draws no terrain.
for program in [
"gbuffers_entities",
"gbuffers_block",
"gbuffers_entities_translucent",
"gbuffers_terrain",
"gbuffers_water",
] {
std::fs::write(shaders.join(format!("{program}.vsh")), vertex).unwrap();
std::fs::write(
+5 -267
View File
@@ -47,53 +47,6 @@ struct Channel {
world: u32,
/// The sound file it started and when, to tell another player's copy of it.
started: Option<(String, std::time::Instant)>,
/// Its sound event plays clearly through a mod's muffle (`Audio::muffle`).
unmuffled: bool,
}
/// The cutoff of the muffle's low-pass at full strength: the world heard through ringing ears.
const MUFFLED_CUTOFF: f32 = 300.0;
/// The cutoff it sweeps down from as its strength rises.
const OPEN_CUTOFF: f32 = 20_000.0;
/// How long the muffle takes to glide to a new level: a game tick's step (50 ms) would click.
const MUFFLE_GLIDE: f32 = 0.02;
/// A mod's muffle (`Audio::muffle`) in the mixer: the other sounds are mixed apart, low-passed by
/// two one-pole stages and turned down, then added to the sounds it leaves clear. While it is off
/// (never set, or faded out) the sounds mix straight into the output, as without it.
#[derive(Default)]
struct Muffle {
/// `[amount, lowpass]`, each 0 to 1, as the game's tick last set them.
target: [f32; 2],
/// Where the output is now, gliding to `target`.
current: [f32; 2],
/// The low-pass stages' state for the left and right output channels.
stages: [[f32; 2]; 2],
/// The muffled sounds of one buffer, reused.
bus: Vec<f32>,
}
impl Muffle {
fn active(&self) -> bool {
self.target != [0.0; 2] || self.current.iter().any(|v| *v > 1e-4)
}
/// Adds `bus` (the muffled sounds) to `data`, low-passed and turned down.
fn apply(&mut self, bus: &[f32], data: &mut [f32], channels: usize, rate: u32) {
let rate = rate as f32;
let glide = 1.0 - (-1.0 / (MUFFLE_GLIDE * rate)).exp();
for (frame, muffled) in data.chunks_mut(channels).zip(bus.chunks(channels)) {
for (current, target) in self.current.iter_mut().zip(self.target) {
*current += (target - *current) * glide;
}
let [amount, lowpass] = self.current;
let gain = 1.0 - amount;
let cutoff = OPEN_CUTOFF * (MUFFLED_CUTOFF / OPEN_CUTOFF).powf(lowpass);
let a = 1.0 - (-std::f32::consts::TAU * cutoff / rate).exp();
for ((out, s), stage) in frame.iter_mut().zip(muffled).zip(&mut self.stages) {
stage[0] += (s - stage[0]) * a;
stage[1] += (stage[0] - stage[1]) * a;
// Dry at no strength, so a muffle that only turns sounds down leaves them as they were.
*out += (s + (stage[1] - s) * lowpass) * gain;
}
}
}
}
#[derive(Default)]
struct Mixer {
@@ -112,8 +65,6 @@ struct Mixer {
cameras: Vec<([f64; 3], Basis, u32)>,
/// Per-source gain/pan/step cache for one buffer, reused across callbacks.
voices: Vec<(ChannelId, [f32; 3], f64)>,
/// A mod's muffle of the sounds it does not exempt.
muffle: Muffle,
limiter_peak: f32,
/// The output device the stream was opened on (`Library.currentDeviceName`).
#[cfg(feature = "audio")]
@@ -149,21 +100,6 @@ pub struct Heard {
pub range: f32,
pub looping: bool,
}
/// A mod's muffle (`Audio::muffle`): its full strength, and the ticks it fades over and has left.
#[derive(Clone, Copy, Debug, PartialEq)]
struct MuffleFade {
amount: f32,
lowpass: f32,
ticks: u32,
left: u32,
}
impl MuffleFade {
/// `[amount, lowpass]` now: full when set, falling in a straight line to none.
fn level(&self) -> [f32; 2] {
let left = self.left as f32 / self.ticks as f32;
[self.amount * left, self.lowpass * left]
}
}
/// The mixer side of the engine (`Backend`).
pub struct MixerBackend {
mixer: Arc<Mutex<Mixer>>,
@@ -311,8 +247,6 @@ pub struct Audio {
pub stopped: std::collections::BTreeMap<String, u32>,
/// The volume sliders' `SoundPreviewHandler`.
preview: crate::sound_preview::SoundPreview,
/// A mod's muffle fading out (`muffle`), advanced by `tick`.
muffle: Option<MuffleFade>,
/// `SoundEngine.lastSeenDevices`: the system default output last seen.
#[cfg(feature = "audio")]
default_seen: Option<String>,
@@ -505,7 +439,6 @@ impl Backend for MixerBackend {
owner: self.owner,
world: self.world,
started: None,
unmuffled: false,
},
);
Some(id)
@@ -533,9 +466,6 @@ impl Backend for MixerBackend {
fn set_relative(&mut self, channel: ChannelId, relative: bool) {
self.with(channel, |c| c.relative = relative);
}
fn set_unmuffled(&mut self, channel: ChannelId, unmuffled: bool) {
self.with(channel, |c| c.unmuffled = unmuffled);
}
/// `getCompleteBuffer` (cached, else decoded on the decoder thread) or
/// `getStream` (decoded on a thread of its own), then attach and
/// `alSourcePlay`. A sound that cannot load leaves its source initial,
@@ -772,13 +702,6 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
let step = f64::from(*source_rate) / f64::from(rate) * f64::from(c.pitch.max(0.0));
voices.push((*id, [left, right, gain], step));
}
// A mod's muffle: the sounds it doesn't exempt mix into a bus of their own first.
let muffling = mixer.muffle.active();
let mut bus = std::mem::take(&mut mixer.muffle.bus);
if muffling {
bus.clear();
bus.resize(data.len(), 0.0);
}
for (id, [left, right, gain], step) in voices.iter().copied() {
let Some(c) = mixer.channels.get_mut(&id) else {
continue;
@@ -788,12 +711,7 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
};
let source_channels = source_channels as usize;
let frames = samples.len() / source_channels;
let out: &mut [f32] = if muffling && !c.unmuffled {
&mut bus
} else {
&mut *data
};
for frame in out.chunks_mut(channels) {
for frame in data.chunks_mut(channels) {
let mut i = c.position as usize;
if i + 1 >= frames {
if c.looping && frames > 1 {
@@ -828,14 +746,6 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
}
}
mixer.voices = voices;
if muffling {
mixer.muffle.apply(&bus, data, channels, rate);
} else {
// Off: the next muffle starts from silence, not from the last one's filter.
mixer.muffle.current = [0.0; 2];
mixer.muffle.stages = [[0.0; 2]; 2];
}
mixer.muffle.bus = bus;
// Limit the whole buffer together, preserving waveform and stereo balance
// instead of flattening each overloaded sample; release over 200 ms
// (OpenAL Soft's output limiter, `ALC_OUTPUT_LIMITER_SOFT`, releases
@@ -1161,13 +1071,12 @@ impl Audio {
listener: [0.0; 3],
listener_yaw: 0.0,
master: 1.0,
random: crate::random_source::create(),
random: LegacyRandom::new(crate::sound_engine::unique_seed()),
source_override: None,
capture: None,
heard: Default::default(),
stopped: Default::default(),
preview: Default::default(),
muffle: None,
#[cfg(feature = "audio")]
default_seen: if live { default_output_name() } else { None },
#[cfg(feature = "audio")]
@@ -1323,13 +1232,12 @@ impl Audio {
listener: [0.0; 3],
listener_yaw: 0.0,
master: self.master,
random: crate::random_source::create(),
random: LegacyRandom::new(crate::sound_engine::unique_seed()),
source_override: None,
capture: None,
heard: Default::default(),
stopped: Default::default(),
preview: Default::default(),
muffle: None,
// The device belongs to the first player's output, which looks after it.
#[cfg(feature = "audio")]
default_seen: self.default_seen.clone(),
@@ -1462,50 +1370,6 @@ impl Audio {
}
self.sync_volumes();
self.engine.tick(paused, ctx, &mut self.backend);
// A paused game holds a mod's muffle where it is, as it holds the sounds.
if !paused && let Some(fade) = &mut self.muffle {
fade.left = fade.left.saturating_sub(1);
let level = fade.level();
if fade.left == 0 {
self.muffle = None;
self.engine.set_unmuffled(Vec::new(), &mut self.backend);
}
self.set_muffle_level(level);
}
}
/// A mod's muffle (`native_muffle_sounds`): `amount` (0 to 1) of every sound's volume taken
/// away and `lowpass` (0 to 1) dulling it, from untouched to a 300 Hz low-pass, fading in a
/// straight line to none over `ticks` client ticks; the sound events in `exempt` (namespaced,
/// `mymod:ear.ringing`) play clearly through it, those already playing too. It replaces any
/// muffle in place; `ticks` 0 or no strength ends it. The output's, so in splitscreen every
/// player hears it. False for an amount or strength outside 0 to 1.
pub fn muffle(&mut self, amount: f32, lowpass: f32, ticks: u32, exempt: &[&str]) -> bool {
if !(0.0..=1.0).contains(&amount) || !(0.0..=1.0).contains(&lowpass) {
return false;
}
let on = ticks > 0 && (amount > 0.0 || lowpass > 0.0);
let exempt = if on {
exempt
.iter()
.map(|e| crate::sound_events::normalize(e))
.collect()
} else {
Vec::new()
};
self.engine.set_unmuffled(exempt, &mut self.backend);
self.muffle = on.then_some(MuffleFade {
amount,
lowpass,
ticks,
left: ticks,
});
self.set_muffle_level(self.muffle.map_or([0.0; 2], |fade| fade.level()));
true
}
fn set_muffle_level(&mut self, level: [f32; 2]) {
if let Ok(mut m) = self.backend.mixer.lock() {
m.muffle.target = level;
}
}
/// The `uiSoundReserve` extra: positional sounds leave
/// `sound_engine::HEARD_RESERVE` static channels free (off: vanilla's pool).
@@ -1604,14 +1468,6 @@ impl Audio {
let id = self.engine.add(instance, false);
self.engine.play_delayed(id, ticks);
}
/// `SoundManager.playDelayed` of an instance the caller made: it starts
/// `ticks` sound-engine ticks from now.
pub fn play_instance_after(&mut self, instance: SoundInstance, ticks: i32) {
*self.heard.entry(instance.identifier.clone()).or_default() += 1;
self.sync_volumes();
let id = self.engine.add(instance, false);
self.engine.play_delayed(id, ticks);
}
/// `play` for this client only: a sound a packet's handler plays
/// (`playLocalSound`), which the guests play from that packet themselves.
pub fn play_unshared(&mut self, event: &str, at: Option<[f64; 3]>, volume: f32, pitch: f32) {
@@ -1626,7 +1482,7 @@ impl Audio {
"minecraft:ui.button.click",
1.0,
0.25,
crate::random_source::create(),
SoundEngine::unseeded(),
),
false,
);
@@ -1768,7 +1624,7 @@ impl Audio {
f64::from(pos[2]) + 0.5,
];
let event = crate::sound_events::normalize(event);
let instance = SoundInstance::for_jukebox_song(&event, at, crate::random_source::create());
let instance = SoundInstance::for_jukebox_song(&event, at, SoundEngine::unseeded());
self.play_instance(instance, true).0
}
/// Stop and drop a held instance.
@@ -1979,7 +1835,6 @@ mod tests {
owner: 0,
world: 0,
started: None,
unmuffled: false,
}
}
@@ -2400,123 +2255,6 @@ mod tests {
assert_eq!(data, [0.5; 2]);
}
/// One second of a `hz` sine at 48 kHz, amplitude 0.3 (looped, it repeats seamlessly).
fn sine(hz: f32) -> Vec<f32> {
(0..48_000)
.map(|i| 0.3 * (std::f32::consts::TAU * hz * i as f32 / 48_000.0).sin())
.collect()
}
fn rms(data: &[f32]) -> f32 {
(data.iter().map(|s| s * s).sum::<f32>() / data.len() as f32).sqrt()
}
/// `seconds` of the mixer's stereo output, in the 10 ms buffers an output stream asks for.
fn listen(m: &mut Mixer, seconds: f32) -> Vec<f32> {
let mut out = Vec::new();
let mut buffer = [0.0; 960];
for _ in 0..(seconds * 100.0).round() as usize {
mix(m, &mut buffer, 2, 48_000);
out.extend_from_slice(&buffer);
}
out
}
#[test]
fn a_muffle_dulls_and_quietens_every_sound_but_the_exempt_ones() {
// A looping tone heard through a muffle of `[amount, lowpass]`, once it has settled.
let heard = |hz: f32, muffle: [f32; 2], unmuffled: bool| {
let mut m = Mixer::default();
let mut c = channel(sine(hz), None, [0.0, 0.0, 2.0], true);
c.unmuffled = unmuffled;
m.channels.insert(1, c);
m.muffle.target = muffle;
listen(&mut m, 0.2);
listen(&mut m, 0.2)
};
let plain = |hz: f32| rms(&heard(hz, [0.0; 2], false));
// Fully low-passed, a 6 kHz whine all but goes and a 100 Hz hum keeps most of its level.
assert!(rms(&heard(6000.0, [0.0, 1.0], false)) < plain(6000.0) * 0.05);
assert!(rms(&heard(100.0, [0.0, 1.0], false)) > plain(100.0) * 0.8);
// Half the volume taken and no low-pass: half the level, the tone itself untouched.
let ducked = rms(&heard(6000.0, [0.5, 0.0], false)) / plain(6000.0);
assert!((ducked - 0.5).abs() < 1e-3, "{ducked}");
// An exempt sound plays through as if there were no muffle, sample for sample.
assert_eq!(
heard(6000.0, [0.5, 1.0], true),
heard(6000.0, [0.0; 2], false)
);
}
#[test]
fn a_faded_muffle_leaves_the_sound_as_it_was() {
let mixer = || {
let mut m = Mixer::default();
m.channels
.insert(1, channel(sine(3000.0), None, [2.0, 0.0, 1.0], true));
m
};
let (mut muffled, mut plain) = (mixer(), mixer());
muffled.muffle.target = [0.7, 1.0];
assert_ne!(listen(&mut muffled, 0.1), listen(&mut plain, 0.1));
muffled.muffle.target = [0.0; 2];
listen(&mut muffled, 0.5);
listen(&mut plain, 0.5);
assert_eq!(listen(&mut muffled, 0.1), listen(&mut plain, 0.1));
}
#[test]
fn a_mods_muffle_fades_with_the_game_ticks_and_spares_its_exempt_sounds() {
let mut audio = Audio::silent(Path::new("/nonexistent/horizon-test-assets"));
audio.engine.registry.add_file(
"minecraft",
br#"{"ambient.cave":{"sounds":["ambient/cave/cave1"]}}"#,
&|_, _| true,
);
audio.engine.registry.add_file(
"hztest",
br#"{"ear.ringing":{"sounds":["ear/ringing"]}}"#,
&|_, _| true,
);
// The engine makes its channels; with no output, none loads a file.
audio.engine.loaded = true;
let marks = |audio: &Audio| {
let m = audio.backend.mixer.lock().unwrap();
let mut marks: Vec<_> = m
.channels
.iter()
.map(|(&id, c)| (id, c.unmuffled))
.collect();
marks.sort();
marks
};
let level = |audio: &Audio| audio.backend.mixer.lock().unwrap().muffle.target;
let close =
|a: [f32; 2], b: [f32; 2]| (a[0] - b[0]).abs() < 1e-6 && (a[1] - b[1]).abs() < 1e-6;
// Two sounds already playing, and one exempt sound started under the muffle.
audio.play("ambient.cave", None, 1.0, 1.0);
audio.play("hztest:ear.ringing", None, 1.0, 1.0);
assert!(audio.muffle(0.8, 1.0, 4, &["hztest:ear.ringing"]));
audio.play("hztest:ear.ringing", None, 1.0, 1.0);
assert_eq!(marks(&audio), [(1, false), (2, true), (3, true)]);
assert!(close(level(&audio), [0.8, 1.0]));
let ticks = TickContext::default();
audio.tick(false, &ticks);
assert!(close(level(&audio), [0.6, 0.75]), "{:?}", level(&audio));
// A paused game holds it.
audio.tick(true, &ticks);
assert!(close(level(&audio), [0.6, 0.75]), "{:?}", level(&audio));
for _ in 0..3 {
audio.tick(false, &ticks);
}
assert_eq!(level(&audio), [0.0; 2]);
// Over: its exemptions go with it.
assert_eq!(marks(&audio), [(1, false), (2, false), (3, false)]);
// Out of range changes nothing.
assert!(!audio.muffle(1.5, 0.0, 10, &[]));
assert_eq!(level(&audio), [0.0; 2]);
}
#[test]
fn output_devices_are_looked_at_again_only_when_the_cards_change() {
// No fingerprint (not Linux, or the cards could not be read): the list
+1 -6
View File
@@ -191,18 +191,13 @@ pub fn discovery_url() -> String {
.unwrap_or_else(|| DEFAULT_DISCOVERY.to_owned())
}
pub(crate) fn agent() -> ureq::Agent {
fn agent() -> ureq::Agent {
ureq::AgentBuilder::new()
.timeout(Duration::from_secs(15))
.user_agent(&format!("Horizon/{}", horizon_core::release::VERSION))
.build()
}
/// Whether `HORIZON_API_DISCOVERY` names another services environment.
pub fn discovery_overridden() -> bool {
discovery_url() != DEFAULT_DISCOVERY
}
/// The discovery document, fetched once per game (a failure is retried next time).
pub(crate) fn discovery() -> Result<Arc<serde_json::Value>, JoinError> {
static CACHE: Mutex<Option<Arc<serde_json::Value>>> = Mutex::new(None);
@@ -1,314 +0,0 @@
//! GPU Culling's Auto mode: the setting switched by what limits the frame, measured here.
//!
//! GPU Culling moves the walk over the near sections and the listing of their draws from the
//! render thread to the GPU. That pays where the CPU is the slower of the two and costs where
//! the GPU is, and which a machine is can't be told from its parts: a fast CPU with a modest
//! GPU, a slow one with a strong GPU and every mix between depend on the scene, the render
//! distance and the resolution. So Auto watches the frames' own CPU and GPU times, and only
//! where the CPU is clearly the slower one it tries GPU Culling, measures the frame again, and
//! keeps it if the frame got faster. A trial that didn't help is undone and not repeated for
//! minutes of play, then for longer, then not until the next start, so a machine or a scene it
//! doesn't suit loses a few frames' time a few times, not every half minute.
//!
//! A verdict is the medians of a window of frames (`WINDOW`), which a few slow frames (a chunk
//! load, say) don't move. Nothing here reads the GPU or draws anything: the caller feeds it the
//! frames and applies what it answers.
/// Frames in one verdict.
pub const WINDOW: usize = 240;
/// The CPU's time must be this many times the GPU's for a window to count as CPU-bound.
const CPU_BOUND: f32 = 1.3;
/// Below this CPU time, in milliseconds, a frame is fast enough that neither matters.
const WORTH_TRYING_MS: f32 = 1.5;
/// A trial is kept when the frame's busier side dropped under this share of what it was.
const KEEP_BELOW: f32 = 0.97;
/// Windows of CPU-bound frames in a row before a trial.
const STREAK: u8 = 2;
/// Windows left out after a switch, while its effects settle.
const SETTLE: u8 = 1;
/// How long, in milliseconds of frames watched, after the first and the second trial that didn't
/// help before another; after a third there is none.
const RETRY_AFTER_MS: [f64; 2] = [5.0 * 60_000.0, 20.0 * 60_000.0];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum State {
/// Off, looking for a CPU-bound frame.
Watching,
/// On for a trial: left out `SETTLE` windows, then measured against the frame before.
Trial,
/// The trial made the frame faster: on from here.
Kept,
}
/// What Auto has decided and why, a step at a time.
#[derive(Clone, Debug)]
pub struct GpuCullingAuto {
cpu: Vec<f32>,
gpu: Vec<f32>,
on: bool,
state: State,
/// CPU-bound windows in a row.
streak: u8,
/// The frame's busier side before the trial, in milliseconds.
before: f32,
/// Windows still left out after a switch.
settling: u8,
/// The frames watched so far, in milliseconds of their own time.
clock_ms: f64,
/// When the next trial may start, on that clock.
retry_at_ms: f64,
/// Trials that didn't help.
rejected: usize,
}
impl Default for GpuCullingAuto {
fn default() -> Self {
Self::new()
}
}
impl GpuCullingAuto {
pub fn new() -> Self {
Self {
cpu: Vec::with_capacity(WINDOW),
gpu: Vec::with_capacity(WINDOW),
on: false,
state: State::Watching,
streak: 0,
before: 0.0,
settling: 0,
clock_ms: 0.0,
retry_at_ms: 0.0,
rejected: 0,
}
}
/// Whether Auto has GPU Culling on now.
pub fn on(&self) -> bool {
self.on
}
/// Where Auto is, for the status: `watching`, `trial` or `kept`.
pub fn state(&self) -> &'static str {
match self.state {
State::Watching => "watching",
State::Trial => "trial",
State::Kept => "kept",
}
}
/// Back to the start: off, with nothing measured (the mode changed, or the world did).
pub fn reset(&mut self) {
*self = Self::new();
}
/// One frame's CPU time, a GPU time where its timestamps have arrived, and its whole time,
/// in milliseconds. Answers the new state when this frame completed a window that changed it.
pub fn observe(&mut self, cpu_ms: f32, gpu_ms: Option<f32>, frame_ms: f32) -> Option<bool> {
self.clock_ms += f64::from(frame_ms);
self.cpu.push(cpu_ms);
if let Some(gpu) = gpu_ms {
self.gpu.push(gpu);
}
if self.cpu.len() < WINDOW {
return None;
}
let cpu = median(&mut self.cpu);
// A GPU that can't be timed leaves nothing to compare with: Auto stays as it is.
let gpu = (self.gpu.len() >= WINDOW / 4).then(|| median(&mut self.gpu));
self.cpu.clear();
self.gpu.clear();
self.verdict(cpu, gpu?)
}
fn verdict(&mut self, cpu: f32, gpu: f32) -> Option<bool> {
if self.settling > 0 {
self.settling -= 1;
return None;
}
match self.state {
State::Kept => None,
State::Watching => {
if self.clock_ms < self.retry_at_ms {
return None;
}
let bound = cpu >= WORTH_TRYING_MS && cpu > gpu * CPU_BOUND;
self.streak = if bound { self.streak + 1 } else { 0 };
if self.streak < STREAK {
return None;
}
self.streak = 0;
self.before = cpu.max(gpu);
self.state = State::Trial;
self.settling = SETTLE;
self.on = true;
Some(true)
}
State::Trial => {
if cpu.max(gpu) < self.before * KEEP_BELOW {
self.state = State::Kept;
return None;
}
self.state = State::Watching;
self.retry_at_ms = RETRY_AFTER_MS
.get(self.rejected)
.map_or(f64::INFINITY, |wait| self.clock_ms + wait);
self.rejected += 1;
self.on = false;
Some(false)
}
}
}
}
fn median(values: &mut [f32]) -> f32 {
values.sort_by(f32::total_cmp);
values[values.len() / 2]
}
#[cfg(test)]
mod tests {
use super::*;
/// A window of frames at constant times, each as long as its busier side; the last answer.
fn window(auto: &mut GpuCullingAuto, cpu: f32, gpu: f32) -> Option<bool> {
(0..WINDOW)
.filter_map(|_| auto.observe(cpu, Some(gpu), cpu.max(gpu)))
.last()
}
/// A machine whose CPU limits the frame is tried, and keeps GPU Culling when the frame
/// gets faster.
#[test]
fn a_cpu_bound_frame_tries_it_and_keeps_it_when_it_helps() {
let mut auto = GpuCullingAuto::new();
assert!(!auto.on());
// One CPU-bound window is not yet a verdict.
assert_eq!(window(&mut auto, 5.0, 2.0), None);
assert_eq!(window(&mut auto, 5.0, 2.0), Some(true));
assert!(auto.on());
assert_eq!(auto.state(), "trial");
// The window after the switch is left out, whatever it shows.
assert_eq!(window(&mut auto, 9.0, 9.0), None);
// The frame's busier side went from 5.0 to 3.0 ms.
assert_eq!(window(&mut auto, 3.0, 2.6), None);
assert_eq!(auto.state(), "kept");
assert!(auto.on());
// Kept stays kept, even if a later scene is GPU-bound.
assert_eq!(window(&mut auto, 1.0, 8.0), None);
assert!(auto.on());
}
/// Where it didn't help, it is undone, and not tried again for 5 minutes, then 20, then
/// not at all: a scene it doesn't suit is not tried every half minute.
#[test]
fn a_trial_that_does_not_help_is_undone_and_waits_longer_each_time() {
let mut auto = GpuCullingAuto::new();
// One window is 240 frames of 5 ms: 1.2 s. A trial, and its rejection:
let reject = |auto: &mut GpuCullingAuto| {
window(auto, 5.0, 2.0);
assert_eq!(window(auto, 5.0, 2.0), Some(true));
window(auto, 5.0, 2.0);
// The GPU took over the frame: 5.0 ms before, 5.4 after.
assert_eq!(window(auto, 2.5, 5.4), Some(false));
assert!(!auto.on());
assert_eq!(auto.state(), "watching");
};
reject(&mut auto);
// CPU-bound as ever, and left alone for 5 minutes of frames: 250 windows.
let mut waited = 0;
while window(&mut auto, 5.0, 2.0).is_none() {
waited += 1;
}
assert!((248..=252).contains(&waited), "{waited}");
// That window started the second trial; it is rejected too.
window(&mut auto, 5.0, 2.0);
assert_eq!(window(&mut auto, 2.5, 5.4), Some(false));
// Now 20 minutes: 1000 windows.
let mut waited = 0;
while window(&mut auto, 5.0, 2.0).is_none() {
waited += 1;
}
assert!((998..=1002).contains(&waited), "{waited}");
window(&mut auto, 5.0, 2.0);
assert_eq!(window(&mut auto, 2.5, 5.4), Some(false));
// A third rejection is for good.
for _ in 0..5000 {
assert_eq!(window(&mut auto, 5.0, 2.0), None);
}
assert!(!auto.on());
}
/// A GPU-bound machine is never switched: its frame is the GPU's.
#[test]
fn a_gpu_bound_frame_is_left_alone() {
let mut auto = GpuCullingAuto::new();
for _ in 0..20 {
assert_eq!(window(&mut auto, 0.5, 3.6), None);
}
assert!(!auto.on());
}
/// Within the margin neither side limits the frame, and a single CPU-bound window among
/// others breaks no streak into a trial.
#[test]
fn neither_a_balanced_nor_an_occasional_cpu_bound_frame_is_tried() {
let mut auto = GpuCullingAuto::new();
for _ in 0..10 {
assert_eq!(window(&mut auto, 3.0, 2.6), None);
}
for _ in 0..10 {
assert_eq!(window(&mut auto, 5.0, 2.0), None);
assert_eq!(window(&mut auto, 2.0, 2.0), None);
}
assert!(!auto.on());
}
/// Fast frames aren't worth a trial, bound or not.
#[test]
fn a_frame_under_the_floor_is_not_tried() {
let mut auto = GpuCullingAuto::new();
for _ in 0..10 {
assert_eq!(window(&mut auto, 1.2, 0.2), None);
}
assert!(!auto.on());
}
/// Without GPU times (a GPU that can't be timed) there is nothing to compare.
#[test]
fn frames_without_gpu_times_decide_nothing() {
let mut auto = GpuCullingAuto::new();
for _ in 0..WINDOW * 10 {
assert_eq!(auto.observe(8.0, None, 8.0), None);
}
assert!(!auto.on());
}
/// A spike in a window doesn't move its verdict: the median ignores it.
#[test]
fn a_few_slow_frames_do_not_make_a_window_cpu_bound() {
let mut auto = GpuCullingAuto::new();
for _ in 0..4 {
let mut answer = None;
for i in 0..WINDOW {
// One frame in ten is a 30 ms chunk load, the rest are GPU-bound.
let cpu = if i % 10 == 0 { 30.0 } else { 0.8 };
answer = answer.or(auto.observe(cpu, Some(3.0), 3.0));
}
assert_eq!(answer, None);
}
assert!(!auto.on());
}
#[test]
fn reset_starts_over() {
let mut auto = GpuCullingAuto::new();
window(&mut auto, 5.0, 2.0);
window(&mut auto, 5.0, 2.0);
assert!(auto.on());
auto.reset();
assert!(!auto.on());
assert_eq!(auto.state(), "watching");
assert_eq!(window(&mut auto, 5.0, 2.0), None);
}
}
+2 -36
View File
@@ -17,15 +17,11 @@ pub struct BanDetails {
pub reason_message: Option<String>,
}
/// authlib's `UserProperties`, as far as the title screen and the friends service read it.
/// authlib's `UserProperties`, as far as the title screen reads it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UserProperties {
/// `UserFlag.SERVERS_ALLOWED` (`privileges.multiplayerServer.enabled`).
pub servers_allowed: bool,
/// `UserFlag.FRIENDS_ENABLED` (`friendsPreferences.friends`).
pub friends_enabled: bool,
/// `UserFlag.ACCEPT_FRIEND_INVITES` (`friendsPreferences.acceptInvites`).
pub accept_friend_invites: bool,
/// `bannedScopes.get("MULTIPLAYER")`.
pub multiplayer_ban: Option<BanDetails>,
}
@@ -34,8 +30,6 @@ impl Default for UserProperties {
fn default() -> Self {
Self {
servers_allowed: true,
friends_enabled: false,
accept_friend_invites: false,
multiplayer_ban: None,
}
}
@@ -215,13 +209,6 @@ pub fn parse_properties(body: &str) -> Option<UserProperties> {
.pointer("/privileges/multiplayerServer/enabled")
.and_then(|e| e.as_bool())
.unwrap_or(false);
let toggle = |name: &str| {
v.pointer(&format!("/friendsPreferences/{name}"))
.and_then(|f| f.as_str())
.is_some_and(|f| f.eq_ignore_ascii_case("ENABLED"))
};
let friends_enabled = toggle("friends");
let accept_friend_invites = toggle("acceptInvites");
let multiplayer_ban = v
.pointer("/banStatus/bannedScopes/MULTIPLAYER")
.filter(|b| b.is_object())
@@ -235,8 +222,6 @@ pub fn parse_properties(body: &str) -> Option<UserProperties> {
});
Some(UserProperties {
servers_allowed,
friends_enabled,
accept_friend_invites,
multiplayer_ban,
})
}
@@ -290,24 +275,6 @@ mod tests {
assert!(clean.servers_allowed && clean.multiplayer_ban.is_none());
}
#[test]
fn friends_preferences_read_their_two_flags() {
let body = r#"{"privileges":{"multiplayerServer":{"enabled":true}},
"friendsPreferences":{"friends":"ENABLED","acceptInvites":"DISABLED"}}"#;
let p = parse_properties(body).unwrap();
assert!(p.friends_enabled);
assert!(!p.accept_friend_invites);
let other = parse_properties(
r#"{"friendsPreferences":{"friends":"DISABLED","acceptInvites":"ENABLED"}}"#,
)
.unwrap();
assert!(!other.friends_enabled);
assert!(other.accept_friend_invites);
// The offline fallback has neither flag, as `OFFLINE_PROPERTIES` does.
assert!(!UserProperties::default().friends_enabled);
assert!(!UserProperties::default().accept_friend_invites);
}
#[test]
fn parses_profile_actions() {
let a = parse_profile_actions(
@@ -330,7 +297,6 @@ mod tests {
properties: UserProperties {
servers_allowed: true,
multiplayer_ban: Some(ban.clone()),
..Default::default()
},
actions: ProfileActions {
forced_name_change: true,
@@ -351,7 +317,7 @@ mod tests {
let no_servers = AccountStatus {
properties: UserProperties {
servers_allowed: false,
..Default::default()
multiplayer_ban: None,
},
..Default::default()
};
+1 -59
View File
@@ -1,7 +1,6 @@
//! Biome colours from the pinned biome definitions and colormaps
//! (`Biome.getGrassColor`/`getFoliageColor`/..., `BlockTintSources`).
use crate::Result;
use horizon_client_core::particle_type::ParticleType;
use horizon_core::{noise::SimplexNoise, position::BlockPos, random::LegacyRandom};
use serde_json::Value;
use std::{
@@ -107,31 +106,11 @@ pub struct BiomeInfo {
/// The biome's own fog colour attribute; otherwise the dimension's applies.
pub fog: Option<u32>,
pub water_fog: u32,
/// `minecraft:visual/ambient_particles` entries and their probabilities.
pub ambient_particles: Vec<(ParticleType, f32)>,
/// `BiomeGenerationSettings.features`: the placed feature ids of each
/// `GenerationStep.Decoration` step, in order.
pub features: Vec<Vec<String>>,
/// The biome's environment attributes (`Biome.getAttributes`): its layer in the
/// environment attribute stack.
pub attributes: horizon_world::environment_attributes::AttributeMap,
}
impl BiomeInfo {
fn ambient_particles(v: &Value) -> Vec<(ParticleType, f32)> {
let list = &v["attributes"]["minecraft:visual/ambient_particles"];
let entries = list["argument"].as_array().or_else(|| list.as_array());
entries
.into_iter()
.flatten()
.filter_map(|entry| {
Some((
ParticleType::from_name(entry["particle"]["type"].as_str()?)?,
entry["probability"].as_f64().unwrap_or(0.0) as f32,
))
})
.collect()
}
/// `Biome.getHeightAdjustedTemperature`: the frozen modifier, then cooler
/// with height above `sea_level + 17`.
pub fn temperature_at(&self, pos: BlockPos, sea_level: i32) -> f32 {
@@ -266,7 +245,6 @@ impl BiomeInfo {
sky: color(&attributes["minecraft:visual/sky_color"]),
fog: color(&attributes["minecraft:visual/fog_color"]),
water_fog: color(&attributes["minecraft:visual/water_fog_color"]).unwrap_or(0x050533),
ambient_particles: Self::ambient_particles(v),
features: v["features"]
.as_array()
.into_iter()
@@ -280,11 +258,6 @@ impl BiomeInfo {
.collect()
})
.collect(),
attributes: horizon_world::environment_attributes::AttributeMap::parse(attributes)
.unwrap_or_else(|e| {
log::warn!("biome attributes: {e}");
Default::default()
}),
name,
}
}
@@ -401,17 +374,6 @@ impl Biomes {
pub fn get(&self, id: u16) -> &BiomeInfo {
&self.list[usize::from(id).min(self.list.len() - 1)]
}
/// The name of biome `id`, plains (as for an unknown name) when the id is
/// past this table: a column a server sent under its earlier, longer
/// registry (a proxy's previous server) keeps its ids until the next
/// login replaces it. `get` clamps such an id to the last entry instead,
/// so for that stale column the colours and this name can come from
/// different biomes until the login.
pub fn name(&self, id: u16) -> &str {
self.list
.get(usize::from(id))
.map_or("minecraft:plains", |b| b.name.as_str())
}
pub fn grass(&self, id: u16, x: f64, z: f64) -> u32 {
let b = self.get(id);
match b.modifier {
@@ -601,9 +563,7 @@ mod pack_tests {
#[test]
fn packs_replace_in_place_and_add_after_the_builtin_biomes() {
let base = builtin(&["minecraft:meadow", "minecraft:plains", "minecraft:river"]);
let red = json!({"temperature": 2.0, "effects": {"water_color": "#ff0000"},
"attributes": {"minecraft:visual/ambient_particles": {
"argument": [{"particle": {"type": "minecraft:ash"}, "probability": 0.25}]}}});
let red = json!({"temperature": 2.0, "effects": {"water_color": "#ff0000"}});
let hot = json!({"effects": {"grass_color": 0x123456}});
let files = [
("pack:zeta", &hot),
@@ -616,9 +576,6 @@ mod pack_tests {
assert_eq!(world.id("minecraft:river"), 2);
assert_eq!(world.get(2).water, 0xFF0000);
assert_eq!(world.get(2).temperature, 2.0);
assert_eq!(world.get(2).ambient_particles.len(), 1);
assert_eq!(world.get(2).ambient_particles[0].0.name(), "ash");
assert_eq!(world.get(2).ambient_particles[0].1, 0.25);
// New biomes follow, in registry order.
assert_eq!(world.id("minecraft:molten_peaks"), 3);
assert_eq!(world.id("pack:zeta"), 4);
@@ -643,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");
+13 -34
View File
@@ -357,24 +357,6 @@ impl Light {
}
/// The smallest form of packed light per block.
pub fn packed(light: Box<[u8; 4096]>) -> Self {
match Self::classify(&light) {
(false, false, first) => Self::Uniform(first),
(true, false, first) => Self::Sky(Self::pack_layer(&light, 4), first & 15),
(false, true, first) => Self::Block(Self::pack_layer(&light, 0), first >> 4),
(true, true, _) => Self::Full(light),
}
}
/// The smallest form of packed light per block, allocating a 4 KiB box only
/// when both sky and block light vary within the section.
pub fn from_slice(light: &[u8; 4096]) -> Self {
match Self::classify(light) {
(false, false, first) => Self::Uniform(first),
(true, false, first) => Self::Sky(Self::pack_layer(light, 4), first & 15),
(false, true, first) => Self::Block(Self::pack_layer(light, 0), first >> 4),
(true, true, _) => Self::Full(Box::new(*light)),
}
}
fn classify(light: &[u8; 4096]) -> (bool, bool, u8) {
let first = light[0];
let (mut sky_varies, mut block_varies) = (false, false);
for &v in light.iter() {
@@ -382,14 +364,19 @@ impl Light {
sky_varies |= d & 0xF0 != 0;
block_varies |= d & 15 != 0;
}
(sky_varies, block_varies, first)
}
fn pack_layer(light: &[u8; 4096], shift: u32) -> Box<[u8; 2048]> {
let mut layer = Box::new([0u8; 2048]);
for (out, pair) in layer.iter_mut().zip(light.as_chunks::<2>().0) {
*out = (pair[0] >> shift & 15) | (pair[1] >> shift & 15) << 4;
let layer = |shift: u32| {
let mut layer = Box::new([0u8; 2048]);
for (out, pair) in layer.iter_mut().zip(light.as_chunks::<2>().0) {
*out = (pair[0] >> shift & 15) | (pair[1] >> shift & 15) << 4;
}
layer
};
match (sky_varies, block_varies) {
(false, false) => Self::Uniform(first),
(true, false) => Self::Sky(layer(4), first & 15),
(false, true) => Self::Block(layer(0), first >> 4),
(true, true) => Self::Full(light),
}
layer
}
/// The packed light of blocks `at..at + out.len()`, into `out`.
#[inline]
@@ -589,7 +576,7 @@ pub struct Chunk {
/// Block entities read from a save, moved into the world on arrival.
pub block_entities: Vec<((i32, i32, i32), block_entity::BlockEntity)>,
/// `CustomName` of the container block entities (shown as the screen title).
pub block_names: Vec<((i32, i32, i32), Tag)>,
pub block_names: Vec<((i32, i32, i32), String)>,
/// Generation postprocessing marks (ProtoChunk.postProcessing) as world
/// positions, applied once by LevelChunk.postProcessGeneration.
pub postprocess: Vec<[i32; 3]>,
@@ -633,14 +620,6 @@ impl Chunk {
entities: Vec::new(),
}
}
/// The original `structures.starts`, including starts with unknown pieces.
pub fn saved_structure_starts(&self) -> Option<&horizon_world::nbt::Compound> {
let structures = self.saved.as_ref()?.root.get(&"structures".into())?;
match structures.get("starts")? {
horizon_world::nbt::Tag::Compound(starts) => Some(starts),
_ => None,
}
}
/// The minimum block Y of the column.
#[inline]
pub fn min_y(&self) -> i32 {
@@ -59,9 +59,6 @@ pub enum BlockEntity {
CreakingHeart(Box<CreakingHeart>),
/// `BannerBlockEntity`: the pattern layers.
Banner(Banner),
/// `CopperGolemStatueBlockEntity`: its own components, which hold the
/// custom name of the golem it was (`createStatue`).
CopperGolemStatue(Compound),
/// `CrafterBlockEntity`.
Crafter(Crafter),
/// `SpawnerBlockEntity` (`crate::mobs::spawner`).
@@ -83,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)]
+2 -55
View File
@@ -208,7 +208,7 @@ where
Ok(resolve_world_movement(
self,
MovementRequest {
entity: context.entity().unwrap_or(EntityIdentity(0)),
entity: EntityIdentity(0),
context,
bounds,
movement,
@@ -279,7 +279,7 @@ impl SceneCollisions<'_, '_> {
Ok(resolve_world_movement(
self,
MovementRequest {
entity: context.entity().unwrap_or(EntityIdentity(0)),
entity: EntityIdentity(0),
context,
bounds,
movement,
@@ -300,7 +300,6 @@ impl SceneCollisions<'_, '_> {
CollisionContext::for_entity(
CollisionEntity {
identity: EntityIdentity(0),
kind: horizon_world::collision_context::EntityContextKind::Living,
y,
descending: false,
main_hand: Some(CollisionItem::EMPTY),
@@ -336,58 +335,6 @@ where
{
no_collision_in(world, bounds, true)
}
/// `CollisionGetter.noCollision(entity, bounds)`: retain the real entity snapshot.
pub fn no_collision_with_context<W, E>(
world: &mut W,
bounds: Aabb,
context: CollisionContext<'_>,
) -> bool
where
W: BlockCollisionWorld<Error = E> + EntityMovementWorld<Error = E>,
{
no_world_collision(
world,
CollisionCheckRequest {
entity: context.entity().unwrap_or(EntityIdentity(0)),
context,
bounds,
candidate_budget: 1_000_000,
},
)
.unwrap_or(false)
}
/// `Level.loadedAndEntityCanStandOn(pos, projectile)` with the projectile's
/// feet at `entity_y` (`loadedAndEntityCanStandOnFace` with `UP`):
/// `Block.isFaceFull(state.getCollisionShape(level, pos,
/// CollisionContext.of(projectile)), UP)`. A projectile is not living and never
/// falls (it moves by `setPos`), so powder snow and liquids hold nothing up
/// and only scaffolding reads the height. An unloaded or out-of-world block
/// reads as air.
pub fn projectile_can_stand_on(
world: &World,
binding: &BlockCollisionRegistry<'_>,
pos: BlockPos,
entity_y: f64,
) -> bool {
use horizon_core::geometry::{Direction, ShapeOp};
let mut scene = SceneCollisions {
world,
binding,
subject: None,
};
let state = world.block(pos.x, pos.y, pos.z);
let context = CollisionContext::position(entity_y);
binding
.collision_shape_with_context(&mut scene, state, pos, &context)
.is_ok_and(|shape| {
shape.is_block()
|| shape.face_shape(Direction::Up).is_ok_and(|face| {
VoxelShape::block()
.join_is_not_empty(&face, ShapeOp::XOR)
.is_ok_and(|differs| !differs)
})
})
}
fn no_collision_in<W, E>(world: &mut W, bounds: Aabb, fluids: bool) -> bool
where
W: BlockCollisionWorld<Error = E> + EntityMovementWorld<Error = E>,
+14 -78
View File
@@ -19,75 +19,26 @@ pub struct ModMenuType {
pub slots: usize,
pub data_slots: usize,
pub results: Vec<usize>,
/// its crafting grid, if it has one (`crafting`).
pub crafting: Option<ModCrafting>,
/// its linked slots, if it has any (`links`).
pub links: Option<ModLinks>,
}
/// a mod menu type's linked slots: `count` container slots from `from` on,
/// which show the slots of the containers a native mod links them to
/// (`native_menu_links`) instead of the block entity's.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ModLinks {
pub from: usize,
pub count: usize,
}
impl ModLinks {
/// The linked container slots.
pub fn slots(&self) -> std::ops::Range<usize> {
self.from..self.from + self.count
}
}
/// a mod menu type's crafting grid: `size`x`size` container slots from
/// `grid` on, row by row, and the slot showing what they craft. The block
/// entity holds the grid; the result is the menu's.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ModCrafting {
pub grid: usize,
pub size: usize,
pub result: usize,
}
impl ModCrafting {
/// The grid's container slots.
pub fn grid_slots(&self) -> std::ops::Range<usize> {
self.grid..self.grid + self.size * self.size
}
}
static MOD_MENU_TYPES: std::sync::OnceLock<Vec<ModMenuType>> = std::sync::OnceLock::new();
/// One menu type as [`register_mod_menu_types`] takes it: `(id, slots, data
/// slots, results, crafting, links)`.
pub type ModMenuTypeEntry = (
String,
usize,
usize,
Vec<usize>,
Option<ModCrafting>,
Option<ModLinks>,
);
/// registers the content mods' menu types, a list set once per process
/// (false when it already was).
pub fn register_mod_menu_types(menus: impl IntoIterator<Item = ModMenuTypeEntry>) -> bool {
/// registers the content mods' menu types, `(id, slots, data slots,
/// results)`, a list set once per process (false when it already was).
pub fn register_mod_menu_types(
menus: impl IntoIterator<Item = (String, usize, usize, Vec<usize>)>,
) -> bool {
if MOD_MENU_TYPES.get().is_some() {
return false;
}
let menus =
menus.into_iter().map(
|(id, slots, data_slots, results, crafting, links)| ModMenuType {
id: Box::leak(id.into_boxed_str()),
slots,
data_slots,
results,
crafting,
links,
},
);
let menus = menus
.into_iter()
.map(|(id, slots, data_slots, results)| ModMenuType {
id: Box::leak(id.into_boxed_str()),
slots,
data_slots,
results,
});
MOD_MENU_TYPES.set(menus.collect()).is_ok()
}
@@ -441,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,
] {
+15 -245
View File
@@ -141,8 +141,7 @@ impl DataPack {
pub fn discover(world: &Path) -> Vec<DataPack> {
discover_folder(&world.join("datapacks"))
}
/// `FolderRepositorySource.loadPacks`: packs in Create World's temporary
/// folder or a world's data-pack directory, with required folder metadata.
/// Packs in Create World's temporary pack folder, before a world exists.
pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
@@ -151,7 +150,7 @@ pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
for e in entries.flatten() {
let name = e.file_name().to_string_lossy().into_owned();
let path = e.path();
if path.is_dir() && folder_metadata_present(&path) {
if path.is_dir() && path.join("pack.mcmeta").exists() {
packs.push(DataPack {
id: format!("file/{name}"),
source: Source::Dir(path),
@@ -169,22 +168,6 @@ pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
packs.sort_by(|a, b| a.id.cmp(&b.id));
packs
}
/// `Pack.readPackMetadata`: even its fallback codec requires a pack section
/// and description. Keep this folder gate separate from format compatibility
/// and the existing interpretation of the description component.
fn folder_metadata_present(path: &Path) -> bool {
let Some(meta) = std::fs::read(path.join("pack.mcmeta"))
.ok()
.and_then(|bytes| crate::packs::parse_meta(&bytes))
else {
return false;
};
meta.as_object()
.and_then(|root| root.get("pack"))
.and_then(serde_json::Value::as_object)
.and_then(|pack| pack.get("description"))
.is_some_and(|description| !description.is_null())
}
/// The resources root's built-in feature packs (`ServerPacksSource`
/// scanning the game's `data/minecraft/datapacks`): every folder with a
/// `pack.mcmeta`, a feature pack (`PackSource.FEATURE`). Off until a world
@@ -278,43 +261,6 @@ pub fn screen_packs(dir: &Path, resources: &Path) -> Vec<crate::pack_screen::Pac
}
infos
}
/// The folder of a game folder whose packs every new world there starts with: a modpack's profile
/// keeps the packs of its data-only jars here (`horizon-port-pack`). Each world gets its own copy,
/// as Create World's packs do, so a world keeps working when the folder changes. Vanilla has no
/// such folder, and without one nothing here happens.
pub const NEW_WORLD_PACKS: &str = "new-world-datapacks";
/// Create World's temporary pack folder holding `game_dir`'s [`NEW_WORLD_PACKS`], and their ids,
/// to show them selected in the Data Packs screen. `None` when there are none.
pub fn seed_create_world(game_dir: &Path) -> Option<(PathBuf, Vec<String>)> {
let from = game_dir.join(NEW_WORLD_PACKS);
let ids: Vec<String> = discover_folder(&from).into_iter().map(|p| p.id).collect();
if ids.is_empty() {
return None;
}
let seeded = temp_folder()
.map_err(|e| e.to_string())
.and_then(|dir| copy(&from, &dir).map(|()| dir));
match seeded {
Ok(dir) => Some((dir, ids)),
Err(e) => {
log::warn!("Horizon: data packs for new worlds: {e}");
None
}
}
}
/// A world about to be created without Create World (`--world`, the control socket) gets
/// `game_dir`'s [`NEW_WORLD_PACKS`]. The game turns on every pack it finds in a world.
pub fn install_new_world_packs(game_dir: &Path, world: &Path) -> Result<(), String> {
let from = game_dir.join(NEW_WORLD_PACKS);
let exists = |name: &str| world.join(name).exists();
if !from.is_dir() || exists("level.dat") || exists("level.dat_old") {
return Ok(());
}
copy(&from, &world.join("datapacks"))
}
/// A new empty folder for Create World's packs
/// (`Files.createTempDirectory("mcworld-")`).
pub fn temp_folder() -> std::io::Result<PathBuf> {
@@ -662,7 +608,7 @@ pub struct DataStore {
/// `item_modifier` files, for `/item modify`.
pub item_modifiers: BTreeMap<String, serde_json::Value>,
/// The content tables' raw files from the packs after the built-in one:
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `dialog/`, `damage_type/`, `data_maps/`, `enchantment/` and `tags/…/` files, by
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `dialog/`, `damage_type/`, `data_maps/` and `tags/…/` files, by
/// `<kind-dir>/<ns>:<path>`, each id's files in pack order. The tables
/// merge them over what they read from the resource root.
pub files: BTreeMap<String, Vec<Vec<u8>>>,
@@ -671,7 +617,7 @@ pub struct DataStore {
}
/// `<kind-dir>/<ns>:<path>` for a pack file the content tables read:
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `trade_set/`,
/// `villager_trade/`, `dialog/`, `damage_type/`, `data_maps/`, `enchantment/` or `tags/<sub>/`, where the id keeps the sub
/// `villager_trade/`, `dialog/`, `damage_type/`, `data_maps/` or `tags/<sub>/`, where the id keeps the sub
/// path so a nested tag kind stays addressable.
fn content_id(file: &str) -> Option<String> {
let rest = file.strip_prefix("data/")?;
@@ -679,7 +625,7 @@ fn content_id(file: &str) -> Option<String> {
let (kind, rest) = rest.split_once('/')?;
let kind = match kind {
"recipe" | "recipe_filter" | "loot_table" | "advancement" | "trade_set"
| "villager_trade" | "dialog" | "damage_type" | "data_maps" | "enchantment" => kind,
| "villager_trade" | "dialog" | "damage_type" | "data_maps" => kind,
// A tag id is `{ns}:{path-under-tags}` (`tags/worldgen/biome/
// test:sub/mytag`), the whole path after the namespace, so nested
// registries keep their directory (`TagLoader`'s id shape).
@@ -713,7 +659,6 @@ impl DataStore {
"damage_type",
"data_maps",
"dialog",
"enchantment",
"function",
"item_modifier",
"loot_table",
@@ -769,55 +714,15 @@ impl DataStore {
}
s
}
/// `ServerFunctionLibrary.getTag`: only valid function tags expand
/// (nested tags included, each function once).
/// `FunctionManager.getTag`: the functions a tag expands to (nested tags
/// included, each function once).
pub fn tag_functions(&self, id: &str) -> Vec<String> {
let mut out = Vec::new();
let mut seen_fns = std::collections::HashSet::new();
let mut seen_tags = std::collections::HashSet::new();
let invalid = self.invalid_function_tags(id);
self.expand(id, &mut out, &mut seen_fns, &mut seen_tags, &invalid);
self.expand(id, &mut out, &mut seen_fns, &mut seen_tags);
out
}
/// `TagLoader.tryBuildTag`: missing required members invalidate their tag and
/// required parents. Optional invalid children contribute nothing.
/// Resolve against the current accepted function map, without caching a
/// candidate store or changing the existing expansion's cycle handling.
fn invalid_function_tags<'a>(&'a self, id: &'a str) -> std::collections::HashSet<&'a str> {
let mut invalid = std::collections::HashSet::new();
let mut parents: std::collections::HashMap<&str, Vec<&str>> =
std::collections::HashMap::new();
let mut seen = std::collections::HashSet::new();
let mut pending = vec![id];
while let Some(tag) = pending.pop() {
if !seen.insert(tag) {
continue;
}
let Some(entries) = self.function_tags.get(tag) else {
invalid.insert(tag);
continue;
};
for (entry, required) in entries {
if let Some(child) = entry.strip_prefix('#') {
pending.push(child);
if *required {
parents.entry(child).or_default().push(tag);
}
} else if *required && !self.functions.contains_key(entry) {
invalid.insert(tag);
}
}
}
pending.extend(invalid.iter().copied());
while let Some(child) = pending.pop() {
for &parent in parents.get(child).into_iter().flatten() {
if invalid.insert(parent) {
pending.push(parent);
}
}
}
invalid
}
/// Depth-first walk keeping first-seen order. Each tag is expanded at most
/// once, so reference cycles end and nesting depth is not capped.
fn expand<'a>(
@@ -826,19 +731,18 @@ impl DataStore {
out: &mut Vec<String>,
seen_fns: &mut std::collections::HashSet<&'a str>,
seen_tags: &mut std::collections::HashSet<&'a str>,
invalid: &std::collections::HashSet<&'a str>,
) {
if invalid.contains(id) || !seen_tags.insert(id) {
if !seen_tags.insert(id) {
return;
}
let Some(entries) = self.function_tags.get(id) else {
return;
};
for (e, _) in entries {
for (e, required) in entries {
match e.strip_prefix('#') {
Some(inner) => self.expand(inner, out, seen_fns, seen_tags, invalid),
Some(inner) => self.expand(inner, out, seen_fns, seen_tags),
None => {
if self.functions.contains_key(e) && seen_fns.insert(e) {
if (self.functions.contains_key(e) || *required) && seen_fns.insert(e) {
out.push(e.clone());
}
}
@@ -1020,97 +924,6 @@ pub fn worldgen_overrides(
mod tests {
use super::*;
#[test]
fn folder_metadata_is_required_before_contents_are_discovered() {
let root =
std::env::temp_dir().join(format!("horizon-folder-metadata-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
for (name, metadata) in [
("broken", Some("{")),
("root_empty", Some("{}")),
("root_array", Some("[]")),
("pack_empty", Some(r#"{"pack":{}}"#)),
("pack_string", Some(r#"{"pack":"bad"}"#)),
("missing", None),
(
"full",
Some(
r#"{"pack":{"description":"full","pack_format":121,"min_format":[121,0],"max_format":[121,0]}}"#,
),
),
(
"description",
Some(r#"{"pack":{"description":"description only"}}"#),
),
("numeric", Some(r#"{"pack":{"description":42}}"#)),
("null", Some(r#"{"pack":{"description":null}}"#)),
("bom", Some("\u{feff}{\"pack\":{\"description\":\"BOM\"}}")),
(
"comment",
Some("{\"pack\":{\"description\":\"comment\"}}\n// trailing"),
),
] {
let dir = root.join(name);
std::fs::create_dir_all(dir.join("data/h146/function")).unwrap();
std::fs::write(
dir.join(format!("data/h146/function/{name}.mcfunction")),
"scoreboard players set admitted h146 1\n",
)
.unwrap();
if let Some(metadata) = metadata {
std::fs::write(dir.join("pack.mcmeta"), metadata).unwrap();
}
}
let packs = discover_folder(&root);
assert_eq!(
packs.iter().map(|p| p.id.as_str()).collect::<Vec<_>>(),
[
"file/bom",
"file/comment",
"file/description",
"file/full",
"file/numeric"
]
);
let store = DataStore::load(&packs.iter().collect::<Vec<_>>());
assert_eq!(
store
.functions
.keys()
.map(String::as_str)
.collect::<Vec<_>>(),
[
"h146:bom",
"h146:comment",
"h146:description",
"h146:full",
"h146:numeric"
]
);
let numeric = packs.iter().find(|p| p.id == "file/numeric").unwrap();
let raw: serde_json::Value =
serde_json::from_slice(&numeric.read("pack.mcmeta").unwrap()).unwrap();
assert_eq!(raw["pack"]["description"], 42);
// Builtin and feature construction still have their independent paths.
let resources = root.join("resources");
let feature = resources.join("data/minecraft/datapacks/feature");
std::fs::create_dir_all(&feature).unwrap();
std::fs::write(
feature.join("pack.mcmeta"),
r#"{"pack":{"description":"feature"}}"#,
)
.unwrap();
assert_eq!(
feature_packs(&resources)
.iter()
.map(|p| p.id.as_str())
.collect::<Vec<_>>(),
["feature"]
);
assert_eq!(DataPack::builtin(&resources).id, "vanilla");
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn data_map_files_are_content_files_keyed_by_map_namespace_and_registry() {
assert_eq!(
@@ -1138,35 +951,6 @@ mod tests {
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_game_folder_s_new_world_packs_go_into_new_worlds_only() {
let root = std::env::temp_dir().join(format!("hz-new-world-packs-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let (game, world, old) = (root.join("game"), root.join("new"), root.join("old"));
assert!(seed_create_world(&game).is_none());
assert_eq!(install_new_world_packs(&game, &world), Ok(()));
assert!(!world.join("datapacks").exists());
for pack in ["void", "islands"] {
let dir = game.join(NEW_WORLD_PACKS).join(pack);
std::fs::create_dir_all(dir.join("data/v/function")).unwrap();
std::fs::write(dir.join("pack.mcmeta"), r#"{"pack":{"description":""}}"#).unwrap();
std::fs::write(dir.join("data/v/function/a.mcfunction"), "say a").unwrap();
}
let (temp, ids) = seed_create_world(&game).unwrap();
assert_eq!(ids, ["file/islands", "file/void"]);
assert!(temp.join("void/data/v/function/a.mcfunction").is_file());
let _ = std::fs::remove_dir_all(&temp);
install_new_world_packs(&game, &world).unwrap();
let ids: Vec<String> = discover(&world).into_iter().map(|p| p.id).collect();
assert_eq!(ids, ["file/islands", "file/void"]);
// A world that exists already keeps the packs it has.
std::fs::create_dir_all(&old).unwrap();
std::fs::write(old.join("level.dat"), "").unwrap();
install_new_world_packs(&game, &old).unwrap();
assert!(discover(&old).is_empty());
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn enabled_packs_override_worldgen_in_order() {
let dir = std::env::temp_dir().join(format!("hz-datapack-worldgen-{}", std::process::id()));
@@ -1177,7 +961,7 @@ mod tests {
std::fs::write(path, t).unwrap();
};
for pack in ["a", "b", "off"] {
write(pack, "pack.mcmeta", r#"{"pack":{"description":"fixture"}}"#);
write(pack, "pack.mcmeta", "{}");
write(pack, "data/minecraft/worldgen/noise/cave.json", pack);
write(pack, "data/p/structure/room/a.nbt", pack);
write(
@@ -1348,20 +1132,6 @@ mod tests {
#[test]
fn tag_resolution_dedupes_fast_ends_cycles_and_nests_deeply() {
let mut store = DataStore::default();
// Every referenced function exists; missing required ones invalidate a tag.
for id in (0..25_000)
.map(|i| format!("t:f{i}"))
.chain(["t:x", "t:y", "t:deep"].map(str::to_owned))
{
store.functions.insert(
id.clone(),
Function {
id,
lines: Vec::new(),
parameters: Vec::new(),
},
);
}
let big: Vec<(String, bool)> = (0..50_000)
.map(|i| (format!("t:f{}", i % 25_000), true))
.collect();
@@ -1446,13 +1216,13 @@ mod tests {
write(
"fns",
"pack.mcmeta",
r#"{"pack":{"description":"fixture","min_format":121,"max_format":121}}"#,
r#"{"pack":{"min_format":121,"max_format":121}}"#,
);
write("fns", "data/x/function/f.mcfunction", "say hi");
write(
"off",
"pack.mcmeta",
r#"{"pack":{"description":"fixture","min_format":121,"max_format":121}}"#,
r#"{"pack":{"min_format":121,"max_format":121}}"#,
);
std::fs::create_dir_all(&dir).unwrap();
let mut zip = zip::ZipWriter::new(std::fs::File::create(dir.join("old.zip")).unwrap());
+4 -37
View File
@@ -33,10 +33,7 @@ struct Globals {
hdr_windows: array<vec4<f32>, 2>,
// HDR lighting: x how strongly the sky lights the world now (`Sky::sky_light_factor`),
// y what the sky and fog get: the same, but none under water.
// w: apply the no-pack HDR gain; HDR_WRITE_LIGHT independently keeps float alpha as coverage.
hdr_light: vec4<f32>,
hdr_settings: vec4<f32>,
hdr_shares: vec4<f32>,
}
@group(0) @binding(0) var<uniform> globals: Globals;
@group(0) @binding(1) var atlas: texture_2d<f32>;
@@ -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
+1 -67
View File
@@ -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));
}
}
+15 -20
View File
@@ -9,7 +9,6 @@ use crate::{
environment::Sky,
world::World,
};
use horizon_core::random::{LegacyRandom, RandomSource};
use std::path::Path;
/// `FogType` of the camera position.
@@ -65,7 +64,7 @@ impl FogTables {
}
/// Fog state that evolves over time.
#[derive(Clone, Debug)]
#[derive(Clone, Copy, Debug, Default)]
pub struct FogState {
/// `LocalPlayer.waterVisionTime` (0..600).
pub water_vision_time: i32,
@@ -73,28 +72,24 @@ pub struct FogState {
pub rain_fog_multiplier: f32,
/// `LightmapRenderStateExtractor.blockLightFlicker`.
pub block_light_flicker: f32,
/// `LightmapRenderStateExtractor.random`: `RandomSource.create()`.
flicker_random: LegacyRandom,
}
impl Default for FogState {
fn default() -> Self {
Self {
water_vision_time: 0,
rain_fog_multiplier: 0.0,
block_light_flicker: 0.0,
flicker_random: crate::random_source::create(),
}
}
flicker_seed: u64,
}
impl FogState {
fn next_float(&mut self) -> f32 {
// splitmix64: the vanilla source is unseeded `RandomSource.create()`.
self.flicker_seed = self.flicker_seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.flicker_seed;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
((z ^ (z >> 31)) >> 40) as f32 / (1u32 << 24) as f32
}
/// `LightmapRenderStateExtractor.tick`: the torch light flickers.
pub fn tick_flicker(&mut self) {
let r = &mut self.flicker_random;
let (a, b, c, d) = (
r.next_float(),
r.next_float(),
r.next_float(),
r.next_float(),
self.next_float(),
self.next_float(),
self.next_float(),
self.next_float(),
);
self.block_light_flicker += (a - b) * c * d * 0.1;
self.block_light_flicker *= 0.9;
@@ -755,7 +750,7 @@ mod tests {
rain_fog_multiplier: 0.1,
..FogState::default()
};
let mut settled = start.clone();
let mut settled = start;
settle_rain_fog(&mut settled, 0.5, 15, true);
assert_eq!(settled.rain_fog_multiplier, 0.5);
let mut eased = start;
+32 -135
View File
@@ -126,16 +126,11 @@ struct Cache {
random: horizon_core::random::LegacyRandom,
}
pub struct Font {
/// The atlas rects of every texture, by resource name: a bitmap provider's
/// `file` may name any texture, not only one under `textures/font`.
/// The atlas rects of the sheets under `textures/font`.
sheets: HashMap<String, Rect>,
missing: Glyph,
white: Rect,
root: PathBuf,
/// The game's own resources, below a pack overlay: a font file the overlay
/// lacks (a pack adds providers over vanilla's, or the overlay left a
/// directory out) is read from here, as vanilla's resource stack does.
fallback: Option<PathBuf>,
/// Atlas size and the reserved dynamic region (y, height).
atlas: (u32, u32),
region: (u32, u32),
@@ -204,13 +199,6 @@ fn parse_hex(text: &[u8], out: &mut HashMap<u32, HexGlyph>) {
out.insert(codepoint, HexGlyph { rows, left, right });
}
}
/// A resource file's bytes, from `root` or, when the overlay lacks it, from
/// the game's own resources below (`defaults`).
fn read_resource(root: &Path, defaults: Option<&Path>, path: &str) -> Option<Vec<u8>> {
std::fs::read(root.join(path))
.ok()
.or_else(|| defaults.and_then(|d| std::fs::read(d.join(path)).ok()))
}
fn read_json(root: &Path, path: &str) -> Option<serde_json::Value> {
serde_json::from_slice(&std::fs::read(root.join(path)).ok()?).ok()
}
@@ -293,7 +281,6 @@ fn hex_advance(h: &HexGlyph) -> f32 {
/// pixels, copied into the atlas when first drawn.
fn load_bitmap(
root: &Path,
defaults: Option<&Path>,
p: &serde_json::Value,
sheets: &HashMap<String, Rect>,
pixels: &[u8],
@@ -303,36 +290,22 @@ fn load_bitmap(
let mut out = HashMap::new();
let file = p["file"].as_str().unwrap_or("");
let (ns, name) = file.split_once(':').unwrap_or(("minecraft", file));
let name = name.trim_end_matches(".png");
// The atlas names the default namespace's textures by their path and every
// other namespace's as `namespace:path` (HudAtlas::build).
let atlas_name = if ns == "minecraft" {
name.to_owned()
} else {
format!("{ns}:{name}")
};
let in_atlas = sheets.get(&atlas_name).copied();
let in_atlas = (ns == "minecraft")
.then(|| sheets.get(name.trim_end_matches(".png")).copied())
.flatten();
// The sheet: its size, and its pixels when they are not in the atlas.
let (sw, sh, sheet) = match in_atlas {
Some(rect) => {
let (w, h) = rect.texels(atlas);
(w, h, None)
}
None => {
let path = resource_path(file, "textures", "");
let sheet = crate::hud::decode(&root.join(&path)).or_else(|first| {
defaults
.and_then(|d| crate::hud::decode(&d.join(&path)).ok())
.ok_or(first)
});
match sheet {
Ok((w, h, rgba)) => (w, h, Some(Arc::new((w, rgba)))),
Err(_) => {
log::warn!("font sheet {file} could not be read");
return out;
}
None => match crate::hud::decode(&root.join(resource_path(file, "textures", ""))) {
Ok((w, h, rgba)) => (w, h, Some(Arc::new((w, rgba)))),
Err(_) => {
log::warn!("font sheet {file} could not be read");
return out;
}
}
},
};
let rows: Vec<Vec<char>> = p["chars"]
.as_array()
@@ -409,13 +382,11 @@ fn load_bitmap(
}
/// `TrueTypeGlyphProviderDefinition.load`: a TrueType file (not CFF) under
/// `font/`, its Unicode characters but the `skip` ones.
fn load_true_type(root: &Path, defaults: Option<&Path>, p: &serde_json::Value) -> Option<TrueType> {
fn load_true_type(root: &Path, p: &serde_json::Value) -> Option<TrueType> {
let file = p["file"].as_str()?;
let path = resource_path(file, "font", "");
let Some(data) = read_resource(root, defaults, &path) else {
log::warn!("font file {file} could not be read");
return None;
};
let data = std::fs::read(root.join(resource_path(file, "font", "")))
.map_err(|e| log::warn!("font file {file}: {e}"))
.ok()?;
// FT_Get_Font_Format must say TrueType: CFF outlines ("OTTO") are refused.
if data.starts_with(b"OTTO") {
log::warn!("font file {file} is not in TTF format");
@@ -515,16 +486,13 @@ impl TrueType {
)
}
}
fn load_hex(root: &Path, defaults: Option<&Path>, p: &serde_json::Value) -> HashMap<u32, HexGlyph> {
fn load_hex(root: &Path, p: &serde_json::Value) -> HashMap<u32, HexGlyph> {
let mut glyphs = HashMap::new();
let Some(file) = p["hex_file"].as_str() else {
return glyphs;
};
let path = resource_path(file, "", "").replace("//", "/");
let Some(zip) = Zip::open(&root.join(&path))
.ok()
.or_else(|| defaults.and_then(|d| Zip::open(&d.join(&path)).ok()))
else {
let path = root.join(resource_path(file, "", "").replace("//", "/"));
let Ok(zip) = Zip::open(&path) else {
return glyphs;
};
let names: Vec<String> = zip
@@ -635,24 +603,7 @@ impl Font {
region: (u32, u32),
white: Rect,
) -> Self {
Self::load_with(root, None, sprites, pixels, atlas, region, white, false)
}
/// [`Font::load`] over a pack overlay: a sheet, TrueType file or Unihex
/// file the overlay lacks is read from `defaults`, the game's own
/// resources, as vanilla's resource stack does. (Definitions stay the
/// overlay's: a pack's filter may hide one from a lower pack.)
#[allow(clippy::too_many_arguments)]
pub fn load_over(
root: &Path,
defaults: &Path,
sprites: &HashMap<String, Rect>,
pixels: &[u8],
atlas: (u32, u32),
region: (u32, u32),
white: Rect,
) -> Self {
let fallback = (root != defaults).then_some(defaults);
Self::load_with(root, fallback, sprites, pixels, atlas, region, white, false)
Self::load_with(root, sprites, pixels, atlas, region, white, false)
}
/// A server measures text but never draws it. Decode bitmap sheets only
/// to obtain advances, then release their pixels; never rasterize glyphs.
@@ -665,21 +616,10 @@ impl Font {
w: 1,
h: 1,
};
Self::load_with(
root,
None,
&HashMap::new(),
&[],
(1, 1),
(0, 0),
white,
true,
)
Self::load_with(root, &HashMap::new(), &[], (1, 1), (0, 0), white, true)
}
#[allow(clippy::too_many_arguments)]
fn load_with(
root: &Path,
fallback: Option<&Path>,
sprites: &HashMap<String, Rect>,
pixels: &[u8],
atlas: (u32, u32),
@@ -687,7 +627,11 @@ impl Font {
white: Rect,
metrics_only: bool,
) -> Self {
let sheets = sprites.clone();
let sheets = sprites
.iter()
.filter(|(k, _)| k.starts_with("font/"))
.map(|(k, v)| (k.clone(), *v))
.collect();
let font = Self {
sheets,
missing: Glyph {
@@ -701,7 +645,6 @@ impl Font {
},
white,
root: root.to_owned(),
fallback: fallback.map(|p| p.to_owned()),
atlas,
region,
metrics_only,
@@ -713,8 +656,11 @@ impl Font {
pending: Vec::new(),
full: false,
},
// `Font.random`: `RandomSource.create()`.
random: crate::random_source::create(),
random: horizon_core::random::LegacyRandom::new(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos() as i64),
),
}),
generation: std::cell::Cell::new(next_generation()),
uniform: false,
@@ -752,7 +698,6 @@ impl Font {
Filter::of(p),
load_bitmap(
&self.root,
self.fallback.as_deref(),
p,
&self.sheets,
pixels,
@@ -763,7 +708,7 @@ impl Font {
"unihex" => Some(Provider::Unihex(Filter::of(p), p.clone(), None)),
"ttf" => Some(Provider::TrueType(
Filter::of(p),
Box::new(load_true_type(&self.root, self.fallback.as_deref(), p)?),
Box::new(load_true_type(&self.root, p)?),
)),
_ => None,
})
@@ -923,8 +868,7 @@ impl Font {
}
}
Provider::Unihex(filter, def, glyphs) if filter.passes(uniform, japanese) => {
let glyphs = glyphs
.get_or_insert_with(|| load_hex(&self.root, self.fallback.as_deref(), def));
let glyphs = glyphs.get_or_insert_with(|| load_hex(&self.root, def));
if let Some(h) = glyphs.get(&u32::from(c)).copied()
&& !(non_fishy && fishy_advance(hex_advance(&h)))
{
@@ -1082,8 +1026,7 @@ impl Font {
}
}
Provider::Unihex(filter, def, glyphs) if filter.passes(uniform, japanese) => {
let glyphs = glyphs
.get_or_insert_with(|| load_hex(&self.root, self.fallback.as_deref(), def));
let glyphs = glyphs.get_or_insert_with(|| load_hex(&self.root, def));
if let Some(h) = glyphs.get(&u32::from(c)) {
return Some(hex_advance(h));
}
@@ -1569,52 +1512,6 @@ mod tests {
assert!(texels.iter().any(|t| t[0] == 255));
}
/// A bitmap provider's `file` can name any texture. When the atlas has that
/// texture (HUD packs keep their glyph sheets outside `textures/font`), the
/// glyphs are read from it instead of being copied into the small run-time
/// region one by one; the region then cannot fill up and turn later glyphs
/// into the missing one, whose advance shifts the rest of a HUD line.
#[test]
fn a_bitmap_sheet_outside_font_is_read_from_the_atlas() {
let sheet = png(16, 8, |x, _| x < 2 || (8..14).contains(&x));
let (_, _, pixels) = crate::hud::decode_bytes(&sheet).unwrap();
let rect = Rect {
u0: 0.0,
v0: 0.0,
u1: 1.0,
v1: 1.0,
w: 16,
h: 8,
};
for (ns, file, key) in [
("minecraft", "minecraft:hud/sheet.png", "hud/sheet"),
(
"hudengine",
"hudengine:font/sheet.png",
"hudengine:font/sheet",
),
] {
let def = format!(
r#"{{"providers":[{{"type":"bitmap","file":"{file}","ascent":7,"chars":["ab"]}}]}}"#
);
// The image is only in the atlas: the pack's copy is not on disk.
let root = root(
ns,
&[("assets/minecraft/font/default.json", def.as_bytes())],
);
let sprites = HashMap::from([(key.to_string(), rect)]);
let font = Font::load(&root, &sprites, &pixels, (16, 8), (0, 8), white());
let _ = std::fs::remove_dir_all(&root);
assert_eq!(font.glyph('a').advance, 3.0, "{ns}");
assert_eq!(font.glyph('b').advance, 7.0, "{ns}");
assert!(font.glyph('a').rect.is_some(), "{ns}");
assert!(
font.take_patches().is_empty(),
"{ns}: the atlas sheet is not copied into the run-time region"
);
}
}
#[test]
fn styles_pick_their_font_and_unknown_fonts_draw_missing_glyphs() {
let sheet = png(16, 8, |x, _| x < 2 || (8..14).contains(&x));
File diff suppressed because it is too large. Load diff
+1 -12
View File
@@ -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]);
}
}
+2 -11
View File
@@ -10,8 +10,8 @@
//! second pass, draws the sections whose box was seen; the others' draws have
//! an instance count of 0.
//!
//! The picture stays the one without culling: the boxes around the mesh's
//! geometry (padded by a block) are drawn front faces
//! The picture stays the one without culling: the boxes (grown by a block,
//! the farthest a block model reaches out of its cell) are drawn front faces
//! only, so a fragment is the box's nearest point on its pixel, and the
//! fragment shader runs only where the depth test passes (forced early), so a
//! section is left out only where all of its fragments would fail the depth
@@ -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);
}
}
+2 -17
View File
@@ -106,8 +106,6 @@ pub struct Flat {
phase2_calls: wgpu::Buffer,
phase2_call_capacity: u64,
phase2_call_data: Vec<[u32; 5]>,
/// GPU culling's phase 2 uses indirect draws; flat draws without culling do not.
indirect_execution: bool,
/// The copy of phase 2's draws, and its ranges.
phase2_group: Option<(wgpu::BindGroup, u32)>,
/// The layout of a page's bind group 1 (shader packs' terrain pipelines share it).
@@ -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",
+15 -21
View File
@@ -50,10 +50,8 @@ impl Pending {
}
}
/// Small, its meshes boxed: `draw_frame` walks every tile each frame (thousands at long far
/// distances), and that walk is bound by the memory it reads.
pub(super) struct Tile {
pub mesh: Box<SectionGpu>,
pub mesh: SectionGpu,
bounds: Bounds,
/// When it began to fade in; none while the rest of its job uploads.
shown: Option<Instant>,
@@ -64,7 +62,7 @@ pub(super) struct Tile {
/// Its mesh before near chunks took some of its columns, drawn in its
/// place until they have faded in (from the job's end, the instant), so
/// no hole shows through them.
previous: Option<Box<(SectionGpu, Option<Instant>)>>,
previous: Option<(SectionGpu, Option<Instant>)>,
/// Its slot in occlusion culling's results (`cull::TILE`), kept while
/// meshes of the same tile replace one another.
pub cull_slot: u32,
@@ -157,12 +155,10 @@ pub(super) struct Visible {
impl Tile {
/// The mesh drawn this frame: the previous one while it stands in.
pub fn drawn(&self) -> &SectionGpu {
self.previous
.as_deref()
.map_or(&self.mesh, |(mesh, _)| mesh)
self.previous.as_ref().map_or(&self.mesh, |(mesh, _)| mesh)
}
pub fn drawn_mut(&mut self) -> &mut SectionGpu {
match self.previous.as_deref_mut() {
match &mut self.previous {
Some((mesh, _)) => mesh,
None => &mut self.mesh,
}
@@ -181,7 +177,7 @@ impl Tile {
}
/// Both meshes it holds in the arena: its own and the stand-in, if any.
pub fn meshes_mut(&mut self) -> impl Iterator<Item = &mut SectionGpu> {
std::iter::once(&mut *self.mesh).chain(self.previous.as_deref_mut().map(|(mesh, _)| mesh))
std::iter::once(&mut self.mesh).chain(self.previous.as_mut().map(|(mesh, _)| mesh))
}
pub fn visible(
&self,
@@ -218,12 +214,12 @@ impl Renderer {
let (shown, previous) = match self.lods.remove(&key) {
Some(old) if old.dying.is_none() => match old.previous {
Some(previous) => {
self.release_mesh(*old.mesh);
self.release_mesh(old.mesh);
(old.shown, Some(previous))
}
None if keep => (old.shown, Some(Box::new((*old.mesh, None)))),
None if keep => (old.shown, Some((old.mesh, None))),
None => {
self.release_mesh(*old.mesh);
self.release_mesh(old.mesh);
(old.shown, None)
}
},
@@ -244,7 +240,7 @@ impl Renderer {
self.lods.insert(
key,
Tile {
mesh: Box::new(mesh),
mesh,
bounds,
shown,
dying: None,
@@ -255,7 +251,7 @@ impl Renderer {
);
}
None => {
if let Some((previous, _)) = previous.map(|p| *p) {
if let Some((previous, _)) = previous {
self.release_mesh(previous);
}
self.lod_cull_free.extend(old_slot);
@@ -264,8 +260,8 @@ impl Renderer {
}
fn release_tile(&mut self, tile: Tile) {
self.release_mesh(*tile.mesh);
if let Some((previous, _)) = tile.previous.map(|p| *p) {
self.release_mesh(tile.mesh);
if let Some((previous, _)) = tile.previous {
self.release_mesh(previous);
}
}
@@ -341,10 +337,8 @@ impl Renderer {
let stood_in: Vec<_> = self
.lods
.values_mut()
.filter_map(|tile| match tile.previous.as_deref() {
Some((_, Some(since))) if start - *since >= FADE => {
tile.previous.take().map(|p| *p)
}
.filter_map(|tile| match tile.previous {
Some((_, Some(since))) if start - since >= FADE => tile.previous.take(),
_ => None,
})
.collect();
@@ -446,7 +440,7 @@ impl Renderer {
if tile.shown.is_none() {
tile.shown = Some(if over_replaced(key) { start } else { whole });
}
if let Some((_, since)) = tile.previous.as_deref_mut() {
if let Some((_, since)) = &mut tile.previous {
since.get_or_insert(start);
}
}
@@ -49,23 +49,9 @@ struct HorizonMaterialOut {
@location(14) @interpolate(flat) normal: vec3<f32>,
}
fn horizon_material_vertex(position: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f32>,
light: vec2<u32>, origin: vec4<f32>) -> HorizonMaterialOut {
let pos = horizon_material_displace(position + origin.xyz, uv, layer, color, light);
var out = horizon_material_out(pos, uv, layer, color, vec2<f32>(light));
// The section's fade-in (`RenderSection.getVisibility`), as how far it is from whole, in the
// high 16 bits of `hdr_code`: no varying of its own, and 0 (whole) where nothing fades. It is
// the same for the whole section, so the flat varying loses nothing.
let instance = horizon_terrain_instance_visibility(origin.w);
out.hdr_code |= u32(round((1.0 - instance) * 65535.0)) << 16u;
return out;
}
// terrain.wgsl's fade-in (terrain.vsh's chunkVisibility): whole within 16 blocks, the section's
// own visibility from 32 out, toward the fog colour, alpha kept. The distance blend runs here on
// the interpolated distance, as vanilla's interpolated `visibility`, not once per triangle.
fn horizon_material_faded(in: HorizonMaterialOut, color: vec4<f32>) -> vec4<f32> {
let instance = 1.0 - f32(in.hdr_code >> 16u) / 65535.0;
let visibility = mix(1.0, instance, clamp((in.spherical - 16.0) / 16.0, 0.0, 1.0));
return vec4(mix(horizon_terrain_globals.fog_color.rgb, color.rgb, visibility), color.a);
light: vec2<u32>, origin: vec3<f32>) -> HorizonMaterialOut {
let pos = horizon_material_displace(position + origin, uv, layer, color, light);
return horizon_material_out(pos, uv, layer, color, vec2<f32>(light));
}
// `light`: sky, block, 16 per level.
fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f32>,
@@ -108,7 +94,7 @@ fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec2<u32>,
@location(5) origin: vec4<f32>) -> HorizonMaterialOut {
return horizon_material_vertex(position, horizon_terrain_terrain_uv(uv, origin.w), layer,
color, light, origin);
color, light, origin.xyz);
}
@vertex fn horizon_material_vs_flat(
@builtin(vertex_index) vertex: u32,
@@ -117,7 +103,7 @@ fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f
) -> HorizonMaterialOut {
let origin = horizon_terrain_slot_origins[horizon_terrain_block_slots[vertex >> 8u]];
return horizon_material_vertex(position, horizon_terrain_terrain_uv(uv, origin.w), layer,
color, light, origin);
color, light, origin.xyz);
}
// The prelude's `block_normal_map` and `block_specular_map`: the resource packs' LabPBR maps
// in the pack's frame group (3), one array each, a blank 1x1 without them.
@@ -195,11 +181,9 @@ struct HorizonMaterialSolid {
// What `fs_solid` leaves in the frame's alpha for HDR output, from the sprite before the pack's
// material: the pack decides the colour, Horizon still knows which pixels are light sources.
fn horizon_material_mark(in: HorizonMaterialOut, texel: vec4<f32>) -> f32 {
if horizon_terrain_HDR_WRITE_LIGHT { return 1.0; }
let code = in.hdr_code & 15u;
if code > 0u {
let whole = code > 4u;
let tier = select(code, code - 4u, whole);
if in.hdr_code > 0u {
let whole = in.hdr_code > 4u;
let tier = select(in.hdr_code, in.hdr_code - 4u, whole);
let l = in.layer & 0x7fffu;
let t = textureLoad(horizon_terrain_emission_thresholds,
vec2(i32(l % horizon_terrain_SPRITE_SPLIT), i32(l / horizon_terrain_SPRITE_SPLIT)), 0);
@@ -218,7 +202,7 @@ fn horizon_material_mark(in: HorizonMaterialOut, texel: vec4<f32>) -> f32 {
return horizon_material_solid(in);
}
fn horizon_material_solid(in: HorizonMaterialOut) -> HorizonMaterialSolid {
let color = horizon_material_faded(in, horizon_material_color(in, false));
let color = horizon_material_color(in, false);
let texel = horizon_terrain_sprite(in.uv, in.layer & 0x7fffu);
// Bit 15 of the layer marks opaque leaves (forceOpaque): no alpha cutout.
if color.a < 0.5 && (in.layer & 0x8000u) == 0u { discard; }
@@ -232,7 +216,7 @@ fn horizon_material_solid(in: HorizonMaterialOut) -> HorizonMaterialSolid {
return out;
}
@fragment fn horizon_material_fs_translucent(in: HorizonMaterialOut) -> @location(0) vec4<f32> {
let color = horizon_material_faded(in, horizon_material_color(in, true));
let color = horizon_material_color(in, true);
if color.a < 0.01 { discard; }
return horizon_material_fogged(color, in.spherical, in.cylindrical);
}
@@ -310,8 +294,7 @@ pub struct NativeMaterial {
/// every name it declares prefixed.
fn vanilla_terrain() -> String {
prefixed(
&(include_str!("../hdr_light.wgsl").to_owned()
+ section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)")),
section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)"),
PREFIX,
)
}
@@ -433,10 +416,6 @@ impl NativeMaterial {
];
constants.extend(sprite_names.into_iter().zip(shape.sprites));
constants.push(("horizon_material_fog", f64::from(u8::from(fog))));
constants.push((
"horizon_terrain_HDR_WRITE_LIGHT",
f64::from(u8::from(shape.format == super::LINEAR_FRAME)),
));
constants.push((
"horizon_displace_distance",
displaced.map_or(0.0, |d| f64::from(d.distance)),
@@ -585,39 +564,6 @@ impl NativeMaterial {
}
}
#[cfg(test)]
pub(super) fn coverage_test_shader(api: horizon_shader_api::Api) -> String {
use horizon_shader_api::{MANIFEST, Pack, StageKind, check};
let manifest = format!(
"schema = 1\nid = \"coverage\"\nname = \"Coverage\"\nversion = \"1\"\napi = \"{api}\"\n[material]\nfile = \"material.wgsl\"\n[displacement]\n"
);
let material = "fn material(input: MaterialInput) -> Surface { return Surface(vec4<f32>(1.4, 0.3, 0.1, 0.7)); }\nfn displacement(input: DisplacementInput) -> f32 { return 0.0; }\n";
let files = [(MANIFEST, manifest.as_str()), ("material.wgsl", material)]
.map(|(n, text)| (n.to_owned(), text.as_bytes().to_vec()))
.into();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let stage = report
.stages
.iter()
.find(|s| s.kind == StageKind::Material)
.unwrap();
shader_source(&stage.source, &[], &[], (false, true), api)
+ r#"
override TEST_TIER: u32 = 0u;
override TEST_OVERLAY: u32 = 0u;
@vertex fn test_vertex(@builtin(vertex_index) i: u32) -> HorizonMaterialOut {
var o: HorizonMaterialOut;
o.clip = vec4(vec2(f32((i << 1u) & 2u), f32(i & 2u)) * 2.0 - 1.0, 0.0, 1.0);
o.uv = vec2(0.5);
o.tint = vec4(1.0);
o.lightmap = vec3(1.0);
o.hdr_code = TEST_TIER;
o.base = vec3(1.0);
return o;
}
"#
}
#[cfg(test)]
mod tests {
use super::*;
@@ -636,25 +582,6 @@ mod tests {
report.stages.remove(0)
}
/// The fade-in packed above the HDR code: a section a quarter visible (`section_instance`
/// with step 1: mantissa `1 - visibility` = 0.75) keeps its visibility through the code, and
/// a fragment 24 blocks away, half-way into the 16-block blend, is 1 - (1 - 0.25) / 2 = 0.625
/// visible; the code's low nibble survives.
#[test]
fn the_fade_in_rides_the_hdr_code() {
let w = f32::from_bits(1.0f32.to_bits() | (0.75 * 8_388_608.0) as u32);
// `horizon_material_vertex`.
let instance = 1.0 - (w.to_bits() & 0x7fffff) as f32 / 8_388_608.0;
let code = 7u32 | (((1.0 - instance) * 65535.0).round() as u32) << 16;
// `horizon_material_faded` and `horizon_material_mark`.
let unpacked = 1.0 - (code >> 16) as f32 / 65535.0;
assert!((unpacked - 0.25).abs() < 1.0 / 65535.0);
let blend = ((24.0f32 - 16.0) / 16.0).clamp(0.0, 1.0);
let visibility = 1.0 + (unpacked - 1.0) * blend;
assert!((visibility - 0.625).abs() < 1.0 / 65535.0);
assert_eq!(code & 15, 7);
}
#[test]
fn the_terrain_takes_a_prefix_and_validates_with_a_pack() {
let terrain = vanilla_terrain();
@@ -90,27 +90,6 @@ impl NativePack {
report: Report,
constants: &[(String, f64)],
lights: super::block_light::LightTable,
) -> Result<(Self, Vec<String>), String> {
// A backend may refuse a layout or pipeline the pack asks for (a stage's bindings
// past its sampled-texture limit, say, as OpenGL's 16 was before #10095): the pack
// is refused with that error instead of a validation panic in the renderer.
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let built = Self::build(device, queue, terrain, report, constants, lights);
let refused = pollster::block_on(scope.pop());
match (built, refused) {
(Ok(built), None) => Ok(built),
(Ok(_), Some(e)) => Err(e.to_string()),
(Err(e), _) => Err(e),
}
}
fn build(
device: &wgpu::Device,
queue: &wgpu::Queue,
terrain: &TerrainShape<'_>,
report: Report,
constants: &[(String, f64)],
lights: super::block_light::LightTable,
) -> Result<(Self, Vec<String>), String> {
let (globals, format) = (terrain.globals, terrain.format);
let textures = super::native_post::pack_textures(device, queue, &report);
@@ -299,90 +278,9 @@ impl NativePack {
#[cfg(test)]
mod tests {
use super::{NativePack, TerrainShape, chosen_shadow};
use super::chosen_shadow;
use horizon_shader_api::manifest::{OptionKind, PackOption, Shadow};
/// A device with OpenGL's minimum 16 sampled textures per stage refuses a pack whose
/// sky adds the terrain's 10 to the frame's 9: that is the pack's error to report, not
/// a validation panic (#10095).
#[test]
fn a_pack_the_backend_refuses_is_an_error_not_a_panic() {
use horizon_shader_api::{MANIFEST, Pack, check};
let instance = crate::gfx::test_gpu::instance();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
let mut limits = adapter.limits();
limits.max_sampled_textures_per_shader_stage = 16;
let Ok((device, queue)) =
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: limits,
..Default::default()
}))
else {
return;
};
// The terrain's group 0: its uniform and the 10 sampled textures (_terrain.wgsl_'s
// 8 sprite arrays, the lightmap and the emission thresholds).
let entry = |binding, ty| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty,
count: None,
};
let mut entries = vec![entry(
0,
wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
)];
entries.extend((1..=10).map(|binding| {
entry(
binding,
wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
)
}));
let globals = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &entries,
});
let empty = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[],
});
let terrain = TerrainShape {
globals: &globals,
flat: None,
sprites: [1.0, 2048.0],
format: wgpu::TextureFormat::Rgba8Unorm,
sprite_tags: &[],
sprite_materials: &[],
entities: &empty,
weather_groups: [&empty, &empty],
pbr: None,
};
let manifest = "schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"0.1\"\n\
[sky]\nfile = \"sky.wgsl\"\n";
let files = [
(MANIFEST, manifest),
(
"sky.wgsl",
"fn sky(input: SkyInput) -> vec4<f32> { return input.color; }\n",
),
]
.map(|(n, t)| (n.to_owned(), t.as_bytes().to_vec()))
.into();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let built = NativePack::new(&device, &queue, &terrain, report, &[], Default::default());
assert!(built.is_err(), "the pack should be refused");
}
fn option(id: &str, kind: OptionKind) -> PackOption {
PackOption {
id: id.into(),
@@ -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"),
+5 -22
View File
@@ -192,7 +192,7 @@ impl Oit {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Horizon improved transparency"),
source: wgpu::ShaderSource::Wgsl(
(super::terrain_shader()
(include_str!("../terrain.wgsl").to_owned()
+ include_str!("../oit.wgsl")
+ include_str!("../oit_world.wgsl"))
.into(),
@@ -253,14 +253,7 @@ impl Oit {
..Default::default()
})
};
let constants = [
("SPRITE_ARRAYS", sprites[0]),
("SPRITE_SPLIT", sprites[1]),
(
"HDR_WRITE_LIGHT",
f64::from(u8::from(format == super::LINEAR_FRAME)),
),
];
let constants = [("SPRITE_ARRAYS", sprites[0]), ("SPRITE_SPLIT", sprites[1])];
let add = wgpu::BlendOperation::Add;
let depth = |write| {
Some(wgpu::DepthStencilState {
@@ -471,10 +464,7 @@ impl Oit {
fragment: Some(wgpu::FragmentState {
module: &cloud_shader,
entry_point: Some(fragment),
compilation_options: wgpu::PipelineCompilationOptions {
constants: &constants[2..],
..Default::default()
},
compilation_options: Default::default(),
targets: &stage_targets(stage),
}),
multiview_mask: None,
@@ -498,14 +488,7 @@ impl Oit {
Stage::Accumulate,
),
];
let features = Features::new(
device,
&layouts,
features[0],
features[1],
features[2],
format,
);
let features = Features::new(device, &layouts, features[0], features[1], features[2]);
Self {
uniform,
layouts,
@@ -929,7 +912,7 @@ mod tests {
#[test]
fn the_terrain_shader_validates() {
validate(
&(crate::gfx::terrain_shader()
&(include_str!("../terrain.wgsl").to_owned()
+ include_str!("../oit.wgsl")
+ include_str!("../oit_world.wgsl")),
&[
@@ -55,7 +55,6 @@ impl Features {
entity: &wgpu::BindGroupLayout,
weather: &wgpu::BindGroupLayout,
border: &wgpu::BindGroupLayout,
format: wgpu::TextureFormat,
) -> Self {
let entity_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Horizon improved transparency features"),
@@ -114,15 +113,6 @@ impl Features {
bind_group_layouts: &[groups[0], groups[1], Some(&layouts[stage as usize])],
..Default::default()
});
let fragment = if kind.label == "text" && stage == Stage::Accumulate {
"fs_oit_text_accumulate".to_owned()
} else {
format!("fs_oit_{}_{suffix}", kind.name)
};
let constants = [(
"HDR_WRITE_LIGHT",
f64::from(u8::from(format == super::LINEAR_FRAME)),
)];
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(&format!("OIT {} {suffix}", kind.label)),
layout: Some(&layout),
@@ -146,15 +136,8 @@ impl Features {
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module,
entry_point: Some(&fragment),
compilation_options: wgpu::PipelineCompilationOptions {
constants: if kind.name == "border" {
&[]
} else {
&constants
},
..Default::default()
},
entry_point: Some(&format!("fs_oit_{}_{suffix}", kind.name)),
compilation_options: Default::default(),
targets: &stage_targets(stage),
}),
multiview_mask: None,
@@ -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,
&macros,
&horizon_shaders::GameInputs {
terrain: Some(atlas.terrain_inputs(flat)),
..Default::default()
},
);
let program = post
.terrain
.as_ref()
.unwrap_or_else(|| panic!("{:?}", post.notes));
for (tier, expected) in cases {
let origin = section_instance([0.0; 3], 0, 1.0);
let pixel = sample(
&device,
&queue,
&atlas,
program.program.as_ref().unwrap(),
flat,
false,
origin,
emitting(0xffff_ffff, tier),
);
assert_eq!(pixel[0], expected, "tier {tier} flat={flat}");
}
}
}
fn sample(
device: &wgpu::Device,
queue: &wgpu::Queue,
@@ -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", &macros);
let inputs = horizon_shaders::GameInputs {
terrain: Some(horizon_shaders::GbuffersInputs {
body: "vec2 p = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); hz_Vertex=vec4(p*2.0-1.0,0.0,1.0);".into(),
paged_maps: true,
..Default::default()
}),
..Default::default()
};
let post = horizon_shaders::load_post(&pack, "", &properties, &macros, &inputs);
assert!(post.terrain.is_some(), "{:?}", post.notes);
let world = World {
sprite_rects: &[],
atlas: None,
material_maps: None,
lightmap: map.clone(),
terrain: Some(Geometry {
buffers: &[],
table: None,
depth_write: true,
atlas: None,
material_maps: false,
ids: None,
cull: false,
}),
block_ids: &[],
celestial: None,
gui_size: [1, 1],
clouds: None,
weather: false,
hand: false,
};
let mut post = PackPost::new(
&device,
&queue,
&adapter,
post,
wgpu::TextureFormat::Rgba8Unorm,
world,
&Default::default(),
)
.expect("the pack runs");
let mut pass = post
.terrain
.take()
.unwrap_or_else(|| panic!("terrain pipeline must compile: {:?}", post.notes));
// As the entity passes are (`paged`), with the maps the renderer hands over.
pass.paged = true;
post.set_paged_maps([Some(map), None]);
post.copied |= 1;
post.resize(&device, [8, 1]);
let read = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 256,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&Default::default());
{
let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Initialize fixture depth"),
color_attachments: &[],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &post.targets.as_ref().unwrap().depth,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default()
});
}
let group = post.bind(&device, &queue, &mut encoder, &mut pass, [8, 1]);
{
let mut render = post.begin(&mut encoder, &pass);
render.set_pipeline(pass.render());
render.set_bind_group(0, &group, &[]);
if pass.draw {
post.draws.bind(&mut render, "TERRAIN_SOLID");
}
render.draw(0..3, 0..1);
}
let output = post.targets.as_ref().unwrap().color[0].as_ref().unwrap()[0].texture();
encoder.copy_texture_to_buffer(
output.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &read,
layout: wgpu::TexelCopyBufferLayout {
bytes_per_row: Some(256),
..Default::default()
},
},
wgpu::Extent3d {
width: 8,
height: 1,
depth_or_array_layers: 1,
},
);
queue.submit([encoder.finish()]);
read.slice(..).map_async(wgpu::MapMode::Read, |_| {});
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
let data = read.slice(..).get_mapped_range().unwrap();
let local = |x: u32, y: u32| texel(x - x0, y - y0);
let expected = [
local(20, 4),
local(39, 23),
local(31, 15),
local(32, 16),
crate::sprites::FLAT_NORMAL,
crate::sprites::FLAT_NORMAL,
crate::sprites::NO_SPECULAR,
local(33, 5),
];
for (i, e) in expected.iter().enumerate() {
assert_eq!(&data[i * 4..i * 4 + 4], e, "pixel {i}");
}
std::fs::remove_dir_all(root).unwrap();
}
@@ -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, &macros, &Default::default())
}
#[test]
fn a_dispatch_over_the_limit_reads_its_declared_work_groups() {
let Some(gpu) = Fixture::new() else { return };
let post = pack();
assert!(post.notes.is_empty(), "{:?}", post.notes);
let mut post = gpu.build(post);
let pixels = gpu.render(&mut post, [8, 8]);
for (k, got) in pixels.iter().enumerate() {
assert_eq!(*got, [0, 255, 0, 255], "pixel {k}; notes {:?}", post.notes);
}
}
@@ -104,7 +104,6 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
alpha_test: None,
item_id_declared: false,
far_depth: true,
paged_maps: false,
}
}
@@ -132,7 +131,6 @@ pub fn celestial_inputs() -> horizon_shaders::GbuffersInputs {
alpha_test: None,
item_id_declared: false,
far_depth: true,
paged_maps: false,
}
}
@@ -355,25 +355,6 @@ pub const NAMES: &[&str] = &[
"hasSkylight",
"hasCeiling",
"cloudTime",
// Horizon's own: Horizon Extras' dynamic lights (`dynamic_light.rs`), nearest first: how
// many, and each one's camera-relative position and light level (zeros past the count).
"horizonDynamicLightCount",
"horizonDynamicLight0",
"horizonDynamicLight1",
"horizonDynamicLight2",
"horizonDynamicLight3",
"horizonDynamicLight4",
"horizonDynamicLight5",
"horizonDynamicLight6",
"horizonDynamicLight7",
"horizonDynamicLight8",
"horizonDynamicLight9",
"horizonDynamicLight10",
"horizonDynamicLight11",
"horizonDynamicLight12",
"horizonDynamicLight13",
"horizonDynamicLight14",
"horizonDynamicLight15",
"isElytraFlying",
"cloudHeight",
];
@@ -405,8 +386,6 @@ pub struct FrameUniforms {
center_depth: Option<f32>,
/// The pack's shadow settings (7.1).
pub shadow: Shadow,
/// The pack's smoothing half-lives in ticks (`horizon_shaders::HALF_LIVES`).
pub half_lives: [Option<f32>; 4],
}
impl Default for FrameUniforms {
@@ -424,7 +403,6 @@ impl Default for FrameUniforms {
center_depth_raw: None,
center_depth: None,
shadow: Shadow::default(),
half_lives: [None; 4],
}
}
}
@@ -737,7 +715,6 @@ impl FrameUniforms {
*self = Self {
atlas: self.atlas,
shadow: std::mem::take(&mut self.shadow),
half_lives: self.half_lives,
..Self::default()
};
}
@@ -802,13 +779,10 @@ impl FrameUniforms {
// a tenth of a second a tick: Iris 1.11.7 smooths that slowly (measured frame by frame
// with a probe pack: wetness 60 s rising, 20 s falling; eyeBrightnessSmooth 1 s;
// centerDepthSmooth 0.1 s).
// The pack's `wetnessHalflife`/`drynessHalflife` when it sets them (OptiFine's defaults
// otherwise), as for `eyeBrightnessHalflife` and `centerDepthHalflife` below.
let [wet, dry, eye_half, depth_half] = self.half_lives;
let half_life = if inputs.rain > self.wetness {
wet.unwrap_or(600.0)
600.0
} else {
dry.unwrap_or(200.0)
200.0
};
self.wetness = smooth(self.wetness, inputs.rain, half_life * SECONDS_A_TICK, dt);
let eye = inputs.eye_light.map(|l| (l * 16) as f32);
@@ -817,18 +791,13 @@ impl FrameUniforms {
self.eye_smooth = eye;
}
for (smoothed, target) in self.eye_smooth.iter_mut().zip(eye) {
*smoothed = smooth(
*smoothed,
target,
eye_half.unwrap_or(10.0) * SECONDS_A_TICK,
dt,
);
*smoothed = smooth(*smoothed, target, 10.0 * SECONDS_A_TICK, dt);
}
// `centerDepthHalflife`'s default, 1 tick (OptiFine's shaders.txt); the first value
// read back is taken as it is.
if let Some(raw) = self.center_depth_raw {
self.center_depth = Some(match self.center_depth {
Some(v) => smooth(v, raw, depth_half.unwrap_or(1.0) * SECONDS_A_TICK, dt),
Some(v) => smooth(v, raw, SECONDS_A_TICK, dt),
None => raw,
});
}
@@ -960,12 +929,6 @@ impl FrameUniforms {
"gbufferProjectionInverse" => m4(projection_inverse),
"gbufferPreviousModelView" => m4(previous_view),
"gbufferPreviousProjection" => m4(previous_projection),
"horizonDynamicLightCount" => Value::i(&[frame.dynamic_lights.len() as i32]),
n if dynamic_light_index(n).is_some() => Value::f(
&(dynamic_light_index(n).and_then(|k| frame.dynamic_lights.get(k)))
.copied()
.unwrap_or([0.0; 4]),
),
"heldItemId" => Value::i(&[inputs.held_items[0]]),
"heldItemId2" => Value::i(&[inputs.held_items[1]]),
"heldBlockLightValue" => Value::i(&[inputs.held_light[0]]),
@@ -1272,12 +1235,10 @@ mod tests {
format!(r#"{{"attributes": {attributes}}}"#),
)
.unwrap();
let environment = crate::environment::Environment::load_dimension(&root, name).unwrap();
let environment =
crate::environment::Environment::load_dimension(&root, name, false).unwrap();
std::fs::remove_dir_all(&root).unwrap();
environment.sample(&crate::environment::Sample {
day_time: 6000,
..Default::default()
})
environment.sample(6000, None, None)
}
#[test]
@@ -1824,29 +1785,6 @@ pub(super) mod initial_history_tests {
assert_eq!(crate::clouds::drift(256 * 400 + 3, 0.25, 256), 3.25);
}
#[test]
fn the_dynamic_lights_reach_the_pack_nearest_first() {
let hud = HudBatch::new([800, 450]);
let mut f = frame(&hud);
let lights = [[1.0, 2.0, 3.0, 14.0], [-4.0, 0.5, 6.0, 7.0]];
f.dynamic_lights = &lights;
let mut u = FrameUniforms::default();
u.update(&f, &PackInputs::default(), [800, 450]);
let count = index("horizonDynamicLightCount").unwrap();
assert_eq!(int(&u, count), 2);
assert_eq!(float(&u, "horizonDynamicLight0")[..4], lights[0]);
assert_eq!(float(&u, "horizonDynamicLight1")[..4], lights[1]);
// Past the count, and the last of the sixteen: zeros.
assert_eq!(float(&u, "horizonDynamicLight2")[..4], [0.0; 4]);
assert_eq!(float(&u, "horizonDynamicLight15")[..4], [0.0; 4]);
assert_eq!(index("horizonDynamicLight16"), None);
// Off (no lights): none.
f.dynamic_lights = &[];
u.update(&f, &PackInputs::default(), [800, 450]);
assert_eq!(int(&u, count), 0);
assert_eq!(float(&u, "horizonDynamicLight0")[..4], [0.0; 4]);
}
#[test]
fn environmental_smoothing_starts_at_the_first_inputs_without_resetting_each_frame() {
let hud = HudBatch::new([800, 450]);
@@ -2232,33 +2170,22 @@ pub(super) mod initial_history_tests {
let hud = HudBatch::new([800, 450]);
let f = frame(&hud);
let mut u = FrameUniforms::default();
// A scripted clock: the next frame comes exactly `seconds` after the last, however long
// the test itself takes (a wall-clock gap of a millisecond moved centerDepthSmooth by
// more than the tolerance on a busy CI runner).
let mut counter = 1;
let mut elapsed = 0.0;
let mut after = |input: &mut PackInputs, seconds: f32| {
counter += 1;
elapsed += seconds;
input.frame_timing = Some(FrameTiming {
counter,
elapsed,
delta: seconds,
});
// The next frame comes `seconds` after the last (and a few microseconds of the test).
let after = |u: &mut FrameUniforms, seconds: f32| {
u.last = Some(std::time::Instant::now() - std::time::Duration::from_secs_f32(seconds));
};
let mut input = PackInputs {
eye_light: [0, 15],
frame_timing: Some(FrameTiming::nth(1, 0.0)),
..Default::default()
};
u.center_depth_raw = Some(1.0);
u.update(&f, &input, [800, 450]);
input.eye_light = [0, 0];
u.center_depth_raw = Some(0.0);
after(&mut input, 0.1);
after(&mut u, 0.1);
u.update(&f, &input, [800, 450]);
assert!((float(&u, "centerDepthSmooth")[0] - 0.5).abs() < 1e-3);
after(&mut input, 0.6);
after(&mut u, 0.6);
u.update(&f, &input, [800, 450]);
// 240 falling toward 0 for 0.7 s: 147.8, read as 147.
match u.values[index("eyeBrightnessSmooth").unwrap()] {
@@ -2266,46 +2193,11 @@ pub(super) mod initial_history_tests {
_ => panic!("integer uniform expected"),
}
input.rain = 1.0;
after(&mut input, 60.0);
after(&mut u, 60.0);
u.update(&f, &input, [800, 450]);
assert!((float(&u, "wetness")[0] - 0.5).abs() < 1e-3);
input.rain = 0.0;
after(&mut input, 20.0);
u.update(&f, &input, [800, 450]);
assert!((float(&u, "wetness")[0] - 0.25).abs() < 1e-3);
}
/// A pack's `const float wetnessHalflife = 30.0;` (and its dryness) replace the defaults:
/// wetness reaches half the rain in 3 s instead of 60 s.
#[test]
fn a_pack_s_half_lives_replace_the_defaults() {
let hud = HudBatch::new([800, 450]);
let f = frame(&hud);
let mut u = FrameUniforms {
half_lives: [Some(30.0), Some(10.0), None, None],
..Default::default()
};
// A scripted clock, as in `smoothed_uniforms_take_iris_half_lives`: the wall clock's gap
// between two updates is not exact on a busy runner.
let mut input = PackInputs {
frame_timing: Some(FrameTiming::nth(1, 0.0)),
..Default::default()
};
u.update(&f, &input, [800, 450]);
input.rain = 1.0;
input.frame_timing = Some(FrameTiming {
counter: 2,
elapsed: 3.0,
delta: 3.0,
});
u.update(&f, &input, [800, 450]);
assert!((float(&u, "wetness")[0] - 0.5).abs() < 1e-3);
input.rain = 0.0;
input.frame_timing = Some(FrameTiming {
counter: 3,
elapsed: 4.0,
delta: 1.0,
});
after(&mut u, 20.0);
u.update(&f, &input, [800, 450]);
assert!((float(&u, "wetness")[0] - 0.25).abs() < 1e-3);
}
@@ -2642,12 +2534,6 @@ pub(super) mod initial_history_tests {
}
}
/// `horizonDynamicLight<k>`'s `k`, for the lights `Frame::dynamic_lights` holds.
fn dynamic_light_index(name: &str) -> Option<usize> {
let k: usize = name.strip_prefix("horizonDynamicLight")?.parse().ok()?;
(k < crate::dynamic_light::MAX).then_some(k)
}
#[cfg(test)]
#[path = "pack_uniforms/ftransform_test.rs"]
mod ftransform_test;
@@ -46,7 +46,6 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
}),
item_id_declared: false,
far_depth: false,
paged_maps: false,
}
}
@@ -274,13 +274,7 @@ impl Panorama {
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_panorama"),
compilation_options: wgpu::PipelineCompilationOptions {
constants: &[(
"HDR_WRITE_LIGHT",
f64::from(u8::from(format == super::LINEAR_FRAME)),
)],
..Default::default()
},
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
@@ -1,5 +1,4 @@
// CubeMap's position-only cube and panorama.vsh/fsh: sample its untransformed direction.
override HDR_WRITE_LIGHT: bool = false;
struct Projection { view_proj: mat4x4<f32> }
@group(0) @binding(0) var<uniform> projection: Projection;
@group(0) @binding(1) var panorama: texture_cube<f32>;
@@ -27,6 +26,5 @@ struct Out {
return out;
}
@fragment fn fs_panorama(in: Out) -> @location(0) vec4<f32> {
let color = textureSample(panorama, panorama_sampler, in.direction);
return vec4(color.rgb, select(color.a, 1.0, HDR_WRITE_LIGHT));
return textureSample(panorama, panorama_sampler, in.direction);
}
@@ -124,10 +124,6 @@ impl<T> Resident<T> {
pub fn get_mut(&mut self, slot: u32) -> &mut T {
&mut self.items[slot as usize].1
}
/// Padded mesh bounds of the section in `slot`, relative to its origin.
pub fn bounds(&self, slot: u32) -> Option<[Vec3; 2]> {
self.bounds[slot as usize]
}
/// Sections from now on count from `min_y` (a level of another column).
pub fn set_min_y(&mut self, min_y: i32) {
self.min_y = min_y;
@@ -439,18 +435,17 @@ impl<T> Resident<T> {
if list.is_empty() {
continue;
}
let ox = f64::from(column.x * 16) - camera.x;
let oz = f64::from(column.z * 16) - camera.z;
let min_x = ox as f32;
let min_z = oz as f32;
let low_oy = (f64::from(self.min_y + column.low * 16) - camera.y) as f32;
let high_oy = (f64::from(self.min_y + column.high * 16) - camera.y) as f32;
let min = Vec3::new(min_x, low_oy, min_z);
let max = Vec3::new(min_x + 16.0, high_oy + 16.0, min_z + 16.0);
let min = origin((column.x, column.low, column.z), self.min_y, camera).as_vec3();
let top = origin((column.x, column.high, column.z), self.min_y, camera).as_vec3();
let max = top + Vec3::splat(16.0);
let inside = frustum.contains(min, max);
if !inside && !frustum.intersects(min, max) {
continue;
}
let ox = f64::from(column.x * 16) - camera.x;
let oz = f64::from(column.z * 16) - camera.z;
let min_x = ox as f32;
let min_z = oz as f32;
let dx8 = ox + 8.0;
let dz8 = oz + 8.0;
let xz_sq = dx8 * dx8 + dz8 * dz8;
+2 -17
View File
@@ -46,9 +46,6 @@ pub fn star_triangles() -> Vec<[f32; 3]> {
struct Uniform {
clip_from_sky: [[f32; 4]; 4],
color: [f32; 4],
hdr_settings: [f32; 4],
hdr_shares: [f32; 4],
hdr_light: [f32; 4],
}
pub(super) struct Stars {
@@ -96,9 +93,7 @@ impl Stars {
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Stars shader"),
source: wgpu::ShaderSource::Wgsl(
(include_str!("../hdr_light.wgsl").to_owned() + include_str!("stars.wgsl")).into(),
),
source: wgpu::ShaderSource::Wgsl(include_str!("stars.wgsl").into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Stars layout"),
@@ -134,13 +129,7 @@ impl Stars {
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_stars"),
compilation_options: wgpu::PipelineCompilationOptions {
constants: &[(
"HDR_WRITE_LIGHT",
f64::from(u8::from(format == super::LINEAR_FRAME)),
)],
..Default::default()
},
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
// RenderPipelines.STARS: the celestial blend (src alpha, one), with the
@@ -176,15 +165,11 @@ impl Stars {
encoder: &mut wgpu::CommandEncoder,
clip_from_sky: Mat4,
brightness: f32,
globals: &super::Globals,
) {
let b = brightness.clamp(0.0, 1.0);
let uniform = Uniform {
clip_from_sky: clip_from_sky.to_cols_array_2d(),
color: [b; 4],
hdr_settings: globals.hdr_settings,
hdr_shares: globals.hdr_shares,
hdr_light: globals.hdr_light,
};
uploads.buffer(encoder, &self.uniform, 0, bytemuck::bytes_of(&uniform));
}
@@ -4,9 +4,6 @@ struct Stars {
clip_from_sky: mat4x4<f32>,
// starBrightness in every channel (RenderSystem.setShaderColor).
color: vec4<f32>,
hdr_settings: vec4<f32>,
hdr_shares: vec4<f32>,
hdr_light: vec4<f32>,
}
@group(0) @binding(0) var<uniform> stars: Stars;
@@ -19,9 +16,5 @@ struct Stars {
// fully lifted wherever they show at all. Nothing else reads the frame's alpha.
@fragment fn fs_stars() -> @location(0) vec4<f32> {
let b = stars.color.a;
if HDR_WRITE_LIGHT && stars.hdr_light.w != 0.0 {
return vec4(hdr_written(stars.color.rgb * b, 0.175, stars.hdr_settings, stars.hdr_shares), b);
}
if HDR_WRITE_LIGHT { return vec4(stars.color.rgb * b, b); }
return vec4(stars.color.rgb * b, 0.175);
}
@@ -8,10 +8,6 @@ pub(super) fn pipelines(
) -> [wgpu::RenderPipeline; 3] {
std::array::from_fn(|i| {
let mut constants = sprite_constants.to_vec();
constants.push((
"HDR_WRITE_LIGHT",
f64::from(u8::from(format == super::LINEAR_FRAME)),
));
constants.push(("NATIVE_MODEL_ALPHA_CUTOFF", [0.0, 0.1, 0.5][i]));
// The ordinary Vertex ABI is unchanged. This material's private payload uses
// normal's four bytes for a float z, and glint UV for float normal x/y.
@@ -65,18 +65,13 @@ impl Visbuffer {
let resolve_constants: Vec<(&str, f64)> = constants
.iter()
.copied()
.chain([
("VIS_BLOCKS_PER_PAGE", blocks_per_page as f64),
(
"HDR_WRITE_LIGHT",
f64::from(u8::from(color == super::LINEAR_FRAME)),
),
])
.chain([("VIS_BLOCKS_PER_PAGE", blocks_per_page as f64)])
.collect();
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Horizon visibility buffer"),
source: wgpu::ShaderSource::Wgsl(
(super::terrain_shader() + include_str!("../visbuffer.wgsl")).into(),
(include_str!("../terrain.wgsl").to_owned() + include_str!("../visbuffer.wgsl"))
.into(),
),
});
let fragment = wgpu::ShaderStages::FRAGMENT;
@@ -55,13 +55,6 @@ pub struct Post {
sampler: wgpu::Sampler,
copy: wgpu::RenderPipeline,
fxaa: wgpu::RenderPipeline,
/// Contrast-adaptive sharpening of TAA's upscaled picture: the jittered frames are
/// spread over more output pixels, and the blend softens edges.
sharpen: wgpu::RenderPipeline,
/// The strength `sharpen` was built with (`set_sharpness`), and what rebuilding it takes.
sharpness: f32,
shader: wgpu::ShaderModule,
pipeline_layout: wgpu::PipelineLayout,
format: wgpu::TextureFormat,
/// World resolution relative to the window (0.5..=2.0).
pub scale: f32,
@@ -79,7 +72,6 @@ pub struct Post {
/// The last TAA frame's unjittered `view_proj` and camera, `None` once a frame ran without it.
taa_previous: Option<(DMat4, DVec3)>,
taa_this_frame: TaaUniform,
light_key: Option<(bool, [f32; 4], [f32; 4], [[f32; 4]; 2])>,
}
impl Post {
@@ -240,12 +232,8 @@ impl Post {
..Default::default()
});
Self {
shader: shader.clone(),
pipeline_layout: pipeline_layout.clone(),
copy: pipeline("fs_copy", &pipeline_layout, format),
fxaa: pipeline("fs_fxaa", &pipeline_layout, format),
sharpen: sharpen_pipeline(device, &shader, &pipeline_layout, format, 0.85),
sharpness: 0.85,
layout,
sampler,
format,
@@ -259,28 +247,6 @@ impl Post {
taa_frame: 0,
taa_previous: None,
taa_this_frame: bytemuck::Zeroable::zeroed(),
light_key: None,
}
}
// History stores baked light, so a change must reject it before jitter records the next frame.
pub(super) fn baked_light(
&mut self,
gain: bool,
settings: [f32; 4],
shares: [f32; 4],
windows: [[f32; 4]; 2],
) {
// These uniforms do not change baked colour without gain. Keep compatibility
// history stable when only its final display settings change.
let key = Some(if gain {
(true, settings, shares, windows)
} else {
(false, [0.0; 4], [0.0; 4], [[0.0; 4]; 2])
});
if self.light_key != key {
self.taa_previous = None;
self.light_key = key;
}
}
@@ -508,21 +474,6 @@ impl Post {
}
}
/// The Upscaling Sharpness setting, 0..=1: rebuilds the sharpen pipeline when it changed.
pub fn set_sharpness(&mut self, device: &wgpu::Device, sharpness: f32) {
let sharpness = sharpness.clamp(0.0, 1.0);
if sharpness != self.sharpness {
self.sharpen = sharpen_pipeline(
device,
&self.shader,
&self.pipeline_layout,
self.format,
sharpness,
);
self.sharpness = sharpness;
}
}
/// The size of the world's own target, when it has one.
pub fn size(&self) -> Option<[u32; 2]> {
self.target.as_ref().map(|t| t.size)
@@ -584,12 +535,7 @@ impl Post {
pass.set_bind_group(0, &taa.resolve[i], &[]);
pass.draw(0..3, 0..1);
drop(pass);
let pipeline = if self.upscale > 1.0 {
&self.sharpen
} else {
&self.copy
};
self.draw_onto(encoder, screen, &taa.show[i], pipeline, timing);
self.finish_with(encoder, screen, &taa.show[i], timing);
}
/// Draw what `group` binds (`bind`) onto `screen`.
@@ -599,23 +545,6 @@ impl Post {
screen: &wgpu::TextureView,
group: &wgpu::BindGroup,
timing: Option<&mut super::pack_timing::PassTiming>,
) {
let pipeline = if self.antialias {
&self.fxaa
} else {
&self.copy
};
self.draw_onto(encoder, screen, group, pipeline, timing);
}
/// Draw what `group` binds onto `screen` through `pipeline`.
fn draw_onto(
&self,
encoder: &mut wgpu::CommandEncoder,
screen: &wgpu::TextureView,
group: &wgpu::BindGroup,
pipeline: &wgpu::RenderPipeline,
timing: Option<&mut super::pack_timing::PassTiming>,
) {
let attachments = [Some(wgpu::RenderPassColorAttachment {
view: screen,
@@ -632,51 +561,16 @@ impl Post {
color_attachments: &attachments,
..Default::default()
});
pass.set_pipeline(pipeline);
pass.set_pipeline(if self.antialias {
&self.fxaa
} else {
&self.copy
});
pass.set_bind_group(0, group, &[]);
pass.draw(0..3, 0..1);
}
}
/// `fs_sharpen` at `sharpness` (its override), drawing onto `format`.
fn sharpen_pipeline(
device: &wgpu::Device,
shader: &wgpu::ShaderModule,
layout: &wgpu::PipelineLayout,
format: wgpu::TextureFormat,
sharpness: f32,
) -> wgpu::RenderPipeline {
let constants = [("sharpness", f64::from(sharpness))];
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Horizon sharpen"),
layout: Some(layout),
vertex: wgpu::VertexState {
module: shader,
entry_point: Some("vs_post"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: shader,
entry_point: Some("fs_sharpen"),
compilation_options: wgpu::PipelineCompilationOptions {
constants: &constants,
..Default::default()
},
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
})
}
/// This frame's clip space to the previous frame's, for camera-relative projections:
/// a point `camera - previous_camera` further from the previous camera than from this one.
pub(super) fn reprojection(
@@ -753,60 +647,6 @@ mod tests {
}
}
#[test]
fn baked_light_changes_and_returning_from_a_pack_reject_taa_history() {
let Some((device, _)) = test_device() else {
eprintln!("TAA baked-light GPU skipped: no test device");
return;
};
let mut post = Post::new(&device, wgpu::TextureFormat::Rgba16Float);
post.taa = true;
if !post.supports_taa() {
eprintln!("TAA baked-light GPU skipped: TAA unsupported");
return;
}
let key = (
[5.0, 4.0, 1.0, 0.0],
[0.2, 0.5, 0.6, 1.0],
[[0.1; 4], [0.4; 4]],
);
for gain in [true, false, true] {
post.baked_light(gain, key.0, key.1, key.2);
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
assert_eq!(post.taa_this_frame.jitter[2], 1.0);
post.baked_light(gain, key.0, key.1, key.2);
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
assert_eq!(post.taa_this_frame.jitter[2], 0.0);
}
for field in 0..16 {
let mut changed = key;
match field {
0..=3 => changed.0[field] += 0.01,
4..=7 => changed.1[field - 4] += 0.01,
_ => changed.2[(field - 8) / 4][(field - 8) % 4] += 0.01,
}
post.baked_light(true, changed.0, changed.1, changed.2);
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
assert_eq!(post.taa_this_frame.jitter[2], 1.0);
}
post.baked_light(false, key.0, key.1, key.2);
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
assert_eq!(post.taa_this_frame.jitter[2], 1.0);
post.baked_light(false, [0.0; 4], [0.0; 4], [[0.0; 4]; 2]);
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
assert_eq!(post.taa_this_frame.jitter[2], 0.0);
// A pack disables TAA even when the light settings stay the same. Its first
// world frame after the pack is dropped must not reuse the pre-pack history.
post.jitter([32, 32], false, Mat4::IDENTITY, DVec3::ZERO);
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
assert_eq!(post.taa_this_frame.jitter[2], 1.0);
post.jitter([32, 32], true, Mat4::IDENTITY, DVec3::ZERO);
assert_eq!(post.taa_this_frame.jitter[2], 0.0);
eprintln!(
"TAA baked-light GPU checked gain, all settings/shares/windows and the first frame after a pack"
);
}
fn test_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = crate::gfx::test_gpu::instance();
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
@@ -33,7 +33,7 @@ pub(super) fn pipeline(
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: shader,
entry_point: Some("fs_entity_unmarked"),
entry_point: Some("fs_entity"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
@@ -67,7 +67,7 @@ struct Out { @builtin(position) pos:vec4<f32>, @location(0) color:vec4<f32> }
@vertex fn vs_entity(@location(0) pos:vec3<f32>, @location(3) color:vec4<f32>)->Out {
return Out(vec4(pos,1.0),color);
}
@fragment fn fs_entity_unmarked(v:Out)->@location(0) vec4<f32> { return v.color; }
@fragment fn fs_entity(v:Out)->@location(0) vec4<f32> { return v.color; }
"#
.into(),
),
+9 -96
View File
@@ -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
+72 -129
View File
@@ -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));
}
+69 -295
View File
@@ -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}");
}
}
}
-6
View File
@@ -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;
+2 -4
View File
@@ -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()
}
+7 -118
View File
@@ -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