mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-08 08:07:53 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17a058bb2a |
No files matched your search
@@ -9,9 +9,9 @@ permissions:
|
||||
actions: write
|
||||
jobs:
|
||||
cancel:
|
||||
# One of our small runners (horizon-light), out of GitHub's pool: no checkout, no code of the pull request
|
||||
# One of playhorizon's small runners, out of GitHub's shared pool: no checkout, no code of the pull request
|
||||
# (docs/ci-runners.md). SELF_HOSTED_CI=off: GitHub's runners.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "playhorizon-light"]') || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Cancel the pull request's unfinished runs
|
||||
env:
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
name: cargo-horizon-mod
|
||||
# `cargo binstall --git https://github.com/horizon-rust/horizon cargo-horizon-mod` (#2120): binstall reads
|
||||
# [package.metadata.binstall] from crates/cargo-horizon-mod/Cargo.toml and downloads the archive this workflow
|
||||
# attaches to the tag `cargo-horizon-mod-v<version>`, whose version must be the manifest's. The targets come from
|
||||
# tools/horizon_mod_binstall.py, the same file the manifest's template is checked against, so a platform added there
|
||||
# is built here and one nobody publishes is caught before it ships. A pull request builds the archives and publishes
|
||||
# nothing.
|
||||
on:
|
||||
push:
|
||||
tags: ['cargo-horizon-mod-v*']
|
||||
pull_request:
|
||||
paths:
|
||||
- 'crates/cargo-horizon-mod/**'
|
||||
- 'tools/horizon_mod_binstall.py'
|
||||
- '.github/workflows/cargo-horizon-mod.yml'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
plan:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
platforms: ${{ steps.platforms.outputs.platforms }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: platforms
|
||||
run: echo "platforms=$(python3 -c 'import json, tools.horizon_mod_binstall as m; print(json.dumps(m.PLATFORMS))')" >> "$GITHUB_OUTPUT"
|
||||
build:
|
||||
needs: plan
|
||||
name: build ${{ matrix.target }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(needs.plan.outputs.platforms) }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: The aarch64 Linux linker
|
||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu
|
||||
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV"
|
||||
- name: Build
|
||||
run: cargo build --release --locked -p cargo-horizon-mod --target ${{ matrix.target }}
|
||||
- name: It starts
|
||||
if: matrix.run
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
binary="target/${{ matrix.target }}/release/cargo-horizon-mod"
|
||||
[ -f "$binary.exe" ] && binary="$binary.exe"
|
||||
out=$("$binary" 2>&1 || true)
|
||||
case "$out" in
|
||||
*"usage: cargo horizon-mod"*) ;;
|
||||
*) echo "$out"; echo "::error::$binary does not start"; exit 1 ;;
|
||||
esac
|
||||
- name: Package
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
binary="target/${{ matrix.target }}/release/cargo-horizon-mod"
|
||||
[ -f "$binary.exe" ] && binary="$binary.exe"
|
||||
mkdir -p dist
|
||||
tar -czf "dist/$(basename "$binary" | sed 's/\.exe$//')-${{ matrix.target }}.tar.gz" -C "$(dirname "$binary")" "$(basename "$binary")"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cargo-horizon-mod-${{ matrix.target }}
|
||||
path: dist/*.tar.gz
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
publish:
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: cargo-horizon-mod-*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: The tag and the manifest name the same release and archives
|
||||
run: python3 tools/horizon_mod_binstall.py dist "$GITHUB_REF_NAME"
|
||||
- name: Checksums
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd dist
|
||||
for file in *.tar.gz; do sha256sum "$file" | cut -d' ' -f1 > "$file.sha256"; done
|
||||
- name: Publish
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
notes="\`cargo binstall --git https://github.com/horizon-rust/horizon cargo-horizon-mod\`"
|
||||
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
|
||||
gh release upload "$GITHUB_REF_NAME" dist/* --clobber
|
||||
else
|
||||
gh release create "$GITHUB_REF_NAME" dist/* --latest=false --title "cargo-horizon-mod ${GITHUB_REF_NAME#cargo-horizon-mod-v}" --notes "$notes"
|
||||
fi
|
||||
@@ -27,18 +27,13 @@ jobs:
|
||||
# One job, not one per check: each job takes a runner from the organisation's shared pool, and with every pull request
|
||||
# and every merge to main starting this workflow, three jobs a run left the pool's queue tens of minutes deep.
|
||||
#
|
||||
# Where (docs/ci-runners.md): our checks runners (label horizon-checks: the desktop's, an isolated user, so a fork's
|
||||
# pull request runs there too) take the pull requests whose number ends in 0 or 1, about what their three runners
|
||||
# can do; GitHub's runners take the others and main's pushes (which also save the Rust cache GitHub's pull request
|
||||
# runs read), so both pools work at once. The repository variable SELF_HOSTED_CI moves them: `all` every job to
|
||||
# ours, `light` (the desktop paused) or `off` every job to GitHub's. On ours the build folder persists
|
||||
# (CARGO_TARGET_DIR, outside the checkout), so the apt packages, Python and the Rust cache steps are GitHub's only.
|
||||
# Where: GitHub's runners. With the repository variable SELF_HOSTED_CI=all (for when that pool is full: their 4 CPUs
|
||||
# are about one GitHub runner, docs/ci-runners.md), main's pushes and this repository's own pull requests run on
|
||||
# playhorizon's checks runners (label playhorizon-ci); a fork's pull request never does (and the runners refuse it
|
||||
# themselves). There the build folder persists (CARGO_TARGET_DIR, outside the checkout), so the apt packages,
|
||||
# Python and the Rust cache steps are GitHub's only.
|
||||
verify:
|
||||
runs-on: >-
|
||||
${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && (vars.SELF_HOSTED_CI == 'all' ||
|
||||
github.event_name == 'pull_request' && (endsWith(format('{0}', github.event.pull_request.number), '0') ||
|
||||
endsWith(format('{0}', github.event.pull_request.number), '1'))) &&
|
||||
fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI == 'all' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON('["self-hosted", "linux", "playhorizon-ci"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -68,16 +63,6 @@ jobs:
|
||||
else
|
||||
python tools/check_public_tree.py --history
|
||||
fi
|
||||
# Our runners as GitHub's: some Python tests compile Rust with a bare `rustc` (GitHub's image has a default
|
||||
# toolchain, our runners' own rustup homes may not), and the GPU tests find no adapter there, so they return
|
||||
# early. Ours have Mesa's software Vulkan for parity; a backend Linux lacks keeps the tests from it, so a pull
|
||||
# request's verify doesn't depend on which pool took it (#8627: a GPU test that flakes on lavapipe).
|
||||
- name: Our runners as GitHub's (a default Rust toolchain, no GPU)
|
||||
if: runner.environment != 'github-hosted'
|
||||
run: |
|
||||
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
|
||||
rustup default stable
|
||||
echo "WGPU_BACKEND=dx12" >> "$GITHUB_ENV"
|
||||
- name: Test offline research tooling
|
||||
run: python -m unittest discover -s tests -v
|
||||
- name: Install Rust toolchain and the ALSA and udev headers the client links against
|
||||
@@ -119,11 +104,7 @@ jobs:
|
||||
# changes it or a crate it builds is checked here, and so is every push to main: two pull requests can each pass
|
||||
# alone and break it together (#2059 and the registry ratings did, 2026-10-26).
|
||||
launcher:
|
||||
runs-on: >-
|
||||
${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && (vars.SELF_HOSTED_CI == 'all' ||
|
||||
github.event_name == 'pull_request' && (endsWith(format('{0}', github.event.pull_request.number), '0') ||
|
||||
endsWith(format('{0}', github.event.pull_request.number), '1'))) &&
|
||||
fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI == 'all' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON('["self-hosted", "linux", "playhorizon-ci"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -164,8 +145,8 @@ jobs:
|
||||
# a harness, an oracle, a probe or the runner, or carries the run-parity label (run-parity.yml, when it is added).
|
||||
parity-scope:
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
# A git diff of this repository's own branch: one of our small runners (horizon-light, docs/ci-runners.md).
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
# A git diff of this repository's own branch: one of playhorizon's small runners (docs/ci-runners.md).
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "playhorizon-light"]') || 'ubuntu-latest' }}
|
||||
outputs:
|
||||
run: ${{ steps.scope.outputs.run }}
|
||||
steps:
|
||||
|
||||
@@ -29,8 +29,8 @@ jobs:
|
||||
# the skipped runs of ordinary comments must not cancel a waiting one.
|
||||
concurrency:
|
||||
group: claim-${{ github.event.issue.number }}
|
||||
# One of our small runners (horizon-light), out of GitHub's pool: main's script only (docs/ci-runners.md).
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
# One of playhorizon's small runners, out of GitHub's shared pool: main's script only (docs/ci-runners.md).
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "playhorizon-light"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -56,8 +56,8 @@ jobs:
|
||||
run: python3 tools/ci/claim_command.py command
|
||||
release:
|
||||
if: github.event_name != 'issue_comment' && github.repository == 'horizon-rust/horizon'
|
||||
# One of our small runners (horizon-light), out of GitHub's pool: main's script only (docs/ci-runners.md).
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
# One of playhorizon's small runners, out of GitHub's shared pool: main's script only (docs/ci-runners.md).
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "playhorizon-light"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -15,13 +15,9 @@ on:
|
||||
- '.github/workflows/mod-registry.yml'
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: mod-registry-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
jobs:
|
||||
check:
|
||||
# Our checks runners (label horizon-checks, docs/ci-runners.md); SELF_HOSTED_CI=light or off: GitHub's.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
@@ -29,9 +25,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy
|
||||
# Ours keep their build folder between jobs.
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: runner.environment == 'github-hosted'
|
||||
with:
|
||||
workspaces: crates/horizon-mod-registry
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
@@ -21,8 +21,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
# One of our small runners (label horizon-light, docs/ci-runners.md): main's script, Mojang's public manifest.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -253,8 +253,7 @@ jobs:
|
||||
# Tells the thread that asked for this build how it ended. A conflict `pick` already explained there.
|
||||
needs: [pick, build, sign, publish]
|
||||
if: always() && inputs.issue != '' && needs.pick.result == 'success'
|
||||
# One comment, no checkout and no secret: one of our small runners (label horizon-light, docs/ci-runners.md).
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
@@ -19,9 +19,9 @@ jobs:
|
||||
concurrency:
|
||||
group: overlap-note-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
# One of our small runners (horizon-light), out of GitHub's pool: it runs main's script only (above), so a fork's
|
||||
# One of playhorizon's small runners, out of GitHub's shared pool: it runs main's script only (above), so a fork's
|
||||
# pull request runs none of its code there (docs/ci-runners.md). SELF_HOSTED_CI=off: GitHub's runners.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "playhorizon-light"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -22,24 +22,17 @@ env:
|
||||
|
||||
jobs:
|
||||
bench:
|
||||
# Our checks runners (label horizon-checks, docs/ci-runners.md): the branch and the base on the same machine, as
|
||||
# the comparison needs. SELF_HOSTED_CI=light or off: GitHub's.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Rust and the ALSA and udev headers
|
||||
env:
|
||||
HOSTED: ${{ runner.environment == 'github-hosted' }}
|
||||
run: |
|
||||
if [ "$HOSTED" = true ]; then
|
||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
|
||||
fi
|
||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
|
||||
rustup toolchain install stable --profile minimal
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: runner.environment == 'github-hosted'
|
||||
with:
|
||||
shared-key: pack-bench
|
||||
- name: Bench the branch
|
||||
@@ -53,15 +46,11 @@ jobs:
|
||||
SCALE: ${{ inputs.scale || '1' }}
|
||||
run: |
|
||||
git fetch https://github.com/horizon-rust/horizon.git "$BASE"
|
||||
# Our runners keep the folder beside the checkout between jobs.
|
||||
git worktree remove --force ../base 2> /dev/null || rm -rf ../base
|
||||
git worktree prune
|
||||
git worktree add ../base FETCH_HEAD
|
||||
cp crates/horizon-client-render/examples/pack_load_bench.rs ../base/crates/horizon-client-render/examples/
|
||||
# A bench row that times a function the base lacks keeps it from building there: then
|
||||
# the branch's own rows compare the two ways.
|
||||
target=${CARGO_TARGET_DIR:-$GITHUB_WORKSPACE/target}
|
||||
(cd ../base && CARGO_TARGET_DIR="$target" cargo run --profile fast -p horizon-client-render \
|
||||
(cd ../base && CARGO_TARGET_DIR="$GITHUB_WORKSPACE/target" cargo run --profile fast -p horizon-client-render \
|
||||
--example pack_load_bench -- --scale "$SCALE") > base.md \
|
||||
|| echo "The bench does not build on the base." > base.md
|
||||
- name: Summary
|
||||
|
||||
@@ -16,9 +16,9 @@ jobs:
|
||||
label:
|
||||
# Title-only edits change nothing: skip them before a runner starts.
|
||||
if: github.event.action != 'edited' || github.event.changes.body != null
|
||||
# One of our small runners (horizon-light), out of GitHub's pool: it runs main's script only (above), so a fork's
|
||||
# One of playhorizon's small runners, out of GitHub's shared pool: it runs main's script only (above), so a fork's
|
||||
# pull request runs none of its code there (docs/ci-runners.md). SELF_HOSTED_CI=off: GitHub's runners.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "playhorizon-light"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -15,13 +15,9 @@ on:
|
||||
- '.github/workflows/telemetry-server.yml'
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: telemetry-server-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
jobs:
|
||||
check:
|
||||
# Our checks runners (label horizon-checks, docs/ci-runners.md); SELF_HOSTED_CI=light or off: GitHub's.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
@@ -29,9 +25,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy
|
||||
# Ours keep their build folder between jobs.
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: runner.environment == 'github-hosted'
|
||||
with:
|
||||
workspaces: crates/horizon-telemetry-server
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
@@ -21,8 +21,7 @@ jobs:
|
||||
startsWith(github.event.comment.body, '/reopen') ||
|
||||
startsWith(github.event.comment.body, '/label') ||
|
||||
startsWith(github.event.comment.body, '/unlabel'))
|
||||
# One of our small runners (label horizon-light, docs/ci-runners.md): main's script only, as above.
|
||||
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
+33
-34
@@ -5,8 +5,7 @@ everything is that **it plays exactly like vanilla**. This page is the short ver
|
||||
welcome on [Discord](https://discord.gg/playhorizon).
|
||||
|
||||
This repository holds the game and the launcher, plus the tools, tests and docs that build, check and release them.
|
||||
The website (playhorizon.dev) is built in a separate repository from this one's README, `docs/api/` and roadmap. Anything
|
||||
else (videos, Discord and other community material) lives elsewhere.
|
||||
The website (playhorizon.dev) is built in a separate repository from this one's README, `docs/api/` and roadmap. Anything else (videos, Discord and other community material) lives elsewhere.
|
||||
|
||||
**Where to start: [playhorizon.dev/hub](https://playhorizon.dev/hub).** It shows what is being done now and next
|
||||
([ROADMAP.md](ROADMAP.md) in the repository), the work board, good first tasks, and the forms to report a bug or
|
||||
@@ -15,25 +14,23 @@ queue (the hub, or `/suggest` on Discord) rather than to issues, which are for b
|
||||
|
||||
## Build it
|
||||
|
||||
- **Prerequisites:** Rust (stable), Python 3.10+, JDK 25, and on Linux the ALSA and udev headers
|
||||
(`libasound2-dev libudev-dev`).
|
||||
- **Resources:** Horizon never downloads the game; it prepares its resources from your own copy of Minecraft 26.3, as
|
||||
described in [Build from source](README.md#build-from-source).
|
||||
- **Commands:**
|
||||
You need Rust (stable), Python 3.10+, JDK 25, and on Linux the ALSA and udev headers (`libasound2-dev libudev-dev`).
|
||||
Horizon never downloads the game: it prepares its resources from your own copy of Minecraft 26.3, as described in
|
||||
[Build from source](README.md#build-from-source).
|
||||
|
||||
```sh
|
||||
cargo run --release -p horizon-prepare -- --prepare --server-jar ~/Downloads/server.jar
|
||||
cargo run --release -p horizon-client
|
||||
```
|
||||
```sh
|
||||
cargo run --release -p horizon-prepare -- --prepare --server-jar ~/Downloads/server.jar
|
||||
cargo run --release -p horizon-client
|
||||
```
|
||||
|
||||
- **Launcher:** its own Cargo workspace in `launcher/` (`cd launcher && cargo run`; on NixOS, `nix-shell` first).
|
||||
The launcher is its own Cargo workspace in `launcher/` (`cd launcher && cargo run`; on NixOS, `nix-shell` first).
|
||||
|
||||
## Vanilla first
|
||||
|
||||
Wherever vanilla has a feature, vanilla is the specification, down to the order of random calls. "Parity" here means
|
||||
Horizon gives the same results as the pinned 26.3 client and server for the same inputs, checked by a **harness**: a
|
||||
`tools/test_*_parity.py` script that runs a small Java oracle against the real game and a Rust probe against Horizon,
|
||||
and compares their outputs. There are about 290 of them.
|
||||
Wherever vanilla has a feature, vanilla is the specification, down to the order of random calls. "Parity" here
|
||||
means Horizon gives the same results as the pinned 26.3 client and server for the same inputs, checked by a
|
||||
**harness**: a `tools/test_*_parity.py` script that runs a small Java oracle against the real game and a Rust
|
||||
probe against Horizon, and compares their outputs. There are about 290 of them.
|
||||
|
||||
- **Read the vanilla code before writing Horizon's.** `tools/rea_vanilla.py` queries the pinned game bytecode:
|
||||
`python3 tools/rea_vanilla.py method net.minecraft.world.level.block.DispenserBlock dispenseFrom`.
|
||||
@@ -44,10 +41,11 @@ and compares their outputs. There are about 290 of them.
|
||||
off by default wherever they would change the vanilla look.
|
||||
- **Everything players run is Rust.** Python and Java are fine for dev tools and harness drivers in `tools/`, never
|
||||
for player-facing code.
|
||||
- **No game files in the repo.** Never commit game files, decompiled source or prepared resources (`.local/`):
|
||||
`tools/check_public_tree.py` rejects them (`git config core.hooksPath .githooks` runs it before every commit).
|
||||
The pre-push hook checks the full history reachable from the branches and tags being pushed. Run
|
||||
`python3 tools/check_public_tree.py --history` to audit the history of all local refs.
|
||||
|
||||
Never commit game files, decompiled source or prepared resources (`.local/`): `tools/check_public_tree.py` rejects
|
||||
them (`git config core.hooksPath .githooks` runs it before every commit).
|
||||
The pre-push hook checks the full history reachable from the branches and tags being pushed. Run
|
||||
`python3 tools/check_public_tree.py --history` to audit the history of all local refs.
|
||||
|
||||
## Only original code, or code whose license allows it
|
||||
|
||||
@@ -56,23 +54,24 @@ another project's source may also be read and used, **only when that project's l
|
||||
|
||||
- **Allowed:** MIT, MIT-0, BSD-2-Clause, BSD-3-Clause, 0BSD, ISC, zlib, Apache-2.0, Unlicense, and the CC0-1.0
|
||||
public-domain dedication. That code may be read, copied or adapted.
|
||||
- **Off limits, even to read:** GPL, LGPL (Iris), AGPL, MPL, EPL, Polyform (Sodium), the Creative Commons licenses
|
||||
(CC BY, CC BY-SA, CC BY-NC and the others; CC0 is a public-domain dedication, not one of them), custom or
|
||||
"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.
|
||||
- **Everything else is off limits, even to read:** GPL, LGPL (Iris), AGPL, MPL, EPL, Polyform (Sodium), the Creative
|
||||
Commons licenses (CC BY, CC BY-SA, CC BY-NC and the others; CC0 is a public-domain dedication, not one of them),
|
||||
custom or "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.
|
||||
- **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.
|
||||
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.
|
||||
- **Say it in the pull request:** the project, a link to the commit, its license, and what you read or copied. A
|
||||
pull request that used another project's source without saying so is closed.
|
||||
- **Copied or adapted code** keeps its copyright line and license notice at the top of the file, and gets an entry
|
||||
in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). Apache-2.0 code also keeps its NOTICE text and says what
|
||||
was changed.
|
||||
|
||||
User documentation and file formats are fine to read, and packs and mods are fine to run and observe, whatever their
|
||||
license. Shader packs are a clean-room rewrite of the pack format, produced from a written description of it (see
|
||||
#1561); they carry no foreign source, and the rules above still apply to anything you read while working on them.
|
||||
User documentation and file formats are fine to read, and packs and mods are fine to run and observe, whatever
|
||||
their license. Shader packs were removed on 24 October 2026 (#1561) because their code had been written from
|
||||
another project's source; they come back only as a clean-room rewrite from a written description of the pack
|
||||
format.
|
||||
|
||||
## Before asking for review
|
||||
|
||||
@@ -88,8 +87,8 @@ your change can affect. Without the vanilla data on your machine it lists those
|
||||
`--quick` skips the harnesses. It exits non-zero when something fails.
|
||||
|
||||
For the game workspace, Clippy checks all targets of the touched crates and their transitive workspace dependents,
|
||||
including examples. The dependency scope includes optional, platform, build and development dependencies. Tests run
|
||||
library, binary, integration and doc tests without building unrelated example executables. Shared Cargo
|
||||
including examples. The dependency scope includes optional, platform, build and development dependencies.
|
||||
Tests run library, binary, integration and doc tests without building unrelated example executables. Shared Cargo
|
||||
configuration and toolchain changes select the whole workspace; CI also keeps its workspace-wide checks.
|
||||
|
||||
## Claim before you start
|
||||
@@ -124,8 +123,8 @@ it (edited on every push, never repeated): talk to its author in the thread befo
|
||||
that label, and area labels (`launcher`, `mobs`, `worldgen`, `rendering`...) from the paths you change.
|
||||
`label:parity` is the list of real progress toward vanilla.
|
||||
- **Release labels** (we set them if you can't): `feature` also puts it under "New" in the release notes,
|
||||
`skip-release` when players see no change (tooling, docs, tests), nothing for a fix. `release-now` is for crash
|
||||
fixes that shouldn't wait. pr_ready suggests one.
|
||||
`skip-release` when players see no change (tooling, docs, tests), nothing for a fix.
|
||||
`release-now` is for crash fixes that shouldn't wait. pr_ready suggests one.
|
||||
|
||||
Who reviews and merges: the Horizon Team, on CI for ordinary pull requests; risky ones (saves, network, world
|
||||
generation, the launcher's updates) wait for a local proof first ([merging](docs/release-flow.md#merging)).
|
||||
|
||||
Generated
-14
@@ -1429,7 +1429,6 @@ dependencies = [
|
||||
"horizon-world",
|
||||
"log",
|
||||
"miniz_oxide 0.8.9",
|
||||
"mio",
|
||||
"naga",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
@@ -1446,7 +1445,6 @@ dependencies = [
|
||||
"ureq",
|
||||
"wgpu",
|
||||
"winit",
|
||||
"xxhash-rust",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -2317,18 +2315,6 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multiversion_no_op"
|
||||
version = "1.0.0"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<img src="https://img.shields.io/badge/Minecraft-Java%2026.3-5D9C3F" alt="Minecraft Java 26.3">
|
||||
<img src="https://img.shields.io/badge/Rust-stable-B7410E?logo=rust&logoColor=white" alt="Rust">
|
||||
<img src="https://img.shields.io/badge/vanilla%20parity-100%25-F0795A" alt="Vanilla parity 100%">
|
||||
<a href="#project-status"><img src="https://img.shields.io/badge/player--visible%20gaps-0%20open-F0795A" alt="Player-visible gaps open"></a>
|
||||
<a href="#project-status"><img src="https://img.shields.io/badge/player--visible%20gaps-833%20open-F0795A" alt="Player-visible gaps open"></a>
|
||||
<img src="https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-2A2470" alt="Linux, macOS and Windows">
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3B2F7E" alt="MIT license"></a>
|
||||
</p>
|
||||
@@ -122,13 +122,13 @@ It runs rustfmt, clippy, the touched crates' tests, the Python tests, the public
|
||||
|
||||
## Project status
|
||||
|
||||
**Player-visible gaps: 0 open** of the 833 the [port ledger](tools/port_ledger.py) found, reading every vanilla 26.3 method against Horizon's code: vanilla behaviour a player would notice that Horizon doesn't have yet. By area: Animals [0](https://github.com/horizon-rust/horizon/issues/3512); Monsters and bosses [0](https://github.com/horizon-rust/horizon/issues/3513); Entities, projectiles, vehicles and decorations [0](https://github.com/horizon-rust/horizon/issues/3514); Items, enchantments and inventories [0](https://github.com/horizon-rust/horizon/issues/3515); Blocks, redstone and fluids [0](https://github.com/horizon-rust/horizon/issues/3516); World, world generation and saves [0](https://github.com/horizon-rust/horizon/issues/3517); Screens, HUD and client packets [0](https://github.com/horizon-rust/horizon/issues/3518); Entity and block rendering, models [0](https://github.com/horizon-rust/horizon/issues/3519); Server and players on the server [0](https://github.com/horizon-rust/horizon/issues/3520); Mob AI, villagers and raids [0](https://github.com/horizon-rust/horizon/issues/3521). This number goes down as gaps close (`tools/parity_gaps.py`); the harness results below say that what is tested matches.
|
||||
**Player-visible gaps: 833 open** of the 833 the [port ledger](tools/port_ledger.py) found, reading every vanilla 26.3 method against Horizon's code: vanilla behaviour a player would notice that Horizon doesn't have yet. By area: Screens, HUD and client packets [197](https://github.com/horizon-rust/horizon/issues/3518); Entity and block rendering, models [129](https://github.com/horizon-rust/horizon/issues/3519); Animals [110](https://github.com/horizon-rust/horizon/issues/3512); Server and players on the server [106](https://github.com/horizon-rust/horizon/issues/3520); Blocks, redstone and fluids [86](https://github.com/horizon-rust/horizon/issues/3516); Entities, projectiles, vehicles and decorations [64](https://github.com/horizon-rust/horizon/issues/3514); Monsters and bosses [53](https://github.com/horizon-rust/horizon/issues/3513); Mob AI, villagers and raids [35](https://github.com/horizon-rust/horizon/issues/3521); World, world generation and saves [32](https://github.com/horizon-rust/horizon/issues/3517); Items, enchantments and inventories [21](https://github.com/horizon-rust/horizon/issues/3515). This number goes down as gaps close (`tools/parity_gaps.py`); the harness results below say that what is tested matches.
|
||||
|
||||
The table is the project's own audit: every subsystem matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not that nothing else differs.
|
||||
|
||||
### Java parity: 100%
|
||||
|
||||
Scores: 0 (absent), 0.25 (started), 0.5 (playable, major features missing), 0.75 (mostly complete) or 1 (complete and verified by differential tests); the percentage is their average. Last audited 2026-10-13: the harnesses pass on Linux and macOS. The [port ledger](tools/port_ledger.py) adds a method-by-method view of what is ported.
|
||||
Scores: 0 (absent), 0.25 (started), 0.5 (playable, major features missing), 0.75 (mostly complete) or 1 (complete and verified by differential tests); the percentage is their average. Last audited 2026-10-13: all 281 harnesses pass on Linux and macOS. The [port ledger](tools/port_ledger.py) adds a method-by-method view of what is ported.
|
||||
|
||||
| Subsystem | Score | Main gaps |
|
||||
| --- | ---: | --- |
|
||||
|
||||
@@ -14,9 +14,6 @@ Apache-2.0; both licenses are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
|
||||
fixes to its GLSL frontend that shader packs need, each applied in the commit that needed it. It is licensed under MIT or Apache-2.0; both licenses
|
||||
are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
|
||||
|
||||
The GLSL frontend also resets the common type for each comma-separated declarator so one
|
||||
variable's array specifier does not change later variables.
|
||||
|
||||
## libmimalloc-sys and mimalloc
|
||||
|
||||
`third_party/libmimalloc-sys` is [libmimalloc-sys](https://github.com/purpleprotocol/mimalloc_rust)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Server load bench: the same horizon-bots load on Horizon, vanilla 26.3 or Pumpkin.
|
||||
|
||||
python3 tools/bench.py --server horizon --exe target/release/horizon-server --bots 50
|
||||
python3 tools/bench.py --server vanilla --jar ~/mc/server.jar --bots 50 --pre-generated ~/worlds/seed12345
|
||||
python3 tools/bench.py --server pumpkin --exe ./pumpkin --bots 50
|
||||
python3 bench/bench.py --server horizon --exe target/release/horizon-server --bots 50
|
||||
python3 bench/bench.py --server vanilla --jar ~/mc/server.jar --bots 50 --pre-generated ~/worlds/seed12345
|
||||
python3 bench/bench.py --server pumpkin --exe ./pumpkin --bots 50
|
||||
|
||||
Every run uses a fresh scratch folder (bound to 127.0.0.1, offline, creative so the bots can fly), starts the
|
||||
server, lets `horizon-bots` join one every --join-every seconds, warms up, then measures for --seconds:
|
||||
@@ -39,11 +39,6 @@ import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
try:
|
||||
from . import lab
|
||||
except ImportError:
|
||||
import lab
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
EXE = '.exe' if os.name == 'nt' else ''
|
||||
SERVERS = ('horizon', 'vanilla', 'pumpkin')
|
||||
@@ -212,7 +207,8 @@ class Server:
|
||||
self.lines = []
|
||||
self.lock = threading.Lock()
|
||||
self.log = log_path.open('w', encoding='utf-8')
|
||||
env = lab.game_env(HORIZON_HOME=directory / 'horizon-home')
|
||||
env = dict(os.environ, HORIZON_NO_AUDIO='1', HORIZON_HOME=str(directory / 'horizon-home'),
|
||||
HORIZON_LAUNCHER_NO_SELF_UPDATE='1')
|
||||
self.process = subprocess.Popen([str(c) for c in command], cwd=directory, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||
encoding='utf-8', errors='replace', env=env)
|
||||
@@ -1,27 +0,0 @@
|
||||
# Particle culling review measurements — 2026-11-02
|
||||
|
||||
Ordinary-scene captures and measurements for PR #7054. The scene contains 24 mobs, about 9,400–10,100 rain particles, and about 50–100 campfire smoke particles. Two opposite-order passes were run with 2 s warmup and 8 s measurement per condition. Host: Intel Core i7-3770S, Intel HD Graphics 4000, Vulkan/Mesa, 675×727, render distance 4, VSync off, 260 FPS cap.
|
||||
|
||||
These are means of the two samples. CPU is the harness's average CPU time per frame; p99 is frame time. The normal scene is not expected to show large gains because most particles are visible.
|
||||
|
||||
| Condition | FPS | CPU ms/frame | Frame p99 ms |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Main, culling Off | 88.39 | 1.930 | 23.85 |
|
||||
| Particle branch, culling Off | 88.55 | 1.828 | 24.88 |
|
||||
| Main, Entity Culling Synchronous | 89.12 | 1.887 | 23.07 |
|
||||
| Particle branch, Entity Culling Synchronous, Particle Culling Off | 89.06 | 1.897 | 25.18 |
|
||||
| Particle Culling Synchronous | 88.87 | 1.774 | 24.94 |
|
||||
| Particle Culling Asynchronous* | 89.00 | 1.754 | 24.90 |
|
||||
|
||||
*The asynchronous samples predate the final switch to a dedicated particle worker. They are retained only as historical context and are not a measurement of the final worker implementation. No FPS improvement is claimed from these ordinary-scene runs. The Off path delegates directly to `render`; `disabled_culled_render_matches_the_regular_particle_path` checks that it produces the same vertices.
|
||||
|
||||
The FPS and CPU differences are within run-to-run variability for this GPU-limited scene. The first four conditions are the reviewer's requested Main-versus-branch default and existing synchronous-mode comparisons. Particle Sync/Async rows are branch-internal mode comparisons.
|
||||
|
||||
## Captures
|
||||
|
||||
The screenshots show the same ordinary test world and FPS overlay:
|
||||
|
||||
- `main-off.png`
|
||||
- `particle-off.png`
|
||||
- `particle-sync.png`
|
||||
- `particle-async.png`
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 342 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 343 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 341 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 341 KiB |
@@ -6,14 +6,6 @@ license.workspace = true
|
||||
rust-version = "1.86"
|
||||
description = "cargo horizon-mod new/build/run/check: make Horizon content mods (docs/mods.md)"
|
||||
|
||||
# `cargo binstall --git https://github.com/horizon-rust/horizon cargo-horizon-mod` (#2120) reads this: the archive
|
||||
# `cargo-horizon-mod`'s workflow attaches to the `cargo-horizon-mod-v<version>` release for the runner's target,
|
||||
# holding the binary at its root. tools/horizon_mod_binstall.py expands it for the workflow's targets.
|
||||
[package.metadata.binstall]
|
||||
pkg-url = "https://github.com/horizon-rust/horizon/releases/download/cargo-horizon-mod-v{ version }/{ name }-{ target }.tar.gz"
|
||||
bin-dir = "{ bin }{ binary-ext }"
|
||||
pkg-fmt = "tgz"
|
||||
|
||||
[dependencies]
|
||||
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["index"] }
|
||||
serde_json = "1.0.151"
|
||||
|
||||
@@ -100,222 +100,6 @@ impl ResourceSource for Bundled {
|
||||
"assets/horizon/lang/ur_pk.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ur_pk.json")
|
||||
}
|
||||
"assets/horizon/lang/fa_ir.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/fa_ir.json")
|
||||
}
|
||||
"assets/horizon/lang/he_il.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/he_il.json")
|
||||
}
|
||||
"assets/horizon/lang/hu_hu.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/hu_hu.json")
|
||||
}
|
||||
"assets/horizon/lang/ro_ro.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ro_ro.json")
|
||||
}
|
||||
"assets/horizon/lang/da_dk.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/da_dk.json")
|
||||
}
|
||||
"assets/horizon/lang/fi_fi.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/fi_fi.json")
|
||||
}
|
||||
"assets/horizon/lang/no_no.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/no_no.json")
|
||||
}
|
||||
"assets/horizon/lang/bg_bg.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/bg_bg.json")
|
||||
}
|
||||
"assets/horizon/lang/sk_sk.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/sk_sk.json")
|
||||
}
|
||||
"assets/horizon/lang/lt_lt.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/lt_lt.json")
|
||||
}
|
||||
"assets/horizon/lang/it_it.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/it_it.json")
|
||||
}
|
||||
"assets/horizon/lang/nl_nl.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/nl_nl.json")
|
||||
}
|
||||
"assets/horizon/lang/vi_vn.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/vi_vn.json")
|
||||
}
|
||||
"assets/horizon/lang/th_th.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/th_th.json")
|
||||
}
|
||||
"assets/horizon/lang/ja_jp.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ja_jp.json")
|
||||
}
|
||||
"assets/horizon/lang/ar_sa.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ar_sa.json")
|
||||
}
|
||||
"assets/horizon/lang/pt_br.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/pt_br.json")
|
||||
}
|
||||
"assets/horizon/lang/cs_cz.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/cs_cz.json")
|
||||
}
|
||||
"assets/horizon/lang/sv_se.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/sv_se.json")
|
||||
}
|
||||
"assets/horizon/lang/el_gr.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/el_gr.json")
|
||||
}
|
||||
"assets/horizon/lang/ca_es.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ca_es.json")
|
||||
}
|
||||
"assets/horizon/lang/et_ee.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/et_ee.json")
|
||||
}
|
||||
"assets/horizon/lang/lv_lv.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/lv_lv.json")
|
||||
}
|
||||
"assets/horizon/lang/sl_si.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/sl_si.json")
|
||||
}
|
||||
"assets/horizon/lang/sr_cs.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/sr_cs.json")
|
||||
}
|
||||
"assets/horizon/lang/is_is.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/is_is.json")
|
||||
}
|
||||
"assets/horizon/lang/af_za.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/af_za.json")
|
||||
}
|
||||
"assets/horizon/lang/ms_my.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ms_my.json")
|
||||
}
|
||||
"assets/horizon/lang/be_by.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/be_by.json")
|
||||
}
|
||||
"assets/horizon/lang/sr_sp.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/sr_sp.json")
|
||||
}
|
||||
"assets/horizon/lang/az_az.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/az_az.json")
|
||||
}
|
||||
"assets/horizon/lang/uz_uz.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/uz_uz.json")
|
||||
}
|
||||
"assets/horizon/lang/kk_kz.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/kk_kz.json")
|
||||
}
|
||||
"assets/horizon/lang/ka_ge.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ka_ge.json")
|
||||
}
|
||||
"assets/horizon/lang/hy_am.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/hy_am.json")
|
||||
}
|
||||
"assets/horizon/lang/bs_ba.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/bs_ba.json")
|
||||
}
|
||||
"assets/horizon/lang/mk_mk.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/mk_mk.json")
|
||||
}
|
||||
"assets/horizon/lang/sq_al.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/sq_al.json")
|
||||
}
|
||||
"assets/horizon/lang/cy_gb.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/cy_gb.json")
|
||||
}
|
||||
"assets/horizon/lang/ga_ie.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ga_ie.json")
|
||||
}
|
||||
"assets/horizon/lang/eu_es.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/eu_es.json")
|
||||
}
|
||||
"assets/horizon/lang/gl_es.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/gl_es.json")
|
||||
}
|
||||
"assets/horizon/lang/fil_ph.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/fil_ph.json")
|
||||
}
|
||||
"assets/horizon/lang/ta_in.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ta_in.json")
|
||||
}
|
||||
"assets/horizon/lang/mn_mn.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/mn_mn.json")
|
||||
}
|
||||
"assets/horizon/lang/lo_la.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/lo_la.json")
|
||||
}
|
||||
"assets/horizon/lang/so_so.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/so_so.json")
|
||||
}
|
||||
"assets/horizon/lang/fy_nl.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/fy_nl.json")
|
||||
}
|
||||
"assets/horizon/lang/gd_gb.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/gd_gb.json")
|
||||
}
|
||||
"assets/horizon/lang/lb_lu.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/lb_lu.json")
|
||||
}
|
||||
"assets/horizon/lang/ky_kg.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ky_kg.json")
|
||||
}
|
||||
"assets/horizon/lang/kn_in.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/kn_in.json")
|
||||
}
|
||||
"assets/horizon/lang/mt_mt.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/mt_mt.json")
|
||||
}
|
||||
"assets/horizon/lang/eo_uy.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/eo_uy.json")
|
||||
}
|
||||
"assets/horizon/lang/haw_us.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/haw_us.json")
|
||||
}
|
||||
"assets/horizon/lang/ig_ng.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ig_ng.json")
|
||||
}
|
||||
"assets/horizon/lang/la_la.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/la_la.json")
|
||||
}
|
||||
"assets/horizon/lang/tt_ru.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/tt_ru.json")
|
||||
}
|
||||
"assets/horizon/lang/yi_de.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/yi_de.json")
|
||||
}
|
||||
"assets/horizon/lang/yo_ng.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/yo_ng.json")
|
||||
}
|
||||
"assets/horizon/lang/ba_ru.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/ba_ru.json")
|
||||
}
|
||||
"assets/horizon/lang/br_fr.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/br_fr.json")
|
||||
}
|
||||
"assets/horizon/lang/cv_cu.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/cv_cu.json")
|
||||
}
|
||||
"assets/horizon/lang/fo_fo.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/fo_fo.json")
|
||||
}
|
||||
"assets/horizon/lang/fur_it.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/fur_it.json")
|
||||
}
|
||||
"assets/horizon/lang/li_li.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/li_li.json")
|
||||
}
|
||||
"assets/horizon/lang/lmo.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/lmo.json")
|
||||
}
|
||||
"assets/horizon/lang/oc_fr.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/oc_fr.json")
|
||||
}
|
||||
"assets/horizon/lang/sah_sah.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/sah_sah.json")
|
||||
}
|
||||
"assets/horizon/lang/se_no.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/se_no.json")
|
||||
}
|
||||
"assets/horizon/lang/szl.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/szl.json")
|
||||
}
|
||||
"assets/horizon/lang/vec_it.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/vec_it.json")
|
||||
}
|
||||
"assets/horizon/lang/de_de.json" | "assets/horizon/lang/de_at.json" => {
|
||||
include_str!("../../horizon-client/assets/horizon/lang/de_de.json")
|
||||
}
|
||||
|
||||
@@ -63,10 +63,6 @@ ab_glyph = "0.2.32"
|
||||
log = "0.4"
|
||||
chrono = { version = "0.4.42", default-features = false, features = ["clock", "std"] }
|
||||
flate2 = { version = "1.1.10", default-features = false, features = ["zlib-rs", "runtime_detection"] }
|
||||
# The network threads of a server's players: epoll, kqueue or Windows' AFD (lan/io_pool.rs).
|
||||
mio = { version = "1.2.4", features = ["os-poll", "net"] }
|
||||
# Shared chunk frames are found by one keyed 128-bit hash of their body (lan/conn.rs SharedFrames).
|
||||
xxhash-rust = { version = "0.8.19", features = ["xxh3"] }
|
||||
horizon-theme = { path = "../horizon-theme" }
|
||||
# Opt-in data collection: the game keeps, the launcher sends (docs/telemetry.md); `live` is
|
||||
# the anonymous "I'm playing" ping the game sends itself (`heartbeat.rs`).
|
||||
|
||||
@@ -64,7 +64,6 @@ fn fixture(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
None,
|
||||
GraphicsApi::Vulkan,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
))?;
|
||||
assert!(!renderer.pack_entity_shadows(), "no selected pack");
|
||||
|
||||
@@ -1,827 +0,0 @@
|
||||
//! Draws a scene `horizon-ctl render-capture DIR` wrote, offscreen, from its camera, with the
|
||||
//! real `Renderer`, and prints GPU and CPU milliseconds per frame and Horizon's own passes'
|
||||
//! GPU times as JSON (docs/graphics.md, "Render capture and replay").
|
||||
//! `cargo run --release -p horizon-client-render --example scene_replay -- DIR [--frames 300]
|
||||
//! [--warmup 60] [--yaw-offsets 0,90,180,270] [--texture-filtering 0|1|2] [--anisotropy-bit N]
|
||||
//! [--mipmaps N] [--lod-distance CHUNKS] [--render-distance CHUNKS] [--size WxH]
|
||||
//! [--section-draws flat|indirect] [--section-cull on|off] [--section-sides on|off]
|
||||
//! [--gpu-culling on|off] [--root RESOURCES] [--screenshot PNG]
|
||||
//! [--world SAVE --leaf-culling MODE] [--memory on]`
|
||||
//! `--screenshot` saves the first view's last frame, after the timed frames.
|
||||
//! `--world` meshes the captured sections again from that save's region files
|
||||
//! (blocks, biomes and saved light; `ao`, `biomeBlendRadius` and `cutoutLeaves`
|
||||
//! from `./options.txt`) with `--leaf-culling` (default `off`), so a mesher
|
||||
//! change can be measured on the captured view. A section whose 3x3 columns are
|
||||
//! not all saved keeps its captured mesh.
|
||||
//! `--memory on` adds the renderer's largest GPU resources and, on macOS, the process's
|
||||
//! physical footprint by category (`footprint`), taken after the timed frames.
|
||||
use horizon_client_render::{
|
||||
assets::{Assets, RenderAssets},
|
||||
biome::Biomes,
|
||||
blocks::BlockFacts,
|
||||
chunk::{BIOME_STRATEGY, Chunk, ColumnLayout, Light, block_strategy},
|
||||
fast_hash::{FxHashMap, FxHashSet},
|
||||
gfx::{
|
||||
PreparedTextures, Renderer, ScreenshotRequest,
|
||||
capture::{self, AtlasCheck, Scene},
|
||||
panorama::Faces,
|
||||
},
|
||||
graphics::LeafCullingMode,
|
||||
hud::HudBatch,
|
||||
lod::TileKey,
|
||||
mesher::{MeshScratch, MeshTables, Mesher, SectionMesh, packed},
|
||||
models,
|
||||
};
|
||||
use horizon_core::{
|
||||
biome::{BiomeZoom, obfuscate_seed},
|
||||
paletted::PalettedContainer,
|
||||
};
|
||||
use horizon_world::{
|
||||
chunk::BlockState,
|
||||
nbt::{Limits, NamedTag, Tag},
|
||||
region::RegionFile,
|
||||
registry::BlockRegistry,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, EventLoop},
|
||||
window::{Window, WindowId},
|
||||
};
|
||||
|
||||
// The game's allocator, so that `--memory`'s footprint is the game's: the system heap kept
|
||||
// gigabytes of the meshes `--world` frees dirty.
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: horizon_threads::Allocator = horizon_threads::ALLOCATOR;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Options {
|
||||
dir: PathBuf,
|
||||
frames: usize,
|
||||
warmup: usize,
|
||||
yaw_offsets: Vec<f32>,
|
||||
root: Option<PathBuf>,
|
||||
texture_filtering: Option<i32>,
|
||||
anisotropy_bit: Option<i32>,
|
||||
mipmaps: Option<u32>,
|
||||
lod_distance: Option<u32>,
|
||||
render_distance: Option<i32>,
|
||||
size: Option<[u32; 2]>,
|
||||
flat: Option<bool>,
|
||||
cull: Option<bool>,
|
||||
sides: Option<bool>,
|
||||
gpu_culling: Option<bool>,
|
||||
screenshot: Option<PathBuf>,
|
||||
world: Option<PathBuf>,
|
||||
leaf_culling: LeafCullingMode,
|
||||
memory: bool,
|
||||
}
|
||||
|
||||
fn on_off(value: &str) -> Result<bool, String> {
|
||||
match value {
|
||||
"on" | "flat" => Ok(true),
|
||||
"off" | "indirect" => Ok(false),
|
||||
other => Err(format!("expected on or off, got {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn options() -> Result<Options, String> {
|
||||
let mut o = Options {
|
||||
frames: 300,
|
||||
warmup: 60,
|
||||
yaw_offsets: vec![0.0],
|
||||
..Default::default()
|
||||
};
|
||||
let mut args = std::env::args().skip(1);
|
||||
let mut dir = None;
|
||||
while let Some(arg) = args.next() {
|
||||
if !arg.starts_with("--") {
|
||||
dir = Some(PathBuf::from(arg));
|
||||
continue;
|
||||
}
|
||||
let value = args.next().ok_or(format!("{arg} needs a value"))?;
|
||||
let number = |v: &str| v.parse::<i64>().map_err(|e| format!("{arg}: {e}"));
|
||||
match arg.as_str() {
|
||||
"--frames" => o.frames = number(&value)?.max(1) as usize,
|
||||
"--warmup" => o.warmup = number(&value)?.max(0) as usize,
|
||||
"--yaw-offsets" => {
|
||||
o.yaw_offsets = value
|
||||
.split(',')
|
||||
.map(|v| v.trim().parse::<f32>().map_err(|e| format!("{arg}: {e}")))
|
||||
.collect::<Result<_, _>>()?
|
||||
}
|
||||
"--root" => o.root = Some(value.into()),
|
||||
"--texture-filtering" => o.texture_filtering = Some(number(&value)? as i32),
|
||||
"--anisotropy-bit" => o.anisotropy_bit = Some(number(&value)? as i32),
|
||||
"--mipmaps" => o.mipmaps = Some(number(&value)?.max(0) as u32),
|
||||
"--lod-distance" => o.lod_distance = Some(number(&value)?.max(0) as u32),
|
||||
"--render-distance" => o.render_distance = Some(number(&value)?.max(0) as i32),
|
||||
"--size" => {
|
||||
let (w, h) = value.split_once('x').ok_or("--size takes WxH")?;
|
||||
o.size = Some([number(w)? as u32, number(h)? as u32]);
|
||||
}
|
||||
"--section-draws" => o.flat = Some(on_off(&value)?),
|
||||
"--section-cull" => o.cull = Some(on_off(&value)?),
|
||||
"--section-sides" => o.sides = Some(on_off(&value)?),
|
||||
"--gpu-culling" => o.gpu_culling = Some(on_off(&value)?),
|
||||
"--screenshot" => o.screenshot = Some(value.into()),
|
||||
"--world" => o.world = Some(value.into()),
|
||||
"--memory" => o.memory = on_off(&value)?,
|
||||
"--leaf-culling" => {
|
||||
o.leaf_culling = LeafCullingMode::ALL
|
||||
.into_iter()
|
||||
.find(|mode| mode.name() == value)
|
||||
.ok_or(format!("no leaf culling mode {value}"))?;
|
||||
}
|
||||
other => return Err(format!("unknown option {other}")),
|
||||
}
|
||||
}
|
||||
o.dir = dir.ok_or("the capture directory is required")?;
|
||||
Ok(o)
|
||||
}
|
||||
|
||||
/// Milliseconds: average, median, 95th percentile and slowest, over `samples`.
|
||||
fn summary(samples: &[f32]) -> Value {
|
||||
if samples.is_empty() {
|
||||
return Value::Null;
|
||||
}
|
||||
let mut sorted = samples.to_vec();
|
||||
sorted.sort_by(f32::total_cmp);
|
||||
let at = |q: f32| sorted[((sorted.len() - 1) as f32 * q).round() as usize];
|
||||
json!({
|
||||
"avg": samples.iter().sum::<f32>() / samples.len() as f32,
|
||||
"min": sorted[0],
|
||||
"p10": at(0.1),
|
||||
"p50": at(0.5),
|
||||
"p95": at(0.95),
|
||||
"max": sorted[sorted.len() - 1],
|
||||
"samples": samples.len(),
|
||||
})
|
||||
}
|
||||
|
||||
fn ms(d: Duration) -> f32 {
|
||||
d.as_secs_f32() * 1000.0
|
||||
}
|
||||
|
||||
fn replay(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let o = options()?;
|
||||
let scene = Scene::read(&o.dir)?;
|
||||
let settings = &scene.settings;
|
||||
let root = o.root.clone().unwrap_or_else(|| settings.resources.clone());
|
||||
let loading = Instant::now();
|
||||
let assets = Assets::load(&root)?;
|
||||
let render = assets.reload(&root)?;
|
||||
if AtlasCheck::of(&render) != settings.atlas {
|
||||
return Err(format!(
|
||||
"the block sprites at {} are not the ones the capture was made with (resource packs \
|
||||
or mods?): pass --root with the game's resource root",
|
||||
root.display()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let one = [255u8; 4];
|
||||
let zero = [0u8; 4];
|
||||
let prepared = PreparedTextures::new(
|
||||
&render.pixels,
|
||||
render.sprite_size,
|
||||
render.texture_layers,
|
||||
&render.sprite_mips,
|
||||
(&zero[..], 1),
|
||||
)
|
||||
.with_pbr(
|
||||
&render.pbr_normals,
|
||||
&render.pbr_specular,
|
||||
render.pbr_size,
|
||||
render.texture_layers,
|
||||
);
|
||||
let mut renderer = pollster::block_on(Renderer::new(
|
||||
window,
|
||||
true,
|
||||
&render.pixels,
|
||||
render.sprite_size,
|
||||
render.sprite_minimum_size,
|
||||
render.texture_layers,
|
||||
&render.sprite_mips,
|
||||
&render.animations,
|
||||
(&one[..], 1, 1),
|
||||
(&zero[..], 1, 1),
|
||||
(&zero[..], 1, 1),
|
||||
(&zero[..], 1),
|
||||
Arc::new(Faces::colour([0, 0, 0, 255])),
|
||||
Some(prepared),
|
||||
None,
|
||||
settings.graphics_api(),
|
||||
settings.translucent_batching,
|
||||
false,
|
||||
false,
|
||||
))?;
|
||||
let state = &scene.renderer;
|
||||
let [width, height] = o.size.unwrap_or(state.size);
|
||||
renderer.resize(width, height);
|
||||
renderer.gamma = state.gamma;
|
||||
renderer.fullbright = state.fullbright;
|
||||
renderer.improved_transparency = state.improved_transparency;
|
||||
renderer.animated_textures = state.animated_textures;
|
||||
let filtering = o.texture_filtering.unwrap_or(settings.texture_filtering);
|
||||
let anisotropy = o.anisotropy_bit.unwrap_or(settings.anisotropy_bit);
|
||||
renderer.set_texture_filtering(filtering, anisotropy);
|
||||
renderer.set_mipmap_levels(o.mipmaps.unwrap_or(settings.mipmap_levels));
|
||||
renderer.set_world_post(
|
||||
settings.resolution_scale,
|
||||
settings.fxaa,
|
||||
settings.taa,
|
||||
settings.upscale,
|
||||
);
|
||||
let mut notes = Vec::new();
|
||||
let mut note = |what: &str, result: horizon_client_render::Result<()>| {
|
||||
if let Err(e) = result {
|
||||
notes.push(format!("{what}: {e}"));
|
||||
}
|
||||
};
|
||||
note(
|
||||
"section-draws",
|
||||
renderer.set_flat_draws(o.flat.unwrap_or(state.flat_draws)),
|
||||
);
|
||||
note(
|
||||
"section-cull",
|
||||
renderer.set_section_cull(o.cull.unwrap_or(state.section_cull)),
|
||||
);
|
||||
note(
|
||||
"gpu-culling",
|
||||
renderer.set_gpu_culling(o.gpu_culling.unwrap_or(state.gpu_culling)),
|
||||
);
|
||||
renderer.set_section_sides(o.sides.unwrap_or(state.section_sides));
|
||||
renderer.set_pass_timing(true);
|
||||
let layout = ColumnLayout::of_level(state.min_y, state.sections as i32 * 16);
|
||||
if layout != ColumnLayout::VANILLA {
|
||||
renderer.clear_sections(layout);
|
||||
}
|
||||
|
||||
// Near sections first: with no far tile resident yet, none fades in.
|
||||
let camera = scene.frame.camera;
|
||||
let column = |x: f64| (x.floor() as i32) >> 4;
|
||||
let (cx, cz) = (column(camera[0]), column(camera[2]));
|
||||
let near = |x: i32, z: i32| {
|
||||
o.render_distance
|
||||
.is_none_or(|r| (x - cx).pow(2) + (z - cz).pow(2) <= r * r)
|
||||
};
|
||||
let (mut remeshed, mut world_report) = (FxHashMap::default(), Value::Null);
|
||||
if let Some(world) = &o.world {
|
||||
let keys: Vec<[i32; 3]> = section_keys(&o.dir)?
|
||||
.into_iter()
|
||||
.filter(|&[x, _, z]| near(x, z))
|
||||
.collect();
|
||||
let started = Instant::now();
|
||||
remeshed = remesh(world, &root, &assets, &render, o.leaf_culling, &keys)?;
|
||||
world_report = json!({
|
||||
"save": world.display().to_string(),
|
||||
"leaf_culling": o.leaf_culling.name(),
|
||||
"remeshed": remeshed.len(),
|
||||
"kept": keys.len() - remeshed.len(),
|
||||
"remesh_ms": ms(started.elapsed()),
|
||||
});
|
||||
}
|
||||
let leaf_layers = leaf_layers(&render);
|
||||
let (mut sections, mut quads, mut leaf_quads) = (0, 0usize, 0usize);
|
||||
let mut packed_quads = 0usize;
|
||||
capture::read_meshes(&o.dir, "sections", |[x, y, z], captured| {
|
||||
if near(x, z) {
|
||||
let mesh = remeshed.remove(&[x, y, z]).unwrap_or(captured);
|
||||
quads += mesh.solid.len() / 4;
|
||||
packed_quads += packed::packed_quads(&mesh.packed, mesh.solid.len() / 4);
|
||||
leaf_quads += mesh
|
||||
.solid
|
||||
.iter()
|
||||
.filter(|v| leaf_layers.contains(&v.layer))
|
||||
.count()
|
||||
/ 4;
|
||||
renderer.upload((x, y, z), &mesh);
|
||||
sections += 1;
|
||||
}
|
||||
})?;
|
||||
let mut tiles: Vec<(TileKey, SectionMesh)> = Vec::new();
|
||||
capture::read_meshes(&o.dir, "tiles", |[x, z, level], mesh| {
|
||||
let key = TileKey {
|
||||
x,
|
||||
z,
|
||||
level: level as u8,
|
||||
};
|
||||
let [ox, _, oz] = key.origin();
|
||||
let half = f64::from(key.size()) / 2.0;
|
||||
let (dx, dz) = (
|
||||
f64::from(ox) + half - camera[0],
|
||||
f64::from(oz) + half - camera[2],
|
||||
);
|
||||
let near = o
|
||||
.lod_distance
|
||||
.is_none_or(|d| (dx * dx + dz * dz).sqrt() <= f64::from(d) * 16.0);
|
||||
if near {
|
||||
tiles.push((key, mesh));
|
||||
}
|
||||
})?;
|
||||
let tile_count = tiles.len();
|
||||
if !tiles.is_empty() {
|
||||
let cutout: FxHashSet<(i32, i32)> = scene.lod_cutout.iter().copied().collect();
|
||||
renderer.queue_lods(tiles, Vec::new(), Some(cutout));
|
||||
renderer.upload_lods(Duration::from_secs(3600));
|
||||
}
|
||||
let loaded_ms = ms(loading.elapsed());
|
||||
|
||||
let hud = HudBatch::new([width, height]);
|
||||
let mut views = Vec::new();
|
||||
for &offset in &o.yaw_offsets {
|
||||
let mut frame = scene.frame(&hud);
|
||||
frame.yaw += offset;
|
||||
frame.eye_rotation[0] += offset;
|
||||
if let Some(r) = o.render_distance {
|
||||
frame.distance = (r * 16) as f32;
|
||||
frame.detail_distance = frame.distance;
|
||||
}
|
||||
let draw = |renderer: &mut Renderer| -> horizon_client_render::Result<Duration> {
|
||||
if frame.cave_culling {
|
||||
renderer.start_occlusion(frame.camera, frame.distance);
|
||||
}
|
||||
let started = Instant::now();
|
||||
renderer.render(&frame)?;
|
||||
Ok(started.elapsed())
|
||||
};
|
||||
for _ in 0..o.warmup {
|
||||
draw(&mut renderer)?;
|
||||
}
|
||||
renderer.gpu_times.clear();
|
||||
let (mut call, mut encode, mut gpu) = (Vec::new(), Vec::new(), Vec::new());
|
||||
for _ in 0..o.frames {
|
||||
call.push(ms(draw(&mut renderer)?));
|
||||
encode.push(ms(renderer.last_encode));
|
||||
gpu.append(&mut renderer.gpu_times);
|
||||
}
|
||||
// Timestamps arrive a few frames late: a few more of the same frame bring them in.
|
||||
for _ in 0..16 {
|
||||
if gpu.len() >= o.frames {
|
||||
break;
|
||||
}
|
||||
draw(&mut renderer)?;
|
||||
gpu.append(&mut renderer.gpu_times);
|
||||
}
|
||||
gpu.truncate(o.frames);
|
||||
if let Some(path) = o.screenshot.as_ref().filter(|_| views.is_empty()) {
|
||||
renderer.screenshot_result = None;
|
||||
renderer.screenshot = Some(ScreenshotRequest {
|
||||
path: path.clone(),
|
||||
size: None,
|
||||
scaled: false,
|
||||
});
|
||||
for _ in 0..32 {
|
||||
draw(&mut renderer)?;
|
||||
if renderer.screenshot_result.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
match renderer.screenshot_result.take() {
|
||||
Some(Ok(_)) => {}
|
||||
Some(Err(e)) => notes.push(format!("screenshot: {e}")),
|
||||
None => notes.push("screenshot: not saved".into()),
|
||||
}
|
||||
}
|
||||
views.push(json!({
|
||||
"yaw_offset": offset,
|
||||
"gpu_ms": summary(&gpu),
|
||||
"render_call_ms": summary(&call),
|
||||
"cpu_encode_ms": summary(&encode),
|
||||
"gpu_passes_median_ms": renderer
|
||||
.own_pass_times()
|
||||
.into_iter()
|
||||
.map(|(name, ms)| (name, json!(ms)))
|
||||
.collect::<serde_json::Map<_, _>>(),
|
||||
"drawn_sections": renderer.drawn_sections,
|
||||
"section_draws": renderer.section_draws,
|
||||
"section_draw_calls": renderer.section_draw_calls,
|
||||
// Occlusion culling's last result: boxes hidden, boxes tested, share of solid
|
||||
// indices hidden.
|
||||
"culled": renderer.culled_sections(),
|
||||
}));
|
||||
}
|
||||
let section_draws = if renderer.flat_draws() {
|
||||
"flat"
|
||||
} else {
|
||||
"indirect"
|
||||
};
|
||||
let memory = o.memory.then(|| memory_report(&renderer));
|
||||
let report = json!({
|
||||
"capture": o.dir.display().to_string(),
|
||||
"adapter": renderer.info.name,
|
||||
"captured_on": state.adapter,
|
||||
"size": [width, height],
|
||||
"sections": sections,
|
||||
"solid_quads": quads,
|
||||
"leaf_quads": leaf_quads,
|
||||
// Solid quads uploaded as one record each (`mesher::packed`).
|
||||
"packed_quads": packed_quads,
|
||||
"world": world_report,
|
||||
"tiles": tile_count,
|
||||
"load_ms": loaded_ms,
|
||||
"frames": o.frames,
|
||||
"warmup": o.warmup,
|
||||
"settings": {
|
||||
"texture_filtering": filtering,
|
||||
"anisotropy_bit": anisotropy,
|
||||
"mipmaps": o.mipmaps.unwrap_or(settings.mipmap_levels),
|
||||
"lod_distance": o.lod_distance,
|
||||
"render_distance": o.render_distance,
|
||||
"section_draws": section_draws,
|
||||
"section_cull": renderer.section_cull(),
|
||||
"section_sides": renderer.section_sides(),
|
||||
"gpu_culling": renderer.gpu_culling(),
|
||||
"shader_pack_left_out": state.shader_pack,
|
||||
},
|
||||
"notes": notes,
|
||||
"views": views,
|
||||
"memory": memory,
|
||||
});
|
||||
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mb(bytes: u64) -> f64 {
|
||||
(bytes as f64 / 1048576.0 * 10.0).round() / 10.0
|
||||
}
|
||||
|
||||
/// `--memory`: the renderer's census in MiB and, on macOS, `footprint`'s summary of this
|
||||
/// process (GPU buffers and textures count as IOAccelerator memory there).
|
||||
fn memory_report(renderer: &Renderer) -> Value {
|
||||
let gpu: serde_json::Map<String, Value> = renderer
|
||||
.gpu_census()
|
||||
.into_iter()
|
||||
.map(|(name, bytes)| (format!("{name}_mb"), json!(mb(bytes))))
|
||||
.collect();
|
||||
let footprint = std::process::Command::new("/usr/bin/footprint")
|
||||
.args(["-f", "bytes", "-p", &std::process::id().to_string()])
|
||||
.output()
|
||||
.ok()
|
||||
.map(|out| String::from_utf8_lossy(&out.stdout).into_owned());
|
||||
let footprint = footprint.map(|text| {
|
||||
let mut total = None;
|
||||
let mut categories = Vec::new();
|
||||
for line in text.lines() {
|
||||
if let Some(at) = line.find("Footprint: ") {
|
||||
total = line[at + 11..]
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.and_then(|v| v.parse::<u64>().ok());
|
||||
}
|
||||
// " <dirty> B <clean> B <reclaimable> B <regions> <category>"
|
||||
let words: Vec<&str> = line.split_whitespace().collect();
|
||||
if words.len() >= 8
|
||||
&& words[1] == "B"
|
||||
&& let Ok(dirty) = words[0].parse::<u64>()
|
||||
&& dirty >= 1 << 20
|
||||
{
|
||||
categories.push(json!([words[7..].join(" "), mb(dirty)]));
|
||||
}
|
||||
}
|
||||
json!({ "total_mb": total.map(mb), "dirty_by_category_mb": categories })
|
||||
});
|
||||
json!({ "gpu": gpu, "footprint": footprint })
|
||||
}
|
||||
|
||||
struct App;
|
||||
impl ApplicationHandler for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let window = Arc::new(
|
||||
event_loop
|
||||
.create_window(Window::default_attributes().with_visible(false))
|
||||
.unwrap(),
|
||||
);
|
||||
if let Err(error) = replay(window) {
|
||||
eprintln!("scene_replay: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
event_loop.exit();
|
||||
}
|
||||
fn window_event(&mut self, _: &ActiveEventLoop, _: WindowId, _: WindowEvent) {}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
EventLoop::new().unwrap().run_app(&mut App).unwrap();
|
||||
}
|
||||
|
||||
/// The captured sections' keys, from the index alone.
|
||||
fn section_keys(dir: &Path) -> Result<Vec<[i32; 3]>, Box<dyn std::error::Error>> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Key {
|
||||
key: [i32; 3],
|
||||
}
|
||||
let records: Vec<Key> = serde_json::from_reader(std::io::BufReader::new(std::fs::File::open(
|
||||
dir.join("sections.json"),
|
||||
)?))?;
|
||||
Ok(records.into_iter().map(|r| r.key).collect())
|
||||
}
|
||||
|
||||
/// Sprite layers of every leaves block, as terrain vertices carry them (with or
|
||||
/// without `OPAQUE_LAYER`).
|
||||
fn leaf_layers(render: &RenderAssets) -> FxHashSet<u16> {
|
||||
render
|
||||
.sprite_layers
|
||||
.iter()
|
||||
.filter(|(name, _)| name.ends_with("_leaves"))
|
||||
.flat_map(|(_, &layer)| {
|
||||
[
|
||||
layer as u16,
|
||||
layer as u16 | horizon_client_render::mesher::OPAQUE_LAYER,
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `--world`: the sections `keys` meshed again from `save`'s overworld region
|
||||
/// files, on every core, as the game's workers mesh them.
|
||||
fn remesh(
|
||||
save: &Path,
|
||||
root: &Path,
|
||||
assets: &Assets,
|
||||
render: &RenderAssets,
|
||||
leaf_culling: LeafCullingMode,
|
||||
keys: &[[i32; 3]],
|
||||
) -> Result<FxHashMap<[i32; 3], SectionMesh>, Box<dyn std::error::Error>> {
|
||||
let facts = BlockFacts::load(root, assets.registry)?;
|
||||
let visibility = models::load_visibility(root, assets.registry)?;
|
||||
let tables = MeshTables::new(assets, render, &facts, visibility)?;
|
||||
let biomes = Biomes::load(root)?;
|
||||
let seed = world_seed(save).unwrap_or_else(|| {
|
||||
eprintln!(
|
||||
"scene_replay: no seed in {}, biome blending uses 0",
|
||||
save.display()
|
||||
);
|
||||
0
|
||||
});
|
||||
let options = std::fs::read_to_string("options.txt").unwrap_or_default();
|
||||
let option = |key: &str| {
|
||||
options
|
||||
.lines()
|
||||
.find_map(|l| l.strip_prefix(key)?.strip_prefix(':').map(str::to_owned))
|
||||
};
|
||||
let smooth_lighting = option("ao").is_none_or(|v| v == "true");
|
||||
let cutout_leaves = option("cutoutLeaves").is_none_or(|v| v == "true");
|
||||
let blend_radius = option("biomeBlendRadius")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(2);
|
||||
|
||||
let mut columns: Vec<(i32, i32)> = keys
|
||||
.iter()
|
||||
.flat_map(|&[x, _, z]| {
|
||||
(-1..=1).flat_map(move |dz| (-1..=1).map(move |dx| (x + dx, z + dz)))
|
||||
})
|
||||
.collect();
|
||||
columns.sort_unstable();
|
||||
columns.dedup();
|
||||
let threads = std::thread::available_parallelism().map_or(4, |n| n.get());
|
||||
let region_dir = save.join("dimensions/minecraft/overworld/region");
|
||||
let next = AtomicUsize::new(0);
|
||||
let chunks: FxHashMap<(i32, i32), Chunk> = std::thread::scope(|scope| {
|
||||
let workers: Vec<_> = (0..threads)
|
||||
.map(|_| {
|
||||
scope.spawn(|| {
|
||||
let mut files: FxHashMap<(i32, i32), Option<RegionFile>> = FxHashMap::default();
|
||||
let mut out = Vec::new();
|
||||
loop {
|
||||
let i = next.fetch_add(1, Ordering::Relaxed);
|
||||
let Some(&(x, z)) = columns.get(i) else {
|
||||
break;
|
||||
};
|
||||
let file = files.entry((x >> 5, z >> 5)).or_insert_with(|| {
|
||||
RegionFile::open(region_dir.join(format!(
|
||||
"r.{}.{}.mca",
|
||||
x >> 5,
|
||||
z >> 5
|
||||
)))
|
||||
.ok()
|
||||
});
|
||||
let root = file
|
||||
.as_mut()
|
||||
.and_then(|f| {
|
||||
f.read(horizon_core::position::ChunkPos::new(x, z))
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
.and_then(|record| record.nbt(Limits::default()).ok());
|
||||
if let Some(chunk) =
|
||||
root.and_then(|root| decode(&root, assets.registry, &biomes))
|
||||
{
|
||||
out.push(((x, z), chunk));
|
||||
}
|
||||
}
|
||||
out
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
workers
|
||||
.into_iter()
|
||||
.flat_map(|w| w.join().unwrap())
|
||||
.collect()
|
||||
});
|
||||
|
||||
let meshable: Vec<[i32; 3]> = keys
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&[x, _, z]| {
|
||||
(-1..=1).all(|dz| (-1..=1).all(|dx| chunks.contains_key(&(x + dx, z + dz))))
|
||||
})
|
||||
.collect();
|
||||
let next = AtomicUsize::new(0);
|
||||
let meshes = std::thread::scope(|scope| {
|
||||
let workers: Vec<_> = (0..threads)
|
||||
.map(|_| {
|
||||
scope.spawn(|| {
|
||||
let mut mesher = Mesher {
|
||||
nether_shading: false,
|
||||
shader_pack_lighting: false,
|
||||
separate_ao: false,
|
||||
assets,
|
||||
render,
|
||||
tables: &tables,
|
||||
biomes: &biomes,
|
||||
visibility: tables.visibility.clone(),
|
||||
zoom: BiomeZoom::new(obfuscate_seed(seed)),
|
||||
blend_radius,
|
||||
smooth_lighting,
|
||||
cutout_leaves,
|
||||
translucent_layers: &[],
|
||||
leaf_culling,
|
||||
};
|
||||
let mut scratch = MeshScratch::default();
|
||||
let mut out = Vec::new();
|
||||
loop {
|
||||
let i = next.fetch_add(1, Ordering::Relaxed);
|
||||
let Some(&[x, y, z]) = meshable.get(i) else {
|
||||
break;
|
||||
};
|
||||
let region: [[Option<&Chunk>; 3]; 3] = std::array::from_fn(|dz| {
|
||||
std::array::from_fn(|dx| {
|
||||
chunks.get(&(x + dx as i32 - 1, z + dz as i32 - 1))
|
||||
})
|
||||
});
|
||||
let mut mesh =
|
||||
mesher.mesh_reusing(®ion, y, SectionMesh::default(), &mut scratch);
|
||||
mesh.sort_solid();
|
||||
out.push(([x, y, z], mesh));
|
||||
}
|
||||
out
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
workers
|
||||
.into_iter()
|
||||
.flat_map(|w| w.join().unwrap())
|
||||
.collect::<FxHashMap<_, _>>()
|
||||
});
|
||||
Ok(meshes)
|
||||
}
|
||||
|
||||
/// The world seed: `data/minecraft/world_gen_settings.dat`, or level.dat's
|
||||
/// `Data.WorldGenSettings` in older saves.
|
||||
fn world_seed(save: &Path) -> Option<i64> {
|
||||
let read = |path: PathBuf| {
|
||||
horizon_world::datafix::files::read_dat(&path)
|
||||
.ok()
|
||||
.flatten()
|
||||
};
|
||||
let seed = |tag: Option<&Tag>| match tag?.get("seed")? {
|
||||
Tag::Long(seed) => Some(*seed),
|
||||
_ => None,
|
||||
};
|
||||
seed(
|
||||
read(save.join("data/minecraft/world_gen_settings.dat"))
|
||||
.as_ref()
|
||||
.and_then(|t| t.get("data")),
|
||||
)
|
||||
.or_else(|| {
|
||||
seed(
|
||||
read(save.join("level.dat"))?
|
||||
.get("Data")?
|
||||
.get("WorldGenSettings"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// A saved full chunk's blocks, biomes and light, as the client's own save
|
||||
/// loader reads them (`save::decode_lit`, in horizon-client).
|
||||
fn decode(root: &NamedTag, registry: &BlockRegistry, biomes: &Biomes) -> Option<Chunk> {
|
||||
let tag = &root.tag;
|
||||
let text = |t: Option<&Tag>| match t {
|
||||
Some(Tag::String(s)) => Some(s.to_string_lossy().to_string()),
|
||||
_ => None,
|
||||
};
|
||||
if text(tag.get("Status")).is_some_and(|s| s != "minecraft:full" && s != "full") {
|
||||
return None;
|
||||
}
|
||||
if tag.get("isLightOn") != Some(&Tag::Byte(1)) {
|
||||
return None;
|
||||
}
|
||||
let (Some(Tag::Int(x)), Some(Tag::Int(z))) = (tag.get("xPos"), tag.get("zPos")) else {
|
||||
return None;
|
||||
};
|
||||
let mut chunk = Chunk::with_layout(*x, *z, ColumnLayout::VANILLA);
|
||||
let air = registry.default_state("minecraft:air")?.0;
|
||||
let Some(Tag::List(sections)) = tag.get("sections") else {
|
||||
return Some(chunk);
|
||||
};
|
||||
let longs = |t: Option<&Tag>| match t {
|
||||
Some(Tag::LongArray(data)) => Some(data.iter().map(|&w| w as u64).collect::<Vec<_>>()),
|
||||
_ => None,
|
||||
};
|
||||
let mut layers = vec![(None, None); chunk.sections.len()];
|
||||
for section in sections {
|
||||
let Some(Tag::Byte(y)) = section.get("Y") else {
|
||||
continue;
|
||||
};
|
||||
let Some(si) = chunk.section_index(i32::from(*y) * 16) else {
|
||||
continue;
|
||||
};
|
||||
let target = Arc::make_mut(&mut chunk.sections[si]);
|
||||
if let Some(states) = section.get("block_states")
|
||||
&& let Some(Tag::List(palette)) = states.get("palette")
|
||||
{
|
||||
let palette: Vec<u32> = palette
|
||||
.iter()
|
||||
.map(|p| {
|
||||
BlockState::from_nbt(p)
|
||||
.ok()
|
||||
.and_then(|s| {
|
||||
registry.id(&s).or_else(|| {
|
||||
let default = registry.default_state(&s.name)?;
|
||||
let mut full = registry.state(default)?.clone();
|
||||
for (k, v) in &s.properties {
|
||||
if full.properties.contains_key(k) {
|
||||
full.properties.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
registry.id(&full).or(Some(default))
|
||||
})
|
||||
})
|
||||
.map_or(air, |id| id.0)
|
||||
})
|
||||
.collect();
|
||||
let data = longs(states.get("data"));
|
||||
if let Ok(blocks) =
|
||||
PalettedContainer::from_packed(block_strategy(), palette, data.as_deref())
|
||||
{
|
||||
target.blocks = blocks;
|
||||
}
|
||||
}
|
||||
if let Some(b) = section.get("biomes")
|
||||
&& let Some(Tag::List(palette)) = b.get("palette")
|
||||
{
|
||||
let palette: Vec<u32> = palette
|
||||
.iter()
|
||||
.map(|p| match p {
|
||||
Tag::String(s) => u32::from(biomes.id(&s.to_string_lossy())),
|
||||
_ => 0,
|
||||
})
|
||||
.collect();
|
||||
let data = longs(b.get("data"));
|
||||
if let Ok(container) =
|
||||
PalettedContainer::from_packed(BIOME_STRATEGY, palette, data.as_deref())
|
||||
{
|
||||
target.biomes = container;
|
||||
}
|
||||
}
|
||||
let layer = |name: &str| match section.get(name) {
|
||||
Some(Tag::ByteArray(a)) if a.len() == 2048 => Some(a.clone()),
|
||||
_ => None,
|
||||
};
|
||||
layers[si] = (layer("SkyLight"), layer("BlockLight"));
|
||||
}
|
||||
// `save::load_light`: a section without saved sky light takes the light
|
||||
// of the blocks above it, from the top of the column down.
|
||||
let mut above = [15u8; 256];
|
||||
for (si, (sky, block)) in layers.into_iter().enumerate().rev() {
|
||||
let mut data = Box::new([0u8; 4096]);
|
||||
for (i, v) in data.iter_mut().enumerate() {
|
||||
let nibble =
|
||||
|layer: &Option<Vec<u8>>| layer.as_ref().map(|l| l[i >> 1] >> ((i & 1) << 2) & 15);
|
||||
let s = nibble(&sky).unwrap_or(above[i & 255]);
|
||||
let b = nibble(&block).unwrap_or(0);
|
||||
*v = s << 4 | b;
|
||||
}
|
||||
if sky.is_some() {
|
||||
for (i, a) in above.iter_mut().enumerate() {
|
||||
*a = data[i] >> 4;
|
||||
}
|
||||
}
|
||||
let section = Arc::make_mut(&mut chunk.sections[si]);
|
||||
section.light = Light::Full(data);
|
||||
section.compact();
|
||||
}
|
||||
chunk.lit = true;
|
||||
chunk.light_loaded = true;
|
||||
Some(chunk)
|
||||
}
|
||||
@@ -121,7 +121,6 @@ fn fixture(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
None,
|
||||
GraphicsApi::Vulkan,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
))?;
|
||||
renderer.resize(320, 180);
|
||||
|
||||
@@ -207,20 +207,6 @@ impl Assets {
|
||||
pub fn reload(&self, root: &Path) -> Result<RenderAssets> {
|
||||
RenderAssets::load(root, self)
|
||||
}
|
||||
/// [`Assets::reload`] with the model library already parsed.
|
||||
///
|
||||
/// Parsing it reads every model file under `assets/`, which needs no
|
||||
/// block registry, so a caller that has the files to spare can start
|
||||
/// it before `Assets::load` finishes and hand the result in here. On
|
||||
/// a cold cache that is most of a second of reading moved off the
|
||||
/// critical path.
|
||||
pub fn reload_with_models(
|
||||
&self,
|
||||
root: &Path,
|
||||
parsed: crate::model_library::ParsedModels,
|
||||
) -> Result<RenderAssets> {
|
||||
RenderAssets::load_with_parsed(root, self, true, Some(parsed))
|
||||
}
|
||||
/// [`Assets::reload`] for a server, which draws nothing: the sprites'
|
||||
/// layers, without the block or item models or the textures' pixels.
|
||||
pub fn reload_for_server(&self, root: &Path) -> Result<RenderAssets> {
|
||||
@@ -232,16 +218,6 @@ impl RenderAssets {
|
||||
Self::load_with(root, assets, true)
|
||||
}
|
||||
fn load_with(root: &Path, assets: &Assets, draws: bool) -> Result<Self> {
|
||||
Self::load_with_parsed(root, assets, draws, None)
|
||||
}
|
||||
/// `parsed`: the model library when a caller parsed it ahead of this
|
||||
/// (see [`Assets::reload_with_models`]); otherwise it is parsed here.
|
||||
fn load_with_parsed(
|
||||
root: &Path,
|
||||
assets: &Assets,
|
||||
draws: bool,
|
||||
parsed: Option<crate::model_library::ParsedModels>,
|
||||
) -> Result<Self> {
|
||||
let registry = assets.registry;
|
||||
let mut sprites = crate::sprites::Sprites::with_disk_cache(root);
|
||||
for name in MATERIALS {
|
||||
@@ -249,24 +225,17 @@ impl RenderAssets {
|
||||
}
|
||||
// A server draws no block and no item: its block models (about 24 MB
|
||||
// baked) and item model library (7 MB) stay empty.
|
||||
let parsed = match parsed {
|
||||
Some(parsed) => parsed,
|
||||
None if draws => {
|
||||
let _step = crate::spans::startup::step("core.models_parse");
|
||||
crate::model_library::ParsedModels::load(root)
|
||||
}
|
||||
None => crate::model_library::ParsedModels::default(),
|
||||
let parsed = if draws {
|
||||
crate::model_library::ParsedModels::load(root)
|
||||
} else {
|
||||
crate::model_library::ParsedModels::default()
|
||||
};
|
||||
let mut models = if draws {
|
||||
let _step = crate::spans::startup::step("core.models_bake");
|
||||
crate::models::prepare_with_models(root, registry, &mut sprites, &parsed)?
|
||||
} else {
|
||||
crate::models::PreparedModels::default()
|
||||
};
|
||||
let model_library = {
|
||||
let _step = crate::spans::startup::step("core.item_library");
|
||||
parsed.into_item_library()
|
||||
};
|
||||
let model_library = parsed.into_item_library();
|
||||
// Fluids are drawn by the fluid renderer, not block models; breaking
|
||||
// progress uses the destroy-stage overlays.
|
||||
for name in [
|
||||
@@ -282,7 +251,6 @@ impl RenderAssets {
|
||||
sprites.insert(root, &format!("minecraft:block/destroy_stage_{stage}"))?;
|
||||
}
|
||||
let fusion = if draws {
|
||||
let _step = crate::spans::startup::step("core.fusion");
|
||||
crate::fusion::FusionTextures::load(root, registry, &mut sprites, &mut models.catalog)
|
||||
} else {
|
||||
Default::default()
|
||||
|
||||
@@ -379,17 +379,14 @@ pub fn generate(
|
||||
let (_, _, key) = load(palette_key, "textures/palettes")?;
|
||||
let (_, _, target) = load(palette, "textures/palettes")?;
|
||||
let mut mapped = argb(&pixels);
|
||||
let key = argb(&key);
|
||||
let target = argb(&target);
|
||||
apply_palette(&mut mapped, &key, &target)?;
|
||||
let out: Vec<u8> = mapped
|
||||
apply_palette(&mut mapped, &argb(&key), &argb(&target))?;
|
||||
let out = mapped
|
||||
.iter()
|
||||
.flat_map(|p| {
|
||||
let [a, r, g, b] = p.to_be_bytes();
|
||||
[r, g, b, a]
|
||||
})
|
||||
.collect();
|
||||
drop(mapped);
|
||||
Some((w, h, out))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,8 +61,6 @@ struct Mixer {
|
||||
world_names: Vec<String>,
|
||||
/// The callback's cameras (position, basis, world), kept to be refilled without allocating.
|
||||
cameras: Vec<([f64; 3], Basis, u32)>,
|
||||
/// Per-source gain/pan/step cache for one buffer, reused across callbacks.
|
||||
voices: Vec<(ChannelId, [f32; 3], f64)>,
|
||||
limiter_peak: f32,
|
||||
/// The output device the stream was opened on (`Library.currentDeviceName`).
|
||||
#[cfg(feature = "audio")]
|
||||
@@ -546,8 +544,9 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
(forward, up, right)
|
||||
};
|
||||
// Every camera hearing (splitscreen: the other players' too), in the mixer's own buffer.
|
||||
mixer.cameras.clear();
|
||||
mixer.cameras.extend(
|
||||
let mut cameras = std::mem::take(&mut mixer.cameras);
|
||||
cameras.clear();
|
||||
cameras.extend(
|
||||
std::iter::once((
|
||||
0,
|
||||
(mixer.listener, mixer.listener_yaw, mixer.listener_pitch),
|
||||
@@ -566,11 +565,10 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
)
|
||||
}),
|
||||
);
|
||||
let shared = mixer.cameras.len() > 1;
|
||||
let shared = cameras.len() > 1;
|
||||
let dot = |a: [f64; 3], b: [f64; 3]| (a[0] * b[0] + a[1] * b[1] + a[2] * b[2]) as f32;
|
||||
// Gains and pans per source for this buffer.
|
||||
let mut voices = std::mem::take(&mut mixer.voices);
|
||||
voices.clear();
|
||||
let mut voices: Vec<(ChannelId, [f32; 3], f64)> = Vec::new();
|
||||
for (id, c) in &mixer.channels {
|
||||
let Some((_, source_rate)) = &c.samples else {
|
||||
continue;
|
||||
@@ -588,8 +586,7 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
};
|
||||
let length = |d: [f64; 3]| d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
|
||||
// The nearest camera hears it (one camera but in splitscreen).
|
||||
let camera = mixer
|
||||
.cameras
|
||||
let camera = cameras
|
||||
.iter()
|
||||
.filter(|camera| c.relative || camera.2 == c.world)
|
||||
.min_by(|a, b| length(away(a.0)).total_cmp(&length(away(b.0))))
|
||||
@@ -620,7 +617,8 @@ 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));
|
||||
}
|
||||
for (id, [left, right, gain], step) in voices.iter().copied() {
|
||||
mixer.cameras = cameras;
|
||||
for (id, [left, right, gain], step) in voices {
|
||||
let Some(c) = mixer.channels.get_mut(&id) else {
|
||||
continue;
|
||||
};
|
||||
@@ -650,7 +648,6 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
c.position += step;
|
||||
}
|
||||
}
|
||||
mixer.voices = voices;
|
||||
// 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
|
||||
|
||||
@@ -101,22 +101,6 @@ impl AuthSession {
|
||||
let _ = std::fs::remove_file(path);
|
||||
Self::parse(&text.map_err(|e| format!("{}: {e}", path.display()))?)
|
||||
}
|
||||
/// The access token's expiry (its JWT `exp`) has passed, or is a minute away: the session
|
||||
/// service would refuse it. A token that doesn't read as a JWT counts as good: the server
|
||||
/// says.
|
||||
pub fn expired(&self) -> bool {
|
||||
let exp = self
|
||||
.access_token
|
||||
.split('.')
|
||||
.nth(1)
|
||||
.and_then(|p| crate::skins::base64_decode(&p.replace('-', "+").replace('_', "/")))
|
||||
.and_then(|json| serde_json::from_slice::<serde_json::Value>(&json).ok())
|
||||
.and_then(|v| v.get("exp")?.as_u64());
|
||||
let now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_secs());
|
||||
exp.is_some_and(|exp| exp <= now + 60)
|
||||
}
|
||||
}
|
||||
|
||||
static CURRENT: OnceLock<Arc<AuthSession>> = OnceLock::new();
|
||||
@@ -452,8 +436,11 @@ struct ChatKeys {
|
||||
fetching: bool,
|
||||
next_try: Option<std::time::Instant>,
|
||||
}
|
||||
static CHAT_KEYS: std::sync::LazyLock<Mutex<std::collections::HashMap<u128, ChatKeys>>> =
|
||||
std::sync::LazyLock::new(|| Mutex::new(std::collections::HashMap::new()));
|
||||
static CHAT_KEYS: Mutex<ChatKeys> = Mutex::new(ChatKeys {
|
||||
current: None,
|
||||
fetching: false,
|
||||
next_try: None,
|
||||
});
|
||||
|
||||
/// prepareKeyPair (`now`: on a join) or, from a tick, shouldRefreshKeyPair
|
||||
/// then prepareKeyPair: start fetching when there is no key pair or it is
|
||||
@@ -462,14 +449,7 @@ pub fn prepare_chat_keys(now: bool) {
|
||||
let Some(session) = current() else {
|
||||
return;
|
||||
};
|
||||
prepare_chat_keys_for(session, now);
|
||||
}
|
||||
|
||||
/// Prepare keys for this player, independently of the other local accounts.
|
||||
pub fn prepare_chat_keys_for(session: Arc<AuthSession>, now: bool) {
|
||||
let uuid = session.uuid;
|
||||
let mut accounts = CHAT_KEYS.lock().unwrap();
|
||||
let keys = accounts.entry(uuid).or_default();
|
||||
let mut keys = CHAT_KEYS.lock().unwrap();
|
||||
let instant = std::time::Instant::now();
|
||||
if keys.fetching || !now && keys.next_try.is_some_and(|t| instant < t) {
|
||||
return;
|
||||
@@ -483,13 +463,12 @@ pub fn prepare_chat_keys_for(session: Arc<AuthSession>, now: bool) {
|
||||
return;
|
||||
}
|
||||
keys.fetching = true;
|
||||
drop(accounts);
|
||||
drop(keys);
|
||||
let spawned = std::thread::Builder::new()
|
||||
.name("Profile key fetch".into())
|
||||
.spawn(move || {
|
||||
let result = fetch_chat_keys(&session);
|
||||
let mut accounts = CHAT_KEYS.lock().unwrap();
|
||||
let keys = accounts.entry(uuid).or_default();
|
||||
let mut keys = CHAT_KEYS.lock().unwrap();
|
||||
keys.fetching = false;
|
||||
match result {
|
||||
Ok(pair) => keys.current = Some(Arc::new(pair)),
|
||||
@@ -497,24 +476,19 @@ pub fn prepare_chat_keys_for(session: Arc<AuthSession>, now: bool) {
|
||||
}
|
||||
});
|
||||
if spawned.is_err() {
|
||||
CHAT_KEYS.lock().unwrap().entry(uuid).or_default().fetching = false;
|
||||
CHAT_KEYS.lock().unwrap().fetching = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// The chat key pair fetched so far, unless it has expired.
|
||||
pub fn chat_keys() -> Option<Arc<ChatKeyPair>> {
|
||||
chat_keys_for(current()?.uuid)
|
||||
}
|
||||
|
||||
/// This account's key pair; another local player's keys can never be returned.
|
||||
pub fn chat_keys_for(uuid: u128) -> Option<Arc<ChatKeyPair>> {
|
||||
let now = now_millis();
|
||||
CHAT_KEYS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&uuid)?
|
||||
.current
|
||||
.clone()
|
||||
.filter(|k| !k.public.has_expired(now_millis()))
|
||||
.filter(|k| !k.public.has_expired(now))
|
||||
}
|
||||
|
||||
const MINECRAFT_PROFILE: &str = "https://api.minecraftservices.com/minecraft/profile";
|
||||
@@ -658,42 +632,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// base64url without padding, as a JWT's parts are.
|
||||
fn base64url(bytes: &[u8]) -> String {
|
||||
const ALPHABET: &[u8; 64] =
|
||||
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
let mut out = String::new();
|
||||
for chunk in bytes.chunks(3) {
|
||||
let n =
|
||||
chunk.iter().fold(0u32, |n, &b| n << 8 | u32::from(b)) << (8 * (3 - chunk.len()));
|
||||
for k in 0..=chunk.len() {
|
||||
out.push(char::from(ALPHABET[(n >> (18 - 6 * k) & 63) as usize]));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_token_past_its_exp_is_expired() {
|
||||
// header.payload.signature, the payload base64url without padding.
|
||||
let token = |exp: u64| {
|
||||
let json = format!(r#"{{"exp":{exp}}}"#);
|
||||
let payload = base64url(json.as_bytes());
|
||||
AuthSession::new("Ann", UUID, &format!("e30.{payload}.sig"))
|
||||
};
|
||||
let now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
assert!(token(now - 10).expired());
|
||||
assert!(
|
||||
token(now + 30).expired(),
|
||||
"a minute from its end: as good as gone"
|
||||
);
|
||||
assert!(!token(now + 3600).expired());
|
||||
assert!(!AuthSession::new("Ann", UUID, "not-a-jwt").expired());
|
||||
}
|
||||
|
||||
fn write(dir: &Path, text: &str, mode: u32) -> std::path::PathBuf {
|
||||
let path = dir.join("session.json");
|
||||
std::fs::write(&path, text).unwrap();
|
||||
@@ -857,37 +795,6 @@ mod tests {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_accounts_use_their_own_chat_keys() {
|
||||
let pair = |signature: &[u8]| {
|
||||
let mut keys = parse_certificates(&certificates(PRIVATE_B64, PUBLIC_B64)).unwrap();
|
||||
keys.public.expires_at = i64::MAX;
|
||||
keys.refreshed_after = i64::MAX;
|
||||
keys.public.signature = signature.to_vec();
|
||||
Arc::new(keys)
|
||||
};
|
||||
let ann = pair(b"ann");
|
||||
let bea = pair(b"bea");
|
||||
{
|
||||
let mut accounts = CHAT_KEYS.lock().unwrap();
|
||||
for (uuid, keys) in [(101, ann.clone()), (102, bea.clone())] {
|
||||
accounts.insert(
|
||||
uuid,
|
||||
ChatKeys {
|
||||
current: Some(keys),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
prepare_chat_keys_for(Arc::new(AuthSession::new("Ann", 101, "unused")), true);
|
||||
assert!(Arc::ptr_eq(&chat_keys_for(101).unwrap(), &ann));
|
||||
assert!(Arc::ptr_eq(&chat_keys_for(102).unwrap(), &bea));
|
||||
assert!(chat_keys_for(103).is_none());
|
||||
CHAT_KEYS.lock().unwrap().remove(&101);
|
||||
CHAT_KEYS.lock().unwrap().remove(&102);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn certificates_response() {
|
||||
let pair = parse_certificates(&certificates(PRIVATE_B64, PUBLIC_B64)).unwrap();
|
||||
|
||||
@@ -50,9 +50,8 @@ struct Out {
|
||||
@builtin(position) clip: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
// The distances fog reads: .x spherical, .y cylindrical. One vec2 rather than two floats, so that
|
||||
// the fragment stage stays within 14 user-defined inputs plus front_facing (GL's limit of 15, #8195).
|
||||
@location(2) fog_distance: vec2<f32>,
|
||||
@location(2) spherical: f32,
|
||||
@location(3) cylindrical: f32,
|
||||
// entity.vsh's overlayColor: the OverlayTexture texel (see overlay_texel).
|
||||
@location(5) overlay_color: vec4<f32>,
|
||||
@location(4) @interpolate(flat) overlay: u32,
|
||||
@@ -158,11 +157,11 @@ fn entity_vertex(position: vec3<f32>, uv: vec2<f32>,
|
||||
out.back_color = back;
|
||||
out.overlay_color = overlay_texel(overlay);
|
||||
// Sky geometry (clouds, celestial bodies) is exempt from render-distance fog.
|
||||
out.fog_distance.x = select(length(position), 0.0, overlay >= 2u && overlay <= 4u);
|
||||
out.fog_distance.y = select(max(length(position.xz), abs(position.y)), 0.0, overlay >= 2u && overlay <= 4u);
|
||||
out.spherical = select(length(position), 0.0, overlay >= 2u && overlay <= 4u);
|
||||
out.cylindrical = select(max(length(position.xz), abs(position.y)), 0.0, overlay >= 2u && overlay <= 4u);
|
||||
out.overlay = overlay;
|
||||
out.glint_uv = vec2(0.0);
|
||||
if overlay == 4u { out.fog_distance.x = length(position); }
|
||||
if overlay == 4u { out.spherical = length(position); }
|
||||
return out;
|
||||
}
|
||||
@vertex fn vs_entity(@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>,
|
||||
@@ -294,7 +293,7 @@ fn entity_color(in: Out, front: bool) -> vec4<f32> {
|
||||
color = vec4(c, 1.0);
|
||||
} else if in.overlay == 4u {
|
||||
// clouds.fsh: alpha fades out towards the cloud fog end.
|
||||
return vec4(color.rgb, color.a * (1.0 - linear_fog(in.fog_distance.x, 0.0, globals.fog_ends.y)));
|
||||
return vec4(color.rgb, color.a * (1.0 - linear_fog(in.spherical, 0.0, globals.fog_ends.y)));
|
||||
}
|
||||
if color.a < 0.1 || (in.cull != 0u && !front) { discard; }
|
||||
// entity.fsh calculateFinalColor: the overlay (hurt red, TNT white) over the
|
||||
@@ -313,8 +312,8 @@ fn entity_color(in: Out, front: bool) -> vec4<f32> {
|
||||
textureSample(glint_armor_tex, glint_sampler, armor_glint_uv(in.glint_uv)),
|
||||
);
|
||||
}
|
||||
let f = 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));
|
||||
let f = max(linear_fog(in.spherical, globals.fog.x, globals.fog.y),
|
||||
linear_fog(in.cylindrical, globals.fog.z, globals.fog.w));
|
||||
return vec4(mix(color.rgb, globals.fog_color.rgb, f * globals.fog_color.a), color.a);
|
||||
}
|
||||
|
||||
@@ -328,8 +327,8 @@ fn entity_color(in: Out, front: bool) -> vec4<f32> {
|
||||
fn shadow_color(in: Out) -> vec4<f32> {
|
||||
var color = textureSample(atlas, atlas_sampler, in.uv) * in.color;
|
||||
color *= in.light_color;
|
||||
let f = 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));
|
||||
let f = max(linear_fog(in.spherical, globals.fog.x, globals.fog.y),
|
||||
linear_fog(in.cylindrical, globals.fog.z, globals.fog.w));
|
||||
return vec4(mix(color.rgb, globals.fog_color.rgb, f * globals.fog_color.a), color.a);
|
||||
}
|
||||
|
||||
@@ -348,8 +347,8 @@ fn shadow_color(in: Out) -> vec4<f32> {
|
||||
let texel = textureSample(atlas, atlas_sampler, uv);
|
||||
if texel.a < 0.1 { discard; }
|
||||
// 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;
|
||||
let fogged = max(linear_fog(in.spherical, globals.fog.x, globals.fog.y),
|
||||
linear_fog(in.cylindrical, globals.fog.z, globals.fog.w)) * globals.fog_color.a;
|
||||
if in.tier == 0u {
|
||||
return vec4(0.0, 0.0, 0.0, hdr_scene_alpha(mix(in.scene, globals.hdr_light.y, fogged)));
|
||||
}
|
||||
@@ -380,7 +379,7 @@ fn hdr_mark(tier: u32, y: f32) -> f32 {
|
||||
// HDR lighting on clouds, drawn again after them with only alpha written: they're lit by the
|
||||
// sky, so they take its value, which their blending would otherwise cover.
|
||||
@fragment fn fs_hdr_cloud_mark(in: Out) -> @location(0) vec4<f32> {
|
||||
let a = in.color.a * (1.0 - linear_fog(in.fog_distance.x, 0.0, globals.fog_ends.y));
|
||||
let a = in.color.a * (1.0 - linear_fog(in.spherical, 0.0, globals.fog_ends.y));
|
||||
if a < 0.1 { discard; }
|
||||
return vec4(0.0, 0.0, 0.0, hdr_scene_alpha(globals.hdr_light.y));
|
||||
}
|
||||
@@ -389,8 +388,8 @@ fn celestial_color(in: Out) -> vec4<f32> {
|
||||
// RenderPipelines.DRAGON_RAYS / rendertype_lightning.fsh: untextured
|
||||
// vertex colour, with fog fading both RGB and alpha before additive blending.
|
||||
if in.overlay == 37u {
|
||||
let fog = 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));
|
||||
let fog = max(linear_fog(in.spherical, globals.fog.x, globals.fog.y),
|
||||
linear_fog(in.cylindrical, globals.fog.z, globals.fog.w));
|
||||
return in.color * (1.0 - fog);
|
||||
}
|
||||
return textureSample(atlas, atlas_sampler, in.uv) * in.color * in.light_color;
|
||||
@@ -429,8 +428,8 @@ fn energy_swirl_base(in: Out) -> vec4<f32> {
|
||||
return vec4(color.rgb * (128.0 / 255.0), color.a);
|
||||
}
|
||||
fn energy_swirl_fog(in: Out) -> f32 {
|
||||
let f = 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));
|
||||
let f = max(linear_fog(in.spherical, globals.fog.x, globals.fog.y),
|
||||
linear_fog(in.cylindrical, globals.fog.z, globals.fog.w));
|
||||
return f * globals.fog_color.a;
|
||||
}
|
||||
@fragment fn fs_energy_swirl(in: Out) -> @location(0) vec4<f32> {
|
||||
@@ -573,7 +572,7 @@ fn weather_color(in: Out) -> vec4<f32> {
|
||||
var color = textureLoad(atlas, texel, 0) * in.color;
|
||||
if color.a < 0.1 { discard; }
|
||||
color *= in.light_color;
|
||||
let f = 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));
|
||||
let f = max(linear_fog(in.spherical, globals.fog.x, globals.fog.y),
|
||||
linear_fog(in.cylindrical, globals.fog.z, globals.fog.w));
|
||||
return vec4(mix(color.rgb, globals.fog_color.rgb, f * globals.fog_color.a), color.a);
|
||||
}
|
||||
@@ -120,7 +120,7 @@ pub struct Environment {
|
||||
tracks: HashMap<String, Track>,
|
||||
base: HashMap<String, [f32; 4]>,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Sky {
|
||||
pub sky_color: [f32; 3],
|
||||
pub fog_color: [f32; 3],
|
||||
|
||||
@@ -8,8 +8,6 @@ use std::{collections::VecDeque, fs, path::Path};
|
||||
pub const FILE: &str = "horizon-extras.txt";
|
||||
/// The name the options screen uses while the sort key waits for a key.
|
||||
pub const SORT_KEY: &str = "horizon.sort";
|
||||
/// The 3D skin layers line, here and in the `options.txt` that held it first.
|
||||
const SKIN_LAYERS_KEY: &str = "threeDSkinLayers";
|
||||
|
||||
/// The minimap's sizes in GUI pixels, and its zooms in blocks per pixel.
|
||||
pub const MINIMAP_SIZES: [(u8, &str); 3] = [(64, "Small"), (96, "Medium"), (128, "Large")];
|
||||
@@ -159,10 +157,6 @@ pub struct Extras {
|
||||
pub cosmetics_mine: bool,
|
||||
pub cosmetics_others: horizon_cosmetics::prefs::Others,
|
||||
pub cosmetics_auras: bool,
|
||||
/// 3D skin layers (`threeDSkinLayers`, Options → Skin Customization): each
|
||||
/// painted texel of a player's outer layer drawn as a slab
|
||||
/// (`horizon_client::mobs::skin_layers`).
|
||||
pub three_d_skin_layers: bool,
|
||||
}
|
||||
impl Default for Extras {
|
||||
fn default() -> Self {
|
||||
@@ -212,7 +206,6 @@ impl Default for Extras {
|
||||
cosmetics_mine: true,
|
||||
cosmetics_others: horizon_cosmetics::prefs::Others::Shown,
|
||||
cosmetics_auras: false,
|
||||
three_d_skin_layers: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,7 +307,6 @@ impl Extras {
|
||||
horizon_cosmetics::prefs::Others::parse(value).unwrap_or(e.cosmetics_others)
|
||||
}
|
||||
"cosmeticsAuras" => e.cosmetics_auras = flag(e.cosmetics_auras),
|
||||
SKIN_LAYERS_KEY => e.three_d_skin_layers = flag(e.three_d_skin_layers),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -322,7 +314,7 @@ impl Extras {
|
||||
}
|
||||
pub fn serialize(&self) -> String {
|
||||
format!(
|
||||
"packBrowser:{}\nfullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nuiSoundReserve:{}\nownNameTag:{}\nhideBlockParticles:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\nchatAnimation:{}\nchatIndicator:{}\n{}:{}\nagentControl:{}\nreproKeys:{}\ndiscordPresence:{}\nsplitscreen:{}\nsplitscreenTwo:{}\nsplitscreenThree:{}\nsplitscreenNames:{}\nwheelCyclesSettings:{}\nmusicOff:{}\nmusicPage:{}\ncosmeticsMine:{}\ncosmeticsOthers:{}\ncosmeticsAuras:{}\n{}:{}\n",
|
||||
"packBrowser:{}\nfullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nuiSoundReserve:{}\nownNameTag:{}\nhideBlockParticles:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\nchatAnimation:{}\nchatIndicator:{}\n{}:{}\nagentControl:{}\nreproKeys:{}\ndiscordPresence:{}\nsplitscreen:{}\nsplitscreenTwo:{}\nsplitscreenThree:{}\nsplitscreenNames:{}\nwheelCyclesSettings:{}\nmusicOff:{}\nmusicPage:{}\ncosmeticsMine:{}\ncosmeticsOthers:{}\ncosmeticsAuras:{}\n",
|
||||
bool_text(self.pack_browser),
|
||||
bool_text(self.fullbright),
|
||||
bool_text(self.dynamic_lights),
|
||||
@@ -373,28 +365,8 @@ impl Extras {
|
||||
bool_text(self.cosmetics_mine),
|
||||
self.cosmetics_others.name(),
|
||||
bool_text(self.cosmetics_auras),
|
||||
SKIN_LAYERS_KEY,
|
||||
bool_text(self.three_d_skin_layers),
|
||||
)
|
||||
}
|
||||
/// Adopts the settings `options.txt` held before they moved here, while
|
||||
/// this file (`own`) has no line of its own: 3D skin layers was an
|
||||
/// `options.txt` line. Vanilla's `Options.save` drops lines it doesn't
|
||||
/// know, so it is read there once, the first start after the update.
|
||||
pub fn read_old_options(&mut self, own: &str, options_txt: &str) {
|
||||
fn line(text: &str) -> Option<&str> {
|
||||
text.lines().find_map(|l| {
|
||||
l.split_once(':')
|
||||
.filter(|(key, _)| *key == SKIN_LAYERS_KEY)
|
||||
.map(|(_, value)| value)
|
||||
})
|
||||
}
|
||||
if line(own).is_none()
|
||||
&& let Some(value) = line(options_txt)
|
||||
{
|
||||
self.three_d_skin_layers = value == "true";
|
||||
}
|
||||
}
|
||||
/// Binds the extra key `name` (`SORT_KEY`); false for
|
||||
/// another name (a vanilla mapping).
|
||||
pub fn set_key(&mut self, name: &str, value: String) -> bool {
|
||||
|
||||
@@ -21,7 +21,7 @@ pub enum FogType {
|
||||
}
|
||||
|
||||
/// The fog uniform (`FogData`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct FogData {
|
||||
pub color: [f32; 4],
|
||||
pub environmental_start: f32,
|
||||
|
||||
@@ -104,11 +104,8 @@ struct FontSet {
|
||||
providers: Vec<Provider>,
|
||||
/// Every char drawn looks up here, so the hasher is Fx, not SipHash.
|
||||
glyphs: crate::fast_hash::FxHashMap<char, Glyph>,
|
||||
/// `SelectedGlyphs.nonFishy` of the characters whose first glyph has a fishy advance (the
|
||||
/// others use `glyphs`): what obfuscated text draws.
|
||||
non_fishy: crate::fast_hash::FxHashMap<char, Glyph>,
|
||||
/// `glyphsByWidth` for obfuscated text (built on first use), by `ceil(advance)`.
|
||||
by_width: Option<HashMap<i32, Vec<char>>>,
|
||||
/// `glyphsByWidth` for obfuscated text (built on first use).
|
||||
by_width: Option<HashMap<u32, Vec<char>>>,
|
||||
}
|
||||
/// The atlas region glyphs are rasterized into, filled in rows.
|
||||
struct Region {
|
||||
@@ -261,11 +258,10 @@ fn used_width(pixels: &[u8], stride: u32, (cx, cy, cw, ch): (u32, u32, u32, u32)
|
||||
})
|
||||
.map_or(0, |x| x + 1)
|
||||
}
|
||||
/// `FontSet.hasFishyAdvance`: an advance under 0 or over 32. Such a glyph is still the one drawn
|
||||
/// (`SelectedGlyphs.any` is the first provider's); only `SelectedGlyphs.nonFishy`, what obfuscated
|
||||
/// text draws, moves on to the next provider that has the character. Packs rely on it: a `space`
|
||||
/// provider's kerning runs from -512 to 512, and a tall bitmap's glyphs pass 32. Horizon keeps one
|
||||
/// advance per glyph and tests it here.
|
||||
/// `FontSet.hasFishyAdvance`: a provider glyph whose advance is under 0 or
|
||||
/// over 32 is not used. `FontSet.computeGlyphInfo` moves on to the next
|
||||
/// provider that has the character, so a pack's absurd advance cannot widen
|
||||
/// a line; Horizon keeps one advance per glyph and tests it here.
|
||||
fn fishy_advance(advance: f32) -> bool {
|
||||
!(0.0..=32.0).contains(&advance)
|
||||
}
|
||||
@@ -678,7 +674,6 @@ impl Font {
|
||||
FontSet {
|
||||
providers,
|
||||
glyphs: Default::default(),
|
||||
non_fishy: Default::default(),
|
||||
by_width: None,
|
||||
}
|
||||
}
|
||||
@@ -687,7 +682,6 @@ impl Font {
|
||||
let mut cache = self.cache.borrow_mut();
|
||||
for (_, set) in &mut cache.sets {
|
||||
set.glyphs.clear();
|
||||
set.non_fishy.clear();
|
||||
set.by_width = None;
|
||||
}
|
||||
cache.region.cursor = (0, 0);
|
||||
@@ -741,7 +735,7 @@ impl Font {
|
||||
return *g;
|
||||
}
|
||||
let was_full = std::mem::replace(&mut region.full, false);
|
||||
let found = self.resolve(set, region, c, false);
|
||||
let found = self.resolve(set, region, c);
|
||||
// No room: the missing glyph for now, not remembered, so the
|
||||
// character is tried again once `begin_frame` cleared the region.
|
||||
let overflowed = std::mem::replace(&mut region.full, was_full);
|
||||
@@ -753,46 +747,13 @@ impl Font {
|
||||
set.glyphs.insert(c, glyph);
|
||||
glyph
|
||||
}
|
||||
/// `FontSet.getRandomGlyph`'s `SelectedGlyphs.nonFishy`: the glyph of the first provider whose
|
||||
/// advance is not fishy, else the missing glyph. A character whose first glyph is fine has
|
||||
/// the same one for both.
|
||||
fn glyph_non_fishy(&self, id: &str, c: char) -> Glyph {
|
||||
let any = self.glyph_of(id, c);
|
||||
if !fishy_advance(any.advance) {
|
||||
return any;
|
||||
}
|
||||
let mut cache = self.cache.borrow_mut();
|
||||
let Cache { sets, region, .. } = &mut *cache;
|
||||
let Some((_, set)) = sets.iter_mut().find(|(k, _)| k == id) else {
|
||||
return self.missing;
|
||||
};
|
||||
if let Some(g) = set.non_fishy.get(&c) {
|
||||
return *g;
|
||||
}
|
||||
let was_full = std::mem::replace(&mut region.full, false);
|
||||
let found = self.resolve(set, region, c, true);
|
||||
let overflowed = std::mem::replace(&mut region.full, was_full);
|
||||
region.full |= overflowed;
|
||||
if overflowed {
|
||||
return self.missing;
|
||||
}
|
||||
let glyph = found.unwrap_or(self.missing);
|
||||
set.non_fishy.insert(c, glyph);
|
||||
glyph
|
||||
}
|
||||
fn resolve(
|
||||
&self,
|
||||
set: &mut FontSet,
|
||||
region: &mut Region,
|
||||
c: char,
|
||||
non_fishy: bool,
|
||||
) -> Option<Glyph> {
|
||||
fn resolve(&self, set: &mut FontSet, region: &mut Region, c: char) -> Option<Glyph> {
|
||||
let (uniform, japanese) = (self.uniform, self.japanese);
|
||||
for provider in &mut set.providers {
|
||||
match provider {
|
||||
Provider::Space(advances) => {
|
||||
if let Some(a) = advances.get(&c)
|
||||
&& !(non_fishy && fishy_advance(*a))
|
||||
&& !fishy_advance(*a)
|
||||
{
|
||||
return Some(Glyph {
|
||||
rect: None,
|
||||
@@ -807,12 +768,10 @@ impl Font {
|
||||
}
|
||||
Provider::Bitmap(filter, glyphs) if filter.passes(uniform, japanese) => {
|
||||
match glyphs.get(&c) {
|
||||
Some(BitmapGlyph::Atlas(g)) if !(non_fishy && fishy_advance(g.advance)) => {
|
||||
Some(BitmapGlyph::Atlas(g)) if !fishy_advance(g.advance) => {
|
||||
return Some(*g);
|
||||
}
|
||||
Some(BitmapGlyph::Sheet(cell))
|
||||
if !(non_fishy && fishy_advance(cell.glyph.advance)) =>
|
||||
{
|
||||
Some(BitmapGlyph::Sheet(cell)) if !fishy_advance(cell.glyph.advance) => {
|
||||
let (stride, pixels) = (&cell.sheet.0, &cell.sheet.1);
|
||||
let mut rgba = Vec::with_capacity((cell.w * cell.h * 4) as usize);
|
||||
for y in cell.y..cell.y + cell.h {
|
||||
@@ -832,14 +791,14 @@ impl Font {
|
||||
Provider::Unihex(filter, def, glyphs) if filter.passes(uniform, japanese) => {
|
||||
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)))
|
||||
&& !fishy_advance(hex_advance(&h))
|
||||
{
|
||||
return self.rasterize_hex(region, h);
|
||||
}
|
||||
}
|
||||
Provider::TrueType(filter, ttf) if filter.passes(uniform, japanese) => {
|
||||
if let Some(id) = ttf.ids.get(&c)
|
||||
&& !(non_fishy && fishy_advance(ttf.advance(*id)))
|
||||
&& !fishy_advance(ttf.advance(*id))
|
||||
{
|
||||
let (glyph, bitmap) = ttf.glyph(*id);
|
||||
return match bitmap {
|
||||
@@ -892,7 +851,7 @@ impl Font {
|
||||
pub fn random_glyph(&self, style: &Style, advance: f32) -> Glyph {
|
||||
use horizon_core::random::RandomSource;
|
||||
let id = style.font.as_deref().unwrap_or(DEFAULT);
|
||||
let width = advance.ceil() as i32;
|
||||
let width = advance.ceil() as u32;
|
||||
let pick = {
|
||||
let mut cache = self.cache.borrow_mut();
|
||||
let Cache { sets, random, .. } = &mut *cache;
|
||||
@@ -910,9 +869,9 @@ impl Font {
|
||||
chars.get(i as usize).copied()
|
||||
})
|
||||
};
|
||||
pick.map_or(self.missing, |c| self.glyph_non_fishy(id, c))
|
||||
pick.map_or(self.missing, |c| self.glyph_of(id, c))
|
||||
}
|
||||
fn glyphs_by_width(&self, set: &mut FontSet) -> HashMap<i32, Vec<char>> {
|
||||
fn glyphs_by_width(&self, set: &mut FontSet) -> HashMap<u32, Vec<char>> {
|
||||
let (uniform, japanese) = (self.uniform, self.japanese);
|
||||
let mut chars = std::collections::BTreeSet::new();
|
||||
let mut in_atlas = false;
|
||||
@@ -934,31 +893,33 @@ impl Font {
|
||||
// Nothing in the atlas: the glyphs below U+0250, rasterized as used.
|
||||
chars.extend((0..0x250u32).filter_map(char::from_u32));
|
||||
}
|
||||
let mut out: HashMap<i32, Vec<char>> = HashMap::new();
|
||||
let mut out: HashMap<u32, Vec<char>> = HashMap::new();
|
||||
for c in chars {
|
||||
if let Some(advance) = self.provider_advance(set, c) {
|
||||
out.entry(advance.ceil() as i32).or_default().push(c);
|
||||
out.entry(advance.ceil() as u32).or_default().push(c);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
/// A character's advance in `set` (its first provider that has it, fishy or not, as the
|
||||
/// by-width table of `FontSet.reload` takes it), without rasterizing it.
|
||||
/// A character's advance in `set` (its first provider that has it),
|
||||
/// without rasterizing it.
|
||||
fn provider_advance(&self, set: &mut FontSet, c: char) -> Option<f32> {
|
||||
let (uniform, japanese) = (self.uniform, self.japanese);
|
||||
for provider in &mut set.providers {
|
||||
match provider {
|
||||
Provider::Space(advances) => {
|
||||
if let Some(a) = advances.get(&c) {
|
||||
if let Some(a) = advances.get(&c)
|
||||
&& !fishy_advance(*a)
|
||||
{
|
||||
return Some(*a);
|
||||
}
|
||||
}
|
||||
Provider::Bitmap(filter, glyphs) if filter.passes(uniform, japanese) => {
|
||||
match glyphs.get(&c) {
|
||||
Some(BitmapGlyph::Atlas(g)) => {
|
||||
Some(BitmapGlyph::Atlas(g)) if !fishy_advance(g.advance) => {
|
||||
return Some(g.advance);
|
||||
}
|
||||
Some(BitmapGlyph::Sheet(cell)) => {
|
||||
Some(BitmapGlyph::Sheet(cell)) if !fishy_advance(cell.glyph.advance) => {
|
||||
return Some(cell.glyph.advance);
|
||||
}
|
||||
_ => {}
|
||||
@@ -966,12 +927,16 @@ impl Font {
|
||||
}
|
||||
Provider::Unihex(filter, def, glyphs) if filter.passes(uniform, japanese) => {
|
||||
let glyphs = glyphs.get_or_insert_with(|| load_hex(&self.root, def));
|
||||
if let Some(h) = glyphs.get(&u32::from(c)) {
|
||||
if let Some(h) = glyphs.get(&u32::from(c))
|
||||
&& !fishy_advance(hex_advance(h))
|
||||
{
|
||||
return Some(hex_advance(h));
|
||||
}
|
||||
}
|
||||
Provider::TrueType(filter, ttf) if filter.passes(uniform, japanese) => {
|
||||
if let Some(id) = ttf.ids.get(&c) {
|
||||
if let Some(id) = ttf.ids.get(&c)
|
||||
&& !fishy_advance(ttf.advance(*id))
|
||||
{
|
||||
return Some(ttf.advance(*id));
|
||||
}
|
||||
}
|
||||
@@ -1441,13 +1406,12 @@ mod tests {
|
||||
assert!(!font.take_patches().is_empty());
|
||||
}
|
||||
|
||||
/// `FontSet.computeGlyphInfo`: `SelectedGlyphs.any` is the first provider's glyph, fishy
|
||||
/// advance or not, and is what is drawn and measured; `nonFishy`, what obfuscated text draws,
|
||||
/// is the first provider whose advance is within 0..32, else the missing glyph.
|
||||
/// `FontSet.computeGlyphInfo`: a provider glyph with an advance outside
|
||||
/// 0..32 is skipped, so the next provider that has the character is used.
|
||||
#[test]
|
||||
fn a_fishy_glyph_is_drawn_and_only_obfuscated_text_falls_through() {
|
||||
// 64 opaque columns: the first provider's 'a' advances 65, past the 32 vanilla
|
||||
// calls fishy, and the narrow provider's 'a' advances 3.
|
||||
fn a_fishy_advance_falls_through_to_the_next_provider() {
|
||||
// 64 opaque columns: the first provider's 'a' would advance 65, past
|
||||
// the 32 vanilla allows, so the narrow provider's 'a' is the one used.
|
||||
let wide = png(64, 8, |_, _| true);
|
||||
let narrow = png(16, 8, |x, _| x < 2);
|
||||
let def = br#"{"providers":[{"type":"bitmap","file":"minecraft:font/wide.png","ascent":7,"chars":["a"]},{"type":"bitmap","file":"minecraft:font/narrow.png","ascent":7,"chars":["a"]}]}"#;
|
||||
@@ -1461,80 +1425,11 @@ mod tests {
|
||||
);
|
||||
let font = load(&root);
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
let drawn = font.glyph('a');
|
||||
assert_eq!(drawn.advance, 65.0, "the first provider's glyph is drawn");
|
||||
assert!(drawn.rect.is_some());
|
||||
assert_eq!(font.advance('a', &Style::default()), 65.0);
|
||||
// The by-width table files 'a' under its first glyph's 65; the glyph it then draws is
|
||||
// the narrow provider's (`nonFishy`).
|
||||
assert_eq!(font.random_glyph(&Style::default(), 65.0).advance, 3.0);
|
||||
// Nothing else is 3 wide in the table, and the missing glyph is 6.
|
||||
assert_eq!(font.random_glyph(&Style::default(), 3.0).advance, 6.0);
|
||||
}
|
||||
|
||||
/// A `space` provider's shift glyphs (kerning in server packs: -512 to 512) are drawn and
|
||||
/// measured as they are, and `nonFishy` has nothing else to draw for them.
|
||||
#[test]
|
||||
fn space_glyphs_with_any_advance_are_kept() {
|
||||
let shifts: Vec<i32> = (0..10)
|
||||
.map(|i| -(1 << i))
|
||||
.chain((0..10).map(|i| 1 << i))
|
||||
.collect();
|
||||
let advances: Vec<String> = shifts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, a)| format!("\"\\ue{:03x}\": {a}", i))
|
||||
.collect();
|
||||
let def = format!(
|
||||
r#"{{"providers":[{{"type":"space","advances":{{{}}}}}]}}"#,
|
||||
advances.join(",")
|
||||
);
|
||||
let root = root(
|
||||
"space-shifts",
|
||||
&[("assets/minecraft/font/default.json", def.as_bytes())],
|
||||
);
|
||||
let font = load(&root);
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
for (i, a) in shifts.iter().enumerate() {
|
||||
let c = char::from_u32(0xe000 + i as u32).unwrap();
|
||||
assert_eq!(font.glyph(c).advance, *a as f32, "{c:?}");
|
||||
assert_eq!(font.advance(c, &Style::default()), *a as f32);
|
||||
}
|
||||
// Obfuscated text asks for a glyph of a given width: a fishy space has none to give.
|
||||
let c = char::from_u32(0xe000 + 19).unwrap();
|
||||
assert_eq!(font.glyph(c).advance, 512.0);
|
||||
assert_eq!(
|
||||
font.glyph_non_fishy(DEFAULT, c).advance,
|
||||
6.0,
|
||||
"the missing glyph"
|
||||
);
|
||||
assert_eq!(font.random_glyph(&Style::default(), 512.0).advance, 6.0);
|
||||
}
|
||||
|
||||
/// Nexo's GUI glyphs: a bitmap `height` of 287 with no advance, so every glyph wider than 31
|
||||
/// pixels has a fishy advance, and is still drawn.
|
||||
#[test]
|
||||
fn a_wide_bitmap_glyph_is_drawn() {
|
||||
let sheet = png(40, 287, |_, _| true);
|
||||
let def = br#"{"providers":[{"type":"bitmap","file":"minecraft:font/gui.png","height":287,"ascent":65,"chars":["a"]}]}"#;
|
||||
let root = root(
|
||||
"wide-bitmap",
|
||||
&[
|
||||
("assets/minecraft/font/default.json", def),
|
||||
("assets/minecraft/textures/font/gui.png", &sheet),
|
||||
],
|
||||
);
|
||||
let font = load(&root);
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
let g = font.glyph('a');
|
||||
assert_eq!(g.advance, 41.0, "the glyph's 40 columns and one");
|
||||
assert!(g.rect.is_some(), "drawn, not the missing glyph");
|
||||
assert!(fishy_advance(g.advance));
|
||||
assert_eq!(
|
||||
font.glyph_non_fishy(DEFAULT, 'a').advance,
|
||||
6.0,
|
||||
"excluded from what obfuscated text draws"
|
||||
);
|
||||
assert_eq!(font.glyph('a').advance, 3.0, "the narrow provider wins");
|
||||
assert_eq!(font.advance('a', &Style::default()), 3.0);
|
||||
// Obfuscated text picks a glyph by its own advance: nothing is 65 wide.
|
||||
let obfuscated = font.random_glyph(&Style::default(), 65.0);
|
||||
assert_eq!(obfuscated.advance, 6.0, "the missing glyph");
|
||||
}
|
||||
|
||||
/// StringDecomposer.iterateFormatted: a `§` that ends a part stops that
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -877,8 +877,6 @@ mod tests {
|
||||
start,
|
||||
translucent_at,
|
||||
solid,
|
||||
solid_slots: u64::from(solid) * 4,
|
||||
packed: false,
|
||||
sides: crate::mesher::Sides::unsorted(solid),
|
||||
translucent,
|
||||
sort: super::super::translucency::SortState::new(Vec::new()),
|
||||
@@ -932,12 +930,11 @@ mod tests {
|
||||
.iter()
|
||||
.map(|s| (s.page, s.start, s.translucent_at))
|
||||
.collect();
|
||||
// The sections with solid quads have records, on page 0, each with
|
||||
// its cell's box.
|
||||
// The sections with solid quads have records, on page 0.
|
||||
let mut records = gpu_culling::Records::default();
|
||||
for (i, s) in sections.iter_mut().enumerate() {
|
||||
if s.solid > 0 {
|
||||
s.gpu_id = Some(records.insert(s.record((i as i32, 0, 0), None)));
|
||||
s.gpu_id = Some(records.insert((i as i32, 0, 0), s.page, s.start, s.solid));
|
||||
}
|
||||
}
|
||||
let recorded = records.live() as u32;
|
||||
@@ -960,8 +957,9 @@ mod tests {
|
||||
let word =
|
||||
|(page, at): (u32, u64)| read(&device, &queue, arena.buffer(page), at * stride);
|
||||
for (i, s) in sections.iter().enumerate() {
|
||||
// Its record names its solid quads where they are now, its box kept.
|
||||
let record = (s.solid > 0).then(|| s.record((i as i32, 0, 0), None));
|
||||
// Its record names its solid quads where they are now.
|
||||
let record = (s.solid > 0)
|
||||
.then(|| gpu_culling::Record::new((i as i32, 0, 0), s.page, s.start, s.solid));
|
||||
let held = s.gpu_id.and_then(|id| records.at(id));
|
||||
assert_eq!(held, record.as_ref(), "split {split}, section {i}");
|
||||
let Some((page, start, len)) = s.vertex_ranges(split).0 else {
|
||||
@@ -1016,7 +1014,7 @@ mod tests {
|
||||
label: None,
|
||||
entries: &[],
|
||||
});
|
||||
let pipeline = |layout: &wgpu::PipelineLayout, label: &str, _: &str, _: &str, _, _, _| {
|
||||
let pipeline = |layout: &wgpu::PipelineLayout, label: &str, _: &str, _: &str, _, _| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(layout),
|
||||
@@ -1073,8 +1071,6 @@ mod tests {
|
||||
start: 0,
|
||||
translucent_at: (0, at),
|
||||
solid: 64,
|
||||
solid_slots: 64 * 4,
|
||||
packed: false,
|
||||
sides: crate::mesher::Sides::unsorted(64),
|
||||
translucent: 128,
|
||||
sort: super::super::translucency::SortState::new(Vec::new()),
|
||||
@@ -1299,8 +1295,6 @@ mod tests {
|
||||
start: 0,
|
||||
translucent_at: if indices { (7, 7) } else { at },
|
||||
solid,
|
||||
solid_slots: u64::from(solid) * 4,
|
||||
packed: false,
|
||||
sides: crate::mesher::Sides::unsorted(solid),
|
||||
translucent,
|
||||
sort: super::super::translucency::SortState::new(Vec::new()),
|
||||
|
||||
@@ -1,574 +0,0 @@
|
||||
//! `horizon-ctl render-capture`: what the renderer needs to draw the current frame again
|
||||
//! without the game, for `examples/scene_replay.rs` (docs/graphics.md, "Render capture and
|
||||
//! replay").
|
||||
//!
|
||||
//! The renderer keeps no CPU copy of the meshes it uploads, so while capturing is on
|
||||
//! (`Renderer::keep_capture_copies`) it keeps one of every section and far tile mesh as it is
|
||||
//! handed over, at the mesher-to-renderer seam. A capture writes those of the sections and
|
||||
//! tiles it holds, the frame's camera and inputs, and the settings that change GPU work. Only
|
||||
//! the build that wrote a capture reads it.
|
||||
//!
|
||||
//! Files: `scene.json` (`Scene`), `sections.json` and `tiles.json` (one `MeshRecord` per mesh,
|
||||
//! in order), `sections.bin` and `tiles.bin` (each mesh's solid then translucent
|
||||
//! `TerrainVertex`es), `celestial.bin` (the sun and moon `Vertex`es).
|
||||
use super::{Frame, Renderer, SectionKey};
|
||||
use crate::{
|
||||
Result,
|
||||
environment::Sky,
|
||||
fast_hash::{FxHashMap, FxHashSet},
|
||||
fog::FogData,
|
||||
graphics::GraphicsApi,
|
||||
hud::HudBatch,
|
||||
lod::TileKey,
|
||||
mesher::{SectionMesh, Sides, TerrainVertex, Vertex},
|
||||
};
|
||||
use glam::{DVec3, Mat4, Vec3};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{BufReader, BufWriter, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// CPU copies of the meshes uploaded since capturing was turned on.
|
||||
#[derive(Default)]
|
||||
pub(super) struct Copies {
|
||||
sections: FxHashMap<SectionKey, SectionMesh>,
|
||||
tiles: FxHashMap<TileKey, SectionMesh>,
|
||||
}
|
||||
|
||||
impl Copies {
|
||||
pub(super) fn section(&mut self, key: SectionKey, mesh: &SectionMesh) {
|
||||
self.sections.insert(key, copy(mesh));
|
||||
}
|
||||
/// Columns unloaded (`Renderer::remove_columns`).
|
||||
pub(super) fn remove_columns(&mut self, columns: &[(i32, i32)]) {
|
||||
if columns.is_empty() {
|
||||
return;
|
||||
}
|
||||
let gone: FxHashSet<(i32, i32)> = columns.iter().copied().collect();
|
||||
self.sections
|
||||
.retain(|&(x, _, z), _| !gone.contains(&(x, z)));
|
||||
}
|
||||
pub(super) fn tile(&mut self, key: TileKey, mesh: &SectionMesh) {
|
||||
self.tiles.insert(key, copy(mesh));
|
||||
}
|
||||
pub(super) fn forget_tile(&mut self, key: TileKey) {
|
||||
self.tiles.remove(&key);
|
||||
}
|
||||
pub(super) fn clear_tiles(&mut self) {
|
||||
self.tiles.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn copy(mesh: &SectionMesh) -> SectionMesh {
|
||||
SectionMesh {
|
||||
solid: mesh.solid.clone(),
|
||||
sides: mesh.sides,
|
||||
translucent: mesh.translucent.clone(),
|
||||
packed: mesh.packed.clone(),
|
||||
solid_tints: Vec::new(),
|
||||
visibility: mesh.visibility,
|
||||
block_entities: mesh.block_entities.clone(),
|
||||
lights: mesh.lights.clone(),
|
||||
sprite_layers: mesh.sprite_layers.clone(),
|
||||
bounds: mesh.bounds,
|
||||
keep: mesh.keep,
|
||||
}
|
||||
}
|
||||
|
||||
/// A capture asked for: where to write it, and the game's settings the renderer does not hold.
|
||||
pub struct Request {
|
||||
pub dir: PathBuf,
|
||||
pub settings: Settings,
|
||||
}
|
||||
|
||||
/// The game's settings that change GPU work, as the game applied them to the renderer.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
/// `GraphicsApi`'s variant name.
|
||||
pub graphics_api: String,
|
||||
pub translucent_batching: bool,
|
||||
/// `textureFiltering` and `maxAnisotropyBit` (`Renderer::set_texture_filtering`).
|
||||
pub texture_filtering: i32,
|
||||
pub anisotropy_bit: i32,
|
||||
pub mipmap_levels: u32,
|
||||
/// `Renderer::set_world_post`'s arguments.
|
||||
pub resolution_scale: f32,
|
||||
pub fxaa: bool,
|
||||
pub taa: bool,
|
||||
pub upscale: f32,
|
||||
/// Chunks.
|
||||
pub render_distance: i32,
|
||||
pub lod_distance: u32,
|
||||
/// The game's prepared resources, where the replay loads the block sprites from.
|
||||
pub resources: PathBuf,
|
||||
/// The block sprites the meshes were made against.
|
||||
pub atlas: AtlasCheck,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn graphics_api(&self) -> GraphicsApi {
|
||||
GraphicsApi::ALL
|
||||
.into_iter()
|
||||
.find(|api| format!("{api:?}") == self.graphics_api)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Which block sprite layers the meshes' vertices name: the replay's must be the same.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AtlasCheck {
|
||||
pub sprite_size: u32,
|
||||
pub texture_layers: u32,
|
||||
/// FNV-1a of every sprite's name and layer.
|
||||
pub layers: u64,
|
||||
}
|
||||
|
||||
impl AtlasCheck {
|
||||
pub fn of(render: &crate::assets::RenderAssets) -> Self {
|
||||
let mut hash = 0xcbf2_9ce4_8422_2325u64;
|
||||
for (name, layer) in &render.sprite_layers {
|
||||
for byte in name.bytes().chain(layer.to_le_bytes()) {
|
||||
hash = (hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3);
|
||||
}
|
||||
}
|
||||
Self {
|
||||
sprite_size: render.sprite_size,
|
||||
texture_layers: render.texture_layers,
|
||||
layers: hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The renderer's own state that changes GPU work.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RendererState {
|
||||
pub adapter: String,
|
||||
pub backend: String,
|
||||
/// The frame's size in pixels.
|
||||
pub size: [u32; 2],
|
||||
/// The column layout: lowest block y and sections.
|
||||
pub min_y: i32,
|
||||
pub sections: usize,
|
||||
pub flat_draws: bool,
|
||||
pub section_cull: bool,
|
||||
pub section_sides: bool,
|
||||
pub gpu_culling: bool,
|
||||
pub gamma: f32,
|
||||
pub fullbright: bool,
|
||||
pub improved_transparency: bool,
|
||||
pub animated_textures: bool,
|
||||
/// A shader pack was running; the replay draws without it.
|
||||
pub shader_pack: bool,
|
||||
}
|
||||
|
||||
/// The cloud inputs of the frame (`clouds::Input`), with the clouds texture.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Clouds {
|
||||
pub texture: (u32, u32, Vec<bool>),
|
||||
pub uv: [f32; 2],
|
||||
pub camera: [f64; 3],
|
||||
pub time: i64,
|
||||
pub partial_tick: f64,
|
||||
pub range: i32,
|
||||
pub fast: bool,
|
||||
pub color: [f32; 4],
|
||||
}
|
||||
|
||||
/// The frame's camera and the inputs of its globals, sky, fog and lightmap.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct FrameInputs {
|
||||
pub camera: [f64; 3],
|
||||
pub yaw: f32,
|
||||
pub pitch: f32,
|
||||
pub fov_degrees: f32,
|
||||
pub eye: [f64; 3],
|
||||
pub eye_rotation: [f32; 2],
|
||||
pub first_person: bool,
|
||||
pub bob: [f32; 16],
|
||||
pub spin: [f32; 16],
|
||||
pub sky: Sky,
|
||||
pub distance: f32,
|
||||
pub detail_distance: f32,
|
||||
pub depth_far: f32,
|
||||
pub underwater: bool,
|
||||
pub water_plane: Option<f32>,
|
||||
pub cave_culling: bool,
|
||||
pub sky_blocked: bool,
|
||||
pub skybox: bool,
|
||||
pub end_sky: bool,
|
||||
pub end_sky_sprite: Option<[f32; 4]>,
|
||||
pub environment_fog: Option<(f32, f32)>,
|
||||
pub water_fog: [f32; 3],
|
||||
pub fog: Option<FogData>,
|
||||
pub dark_disc: bool,
|
||||
pub nether_lighting: bool,
|
||||
pub glint_alpha: f32,
|
||||
pub glint_ticks: f32,
|
||||
pub hand_fov_degrees: f32,
|
||||
pub stars: Option<([f32; 16], f32)>,
|
||||
pub clouds: Option<Clouds>,
|
||||
pub game_time: f32,
|
||||
pub shader_game_time: f32,
|
||||
pub end_portal_sprites: [[f32; 4]; 2],
|
||||
pub crumbling_sprite: [f32; 4],
|
||||
pub ticks: u64,
|
||||
pub darkness: (f32, f32),
|
||||
pub night_vision: f32,
|
||||
pub block_flicker: f32,
|
||||
pub boss_darken: f32,
|
||||
}
|
||||
|
||||
/// `scene.json`.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Scene {
|
||||
pub settings: Settings,
|
||||
pub renderer: RendererState,
|
||||
pub frame: FrameInputs,
|
||||
pub sections: usize,
|
||||
pub tiles: usize,
|
||||
/// The chunk columns far tiles leave out for full-detail sections.
|
||||
pub lod_cutout: Vec<(i32, i32)>,
|
||||
/// `celestial.bin`.
|
||||
#[serde(skip)]
|
||||
pub celestial: Vec<Vertex>,
|
||||
}
|
||||
|
||||
/// One mesh of `sections.json` or `tiles.json`; its vertices follow the previous mesh's in
|
||||
/// the `.bin` file. `key` is (x, section y, z) for a section, (x, z, level) for a tile.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct MeshRecord {
|
||||
key: [i32; 3],
|
||||
solid: usize,
|
||||
translucent: usize,
|
||||
sides: Sides,
|
||||
visibility: u64,
|
||||
block_entities: Vec<([i32; 3], u32)>,
|
||||
lights: Vec<([i32; 3], u32)>,
|
||||
sprite_layers: Vec<u16>,
|
||||
bounds: Option<[[f32; 3]; 2]>,
|
||||
keep: bool,
|
||||
}
|
||||
|
||||
/// Writes `name.json` and `name.bin`: (meshes, vertex bytes).
|
||||
fn write_meshes<'m>(
|
||||
dir: &Path,
|
||||
name: &str,
|
||||
meshes: impl Iterator<Item = ([i32; 3], &'m SectionMesh)>,
|
||||
) -> Result<(usize, u64)> {
|
||||
let mut blob = BufWriter::new(File::create(dir.join(format!("{name}.bin")))?);
|
||||
let mut records = Vec::new();
|
||||
let mut bytes = 0u64;
|
||||
for (key, mesh) in meshes {
|
||||
for part in [&mesh.solid, &mesh.translucent] {
|
||||
let raw: &[u8] = bytemuck::cast_slice(part.as_slice());
|
||||
blob.write_all(raw)?;
|
||||
bytes += raw.len() as u64;
|
||||
}
|
||||
records.push(MeshRecord {
|
||||
key,
|
||||
solid: mesh.solid.len(),
|
||||
translucent: mesh.translucent.len(),
|
||||
sides: mesh.sides,
|
||||
visibility: mesh.visibility,
|
||||
block_entities: mesh.block_entities.clone(),
|
||||
lights: mesh.lights.clone(),
|
||||
sprite_layers: mesh.sprite_layers.clone(),
|
||||
bounds: mesh
|
||||
.bounds
|
||||
.map(|[min, max]| [min.to_array(), max.to_array()]),
|
||||
keep: mesh.keep,
|
||||
});
|
||||
}
|
||||
blob.flush()?;
|
||||
let mut list = BufWriter::new(File::create(dir.join(format!("{name}.json")))?);
|
||||
serde_json::to_writer(&mut list, &records)?;
|
||||
list.flush()?;
|
||||
Ok((records.len(), bytes))
|
||||
}
|
||||
|
||||
/// Reads `name.json` and `name.bin`, handing each mesh to `each` as it is read.
|
||||
pub fn read_meshes(
|
||||
dir: &Path,
|
||||
name: &str,
|
||||
mut each: impl FnMut([i32; 3], SectionMesh),
|
||||
) -> Result<()> {
|
||||
let records: Vec<MeshRecord> = serde_json::from_reader(BufReader::new(File::open(
|
||||
dir.join(format!("{name}.json")),
|
||||
)?))?;
|
||||
let mut blob = BufReader::new(File::open(dir.join(format!("{name}.bin")))?);
|
||||
for record in records {
|
||||
let mut solid = vec![<TerrainVertex as bytemuck::Zeroable>::zeroed(); record.solid];
|
||||
blob.read_exact(bytemuck::cast_slice_mut::<TerrainVertex, u8>(
|
||||
solid.as_mut_slice(),
|
||||
))?;
|
||||
let mut translucent =
|
||||
vec![<TerrainVertex as bytemuck::Zeroable>::zeroed(); record.translucent];
|
||||
blob.read_exact(bytemuck::cast_slice_mut::<TerrainVertex, u8>(
|
||||
translucent.as_mut_slice(),
|
||||
))?;
|
||||
each(
|
||||
record.key,
|
||||
SectionMesh {
|
||||
solid,
|
||||
sides: record.sides,
|
||||
translucent,
|
||||
visibility: record.visibility,
|
||||
block_entities: record.block_entities,
|
||||
lights: record.lights,
|
||||
sprite_layers: record.sprite_layers,
|
||||
bounds: record
|
||||
.bounds
|
||||
.map(|[min, max]| [Vec3::from_array(min), Vec3::from_array(max)]),
|
||||
keep: record.keep,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
/// Keeps CPU copies of the meshes uploaded from now on, for `render-capture`, or drops
|
||||
/// them. They start empty: the caller meshes everything again for a complete capture.
|
||||
pub fn keep_capture_copies(&mut self, on: bool) {
|
||||
if on != self.capture_copies.is_some() {
|
||||
self.capture_copies = on.then(Copies::default);
|
||||
}
|
||||
}
|
||||
pub fn keeps_capture_copies(&self) -> bool {
|
||||
self.capture_copies.is_some()
|
||||
}
|
||||
|
||||
/// Writes the scene `frame` draws to `request.dir` (`render` calls it for `capture`).
|
||||
pub(super) fn write_capture(
|
||||
&self,
|
||||
request: &Request,
|
||||
frame: &Frame,
|
||||
) -> Result<serde_json::Value> {
|
||||
let copies = self
|
||||
.capture_copies
|
||||
.as_ref()
|
||||
.ok_or("no mesh copies kept: render-capture on first")?;
|
||||
let dir = &request.dir;
|
||||
fs::create_dir_all(dir)?;
|
||||
let (sections, section_bytes) = write_meshes(
|
||||
dir,
|
||||
"sections",
|
||||
copies
|
||||
.sections
|
||||
.iter()
|
||||
.map(|(&(x, y, z), mesh)| ([x, y, z], mesh)),
|
||||
)?;
|
||||
let standing = self.standing_lods();
|
||||
let (tiles, tile_bytes) = write_meshes(
|
||||
dir,
|
||||
"tiles",
|
||||
copies
|
||||
.tiles
|
||||
.iter()
|
||||
.filter(|(key, _)| standing.contains(*key))
|
||||
.map(|(key, mesh)| ([key.x, key.z, i32::from(key.level)], mesh)),
|
||||
)?;
|
||||
fs::write(
|
||||
dir.join("celestial.bin"),
|
||||
bytemuck::cast_slice::<Vertex, u8>(frame.celestial),
|
||||
)?;
|
||||
let scene = Scene {
|
||||
settings: request.settings.clone(),
|
||||
renderer: RendererState {
|
||||
adapter: self.info.name.clone(),
|
||||
backend: format!("{:?}", self.info.backend),
|
||||
size: [self.config.width, self.config.height],
|
||||
min_y: self.layout.min_y(),
|
||||
sections: self.layout.sections(),
|
||||
flat_draws: self.flat_draws(),
|
||||
section_cull: self.section_cull(),
|
||||
section_sides: self.section_sides(),
|
||||
gpu_culling: self.gpu_culling(),
|
||||
gamma: self.gamma,
|
||||
fullbright: self.fullbright,
|
||||
improved_transparency: self.improved_transparency,
|
||||
animated_textures: self.animated_textures,
|
||||
shader_pack: self.pack_post.is_some() || self.native.is_some(),
|
||||
},
|
||||
frame: FrameInputs {
|
||||
camera: frame.camera.to_array(),
|
||||
yaw: frame.yaw,
|
||||
pitch: frame.pitch,
|
||||
fov_degrees: frame.fov_degrees,
|
||||
eye: frame.eye.to_array(),
|
||||
eye_rotation: frame.eye_rotation,
|
||||
first_person: frame.first_person,
|
||||
bob: frame.bob.to_cols_array(),
|
||||
spin: frame.spin.to_cols_array(),
|
||||
sky: frame.sky,
|
||||
distance: frame.distance,
|
||||
detail_distance: frame.detail_distance,
|
||||
depth_far: frame.depth_far,
|
||||
underwater: frame.underwater,
|
||||
water_plane: frame.water_plane,
|
||||
cave_culling: frame.cave_culling,
|
||||
sky_blocked: frame.sky_blocked,
|
||||
skybox: frame.skybox,
|
||||
end_sky: frame.end_sky,
|
||||
end_sky_sprite: frame.end_sky_sprite,
|
||||
environment_fog: frame.environment_fog,
|
||||
water_fog: frame.water_fog,
|
||||
fog: frame.fog,
|
||||
dark_disc: frame.dark_disc,
|
||||
nether_lighting: frame.nether_lighting,
|
||||
glint_alpha: frame.glint_alpha,
|
||||
glint_ticks: frame.glint_ticks,
|
||||
hand_fov_degrees: frame.hand_fov_degrees,
|
||||
stars: frame
|
||||
.stars
|
||||
.map(|(rotation, brightness)| (rotation.to_cols_array(), brightness)),
|
||||
clouds: frame.clouds.as_ref().map(|c| Clouds {
|
||||
texture: c.texture.clone(),
|
||||
uv: c.uv,
|
||||
camera: c.camera.to_array(),
|
||||
time: c.time,
|
||||
partial_tick: c.partial_tick,
|
||||
range: c.range,
|
||||
fast: c.fast,
|
||||
color: c.color,
|
||||
}),
|
||||
game_time: frame.game_time,
|
||||
shader_game_time: frame.shader_game_time,
|
||||
end_portal_sprites: frame.end_portal_sprites,
|
||||
crumbling_sprite: frame.crumbling_sprite,
|
||||
ticks: frame.ticks,
|
||||
darkness: frame.darkness,
|
||||
night_vision: frame.night_vision,
|
||||
block_flicker: frame.block_flicker,
|
||||
boss_darken: frame.boss_darken,
|
||||
},
|
||||
sections,
|
||||
tiles,
|
||||
lod_cutout: self.lod_cutout.iter().copied().collect(),
|
||||
celestial: Vec::new(),
|
||||
};
|
||||
let mut out = BufWriter::new(File::create(dir.join("scene.json"))?);
|
||||
serde_json::to_writer_pretty(&mut out, &scene)?;
|
||||
out.flush()?;
|
||||
Ok(serde_json::json!({
|
||||
"ok": true,
|
||||
"dir": dir.display().to_string(),
|
||||
"sections": sections,
|
||||
"tiles": tiles,
|
||||
"vertex_bytes": section_bytes + tile_bytes,
|
||||
"shader_pack_left_out": scene.renderer.shader_pack,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
/// Reads `scene.json` and `celestial.bin` (the meshes: `read_meshes`).
|
||||
pub fn read(dir: &Path) -> Result<Self> {
|
||||
let mut scene: Scene =
|
||||
serde_json::from_reader(BufReader::new(File::open(dir.join("scene.json"))?))?;
|
||||
let bytes = fs::read(dir.join("celestial.bin"))?;
|
||||
let size = std::mem::size_of::<Vertex>();
|
||||
let mut celestial = vec![<Vertex as bytemuck::Zeroable>::zeroed(); bytes.len() / size];
|
||||
let len = celestial.len() * size;
|
||||
bytemuck::cast_slice_mut::<Vertex, u8>(celestial.as_mut_slice())
|
||||
.copy_from_slice(&bytes[..len]);
|
||||
scene.celestial = celestial;
|
||||
Ok(scene)
|
||||
}
|
||||
|
||||
/// The captured frame: its camera, sky, fog, clouds, sun, moon and stars, without
|
||||
/// entities, particles, weather, the hand, the outline or the HUD.
|
||||
pub fn frame<'a>(&'a self, hud: &'a HudBatch) -> Frame<'a> {
|
||||
let f = &self.frame;
|
||||
Frame {
|
||||
weather: &[],
|
||||
weather_time: crate::weather::Time::new(0, 0.0),
|
||||
camera: DVec3::from_array(f.camera),
|
||||
yaw: f.yaw,
|
||||
pitch: f.pitch,
|
||||
fov_degrees: f.fov_degrees,
|
||||
eye: DVec3::from_array(f.eye),
|
||||
eye_rotation: f.eye_rotation,
|
||||
first_person: f.first_person,
|
||||
bob: Mat4::from_cols_array(&f.bob),
|
||||
spin: Mat4::from_cols_array(&f.spin),
|
||||
sky: f.sky,
|
||||
shadow_scene_revision: (0, 0),
|
||||
shadow_cache: false,
|
||||
distance: f.distance,
|
||||
detail_distance: f.detail_distance,
|
||||
depth_far: f.depth_far,
|
||||
underwater: f.underwater,
|
||||
water_plane: f.water_plane,
|
||||
cave_culling: f.cave_culling,
|
||||
title: false,
|
||||
sky_blocked: f.sky_blocked,
|
||||
skybox: f.skybox,
|
||||
end_sky: f.end_sky,
|
||||
end_sky_sprite: f.end_sky_sprite,
|
||||
environment_fog: f.environment_fog,
|
||||
water_fog: f.water_fog,
|
||||
fog: f.fog,
|
||||
dark_disc: f.dark_disc,
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
hud,
|
||||
glint_alpha: f.glint_alpha,
|
||||
glint_ticks: f.glint_ticks,
|
||||
nether_lighting: f.nether_lighting,
|
||||
entities: &[],
|
||||
static_block_entities: &[],
|
||||
visible_static_block_entity_sections: &[],
|
||||
static_block_entity_stats: Default::default(),
|
||||
entity_motion: &[],
|
||||
entity_spans: &[],
|
||||
skin: None,
|
||||
glowing: &[],
|
||||
dynamic_lights: &[],
|
||||
colored_lights: &[],
|
||||
emissive_layers: &[],
|
||||
clouds: f.clouds.as_ref().map(|c| crate::clouds::Input {
|
||||
texture: &c.texture,
|
||||
uv: c.uv,
|
||||
camera: DVec3::from_array(c.camera),
|
||||
time: c.time,
|
||||
partial_tick: c.partial_tick,
|
||||
range: c.range,
|
||||
fast: c.fast,
|
||||
color: c.color,
|
||||
}),
|
||||
lightning: Default::default(),
|
||||
additive: &[],
|
||||
see_through: &[],
|
||||
shadow_only: &[],
|
||||
celestial: &self.celestial,
|
||||
stars: f
|
||||
.stars
|
||||
.map(|(rotation, brightness)| (Mat4::from_cols_array(&rotation), brightness)),
|
||||
hand: &[],
|
||||
hand_fov_degrees: f.hand_fov_degrees,
|
||||
screen: &[],
|
||||
pack: Default::default(),
|
||||
border: None,
|
||||
game_time: f.game_time,
|
||||
shader_game_time: f.shader_game_time,
|
||||
end_portal_sprites: f.end_portal_sprites,
|
||||
crumbling_sprite: f.crumbling_sprite,
|
||||
ticks: f.ticks,
|
||||
darkness: f.darkness,
|
||||
night_vision: f.night_vision,
|
||||
block_flicker: f.block_flicker,
|
||||
boss_darken: f.boss_darken,
|
||||
chunk_fade: 0.0,
|
||||
post_effects: crate::post_chain::Request::none(),
|
||||
menu_blur: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,486 +0,0 @@
|
||||
//! A resource pack's `core/rendertype_lines` shader on the block outline (`RenderTypes.LINES`,
|
||||
//! and `secondaryBlockOutline` under a high contrast one): an outline its fragment shader colours
|
||||
//! or animates. Without one the outline draws natively (`vs_line` in terrain.wgsl).
|
||||
|
||||
use glam::Mat4;
|
||||
use horizon_shaders::{CoreProgram, Glue};
|
||||
|
||||
/// The outline's edges (`pack_lines::layout`: start, end, ARGB colour and width per instance) as
|
||||
/// vanilla's lines: four vertices an edge, the start twice and the end twice, the side
|
||||
/// `gl_VertexIndex % 2`, as `vs_line` draws them.
|
||||
pub(crate) const GLUE: Glue = Glue {
|
||||
inputs: &[
|
||||
("Position", "vec3"),
|
||||
("Color", "vec4"),
|
||||
("Normal", "vec3"),
|
||||
("LineWidth", "float"),
|
||||
],
|
||||
prelude: "layout(location = 0) in vec3 hz_start;\n\
|
||||
layout(location = 1) in vec3 hz_end;\n\
|
||||
layout(location = 2) in vec4 hz_argb;\n\
|
||||
layout(location = 3) in float hz_width;",
|
||||
// ARGB in a little-endian u32 reads as B, G, R, A. ShapeRenderer's normal: the direction.
|
||||
setup: "Position = hz_start; if (int(gl_VertexIndex) >= 2) Position = hz_end; \
|
||||
Color = hz_argb.zyxw; Normal = normalize(hz_end - hz_start); LineWidth = hz_width;",
|
||||
// 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];
|
||||
|
||||
pub(super) struct CoreLines {
|
||||
lines: wgpu::RenderPipeline,
|
||||
secondary: wgpu::RenderPipeline,
|
||||
group: wgpu::BindGroup,
|
||||
uniform: wgpu::Buffer,
|
||||
}
|
||||
|
||||
impl CoreLines {
|
||||
pub(super) fn new(
|
||||
device: &wgpu::Device,
|
||||
format: wgpu::TextureFormat,
|
||||
program: &CoreProgram,
|
||||
) -> 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 = |label, module: &naga::Module| {
|
||||
device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some(label),
|
||||
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module.clone())),
|
||||
})
|
||||
};
|
||||
let vertex = module("Resource pack lines (vertex)", &program.vertex);
|
||||
let fragment = module("Resource pack lines (fragment)", &program.fragment);
|
||||
// Vanilla's LINES pipeline has no samplers: a pack that reads one is not used. The
|
||||
// blocks' sizes are checked here too, not at the draw: a pack's own larger block fails
|
||||
// in this scope.
|
||||
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("Resource pack lines"),
|
||||
entries: &entries,
|
||||
});
|
||||
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Resource pack lines uniforms"),
|
||||
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("Resource pack lines"),
|
||||
layout: &layout,
|
||||
entries: &resources,
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Resource pack lines"),
|
||||
bind_group_layouts: &[Some(&layout)],
|
||||
..Default::default()
|
||||
});
|
||||
// The built-in outline's state: a strip per edge, reversed-Z depth test, translucent.
|
||||
let pipeline = |label, depth_write| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &vertex,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[Some(super::pack_lines::layout())],
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleStrip,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: Some(depth_write),
|
||||
depth_compare: Some(wgpu::CompareFunction::GreaterEqual),
|
||||
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: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let lines = pipeline("Resource pack block outline", true);
|
||||
let secondary = pipeline("Resource pack block outline secondary", false);
|
||||
let internal = pollster::block_on(internal.pop());
|
||||
if let Some(e) = internal.or(pollster::block_on(validation.pop())) {
|
||||
log::warn!("the resource pack's rendertype_lines shader is not used: {e}");
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
lines,
|
||||
secondary,
|
||||
group,
|
||||
uniform,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn uniform(&self) -> &wgpu::Buffer {
|
||||
&self.uniform
|
||||
}
|
||||
|
||||
/// Draw `edges` (instances of the outline buffer bound at slot 0) with the pack's shader:
|
||||
/// the secondary stroke without depth writes. The caller sets its own group 0 again after.
|
||||
pub(super) fn draw(
|
||||
&self,
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
secondary: bool,
|
||||
edges: std::ops::Range<u32>,
|
||||
) {
|
||||
pass.set_pipeline(if secondary {
|
||||
&self.secondary
|
||||
} else {
|
||||
&self.lines
|
||||
});
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
pass.draw(0..4, edges);
|
||||
}
|
||||
}
|
||||
|
||||
/// The std140 blocks: `Globals` (`post_chain::Globals::bytes`), `Projection` (`jitter`, the lens
|
||||
/// and the view bob `pose`, as vanilla's projection matrix carries it), `DynamicTransforms` (the
|
||||
/// camera's rotation with the PERSPECTIVE `layering` scale, colour modulator white) and `Fog`
|
||||
/// (`fog`: environmental start and end, render distance start and end, sky end, clouds end).
|
||||
/// `ProjMat · ModelViewMat` is `vs_line`'s `view_proj` with its layering scale.
|
||||
pub(super) fn uniforms(
|
||||
globals: [u8; 48],
|
||||
view: &super::View,
|
||||
pose: Mat4,
|
||||
jitter: Mat4,
|
||||
layering: f32,
|
||||
fog_color: [f32; 4],
|
||||
fog: [f32; 6],
|
||||
) -> Vec<u8> {
|
||||
let projection = jitter * view.projection * pose;
|
||||
let model_view = view.view * Mat4::from_scale(glam::Vec3::splat(layering));
|
||||
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].fill(1.0);
|
||||
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::*;
|
||||
use glam::{Vec3, Vec4};
|
||||
|
||||
const INCLUDES: [(&str, &str); 3] = [
|
||||
(
|
||||
"minecraft:shaders/include/globals.glsl",
|
||||
"layout(std140) uniform Globals {\n ivec3 CameraBlockPos;\n float GlintAlpha;\n vec3 CameraOffset;\n float GameTime;\n vec2 ScreenSize;\n int MenuBlurRadius;\n int UseRgss;\n};\n",
|
||||
),
|
||||
(
|
||||
"minecraft:shaders/include/dynamictransforms.glsl",
|
||||
"layout(std140) uniform DynamicTransforms {\n mat4 ModelViewMat;\n mat4 TextureMat;\n vec4 ColorModulator;\n vec3 ModelOffset;\n};\n",
|
||||
),
|
||||
(
|
||||
"minecraft:shaders/include/projection.glsl",
|
||||
"layout(std140) uniform Projection {\n mat4 ProjMat;\n};\n",
|
||||
),
|
||||
];
|
||||
|
||||
fn include(id: &str) -> Option<String> {
|
||||
INCLUDES
|
||||
.iter()
|
||||
.find(|(path, _)| *path == id)
|
||||
.map(|(_, text)| (*text).to_owned())
|
||||
}
|
||||
|
||||
/// 26.3's `rendertype_lines.vsh` in shape: a stroke `LineWidth` pixels wide across the
|
||||
/// screen-space direction of `Normal`, its side from `gl_VertexIndex % 2`.
|
||||
const LINES_VSH: &str = "#version 330\n#extension GL_ARB_separate_shader_objects : require\n\
|
||||
#include <minecraft:globals.glsl>\n#include <minecraft:dynamictransforms.glsl>\n#include <minecraft:projection.glsl>\n\
|
||||
layout(location = 0) in vec3 Position;\nlayout(location = 1) in vec4 Color;\n\
|
||||
layout(location = 2) in vec3 Normal;\nlayout(location = 3) in float LineWidth;\n\
|
||||
layout(location = 2) out vec4 vertexColor;\n\
|
||||
const mat4 SHRINK = mat4(0.99, 0.0, 0.0, 0.0, 0.0, 0.99, 0.0, 0.0, 0.0, 0.0, 0.99, 0.0, 0.0, 0.0, 0.0, 1.0);\n\
|
||||
void main() {\n vec4 from = ProjMat * SHRINK * ModelViewMat * vec4(Position, 1.0);\n\
|
||||
vec4 to = ProjMat * SHRINK * ModelViewMat * vec4(Position + Normal, 1.0);\n\
|
||||
vec2 along = normalize((to.xy / to.w - from.xy / from.w) * ScreenSize);\n\
|
||||
vec2 side = vec2(-along.y, along.x) * LineWidth / ScreenSize;\n\
|
||||
if (gl_VertexIndex % 2 == 0) {\n side = -side;\n }\n\
|
||||
gl_Position = vec4(from.xy / from.w + side, from.z / from.w, 1.0) * from.w;\n\
|
||||
vertexColor = Color;\n}\n";
|
||||
|
||||
/// An RGB outline pack's fragment shader in shape: the colour from `GameTime`.
|
||||
const RGB_FSH: &str = "#version 330\n#extension GL_ARB_separate_shader_objects : require\n\
|
||||
#include <minecraft:globals.glsl>\n\
|
||||
layout(location = 2) in vec4 vertexColor;\nlayout(location = 0) out vec4 fragColor;\n\
|
||||
void main() {\n fragColor = vec4(GameTime, 1.0 - GameTime, vertexColor.b, 1.0);\n}\n";
|
||||
|
||||
/// ProjMat · ModelViewMat (with the vertex shader's shrink between them) is `vs_line`'s
|
||||
/// view_proj on the layered, shrunk position: the pack's outline sits where the built-in one
|
||||
/// does, view bob and TAA jitter included.
|
||||
#[test]
|
||||
fn the_pack_matrices_put_the_outline_where_vs_line_does() {
|
||||
let view = super::super::View::screen(37.0, -21.0, 70.0, 16.0 / 9.0, 256.0);
|
||||
let pose =
|
||||
Mat4::from_rotation_z(0.02) * Mat4::from_translation(Vec3::new(0.01, -0.03, 0.0));
|
||||
let jitter = Mat4::from_translation(Vec3::new(0.0004, -0.0002, 0.0));
|
||||
let layering = 1.0 - 1.0 / 4096.0;
|
||||
let bytes = uniforms([0; 48], &view, pose, jitter, layering, [0.0; 4], [0.0; 6]);
|
||||
let data: Vec<f32> = bytes
|
||||
.as_chunks()
|
||||
.0
|
||||
.iter()
|
||||
.map(|b| f32::from_le_bytes(*b))
|
||||
.collect();
|
||||
let projection = Mat4::from_cols_slice(&data[64..80]);
|
||||
let model_view = Mat4::from_cols_slice(&data[128..144]);
|
||||
assert_eq!(&data[160..164], &[1.0; 4], "ColorModulator");
|
||||
let shrink = 1.0 - 1.0 / 256.0;
|
||||
for p in [Vec3::new(1.5, -0.2, 3.0), Vec3::new(-4.0, 2.0, -0.5)] {
|
||||
let pack =
|
||||
projection * Mat4::from_scale(Vec3::splat(shrink)) * model_view * p.extend(1.0);
|
||||
let native = jitter * view.view_proj(pose) * (p * layering * shrink).extend(1.0);
|
||||
let error: Vec4 = (pack - native).abs();
|
||||
assert!(
|
||||
error.max_element() < 1e-5 * native.w.abs(),
|
||||
"{pack} {native}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn device() -> Option<(wgpu::Device, wgpu::Queue)> {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
|
||||
pollster::block_on(adapter.request_device(&Default::default())).ok()
|
||||
}
|
||||
|
||||
/// One edge across the middle of a 64 by 64 target, 4 pixels wide, blue (ARGB `ff0000ff`),
|
||||
/// at `GameTime` 0.25, drawn with `program`: the RGBA rows read back.
|
||||
fn draw_edge(device: &wgpu::Device, queue: &wgpu::Queue, program: &CoreProgram) -> Vec<u8> {
|
||||
const SIZE: u32 = 64;
|
||||
let lines = CoreLines::new(device, wgpu::TextureFormat::Rgba8Unorm, program)
|
||||
.expect("the pack's pipelines");
|
||||
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(
|
||||
wgpu::TextureFormat::Rgba8Unorm,
|
||||
wgpu::TextureUsages::COPY_SRC,
|
||||
);
|
||||
let depth = target(
|
||||
wgpu::TextureFormat::Depth32Float,
|
||||
wgpu::TextureUsages::empty(),
|
||||
);
|
||||
let view = super::super::View {
|
||||
view: Mat4::IDENTITY,
|
||||
projection: super::super::world_projection(70.0, 1.0, 64.0),
|
||||
aspect: 1.0,
|
||||
tan_half_fov: 35f32.to_radians().tan(),
|
||||
};
|
||||
let globals = crate::post_chain::Globals {
|
||||
game_time: 0.25,
|
||||
..Default::default()
|
||||
};
|
||||
let data = uniforms(
|
||||
globals.bytes([SIZE, SIZE], 0),
|
||||
&view,
|
||||
Mat4::IDENTITY,
|
||||
Mat4::IDENTITY,
|
||||
1.0,
|
||||
[0.0; 4],
|
||||
[0.0; 6],
|
||||
);
|
||||
queue.write_buffer(lines.uniform(), 0, &data);
|
||||
let mut edge = [-1.0, 0.0, -2.0, 1.0, 0.0, -2.0, 0.0, 4.0f32];
|
||||
edge[6] = f32::from_bits(0xff00_00ff);
|
||||
let edges = wgpu::util::DeviceExt::create_buffer_init(
|
||||
device,
|
||||
&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytemuck::cast_slice(&edge),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
},
|
||||
);
|
||||
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()
|
||||
});
|
||||
pass.set_vertex_buffer(0, edges.slice(..));
|
||||
lines.draw(&mut pass, false, 0..1);
|
||||
}
|
||||
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();
|
||||
read.slice(..).get_mapped_range().unwrap().to_vec()
|
||||
}
|
||||
|
||||
/// The stroke's middle takes the pack's colour (`GameTime` red, its inverse green, the
|
||||
/// vertex colour's blue); a pixel far from it stays clear.
|
||||
fn assert_pack_coloured(pixels: &[u8]) {
|
||||
let at = |x: usize, y: usize| &pixels[(y * 64 + x) * 4..(y * 64 + x) * 4 + 4];
|
||||
for y in [31, 32] {
|
||||
let pixel = at(32, y);
|
||||
let want = [64, 191, 255, 255];
|
||||
assert!(
|
||||
pixel.iter().zip(want).all(|(&p, w)| p.abs_diff(w) <= 1),
|
||||
"row {y}: {pixel:?}"
|
||||
);
|
||||
}
|
||||
assert_eq!(at(32, 4), [0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pack_fragment_shader_colours_the_outline() {
|
||||
let Some((device, queue)) = device() else {
|
||||
eprintln!("no GPU: skipping");
|
||||
return;
|
||||
};
|
||||
let program = horizon_shaders::translate_core(LINES_VSH, RGB_FSH, &[], &include, &GLUE)
|
||||
.expect("the shaders translate");
|
||||
assert_pack_coloured(&draw_edge(&device, &queue, &program));
|
||||
}
|
||||
|
||||
/// A pack's own include with a bigger block than vanilla's (an older `DynamicTransforms`)
|
||||
/// is not used, and the outline draws natively, rather than failing at the draw.
|
||||
#[test]
|
||||
fn a_pack_block_bigger_than_vanillas_falls_back() {
|
||||
let Some((device, _)) = device() else {
|
||||
return;
|
||||
};
|
||||
let bigger = |id: &str| {
|
||||
include(id)
|
||||
.map(|text| text.replace("vec3 ModelOffset;", "vec3 ModelOffset;\n vec4 More;"))
|
||||
};
|
||||
let program = horizon_shaders::translate_core(LINES_VSH, RGB_FSH, &[], &bigger, &GLUE)
|
||||
.expect("the shaders translate");
|
||||
assert!(CoreLines::new(&device, wgpu::TextureFormat::Rgba8Unorm, &program).is_none());
|
||||
}
|
||||
|
||||
/// Vanilla's own `rendertype_lines.vsh` (from the prepared resources) under a pack's
|
||||
/// fragment shader.
|
||||
#[test]
|
||||
fn vanillas_lines_vertex_shader_takes_a_packs_fragment_shader() {
|
||||
let root = 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)
|
||||
});
|
||||
let Ok(vertex) = std::fs::read_to_string(
|
||||
root.join("assets/minecraft/shaders/core/rendertype_lines.vsh"),
|
||||
) else {
|
||||
eprintln!(
|
||||
"skipping resource-backed lines shader check: {}",
|
||||
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 program = horizon_shaders::translate_core(&vertex, RGB_FSH, &[], &include, &GLUE)
|
||||
.expect("vanilla's vertex shader translates");
|
||||
// A pack that replaces only the vertex shader runs vanilla's fragment shader.
|
||||
let fragment = include("minecraft:shaders/core/rendertype_lines.fsh").unwrap();
|
||||
let vanilla = horizon_shaders::translate_core(&vertex, &fragment, &[], &include, &GLUE)
|
||||
.expect("vanilla's fragment shader translates");
|
||||
let Some((device, queue)) = device() else {
|
||||
eprintln!("no GPU: skipping the draw");
|
||||
return;
|
||||
};
|
||||
assert_pack_coloured(&draw_edge(&device, &queue, &program));
|
||||
assert!(CoreLines::new(&device, wgpu::TextureFormat::Rgba8Unorm, &vanilla).is_some());
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,6 @@
|
||||
//! the results only sorts sections between the phases: a stale result draws
|
||||
//! a hidden section in phase 1, or tests a section that is in sight.
|
||||
//!
|
||||
//! Far terrain tiles are tested the same way, with their meshes' own
|
||||
//! bounds for a box (grown by a block too, which keeps their faces off the
|
||||
//! box's), and resident slots of their own (`TILE`).
|
||||
//!
|
||||
//! Every solid draw is an entry of one of the two lists, so the CPU records
|
||||
//! as many draws as without culling: drivers that unroll an indirect
|
||||
//! multi-draw on the CPU (Mesa's on Intel Gen9) pay per entry. Phase 2's
|
||||
@@ -43,11 +39,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]];
|
||||
/// 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;
|
||||
/// Phase 1's sections are each tested every `RETEST` frames.
|
||||
const RETEST: u32 = 8;
|
||||
/// Culling runs while the last result read back hid at least this share of
|
||||
@@ -72,17 +63,6 @@ const FREE: u8 = 0;
|
||||
const PENDING: u8 = 1;
|
||||
const MAPPED: u8 = 2;
|
||||
|
||||
/// One test, as `cull.wgsl` reads it: visible index and phase 2 draws
|
||||
/// (`place`), then its box's corners around the origin.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Test {
|
||||
data: [u32; 4],
|
||||
low: [f32; 4],
|
||||
high: [f32; 4],
|
||||
}
|
||||
const TEST_BYTES: u64 = std::mem::size_of::<Test>() as u64;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Params {
|
||||
@@ -151,10 +131,8 @@ pub struct Cull {
|
||||
phase2_capacity: u64,
|
||||
tests: wgpu::Buffer,
|
||||
tests_capacity: u64,
|
||||
/// This frame's tests: visible index, then its phase 2 draws in each
|
||||
/// solid pass (`place`): the first of the first pass's, both counts (the
|
||||
/// second's in the high half), the first of the second pass's; and its box.
|
||||
test_data: Vec<Test>,
|
||||
/// This frame's tests: visible index, first phase 2 draw, phase 2 draws.
|
||||
test_data: Vec<[u32; 4]>,
|
||||
test_slots: Vec<u32>,
|
||||
test_indices: Vec<u32>,
|
||||
total: u64,
|
||||
@@ -164,10 +142,8 @@ pub struct Cull {
|
||||
readbacks: Vec<Readback>,
|
||||
next: usize,
|
||||
current: Option<usize>,
|
||||
/// Per resident slot: whether the last result read back saw it; and per
|
||||
/// far tile's slot (`TILE`).
|
||||
/// Per resident slot: whether the last result read back saw it.
|
||||
seen: Vec<bool>,
|
||||
seen_tiles: Vec<bool>,
|
||||
stamp: u32,
|
||||
/// Of the boxes the last test read back drew, how many it did not see
|
||||
/// and how many it drew, and the share of solid indices hidden.
|
||||
@@ -349,7 +325,7 @@ impl Cull {
|
||||
),
|
||||
phase2: buffer(device, "Section draws, phase 2", WORDS, PHASE2_USAGE),
|
||||
phase2_capacity: 1,
|
||||
tests: buffer(device, "Section tests", TEST_BYTES, TESTS_USAGE),
|
||||
tests: buffer(device, "Section tests", 16, TESTS_USAGE),
|
||||
tests_capacity: 1,
|
||||
test_data: Vec::new(),
|
||||
test_slots: Vec::new(),
|
||||
@@ -371,7 +347,6 @@ impl Cull {
|
||||
next: 0,
|
||||
current: None,
|
||||
seen: Vec::new(),
|
||||
seen_tiles: Vec::new(),
|
||||
stamp: 0,
|
||||
hidden: ([0; 2], 1.0),
|
||||
always: false,
|
||||
@@ -403,15 +378,11 @@ impl Cull {
|
||||
let share = indices as f32 / r.total.max(1) as f32;
|
||||
self.hidden = ([hidden, results.len()], share);
|
||||
for (&slot, &result) in r.slots.iter().zip(&results) {
|
||||
let (seen, slot) = if slot & TILE != 0 {
|
||||
(&mut self.seen_tiles, (slot & !TILE) as usize)
|
||||
} else {
|
||||
(&mut self.seen, slot as usize)
|
||||
};
|
||||
if slot >= seen.len() {
|
||||
seen.resize(slot + 1, false);
|
||||
let slot = slot as usize;
|
||||
if slot >= self.seen.len() {
|
||||
self.seen.resize(slot + 1, false);
|
||||
}
|
||||
seen[slot] = result == r.stamp;
|
||||
self.seen[slot] = result == r.stamp;
|
||||
}
|
||||
}
|
||||
r.buffer.unmap();
|
||||
@@ -426,61 +397,30 @@ impl Cull {
|
||||
!self.probe || self.stamp.is_multiple_of(PROBE)
|
||||
}
|
||||
|
||||
/// Whether phase 1 draws the section in resident `slot` (or the far tile
|
||||
/// in its `TILE` slot).
|
||||
/// Whether phase 1 draws the section in resident `slot`.
|
||||
pub fn first(&self, slot: u32) -> bool {
|
||||
if slot & TILE != 0 {
|
||||
return self
|
||||
.seen_tiles
|
||||
.get((slot & !TILE) as usize)
|
||||
.copied()
|
||||
.unwrap_or(false);
|
||||
}
|
||||
self.seen.get(slot as usize).copied().unwrap_or(false)
|
||||
}
|
||||
|
||||
/// A section phase 1 draws, `index` in the visible list, with `indices`
|
||||
/// solid indices and the box `bounds` around its origin: tested in turn.
|
||||
pub fn drawn(&mut self, index: usize, slot: u32, indices: u32, bounds: [[f32; 3]; 2]) {
|
||||
/// solid indices: tested in turn.
|
||||
pub fn drawn(&mut self, index: usize, slot: u32, indices: u32) {
|
||||
self.total += u64::from(indices);
|
||||
if indices > 0 && (self.probe || slot.wrapping_add(self.stamp).is_multiple_of(RETEST)) {
|
||||
self.push(index, bounds, slot, indices);
|
||||
self.push([index as u32, 0, 0, 0], slot, indices);
|
||||
}
|
||||
}
|
||||
|
||||
/// A section left to phase 2, with `draws` draws there: its test, whose
|
||||
/// draws `place` gives once phase 2's list is in its final order.
|
||||
pub fn deferred(
|
||||
&mut self,
|
||||
index: usize,
|
||||
slot: u32,
|
||||
draws: usize,
|
||||
indices: u32,
|
||||
bounds: [[f32; 3]; 2],
|
||||
) -> Option<u32> {
|
||||
/// A section left to phase 2: its `draws` from phase 2's `first`.
|
||||
pub fn deferred(&mut self, index: usize, slot: u32, first: usize, draws: usize, indices: u32) {
|
||||
self.total += u64::from(indices);
|
||||
(draws > 0).then(|| {
|
||||
self.push(index, bounds, slot, indices);
|
||||
self.test_data.len() as u32 - 1
|
||||
})
|
||||
if draws > 0 {
|
||||
self.push([index as u32, first as u32, draws as u32, 0], slot, indices);
|
||||
}
|
||||
}
|
||||
|
||||
/// `test`'s draws in solid pass `pass` (0 or 1): `draws` from phase 2's
|
||||
/// `first`. Each pass draws its own part of the section's quads.
|
||||
pub fn place(&mut self, test: u32, pass: u32, first: u32, draws: u32) {
|
||||
debug_assert!(pass < 2 && draws < 1 << 16);
|
||||
let t = &mut self.test_data[test as usize].data;
|
||||
t[1 + 2 * pass as usize] = first;
|
||||
t[2] |= draws << (16 * pass);
|
||||
}
|
||||
|
||||
fn push(&mut self, index: usize, bounds: [[f32; 3]; 2], slot: u32, indices: u32) {
|
||||
let [low, high] = bounds.map(|[x, y, z]| [x, y, z, 0.0]);
|
||||
self.test_data.push(Test {
|
||||
data: [index as u32, 0, 0, 0],
|
||||
low,
|
||||
high,
|
||||
});
|
||||
fn push(&mut self, test: [u32; 4], slot: u32, indices: u32) {
|
||||
self.test_data.push(test);
|
||||
self.test_slots.push(slot);
|
||||
self.test_indices.push(indices);
|
||||
}
|
||||
@@ -514,7 +454,7 @@ impl Cull {
|
||||
self.tests = buffer(
|
||||
device,
|
||||
"Section tests",
|
||||
self.tests_capacity * TEST_BYTES,
|
||||
self.tests_capacity * 16,
|
||||
TESTS_USAGE,
|
||||
);
|
||||
self.results = buffer(
|
||||
|
||||
@@ -11,19 +11,10 @@ struct Globals {
|
||||
view_proj: mat4x4<f32>,
|
||||
}
|
||||
|
||||
// One test per section or far tile: its visible index (which names its
|
||||
// origin), then its draws in `phase2` for each solid pass (gfx/cull.rs's
|
||||
// `place`): the first pass's first draw, both passes' draw counts (the
|
||||
// second's in the high 16 bits), the second pass's first draw. No draws:
|
||||
// phase 1 drew it, and the test only tells whether it is still in sight.
|
||||
// Then its box's lowest and highest corner around the origin (a section's:
|
||||
// -1 and 17, `SECTION_BOX`).
|
||||
struct Test {
|
||||
data: vec4<u32>,
|
||||
low: vec4<f32>,
|
||||
high: vec4<f32>,
|
||||
}
|
||||
@group(0) @binding(0) var<storage, read> tests: array<Test>;
|
||||
// One test per section: its visible index (which names its origin), its
|
||||
// first draw in `phase2` and how many draws it has there (0: phase 1 drew
|
||||
// it, and the test only tells whether it is still in sight).
|
||||
@group(0) @binding(0) var<storage, read> tests: array<vec4<u32>>;
|
||||
// Per test: the stamp of the frame whose test saw the box.
|
||||
@group(0) @binding(1) var<storage, read_write> results: array<u32>;
|
||||
// Phase 2's draws, 5 words each, with instance counts of 0 until seen.
|
||||
@@ -45,27 +36,23 @@ struct Box {
|
||||
@location(0) @interpolate(flat) test: u32,
|
||||
}
|
||||
|
||||
fn corner(origin: vec3<f32>, low: vec3<f32>, high: vec3<f32>, c: u32) -> vec3<f32> {
|
||||
fn corner(origin: vec3<f32>, c: u32) -> vec3<f32> {
|
||||
return origin + vec3(
|
||||
select(low.x, high.x, (c & 1u) != 0u),
|
||||
select(low.y, high.y, (c & 2u) != 0u),
|
||||
select(low.z, high.z, (c & 4u) != 0u),
|
||||
select(-1.0, 17.0, (c & 1u) != 0u),
|
||||
select(-1.0, 17.0, (c & 2u) != 0u),
|
||||
select(-1.0, 17.0, (c & 4u) != 0u),
|
||||
);
|
||||
}
|
||||
|
||||
// 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
|
||||
// fragment of its section fail the depth test against the final depth,
|
||||
// which is only nearer.
|
||||
// Each tested section'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 fragment of its
|
||||
// section fail the depth test against the final depth, which is only nearer.
|
||||
@vertex
|
||||
fn vs_box(@builtin(vertex_index) v: u32, @builtin(instance_index) t: u32) -> Box {
|
||||
let test = tests[t];
|
||||
let origin = origins[test.data.x].xyz;
|
||||
let low = test.low.xyz;
|
||||
let high = test.high.xyz;
|
||||
let origin = origins[tests[t].x].xyz;
|
||||
var out: Box;
|
||||
out.test = t;
|
||||
// The box's screen rectangle, from the corners in front of the camera.
|
||||
@@ -73,7 +60,7 @@ fn vs_box(@builtin(vertex_index) v: u32, @builtin(instance_index) t: u32) -> Box
|
||||
var hi = vec2(-1e30);
|
||||
var behind = false;
|
||||
for (var c = 0u; c < 8u; c++) {
|
||||
let p = globals.view_proj * vec4(corner(origin, low, high, c), 1.0);
|
||||
let p = globals.view_proj * vec4(corner(origin, c), 1.0);
|
||||
behind = behind || p.w <= 0.0;
|
||||
let q = p.xy / max(p.w, 1e-6);
|
||||
lo = min(lo, q);
|
||||
@@ -84,7 +71,7 @@ fn vs_box(@builtin(vertex_index) v: u32, @builtin(instance_index) t: u32) -> Box
|
||||
// clips its front faces away, and a box over a large part of the screen
|
||||
// is nearly always in sight and costly to test: both count as seen,
|
||||
// through a small triangle in the top left corner at the nearest depth.
|
||||
let near = all(origin < vec3(1.0) - low) && all(origin > vec3(-1.0) - high);
|
||||
let near = all(origin < vec3(2.0)) && all(origin > vec3(-18.0));
|
||||
if near || (!behind && size.x * size.y > params.big * 4.0) {
|
||||
var p = vec2(-1.0, 1.0);
|
||||
if v == 1u {
|
||||
@@ -95,7 +82,7 @@ fn vs_box(@builtin(vertex_index) v: u32, @builtin(instance_index) t: u32) -> Box
|
||||
out.position = select(vec4(0.0, 0.0, 0.0, 1.0), vec4(p, 1.0, 1.0), v < 3u);
|
||||
return out;
|
||||
}
|
||||
out.position = globals.view_proj * vec4(corner(origin, low, high, CORNERS[v]), 1.0);
|
||||
out.position = globals.view_proj * vec4(corner(origin, CORNERS[v]), 1.0);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -104,13 +91,10 @@ fn vs_box(@builtin(vertex_index) v: u32, @builtin(instance_index) t: u32) -> Box
|
||||
fn see(t: u32) {
|
||||
if results[t] != params.stamp {
|
||||
results[t] = params.stamp;
|
||||
let test = tests[t].data;
|
||||
for (var k = 0u; k < (test.z & 0xffffu); k++) {
|
||||
let test = tests[t];
|
||||
for (var k = 0u; k < test.z; k++) {
|
||||
phase2[(test.y + k) * 5u + 1u] = 1u;
|
||||
}
|
||||
for (var k = 0u; k < (test.z >> 16u); k++) {
|
||||
phase2[(test.w + k) * 5u + 1u] = 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,707 +0,0 @@
|
||||
//! `[displacement]` of Horizon's own shader packs (docs/api/shaders.md): near the camera the
|
||||
//! solid faces whose textures have LabPBR heights are drawn again as a grid of cells, each
|
||||
//! corner moved along the face's normal by the pack's `displacement`, while the flat quads
|
||||
//! leave those pixels to them (`native_material.rs`'s `horizon_displaced_layer`).
|
||||
//!
|
||||
//! Each frame the solid quads of the sections around the camera are copied out of the arena
|
||||
//! into one buffer, a compute pass picks the ones the grid draws (opaque, with relief, within
|
||||
//! the distance, facing the eye and in view) into an indirect draw, and the material's
|
||||
//! displaced pipeline draws one instance a quad.
|
||||
|
||||
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;
|
||||
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.
|
||||
const RELIEF_MARGIN: f32 = 0.5;
|
||||
|
||||
/// Which quads the grid takes over, for the flat terrain's pipelines (the material's and the
|
||||
/// shadow's) and the grid's: `horizon_displace_distance` and the LabPBR normal maps
|
||||
/// `horizon_pbr_normals` (group 3, binding 12), declared by the module.
|
||||
pub const RELIEF: &str = "
|
||||
// `[displacement] distance` (0 without): the flat quads leave the pixels within it on faces
|
||||
// with relief to the grid.
|
||||
override horizon_displace_distance: f32 = 0.0;
|
||||
// The mean LabPBR height of a sprite: its normal map's last mip level (1 without one).
|
||||
fn horizon_relief_mean(layer: u32) -> f32 {
|
||||
if textureDimensions(horizon_pbr_normals).x <= 1u
|
||||
|| layer >= textureNumLayers(horizon_pbr_normals) {
|
||||
return 1.0;
|
||||
}
|
||||
let top = i32(textureNumLevels(horizon_pbr_normals)) - 1;
|
||||
return textureLoad(horizon_pbr_normals, vec2(0i), i32(layer), top).a;
|
||||
}
|
||||
// The quads the grid redraws near the camera (`PICK`): opaque, on a texture whose height
|
||||
// varies.
|
||||
fn horizon_displaced_layer(layer: u32) -> bool {
|
||||
return horizon_displace_distance > 0.0 && (layer & 0x8000u) != 0u
|
||||
&& horizon_relief_mean(layer & 0x7fffu) < 0.998;
|
||||
}
|
||||
";
|
||||
|
||||
/// The grid's points (`horizon_displace_point`): one instance a quad the pick chose, its
|
||||
/// corners read from the copy of the arena, each of its `horizon_displace_cells` squared
|
||||
/// cells two triangles moved along the face's normal by the pack's `displacement`, measured
|
||||
/// from the texture's mean height so that the face's edges, pinned there, meet their
|
||||
/// neighbours whatever their relief. Needs `RELIEF`, the pack's `displacement` and
|
||||
/// `horizon_material_displace` (its vertex stage).
|
||||
pub const GRID: &str = "
|
||||
struct HorizonDisplaceSection {
|
||||
origin: vec4<f32>,
|
||||
first: u32,
|
||||
quads: u32,
|
||||
pad0: u32,
|
||||
pad1: u32,
|
||||
}
|
||||
@group(1) @binding(8) var<storage, read> horizon_displace_vertices: array<u32>;
|
||||
@group(1) @binding(9) var<storage, read> horizon_displace_picked: array<vec2<u32>>;
|
||||
@group(1) @binding(10) var<storage, read> horizon_displace_sections: array<HorizonDisplaceSection>;
|
||||
// The grid's cells along a face's side.
|
||||
override horizon_displace_cells: u32 = 16u;
|
||||
// Each cell a flat step with walls to its neighbours (`grid_vertices`), not a smooth surface.
|
||||
override horizon_displace_steps: bool = false;
|
||||
struct HorizonDisplaceCorner {
|
||||
position: vec3<f32>,
|
||||
uv: vec2<f32>,
|
||||
layer: u32,
|
||||
light: vec2<f32>,
|
||||
color: vec4<f32>,
|
||||
}
|
||||
// A corner of a quad as `TerrainVertex` holds it (24 bytes); light: sky, block.
|
||||
fn horizon_displace_corner(quad: u32, i: u32, step: f32) -> HorizonDisplaceCorner {
|
||||
let b = (quad * 4u + i) * 6u;
|
||||
var c: HorizonDisplaceCorner;
|
||||
c.position = vec3(bitcast<f32>(horizon_displace_vertices[b]),
|
||||
bitcast<f32>(horizon_displace_vertices[b + 1u]),
|
||||
bitcast<f32>(horizon_displace_vertices[b + 2u]));
|
||||
let uv = horizon_displace_vertices[b + 3u];
|
||||
c.uv = vec2<f32>(vec2(uv & 0xffffu, uv >> 16u)) * step;
|
||||
let w = horizon_displace_vertices[b + 4u];
|
||||
c.layer = w & 0xffffu;
|
||||
c.light = vec2(f32((w >> 16u) & 0xffu), f32(w >> 24u));
|
||||
c.color = unpack4x8unorm(horizon_displace_vertices[b + 5u]);
|
||||
return c;
|
||||
}
|
||||
// The height map at `uv`, filtered between the texels of mip level `lod`.
|
||||
fn horizon_displace_height(uv: vec2<f32>, layer: u32, lod: i32) -> f32 {
|
||||
let size = vec2<i32>(textureDimensions(horizon_pbr_normals, lod));
|
||||
let p = clamp(uv, vec2(0.0), vec2(1.0)) * vec2<f32>(size) - 0.5;
|
||||
let i = vec2<i32>(floor(p));
|
||||
let f = p - floor(p);
|
||||
let m = size - 1;
|
||||
let l = i32(layer);
|
||||
let a = textureLoad(horizon_pbr_normals, clamp(i, vec2(0), m), l, lod).a;
|
||||
let b = textureLoad(horizon_pbr_normals, clamp(i + vec2(1, 0), vec2(0), m), l, lod).a;
|
||||
let c = textureLoad(horizon_pbr_normals, clamp(i + vec2(0, 1), vec2(0), m), l, lod).a;
|
||||
let d = textureLoad(horizon_pbr_normals, clamp(i + vec2(1, 1), vec2(0), m), l, lod).a;
|
||||
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
||||
}
|
||||
// The height map's texel at `uv` of mip level `lod`.
|
||||
fn horizon_displace_texel(uv: vec2<f32>, layer: u32, lod: i32) -> f32 {
|
||||
let size = vec2<i32>(textureDimensions(horizon_pbr_normals, lod));
|
||||
let i = clamp(vec2<i32>(uv * vec2<f32>(size)), vec2(0), size - 1);
|
||||
return textureLoad(horizon_pbr_normals, i, i32(layer), lod).a;
|
||||
}
|
||||
struct HorizonDisplacePoint {
|
||||
// Where it is drawn, and where it lies on the flat face (both camera-relative).
|
||||
position: vec3<f32>,
|
||||
base: vec3<f32>,
|
||||
uv: vec2<f32>,
|
||||
layer: u32,
|
||||
light: vec2<f32>,
|
||||
color: vec4<f32>,
|
||||
// The face's normal, and how much of the relief the point has by its distance.
|
||||
normal: vec3<f32>,
|
||||
near: f32,
|
||||
}
|
||||
// A quad's corners, for the steps.
|
||||
struct HorizonDisplaceQuad {
|
||||
c0: HorizonDisplaceCorner,
|
||||
c1: HorizonDisplaceCorner,
|
||||
c2: HorizonDisplaceCorner,
|
||||
c3: HorizonDisplaceCorner,
|
||||
origin: vec3<f32>,
|
||||
normal: vec3<f32>,
|
||||
}
|
||||
fn horizon_displace_at(q: HorizonDisplaceQuad, s: f32, t: f32) -> vec3<f32> {
|
||||
return mix(mix(q.c0.position, q.c1.position, s), mix(q.c3.position, q.c2.position, s), t)
|
||||
+ q.origin;
|
||||
}
|
||||
fn horizon_displace_uv(q: HorizonDisplaceQuad, s: f32, t: f32) -> vec2<f32> {
|
||||
return mix(mix(q.c0.uv, q.c1.uv, s), mix(q.c3.uv, q.c2.uv, s), t);
|
||||
}
|
||||
// How far cell (`i`, `j`) sinks: nothing outside the face, whose edges keep the mean height so
|
||||
// that they meet their neighbours.
|
||||
fn horizon_displace_cell_depth(q: HorizonDisplaceQuad, i: i32, j: i32) -> f32 {
|
||||
let g = i32(horizon_displace_cells);
|
||||
if i < 0 || j < 0 || i >= g || j >= g {
|
||||
return 0.0;
|
||||
}
|
||||
let s = (f32(i) + 0.5) / f32(g);
|
||||
let t = (f32(j) + 0.5) / f32(g);
|
||||
let base = horizon_displace_at(q, s, t);
|
||||
let uv = horizon_displace_uv(q, s, t);
|
||||
let layer = q.c0.layer & 0x7fffu;
|
||||
let lods = i32(textureNumLevels(horizon_pbr_normals)) - 1;
|
||||
let size = f32(textureDimensions(horizon_pbr_normals).x);
|
||||
let lod = clamp(i32(floor(log2(size / f32(g)))), 0, lods);
|
||||
let material_id = horizon_sprite_material(layer);
|
||||
let height = horizon_displace_texel(uv, layer, lod);
|
||||
let mean = horizon_relief_mean(layer);
|
||||
let depth = displacement(DisplacementInput(base, q.normal, uv, material_id, height))
|
||||
- displacement(DisplacementInput(base, q.normal, uv, material_id, mean));
|
||||
let r = horizon_displace_distance;
|
||||
return depth * (1.0 - smoothstep(0.75 * r, r - 0.1, length(base)));
|
||||
}
|
||||
// Vertex `v` of a stepped grid: per cell, from -1 so that the first walls have a cell before
|
||||
// them, its flat top and its walls to the next cell along each side. A wall faces the way
|
||||
// out of the higher cell and takes that cell's texture.
|
||||
fn horizon_displace_step(v: u32, q: HorizonDisplaceQuad) -> HorizonDisplacePoint {
|
||||
let g = horizon_displace_cells;
|
||||
let cell = v / 18u;
|
||||
let part = (v / 6u) % 3u;
|
||||
let i = i32(cell % (g + 1u)) - 1;
|
||||
let j = i32(cell / (g + 1u)) - 1;
|
||||
let cells = f32(g);
|
||||
var k = v % 6u;
|
||||
var s = 0.0;
|
||||
var t = 0.0;
|
||||
var uv_s = 0.0;
|
||||
var uv_t = 0.0;
|
||||
var depth = 0.0;
|
||||
if part == 0u {
|
||||
if i >= 0 && j >= 0 {
|
||||
s = (f32(i) + f32((0x16u >> k) & 1u)) / cells;
|
||||
t = (f32(j) + f32((0x34u >> k) & 1u)) / cells;
|
||||
depth = horizon_displace_cell_depth(q, i, j);
|
||||
}
|
||||
uv_s = s;
|
||||
uv_t = t;
|
||||
} else if part == 1u {
|
||||
if j >= 0 {
|
||||
let here = horizon_displace_cell_depth(q, i, j);
|
||||
let next = horizon_displace_cell_depth(q, i + 1, j);
|
||||
if next < here {
|
||||
k = (0x453120u >> (k * 4u)) & 15u;
|
||||
}
|
||||
s = f32(i + 1) / cells;
|
||||
t = (f32(j) + f32((0x16u >> k) & 1u)) / cells;
|
||||
depth = select(max(here, next), min(here, next), ((0x34u >> k) & 1u) == 1u);
|
||||
uv_s = s + select(0.25, -0.25, here <= next) / cells;
|
||||
uv_t = t;
|
||||
}
|
||||
} else {
|
||||
if i >= 0 {
|
||||
let here = horizon_displace_cell_depth(q, i, j);
|
||||
let next = horizon_displace_cell_depth(q, i, j + 1);
|
||||
if here < next {
|
||||
k = (0x453120u >> (k * 4u)) & 15u;
|
||||
}
|
||||
s = (f32(i) + f32((0x16u >> k) & 1u)) / cells;
|
||||
t = f32(j + 1) / cells;
|
||||
depth = select(max(here, next), min(here, next), ((0x34u >> k) & 1u) == 1u);
|
||||
uv_s = s;
|
||||
uv_t = t + select(0.25, -0.25, here <= next) / cells;
|
||||
}
|
||||
}
|
||||
var p: HorizonDisplacePoint;
|
||||
let position = horizon_displace_at(q, s, t);
|
||||
let c = clamp(vec2(uv_s, uv_t), vec2(0.0), vec2(1.0));
|
||||
p.uv = horizon_displace_uv(q, c.x, c.y);
|
||||
p.light = mix(mix(q.c0.light, q.c1.light, s), mix(q.c3.light, q.c2.light, s), t);
|
||||
p.color = mix(mix(q.c0.color, q.c1.color, s), mix(q.c3.color, q.c2.color, s), t);
|
||||
p.layer = q.c0.layer;
|
||||
p.normal = q.normal;
|
||||
p.base = horizon_material_displace(position, p.uv, q.c0.layer, p.color,
|
||||
vec2<u32>(p.light + 0.5));
|
||||
p.near = 1.0 - smoothstep(0.75 * horizon_displace_distance, horizon_displace_distance - 0.1,
|
||||
length(p.base));
|
||||
p.position = p.base - p.normal * depth;
|
||||
return p;
|
||||
}
|
||||
// Vertex `v` of instance `instance`'s grid.
|
||||
fn horizon_displace_point(v: u32, instance: u32) -> HorizonDisplacePoint {
|
||||
let pick = horizon_displace_picked[instance];
|
||||
let section = horizon_displace_sections[pick.y];
|
||||
let c0 = horizon_displace_corner(pick.x, 0u, section.origin.w);
|
||||
let c1 = horizon_displace_corner(pick.x, 1u, section.origin.w);
|
||||
let c2 = horizon_displace_corner(pick.x, 2u, section.origin.w);
|
||||
let c3 = horizon_displace_corner(pick.x, 3u, section.origin.w);
|
||||
if horizon_displace_steps {
|
||||
return horizon_displace_step(v, HorizonDisplaceQuad(c0, c1, c2, c3, section.origin.xyz,
|
||||
normalize(cross(c1.position - c0.position, c2.position - c0.position))));
|
||||
}
|
||||
// The cell's two triangles wind as the quad's (0, 1, 2) and (0, 2, 3).
|
||||
let g = horizon_displace_cells;
|
||||
let cell = v / 6u;
|
||||
let k = v % 6u;
|
||||
let s = f32(cell % g + ((0x16u >> k) & 1u)) / f32(g);
|
||||
let t = f32(cell / g + ((0x34u >> k) & 1u)) / f32(g);
|
||||
var p: HorizonDisplacePoint;
|
||||
let position = mix(mix(c0.position, c1.position, s), mix(c3.position, c2.position, s), t);
|
||||
p.uv = mix(mix(c0.uv, c1.uv, s), mix(c3.uv, c2.uv, s), t);
|
||||
p.light = mix(mix(c0.light, c1.light, s), mix(c3.light, c2.light, s), t);
|
||||
p.color = mix(mix(c0.color, c1.color, s), mix(c3.color, c2.color, s), t);
|
||||
p.layer = c0.layer;
|
||||
p.normal = normalize(cross(c1.position - c0.position, c2.position - c0.position));
|
||||
p.base = horizon_material_displace(position + section.origin.xyz, p.uv, c0.layer, p.color,
|
||||
vec2<u32>(p.light + 0.5));
|
||||
let layer = c0.layer & 0x7fffu;
|
||||
// The heights at about a texel a cell.
|
||||
let lods = i32(textureNumLevels(horizon_pbr_normals)) - 1;
|
||||
let size = f32(textureDimensions(horizon_pbr_normals).x);
|
||||
let lod = clamp(i32(floor(log2(size / f32(g)))), 0, lods);
|
||||
let material_id = horizon_sprite_material(layer);
|
||||
let height = horizon_displace_height(p.uv, layer, lod);
|
||||
let mean = horizon_relief_mean(layer);
|
||||
let depth = displacement(DisplacementInput(p.base, p.normal, p.uv, material_id, height))
|
||||
- displacement(DisplacementInput(p.base, p.normal, p.uv, material_id, mean));
|
||||
// Pinned at the face's edges, easing out toward the distance.
|
||||
let edge = min(min(s, 1.0 - s), min(t, 1.0 - t)) * f32(g);
|
||||
let r = horizon_displace_distance;
|
||||
p.near = 1.0 - smoothstep(0.75 * r, r - 0.1, length(p.base));
|
||||
p.position = p.base - p.normal * depth * clamp(edge, 0.0, 1.0) * p.near;
|
||||
return p;
|
||||
}
|
||||
";
|
||||
|
||||
const PICK: &str = "
|
||||
struct Params {
|
||||
view_proj: mat4x4<f32>,
|
||||
distance: f32,
|
||||
sections: u32,
|
||||
margin: f32,
|
||||
capacity: u32,
|
||||
// 1 for the shadow map's list: every side, wherever the eye looks.
|
||||
shadow: u32,
|
||||
pad0: u32,
|
||||
pad1: u32,
|
||||
pad2: u32,
|
||||
}
|
||||
struct Section {
|
||||
origin: vec4<f32>,
|
||||
first: u32,
|
||||
quads: u32,
|
||||
pad0: u32,
|
||||
pad1: u32,
|
||||
}
|
||||
@group(0) @binding(0) var<uniform> params: Params;
|
||||
@group(0) @binding(1) var<storage, read> sections: array<Section>;
|
||||
@group(0) @binding(2) var<storage, read> vertices: array<u32>;
|
||||
@group(0) @binding(3) var<storage, read_write> picked: array<vec2<u32>>;
|
||||
@group(0) @binding(4) var<storage, read_write> draw: array<atomic<u32>, 4>;
|
||||
@group(0) @binding(5) var normals: texture_2d_array<f32>;
|
||||
|
||||
fn corner(quad: u32, i: u32, origin: vec3<f32>) -> vec3<f32> {
|
||||
let b = (quad * 4u + i) * 6u;
|
||||
return vec3(bitcast<f32>(vertices[b]), bitcast<f32>(vertices[b + 1u]),
|
||||
bitcast<f32>(vertices[b + 2u])) + origin;
|
||||
}
|
||||
|
||||
// Every corner of the box beyond one side of the view.
|
||||
fn outside(lo: vec3<f32>, hi: vec3<f32>) -> bool {
|
||||
var out = vec4(true);
|
||||
var behind = true;
|
||||
for (var i = 0u; i < 8u; i++) {
|
||||
let p = select(lo, hi, vec3((i & 1u) != 0u, (i & 2u) != 0u, (i & 4u) != 0u));
|
||||
let c = params.view_proj * vec4(p, 1.0);
|
||||
out = out & vec4((c.x < -c.w), (c.x > c.w), (c.y < -c.w), (c.y > c.w));
|
||||
behind = behind && c.w <= 0.0;
|
||||
}
|
||||
return any(out) || behind;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(64) fn pick(@builtin(global_invocation_id) id: vec3<u32>) {
|
||||
if id.y >= params.sections {
|
||||
return;
|
||||
}
|
||||
let section = sections[id.y];
|
||||
if id.x >= section.quads {
|
||||
return;
|
||||
}
|
||||
let quad = section.first + id.x;
|
||||
// As `horizon_displaced_layer`: opaque, with a LabPBR height that varies.
|
||||
let layer = vertices[quad * 24u + 4u] & 0xffffu;
|
||||
let l = layer & 0x7fffu;
|
||||
if (layer & 0x8000u) == 0u || textureDimensions(normals).x <= 1u
|
||||
|| l >= textureNumLayers(normals) {
|
||||
return;
|
||||
}
|
||||
let top = i32(textureNumLevels(normals)) - 1;
|
||||
if textureLoad(normals, vec2(0i), i32(l), top).a >= 0.998 {
|
||||
return;
|
||||
}
|
||||
let p0 = corner(quad, 0u, section.origin.xyz);
|
||||
let p1 = corner(quad, 1u, section.origin.xyz);
|
||||
let p2 = corner(quad, 2u, section.origin.xyz);
|
||||
let p3 = corner(quad, 3u, section.origin.xyz);
|
||||
let lo = min(min(p0, p1), min(p2, p3));
|
||||
let hi = max(max(p0, p1), max(p2, p3));
|
||||
if length(clamp(vec3(0.0), lo, hi)) >= params.distance {
|
||||
return;
|
||||
}
|
||||
// Facing away from the eye (at the origin), where the flat quad is culled too, or out of
|
||||
// its view; the shadow map takes them all, as the flat terrain draws both sides there.
|
||||
if params.shadow == 0u && (dot(cross(p1 - p0, p2 - p0), p0) >= 0.0
|
||||
|| outside(lo - params.margin, hi + params.margin)) {
|
||||
return;
|
||||
}
|
||||
let i = atomicAdd(&draw[1], 1u);
|
||||
if i >= params.capacity {
|
||||
atomicSub(&draw[1], 1u);
|
||||
return;
|
||||
}
|
||||
picked[i] = vec2(quad, id.y);
|
||||
}
|
||||
";
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Params {
|
||||
view_proj: [[f32; 4]; 4],
|
||||
distance: f32,
|
||||
sections: u32,
|
||||
margin: f32,
|
||||
capacity: u32,
|
||||
shadow: u32,
|
||||
pad: [u32; 3],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Section {
|
||||
origin: [f32; 4],
|
||||
first: u32,
|
||||
quads: u32,
|
||||
pad: [u32; 2],
|
||||
}
|
||||
|
||||
/// A section the grid may draw from this frame: its arena range of solid quads and its
|
||||
/// camera-relative origin.
|
||||
pub struct Near<'a> {
|
||||
pub buffer: &'a wgpu::Buffer,
|
||||
pub start: u64,
|
||||
pub quads: u32,
|
||||
pub origin: [f32; 3],
|
||||
}
|
||||
|
||||
/// One list of picked quads and its indirect draw.
|
||||
pub struct Picks {
|
||||
/// The grid's cells along a face's side.
|
||||
pub cells: u32,
|
||||
/// Each cell a flat step with walls.
|
||||
pub steps: bool,
|
||||
params: wgpu::Buffer,
|
||||
/// The indirect draw: the grid's vertices, then the quads picked.
|
||||
draw: wgpu::Buffer,
|
||||
pick_group: wgpu::BindGroup,
|
||||
/// Group 1 of the pipelines drawing it (bindings 8 to 10).
|
||||
pub draw_group: wgpu::BindGroup,
|
||||
}
|
||||
|
||||
pub struct Displacement {
|
||||
/// `[displacement] distance`, in blocks.
|
||||
pub distance: f32,
|
||||
vertices: wgpu::Buffer,
|
||||
sections: wgpu::Buffer,
|
||||
pick: wgpu::ComputePipeline,
|
||||
/// The quads the eye sees, and the ones the shadow map draws (when the pack has one).
|
||||
pub view: Picks,
|
||||
pub shadow: Option<Picks>,
|
||||
/// Group 1's layout for the grid's pipelines.
|
||||
pub draw_layout: wgpu::BindGroupLayout,
|
||||
/// Sections the pick ran over this frame (0: nothing to draw).
|
||||
pub drawn: usize,
|
||||
}
|
||||
|
||||
fn storage(
|
||||
binding: u32,
|
||||
read_only: bool,
|
||||
visibility: wgpu::ShaderStages,
|
||||
) -> wgpu::BindGroupLayoutEntry {
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Storage { read_only },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The shadow map's grid is coarser: its texels near the camera are a 32nd of a block.
|
||||
pub const SHADOW_CELLS: u32 = 16;
|
||||
|
||||
/// The vertices of one quad's grid: two triangles a cell, or in `steps` also two walls a cell,
|
||||
/// with a row and a column more for the walls at the face's first edges.
|
||||
fn grid_vertices(cells: u32, steps: bool) -> u32 {
|
||||
if steps {
|
||||
(cells + 1) * (cells + 1) * 18
|
||||
} else {
|
||||
cells * cells * 6
|
||||
}
|
||||
}
|
||||
|
||||
impl Displacement {
|
||||
/// The grid's buffers and pick pass for `[displacement]` with `distance` and `cells`, in
|
||||
/// `steps` or not, reading the LabPBR normal maps `normals`; with a list for the shadow map
|
||||
/// when `shadow`.
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
distance: f32,
|
||||
cells: u32,
|
||||
steps: bool,
|
||||
normals: &wgpu::TextureView,
|
||||
shadow: bool,
|
||||
) -> Self {
|
||||
let buffer = |label, size, usage| {
|
||||
device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some(label),
|
||||
size,
|
||||
usage,
|
||||
mapped_at_creation: false,
|
||||
})
|
||||
};
|
||||
use wgpu::BufferUsages as U;
|
||||
let vertices = buffer(
|
||||
"Displacement quads",
|
||||
MAX_QUADS * QUAD_BYTES,
|
||||
U::STORAGE | U::COPY_DST,
|
||||
);
|
||||
let sections = buffer(
|
||||
"Displacement sections",
|
||||
(MAX_SECTIONS * std::mem::size_of::<Section>()) as u64,
|
||||
U::STORAGE | U::COPY_DST,
|
||||
);
|
||||
let compute = wgpu::ShaderStages::COMPUTE;
|
||||
let pick_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Displacement pick"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: compute,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
storage(1, true, compute),
|
||||
storage(2, true, compute),
|
||||
storage(3, false, compute),
|
||||
storage(4, false, compute),
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 5,
|
||||
visibility: compute,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2Array,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
let vertex = wgpu::ShaderStages::VERTEX;
|
||||
let draw_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Displacement draw"),
|
||||
entries: &[
|
||||
storage(8, true, vertex),
|
||||
storage(9, true, vertex),
|
||||
storage(10, true, vertex),
|
||||
],
|
||||
});
|
||||
fn entry(binding: u32, resource: wgpu::BindingResource<'_>) -> wgpu::BindGroupEntry<'_> {
|
||||
wgpu::BindGroupEntry { binding, resource }
|
||||
}
|
||||
let picks = |cells: u32| {
|
||||
let params = buffer(
|
||||
"Displacement parameters",
|
||||
std::mem::size_of::<Params>() as u64,
|
||||
U::UNIFORM | U::COPY_DST,
|
||||
);
|
||||
let picked = buffer("Displacement picks", MAX_QUADS * 8, U::STORAGE);
|
||||
let draw = buffer(
|
||||
"Displacement draw",
|
||||
16,
|
||||
U::STORAGE | U::INDIRECT | U::COPY_DST,
|
||||
);
|
||||
let pick_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Displacement pick"),
|
||||
layout: &pick_layout,
|
||||
entries: &[
|
||||
entry(0, params.as_entire_binding()),
|
||||
entry(1, sections.as_entire_binding()),
|
||||
entry(2, vertices.as_entire_binding()),
|
||||
entry(3, picked.as_entire_binding()),
|
||||
entry(4, draw.as_entire_binding()),
|
||||
entry(5, wgpu::BindingResource::TextureView(normals)),
|
||||
],
|
||||
});
|
||||
let draw_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Displacement draw"),
|
||||
layout: &draw_layout,
|
||||
entries: &[
|
||||
entry(8, vertices.as_entire_binding()),
|
||||
entry(9, picked.as_entire_binding()),
|
||||
entry(10, sections.as_entire_binding()),
|
||||
],
|
||||
});
|
||||
Picks {
|
||||
cells,
|
||||
steps,
|
||||
params,
|
||||
draw,
|
||||
pick_group,
|
||||
draw_group,
|
||||
}
|
||||
};
|
||||
let view = picks(cells);
|
||||
// A step's flat top has to lie where its shadow was cast from, or it shadows itself in
|
||||
// grain; a smooth surface hides the difference and can take fewer cells.
|
||||
let shadow = shadow.then(|| {
|
||||
picks(if steps {
|
||||
cells
|
||||
} else {
|
||||
cells.min(SHADOW_CELLS)
|
||||
})
|
||||
});
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Displacement pick"),
|
||||
source: wgpu::ShaderSource::Wgsl(PICK.into()),
|
||||
});
|
||||
let pick = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: Some("Displacement pick"),
|
||||
layout: Some(
|
||||
&device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Displacement pick"),
|
||||
bind_group_layouts: &[Some(&pick_layout)],
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
module: &module,
|
||||
entry_point: Some("pick"),
|
||||
compilation_options: Default::default(),
|
||||
cache: None,
|
||||
});
|
||||
Self {
|
||||
distance,
|
||||
vertices,
|
||||
sections,
|
||||
pick,
|
||||
view,
|
||||
shadow,
|
||||
draw_layout,
|
||||
drawn: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies the solid quads of `near` (nearest first) and picks this frame's in `encoder`.
|
||||
pub fn prepare(
|
||||
&mut self,
|
||||
queue: &wgpu::Queue,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
near: &[Near<'_>],
|
||||
view_proj: glam::Mat4,
|
||||
) {
|
||||
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) {
|
||||
let quads = u64::from(n.quads).min(MAX_QUADS - first);
|
||||
if quads == 0 {
|
||||
break;
|
||||
}
|
||||
let stride = std::mem::size_of::<TerrainVertex>() as u64;
|
||||
encoder.copy_buffer_to_buffer(
|
||||
n.buffer,
|
||||
n.start * stride,
|
||||
&self.vertices,
|
||||
first * QUAD_BYTES,
|
||||
quads * QUAD_BYTES,
|
||||
);
|
||||
table.push(Section {
|
||||
origin: [
|
||||
n.origin[0],
|
||||
n.origin[1],
|
||||
n.origin[2],
|
||||
1.0 / crate::mesher::UV_STEPS,
|
||||
],
|
||||
first: first as u32,
|
||||
quads: quads as u32,
|
||||
pad: [0; 2],
|
||||
});
|
||||
first += quads;
|
||||
most = most.max(quads as u32);
|
||||
}
|
||||
self.drawn = table.len();
|
||||
if table.is_empty() {
|
||||
return;
|
||||
}
|
||||
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"),
|
||||
timestamp_writes: None,
|
||||
});
|
||||
pass.set_pipeline(&self.pick);
|
||||
for (picks, shadow) in lists {
|
||||
let params = Params {
|
||||
view_proj: view_proj.to_cols_array_2d(),
|
||||
distance: self.distance,
|
||||
sections: table.len() as u32,
|
||||
margin: RELIEF_MARGIN,
|
||||
capacity: MAX_QUADS as u32,
|
||||
shadow,
|
||||
pad: [0; 3],
|
||||
};
|
||||
queue.write_buffer(&picks.params, 0, bytemuck::bytes_of(¶ms));
|
||||
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), table.len() as u32, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws `picks`' grids with `pipeline`, its other groups bound.
|
||||
pub fn draw(
|
||||
&self,
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
picks: &Picks,
|
||||
pipeline: &wgpu::RenderPipeline,
|
||||
) {
|
||||
if self.drawn == 0 {
|
||||
return;
|
||||
}
|
||||
pass.set_pipeline(pipeline);
|
||||
pass.set_bind_group(1, &picks.draw_group, &[]);
|
||||
pass.draw_indirect(&picks.draw, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn a_stepped_grid_has_a_top_and_two_walls_a_cell_and_the_first_edges() {
|
||||
assert_eq!(super::grid_vertices(16, false), 16 * 16 * 6);
|
||||
assert_eq!(super::grid_vertices(16, true), 17 * 17 * 18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_pick_validates() {
|
||||
let module = naga::front::wgsl::parse_str(super::PICK)
|
||||
.unwrap_or_else(|e| panic!("{}", e.emit_to_string(super::PICK)));
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
)
|
||||
.validate(&module)
|
||||
.unwrap_or_else(|e| panic!("{}", e.emit_to_string(super::PICK)));
|
||||
}
|
||||
}
|
||||
@@ -42,9 +42,8 @@ struct Plan {
|
||||
solid_calls: usize,
|
||||
/// Where the shadow camera's solid and translucent calls begin.
|
||||
shadow_calls: [usize; 2],
|
||||
/// Where Metal's opaque pass ends and its cutout pass begins in phase 1's calls,
|
||||
/// and where its cutout pass begins in phase 2's.
|
||||
pass_calls: [usize; 3],
|
||||
/// Where Metal's opaque pass ends and its cutout pass begins in phase 1's calls.
|
||||
pass_calls: [usize; 2],
|
||||
total: u64,
|
||||
}
|
||||
const QUADS: u32 = u32::MAX;
|
||||
@@ -54,15 +53,10 @@ const GROUP_LIMIT: u32 = 65535;
|
||||
/// below it. A call names its key: its vertices come from that arena's page.
|
||||
pub const TRANSLUCENT_KEYS: u32 = 1 << 10;
|
||||
|
||||
/// terrain.wgsl's `arena_page`: the page the solid draws read their vertices from.
|
||||
pub const ARENA_BINDING: u32 = 19;
|
||||
|
||||
struct PageTable {
|
||||
/// Section id of every block of the page.
|
||||
blocks: wgpu::Buffer,
|
||||
group: Option<wgpu::BindGroup>,
|
||||
/// The section arena's page `group` binds at `ARENA_BINDING` (`bind_pages`).
|
||||
arena: Option<wgpu::Buffer>,
|
||||
}
|
||||
|
||||
/// Writes the fragment's alpha over the frame's and leaves its colour.
|
||||
@@ -87,9 +81,8 @@ pub struct Flat {
|
||||
/// The solid draws in one pass (`fs_solid`), where the GPU doesn't hide pixels
|
||||
/// by tiles (`split_solid` in `Renderer::draw_frame`: elsewhere than Metal).
|
||||
pub solid: wgpu::RenderPipeline,
|
||||
/// The solid draws in two passes, each over its own part of the draws (terrain.wgsl's
|
||||
/// `opaque_quad`): the quads with no cutout first, through a shader that never discards,
|
||||
/// then the rest.
|
||||
/// The solid draws in two passes over the same draws (terrain.wgsl's `opaque_quad`):
|
||||
/// the quads with no cutout first, through a shader that never discards, then the rest.
|
||||
pub solid_opaque: wgpu::RenderPipeline,
|
||||
pub solid_cutout: wgpu::RenderPipeline,
|
||||
/// The solid draws as the water mirror's camera draws them (`fs_solid_mirror`).
|
||||
@@ -135,9 +128,8 @@ pub struct Flat {
|
||||
pub phase1_calls: usize,
|
||||
pub solid_calls: usize,
|
||||
/// Metal's two solid passes over phase 1: the opaque one draws
|
||||
/// `calls[..pass_calls[0]]`, the cutout one `calls[pass_calls[1]..phase1_calls]`;
|
||||
/// over phase 2, `calls[phase1_calls..pass_calls[2]]` and the rest.
|
||||
pub pass_calls: [usize; 3],
|
||||
/// `calls[..pass_calls[0]]`, the cutout one `calls[pass_calls[1]..phase1_calls]`.
|
||||
pub pass_calls: [usize; 2],
|
||||
/// Where a shader pack's shadow camera's solid and translucent calls
|
||||
/// begin (`calls.len()` without them): the camera's translucent calls
|
||||
/// end at the first.
|
||||
@@ -145,10 +137,6 @@ pub struct Flat {
|
||||
/// Frames with fewer sections in view keep the per-section draws.
|
||||
pub min_sections: usize,
|
||||
range_align: u64,
|
||||
/// Phase 2's calls draw their page key as their first instance, as `draw_paged` does:
|
||||
/// the visibility buffer's ID pass (`visbuffer`) reads it. The other flat pipelines
|
||||
/// have no instance-rate inputs.
|
||||
pub page_instances: bool,
|
||||
}
|
||||
|
||||
/// Sections in view (and distant tiles) from which a frame is drawn flat.
|
||||
@@ -259,8 +247,7 @@ fn buffer(
|
||||
/// instead of a destination: the call's room holds all of its draws. Draws
|
||||
/// from `shadow[0]` (solid) and `shadow[1]` (translucent) on are a shader
|
||||
/// pack's shadow camera's, with calls of their own. Phase 1's draws before
|
||||
/// `passes[0]` and from `passes[1]` on, the two solid passes' on Metal, too,
|
||||
/// and phase 2's from `passes[2]` on (its cutout pass's).
|
||||
/// `passes[0]` and from `passes[1]` on, the two solid passes' on Metal, too.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn plan(
|
||||
draws: &[[u32; 5]],
|
||||
@@ -268,7 +255,7 @@ fn plan(
|
||||
phase1_draws: usize,
|
||||
solid_draws: usize,
|
||||
shadow: [usize; 2],
|
||||
passes: [usize; 3],
|
||||
passes: [usize; 2],
|
||||
align: u64,
|
||||
key: impl Fn(u32, Option<u32>) -> u32,
|
||||
calls: &mut Vec<Call>,
|
||||
@@ -280,7 +267,7 @@ fn plan(
|
||||
segments.clear();
|
||||
let (mut phase1_calls, mut solid_calls) = (None, None);
|
||||
let mut shadow_calls = [None; 2];
|
||||
let mut pass_calls = [None; 3];
|
||||
let mut pass_calls = [None; 2];
|
||||
let mut total = 0u64;
|
||||
let mut by_source: Vec<(Source, Vec<Range>)> = Vec::new();
|
||||
for (i, (d, &(page, sorted))) in draws.iter().zip(pages).enumerate() {
|
||||
@@ -342,22 +329,14 @@ fn plan(
|
||||
phase1_calls,
|
||||
solid_calls,
|
||||
shadow_calls: shadow_calls.map(|c| c.unwrap_or(calls.len())),
|
||||
pass_calls: {
|
||||
let [opaque, cutout, phase2] = pass_calls.map(|c| c.unwrap_or(calls.len()));
|
||||
[
|
||||
opaque.min(phase1_calls),
|
||||
cutout.min(phase1_calls),
|
||||
phase2.clamp(phase1_calls, solid_calls),
|
||||
]
|
||||
},
|
||||
pass_calls: pass_calls.map(|c| c.unwrap_or(calls.len()).min(phase1_calls)),
|
||||
total,
|
||||
}
|
||||
}
|
||||
|
||||
impl Flat {
|
||||
/// `pipeline(layout, label, vertex entry, fragment entry, blend, cull, pulled)` builds a
|
||||
/// terrain pipeline as the per-section ones, without the origin buffer, and without
|
||||
/// vertex buffers when `pulled` (its vertex shader reads the page, `ARENA_BINDING`).
|
||||
/// `pipeline(layout, label, vertex entry, fragment entry, blend, cull)` builds a
|
||||
/// terrain pipeline as the per-section ones, without the origin buffer.
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
terrain_layout: &wgpu::BindGroupLayout,
|
||||
@@ -370,18 +349,13 @@ impl Flat {
|
||||
&str,
|
||||
Option<wgpu::BlendState>,
|
||||
Option<wgpu::Face>,
|
||||
bool,
|
||||
) -> wgpu::RenderPipeline,
|
||||
) -> Self {
|
||||
let vertex = wgpu::ShaderStages::VERTEX;
|
||||
let compute = wgpu::ShaderStages::COMPUTE;
|
||||
let draw_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Flat section draws"),
|
||||
entries: &[
|
||||
storage(0, vertex, true),
|
||||
storage(1, vertex, true),
|
||||
storage(ARENA_BINDING, vertex, true),
|
||||
],
|
||||
entries: &[storage(0, vertex, true), storage(1, vertex, true)],
|
||||
});
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Flat terrain layout"),
|
||||
@@ -430,44 +404,38 @@ impl Flat {
|
||||
let storage_usage = wgpu::BufferUsages::STORAGE;
|
||||
let back = Some(wgpu::Face::Back);
|
||||
Self {
|
||||
// The solid draws read either layout of the solid quads (`mesher::packed`) from
|
||||
// the page; translucent ones keep their vertex buffer.
|
||||
solid: pipeline(
|
||||
&layout,
|
||||
"Terrain solid (flat)",
|
||||
"vs_section_flat",
|
||||
"vs_terrain_flat",
|
||||
"fs_solid",
|
||||
None,
|
||||
back,
|
||||
true,
|
||||
),
|
||||
solid_opaque: pipeline(
|
||||
&layout,
|
||||
"Terrain solid opaque (flat)",
|
||||
"vs_section_flat_opaque",
|
||||
"vs_terrain_flat_opaque",
|
||||
"fs_opaque",
|
||||
None,
|
||||
back,
|
||||
true,
|
||||
),
|
||||
solid_cutout: pipeline(
|
||||
&layout,
|
||||
"Terrain solid cutout (flat)",
|
||||
"vs_section_flat_cutout",
|
||||
"vs_terrain_flat_cutout",
|
||||
"fs_solid",
|
||||
None,
|
||||
back,
|
||||
true,
|
||||
),
|
||||
// The mirror flips the winding: its solid terrain culls the front.
|
||||
solid_mirror: pipeline(
|
||||
&layout,
|
||||
"Terrain solid mirrored (flat)",
|
||||
"vs_section_flat",
|
||||
"vs_terrain_flat",
|
||||
"fs_solid_mirror",
|
||||
None,
|
||||
Some(wgpu::Face::Front),
|
||||
true,
|
||||
),
|
||||
translucent: pipeline(
|
||||
&layout,
|
||||
@@ -476,7 +444,6 @@ impl Flat {
|
||||
"fs_translucent",
|
||||
Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
back,
|
||||
false,
|
||||
),
|
||||
translucent_mark: pipeline(
|
||||
&layout,
|
||||
@@ -485,7 +452,6 @@ impl Flat {
|
||||
"fs_translucent_mark",
|
||||
Some(ALPHA_ONLY),
|
||||
back,
|
||||
false,
|
||||
),
|
||||
flatten,
|
||||
compute_layout,
|
||||
@@ -524,12 +490,11 @@ impl Flat {
|
||||
calls: Vec::new(),
|
||||
phase1_calls: 0,
|
||||
solid_calls: 0,
|
||||
pass_calls: [0; 3],
|
||||
pass_calls: [0; 2],
|
||||
shadow_calls: [0; 2],
|
||||
min_sections,
|
||||
range_align: (u64::from(device.limits().min_storage_buffer_offset_alignment) / 16)
|
||||
.max(1),
|
||||
page_instances: false,
|
||||
}
|
||||
}
|
||||
/// A section uploaded to `vertices` vertices from `start` of the page keyed `page`:
|
||||
@@ -574,15 +539,13 @@ impl Flat {
|
||||
self.pages.resize_with(page + 1, || None);
|
||||
}
|
||||
let table = self.pages[page].get_or_insert_with(|| PageTable {
|
||||
// Copied whole into the visibility buffer's table of every page (`visbuffer`).
|
||||
blocks: buffer(
|
||||
device,
|
||||
"Section ids by block",
|
||||
blocks * 4,
|
||||
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
||||
wgpu::BufferUsages::STORAGE,
|
||||
),
|
||||
group: None,
|
||||
arena: None,
|
||||
});
|
||||
let first = start / GRAIN;
|
||||
let ids = vec![id; vertices.div_ceil(GRAIN) as usize];
|
||||
@@ -643,7 +606,7 @@ impl Flat {
|
||||
phase1_draws: usize,
|
||||
solid_draws: usize,
|
||||
shadow: [usize; 2],
|
||||
passes: [usize; 3],
|
||||
passes: [usize; 2],
|
||||
phase2: Option<&wgpu::Buffer>,
|
||||
translucent_arena: bool,
|
||||
origins: impl Iterator<Item = (u32, [f32; 4])>,
|
||||
@@ -717,7 +680,7 @@ impl Flat {
|
||||
self.phase1_calls = 0;
|
||||
self.solid_calls = 0;
|
||||
self.shadow_calls = [0; 2];
|
||||
self.pass_calls = [0; 3];
|
||||
self.pass_calls = [0; 2];
|
||||
return false;
|
||||
}
|
||||
if self.range_data.len() as u64 > self.range_capacity {
|
||||
@@ -746,11 +709,10 @@ impl Flat {
|
||||
);
|
||||
// Phase 2's calls start empty, each at the start of its room.
|
||||
self.phase2_call_data.clear();
|
||||
let page_instances = self.page_instances;
|
||||
self.phase2_call_data.extend(
|
||||
self.calls[phase1_calls..solid_calls]
|
||||
.iter()
|
||||
.map(|c| [0, 1, c.first, 0, if page_instances { c.page } else { 0 }]),
|
||||
.map(|c| [0, 1, c.first, 0, 0]),
|
||||
);
|
||||
let phase2_calls = self.phase2_call_data.len() as u64;
|
||||
if phase2_calls > self.phase2_call_capacity {
|
||||
@@ -845,52 +807,28 @@ impl Flat {
|
||||
pass.dispatch_workgroups((*count).min(GROUP_LIMIT), count.div_ceil(GROUP_LIMIT), 1);
|
||||
}
|
||||
drop(pass);
|
||||
true
|
||||
}
|
||||
/// Gives every page table its bind group (`bind_page`): its blocks, the origins and, for
|
||||
/// a page of the section arena, that page (`arena`, by page) for the solid draws to read.
|
||||
/// Before each frame's draws, flat or not: the per-section solid draws bind the groups too
|
||||
/// (`page_group`), and a page that grew is another buffer.
|
||||
pub fn bind_pages<'a>(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
arena: impl Fn(u32) -> Option<&'a wgpu::Buffer>,
|
||||
) {
|
||||
for (key, slot) in self.pages.iter_mut().enumerate() {
|
||||
let Some(table) = slot else {
|
||||
continue;
|
||||
};
|
||||
let page = (key < TRANSLUCENT_KEYS as usize)
|
||||
.then(|| arena(key as u32))
|
||||
.flatten();
|
||||
if table.group.is_some() && table.arena.as_ref() == page {
|
||||
continue;
|
||||
for page in self.calls.iter().map(|c| c.page) {
|
||||
let table = self.pages[page as usize]
|
||||
.as_mut()
|
||||
.expect("a drawn page has its block table");
|
||||
if table.group.is_none() {
|
||||
table.group = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Flat section draws"),
|
||||
layout: &self.draw_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: table.blocks.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: self.origins.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
table.arena = page.cloned();
|
||||
table.group = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Flat section draws"),
|
||||
layout: &self.draw_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: table.blocks.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: self.origins.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: ARENA_BINDING,
|
||||
resource: page.unwrap_or(&self.empty).as_entire_binding(),
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
}
|
||||
/// The bind group of the section arena's page `page` (`bind_pages`), which the solid
|
||||
/// draws read their vertices through.
|
||||
pub fn page_group(&self, page: u32) -> Option<&wgpu::BindGroup> {
|
||||
self.pages.get(page as usize)?.as_ref()?.group.as_ref()
|
||||
true
|
||||
}
|
||||
/// After occlusion culling's test: copies the phase 2 draws it saw.
|
||||
pub fn flatten_phase2(
|
||||
@@ -941,46 +879,6 @@ impl Flat {
|
||||
pass.draw_indexed(call.first..call.first + call.count, 0, 0..1);
|
||||
}
|
||||
}
|
||||
/// `draw`, each call as the instance its page key names: the visibility buffer's ID
|
||||
/// pass (`visbuffer`) tells the pages apart by it.
|
||||
pub(super) fn draw_paged<'a>(
|
||||
&self,
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
calls: &[Call],
|
||||
vertices: impl Fn(u32) -> wgpu::BufferSlice<'a>,
|
||||
) {
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
for call in calls {
|
||||
self.bind_page(pass, call.page, &vertices);
|
||||
pass.draw_indexed(
|
||||
call.first..call.first + call.count,
|
||||
0,
|
||||
call.page..call.page + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
/// Bytes of its buffers: the index list, ranges, origins and block tables.
|
||||
pub fn bytes(&self) -> u64 {
|
||||
let tables: u64 = self.pages.iter().flatten().map(|t| t.blocks.size()).sum();
|
||||
self.indices.size()
|
||||
+ self.ranges.size()
|
||||
+ self.origins.size()
|
||||
+ self.phase2_calls.size()
|
||||
+ tables
|
||||
}
|
||||
/// The origins by section id, which the visibility buffer's resolve (`visbuffer`) reads
|
||||
/// in its fragment shader.
|
||||
pub(super) fn origins(&self) -> &wgpu::Buffer {
|
||||
&self.origins
|
||||
}
|
||||
/// The block table of the section arena's page `page` (`blocks_per_page` entries), which
|
||||
/// the visibility buffer copies into its table of every page.
|
||||
pub(super) fn block_table(&self, page: u32) -> Option<&wgpu::Buffer> {
|
||||
Some(&self.pages.get(page as usize)?.as_ref()?.blocks)
|
||||
}
|
||||
pub(super) fn blocks_per_page(&self) -> u64 {
|
||||
self.blocks_per_page
|
||||
}
|
||||
/// Draws `calls` without their index buffer, `instances` times: a shader pack's geometry
|
||||
/// stage reads the indices itself (`hz_indices`) and stands for one emitted triangle per
|
||||
/// instance.
|
||||
@@ -998,22 +896,19 @@ impl Flat {
|
||||
pass.draw(call.first..call.first + call.count, 0..instances);
|
||||
}
|
||||
}
|
||||
/// Draws phase 2's `calls` (`calls[phase1_calls..solid_calls]` counted from 0), as
|
||||
/// `flatten_phase2` left them.
|
||||
/// Draws phase 2's calls, as `flatten_phase2` left them.
|
||||
pub fn draw_phase2<'a>(
|
||||
&self,
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
calls: std::ops::Range<usize>,
|
||||
vertices: impl Fn(u32) -> wgpu::BufferSlice<'a>,
|
||||
) {
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
let first = self.phase1_calls + calls.start;
|
||||
for (k, call) in self.calls[first..self.phase1_calls + calls.end]
|
||||
for (k, call) in self.calls[self.phase1_calls..self.solid_calls]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
self.bind_page(pass, call.page, &vertices);
|
||||
pass.draw_indexed_indirect(&self.phase2_calls, (calls.start + k) as u64 * 20);
|
||||
pass.draw_indexed_indirect(&self.phase2_calls, k as u64 * 20);
|
||||
}
|
||||
}
|
||||
fn bind_page<'a>(
|
||||
@@ -1073,7 +968,7 @@ mod tests {
|
||||
solid,
|
||||
solid,
|
||||
[draws.len(); 2],
|
||||
[draws.len(), 0, solid],
|
||||
[draws.len(), 0],
|
||||
4,
|
||||
|page, _| page,
|
||||
&mut calls,
|
||||
@@ -1122,14 +1017,14 @@ mod tests {
|
||||
solid,
|
||||
solid,
|
||||
[draws.len(); 2],
|
||||
[3, 1, solid],
|
||||
[3, 1],
|
||||
4,
|
||||
|page, _| page,
|
||||
&mut calls,
|
||||
&mut ranges,
|
||||
&mut segments,
|
||||
);
|
||||
assert_eq!((plan.pass_calls, plan.solid_calls), ([3, 1, 4], 4));
|
||||
assert_eq!((plan.pass_calls, plan.solid_calls), ([3, 1], 4));
|
||||
let call = |page, first, count| Call { page, first, count };
|
||||
assert_eq!(
|
||||
calls,
|
||||
@@ -1158,7 +1053,7 @@ mod tests {
|
||||
solid,
|
||||
solid,
|
||||
[camera, camera + 2],
|
||||
[camera, 0, solid],
|
||||
[camera, 0],
|
||||
4,
|
||||
|page, _| page,
|
||||
&mut calls,
|
||||
@@ -1187,7 +1082,7 @@ mod tests {
|
||||
2,
|
||||
solid,
|
||||
[draws.len(); 2],
|
||||
[draws.len(), 0, solid],
|
||||
[draws.len(), 0],
|
||||
4,
|
||||
|page, _| page,
|
||||
&mut calls,
|
||||
@@ -1200,7 +1095,7 @@ mod tests {
|
||||
phase1_calls: 1,
|
||||
solid_calls: 3,
|
||||
shadow_calls: [4, 4],
|
||||
pass_calls: [1, 0, 3],
|
||||
pass_calls: [1, 0],
|
||||
total: 6 * 13
|
||||
}
|
||||
);
|
||||
@@ -1305,7 +1200,7 @@ mod tests {
|
||||
phase1,
|
||||
solid,
|
||||
[draws.len(); 2],
|
||||
[draws.len(), 0, solid],
|
||||
[draws.len(), 0],
|
||||
align,
|
||||
|page, _| page,
|
||||
&mut calls,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,42 +1,30 @@
|
||||
// GPU culling (gfx/gpu_culling.rs). `apply` writes the records that changed.
|
||||
// `cull`: one invocation per section id keeps the sections whose column is
|
||||
// within the render distance, that cave culling's graph reaches (occlusion.rs)
|
||||
// and whose box is in the frustum, writes each one's origin and counts it in
|
||||
// its page, per block of 64 ids. `scan`: one workgroup per page turns the
|
||||
// blocks' counts into where each block's draws start, and the page's count.
|
||||
// `scatter`: each block writes its sections' draws there, in id order, so a
|
||||
// page's draws come in the same order every frame (atomics alone would order
|
||||
// them as the GPU schedules, and where faces of two sections meet at the same
|
||||
// depth the later draw keeps the pixel). Nothing here computes with floats: the
|
||||
// origins are the CPU's bits, copied, and the frustum test adds integers the
|
||||
// CPU computed.
|
||||
// `cull`: one invocation per section id keeps the sections the CPU's walk
|
||||
// chose, writes each one's origin and counts it in its page, per block of 64
|
||||
// ids. `scan`: one workgroup per page turns the blocks' counts into where
|
||||
// each block's draws start, and the page's count. `scatter`: each block
|
||||
// writes its sections' draws there, in id order, so a page's draws come in
|
||||
// the same order every frame (atomics alone would order them as the GPU
|
||||
// schedules, and where faces of two sections meet at the same depth the later
|
||||
// draw keeps the pixel). Nothing here computes with floats: the origins are
|
||||
// the CPU's bits, copied.
|
||||
|
||||
struct Params {
|
||||
// Ids to test, records `apply` writes, the arena pages the counts and
|
||||
// regions cover, and where the regions start in `tables`.
|
||||
// regions cover, and where the regions and the chosen ids' bits start in
|
||||
// `tables`.
|
||||
ids: u32,
|
||||
updates: u32,
|
||||
pages: u32,
|
||||
regions: u32,
|
||||
// Bits 3j, 3j + 1 and 3j + 2: frustum plane j's x, y and z are at least 0.
|
||||
signs: u32,
|
||||
// The render distance in columns, and the camera's column.
|
||||
near: i32,
|
||||
cam_x: i32,
|
||||
cam_z: i32,
|
||||
mask: u32,
|
||||
pad0: u32,
|
||||
pad1: u32,
|
||||
pad2: u32,
|
||||
// The tables' first column: x, 0, z, 0.
|
||||
first: vec4<i32>,
|
||||
// Their entries along x, y and z, and 0.
|
||||
cells: vec4<u32>,
|
||||
// Each frustum plane's w term, its margin in, then 0, 0.
|
||||
w: array<vec4<i32>, 2>,
|
||||
// Cave culling's graph (occlusion.rs `Reach`): from x, from z, radius, lowest
|
||||
// section y; its height in sections and the level's; REACH and SKY; 0.
|
||||
reach: vec4<i32>,
|
||||
reach_height: i32,
|
||||
reach_sections: i32,
|
||||
flags: u32,
|
||||
pad: u32,
|
||||
}
|
||||
|
||||
// gpu_culling.rs `Record`.
|
||||
@@ -44,8 +32,8 @@ struct Section {
|
||||
// Section x, y (from the level's bottom), z, and its arena page (-1: a
|
||||
// free id).
|
||||
key: vec4<i32>,
|
||||
// The first vertex of its solid quads, its solid quads, its box
|
||||
// (gpu_culling.rs `rounded_box`), then a word unused here.
|
||||
// The first vertex of its solid quads, its solid quads, then two words
|
||||
// unused here.
|
||||
a: vec4<u32>,
|
||||
}
|
||||
|
||||
@@ -57,10 +45,9 @@ struct Update {
|
||||
|
||||
@group(0) @binding(0) var<uniform> params: Params;
|
||||
@group(0) @binding(1) var<storage, read_write> sections: array<Section>;
|
||||
// gpu_culling.rs `tables`: the origins' f32 bits along x, y and z (n of them);
|
||||
// per frustum plane its terms for those origins, n each; per plane and axis its
|
||||
// terms for the 32 box codes; then each page's region of `draws` (its first
|
||||
// draw and its size).
|
||||
// gpu_culling.rs `GpuCulling::prepare`: the origins' f32 bits along x, y and
|
||||
// z, each page's region of `draws` (its first draw and its size), then a bit
|
||||
// per id the CPU's walk chose.
|
||||
@group(0) @binding(2) var<storage, read> tables: array<u32>;
|
||||
@group(0) @binding(3) var<storage, read> updates: array<Update>;
|
||||
// Sections kept, draws, each page's draws, then per block of ids and page its
|
||||
@@ -72,9 +59,6 @@ struct Update {
|
||||
@group(0) @binding(6) var<storage, read_write> origins: array<vec4<u32>>;
|
||||
// Per id its page + 1 when it is drawn this frame, else 0.
|
||||
@group(0) @binding(7) var<storage, read_write> runs: array<u32>;
|
||||
// occlusion.rs `Reach::bits`: whether the search reached each cell of its box,
|
||||
// a bit each (32 a word, lowest first).
|
||||
@group(0) @binding(8) var<storage, read> reached_cells: array<u32>;
|
||||
|
||||
// gfx.rs `section_instance`'s w for a whole section: 1 / mesher::UV_STEPS.
|
||||
const STEP_BITS: u32 = 0x38000000u;
|
||||
@@ -84,13 +68,6 @@ const ROW: u32 = 65535u;
|
||||
const BLOCK: u32 = 64u;
|
||||
// Counts before the pages'.
|
||||
const STATS: u32 = 2u;
|
||||
// gpu_culling.rs `rounded_box` for a box past what its codes hold: no plane
|
||||
// rejects it.
|
||||
const UNBOUNDED: u32 = 0x80000000u;
|
||||
// In `params.flags`: cave culling's graph is in `params.reach`, and its search
|
||||
// reached the box's top layer (the sky band).
|
||||
const REACH: u32 = 1u;
|
||||
const SKY: u32 = 2u;
|
||||
|
||||
// The count of `block`'s draws in `page` (`cull`), then where they start (`scan`).
|
||||
fn block_count(block: u32, page: u32) -> u32 {
|
||||
@@ -103,69 +80,9 @@ fn region(page: u32) -> vec2<u32> {
|
||||
return vec2<u32>(tables[at], tables[at + 1u]);
|
||||
}
|
||||
|
||||
// streaming.rs `in_view_distance` around the camera's column. A gap at or past
|
||||
// the distance cannot pass, so testing it first keeps the squares in range.
|
||||
fn near(x: i32, z: i32) -> bool {
|
||||
let r = params.near;
|
||||
let gx = max(abs(x - params.cam_x) - 1, 0);
|
||||
let gz = max(abs(z - params.cam_z) - 1, 0);
|
||||
if gx >= r || gz >= r {
|
||||
return false;
|
||||
}
|
||||
return gx * gx + gz * gz < r * r;
|
||||
}
|
||||
|
||||
// occlusion.rs `Occlusion::contains` (`Graph::contains`): with no graph, or with
|
||||
// cave culling off, every section; in the searched box, whether the search
|
||||
// reached it; above a clamped box, over its columns and within the level, the
|
||||
// sky band (all of it or none); else none. `Reach::contains` on the CPU.
|
||||
fn reached(key: vec3<i32>) -> bool {
|
||||
if (params.flags & REACH) == 0u {
|
||||
return true;
|
||||
}
|
||||
let r = params.reach.z;
|
||||
let dx = key.x - params.reach.x;
|
||||
let dz = key.z - params.reach.y;
|
||||
let dy = key.y - params.reach.w;
|
||||
if dx < -r || dx > r || dz < -r || dz > r || dy < 0 {
|
||||
return false;
|
||||
}
|
||||
if dy >= params.reach_height {
|
||||
return (params.flags & SKY) != 0u && key.y >= 0 && key.y <= params.reach_sections;
|
||||
}
|
||||
let i = u32(((dz + r) * (2 * r + 1) + dx + r) * params.reach_height + dy);
|
||||
return ((reached_cells[i >> 5u] >> (i & 31u)) & 1u) != 0u;
|
||||
}
|
||||
|
||||
fn term(at: u32) -> i32 {
|
||||
return bitcast<i32>(tables[at]);
|
||||
}
|
||||
|
||||
// gfx.rs `Frustum::intersects` for the box `b` of the section in cell `c` of the
|
||||
// tables, in integers (gpu_culling.rs `tables`): it keeps every box the CPU keeps.
|
||||
fn in_frustum(c: vec3<u32>, b: u32) -> bool {
|
||||
if (b & UNBOUNDED) != 0u {
|
||||
return true;
|
||||
}
|
||||
let nx = params.cells.x;
|
||||
let ny = params.cells.y;
|
||||
let n = nx + ny + params.cells.z;
|
||||
for (var j = 0u; j < 6u; j++) {
|
||||
// The box's codes at the corner the plane looks at.
|
||||
let s = params.signs >> (3u * j);
|
||||
let kx = (b >> select(0u, 15u, (s & 1u) != 0u)) & 31u;
|
||||
let ky = (b >> select(5u, 20u, (s & 2u) != 0u)) & 31u;
|
||||
let kz = (b >> select(10u, 25u, (s & 4u) != 0u)) & 31u;
|
||||
let origin_terms = n + j * n;
|
||||
let code_terms = 7u * n + 96u * j;
|
||||
let sum = params.w[j / 4u][j % 4u] + term(origin_terms + c.x)
|
||||
+ term(origin_terms + nx + c.y) + term(origin_terms + nx + ny + c.z)
|
||||
+ term(code_terms + kx) + term(code_terms + 32u + ky) + term(code_terms + 64u + kz);
|
||||
if sum < 0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
// The CPU's walk chose it (gpu_culling.rs `GpuCulling::choose`).
|
||||
fn chosen(id: u32) -> bool {
|
||||
return ((tables[params.mask + id / 32u] >> (id % 32u)) & 1u) != 0u;
|
||||
}
|
||||
|
||||
// One invocation per changed record; the CPU lists each id once.
|
||||
@@ -180,20 +97,19 @@ fn apply(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
@compute @workgroup_size(64)
|
||||
fn cull(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let id = gid.y * (ROW * BLOCK) + gid.x;
|
||||
// Past the table's end the records are stale: the CPU never writes them free.
|
||||
if id >= params.ids {
|
||||
return;
|
||||
}
|
||||
runs[id] = 0u;
|
||||
let s = sections[id];
|
||||
if s.key.w < 0 || s.a.y == 0u || !near(s.key.x, s.key.z) || !reached(s.key.xyz) {
|
||||
if s.key.w < 0 || s.a.y == 0u || !chosen(id) {
|
||||
return;
|
||||
}
|
||||
let page = u32(s.key.w);
|
||||
// The tables hold every column within the distance; a key before their
|
||||
// first wraps past their end.
|
||||
let c = vec3<u32>(s.key.xyz - params.first.xyz);
|
||||
if page >= params.pages || any(c >= params.cells.xyz) || !in_frustum(c, s.a.z) {
|
||||
if page >= params.pages || any(c >= params.cells.xyz) {
|
||||
return;
|
||||
}
|
||||
let nx = params.cells.x;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
//! HDR output (Horizon setting, off by default): the frame renders exactly as
|
||||
//! without it, in vanilla's gamma space, into a 16-bit target when HDR output
|
||||
//! was on when the renderer was made, else an 8-bit one
|
||||
//! (`Renderer::frame_format`), and one pass (`hdr.wgsl`) writes it to an HDR10
|
||||
//! or scRGB swapchain at the player's paper white, expanding the world's
|
||||
//! highlights towards their peak brightness. The world is copied aside before
|
||||
//! the hand and HUD so only it is expanded.
|
||||
//! without it, into an 8-bit target in vanilla's gamma space, and one pass
|
||||
//! (`hdr.wgsl`) writes it to an HDR10 or scRGB swapchain at the player's paper
|
||||
//! white, expanding the world's highlights towards their peak brightness. The
|
||||
//! world is copied aside before the hand and HUD so only it is expanded.
|
||||
|
||||
/// The swapchain HDR output presents to.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
@@ -303,7 +301,7 @@ impl Hdr {
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame every pass draws into this frame, sized like the window.
|
||||
/// The 8-bit frame every pass draws into this frame, sized like the window.
|
||||
pub fn frame(&mut self, device: &wgpu::Device, size: [u32; 2]) -> wgpu::Texture {
|
||||
if self.targets.as_ref().is_none_or(|t| t.size != size) {
|
||||
let texture = |label, usage| {
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
//! Every render pipeline's stage interface against the weakest GPUs' limit (#8195).
|
||||
//!
|
||||
//! OpenGL 3.3 drivers (a GeForce 9100M G) allow `max_inter_stage_shader_variables` = 15, and wgpu
|
||||
//! counts the fragment stage's built-in inputs (`front_facing`, `sample_index`, ...) against it.
|
||||
//! A pipeline over that limit panics at start-up on such a GPU, with no way to run the game, so
|
||||
//! this checks the shaders with naga, without a GPU.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// `Limits::downlevel_defaults().max_inter_stage_shader_variables`, the least a GPU offers.
|
||||
const LIMIT: u32 = 15;
|
||||
|
||||
/// One entry point's variables: the user-defined `@location`s and the built-ins wgpu counts.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct Interface {
|
||||
entry: String,
|
||||
/// Vertex outputs or fragment inputs, user-defined.
|
||||
user: u32,
|
||||
/// The fragment stage's `front_facing`, `sample_index`, `sample_mask` and `primitive_index`
|
||||
/// inputs, which wgpu counts as one variable each; 0 for a vertex stage.
|
||||
built_ins: u32,
|
||||
}
|
||||
|
||||
impl Interface {
|
||||
fn total(&self) -> u32 {
|
||||
self.user + self.built_ins
|
||||
}
|
||||
}
|
||||
|
||||
/// The bindings an argument or a result carries, a struct's members flattened.
|
||||
fn bindings(
|
||||
module: &naga::Module,
|
||||
binding: &Option<naga::Binding>,
|
||||
ty: naga::Handle<naga::Type>,
|
||||
) -> Vec<naga::Binding> {
|
||||
if let Some(binding) = binding {
|
||||
return vec![binding.clone()];
|
||||
}
|
||||
match &module.types[ty].inner {
|
||||
naga::TypeInner::Struct { members, .. } => {
|
||||
members.iter().filter_map(|m| m.binding.clone()).collect()
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_location(binding: &naga::Binding) -> bool {
|
||||
matches!(binding, naga::Binding::Location { .. })
|
||||
}
|
||||
|
||||
/// The stage interface of every vertex and fragment entry point of `source`.
|
||||
fn interfaces(source: &str) -> Vec<Interface> {
|
||||
let module = naga::front::wgsl::parse_str(source).unwrap();
|
||||
let mut out = Vec::new();
|
||||
for entry in &module.entry_points {
|
||||
let function = &entry.function;
|
||||
let (user, built_ins) = match entry.stage {
|
||||
naga::ShaderStage::Vertex => {
|
||||
let outputs: Vec<_> = function
|
||||
.result
|
||||
.iter()
|
||||
.flat_map(|r| bindings(&module, &r.binding, r.ty))
|
||||
.collect();
|
||||
(outputs.iter().filter(|b| is_location(b)).count(), 0)
|
||||
}
|
||||
naga::ShaderStage::Fragment => {
|
||||
let inputs: Vec<_> = function
|
||||
.arguments
|
||||
.iter()
|
||||
.flat_map(|a| bindings(&module, &a.binding, a.ty))
|
||||
.collect();
|
||||
let built_ins = inputs
|
||||
.iter()
|
||||
.filter(|b| {
|
||||
matches!(
|
||||
b,
|
||||
naga::Binding::BuiltIn(
|
||||
naga::BuiltIn::FrontFacing
|
||||
| naga::BuiltIn::SampleIndex
|
||||
| naga::BuiltIn::SampleMask
|
||||
| naga::BuiltIn::PrimitiveIndex
|
||||
)
|
||||
)
|
||||
})
|
||||
.count();
|
||||
(inputs.iter().filter(|b| is_location(b)).count(), built_ins)
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
out.push(Interface {
|
||||
entry: entry.name.clone(),
|
||||
user: user as u32,
|
||||
built_ins: built_ins as u32,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The shaders as the renderer assembles them, by the `.wgsl` files each is made of.
|
||||
fn composed() -> Vec<(&'static str, Vec<&'static str>, String)> {
|
||||
let entity = crate::overlay::wgsl_constants() + include_str!("../entity.wgsl");
|
||||
vec![
|
||||
("entity", vec!["entity.wgsl"], entity.clone()),
|
||||
(
|
||||
"static entity",
|
||||
vec!["entity.wgsl", "static_entity.wgsl"],
|
||||
entity.clone() + include_str!("../static_entity.wgsl"),
|
||||
),
|
||||
(
|
||||
"improved transparency clouds",
|
||||
vec!["entity.wgsl", "oit.wgsl", "oit_clouds.wgsl"],
|
||||
entity.clone() + include_str!("../oit.wgsl") + include_str!("../oit_clouds.wgsl"),
|
||||
),
|
||||
(
|
||||
"improved transparency features",
|
||||
vec!["entity.wgsl", "oit.wgsl", "oit_features.wgsl"],
|
||||
entity + include_str!("../oit.wgsl") + include_str!("../oit_features.wgsl"),
|
||||
),
|
||||
(
|
||||
"improved transparency world",
|
||||
vec!["terrain.wgsl", "oit.wgsl", "oit_world.wgsl"],
|
||||
include_str!("../terrain.wgsl").to_owned()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_world.wgsl"),
|
||||
),
|
||||
(
|
||||
"visibility buffer",
|
||||
vec!["terrain.wgsl", "visbuffer.wgsl"],
|
||||
include_str!("../terrain.wgsl").to_owned() + include_str!("../visbuffer.wgsl"),
|
||||
),
|
||||
(
|
||||
"improved transparency world border",
|
||||
vec!["border.wgsl", "oit.wgsl", "oit_border.wgsl"],
|
||||
include_str!("../border.wgsl").to_owned()
|
||||
+ include_str!("../oit.wgsl")
|
||||
+ include_str!("../oit_border.wgsl"),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn wgsl_files(dir: &Path, out: &mut Vec<PathBuf>) {
|
||||
for entry in std::fs::read_dir(dir).unwrap().flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
wgsl_files(&path, out);
|
||||
} else if path.extension().is_some_and(|e| e == "wgsl") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every shader: the assembled ones, and each `.wgsl` file that stands alone. A file that does
|
||||
/// not (it needs another's declarations) must be part of an assembled one, so that a new shader
|
||||
/// cannot slip past the audit.
|
||||
fn audited() -> Vec<(String, Vec<Interface>)> {
|
||||
let composed = composed();
|
||||
let mut files = Vec::new();
|
||||
wgsl_files(
|
||||
&Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
|
||||
&mut files,
|
||||
);
|
||||
files.sort();
|
||||
let mut out: Vec<(String, Vec<Interface>)> = composed
|
||||
.iter()
|
||||
.map(|(name, _, source)| ((*name).to_owned(), interfaces(source)))
|
||||
.collect();
|
||||
for file in files {
|
||||
let name = file.file_name().unwrap().to_string_lossy().into_owned();
|
||||
let source = std::fs::read_to_string(&file).unwrap();
|
||||
let alone = naga::front::wgsl::parse_str(&source).is_ok();
|
||||
let part = composed.iter().any(|(_, parts, _)| parts.contains(&&*name));
|
||||
assert!(
|
||||
alone || part,
|
||||
"{name} needs other files' declarations: add its assembly to `composed`"
|
||||
);
|
||||
if alone {
|
||||
out.push((name, interfaces(&source)));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_pipeline_exceeds_the_weakest_gpus_inter_stage_limit() {
|
||||
let mut over = Vec::new();
|
||||
for (shader, entries) in audited() {
|
||||
for e in entries {
|
||||
if e.total() > LIMIT {
|
||||
over.push(format!(
|
||||
"{shader}::{}: {} user-defined + {} built-in = {} > {LIMIT}",
|
||||
e.entry,
|
||||
e.user,
|
||||
e.built_ins,
|
||||
e.total()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(over.is_empty(), "over the limit:\n{}", over.join("\n"));
|
||||
}
|
||||
|
||||
/// The audit counts what wgpu counts (the failure of #8195: 15 user-defined + `FrontFacing`).
|
||||
#[test]
|
||||
fn a_front_facing_input_counts_against_the_limit() {
|
||||
let source = "struct Io { @builtin(position) p: vec4<f32>, @location(0) a: vec4<f32>, @location(1) b: f32 }
|
||||
@vertex fn vs() -> Io { return Io(vec4<f32>(0.0), vec4<f32>(0.0), 0.0); }
|
||||
@fragment fn fs(in: Io, @builtin(front_facing) f: bool) -> @location(0) vec4<f32> { return in.a; }";
|
||||
assert_eq!(
|
||||
interfaces(source),
|
||||
vec![
|
||||
Interface {
|
||||
entry: "vs".into(),
|
||||
user: 2,
|
||||
built_ins: 0
|
||||
},
|
||||
Interface {
|
||||
entry: "fs".into(),
|
||||
user: 2,
|
||||
built_ins: 1
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -63,9 +63,6 @@ pub(super) struct Tile {
|
||||
/// place until they have faded in (from the job's end, the instant), so
|
||||
/// no hole shows through them.
|
||||
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,
|
||||
}
|
||||
|
||||
/// How long a tile takes to fade in over the ones it replaces.
|
||||
@@ -163,18 +160,6 @@ impl Tile {
|
||||
None => &mut self.mesh,
|
||||
}
|
||||
}
|
||||
/// The box occlusion culling tests (`cull::Cull`), around its origin: its
|
||||
/// mesh's bounds grown by a block, while that mesh is the one drawn
|
||||
/// whole (no stand-in, no fade). A fading tile's dither moves its depth
|
||||
/// toward the camera (terrain.wgsl's `fade_depth`), out of the box.
|
||||
pub fn cull_bounds(&self, fade: u32) -> Option<[[f32; 3]; 2]> {
|
||||
(fade == 0 && self.previous.is_none()).then(|| {
|
||||
[
|
||||
(self.bounds.min - 1.0).to_array(),
|
||||
(self.bounds.max + 1.0).to_array(),
|
||||
]
|
||||
})
|
||||
}
|
||||
/// 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_mut().map(|(mesh, _)| mesh))
|
||||
@@ -207,10 +192,6 @@ impl Renderer {
|
||||
/// its old mesh stands in while they fade in. A new one waits for the rest of its job
|
||||
/// (`upload_lods`), then fades in.
|
||||
pub fn upload_lod(&mut self, key: TileKey, mesh: &SectionMesh, keep: bool) {
|
||||
if let Some(copies) = &mut self.capture_copies {
|
||||
copies.tile(key, mesh);
|
||||
}
|
||||
let old_slot = self.lods.get(&key).map(|tile| tile.cull_slot);
|
||||
let (shown, previous) = match self.lods.remove(&key) {
|
||||
Some(old) if old.dying.is_none() => match old.previous {
|
||||
Some(previous) => {
|
||||
@@ -231,12 +212,6 @@ impl Renderer {
|
||||
};
|
||||
match Bounds::mesh(mesh).zip(self.upload_mesh(mesh)) {
|
||||
Some((bounds, mesh)) => {
|
||||
let cull_slot = old_slot.unwrap_or_else(|| {
|
||||
self.lod_cull_free.pop().unwrap_or_else(|| {
|
||||
self.lod_cull_next += 1;
|
||||
self.lod_cull_next - 1
|
||||
})
|
||||
});
|
||||
self.lods.insert(
|
||||
key,
|
||||
Tile {
|
||||
@@ -246,7 +221,6 @@ impl Renderer {
|
||||
dying: None,
|
||||
alone: false,
|
||||
previous,
|
||||
cull_slot,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -254,7 +228,6 @@ impl Renderer {
|
||||
if let Some((previous, _)) = previous {
|
||||
self.release_mesh(previous);
|
||||
}
|
||||
self.lod_cull_free.extend(old_slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -267,19 +240,12 @@ impl Renderer {
|
||||
}
|
||||
|
||||
pub fn remove_lod(&mut self, key: TileKey) {
|
||||
if let Some(copies) = &mut self.capture_copies {
|
||||
copies.forget_tile(key);
|
||||
}
|
||||
if let Some(tile) = self.lods.remove(&key) {
|
||||
self.lod_cull_free.push(tile.cull_slot);
|
||||
self.release_tile(tile);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_lods(&mut self) {
|
||||
if let Some(copies) = &mut self.capture_copies {
|
||||
copies.clear_tiles();
|
||||
}
|
||||
self.lod_pending = None;
|
||||
// No far terrain left to fade out over.
|
||||
for (key, since) in std::mem::take(&mut self.lod_leaving) {
|
||||
@@ -294,8 +260,6 @@ impl Renderer {
|
||||
for (_, tile) in std::mem::take(&mut self.lods) {
|
||||
self.release_tile(tile);
|
||||
}
|
||||
self.lod_cull_free.clear();
|
||||
self.lod_cull_next = 0;
|
||||
self.set_lod_cutout(Default::default());
|
||||
}
|
||||
|
||||
@@ -473,15 +437,6 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// The tiles drawn for good, not fading out (`render-capture`).
|
||||
pub(super) fn standing_lods(&self) -> FxHashSet<TileKey> {
|
||||
self.lods
|
||||
.iter()
|
||||
.filter(|(_, tile)| tile.dying.is_none())
|
||||
.map(|(&key, _)| key)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The chunk columns the resident tiles now leave out (`Lod::take_cutout`).
|
||||
/// Removed sections they no longer leave out are released.
|
||||
pub fn set_lod_cutout(&mut self, cutout: crate::fast_hash::FxHashSet<(i32, i32)>) {
|
||||
|
||||
@@ -41,28 +41,17 @@ struct HorizonMaterialOut {
|
||||
@location(10) @interpolate(flat) emission: f32,
|
||||
// terrain.wgsl's `emission`, for HDR output's mark.
|
||||
@location(11) @interpolate(flat) hdr_code: u32,
|
||||
// The point on the flat face (`position` without `[displacement]`'s relief), how much of
|
||||
// that relief it has (`MaterialInput.displaced`) and the face's normal (0 when the
|
||||
// fragment finds it).
|
||||
@location(12) base: vec3<f32>,
|
||||
@location(13) displaced: f32,
|
||||
@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: 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>,
|
||||
light: vec2<f32>) -> HorizonMaterialOut {
|
||||
var out: HorizonMaterialOut;
|
||||
out.clip = horizon_terrain_globals.view_proj * vec4(pos, 1.0);
|
||||
out.uv = uv;
|
||||
out.layer = layer;
|
||||
out.tint = vec4(color.rgb, 1.0);
|
||||
let block = max(light.y, horizon_terrain_dynamic_block_light(pos));
|
||||
let sky = light.x;
|
||||
let block = max(f32(light.y), horizon_terrain_dynamic_block_light(pos));
|
||||
let sky = f32(light.x);
|
||||
// 16 per level, 15 levels.
|
||||
out.light = min(vec2(block, sky) / 240.0, vec2(1.0));
|
||||
out.lightmap = horizon_terrain_sample_lightmap(sky / 16.0, block / 16.0).rgb;
|
||||
@@ -84,9 +73,6 @@ fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f
|
||||
// Codes above 4 are the same tiers, shining all over rather than in their bright parts.
|
||||
let tier = select(out.hdr_code, out.hdr_code - 4u, out.hdr_code > 4u);
|
||||
out.emission = select(min(f32(tier) * 7.0 / 15.0, 1.0), 0.0, tier == 0u);
|
||||
out.base = pos;
|
||||
out.displaced = 0.0;
|
||||
out.normal = vec3(0.0);
|
||||
return out;
|
||||
}
|
||||
@vertex fn horizon_material_vs(
|
||||
@@ -156,17 +142,15 @@ fn horizon_material_color(in: HorizonMaterialOut, translucent: bool) -> vec4<f32
|
||||
horizon_block_lod = max(0.5 * log2(max(footprint, 1e-8)), 0.0);
|
||||
// Water draws translucent and lava solid, so the pass resolves the fluid kind.
|
||||
let fluid = select(0u, select(2u, 1u, translucent), in.fluid_quad != 0u);
|
||||
// `[displacement]`'s grid hands down its face's normal: its relief is the pack's to shade.
|
||||
let normal = select(horizon_material_normal(in.position), in.normal,
|
||||
dot(in.normal, in.normal) > 0.5);
|
||||
let normal = horizon_material_normal(in.position);
|
||||
horizon_material_face_normal = normal;
|
||||
horizon_surface = vec4<f32>(0.0);
|
||||
let tangent = horizon_material_tangent(in.base, in.uv, normal);
|
||||
let tangent = horizon_material_tangent(in.position, in.uv, normal);
|
||||
return material(MaterialInput(albedo, in.tint, in.light, in.lightmap, in.position, in.uv,
|
||||
translucent, horizon_sprite_tags(in.layer & 0x7fffu),
|
||||
horizon_sprite_material(in.layer & 0x7fffu), in.clip.xy / frame.resolution,
|
||||
in.face, fluid, in.emission, normal, tangent,
|
||||
horizon_terrain_native_colored_light(in.position), in.displaced)).color;
|
||||
horizon_terrain_native_colored_light(in.position))).color;
|
||||
}
|
||||
var<private> horizon_material_face_normal: vec3<f32>;
|
||||
struct HorizonMaterialSolid {
|
||||
@@ -192,13 +176,6 @@ fn horizon_material_mark(in: HorizonMaterialOut, texel: vec4<f32>) -> f32 {
|
||||
return 1.0;
|
||||
}
|
||||
@fragment fn horizon_material_fs_solid(in: HorizonMaterialOut) -> HorizonMaterialSolid {
|
||||
// `[displacement]`'s grid draws these pixels.
|
||||
if length(in.base) < horizon_displace_distance - 0.05 && horizon_displaced_layer(in.layer) {
|
||||
discard;
|
||||
}
|
||||
return horizon_material_solid(in);
|
||||
}
|
||||
fn horizon_material_solid(in: HorizonMaterialOut) -> HorizonMaterialSolid {
|
||||
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.
|
||||
@@ -219,37 +196,6 @@ fn horizon_material_solid(in: HorizonMaterialOut) -> HorizonMaterialSolid {
|
||||
}
|
||||
";
|
||||
|
||||
/// `[displacement]`'s grid (gfx/displace.rs): one instance a quad the pick chose, its corners
|
||||
/// read from the copy of the arena, each of its cells two triangles moved along the face's
|
||||
/// normal by the pack's `displacement`, measured from the texture's mean height so that the
|
||||
/// face's edges, pinned there, meet their neighbours whatever their relief.
|
||||
const DISPLACED_ENTRIES: &str = "
|
||||
@vertex fn horizon_material_vs_displaced(@builtin(vertex_index) v: u32,
|
||||
@builtin(instance_index) instance: u32) -> HorizonMaterialOut {
|
||||
let p = horizon_displace_point(v, instance);
|
||||
var out = horizon_material_out(p.position, p.uv, p.layer, p.color, p.light);
|
||||
out.base = p.base;
|
||||
out.displaced = p.near;
|
||||
out.normal = p.normal;
|
||||
return out;
|
||||
}
|
||||
@fragment fn horizon_material_fs_displaced(in: HorizonMaterialOut) -> HorizonMaterialSolid {
|
||||
if length(in.base) >= horizon_displace_distance + 0.05 {
|
||||
discard;
|
||||
}
|
||||
return horizon_material_solid(in);
|
||||
}
|
||||
";
|
||||
|
||||
/// What the material's displaced pipeline is built against: `[displacement]`'s values and
|
||||
/// its group 1.
|
||||
pub struct DisplacedShape<'a> {
|
||||
pub distance: f32,
|
||||
pub cells: u32,
|
||||
pub steps: bool,
|
||||
pub layout: &'a wgpu::BindGroupLayout,
|
||||
}
|
||||
|
||||
/// What terrain pipelines are built against: the renderer's layouts and constants.
|
||||
pub struct TerrainShape<'a> {
|
||||
/// The terrain's group 0.
|
||||
@@ -281,8 +227,6 @@ pub struct NativeMaterial {
|
||||
pub solid: wgpu::RenderPipeline,
|
||||
pub translucent: wgpu::RenderPipeline,
|
||||
pub flat: Option<(wgpu::RenderPipeline, wgpu::RenderPipeline)>,
|
||||
/// `[displacement]`'s grid of the solid terrain near the camera (gfx/displace.rs).
|
||||
pub displaced: Option<wgpu::RenderPipeline>,
|
||||
/// The stage calls `set_surface`: its solid pipelines also write the surface target.
|
||||
pub writes_surface: bool,
|
||||
}
|
||||
@@ -346,10 +290,9 @@ fn shader_source(
|
||||
tags: &[(u32, u32)],
|
||||
materials: &[(u32, u32)],
|
||||
vertex: bool,
|
||||
displaced: bool,
|
||||
) -> String {
|
||||
format!(
|
||||
"{source}\n// Horizon's terrain (terrain.wgsl)\n{}{}{}{}{ENTRIES}{}{}",
|
||||
"{source}\n// Horizon's terrain (terrain.wgsl)\n{}{}{}{}{ENTRIES}",
|
||||
vanilla_terrain(),
|
||||
if vertex {
|
||||
DISPLACE_PACK
|
||||
@@ -357,13 +300,7 @@ fn shader_source(
|
||||
DISPLACE_VANILLA
|
||||
},
|
||||
layer_table("horizon_sprite_tags", tags),
|
||||
layer_table("horizon_sprite_material", materials),
|
||||
super::displace::RELIEF,
|
||||
if displaced {
|
||||
super::displace::GRID.to_owned() + DISPLACED_ENTRIES
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
layer_table("horizon_sprite_material", materials)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -373,9 +310,8 @@ fn module(
|
||||
tags: &[(u32, u32)],
|
||||
materials: &[(u32, u32)],
|
||||
vertex: bool,
|
||||
displaced: bool,
|
||||
) -> Result<naga::Module, String> {
|
||||
let text = shader_source(&stage.source, tags, materials, vertex, displaced);
|
||||
let text = shader_source(&stage.source, tags, materials, vertex);
|
||||
naga::front::wgsl::parse_str(&text)
|
||||
.map_err(|e| format!("{}: {}", stage.file, e.emit_to_string(&text)))
|
||||
}
|
||||
@@ -390,16 +326,9 @@ impl NativeMaterial {
|
||||
constants: &[(String, f64)],
|
||||
fog: bool,
|
||||
vertex: bool,
|
||||
displaced: Option<&DisplacedShape<'_>>,
|
||||
) -> Result<Self, String> {
|
||||
let writes_surface = horizon_shader_api::stages::calls_set_surface(&stage.source);
|
||||
let module = module(
|
||||
stage,
|
||||
shape.sprite_tags,
|
||||
shape.sprite_materials,
|
||||
vertex,
|
||||
displaced.is_some(),
|
||||
)?;
|
||||
let module = module(stage, shape.sprite_tags, shape.sprite_materials, vertex)?;
|
||||
let mut constants = present_constants(&module, constants);
|
||||
let sprite_names = [
|
||||
"horizon_terrain_SPRITE_ARRAYS",
|
||||
@@ -407,14 +336,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_displace_distance",
|
||||
displaced.map_or(0.0, |d| f64::from(d.distance)),
|
||||
));
|
||||
if let Some(d) = displaced {
|
||||
constants.push(("horizon_displace_cells", f64::from(d.cells)));
|
||||
constants.push(("horizon_displace_steps", f64::from(u8::from(d.steps))));
|
||||
}
|
||||
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some(&stage.label),
|
||||
@@ -490,7 +411,7 @@ impl NativeMaterial {
|
||||
module: &module,
|
||||
entry_point: Some(fragment),
|
||||
compilation_options: options,
|
||||
targets: if fragment != "horizon_material_fs_translucent" && writes_surface {
|
||||
targets: if fragment == "horizon_material_fs_solid" && writes_surface {
|
||||
&solid_targets
|
||||
} else {
|
||||
&solid_targets[..1]
|
||||
@@ -532,16 +453,6 @@ impl NativeMaterial {
|
||||
),
|
||||
)
|
||||
});
|
||||
// The grid's corners come from its group 1, not from vertex buffers.
|
||||
let displaced = displaced.map(|d| {
|
||||
pipeline(
|
||||
&layout(Some(d.layout)),
|
||||
"horizon_material_vs_displaced",
|
||||
&[],
|
||||
"horizon_material_fs_displaced",
|
||||
None,
|
||||
)
|
||||
});
|
||||
if let Some(e) = pollster::block_on(scope.pop()) {
|
||||
return Err(format!("{}: {e}", stage.file));
|
||||
}
|
||||
@@ -549,7 +460,6 @@ impl NativeMaterial {
|
||||
solid,
|
||||
translucent,
|
||||
flat,
|
||||
displaced,
|
||||
writes_surface,
|
||||
})
|
||||
}
|
||||
@@ -605,7 +515,7 @@ mod tests {
|
||||
let tags = [(7, 1), (3, 1), (9, 2)];
|
||||
let materials = [(5, 1000)];
|
||||
assert!(layer_table("t", &tags).contains("case 3u, 7u: { return 1u; }"));
|
||||
let module = module(&stage, &tags, &materials, false, false).unwrap();
|
||||
let module = module(&stage, &tags, &materials, false).unwrap();
|
||||
let info = naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
@@ -614,13 +524,7 @@ mod tests {
|
||||
.unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"{}",
|
||||
e.emit_to_string(&shader_source(
|
||||
&stage.source,
|
||||
&tags,
|
||||
&materials,
|
||||
false,
|
||||
false
|
||||
))
|
||||
e.emit_to_string(&shader_source(&stage.source, &tags, &materials, false))
|
||||
)
|
||||
});
|
||||
let entries: Vec<&str> = module
|
||||
@@ -642,39 +546,4 @@ mod tests {
|
||||
[("gain", 1.5)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_displaced_grid_validates_with_a_pack() {
|
||||
let manifest = "schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"0.1\"\n\
|
||||
[material]\nfile = \"material.wgsl\"\n[displacement]\n";
|
||||
let material = "fn material(input: MaterialInput) -> Surface {\n \
|
||||
return Surface(input.albedo * (1.0 - 0.5 * input.displaced));\n}\n\
|
||||
fn displacement(input: DisplacementInput) -> f32 {\n \
|
||||
return (1.0 - input.height) * 0.125 * f32(input.material + 1u);\n}\n";
|
||||
let files = [(MANIFEST, manifest), ("material.wgsl", material)]
|
||||
.map(|(n, t)| (n.to_owned(), t.as_bytes().to_vec()))
|
||||
.into();
|
||||
let mut report = check(&Pack::from_files(files).unwrap()).unwrap();
|
||||
let stage = report.stages.remove(0);
|
||||
let materials = [(5, 1000)];
|
||||
let module = module(&stage, &[], &materials, false, true).unwrap();
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
)
|
||||
.validate(&module)
|
||||
.unwrap_or_else(|e| {
|
||||
let text = shader_source(&stage.source, &[], &materials, false, true);
|
||||
panic!("{}", e.emit_to_string(&text))
|
||||
});
|
||||
for entry in [
|
||||
"horizon_material_vs_displaced",
|
||||
"horizon_material_fs_displaced",
|
||||
] {
|
||||
assert!(
|
||||
module.entry_points.iter().any(|e| e.name == entry),
|
||||
"{entry}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,9 @@
|
||||
|
||||
use horizon_shader_api::{Report, StageKind};
|
||||
|
||||
use super::displace::Displacement;
|
||||
use super::native_material::{DisplacedShape, NativeMaterial, TerrainShape};
|
||||
use super::native_material::{NativeMaterial, TerrainShape};
|
||||
use super::native_post::{NativeFrame, NativePost};
|
||||
use super::native_shadow::{NativeShadow, ShadowGrid};
|
||||
use super::native_shadow::NativeShadow;
|
||||
use super::native_sky::NativeSky;
|
||||
use super::pack_timing::PassTiming;
|
||||
|
||||
@@ -22,8 +21,6 @@ pub struct NativePack {
|
||||
pub weather: Option<wgpu::RenderPipeline>,
|
||||
/// The draw of the shadow map the stages read, if the pack asks for one.
|
||||
pub shadow: Option<NativeShadow>,
|
||||
/// `[displacement]`'s grid, while it is on and the resource packs have height maps.
|
||||
pub displacement: Option<Displacement>,
|
||||
/// `[[particle_lights]]`.
|
||||
pub particle_lights: Vec<horizon_shader_api::manifest::ParticleLight>,
|
||||
/// `[[lights]]`, for the colour of held and dropped items' light.
|
||||
@@ -32,33 +29,6 @@ pub struct NativePack {
|
||||
pub timing: Option<PassTiming>,
|
||||
}
|
||||
|
||||
/// Whether the option `id` (a bool, or a number above 0) is on, or there is none to ask.
|
||||
fn option_on(constants: &[(String, f64)], id: Option<&String>) -> bool {
|
||||
id.is_none_or(|id| {
|
||||
constants
|
||||
.iter()
|
||||
.find(|(k, _)| k == id)
|
||||
.is_none_or(|(_, v)| *v != 0.0)
|
||||
})
|
||||
}
|
||||
|
||||
/// `[displacement]`'s cells: the value of its `cells_option`, if any.
|
||||
fn chosen_cells(
|
||||
d: &horizon_shader_api::manifest::Displacement,
|
||||
options: &[horizon_shader_api::manifest::PackOption],
|
||||
constants: &[(String, f64)],
|
||||
) -> u32 {
|
||||
let chosen = d.cells_option.as_ref().and_then(|id| {
|
||||
let option = options.iter().find(|o| o.id == *id)?;
|
||||
let horizon_shader_api::manifest::OptionKind::Choice { values, .. } = &option.kind else {
|
||||
return None;
|
||||
};
|
||||
let (_, index) = constants.iter().find(|(k, _)| k == id)?;
|
||||
values.get(*index as usize)?.parse().ok()
|
||||
});
|
||||
chosen.unwrap_or(d.cells)
|
||||
}
|
||||
|
||||
impl NativePack {
|
||||
/// The pack of the checked stages `report` with the option values `constants`, drawing
|
||||
/// onto the world's targets as `terrain` describes; the sky reads the terrain's group 0.
|
||||
@@ -79,48 +49,11 @@ impl NativePack {
|
||||
.as_ref()
|
||||
.map(|settings| super::native_shadow::fitted(device, settings));
|
||||
let mut frame = NativeFrame::new(device, settings.as_ref(), textures, lights, terrain.pbr);
|
||||
// `[displacement]`, unless its option is off; nothing to displace without heights.
|
||||
let displacement = report
|
||||
.manifest
|
||||
.displacement
|
||||
.as_ref()
|
||||
.filter(|d| option_on(constants, d.option.as_ref()))
|
||||
.filter(|_| terrain.pbr.is_some())
|
||||
.map(|d| {
|
||||
// No more cells than the height maps have texels.
|
||||
let texels = terrain.pbr.map_or(16, |p| p[0].width());
|
||||
let cells = chosen_cells(d, &report.manifest.options, constants)
|
||||
.min(texels)
|
||||
.max(1);
|
||||
let steps =
|
||||
d.steps_option.is_some() && option_on(constants, d.steps_option.as_ref());
|
||||
Displacement::new(
|
||||
device,
|
||||
d.distance,
|
||||
cells,
|
||||
steps,
|
||||
&frame.pbr[0],
|
||||
report.shadow.is_some(),
|
||||
)
|
||||
});
|
||||
let shadow = report
|
||||
.shadow
|
||||
.as_ref()
|
||||
.map(|s| {
|
||||
let vertex = report.stages.iter().find(|s| s.kind == StageKind::Vertex);
|
||||
// `[displacement]`'s grid casts too, from the material's module.
|
||||
let material = report.stages.iter().find(|s| s.kind == StageKind::Material);
|
||||
let grid = displacement.as_ref().and_then(|d| {
|
||||
let picks = d.shadow.as_ref()?;
|
||||
Some(ShadowGrid {
|
||||
distance: d.distance,
|
||||
cells: picks.cells,
|
||||
steps: picks.steps,
|
||||
material: material?,
|
||||
layout: &d.draw_layout,
|
||||
normals: &frame.pbr[0],
|
||||
})
|
||||
});
|
||||
NativeShadow::new(
|
||||
device,
|
||||
terrain,
|
||||
@@ -128,7 +61,6 @@ impl NativePack {
|
||||
&s.solid_materials,
|
||||
vertex,
|
||||
constants,
|
||||
grid,
|
||||
)
|
||||
})
|
||||
.transpose()?;
|
||||
@@ -139,7 +71,12 @@ impl NativePack {
|
||||
.iter()
|
||||
.find(|s| s.kind == StageKind::Material)
|
||||
.is_some_and(|s| s.reads("reflection"))
|
||||
&& option_on(constants, option);
|
||||
&& option.is_none_or(|id| {
|
||||
constants
|
||||
.iter()
|
||||
.find(|(k, _)| k == id)
|
||||
.is_none_or(|(_, v)| *v != 0.0)
|
||||
});
|
||||
let fog = report.material_fog;
|
||||
let keeps = |key: fn(&horizon_shader_api::Stage) -> Option<bool>| {
|
||||
report.sky.as_ref().and_then(key).unwrap_or(true)
|
||||
@@ -182,22 +119,7 @@ impl NativePack {
|
||||
.iter()
|
||||
.find(|s| s.kind == StageKind::Material)
|
||||
.map(|stage| {
|
||||
let displaced = displacement.as_ref().map(|d| DisplacedShape {
|
||||
distance: d.distance,
|
||||
cells: d.view.cells,
|
||||
steps: d.view.steps,
|
||||
layout: &d.draw_layout,
|
||||
});
|
||||
NativeMaterial::new(
|
||||
device,
|
||||
terrain,
|
||||
&frame,
|
||||
stage,
|
||||
constants,
|
||||
fog,
|
||||
has_vertex,
|
||||
displaced.as_ref(),
|
||||
)
|
||||
NativeMaterial::new(device, terrain, &frame, stage, constants, fog, has_vertex)
|
||||
})
|
||||
.transpose()?;
|
||||
let post = (!post.is_empty())
|
||||
@@ -226,7 +148,6 @@ impl NativePack {
|
||||
vanilla_stars,
|
||||
weather,
|
||||
shadow,
|
||||
displacement,
|
||||
timing: None,
|
||||
};
|
||||
Ok((pack, labels))
|
||||
|
||||
@@ -122,7 +122,7 @@ pub struct NativeFrame {
|
||||
pub opaque_color: wgpu::Texture,
|
||||
/// The resource packs' LabPBR maps (bindings 12 and 13: normals, specular), a blank 1x1
|
||||
/// without them.
|
||||
pub(super) pbr: [wgpu::TextureView; 2],
|
||||
pbr: [wgpu::TextureView; 2],
|
||||
/// The material stage's `reflection` (binding 1): this frame's water mirror, else
|
||||
/// `blank`; and its sampler (binding 2).
|
||||
reflection: wgpu::TextureView,
|
||||
@@ -623,8 +623,7 @@ fn texture_entry(binding: u32, sample_type: wgpu::TextureSampleType) -> wgpu::Bi
|
||||
fn pbr_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
// `[displacement]`'s grid moves its corners by the heights (gfx/displace.rs).
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2Array,
|
||||
|
||||
@@ -23,9 +23,6 @@ struct HorizonShadowOut {
|
||||
@location(1) @interpolate(flat) layer: u32,
|
||||
@location(2) tint: vec3<f32>,
|
||||
@location(3) @interpolate(flat) fluid: u32,
|
||||
// The point on the flat face, camera-relative (`[displacement]`'s grid takes it over near
|
||||
// the camera).
|
||||
@location(4) base: vec3<f32>,
|
||||
}
|
||||
// `color` is the vertex colour: the tint, and the fluid flag in its alpha's high nibble
|
||||
// (mesher.rs's `fluid_face`).
|
||||
@@ -40,7 +37,6 @@ fn horizon_shadow_vertex(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<
|
||||
out.tint = color.rgb;
|
||||
let high = u32(round(color.a * 255.0)) >> 4u;
|
||||
out.fluid = u32(high >= 8u && (high & 7u) < 6u);
|
||||
out.base = pos;
|
||||
return out;
|
||||
}
|
||||
@vertex fn horizon_shadow_vs(
|
||||
@@ -62,10 +58,6 @@ fn horizon_shadow_vertex(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<
|
||||
return horizon_shadow_vertex(pos, atlas, layer, color);
|
||||
}
|
||||
@fragment fn horizon_shadow_fs(in: HorizonShadowOut) {
|
||||
if horizon_shadow_taken(in.base, in.layer) { discard; }
|
||||
horizon_shadow_cutout(in);
|
||||
}
|
||||
fn horizon_shadow_cutout(in: HorizonShadowOut) {
|
||||
// Bit 15 of the layer marks opaque leaves (forceOpaque): no alpha cutout.
|
||||
let alpha = horizon_terrain_sprite(in.uv, in.layer & 0x7fffu).a;
|
||||
if alpha < 0.5 && (in.layer & 0x8000u) == 0u && !horizon_shadow_solid(in.layer & 0x7fffu) { discard; }
|
||||
@@ -113,45 +105,6 @@ struct HorizonEntityShadowOut {
|
||||
}
|
||||
";
|
||||
|
||||
/// No `[displacement]` grid: the flat terrain casts everywhere.
|
||||
const UNTAKEN: &str = "
|
||||
fn horizon_shadow_taken(base: vec3<f32>, layer: u32) -> bool {
|
||||
return false;
|
||||
}
|
||||
";
|
||||
|
||||
/// `[displacement]`'s grid in the shadow map (gfx/displace.rs), cast in the flat quads' place.
|
||||
/// The height maps join the pass's frame group.
|
||||
const GRID_ENTRIES: &str = "
|
||||
@group(3) @binding(12) var horizon_pbr_normals: texture_2d_array<f32>;
|
||||
fn horizon_shadow_taken(base: vec3<f32>, layer: u32) -> bool {
|
||||
return length(base) < horizon_displace_distance - 0.05 && horizon_displaced_layer(layer);
|
||||
}
|
||||
@vertex fn horizon_shadow_vs_displaced(@builtin(vertex_index) v: u32,
|
||||
@builtin(instance_index) instance: u32) -> HorizonShadowOut {
|
||||
let p = horizon_displace_point(v, instance);
|
||||
var out = horizon_shadow_vertex(p.position, p.uv, p.layer, p.color);
|
||||
out.base = p.base;
|
||||
return out;
|
||||
}
|
||||
@fragment fn horizon_shadow_fs_displaced(in: HorizonShadowOut) {
|
||||
if length(in.base) >= horizon_displace_distance + 0.05 { discard; }
|
||||
horizon_shadow_cutout(in);
|
||||
}
|
||||
";
|
||||
|
||||
/// What the shadow pass's grid is built from: `[displacement]`'s values, the material stage
|
||||
/// (whose module holds `displacement` and the vertex stage), group 1's layout and the height
|
||||
/// maps.
|
||||
pub struct ShadowGrid<'a> {
|
||||
pub distance: f32,
|
||||
pub cells: u32,
|
||||
pub steps: bool,
|
||||
pub material: &'a horizon_shader_api::check::CheckedStage,
|
||||
pub layout: &'a wgpu::BindGroupLayout,
|
||||
pub normals: &'a wgpu::TextureView,
|
||||
}
|
||||
|
||||
/// `shadow_color`'s format.
|
||||
const SHADOW_COLOR: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
@@ -373,8 +326,6 @@ pub struct NativeShadow {
|
||||
/// The entities' draw, with the entities' group 0 (`ENTITY_ENTRIES`).
|
||||
pub entity: wgpu::RenderPipeline,
|
||||
pub group: wgpu::BindGroup,
|
||||
/// `[displacement]`'s grid (gfx/displace.rs's shadow list).
|
||||
pub displaced: Option<wgpu::RenderPipeline>,
|
||||
}
|
||||
|
||||
/// The entity draw's shader.
|
||||
@@ -467,36 +418,17 @@ fn shadow_solid(materials: &[(u32, u32)], solid: &[u32]) -> String {
|
||||
}
|
||||
|
||||
/// The shadow pass's shader: the frame, Horizon's terrain, and the entries, with the layers
|
||||
/// `solid` of `shadow_solid`. Under `[displacement]` (`grid`: the material stage's source, which
|
||||
/// holds the vertex stage and `displacement`), its grid too.
|
||||
fn shader_source(
|
||||
solid: &str,
|
||||
vertex: Option<&str>,
|
||||
materials: &[(u32, u32)],
|
||||
grid: Option<&str>,
|
||||
) -> String {
|
||||
let table = super::native_material::layer_table("horizon_sprite_material", materials);
|
||||
let (head, displace) = match (grid, vertex) {
|
||||
(Some(material), _) => (
|
||||
format!(
|
||||
"{material}{}{table}{}{}{GRID_ENTRIES}",
|
||||
horizon_shader_api::stages::PBR_STUB,
|
||||
super::displace::RELIEF,
|
||||
super::displace::GRID,
|
||||
),
|
||||
if vertex.is_some() {
|
||||
super::native_material::DISPLACE_PACK
|
||||
} else {
|
||||
super::native_material::DISPLACE_VANILLA
|
||||
},
|
||||
),
|
||||
(None, Some(stage)) => (
|
||||
format!("{stage}{table}{UNTAKEN}"),
|
||||
/// `solid` of `shadow_solid`.
|
||||
fn shader_source(solid: &str, vertex: Option<&str>, materials: &[(u32, u32)]) -> String {
|
||||
let (head, displace) = match vertex {
|
||||
Some(stage) => (
|
||||
stage.to_owned()
|
||||
+ &super::native_material::layer_table("horizon_sprite_material", materials),
|
||||
super::native_material::DISPLACE_PACK,
|
||||
),
|
||||
(None, None) => (
|
||||
None => (
|
||||
format!(
|
||||
"{}@group({GROUP}) @binding(0) var<uniform> frame: Frame;\n{}{UNTAKEN}",
|
||||
"{}@group({GROUP}) @binding(0) var<uniform> frame: Frame;\n{}",
|
||||
FrameInputs::WGSL.wgsl(),
|
||||
horizon_shader_api::stages::SHADOW_HELPERS,
|
||||
),
|
||||
@@ -520,13 +452,11 @@ impl NativeShadow {
|
||||
solid_materials: &[u32],
|
||||
vertex: Option<&horizon_shader_api::check::CheckedStage>,
|
||||
constants: &[(String, f64)],
|
||||
grid: Option<ShadowGrid<'_>>,
|
||||
) -> Result<Self, String> {
|
||||
let text = shader_source(
|
||||
&shadow_solid(shape.sprite_materials, solid_materials),
|
||||
vertex.map(|s| s.source.as_str()),
|
||||
shape.sprite_materials,
|
||||
grid.as_ref().map(|g| g.material.source.as_str()),
|
||||
);
|
||||
let module = naga::front::wgsl::parse_str(&text)
|
||||
.map_err(|e| format!("shadow pass: {}", e.emit_to_string(&text)))?;
|
||||
@@ -535,59 +465,31 @@ impl NativeShadow {
|
||||
("horizon_terrain_SPRITE_ARRAYS", shape.sprites[0]),
|
||||
("horizon_terrain_SPRITE_SPLIT", shape.sprites[1]),
|
||||
]);
|
||||
if let Some(g) = &grid {
|
||||
constants.extend([
|
||||
("horizon_displace_distance", f64::from(g.distance)),
|
||||
("horizon_displace_cells", f64::from(g.cells)),
|
||||
("horizon_displace_steps", f64::from(u8::from(g.steps))),
|
||||
]);
|
||||
}
|
||||
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Native shader pack shadow"),
|
||||
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)),
|
||||
});
|
||||
// The frame alone, and under `[displacement]` the height maps its grid moves by.
|
||||
let frame_entry = wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: wgpu::BufferSize::new(size_of::<FrameInputs>() as u64),
|
||||
},
|
||||
count: None,
|
||||
};
|
||||
let normals_entry = wgpu::BindGroupLayoutEntry {
|
||||
binding: 12,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2Array,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
};
|
||||
let layout_entries: Vec<_> = std::iter::once(frame_entry)
|
||||
.chain(grid.is_some().then_some(normals_entry))
|
||||
.collect();
|
||||
let frame_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Native shader pack shadow frame"),
|
||||
entries: &layout_entries,
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: wgpu::BufferSize::new(size_of::<FrameInputs>() as u64),
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
let group_entries: Vec<_> = std::iter::once(wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: frame.buffer.as_entire_binding(),
|
||||
})
|
||||
.chain(grid.as_ref().map(|g| wgpu::BindGroupEntry {
|
||||
binding: 12,
|
||||
resource: wgpu::BindingResource::TextureView(g.normals),
|
||||
}))
|
||||
.collect();
|
||||
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Native shader pack shadow frame"),
|
||||
layout: &frame_layout,
|
||||
entries: &group_entries,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: frame.buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
let layout = |flat: Option<&wgpu::BindGroupLayout>| {
|
||||
let mut groups: [Option<&wgpu::BindGroupLayout>; GROUP as usize + 1] =
|
||||
@@ -617,11 +519,10 @@ impl NativeShadow {
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})];
|
||||
let pipeline_with = |layout: &wgpu::PipelineLayout,
|
||||
vertex: &str,
|
||||
buffers: &[Option<wgpu::VertexBufferLayout<'_>>],
|
||||
fragment: &str| {
|
||||
let translucent = fragment == "horizon_shadow_fs_translucent";
|
||||
let pipeline = |layout: &wgpu::PipelineLayout,
|
||||
vertex: &str,
|
||||
buffers: &[Option<wgpu::VertexBufferLayout<'_>>],
|
||||
translucent: bool| {
|
||||
let options = wgpu::PipelineCompilationOptions {
|
||||
constants: &constants,
|
||||
..Default::default()
|
||||
@@ -655,7 +556,11 @@ impl NativeShadow {
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some(fragment),
|
||||
entry_point: Some(if translucent {
|
||||
"horizon_shadow_fs_translucent"
|
||||
} else {
|
||||
"horizon_shadow_fs"
|
||||
}),
|
||||
compilation_options: options,
|
||||
targets: if translucent { &color_target } else { &[] },
|
||||
}),
|
||||
@@ -663,17 +568,6 @@ impl NativeShadow {
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let pipeline = |layout: &wgpu::PipelineLayout,
|
||||
vertex: &str,
|
||||
buffers: &[Option<wgpu::VertexBufferLayout<'_>>],
|
||||
translucent: bool| {
|
||||
let fragment = if translucent {
|
||||
"horizon_shadow_fs_translucent"
|
||||
} else {
|
||||
"horizon_shadow_fs"
|
||||
};
|
||||
pipeline_with(layout, vertex, buffers, fragment)
|
||||
};
|
||||
let instanced = layout(None);
|
||||
let solid = pipeline(
|
||||
&instanced,
|
||||
@@ -701,15 +595,6 @@ impl NativeShadow {
|
||||
let flat = flat_pipeline(false);
|
||||
let flat_translucent = flat_pipeline(true);
|
||||
let entity = entity_pipeline(device, shape.entities, &frame_layout);
|
||||
// The grid's corners come from its group 1, not from vertex buffers.
|
||||
let displaced = grid.as_ref().map(|g| {
|
||||
pipeline_with(
|
||||
&layout(Some(g.layout)),
|
||||
"horizon_shadow_vs_displaced",
|
||||
&[],
|
||||
"horizon_shadow_fs_displaced",
|
||||
)
|
||||
});
|
||||
if let Some(e) = pollster::block_on(scope.pop()) {
|
||||
return Err(format!("shadow pass: {e}"));
|
||||
}
|
||||
@@ -720,7 +605,6 @@ impl NativeShadow {
|
||||
flat_translucent,
|
||||
entity,
|
||||
group,
|
||||
displaced,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -732,7 +616,7 @@ mod tests {
|
||||
#[test]
|
||||
fn the_shadow_shader_validates() {
|
||||
for materials in [&[][..], &[(3, 7), (4, 8), (9, 7)][..]] {
|
||||
let text = shader_source(&shadow_solid(materials, &[7]), None, materials, None);
|
||||
let text = shader_source(&shadow_solid(materials, &[7]), None, materials);
|
||||
let module = naga::front::wgsl::parse_str(&text).unwrap();
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
@@ -743,47 +627,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_shadow_shader_with_the_displaced_grid_validates() {
|
||||
use horizon_shader_api::{MANIFEST, Pack, StageKind, check};
|
||||
let manifest = "schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"0.1\"\n\
|
||||
[material]\nfile = \"material.wgsl\"\n[shadow]\n[displacement]\n";
|
||||
let material = "fn material(input: MaterialInput) -> Surface {\n \
|
||||
return Surface(input.albedo);\n}\n\
|
||||
fn displacement(input: DisplacementInput) -> f32 {\n \
|
||||
return (1.0 - input.height) * 0.125;\n}\n";
|
||||
let files = [(MANIFEST, manifest), ("material.wgsl", material)]
|
||||
.map(|(n, t)| (n.to_owned(), t.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();
|
||||
let materials = [(3, 7)];
|
||||
let text = shader_source(
|
||||
&shadow_solid(&materials, &[7]),
|
||||
None,
|
||||
&materials,
|
||||
Some(&stage.source),
|
||||
);
|
||||
let module = naga::front::wgsl::parse_str(&text)
|
||||
.unwrap_or_else(|e| panic!("{}", e.emit_to_string(&text)));
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
)
|
||||
.validate(&module)
|
||||
.unwrap_or_else(|e| panic!("{}", e.emit_to_string(&text)));
|
||||
for entry in ["horizon_shadow_vs_displaced", "horizon_shadow_fs_displaced"] {
|
||||
assert!(
|
||||
module.entry_points.iter().any(|e| e.name == entry),
|
||||
"{entry}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_layers_of_solid_ids_lose_their_cutouts() {
|
||||
let materials = [(3, 7), (4, 8), (9, 7)];
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
//! for the next one. A section meshed in between propagates into the graph the
|
||||
//! frame has (vanilla's partial update), so what it opens shows at once; what
|
||||
//! it closes goes at the next full search.
|
||||
//!
|
||||
//! GPU culling (gfx/gpu_culling.rs) answers as `contains` does from a copy of
|
||||
//! the graph (`Reach`): the cells it reached, kept as bits too, sent again
|
||||
//! whenever the generation moves, which it does whenever an answer may change.
|
||||
use super::SectionKey;
|
||||
use crate::fast_hash::FxHashSet;
|
||||
use crate::mesher::ALL_VISIBLE;
|
||||
@@ -133,10 +129,6 @@ struct Graph {
|
||||
/// (`Node.sourceDirections`, as the faces rather than the steps); none for
|
||||
/// the camera's.
|
||||
entered: Vec<u8>,
|
||||
/// `REACHED` of each cell again, a bit each at `index` (32 a word, lowest
|
||||
/// bit first): the GPU's copy (`Occlusion::reach`), written with it
|
||||
/// (`set_reached`).
|
||||
bits: Vec<u32>,
|
||||
/// Whether any section in the searched top layer was reached. Sections
|
||||
/// above a clamped box use this conservative sky-band answer.
|
||||
sky_reached: bool,
|
||||
@@ -185,13 +177,6 @@ impl Graph {
|
||||
false
|
||||
}
|
||||
}
|
||||
/// Cell `i` is reached, having travelled `directions` (`REACHED` among
|
||||
/// them): the one writer of `REACHED`, so `bits` say what `directions` do.
|
||||
fn set_reached(&mut self, i: usize, directions: u8) {
|
||||
debug_assert!(directions & REACHED != 0);
|
||||
self.directions[i] = directions;
|
||||
self.bits[i / 32] |= 1 << (i % 32);
|
||||
}
|
||||
/// The full search from the camera (vanilla's `scheduleFullUpdate`).
|
||||
fn search(
|
||||
&mut self,
|
||||
@@ -235,8 +220,6 @@ impl Graph {
|
||||
let size = side * side * self.height as usize;
|
||||
self.directions.clear();
|
||||
self.directions.resize(size, 0);
|
||||
self.bits.clear();
|
||||
self.bits.resize(size.div_ceil(32), 0);
|
||||
self.entered.clear();
|
||||
self.entered.resize(size, 0);
|
||||
self.sky_reached = false;
|
||||
@@ -260,7 +243,7 @@ impl Graph {
|
||||
queue.clear();
|
||||
if (0..columns.sections).contains(&from.1) {
|
||||
let start = self.index(from).expect("camera inside the searched box");
|
||||
self.set_reached(start, REACHED);
|
||||
self.directions[start] = REACHED;
|
||||
queue.push_back(start as u32);
|
||||
} else {
|
||||
self.seed_outside(camera, queue);
|
||||
@@ -311,7 +294,7 @@ impl Graph {
|
||||
let i = self
|
||||
.index((self.from.0 + dx, y, self.from.2 + dz))
|
||||
.expect("seed inside the searched box");
|
||||
self.set_reached(i, directions);
|
||||
self.directions[i] = directions;
|
||||
// Entered through the face opposite the travel (`sourceDirection`).
|
||||
self.entered[i] = 1 << (travel ^ 1);
|
||||
queue.push_back(i as u32);
|
||||
@@ -321,15 +304,14 @@ impl Graph {
|
||||
/// every face their entry faces connect to, away from the camera. A step
|
||||
/// into a section already reached adds the face it enters through.
|
||||
/// `full`: the search from `camera` (`rays` holds its lines), not a
|
||||
/// partial update. Whether it reached a cell it had not.
|
||||
/// partial update.
|
||||
fn propagate(
|
||||
&mut self,
|
||||
queue: &mut VecDeque<u32>,
|
||||
camera: DVec3,
|
||||
full: bool,
|
||||
vis: impl Fn(usize, SectionKey) -> u64,
|
||||
) -> bool {
|
||||
let mut grew = false;
|
||||
) {
|
||||
let eye = (
|
||||
(camera.x.floor() as i32) >> 4,
|
||||
(camera.y.floor() as i32 - self.min_y) >> 4,
|
||||
@@ -383,16 +365,14 @@ impl Graph {
|
||||
}
|
||||
}
|
||||
if self.directions[n] & REACHED == 0 {
|
||||
self.set_reached(n, directions | 1 << d);
|
||||
self.directions[n] = directions | 1 << d;
|
||||
self.entered[n] = 1 << (d ^ 1);
|
||||
queue.push_back(n as u32);
|
||||
grew = true;
|
||||
} else {
|
||||
self.entered[n] |= 1 << (d ^ 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
grew
|
||||
}
|
||||
/// Vanilla's distant step test: from the corner of `key` that the step `d`
|
||||
/// picks, walk towards the camera in section diagonals while farther than
|
||||
@@ -492,42 +472,6 @@ impl Graph {
|
||||
}
|
||||
}
|
||||
|
||||
/// The graph `Occlusion::contains` reads, for GPU culling's copy
|
||||
/// (gfx/gpu_culling.rs): its searched box (`Graph::index`), the level's height
|
||||
/// in sections, whether the search reached the box's top layer (the sky band
|
||||
/// above a clamped box), and its cells' `REACHED` as bits.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Reach<'a> {
|
||||
/// `Occlusion::generation` when it was taken.
|
||||
pub generation: u64,
|
||||
pub from_x: i32,
|
||||
pub from_z: i32,
|
||||
pub radius: i32,
|
||||
pub y0: i32,
|
||||
pub height: i32,
|
||||
pub sections: i32,
|
||||
pub sky_reached: bool,
|
||||
pub bits: &'a [u32],
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Reach<'_> {
|
||||
/// gpu_culling.wgsl's `reached`, statement for statement: what
|
||||
/// `Graph::contains` answers.
|
||||
pub fn contains(&self, key: SectionKey) -> bool {
|
||||
let r = self.radius;
|
||||
let (dx, dz, dy) = (key.0 - self.from_x, key.2 - self.from_z, key.1 - self.y0);
|
||||
if dx < -r || dx > r || dz < -r || dz > r || dy < 0 {
|
||||
return false;
|
||||
}
|
||||
if dy >= self.height {
|
||||
return self.sky_reached && (0..=self.sections).contains(&key.1);
|
||||
}
|
||||
let i = (((dz + r) * (2 * r + 1) + dx + r) * self.height + dy) as usize;
|
||||
(self.bits[i / 32] >> (i % 32)) & 1 != 0
|
||||
}
|
||||
}
|
||||
|
||||
/// `Mth.floor` for coordinates inside the world, without a libm call (the
|
||||
/// x86-64 baseline has no rounding instruction).
|
||||
fn floor(x: f64) -> i32 {
|
||||
@@ -574,14 +518,6 @@ impl Worker {
|
||||
}
|
||||
}
|
||||
|
||||
/// A new value for `Occlusion::generation`, unique in the process: a copy made
|
||||
/// from an older `Occlusion` (another level's) never matches. As gfx.rs
|
||||
/// `next_block_entity_revision`.
|
||||
fn next_generation() -> u64 {
|
||||
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub struct Occlusion {
|
||||
/// The render thread's copy, for partial updates (and searches when the
|
||||
/// worker is gone).
|
||||
@@ -609,8 +545,6 @@ pub struct Occlusion {
|
||||
changed_late: Vec<SectionKey>,
|
||||
vis: Vec<u64>,
|
||||
queue: VecDeque<u32>,
|
||||
/// Changes whenever `contains` may answer differently (`generation`).
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
impl Occlusion {
|
||||
@@ -630,7 +564,6 @@ impl Occlusion {
|
||||
changed_late: Vec::new(),
|
||||
vis: Vec::new(),
|
||||
queue: VecDeque::new(),
|
||||
generation: next_generation(),
|
||||
}
|
||||
}
|
||||
pub fn set(&mut self, key: SectionKey, vis: u64) {
|
||||
@@ -675,11 +608,9 @@ impl Occlusion {
|
||||
self.edits.clear();
|
||||
self.edits.push(Edit::Clear);
|
||||
self.dirty = true;
|
||||
// Another world: the old graph says nothing about it. Without one, every
|
||||
// section was visible already, and stays so.
|
||||
// Another world: the old graph says nothing about it.
|
||||
if let Some(graph) = self.graph.take() {
|
||||
self.spare = graph;
|
||||
self.generation = next_generation();
|
||||
}
|
||||
self.searched = None;
|
||||
self.discard = self.running.is_some();
|
||||
@@ -692,38 +623,6 @@ impl Occlusion {
|
||||
pub fn contains(&self, key: SectionKey) -> bool {
|
||||
self.graph.as_ref().is_none_or(|g| g.contains(key))
|
||||
}
|
||||
/// Changes whenever `contains` may answer differently: a search landed, a
|
||||
/// partial update reached more sections, or the graph went.
|
||||
#[cfg(any(test, debug_assertions))]
|
||||
pub fn generation(&self) -> u64 {
|
||||
self.generation
|
||||
}
|
||||
/// The graph `contains` reads; `None` while every section counts as
|
||||
/// visible.
|
||||
pub fn reach(&self) -> Option<Reach<'_>> {
|
||||
self.graph.as_ref().map(|g| Reach {
|
||||
generation: self.generation,
|
||||
from_x: g.from.0,
|
||||
from_z: g.from.2,
|
||||
radius: g.radius,
|
||||
y0: g.y0,
|
||||
height: g.height,
|
||||
sections: g.sections,
|
||||
sky_reached: g.sky_reached,
|
||||
bits: &g.bits,
|
||||
})
|
||||
}
|
||||
/// The bits `reach` exports are the cells' `REACHED`, cell for cell.
|
||||
#[cfg(any(test, debug_assertions))]
|
||||
pub fn reach_is_exact(&self) -> bool {
|
||||
self.graph.as_ref().is_none_or(|g| {
|
||||
g.bits.len() == g.directions.len().div_ceil(32)
|
||||
&& g.directions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.all(|(i, &d)| (d & REACHED != 0) == ((g.bits[i / 32] >> (i % 32)) & 1 != 0))
|
||||
})
|
||||
}
|
||||
/// Take a finished search, propagate changed sections into the graph,
|
||||
/// and start a search when sections changed or the camera moved to
|
||||
/// another section. Never waits for the worker.
|
||||
@@ -743,7 +642,6 @@ impl Occlusion {
|
||||
&& let Some(graph) = self.graph.take()
|
||||
{
|
||||
self.spare = graph;
|
||||
self.generation = next_generation();
|
||||
}
|
||||
}
|
||||
fn start(&mut self, camera: DVec3, radius: i32) {
|
||||
@@ -815,7 +713,6 @@ impl Occlusion {
|
||||
if let Some(old) = self.graph.replace(graph) {
|
||||
self.spare = old;
|
||||
}
|
||||
self.generation = next_generation();
|
||||
}
|
||||
/// Vanilla's `runPartialUpdate`: what changed sections now connect to.
|
||||
fn propagate_changes(&mut self, camera: DVec3) {
|
||||
@@ -832,9 +729,7 @@ impl Occlusion {
|
||||
}
|
||||
}
|
||||
let columns = &self.columns;
|
||||
if graph.propagate(&mut self.queue, camera, false, |_, key| columns.get(key)) {
|
||||
self.generation = next_generation();
|
||||
}
|
||||
graph.propagate(&mut self.queue, camera, false, |_, key| columns.get(key));
|
||||
}
|
||||
/// Wait for the running search: tests, and A/B timing of the old
|
||||
/// blocking search.
|
||||
@@ -1510,197 +1405,4 @@ mod tests {
|
||||
"enclosed cave does not see the sky"
|
||||
);
|
||||
}
|
||||
|
||||
/// `contains` over the columns `radius` + 3 around `from`, from two
|
||||
/// sections below the level to two above it, checked against the graph
|
||||
/// `reach` exports: `None` exactly without a graph, the same answer for
|
||||
/// every key, nothing past its box, its bits its cells' `REACHED`, and its
|
||||
/// sky band reached exactly when a bit of its top layer is.
|
||||
fn answers(o: &Occlusion, from: SectionKey, radius: i32) -> Vec<bool> {
|
||||
let reach = o.reach();
|
||||
assert_eq!(reach.is_some(), o.graph.is_some(), "a graph exported");
|
||||
assert!(o.reach_is_exact(), "the bits are the cells'");
|
||||
if let Some(g) = &o.graph {
|
||||
let height = g.height as usize;
|
||||
let mut top = (height - 1..g.directions.len()).step_by(height);
|
||||
let sky = top.any(|i| (g.bits[i / 32] >> (i % 32)) & 1 != 0);
|
||||
assert_eq!(g.sky_reached, sky, "the sky band");
|
||||
}
|
||||
let past = |key: SectionKey| {
|
||||
o.graph.as_ref().is_some_and(|g| {
|
||||
(key.0 - g.from.0).abs() > g.radius
|
||||
|| (key.2 - g.from.2).abs() > g.radius
|
||||
|| !(g.y0..=g.sections).contains(&key.1)
|
||||
})
|
||||
};
|
||||
let mut answers = Vec::new();
|
||||
for x in from.0 - radius - 3..=from.0 + radius + 3 {
|
||||
for z in from.2 - radius - 3..=from.2 + radius + 3 {
|
||||
for y in -2..=o.columns.sections + 1 {
|
||||
let key = (x, y, z);
|
||||
let contains = o.contains(key);
|
||||
assert_eq!(reach.is_none_or(|r| r.contains(key)), contains, "{key:?}");
|
||||
assert!(!(contains && past(key)), "{key:?} past the box");
|
||||
answers.push(contains);
|
||||
}
|
||||
}
|
||||
}
|
||||
answers
|
||||
}
|
||||
|
||||
/// The graph `reach` exports answers as `contains` (`answers`): walled in,
|
||||
/// opened by a partial update, cleared; over clamped boxes, the sky band
|
||||
/// above them reached or not (a closed cave); from above the world. The
|
||||
/// search, the partial update and `clear` each change the generation, and
|
||||
/// another level's graph never repeats one.
|
||||
#[test]
|
||||
fn the_exported_graph_answers_as_contains() {
|
||||
let mut o = Occlusion::new(0, 4);
|
||||
let first = o.generation();
|
||||
answers(&o, (0, 1, 0), 3);
|
||||
for (dx, dy, dz) in STEPS {
|
||||
o.set((dx, 1 + dy, dz), 0);
|
||||
}
|
||||
o.settle(at((0, 1, 0)), 3);
|
||||
assert_ne!(o.generation(), first, "a search landed");
|
||||
let walled = answers(&o, (0, 1, 0), 3);
|
||||
let searched = o.generation();
|
||||
o.set((0, 1, 1), ALL_VISIBLE);
|
||||
o.update(at((0, 1, 0)), 3);
|
||||
assert_ne!(o.generation(), searched, "a partial update reached more");
|
||||
assert_ne!(answers(&o, (0, 1, 0), 3), walled);
|
||||
let opened = o.generation();
|
||||
o.clear();
|
||||
assert_ne!(o.generation(), opened, "the graph went");
|
||||
answers(&o, (0, 1, 0), 3);
|
||||
// Another level's graph never takes one of this one's generations, or
|
||||
// the pass would keep its copy of this one.
|
||||
let mut other = Occlusion::new(0, 4);
|
||||
other.settle(at((0, 1, 0)), 3);
|
||||
assert_ne!(other.generation(), searched, "another level's graph");
|
||||
|
||||
let (mut clamped, mut band) = (0, [0; 2]);
|
||||
let mut check = |mut o: Occlusion, camera: DVec3, radius: i32| {
|
||||
o.settle(camera, radius);
|
||||
answers(&o, o.columns.section_of(camera), radius);
|
||||
let g = o.graph.as_ref().expect("searched");
|
||||
if g.height < o.columns.sections + 2 {
|
||||
clamped += 1;
|
||||
// The first section above the box, over the camera's column.
|
||||
band[usize::from(o.contains((g.from.0, g.y0 + g.height, g.from.2)))] += 1;
|
||||
}
|
||||
};
|
||||
for seed in [0x9e37_79b9, 7, 0xdead_beef, 0x2545_f491] {
|
||||
for (spread, count, radius) in [(6, 900, 6), (12, 2600, 12)] {
|
||||
let mut o = Occlusion::new(0, 24);
|
||||
random_world(&mut o, seed, count, spread);
|
||||
check(o, at((0, 2, 0)) + DVec3::new(3.3, -5.7, 1.9), radius);
|
||||
}
|
||||
}
|
||||
let mut cave = Occlusion::new(0, 24);
|
||||
for (dx, dy, dz) in STEPS {
|
||||
cave.set((dx, 1 + dy, dz), 0);
|
||||
}
|
||||
check(cave, at((0, 1, 0)), 4);
|
||||
let mut above = Occlusion::new(0, 24);
|
||||
random_world(&mut above, 7, 900, 6);
|
||||
check(above, DVec3::new(3.3, 400.0, -5.6), 6);
|
||||
assert!(
|
||||
clamped > 0 && band[0] > 0 && band[1] > 0,
|
||||
"{clamped} clamped, the band above hidden and seen {band:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Whenever an answer of `contains` changes, so does the generation: over
|
||||
/// 300 random steps, each a section set (open, closed or anything between),
|
||||
/// a column removed, the camera moved to another section (now and then
|
||||
/// above or below the world), or now and then another world, after a
|
||||
/// `clear` or a teleport that drops every column (`remove_columns`), each
|
||||
/// followed by a frame's `update` or a settled search; then an update from
|
||||
/// a section the graph never reached. The exported graph answers as
|
||||
/// `contains` after every step.
|
||||
#[test]
|
||||
fn every_change_of_the_answers_changes_the_generation() {
|
||||
let mut seed = 0x0bad_5eed_u64;
|
||||
let mut next = || {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 7;
|
||||
seed ^= seed << 17;
|
||||
seed
|
||||
};
|
||||
let (spread, radius, around) = (5, 3, (0, 0, 0));
|
||||
let offset = DVec3::new(3.3, -5.7, 1.9);
|
||||
let mut o = Occlusion::new(0, 24);
|
||||
random_world(&mut o, 0x2545_f491, 400, spread);
|
||||
let mut camera = at((0, 2, 0)) + offset;
|
||||
o.settle(camera, radius);
|
||||
let mut last = (o.generation(), answers(&o, around, spread + radius));
|
||||
let mut changed = 0;
|
||||
for step in 0..300 {
|
||||
let r = next();
|
||||
let key = (
|
||||
(r % 11) as i32 - spread,
|
||||
((r >> 8) % 7) as i32,
|
||||
((r >> 16) % 11) as i32 - spread,
|
||||
);
|
||||
match r >> 24 & 15 {
|
||||
0..=7 => {
|
||||
let vis = match r >> 28 & 3 {
|
||||
0 => ALL_VISIBLE,
|
||||
1 => 0,
|
||||
_ => next() & ALL_VISIBLE,
|
||||
};
|
||||
o.set(key, vis);
|
||||
}
|
||||
8 | 9 => o.remove_column(key.0, key.2),
|
||||
10..=13 => {
|
||||
let y = match r >> 32 & 7 {
|
||||
0 => 30,
|
||||
1 => -3,
|
||||
_ => key.1,
|
||||
};
|
||||
camera = at((key.0, y, key.2)) + offset;
|
||||
}
|
||||
_ if r >> 36 & 7 == 0 => {
|
||||
o.clear();
|
||||
random_world(&mut o, next(), 400, spread);
|
||||
}
|
||||
_ if r >> 36 & 7 == 1 => {
|
||||
// A teleport: every column goes at once.
|
||||
let all: FxHashSet<_> = o.columns.map.keys().copied().collect();
|
||||
o.remove_columns(&all);
|
||||
random_world(&mut o, next(), 400, spread);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if r >> 40 & 1 == 0 {
|
||||
o.update(camera, radius);
|
||||
} else {
|
||||
o.settle(camera, radius);
|
||||
}
|
||||
let now = (o.generation(), answers(&o, around, spread + radius));
|
||||
if now.1 != last.1 {
|
||||
assert_ne!(
|
||||
now.0, last.0,
|
||||
"step {step}: answers changed, the generation not"
|
||||
);
|
||||
changed += 1;
|
||||
}
|
||||
last = now;
|
||||
}
|
||||
assert!(changed >= 50, "{changed} steps changed answers");
|
||||
// Walled in, then past the wall: the graph never reached the camera's
|
||||
// section, and goes until the next search lands. (The first settle may
|
||||
// land a search that started before the wall.)
|
||||
for (dx, dy, dz) in STEPS {
|
||||
o.set((dx, 1 + dy, dz), 0);
|
||||
}
|
||||
o.settle(at((0, 1, 0)), radius);
|
||||
o.settle(at((0, 1, 0)), radius);
|
||||
let walled = (o.generation(), answers(&o, around, spread + radius));
|
||||
assert!(!o.contains((3, 1, 0)), "past the wall");
|
||||
o.update(at((3, 1, 0)), radius);
|
||||
assert_ne!(answers(&o, around, spread + radius), walled.1);
|
||||
assert_ne!(o.generation(), walled.0, "the graph went");
|
||||
}
|
||||
}
|
||||
@@ -194,8 +194,6 @@ pub(super) struct Draw {
|
||||
/// The pack's shadow program draws it into the shadow map too.
|
||||
pub shadow: bool,
|
||||
pub slot: u32,
|
||||
/// End portal or gateway faces: drawn with `end_portal.png` bound as the atlas.
|
||||
pub portal: bool,
|
||||
}
|
||||
|
||||
/// Which entity runs a pack's shadow pass draws (2.4, 3.2).
|
||||
@@ -377,87 +375,38 @@ impl PackEntities {
|
||||
pack,
|
||||
casts,
|
||||
overlay,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
let first = spans.first().map_or(entities.len(), |s| s.start as usize);
|
||||
extend_without_portals(rest, &entities[..first.min(entities.len())]);
|
||||
let (mut other, mut portal_faces, mut beam_faces) = (Vec::new(), Vec::new(), Vec::new());
|
||||
rest.extend_from_slice(&entities[..first.min(entities.len())]);
|
||||
for (span, range) in runs(entities.len(), spans) {
|
||||
let whole = &entities[range];
|
||||
// A block entity's run can hold End portal faces and beams beside plain parts (an End
|
||||
// gateway: its cube and its beam): each goes to its own program.
|
||||
// Beacon beams come in a run of their own, at stage NONE (game.rs: no stage of 5.4
|
||||
// is theirs), for gbuffers_beaconbeam.
|
||||
let split = (program_of(&span) == Some(BLOCK) || span.stage == render_stage("NONE"))
|
||||
&& whole
|
||||
.chunks(4)
|
||||
.any(|q| matches!(q[0].layer & 0xffff, 6 | 9 | 10));
|
||||
let parts: [(&[Vertex], Part); 3] = if split {
|
||||
other.clear();
|
||||
portal_faces.clear();
|
||||
beam_faces.clear();
|
||||
for quad in whole.chunks(4) {
|
||||
match quad[0].layer & 0xffff {
|
||||
9 | 10 => portal_faces.extend_from_slice(quad),
|
||||
6 => beam_faces.extend_from_slice(quad),
|
||||
_ => other.extend_from_slice(quad),
|
||||
}
|
||||
}
|
||||
[
|
||||
(&other, Part::Plain),
|
||||
(&portal_faces, Part::Portal),
|
||||
(&beam_faces, Part::Beam),
|
||||
]
|
||||
} else {
|
||||
[(whole, Part::Plain), (&[], Part::Plain), (&[], Part::Plain)]
|
||||
};
|
||||
for (run, part) in parts {
|
||||
if run.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let program = match part {
|
||||
Part::Portal => Some(BLOCK),
|
||||
Part::Beam => Some(BEAM),
|
||||
Part::Plain => program_of(&span).filter(|&p| {
|
||||
plain(run)
|
||||
|| (p == DESTROY && run.iter().all(|v| matches!(v.layer, 11 | 12)))
|
||||
}),
|
||||
};
|
||||
let pack = program.is_some_and(|p| drawn[p]) && !span.hidden;
|
||||
let casts = part == Part::Plain && program.is_some_and(|p| shadow.cast(&span, p));
|
||||
if !pack && !span.hidden {
|
||||
extend_without_portals(rest, run);
|
||||
}
|
||||
let Some(program) = program.filter(|_| pack || casts) else {
|
||||
continue;
|
||||
};
|
||||
let start = self.scratch.len() as u32;
|
||||
match part {
|
||||
Part::Beam => beam_segments(run, &mut self.scratch),
|
||||
_ => self.scratch.extend_from_slice(run),
|
||||
}
|
||||
if program == DESTROY {
|
||||
project_cracks(&mut self.scratch[start as usize..], crack.0, crack.1);
|
||||
}
|
||||
if part == Part::Portal {
|
||||
as_portal_texture(&mut self.scratch[start as usize..]);
|
||||
}
|
||||
let vertices = self.scratch.len() as u32 - start;
|
||||
let overlay = self.scratch[start as usize].layer & 0xffff;
|
||||
let portal = part == Part::Portal;
|
||||
self.push_draw(
|
||||
span,
|
||||
posed + start,
|
||||
vertices,
|
||||
program,
|
||||
pack,
|
||||
casts,
|
||||
overlay,
|
||||
portal,
|
||||
);
|
||||
let run = &entities[range];
|
||||
let program = program_of(&span).filter(|&p| {
|
||||
plain(run) || (p == DESTROY && run.iter().all(|v| matches!(v.layer, 11 | 12)))
|
||||
});
|
||||
let pack = program.is_some_and(|p| drawn[p]) && !span.hidden;
|
||||
let casts = program.is_some_and(|p| shadow.cast(&span, p));
|
||||
if !pack && !span.hidden {
|
||||
rest.extend_from_slice(run);
|
||||
}
|
||||
let Some(program) = program.filter(|_| (pack || casts) && !run.is_empty()) else {
|
||||
continue;
|
||||
};
|
||||
let start = self.scratch.len() as u32;
|
||||
self.scratch.extend_from_slice(run);
|
||||
if program == DESTROY {
|
||||
project_cracks(&mut self.scratch[start as usize..], crack.0, crack.1);
|
||||
}
|
||||
self.push_draw(
|
||||
span,
|
||||
posed + start,
|
||||
run.len() as u32,
|
||||
program,
|
||||
pack,
|
||||
casts,
|
||||
run[0].layer & 0xffff,
|
||||
);
|
||||
}
|
||||
if self.draws.is_empty() && posed == 0 {
|
||||
return;
|
||||
@@ -504,7 +453,6 @@ impl PackEntities {
|
||||
pack: bool,
|
||||
casts: bool,
|
||||
overlay: u32,
|
||||
portal: bool,
|
||||
) {
|
||||
let (entity_id, block_id) = match program {
|
||||
BLOCK => (0, span.id),
|
||||
@@ -528,7 +476,6 @@ impl PackEntities {
|
||||
pack,
|
||||
shadow: casts,
|
||||
slot: self.draws.len() as u32,
|
||||
portal,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -557,104 +504,13 @@ fn plain(run: &[Vertex]) -> bool {
|
||||
.all(|v| !matches!(v.layer & 0xffff, 2 | 4 | 6 | 9 | 10 | 11 | 12 | 34))
|
||||
}
|
||||
|
||||
/// What part of a block entity's run goes to which program (`prepare`).
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Part {
|
||||
Plain,
|
||||
Portal,
|
||||
Beam,
|
||||
}
|
||||
|
||||
/// Beam faces (`block_entity_render::beam`: layer 6, v counting the sprite's repeats up the
|
||||
/// beam, the sprite's v range in the light bytes) for a pack: each face cut where its texture
|
||||
/// repeats, so every piece holds one copy of the sprite in plain atlas coordinates (at most 64
|
||||
/// pieces a face; a taller beam stretches its copies), white light and its colour kept.
|
||||
fn beam_segments(run: &[Vertex], out: &mut Vec<Vertex>) {
|
||||
for quad in run.as_chunks::<4>().0 {
|
||||
let wrap = quad[0].light;
|
||||
let (v0, size) = (
|
||||
(wrap & 65535) as f32 / 65535.0,
|
||||
(wrap >> 16) as f32 / 65535.0,
|
||||
);
|
||||
// Corners 0 and 3 at the beam's end (v1), 1 and 2 at its start (v2); 0 and 1 on one
|
||||
// side, 2 and 3 on the other.
|
||||
let (v1, v2) = (quad[0].uv[1], quad[1].uv[1]);
|
||||
if (v1 - v2).abs() < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
let (lo, hi) = (v1.min(v2), v1.max(v2));
|
||||
let first = lo.floor();
|
||||
let repeats = (hi.ceil() - first).max(1.0);
|
||||
let step = (repeats / 64.0).ceil().max(1.0);
|
||||
let pieces = (repeats / step).ceil() as u32;
|
||||
let at = |side: usize, v: f32| {
|
||||
let (a, b) = if side == 0 {
|
||||
(quad[1], quad[0])
|
||||
} else {
|
||||
(quad[2], quad[3])
|
||||
};
|
||||
let t = (v - v2) / (v1 - v2);
|
||||
glam::Vec3::from(a.pos)
|
||||
.lerp(glam::Vec3::from(b.pos), t)
|
||||
.to_array()
|
||||
};
|
||||
for k in 0..pieces {
|
||||
let base = first + k as f32 * step;
|
||||
let (a, b) = (base.max(lo), (base + step).min(hi));
|
||||
if b <= a {
|
||||
continue;
|
||||
}
|
||||
// The end-side corners take the piece's bound nearer v1.
|
||||
let (end_v, start_v) = if v1 > v2 { (b, a) } else { (a, b) };
|
||||
for (i, corner) in quad.iter().enumerate() {
|
||||
let side = usize::from(i >= 2);
|
||||
let v = if i == 0 || i == 3 { end_v } else { start_v };
|
||||
let mut out_v = *corner;
|
||||
out_v.pos = at(side, v);
|
||||
out_v.uv = [corner.uv[0], v0 + (v - base) / step * size];
|
||||
out_v.layer = 0;
|
||||
out_v.light = 0x0000_f0f0;
|
||||
out.push(out_v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `run` added to Horizon's own draw without End portal faces: under a shader pack Horizon's
|
||||
/// entity shader would put its screen-projected starfield into the pack's buffers as if they
|
||||
/// were its own frame (misplaced where the pack renders at another size), so a portal the pack
|
||||
/// does not draw (its programs not there, or not yet) is better left out.
|
||||
fn extend_without_portals(rest: &mut Vec<Vertex>, run: &[Vertex]) {
|
||||
for quad in run.chunks(4) {
|
||||
if !quad.iter().all(|v| matches!(v.layer & 0xffff, 6 | 9 | 10)) {
|
||||
rest.extend_from_slice(quad);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// End portal faces for a pack's `gbuffers_block` (block entities): each face textured with the
|
||||
/// whole of `end_portal.png`, which the draw binds as the atlas (a texture of its own, so a
|
||||
/// pack's own portal effect can wrap it), white, at full light; the pack tells the portal by its
|
||||
/// `blockEntityId`.
|
||||
fn as_portal_texture(run: &mut [Vertex]) {
|
||||
const CORNERS: [[f32; 2]; 4] = [[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]];
|
||||
for (i, v) in run.iter_mut().enumerate() {
|
||||
v.uv = CORNERS[i % 4];
|
||||
v.layer = 0;
|
||||
v.color = 0xFFFF_FFFF;
|
||||
v.light = 0x0000_f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
/// The programs of entity runs, in the order they draw.
|
||||
pub const ENTITY: usize = 0;
|
||||
pub const BLOCK: usize = 1;
|
||||
pub const TRANSLUCENT: usize = 2;
|
||||
pub const PARTICLE: usize = 3;
|
||||
pub const DESTROY: usize = 4;
|
||||
/// Beacon and End gateway beams (`gbuffers_beaconbeam`).
|
||||
pub const BEAM: usize = 5;
|
||||
pub const PROGRAMS: usize = 6;
|
||||
pub const PROGRAMS: usize = 5;
|
||||
|
||||
/// Which program draws `span`'s run, given which of them (or their fallbacks) the pack has;
|
||||
/// `None`: Horizon draws it.
|
||||
@@ -731,11 +587,11 @@ mod tests {
|
||||
assert_eq!(drawn_by_pack(&spans[0], all), Some(ENTITY));
|
||||
assert_eq!(drawn_by_pack(&spans[1], all), Some(PARTICLE));
|
||||
assert_eq!(
|
||||
drawn_by_pack(&spans[1], [true, true, true, false, true, true]),
|
||||
drawn_by_pack(&spans[1], [true, true, true, false, true]),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
drawn_by_pack(&spans[2], [true, false, true, true, true, true]),
|
||||
drawn_by_pack(&spans[2], [true, false, true, true, true]),
|
||||
None
|
||||
);
|
||||
assert_eq!(drawn_by_pack(&spans[2], all), Some(BLOCK));
|
||||
@@ -798,103 +654,6 @@ fn project_cracks(vertices: &mut [Vertex], sprite: [f32; 4], atlas: [u32; 2]) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod portal_beam_tests {
|
||||
use super::*;
|
||||
|
||||
fn quad(layer: u32, light: u32, corners: [([f32; 3], [f32; 2]); 4]) -> [Vertex; 4] {
|
||||
corners.map(|(pos, uv)| Vertex {
|
||||
pos,
|
||||
uv,
|
||||
layer,
|
||||
color: 0xFF20_40FF,
|
||||
light,
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_beam_face_becomes_one_sprite_copy_per_repeat() {
|
||||
// `block_entity_render::beam`'s corner order: 0 and 3 at the end (v1), 1 and 2 at the
|
||||
// start (v2); the sprite's v range (0.25, height 0.5) in the light bytes.
|
||||
let wrap = (0.25f32 * 65535.0).round() as u32 | ((0.5f32 * 65535.0).round() as u32) << 16;
|
||||
let face = quad(
|
||||
6,
|
||||
wrap,
|
||||
[
|
||||
([0.0, 3.0, 0.0], [0.9, 3.0]),
|
||||
([0.0, 0.0, 0.0], [0.9, 0.0]),
|
||||
([1.0, 0.0, 0.0], [0.1, 0.0]),
|
||||
([1.0, 3.0, 0.0], [0.1, 3.0]),
|
||||
],
|
||||
);
|
||||
let mut out = Vec::new();
|
||||
beam_segments(&face, &mut out);
|
||||
assert_eq!(out.len(), 12, "three repeats, three pieces");
|
||||
for (k, piece) in out.chunks(4).enumerate() {
|
||||
let (bottom, top) = (k as f32, k as f32 + 1.0);
|
||||
assert_eq!([piece[0].pos[1], piece[1].pos[1]], [top, bottom]);
|
||||
assert_eq!([piece[2].pos[1], piece[3].pos[1]], [bottom, top]);
|
||||
for v in piece {
|
||||
assert!((0.25 - 1e-4..=0.75 + 1e-4).contains(&v.uv[1]), "{:?}", v.uv);
|
||||
assert_eq!(v.layer, 0);
|
||||
assert_eq!(v.light, 0x0000_f0f0);
|
||||
assert_eq!(v.color, 0xFF20_40FF);
|
||||
}
|
||||
assert!((piece[0].uv[1] - 0.75).abs() < 1e-4 && (piece[1].uv[1] - 0.25).abs() < 1e-4);
|
||||
assert_eq!([piece[0].uv[0], piece[2].uv[0]], [0.9, 0.1]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_very_tall_beam_is_cut_into_at_most_64_pieces() {
|
||||
let face = quad(
|
||||
6,
|
||||
0xFFFF_0000,
|
||||
[
|
||||
([0.0, 2000.0, 0.0], [1.0, 2000.0]),
|
||||
([0.0, 0.0, 0.0], [1.0, 0.0]),
|
||||
([1.0, 0.0, 0.0], [0.0, 0.0]),
|
||||
([1.0, 2000.0, 0.0], [0.0, 2000.0]),
|
||||
],
|
||||
);
|
||||
let mut out = Vec::new();
|
||||
beam_segments(&face, &mut out);
|
||||
assert!(out.len() / 4 <= 64, "{} pieces", out.len() / 4);
|
||||
let top = out.iter().map(|v| v.pos[1]).fold(f32::MIN, f32::max);
|
||||
let bottom = out.iter().map(|v| v.pos[1]).fold(f32::MAX, f32::min);
|
||||
assert_eq!(
|
||||
(bottom, top),
|
||||
(0.0, 2000.0),
|
||||
"the pieces still span the whole beam"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portal_faces_take_the_whole_portal_texture_at_full_light() {
|
||||
let mut face = quad(9, 0, [([0.0; 3], [0.0; 2]); 4]).to_vec();
|
||||
as_portal_texture(&mut face);
|
||||
let uvs: Vec<[f32; 2]> = face.iter().map(|v| v.uv).collect();
|
||||
assert_eq!(uvs, [[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]]);
|
||||
assert!(
|
||||
face.iter()
|
||||
.all(|v| v.layer == 0 && v.color == 0xFFFF_FFFF && v.light == 0x0000_f0f0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizon_keeps_no_portal_or_beam_face_under_a_pack() {
|
||||
let mut run = Vec::new();
|
||||
for layer in [0, 6, 9, 10, 2] {
|
||||
run.extend(quad(layer, 0, [([0.0; 3], [0.0; 2]); 4]));
|
||||
}
|
||||
let mut rest = Vec::new();
|
||||
extend_without_portals(&mut rest, &run);
|
||||
let kept: Vec<u32> = rest.chunks(4).map(|q| q[0].layer).collect();
|
||||
assert_eq!(kept, [0, 2]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod crack_projection_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -35,9 +35,7 @@ pub(super) fn images(defs: Vec<CustomImage>) -> Vec<PackImage> {
|
||||
defs.into_iter()
|
||||
.map(|def| PackImage {
|
||||
format: format(def.format),
|
||||
dimension: if def.is_1d() {
|
||||
wgpu::TextureViewDimension::D1
|
||||
} else if def.is_3d() {
|
||||
dimension: if def.is_3d() {
|
||||
wgpu::TextureViewDimension::D3
|
||||
} else {
|
||||
wgpu::TextureViewDimension::D2
|
||||
@@ -53,15 +51,6 @@ pub(super) fn images(defs: Vec<CustomImage>) -> Vec<PackImage> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The texture dimension a view of `view` needs.
|
||||
fn texture_dimension(view: wgpu::TextureViewDimension) -> wgpu::TextureDimension {
|
||||
match view {
|
||||
wgpu::TextureViewDimension::D1 => wgpu::TextureDimension::D1,
|
||||
wgpu::TextureViewDimension::D3 => wgpu::TextureDimension::D3,
|
||||
_ => wgpu::TextureDimension::D2,
|
||||
}
|
||||
}
|
||||
|
||||
/// The wgpu format of an image's internal format: a storage format of the same channels.
|
||||
pub(super) fn format(f: Format) -> wgpu::TextureFormat {
|
||||
use wgpu::TextureFormat as T;
|
||||
@@ -96,9 +85,7 @@ pub(super) fn makeable(device: &wgpu::Device, def: &CustomImage) -> Result<(), S
|
||||
));
|
||||
}
|
||||
let limits = device.limits();
|
||||
let largest = if def.is_1d() {
|
||||
limits.max_texture_dimension_1d
|
||||
} else if def.is_3d() {
|
||||
let largest = if def.is_3d() {
|
||||
limits.max_texture_dimension_3d
|
||||
} else {
|
||||
limits.max_texture_dimension_2d
|
||||
@@ -141,7 +128,11 @@ pub(super) fn resize_images(
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: texture_dimension(image.dimension),
|
||||
dimension: if image.dimension == wgpu::TextureViewDimension::D3 {
|
||||
wgpu::TextureDimension::D3
|
||||
} else {
|
||||
wgpu::TextureDimension::D2
|
||||
},
|
||||
format: image.format,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | usage,
|
||||
view_formats: &[],
|
||||
@@ -330,7 +321,11 @@ pub(super) fn stand_ins(
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: texture_dimension(dimension),
|
||||
dimension: if dimension == wgpu::TextureViewDimension::D3 {
|
||||
wgpu::TextureDimension::D3
|
||||
} else {
|
||||
wgpu::TextureDimension::D2
|
||||
},
|
||||
format,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::COPY_DST
|
||||
|
||||
@@ -7,17 +7,14 @@
|
||||
//! Every colortex buffer a program reads or writes has two textures, A and B. Programs read
|
||||
//! "main" and write "alt", and each buffer a program wrote flips afterwards (6.1.6). The
|
||||
//! mapping carries over to the next frame, so a buffer that is not cleared reads back what was
|
||||
//! written last (OQ-8). A cleared buffer has both textures cleared at the start of the frame:
|
||||
//! with an odd number of flips a frame, the texture a program writes was "main" the frame before
|
||||
//! and may still hold what was stored into it after that frame's clear (Iris 1.11.7 blends onto
|
||||
//! the clear value there, probed with a pack of our own).
|
||||
//! written last (OQ-8). Buffers are cleared at the start of the frame, "main" only.
|
||||
//! Pack buffers store rows as GL does (row 0 at the bottom: the translator negates clip y), so
|
||||
//! the world is copied in upside down, and its reverse-Z depth becomes GL window depth.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use glam::{DMat4, DVec3};
|
||||
use horizon_shaders::{Format, Group, Kind, Post, SHADOW_COLORS, Val};
|
||||
use horizon_shaders::{Format, Group, Kind, Post, Val};
|
||||
|
||||
use super::pack_draw::DrawUniforms;
|
||||
use super::pack_entities::{PROGRAMS, PackEntities};
|
||||
@@ -31,9 +28,6 @@ const BUFFERS: usize = horizon_shaders::BUFFERS;
|
||||
#[cfg(test)]
|
||||
mod world_target_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod higher_shadowcolor_tests;
|
||||
|
||||
/// The texture behind a pack sampler name.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Source {
|
||||
@@ -371,8 +365,6 @@ struct Pass {
|
||||
/// Bind groups by the sides read (bits 0–15), the buffers whose mip chain is current (bits
|
||||
/// 16–31) and the shadowcolor sides read (bits 32–33).
|
||||
groups: HashMap<u64, wgpu::BindGroup>,
|
||||
/// The same for End portal draws, with `end_portal.png` bound as the atlas.
|
||||
portal_groups: HashMap<u64, wgpu::BindGroup>,
|
||||
/// (binding, target) of each image: for a buffer, the side its sampler reads.
|
||||
images: Vec<(u32, Image)>,
|
||||
/// Binds the pack's storage buffers in set 4.
|
||||
@@ -547,10 +539,6 @@ pub struct PackPost {
|
||||
shadow_targets: Option<super::pack_shadow::ShadowTargets>,
|
||||
/// What draws terrain from the light (`shadow`), and translucent terrain (`shadow_water`).
|
||||
shadow_terrain: Option<Pass>,
|
||||
/// `end_portal.png`'s atlas rect this frame, and a texture of its own copied from it that
|
||||
/// End portal draws bind as their atlas.
|
||||
portal_rect: [f32; 4],
|
||||
portal_texture: Option<([f32; 4], wgpu::TextureView)>,
|
||||
shadow_water: Option<Pass>,
|
||||
shadow_comp: Vec<Pass>,
|
||||
/// Buffers with mipmaps (filterable ones a program asks them for).
|
||||
@@ -578,10 +566,6 @@ pub struct PackPost {
|
||||
composite_pre: u16,
|
||||
formats: [wgpu::TextureFormat; BUFFERS],
|
||||
clears: [Option<[f32; 4]>; BUFFERS],
|
||||
/// Buffers of a three-channel format, and whether their textures are new: then they get
|
||||
/// alpha 1 before the next frame.
|
||||
rgb: u16,
|
||||
rgb_unset: bool,
|
||||
used: u16,
|
||||
/// `size.buffer.*`.
|
||||
sizes: [Option<horizon_shaders::BufferSize>; BUFFERS],
|
||||
@@ -666,33 +650,6 @@ fn texture_format(f: Format, features: wgpu::Features) -> wgpu::TextureFormat {
|
||||
}
|
||||
}
|
||||
|
||||
/// The SNORM wgpu format of SNORM pack format `f`, where this adapter can render to it. Iris
|
||||
/// stores such a buffer clamped to [-1, 1] and quantised to its bits; elsewhere the half floats of
|
||||
/// [`texture_format`] stand in.
|
||||
fn snorm_format(
|
||||
f: Format,
|
||||
features: wgpu::Features,
|
||||
adapter: &wgpu::Adapter,
|
||||
) -> Option<wgpu::TextureFormat> {
|
||||
use wgpu::TextureFormat as T;
|
||||
let format = match (f.kind, f.bits, f.channels) {
|
||||
(Kind::Snorm, 8, 1) => T::R8Snorm,
|
||||
(Kind::Snorm, 8, 2) => T::Rg8Snorm,
|
||||
(Kind::Snorm, 8, _) => T::Rgba8Snorm,
|
||||
(Kind::Snorm, 16, 1) => T::R16Snorm,
|
||||
(Kind::Snorm, 16, 2) => T::Rg16Snorm,
|
||||
(Kind::Snorm, 16, _) => T::Rgba16Snorm,
|
||||
_ => return None,
|
||||
};
|
||||
let mut needs = wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES;
|
||||
if f.bits == 16 {
|
||||
needs |= wgpu::Features::TEXTURE_FORMAT_16BIT_NORM;
|
||||
}
|
||||
let renders = (adapter.get_texture_format_features(format).allowed_usages)
|
||||
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT);
|
||||
(features.contains(needs) && renders).then_some(format)
|
||||
}
|
||||
|
||||
/// The texture behind sampler `name` in a program of the shadow buffers (`shadow_space`: buffer
|
||||
/// n is shadowcolor n) or of the others, when the shadow pass runs (`shadow_pass`). `shadow` is
|
||||
/// `shadowtex1` when the program also declares `watershadow` (6.3).
|
||||
@@ -709,7 +666,11 @@ fn fits(ty: &str, sample_type: wgpu::TextureSampleType) -> bool {
|
||||
}
|
||||
|
||||
fn source(name: &str, shadow_space: bool, shadow_pass: bool, watershadow: bool) -> Source {
|
||||
let shadow_color = horizon_shaders::shadow_color(name);
|
||||
let shadow_color = match name {
|
||||
"shadowcolor" | "shadowcolor0" => Some(0),
|
||||
"shadowcolor1" => Some(1),
|
||||
_ => None,
|
||||
};
|
||||
let shadow_depth = match name {
|
||||
"shadowtex0" | "watershadow" | "shadowtex0HW" => Some(0),
|
||||
"shadowtex1" | "shadowtex1HW" => Some(1),
|
||||
@@ -1078,8 +1039,6 @@ struct Resources<'a> {
|
||||
gpu: Gpu,
|
||||
/// The buffers whose format can be bound as an image.
|
||||
storable: u16,
|
||||
/// The buffers of a three-channel format (`rgb`): they have no alpha to write, and read 1.
|
||||
rgb: u16,
|
||||
/// The stage (`texture.<stage>`) of the programs built so far, and the buffers one of them
|
||||
/// flipped: a `texture.<stage>.colortexN` no longer stands in for buffer N in the stage's
|
||||
/// later programs, which read what was written, as in Iris. `final` continues
|
||||
@@ -1102,7 +1061,7 @@ fn bindings(
|
||||
buffers: &[u32],
|
||||
geometry: Option<&Geometry<'_>>,
|
||||
formats: &[wgpu::TextureFormat; BUFFERS],
|
||||
shadow: Option<&[wgpu::TextureFormat; SHADOW_COLORS]>,
|
||||
shadow: Option<&[wgpu::TextureFormat; 2]>,
|
||||
notes: &mut Vec<String>,
|
||||
pack: &Resources<'_>,
|
||||
) -> Option<(
|
||||
@@ -1294,9 +1253,7 @@ fn bindings(
|
||||
notes.push(format!("{name}: `{}` has format {}", i.name, i.format));
|
||||
return None;
|
||||
};
|
||||
let dimension = if i.ty.ends_with("1D") {
|
||||
wgpu::TextureViewDimension::D1
|
||||
} else if i.ty.ends_with("3D") {
|
||||
let dimension = if i.ty.ends_with("3D") {
|
||||
wgpu::TextureViewDimension::D3
|
||||
} else {
|
||||
wgpu::TextureViewDimension::D2
|
||||
@@ -1424,7 +1381,7 @@ fn build_pass(
|
||||
device: &wgpu::Device,
|
||||
p: horizon_shaders::PostProgram,
|
||||
formats: &[wgpu::TextureFormat; BUFFERS],
|
||||
shadow: Option<&[wgpu::TextureFormat; SHADOW_COLORS]>,
|
||||
shadow: Option<&[wgpu::TextureFormat; 2]>,
|
||||
screen: wgpu::TextureFormat,
|
||||
notes: &mut Vec<String>,
|
||||
geometry: Option<&Geometry<'_>>,
|
||||
@@ -1435,7 +1392,7 @@ fn build_pass(
|
||||
let features = device.features();
|
||||
// The frame's uniforms, then the pack's custom ones.
|
||||
let index = |name: &str| {
|
||||
pack_uniforms::index_in(name, geometry.is_some()).or_else(|| {
|
||||
pack_uniforms::index(name).or_else(|| {
|
||||
let k = custom.iter().position(|c| c == name)?;
|
||||
Some(pack_uniforms::NAMES.len() + k)
|
||||
})
|
||||
@@ -1484,12 +1441,6 @@ fn build_pass(
|
||||
} else if let Some(&buffer) = p.buffers.get(l) {
|
||||
let format = formats[buffer as usize];
|
||||
let mut state = target(format);
|
||||
if !p.group.shadow()
|
||||
&& pack.rgb & 1 << buffer != 0
|
||||
&& let Some(t) = &mut state
|
||||
{
|
||||
t.write_mask = wgpu::ColorWrites::COLOR;
|
||||
}
|
||||
if let (Some(t), Some(b)) = (
|
||||
&mut state,
|
||||
p.buffer_blends
|
||||
@@ -1654,7 +1605,6 @@ fn build_pass(
|
||||
atlas: geometry.and_then(|g| g.atlas.clone()),
|
||||
ids: geometry.is_some_and(|g| g.ids.is_some()),
|
||||
groups: HashMap::new(),
|
||||
portal_groups: HashMap::new(),
|
||||
copies: image_buffers(&images) & reads,
|
||||
reads: reads | image_buffers(&images),
|
||||
buffers: !interface.buffers.is_empty(),
|
||||
@@ -1688,7 +1638,7 @@ fn build_compute(
|
||||
group: Group,
|
||||
c: horizon_shaders::ComputeProgram,
|
||||
formats: &[wgpu::TextureFormat; BUFFERS],
|
||||
shadow: Option<&[wgpu::TextureFormat; SHADOW_COLORS]>,
|
||||
shadow: Option<&[wgpu::TextureFormat; 2]>,
|
||||
notes: &mut Vec<String>,
|
||||
custom: &[String],
|
||||
pack: &Resources<'_>,
|
||||
@@ -1789,7 +1739,6 @@ fn build_compute(
|
||||
atlas: None,
|
||||
ids: false,
|
||||
groups: HashMap::new(),
|
||||
portal_groups: HashMap::new(),
|
||||
buffers: !interface.buffers.is_empty(),
|
||||
images,
|
||||
compute: Vec::new(),
|
||||
@@ -1814,26 +1763,16 @@ impl PackPost {
|
||||
// What the pack's shaders need of its storage buffers, before its parts move out.
|
||||
let minimums = post.buffer_minimums();
|
||||
let features = device.features();
|
||||
let formats: [wgpu::TextureFormat; BUFFERS] = std::array::from_fn(|i| {
|
||||
let f = post.config.buffers[i].format;
|
||||
snorm_format(f, features, adapter).unwrap_or_else(|| texture_format(f, features))
|
||||
});
|
||||
let formats: [wgpu::TextureFormat; BUFFERS] =
|
||||
std::array::from_fn(|i| texture_format(post.config.buffers[i].format, features));
|
||||
let sky_clear = Some(&post.config.buffers[0])
|
||||
.filter(|b| b.clear)
|
||||
.and_then(|b| b.clear_color);
|
||||
// A three-channel buffer reads alpha 1 whatever is written or cleared, as in Iris: its
|
||||
// passes leave alpha alone, and its clears and first contents set it to 1.
|
||||
let rgb = (0..BUFFERS)
|
||||
.filter(|&i| post.config.buffers[i].format.channels == 3)
|
||||
.fold(0u16, |m, i| m | 1 << i);
|
||||
let clears = std::array::from_fn(|i| {
|
||||
let b = &post.config.buffers[i];
|
||||
b.clear.then(|| {
|
||||
let mut c = (b.clear_color).unwrap_or(if i == 1 { [1.0; 4] } else { [0.0; 4] });
|
||||
if rgb & 1 << i != 0 {
|
||||
c[3] = 1.0;
|
||||
}
|
||||
c
|
||||
b.clear_color
|
||||
.unwrap_or(if i == 1 { [1.0; 4] } else { [0.0; 4] })
|
||||
})
|
||||
});
|
||||
let mut notes = post.notes;
|
||||
@@ -1845,8 +1784,6 @@ impl PackPost {
|
||||
}
|
||||
let textures = pack_textures::upload(device, queue, post.textures, &mut notes);
|
||||
let shadow_config = post.config.shadow.clone();
|
||||
// shadowcolor2 to 7 only for a pack that names `HIGHER_SHADOWCOLOR`.
|
||||
let shadow_colors = (1u16 << post.shadow_colors) - 1;
|
||||
let mut custom = post.custom;
|
||||
let custom_names: Vec<String> = custom.uniforms().map(|u| u.0.to_owned()).collect();
|
||||
// A first run finds the names neither the frame nor the pack defines.
|
||||
@@ -1862,14 +1799,13 @@ impl PackPost {
|
||||
let mut used = 1u16;
|
||||
let draws = DrawUniforms::new(device);
|
||||
// The shadow buffers' formats when the shadow pass runs; their programs see them as
|
||||
// buffers 0 to 7.
|
||||
let shadow_formats: Option<[wgpu::TextureFormat; SHADOW_COLORS]> =
|
||||
post.shadow_pass.then(|| {
|
||||
std::array::from_fn(|n| texture_format(shadow_config.colors[n].format, features))
|
||||
});
|
||||
// buffers 0 and 1.
|
||||
let shadow_formats: Option<[wgpu::TextureFormat; 2]> = post.shadow_pass.then(|| {
|
||||
std::array::from_fn(|n| texture_format(shadow_config.colors[n].format, features))
|
||||
});
|
||||
let shadow_space: [wgpu::TextureFormat; BUFFERS] = std::array::from_fn(|i| {
|
||||
shadow_formats
|
||||
.filter(|_| i < SHADOW_COLORS)
|
||||
.filter(|_| i < 2)
|
||||
.map_or(formats[i], |f| f[i])
|
||||
});
|
||||
let (mut storage, mut copied, mut image_copied, mut snapshotted) = (0u16, 0u16, 0u64, 0u64);
|
||||
@@ -1937,7 +1873,6 @@ impl PackPost {
|
||||
indices: indices.as_ref(),
|
||||
gpu,
|
||||
storable,
|
||||
rgb,
|
||||
released: std::cell::Cell::new(("", 0)),
|
||||
stand_ins: &stand_ins,
|
||||
};
|
||||
@@ -2085,7 +2020,6 @@ impl PackPost {
|
||||
post.entities_translucent,
|
||||
post.particles,
|
||||
post.damaged_block,
|
||||
post.beacon_beam,
|
||||
];
|
||||
let late = [
|
||||
false,
|
||||
@@ -2093,7 +2027,6 @@ impl PackPost {
|
||||
post.translucent_late,
|
||||
post.particles_late,
|
||||
false,
|
||||
true,
|
||||
];
|
||||
let shadow_only = (post.shadow_only_player, post.shadow_only_lit_blocks);
|
||||
let shadow_programs = [post.shadow_entities, post.shadow_block];
|
||||
@@ -2309,7 +2242,7 @@ impl PackPost {
|
||||
0
|
||||
};
|
||||
m | own | p.shadow_reads
|
||||
}) & shadow_colors;
|
||||
});
|
||||
let depth1 = every_pass()
|
||||
.flat_map(|p| &p.samplers)
|
||||
.any(|s| matches!(s.source, Source::ShadowDepth(1, _)));
|
||||
@@ -2385,13 +2318,7 @@ impl PackPost {
|
||||
}
|
||||
});
|
||||
let float = wgpu::TextureSampleType::Float { filterable: true };
|
||||
let mut world_target = target(formats[0]);
|
||||
if rgb & 1 != 0
|
||||
&& let Some(t) = &mut world_target
|
||||
{
|
||||
t.write_mask = wgpu::ColorWrites::COLOR;
|
||||
}
|
||||
let blit_targets = std::iter::once(world_target);
|
||||
let blit_targets = std::iter::once(target(formats[0]));
|
||||
// Cleared by the pass's load operation; the shader writes nothing there.
|
||||
let blit_targets: Vec<_> = blit_targets
|
||||
.chain(cleared.iter().map(|&b| {
|
||||
@@ -2498,8 +2425,6 @@ impl PackPost {
|
||||
depth2,
|
||||
shadow_targets,
|
||||
shadow_terrain,
|
||||
portal_rect: [0.0; 4],
|
||||
portal_texture: None,
|
||||
shadow_water,
|
||||
shadow_comp,
|
||||
mipped,
|
||||
@@ -2518,8 +2443,6 @@ impl PackPost {
|
||||
composite_pre: mask(&post.composite_pre),
|
||||
formats,
|
||||
clears,
|
||||
rgb,
|
||||
rgb_unset: true,
|
||||
used,
|
||||
storage,
|
||||
copied,
|
||||
@@ -2887,10 +2810,7 @@ impl PackPost {
|
||||
let group = self.bind(device, queue, encoder, pass, size);
|
||||
let mut cp = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
||||
label: Some(&pass.name),
|
||||
timestamp_writes: self
|
||||
.timing
|
||||
.as_mut()
|
||||
.and_then(|t| t.compute_writes(&pass.name)),
|
||||
timestamp_writes: None,
|
||||
});
|
||||
cp.set_pipeline(&pipeline);
|
||||
cp.set_bind_group(0, &group, &[]);
|
||||
@@ -2919,7 +2839,7 @@ impl PackPost {
|
||||
// one clearing them.
|
||||
let (depth_ops, shadow_ops) = match &mut self.shadow_targets {
|
||||
Some(s) if pass.group.shadow() => s.clear_ops(pass.writes.iter().map(|w| w.1)),
|
||||
_ => (super::pack_shadow::load(), [load; SHADOW_COLORS]),
|
||||
_ => (super::pack_shadow::load(), [load; 2]),
|
||||
};
|
||||
let shadow = self.shadow_targets.as_ref().filter(|_| pass.group.shadow());
|
||||
let mut attachments: Vec<Option<wgpu::RenderPassColorAttachment>> = Vec::new();
|
||||
@@ -3104,42 +3024,6 @@ impl PackPost {
|
||||
continue;
|
||||
}
|
||||
let group = self.bind(device, queue, encoder, pass, size);
|
||||
// End portal faces bind `end_portal.png` as the atlas, a texture of its own, so packs
|
||||
// that wrap it with their own coordinates sample the portal sprite only.
|
||||
let portal_group = if geometry
|
||||
.draws
|
||||
.iter()
|
||||
.any(|d| d.pack && d.program == program && d.portal)
|
||||
&& let Some(atlas) = pass.atlas.clone().or_else(|| self.atlas.clone())
|
||||
&& self.portal_rect[2] > 0.0
|
||||
{
|
||||
if self
|
||||
.portal_texture
|
||||
.as_ref()
|
||||
.is_none_or(|(rect, _)| *rect != self.portal_rect)
|
||||
{
|
||||
pass.portal_groups.clear();
|
||||
let view = super::pack_weather::copy_sprite(
|
||||
device,
|
||||
encoder,
|
||||
atlas.texture(),
|
||||
self.portal_rect,
|
||||
);
|
||||
self.portal_texture = Some((self.portal_rect, view));
|
||||
}
|
||||
let view = self.portal_texture.as_ref().map(|p| p.1.clone());
|
||||
// `bind` caches groups by a key without the atlas: the portal groups are kept
|
||||
// in a cache of their own, swapped in with the texture.
|
||||
let saved = std::mem::replace(&mut pass.atlas, view);
|
||||
let groups =
|
||||
std::mem::replace(&mut pass.groups, std::mem::take(&mut pass.portal_groups));
|
||||
let g = self.bind(device, queue, encoder, pass, size);
|
||||
pass.portal_groups = std::mem::replace(&mut pass.groups, groups);
|
||||
pass.atlas = saved;
|
||||
Some(g)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
{
|
||||
let mut rp = self.begin(encoder, pass);
|
||||
rp.set_pipeline(pass.render());
|
||||
@@ -3152,14 +3036,7 @@ impl PackPost {
|
||||
rp.set_bind_group(1, &geometry.table_group, &[]);
|
||||
rp.set_vertex_buffer(0, geometry.vertices());
|
||||
rp.set_index_buffer(geometry.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
let mut on_portal = false;
|
||||
for d in draws {
|
||||
if let Some(portal) = &portal_group
|
||||
&& d.portal != on_portal
|
||||
{
|
||||
rp.set_bind_group(0, if d.portal { portal } else { &group }, &[]);
|
||||
on_portal = d.portal;
|
||||
}
|
||||
if pass.draw {
|
||||
rp.set_bind_group(2, &geometry.group, &[geometry.offset(d.slot)]);
|
||||
}
|
||||
@@ -3233,11 +3110,9 @@ impl PackPost {
|
||||
entities: &[super::Vertex],
|
||||
spans: &[super::pack_draw::EntitySpan],
|
||||
crumbling: [f32; 4],
|
||||
portal: [f32; 4],
|
||||
rest: &mut Vec<super::Vertex>,
|
||||
skin: Option<&super::entity_skin::SkinFrame>,
|
||||
) {
|
||||
self.portal_rect = portal;
|
||||
let shadow = self.shadow_casters();
|
||||
let Some(e) = &mut self.entities else {
|
||||
rest.clear();
|
||||
@@ -3561,25 +3436,16 @@ impl PackPost {
|
||||
}
|
||||
|
||||
/// Regenerates the mip chain of buffer `b`'s side that programs read.
|
||||
fn mipmap(&mut self, encoder: &mut wgpu::CommandEncoder, b: usize) {
|
||||
fn mipmap(&self, encoder: &mut wgpu::CommandEncoder, b: usize) {
|
||||
let (Some(targets), Some((pipeline, _))) = (&self.targets, self.down.get(&self.formats[b]))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(mips) = &targets.mips[b] else { return };
|
||||
let side = &mips[usize::from(self.main & 1 << b != 0)];
|
||||
// Timed as one pass per buffer (its first level), so the pass timer shows the chains.
|
||||
let label = format!("mipmaps colortex{b}");
|
||||
let mut first = true;
|
||||
for (attachment, group) in &side.levels {
|
||||
let timestamp_writes = if std::mem::take(&mut first) {
|
||||
self.timing.as_mut().and_then(|t| t.writes(&label))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Shader pack mipmap"),
|
||||
timestamp_writes,
|
||||
color_attachments: &[attach(
|
||||
attachment,
|
||||
wgpu::Operations {
|
||||
@@ -3693,7 +3559,6 @@ impl PackPost {
|
||||
return;
|
||||
}
|
||||
self.setup_pending = true;
|
||||
self.rgb_unset = true;
|
||||
let sizes: [[u32; 2]; BUFFERS] =
|
||||
std::array::from_fn(|i| self.sizes[i].map_or(size, |s| s.resolve(size)));
|
||||
let full_chain = |size: [u32; 2]| 32 - size[0].max(size[1]).max(1).leading_zeros();
|
||||
@@ -4150,26 +4015,6 @@ impl PackPost {
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
// Three-channel buffers start out with alpha 1 on both sides: a pass writes the side the
|
||||
// frame's clear didn't touch.
|
||||
if self.rgb_unset {
|
||||
self.rgb_unset = false;
|
||||
let start = wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
};
|
||||
for b in (0..BUFFERS).filter(|&b| self.rgb & 1 << b != 0) {
|
||||
for alt in [false, true] {
|
||||
if let Some(v) = view(self.main, b, alt) {
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Shader pack three-channel start"),
|
||||
color_attachments: &[attach(v, start)],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let clear_op = |b: usize| {
|
||||
let c = self.clears[b].unwrap_or_default();
|
||||
wgpu::Operations {
|
||||
@@ -4190,35 +4035,26 @@ impl PackPost {
|
||||
// Attachments share a size and stay within the device's count and byte limits.
|
||||
let limits = device.limits();
|
||||
let max_attachments = (limits.max_color_attachments as usize).min(8);
|
||||
// The buffers the blit doesn't clear, and the other texture of every cleared buffer.
|
||||
let alt_clears = (0..BUFFERS)
|
||||
.filter(|&b| self.clears[b].is_some() && view(self.main, b, true).is_some())
|
||||
.map(|b| (b, true));
|
||||
let mut by_size: Vec<([u32; 2], Vec<(usize, bool)>)> = Vec::new();
|
||||
for (b, alt) in self
|
||||
.other_clears
|
||||
.iter()
|
||||
.map(|&b| (b, false))
|
||||
.chain(alt_clears)
|
||||
{
|
||||
let mut by_size: Vec<([u32; 2], Vec<usize>)> = Vec::new();
|
||||
for &b in &self.other_clears {
|
||||
let at = targets.sizes[b];
|
||||
match by_size.iter_mut().find(|(s, buffers)| {
|
||||
let bytes = buffers
|
||||
.iter()
|
||||
.fold(0, |bytes, &(b, _)| attachment_bytes(bytes, self.formats[b]));
|
||||
.fold(0, |bytes, &b| attachment_bytes(bytes, self.formats[b]));
|
||||
*s == at
|
||||
&& buffers.len() < max_attachments
|
||||
&& attachment_bytes(bytes, self.formats[b])
|
||||
<= limits.max_color_attachment_bytes_per_sample
|
||||
}) {
|
||||
Some((_, l)) => l.push((b, alt)),
|
||||
None => by_size.push((at, vec![(b, alt)])),
|
||||
Some((_, l)) => l.push(b),
|
||||
None => by_size.push((at, vec![b])),
|
||||
}
|
||||
}
|
||||
for (_, chunk) in &by_size {
|
||||
let attachments: Vec<_> = chunk
|
||||
.iter()
|
||||
.map(|&(b, alt)| attach(view(self.main, b, alt).expect("used buffer"), clear_op(b)))
|
||||
.map(|&b| attach(view(self.main, b, false).expect("used buffer"), clear_op(b)))
|
||||
.collect();
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Shader pack clear"),
|
||||
@@ -4251,18 +4087,9 @@ impl PackPost {
|
||||
self.blit_group = Some(([world.clone(), world_depth.clone()], group));
|
||||
}
|
||||
{
|
||||
// The blit leaves a three-channel colortex0's alpha at the 1 it starts with.
|
||||
let world_ops = if self.rgb & 1 != 0 {
|
||||
wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
}
|
||||
} else {
|
||||
overwrite
|
||||
};
|
||||
let mut attachments = vec![attach(
|
||||
view(self.main, 0, false).expect("colortex0"),
|
||||
world_ops,
|
||||
overwrite,
|
||||
)];
|
||||
for &b in &self.blit_clears {
|
||||
attachments.push(attach(
|
||||
@@ -4464,9 +4291,6 @@ impl PackPost {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snorm_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "pack_post/depth_test.rs"]
|
||||
mod depth_test;
|
||||
@@ -4613,7 +4437,6 @@ mod tests {
|
||||
indices: None,
|
||||
gpu: Gpu::of(&adapter, device.features()),
|
||||
storable: 0,
|
||||
rgb: 0,
|
||||
released: std::cell::Cell::new(("", 0)),
|
||||
stand_ins: &stand_ins,
|
||||
};
|
||||
@@ -4691,7 +4514,6 @@ mod tests {
|
||||
indices: None,
|
||||
gpu: Gpu::of(&adapter, device.features()),
|
||||
storable: 0,
|
||||
rgb: 0,
|
||||
released: std::cell::Cell::new(("", 0)),
|
||||
stand_ins: &stand_ins,
|
||||
};
|
||||
@@ -4772,169 +4594,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// `blend.<program>` of a composite program blends its output onto what the buffer's side
|
||||
/// holds (Iris 1.11.7, probed: 0.25 over 0.75 with SRC_ALPHA ONE_MINUS_SRC_ALPHA ONE ZERO and
|
||||
/// a source alpha of 0.25 reads 0.625).
|
||||
#[test]
|
||||
fn a_composite_program_blends_onto_its_buffer() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
|
||||
return;
|
||||
};
|
||||
// A pass's pipeline layout has six bind group slots.
|
||||
let desc = wgpu::DeviceDescriptor {
|
||||
required_limits: adapter.limits(),
|
||||
..Default::default()
|
||||
};
|
||||
let Ok((device, queue)) = pollster::block_on(adapter.request_device(&desc)) else {
|
||||
return;
|
||||
};
|
||||
let dir = std::env::temp_dir().join(format!("hz-composite-blend-{}", std::process::id()));
|
||||
let shaders = dir.join("shaders");
|
||||
std::fs::create_dir_all(&shaders).unwrap();
|
||||
let file = |name: &str, text: &str| std::fs::write(shaders.join(name), text).unwrap();
|
||||
file(
|
||||
"composite.vsh",
|
||||
"#version 120\nvoid main() { gl_Position = ftransform(); }\n",
|
||||
);
|
||||
file(
|
||||
"composite.fsh",
|
||||
"#version 120\n/* DRAWBUFFERS:3 */\n\
|
||||
void main() { gl_FragData[0] = vec4(0.25, 0.25, 1.0, 0.25); }\n",
|
||||
);
|
||||
let pack = horizon_shaders::Pack::open(&dir).unwrap();
|
||||
let properties = horizon_shaders::Properties::parse(
|
||||
"blend.composite = SRC_ALPHA ONE_MINUS_SRC_ALPHA ONE ZERO\n",
|
||||
&horizon_shaders::Macros::default(),
|
||||
);
|
||||
let mut post = horizon_shaders::load_post(
|
||||
&pack,
|
||||
"",
|
||||
&properties,
|
||||
&horizon_shaders::Macros::standard(),
|
||||
&Default::default(),
|
||||
);
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
let program = post.programs.remove(0);
|
||||
let buffers = pack_images::Buffers::new(
|
||||
&device,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
wgpu::ShaderStages::FRAGMENT,
|
||||
);
|
||||
let stand_ins = std::cell::RefCell::new(Vec::new());
|
||||
let pack = Resources {
|
||||
textures: &[],
|
||||
images: &[],
|
||||
left_out: &[],
|
||||
buffers: &buffers,
|
||||
indices: None,
|
||||
gpu: Gpu::of(&adapter, device.features()),
|
||||
storable: 0,
|
||||
rgb: 0,
|
||||
released: std::cell::Cell::new(("", 0)),
|
||||
stand_ins: &stand_ins,
|
||||
};
|
||||
let draws = DrawUniforms::new(&device);
|
||||
let mut notes = Vec::new();
|
||||
let pass = build_pass(
|
||||
&device,
|
||||
program,
|
||||
&[T::Rgba8Unorm; BUFFERS],
|
||||
None,
|
||||
T::Rgba8Unorm,
|
||||
&mut notes,
|
||||
None,
|
||||
&[],
|
||||
&draws.layout,
|
||||
&pack,
|
||||
)
|
||||
.unwrap_or_else(|| panic!("the pass builds: {notes:?}"));
|
||||
// colortex3 holds (0.75, 0.5, 0.25, 0.5) before the pass.
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: 64,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: T::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = texture.create_view(&Default::default());
|
||||
let head = [64.0f32, 1.0, 1.0 / 64.0, 1.0];
|
||||
queue.write_buffer(&pass.ubo, 0, bytemuck::cast_slice(&head));
|
||||
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &pass.layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: pass.ubo.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
let before = wgpu::Color {
|
||||
r: 0.75,
|
||||
g: 0.5,
|
||||
b: 0.25,
|
||||
a: 0.5,
|
||||
};
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[attach(
|
||||
&view,
|
||||
wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(before),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
)],
|
||||
..Default::default()
|
||||
});
|
||||
{
|
||||
// Loaded, as `PackPost::render` attaches a buffer a composite pass writes.
|
||||
let load = wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
};
|
||||
let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[attach(&view, load)],
|
||||
..Default::default()
|
||||
});
|
||||
rp.set_pipeline(pass.render());
|
||||
rp.set_bind_group(0, &group, &[]);
|
||||
rp.draw(0..6, 0..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,
|
||||
});
|
||||
encoder.copy_texture_to_buffer(
|
||||
texture.as_image_copy(),
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &read,
|
||||
layout: wgpu::TexelCopyBufferLayout::default(),
|
||||
},
|
||||
texture.size(),
|
||||
);
|
||||
queue.submit([encoder.finish()]);
|
||||
read.slice(..).map_async(wgpu::MapMode::Read, |_| {});
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
let texel: Vec<u8> = read.slice(..).get_mapped_range().unwrap()[..4].to_vec();
|
||||
// rgb = 0.25 × src + 0.75 × dst, alpha = the source's.
|
||||
let expected = [0.625, 0.4375, 0.4375, 0.25].map(|v: f32| v * 255.0);
|
||||
for (got, want) in texel.iter().zip(expected) {
|
||||
assert!(
|
||||
(f32::from(*got) - want).abs() <= 1.0,
|
||||
"{texel:?} vs {expected:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_formats_map_to_renderable_ones() {
|
||||
let f = |name| texture_format(Format::parse(name).unwrap(), wgpu::Features::empty());
|
||||
@@ -4995,6 +4654,3 @@ mod tests {
|
||||
|
||||
#[cfg(test)]
|
||||
mod destination_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod rgb_alpha_tests;
|
||||
@@ -151,14 +151,14 @@ fn pack(
|
||||
post
|
||||
}
|
||||
|
||||
pub(super) struct Fixture {
|
||||
pub(super) adapter: wgpu::Adapter,
|
||||
pub(super) device: wgpu::Device,
|
||||
pub(super) queue: wgpu::Queue,
|
||||
struct Fixture {
|
||||
adapter: wgpu::Adapter,
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
pub(super) fn new() -> Option<Self> {
|
||||
fn new() -> Option<Self> {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
|
||||
eprintln!("Post destination test skipped: no GPU adapter");
|
||||
@@ -198,7 +198,7 @@ impl Fixture {
|
||||
.create_view(&Default::default())
|
||||
}
|
||||
|
||||
pub(super) fn build(&self, post: Post) -> PackPost {
|
||||
fn build(&self, post: Post) -> PackPost {
|
||||
let lightmap = self.texture([1, 1], T::Rgba8Unorm);
|
||||
let post = PackPost::new(
|
||||
&self.device,
|
||||
@@ -226,7 +226,7 @@ impl Fixture {
|
||||
post
|
||||
}
|
||||
|
||||
pub(super) fn render(&self, post: &mut PackPost, size: [u32; 2]) -> Vec<[u8; 4]> {
|
||||
fn render(&self, post: &mut PackPost, size: [u32; 2]) -> Vec<[u8; 4]> {
|
||||
let world = self.texture(size, T::Rgba8Unorm);
|
||||
let depth = self.texture(size, T::Depth32Float);
|
||||
let screen = self.texture(size, T::Rgba8Unorm);
|
||||
@@ -454,70 +454,3 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
|
||||
#[path = "clear_budget_tests.rs"]
|
||||
mod clear_budget_tests;
|
||||
|
||||
/// A cleared buffer written three times a frame flips an odd number of times, so its first write
|
||||
/// goes to the texture that was "main" the frame before, which a later program wrote after that
|
||||
/// frame's clear. Where that first write discards, Iris 1.11.7 reads the clear value (a probe
|
||||
/// pack of our own with these three programs reads 0 there, not the previous frame's 0.25).
|
||||
#[test]
|
||||
fn a_cleared_buffer_starts_every_frame_cleared_on_both_textures() {
|
||||
let Some(gpu) = Fixture::new() else { return };
|
||||
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();
|
||||
};
|
||||
for (program, fragment) in [
|
||||
(
|
||||
"composite",
|
||||
"#version 120\nvarying vec2 uv;\n/* RENDERTARGETS:4 */\n\
|
||||
void main() { if(uv.x>0.5) discard; gl_FragData[0]=vec4(0.5,0.5,0.5,1); }\n",
|
||||
),
|
||||
(
|
||||
"composite1",
|
||||
"#version 120\nuniform sampler2D colortex4;\nvarying vec2 uv;\n/* RENDERTARGETS:4,5 */\n\
|
||||
void main() { gl_FragData[0]=vec4(0.25,0.25,0.25,1); gl_FragData[1]=texture2D(colortex4,uv); }\n",
|
||||
),
|
||||
(
|
||||
"composite2",
|
||||
"#version 120\n/* RENDERTARGETS:4 */\nvoid main() { gl_FragData[0]=vec4(0.125,0.125,0.125,1); }\n",
|
||||
),
|
||||
(
|
||||
"final",
|
||||
"#version 120\nuniform sampler2D colortex5;\nvarying vec2 uv;\n\
|
||||
void main() { gl_FragColor=vec4(texture2D(colortex5,uv).rgb,1); }\n",
|
||||
),
|
||||
] {
|
||||
add(&format!("{program}.vsh"), VERTEX);
|
||||
add(&format!("{program}.fsh"), fragment);
|
||||
}
|
||||
let pack = horizon_shaders::Pack::from_zip_bytes(
|
||||
"both-textures-cleared",
|
||||
zip.finish().unwrap().into_inner(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut macros = horizon_shaders::Macros::standard();
|
||||
let (properties, _) = horizon_shaders::read_properties(&pack, &mut macros);
|
||||
let post = horizon_shaders::load_post(&pack, "", &properties, ¯os, &Default::default());
|
||||
let mut post = gpu.build(post);
|
||||
for frame in 0..3 {
|
||||
let actual = gpu.render(&mut post, [16, 16]);
|
||||
let wrong: Vec<_> = (actual.iter().enumerate())
|
||||
.filter(|(n, p)| {
|
||||
let expected = if n % 16 < 8 {
|
||||
[128, 128, 128, 255]
|
||||
} else {
|
||||
[0, 0, 0, 255]
|
||||
};
|
||||
p.iter().zip(expected).any(|(a, b)| a.abs_diff(b) > 1)
|
||||
})
|
||||
.map(|(n, p)| (n, *p))
|
||||
.take(4)
|
||||
.collect();
|
||||
assert!(wrong.is_empty(), "frame {frame}: {wrong:?}");
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
//! `HIGHER_SHADOWCOLOR`: shadowcolor2 to 7 are written by shadow programs' directives, read by
|
||||
//! them and by the other programs, and cleared to their own colours, as in Iris 1.11.7 (probe
|
||||
//! packs p7951a to c; the PR has the readings). The programs here are original and cut down.
|
||||
use super::destination_tests::Fixture;
|
||||
use super::*;
|
||||
use std::io::{Cursor, Write};
|
||||
|
||||
const VERTEX: &str = "#version 120\nvarying vec2 uv;\nvoid main() { uv=gl_MultiTexCoord0.xy; gl_Position=ftransform(); }\n";
|
||||
|
||||
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();
|
||||
};
|
||||
add("shadowcomp.vsh", VERTEX);
|
||||
add(
|
||||
"shadowcomp.fsh",
|
||||
"#version 120\n/* RENDERTARGETS: 2,7 */\n\
|
||||
void main() { gl_FragData[0]=vec4(0.25,0,0,1); gl_FragData[1]=vec4(0,0.5,0,1); }\n",
|
||||
);
|
||||
// A shadow program reads a higher buffer it didn't write.
|
||||
add("shadowcomp1.vsh", VERTEX);
|
||||
add(
|
||||
"shadowcomp1.fsh",
|
||||
"#version 120\n/* RENDERTARGETS: 3 */\nuniform sampler2D shadowcolor7;\nvarying vec2 uv;\n\
|
||||
void main() { gl_FragData[0]=texture2D(shadowcolor7,uv); }\n",
|
||||
);
|
||||
add("final.vsh", VERTEX);
|
||||
// Pixel column x shows shadowcolor 2, 3, 5 and 6 in turn.
|
||||
add(
|
||||
"final.fsh",
|
||||
"#version 330 compatibility\nconst int shadowMapResolution=16;\n\
|
||||
const vec4 shadowcolor5ClearColor=vec4(0,0,0.75,1);\n\
|
||||
uniform sampler2D shadowcolor2;\nuniform sampler2D shadowcolor3;\n\
|
||||
uniform sampler2D shadowcolor5;\nuniform sampler2D shadowcolor6;\nin vec2 uv;\n\
|
||||
void main() {\n vec4 c[4] = vec4[4](texture(shadowcolor2,uv), texture(shadowcolor3,uv),\n\
|
||||
texture(shadowcolor5,uv), texture(shadowcolor6,uv));\n\
|
||||
gl_FragColor = c[int(gl_FragCoord.x) % 4];\n}\n",
|
||||
);
|
||||
add(
|
||||
"shaders.properties",
|
||||
"shadow.enabled=true\niris.features.required = HIGHER_SHADOWCOLOR\n",
|
||||
);
|
||||
let pack = horizon_shaders::Pack::from_zip_bytes(
|
||||
"original-higher-shadowcolor",
|
||||
zip.finish().unwrap().into_inner(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut macros = horizon_shaders::Macros::standard();
|
||||
let (properties, _) = horizon_shaders::read_properties(&pack, &mut macros);
|
||||
horizon_shaders::load_post(&pack, "", &properties, ¯os, &Default::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn higher_shadowcolor_buffers_are_written_read_and_cleared() {
|
||||
let Some(gpu) = Fixture::new() else { return };
|
||||
let mut post = gpu.build(pack());
|
||||
// Two frames: the second starts from the first one's buffers.
|
||||
for frame in 0..2 {
|
||||
let pixels = gpu.render(&mut post, [8, 8]);
|
||||
// shadowcolor2 written red, 3 a copy of 7 (green), 5 its clear colour, 6 white.
|
||||
let expected = [
|
||||
[64, 0, 0, 255],
|
||||
[0, 128, 0, 255],
|
||||
[0, 0, 191, 255],
|
||||
[255; 4],
|
||||
];
|
||||
for (x, want) in expected.iter().enumerate() {
|
||||
let got = pixels[x];
|
||||
assert!(
|
||||
got.iter().zip(want).all(|(g, w)| g.abs_diff(*w) <= 1),
|
||||
"frame {frame}, column {x}: {got:?}, expected {want:?}; row {:?}",
|
||||
&pixels[..4]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
//! Three-channel buffers read alpha 1, as in Iris 1.11.7: written with alpha 0, cleared with
|
||||
//! alpha 0, or never written. The values come from probe pack p8106 run in Iris (the PR has
|
||||
//! them); the programs here are the probe's, cut down.
|
||||
use super::destination_tests::Fixture;
|
||||
use super::*;
|
||||
use std::io::{Cursor, Write};
|
||||
|
||||
const VERTEX: &str = "#version 120\nvarying vec2 uv;\nvoid main() { uv=gl_MultiTexCoord0.xy; gl_Position=ftransform(); }\n";
|
||||
|
||||
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();
|
||||
};
|
||||
add("composite.vsh", VERTEX);
|
||||
add(
|
||||
"composite.fsh",
|
||||
"#version 120\n/* RENDERTARGETS: 1,5,7 */\n\
|
||||
/*\nconst int colortex0Format = RGB16F;\nconst int colortex1Format = RGB16F;\n\
|
||||
const int colortex2Format = RGB8;\nconst int colortex3Format = RGB32F;\n\
|
||||
const int colortex4Format = RGB32UI;\nconst int colortex5Format = RGBA16F;\n\
|
||||
const int colortex7Format = RGB16;\n*/\n\
|
||||
const vec4 colortex2ClearColor = vec4(0.25, 0.5, 0.75, 0.0);\n\
|
||||
const bool colortex3Clear = false;\n\
|
||||
void main() {\n gl_FragData[0] = vec4(0.25, 0.5, 0.75, 0.0);\n\
|
||||
gl_FragData[1] = vec4(0.25, 0.5, 0.75, 0.0);\n gl_FragData[2] = vec4(0.25, 0.5, 0.75, 0.0);\n}\n",
|
||||
);
|
||||
add("composite1.vsh", VERTEX);
|
||||
add(
|
||||
"composite1.fsh",
|
||||
"#version 330 compatibility\n/* RENDERTARGETS: 4 */\nlayout(location = 0) out uvec4 o;\n\
|
||||
void main() { o = uvec4(5u, 6u, 7u, 0u); }\n",
|
||||
);
|
||||
add("final.vsh", VERTEX);
|
||||
// Pixel column x shows buffer x % 8: its alpha in red, its green in green.
|
||||
add(
|
||||
"final.fsh",
|
||||
"#version 330 compatibility\nuniform sampler2D colortex0;\nuniform sampler2D colortex1;\n\
|
||||
uniform sampler2D colortex2;\nuniform sampler2D colortex3;\nuniform usampler2D colortex4;\n\
|
||||
uniform sampler2D colortex5;\nuniform sampler2D colortex7;\n\
|
||||
void main() {\n ivec2 p = ivec2(gl_FragCoord.xy);\n vec4 c = vec4(0.0);\n\
|
||||
switch (p.x % 8) {\n\
|
||||
case 0: c = texelFetch(colortex0, p, 0); break;\n\
|
||||
case 1: c = texelFetch(colortex1, p, 0); break;\n\
|
||||
case 2: c = texelFetch(colortex2, p, 0); break;\n\
|
||||
case 3: c = texelFetch(colortex3, p, 0); break;\n\
|
||||
case 4: c = vec4(texelFetch(colortex4, p, 0)) / 12.0; c.a *= 12.0; break;\n\
|
||||
case 5: c = texelFetch(colortex5, p, 0); break;\n\
|
||||
case 7: c = texelFetch(colortex7, p, 0); break;\n\
|
||||
}\n gl_FragColor = vec4(c.a, c.g, 0.0, 1.0);\n}\n",
|
||||
);
|
||||
let pack = horizon_shaders::Pack::from_zip_bytes(
|
||||
"original-rgb-alpha",
|
||||
zip.finish().unwrap().into_inner(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut macros = horizon_shaders::Macros::standard();
|
||||
let (properties, _) = horizon_shaders::read_properties(&pack, &mut macros);
|
||||
let post = horizon_shaders::load_post(&pack, "", &properties, ¯os, &Default::default());
|
||||
assert_eq!(post.programs.len(), 3, "{:?}", post.notes);
|
||||
post
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_channel_buffers_read_alpha_1() {
|
||||
let Some(gpu) = Fixture::new() else { return };
|
||||
let mut post = gpu.build(pack());
|
||||
// Twice: the second frame reads buffers that a first frame already used.
|
||||
for frame in 0..2 {
|
||||
let pixels = gpu.render(&mut post, [8, 8]);
|
||||
// (alpha, green) per buffer: 0 is the world (black) through the blit; 1, 5 and 7 are
|
||||
// written with alpha 0, 2 is cleared with alpha 0, 3 is never cleared nor written, 4 is
|
||||
// RGB32UI written (5, 6, 7, 0). Only 5 (RGBA16F) keeps the alpha 0 it was given.
|
||||
let expected = [
|
||||
[255, 0],
|
||||
[255, 128],
|
||||
[255, 128],
|
||||
[255, 0],
|
||||
[255, 128],
|
||||
[0, 128],
|
||||
[0, 0],
|
||||
[255, 128],
|
||||
];
|
||||
let row: Vec<[u8; 2]> = pixels[..8].iter().map(|p| [p[0], p[1]]).collect();
|
||||
for (b, (got, want)) in row.iter().zip(expected).enumerate() {
|
||||
if b == 6 {
|
||||
continue;
|
||||
}
|
||||
assert!(
|
||||
got.iter().zip(want).all(|(g, w)| g.abs_diff(w) <= 1),
|
||||
"frame {frame}, colortex{b}: (alpha, green) {got:?}, expected {want:?}; row {row:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
//! SNORM colortex buffers clamp to [-1, 1] and quantise, as in Iris 1.11.7, where the adapter
|
||||
//! renders to SNORM formats. Expected values are those probe pack p8106 read back in Iris (the
|
||||
//! PR has them); the programs here are the probe's, cut down. Values that land halfway between
|
||||
//! two steps (-0.5 × 127) are left out: GPUs may round them either way.
|
||||
use super::destination_tests::Fixture;
|
||||
use super::*;
|
||||
use std::io::{Cursor, Write};
|
||||
|
||||
const VERTEX: &str = "#version 120\nvarying vec2 uv;\nvoid main() { uv=gl_MultiTexCoord0.xy; gl_Position=ftransform(); }\n";
|
||||
|
||||
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();
|
||||
};
|
||||
add("composite.vsh", VERTEX);
|
||||
add(
|
||||
"composite.fsh",
|
||||
"#version 120\n/* RENDERTARGETS: 1,2,3 */\n\
|
||||
/*\nconst int colortex1Format = RGBA8_SNORM;\nconst int colortex2Format = RGBA16_SNORM;\n\
|
||||
const int colortex3Format = R16_SNORM;\nconst int colortex4Format = RGBA8_SNORM;\n*/\n\
|
||||
const vec4 colortex4ClearColor = vec4(-1.5, 0.1, 0.3337, 0.0);\n\
|
||||
void main() {\n gl_FragData[0] = vec4(-1.5, 0.1, 0.3337, 1000.0);\n\
|
||||
gl_FragData[1] = vec4(-1.5, 0.1, 0.3337, 1000.0);\n gl_FragData[2] = vec4(0.1);\n}\n",
|
||||
);
|
||||
add("final.vsh", VERTEX);
|
||||
// Pixel column x shows component x % 4 of buffer 1 + x / 4, as the bytes of its bits.
|
||||
add(
|
||||
"final.fsh",
|
||||
"#version 330 compatibility\nuniform sampler2D colortex1;\nuniform sampler2D colortex2;\n\
|
||||
uniform sampler2D colortex3;\nuniform sampler2D colortex4;\n\
|
||||
void main() {\n ivec2 p = ivec2(gl_FragCoord.xy);\n vec4 c = vec4(0.0);\n\
|
||||
switch (p.x / 4) {\n\
|
||||
case 0: c = texelFetch(colortex1, p, 0); break;\n\
|
||||
case 1: c = texelFetch(colortex2, p, 0); break;\n\
|
||||
case 2: c = texelFetch(colortex3, p, 0); break;\n\
|
||||
case 3: c = texelFetch(colortex4, p, 0); break;\n\
|
||||
}\n uint u = floatBitsToUint(c[p.x % 4]);\n\
|
||||
gl_FragColor = vec4(uvec4(u >> 24u, u >> 16u, u >> 8u, u) & 255u) / 255.0;\n}\n",
|
||||
);
|
||||
let pack =
|
||||
horizon_shaders::Pack::from_zip_bytes("original-snorm", zip.finish().unwrap().into_inner())
|
||||
.unwrap();
|
||||
let mut macros = horizon_shaders::Macros::standard();
|
||||
let (properties, _) = horizon_shaders::read_properties(&pack, &mut macros);
|
||||
let post = horizon_shaders::load_post(&pack, "", &properties, ¯os, &Default::default());
|
||||
assert_eq!(post.programs.len(), 2, "{:?}", post.notes);
|
||||
post
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snorm_buffers_clamp_and_quantise() {
|
||||
let Some(gpu) = Fixture::new() else { return };
|
||||
// The fixture's device asks for no features; SNORM targets need these.
|
||||
let wanted = wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES
|
||||
| wgpu::Features::TEXTURE_FORMAT_16BIT_NORM;
|
||||
let renders = [
|
||||
wgpu::TextureFormat::Rgba8Snorm,
|
||||
wgpu::TextureFormat::Rgba16Snorm,
|
||||
]
|
||||
.iter()
|
||||
.all(|&f| {
|
||||
(gpu.adapter.get_texture_format_features(f).allowed_usages)
|
||||
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
|
||||
});
|
||||
if !gpu.adapter.features().contains(wanted) || !renders {
|
||||
eprintln!("SNORM test skipped: this adapter doesn't render to SNORM formats");
|
||||
return;
|
||||
}
|
||||
let (device, queue) = pollster::block_on(gpu.adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_features: wanted,
|
||||
required_limits: gpu.adapter.limits(),
|
||||
..Default::default()
|
||||
}))
|
||||
.expect("GPU device with SNORM formats");
|
||||
let gpu = Fixture {
|
||||
adapter: gpu.adapter,
|
||||
device,
|
||||
queue,
|
||||
};
|
||||
let mut post = gpu.build(pack());
|
||||
let pixels = gpu.render(&mut post, [16, 1]);
|
||||
let read: Vec<f32> = pixels
|
||||
.iter()
|
||||
.map(|p| f32::from_bits(u32::from_be_bytes(*p)))
|
||||
.collect();
|
||||
// Iris 1.11.7: RGBA8_SNORM written (-1.5, 0.1, 0.3337, 1000); RGBA16_SNORM the same;
|
||||
// R16_SNORM written 0.1 (G and B read 0, alpha 1); RGBA8_SNORM cleared to
|
||||
// (-1.5, 0.1, 0.3337, 0).
|
||||
let expected = [
|
||||
-1.0,
|
||||
0.102_362_2,
|
||||
0.330_708_7,
|
||||
1.0, //
|
||||
-1.0,
|
||||
0.100_009_2,
|
||||
0.333_689_4,
|
||||
1.0, //
|
||||
0.100_009_2,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0, //
|
||||
-1.0,
|
||||
0.102_362_2,
|
||||
0.330_708_7,
|
||||
0.0,
|
||||
];
|
||||
for (n, (got, want)) in read.iter().zip(expected).enumerate() {
|
||||
assert!(
|
||||
(got - want).abs() <= 1e-6,
|
||||
"buffer {} component {}: {got}, expected {want}; all {read:?}",
|
||||
1 + n / 4,
|
||||
n % 4
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
//! A shader pack's shadow buffers (spec 6.3): the shadow map's depth (`shadowtex0`, and
|
||||
//! `shadowtex1` without translucent geometry), and the colour buffers `shadowcolor0` to `7`, each
|
||||
//! `shadowtex1` without translucent geometry), and the colour buffers `shadowcolor0/1`, each
|
||||
//! with two sides that `shadowcomp` programs flip as composite programs flip colortex (6.1.6).
|
||||
//! Square, `shadowMapResolution` pixels.
|
||||
|
||||
use horizon_shaders::SHADOW_COLORS;
|
||||
|
||||
type Ops<T> = wgpu::Operations<T>;
|
||||
type Color = wgpu::Color;
|
||||
|
||||
@@ -36,9 +34,9 @@ pub(super) struct ShadowTargets {
|
||||
/// `shadowtex1`: a copy of `shadowtex0` taken before translucent terrain, when read.
|
||||
pub depth1: Option<(wgpu::Texture, wgpu::TextureView)>,
|
||||
/// Both sides of each shadowcolor buffer used.
|
||||
pub color: [Option<[wgpu::TextureView; 2]>; SHADOW_COLORS],
|
||||
pub color: [Option<[wgpu::TextureView; 2]>; 2],
|
||||
/// The clear colour of each buffer cleared every frame.
|
||||
pub clears: [Option<[f32; 4]>; SHADOW_COLORS],
|
||||
pub clears: [Option<[f32; 4]>; 2],
|
||||
/// Bit n set: B is shadowcolor n's "main".
|
||||
pub main: u16,
|
||||
/// Nothing cleared yet this frame: the first pass into the shadow buffers clears them as it
|
||||
@@ -81,7 +79,7 @@ impl ShadowTargets {
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
shadow: &horizon_shaders::Shadow,
|
||||
formats: [wgpu::TextureFormat; SHADOW_COLORS],
|
||||
formats: [wgpu::TextureFormat; 2],
|
||||
colors: u16,
|
||||
depth1: bool,
|
||||
) -> Self {
|
||||
@@ -145,12 +143,12 @@ impl ShadowTargets {
|
||||
pub fn clear_ops(
|
||||
&mut self,
|
||||
writes: impl Iterator<Item = usize>,
|
||||
) -> (Ops<f32>, [Ops<Color>; SHADOW_COLORS]) {
|
||||
) -> (Ops<f32>, [Ops<Color>; 2]) {
|
||||
if !std::mem::take(&mut self.fresh) {
|
||||
return (load(), [load(); SHADOW_COLORS]);
|
||||
return (load(), [load(); 2]);
|
||||
}
|
||||
let mut colors = [load(); SHADOW_COLORS];
|
||||
for n in writes.filter(|&n| n < SHADOW_COLORS) {
|
||||
let mut colors = [load(); 2];
|
||||
for n in writes.filter(|&n| n < 2) {
|
||||
if let Some(c) = self.clears[n] {
|
||||
colors[n] = clear(c);
|
||||
self.cleared |= 1 << n;
|
||||
@@ -164,12 +162,11 @@ impl ShadowTargets {
|
||||
}
|
||||
|
||||
/// Clears what no pass has cleared this frame: all of it when no shadow program drew, the
|
||||
/// cleared shadowcolor buffers the first one did not write. One buffer a pass: eight wide
|
||||
/// formats together would pass the device's bytes-per-sample limit.
|
||||
/// cleared shadowcolor buffers the first one did not write.
|
||||
pub fn clear_rest(&mut self, encoder: &mut wgpu::CommandEncoder) {
|
||||
let mut depth = std::mem::take(&mut self.fresh);
|
||||
let cleared = std::mem::replace(&mut self.cleared, u8::MAX);
|
||||
let attachments = (0..SHADOW_COLORS)
|
||||
let depth = std::mem::take(&mut self.fresh);
|
||||
let cleared = std::mem::replace(&mut self.cleared, 0b11);
|
||||
let attachments: Vec<_> = (0..2)
|
||||
.filter(|n| cleared & 1 << n == 0)
|
||||
.filter_map(|n| {
|
||||
Some(Some(wgpu::RenderPassColorAttachment {
|
||||
@@ -178,30 +175,24 @@ impl ShadowTargets {
|
||||
resolve_target: None,
|
||||
ops: clear(self.clears[n]?),
|
||||
}))
|
||||
});
|
||||
let passes: Vec<Vec<_>> = attachments.map(|a| vec![a]).collect();
|
||||
let passes = if passes.is_empty() && depth {
|
||||
vec![Vec::new()]
|
||||
} else {
|
||||
passes
|
||||
};
|
||||
for attachments in &passes {
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Shader pack shadow clear"),
|
||||
color_attachments: attachments,
|
||||
depth_stencil_attachment: std::mem::take(&mut depth).then_some(
|
||||
wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &self.depth.1,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
},
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
})
|
||||
.collect();
|
||||
if !depth && attachments.is_empty() {
|
||||
return;
|
||||
}
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Shader pack shadow clear"),
|
||||
color_attachments: &attachments,
|
||||
depth_stencil_attachment: depth.then_some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &self.depth.1,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
/// `shadowtex1`: the shadow map as it is now (before translucent geometry).
|
||||
|
||||
@@ -265,13 +265,6 @@ pub const NAMES: &[&str] = &[
|
||||
"playerMood",
|
||||
"constantMood",
|
||||
"centerDepthSmooth",
|
||||
"alphaTestRef",
|
||||
"entityId",
|
||||
"blockEntityId",
|
||||
"entityColor",
|
||||
GBUFFERS_FOG_MODE,
|
||||
GBUFFERS_FOG_SHAPE,
|
||||
GBUFFERS_CENTER_DEPTH,
|
||||
"isRightHanded",
|
||||
"is_on_ground",
|
||||
"bedrockLevel",
|
||||
@@ -284,13 +277,6 @@ pub const NAMES: &[&str] = &[
|
||||
"cloudHeight",
|
||||
];
|
||||
|
||||
/// What programs that draw the game's geometry (gbuffers, shadow) read as `fogMode`,
|
||||
/// `fogShape` and `centerDepthSmooth`: no GLSL name can hold a dot, so no pack reads these
|
||||
/// directly.
|
||||
const GBUFFERS_FOG_MODE: &str = "gbuffers.fogMode";
|
||||
const GBUFFERS_FOG_SHAPE: &str = "gbuffers.fogShape";
|
||||
const GBUFFERS_CENTER_DEPTH: &str = "gbuffers.centerDepthSmooth";
|
||||
|
||||
/// Values carried from frame to frame.
|
||||
pub struct FrameUniforms {
|
||||
pub values: Vec<Value>,
|
||||
@@ -486,16 +472,18 @@ impl FrameUniforms {
|
||||
});
|
||||
}
|
||||
let center_depth = self.center_depth.unwrap_or(1.0);
|
||||
// Vanilla fades by the stronger of two linear fogs; a pack gets the environmental one
|
||||
// (`FogData::environmental_start`/`environmental_end`), as Iris 1.11.7 gives it in every
|
||||
// program kind: 0 to 1024 on a clear day, -160 to 768 in rain, -8 to 96 under water, 10
|
||||
// to 96 in the Nether (measured with a probe pack).
|
||||
// Vanilla fades by the stronger of two linear fogs; a pack gets one: the pair (start
|
||||
// and end together) of the one that is complete nearer (OQ-36). Mixing the nearer start
|
||||
// of one with the nearer end of the other made fog begin at the camera on clear days.
|
||||
let (fog_color, fog_start, fog_end) = match &frame.fog {
|
||||
Some(f) => (
|
||||
[f.color[0], f.color[1], f.color[2]],
|
||||
f.environmental_start,
|
||||
f.environmental_end,
|
||||
),
|
||||
Some(f) => {
|
||||
let (start, end) = if f.environmental_end < f.render_distance_end {
|
||||
(f.environmental_start, f.environmental_end)
|
||||
} else {
|
||||
(f.render_distance_start, f.render_distance_end)
|
||||
};
|
||||
([f.color[0], f.color[1], f.color[2]], start, end)
|
||||
}
|
||||
None => (frame.sky.fog_color, frame.distance * 0.9, frame.distance),
|
||||
};
|
||||
let light = shadow_light(frame, &self.shadow);
|
||||
@@ -557,22 +545,12 @@ impl FrameUniforms {
|
||||
"thunderStrength" => Value::f(&[inputs.thunder]),
|
||||
"fogColor" => Value::f(&fog_color),
|
||||
"skyColor" => Value::f(&frame.sky.sky_color),
|
||||
// Iris 1.11.7, measured: programs that draw no geometry read no fog mode (0) and
|
||||
// shape (-1); those that do read GL_LINEAR and a cylinder (1). The density is 0.
|
||||
"fogMode" => Value::i(&[0]),
|
||||
GBUFFERS_FOG_MODE => Value::i(&[9729]),
|
||||
// GL_LINEAR: vanilla's fog is linear.
|
||||
"fogMode" => Value::i(&[9729]),
|
||||
"fogStart" => Value::f(&[fog_start]),
|
||||
"fogEnd" => Value::f(&[fog_end]),
|
||||
"fogDensity" => Value::f(&[0.0]),
|
||||
"fogShape" => Value::i(&[-1]),
|
||||
GBUFFERS_FOG_SHAPE => Value::i(&[1]),
|
||||
// Iris 1.11.7 gives gbuffers programs 0 (measured in 16 scenes).
|
||||
GBUFFERS_CENTER_DEPTH => Value::f(&[0.0]),
|
||||
// Outside a draw (gbuffers programs get these per draw), as Iris 1.11.7 gives
|
||||
// them to composite and final programs (measured).
|
||||
"alphaTestRef" => Value::f(&[0.1]),
|
||||
"blockEntityId" => Value::i(&[-1]),
|
||||
"entityColor" => Value::f(&[0.0; 4]),
|
||||
"fogDensity" => Value::f(&[1.0]),
|
||||
"fogShape" => Value::i(&[0]),
|
||||
"isEyeInWater" => Value::i(&[inputs.eye_in]),
|
||||
"blindness" => Value::f(&[inputs.blindness]),
|
||||
"nightVision" => Value::f(&[frame.night_vision]),
|
||||
@@ -712,7 +690,6 @@ fn components(name: &str) -> usize {
|
||||
| "skyColor"
|
||||
| "endFlashPosition" => 3,
|
||||
"eyeBrightness" | "eyeBrightnessSmooth" | "atlasSize" => 2,
|
||||
"entityColor" => 4,
|
||||
n if n.starts_with("gbuffer") || n.starts_with("shadow") && n != "shadowAngle" => 16,
|
||||
_ => 1,
|
||||
}
|
||||
@@ -723,18 +700,6 @@ pub fn index(name: &str) -> Option<usize> {
|
||||
NAMES.iter().position(|n| *n == name)
|
||||
}
|
||||
|
||||
/// The position in [`NAMES`] of the value a program reads as `name`; `geometry`: the program
|
||||
/// draws the game's geometry (gbuffers, shadow), which reads its own fog mode and shape and no
|
||||
/// centre depth.
|
||||
pub fn index_in(name: &str, geometry: bool) -> Option<usize> {
|
||||
match name {
|
||||
"fogMode" if geometry => index(GBUFFERS_FOG_MODE),
|
||||
"fogShape" if geometry => index(GBUFFERS_FOG_SHAPE),
|
||||
"centerDepthSmooth" if geometry => index(GBUFFERS_CENTER_DEPTH),
|
||||
_ => index(name),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1213,64 +1178,6 @@ mod initial_history_tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn int(u: &FrameUniforms, at: usize) -> i32 {
|
||||
match u.values[at] {
|
||||
Value::I(v) => v[0],
|
||||
_ => panic!("integer uniform expected"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iris 1.11.7, read back exactly with a probe pack on a clear day (render distance 12,
|
||||
/// cloud range 64): fogStart 0 and fogEnd 1024 (the environmental fog, not the render
|
||||
/// distance's 172.8 to 192), fogDensity 0; composite and final programs read alphaTestRef
|
||||
/// 0.1, blockEntityId -1, fogMode 0 and fogShape -1.
|
||||
#[test]
|
||||
fn fog_and_draw_uniforms_outside_a_draw_follow_iris() {
|
||||
let hud = HudBatch::new([800, 450]);
|
||||
let mut f = frame(&hud);
|
||||
f.fog = Some(crate::fog::FogData {
|
||||
color: [0.7, 0.8, 1.0, 1.0],
|
||||
environmental_start: 0.0,
|
||||
environmental_end: 1024.0,
|
||||
render_distance_start: 172.8,
|
||||
render_distance_end: 192.0,
|
||||
sky_end: 192.0,
|
||||
cloud_end: 1024.0,
|
||||
});
|
||||
let mut u = FrameUniforms::default();
|
||||
u.update(&f, &PackInputs::default(), [800, 450]);
|
||||
assert_eq!(float(&u, "fogStart")[0], 0.0);
|
||||
assert_eq!(float(&u, "fogEnd")[0], 1024.0);
|
||||
assert_eq!(float(&u, "fogDensity")[0], 0.0);
|
||||
assert_eq!(float(&u, "alphaTestRef")[0], 0.1);
|
||||
assert_eq!(int(&u, index("blockEntityId").unwrap()), -1);
|
||||
assert_eq!(int(&u, index("entityId").unwrap()), 0);
|
||||
assert_eq!(int(&u, index("fogMode").unwrap()), 0);
|
||||
assert_eq!(int(&u, index("fogShape").unwrap()), -1);
|
||||
}
|
||||
|
||||
/// Iris 1.11.7 (probe pack): gbuffers programs read fogMode GL_LINEAR (9729) and fogShape 1
|
||||
/// (cylinder) where composite and final programs read 0 and -1.
|
||||
#[test]
|
||||
fn programs_that_draw_geometry_read_their_own_fog_and_centre_depth() {
|
||||
let hud = HudBatch::new([800, 450]);
|
||||
let mut u = FrameUniforms::default();
|
||||
u.update(&frame(&hud), &PackInputs::default(), [800, 450]);
|
||||
let at = |name, geometry| index_in(name, geometry).unwrap();
|
||||
assert_eq!(int(&u, at("fogMode", true)), 9729);
|
||||
assert_eq!(int(&u, at("fogShape", true)), 1);
|
||||
assert_eq!(int(&u, at("fogMode", false)), 0);
|
||||
assert_eq!(int(&u, at("fogShape", false)), -1);
|
||||
assert_eq!(at("fogStart", true), at("fogStart", false));
|
||||
// Iris 1.11.7 (probe pack): `centerDepthSmooth` reads 0 in gbuffers programs.
|
||||
let depth = |geometry| match u.values[at("centerDepthSmooth", geometry)] {
|
||||
Value::F(v) => v[0],
|
||||
_ => panic!("float uniform expected"),
|
||||
};
|
||||
assert_eq!(depth(true), 0.0);
|
||||
assert_eq!(depth(false), 1.0);
|
||||
}
|
||||
|
||||
/// Iris 1.11.7, read frame by frame with a probe pack: eyeBrightnessSmooth falls by half
|
||||
/// in 1 s and is rounded down; wetness takes 60 s to rise halfway and 20 s to fall; the
|
||||
/// centre depth 0.1 s.
|
||||
|
||||
@@ -162,7 +162,7 @@ impl PackWeather {
|
||||
|
||||
/// A texture holding the atlas's texels under `rect` (normalised x, y, width, height), as
|
||||
/// Horizon's weather shader reads them (`weather_texel`).
|
||||
pub(super) fn copy_sprite(
|
||||
fn copy_sprite(
|
||||
device: &wgpu::Device,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
atlas: &wgpu::Texture,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! Screenshots of a swapchain that cannot be copied from. OpenGL's surfaces
|
||||
//! offer `RENDER_ATTACHMENT` only, so a frame whose pixels are read back
|
||||
//! (the screenshot key, a world's icon) is drawn into a texture that can be,
|
||||
//! then drawn onto the swapchain image as a fullscreen triangle. A 16-bit frame
|
||||
//! (`Renderer::frame_format`) with HDR output off goes the same way every frame.
|
||||
//! then drawn onto the swapchain image as a fullscreen triangle.
|
||||
|
||||
/// The texture a frame to read back is drawn into, and the pass that puts it
|
||||
/// on the swapchain afterwards.
|
||||
@@ -16,21 +15,18 @@ pub struct Readback {
|
||||
|
||||
impl Readback {
|
||||
/// The texture this frame draws into instead of the swapchain image
|
||||
/// `surface`, in the frame's `format`: `None` unless that differs from the
|
||||
/// swapchain's, or the frame is read back (`screenshot`) and the image
|
||||
/// cannot be copied from.
|
||||
/// `surface`: `None` unless the frame is read back (`screenshot`) and the
|
||||
/// image cannot be copied from.
|
||||
pub fn target(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
surface: &wgpu::Texture,
|
||||
format: wgpu::TextureFormat,
|
||||
screenshot: bool,
|
||||
) -> Option<wgpu::Texture> {
|
||||
if format == surface.format().remove_srgb_suffix()
|
||||
&& (!screenshot || surface.usage().contains(wgpu::TextureUsages::COPY_SRC))
|
||||
{
|
||||
if !screenshot || surface.usage().contains(wgpu::TextureUsages::COPY_SRC) {
|
||||
return None;
|
||||
}
|
||||
let format = surface.format().remove_srgb_suffix();
|
||||
if self
|
||||
.texture
|
||||
.as_ref()
|
||||
@@ -64,57 +60,22 @@ impl Readback {
|
||||
if !std::mem::take(&mut self.drawn) {
|
||||
return;
|
||||
}
|
||||
let Some(texture) = self.texture.clone() else {
|
||||
let Some(texture) = &self.texture else {
|
||||
return;
|
||||
};
|
||||
// The frame's passes write the swapchain through its non-sRGB view
|
||||
// (`view_formats`), so the copy goes there unconverted too.
|
||||
let format = surface.format().remove_srgb_suffix();
|
||||
if self.blitter.as_ref().is_none_or(|b| b.0 != format) {
|
||||
self.blitter = Some((format, wgpu::util::TextureBlitter::new(device, format)));
|
||||
}
|
||||
let target = surface.create_view(&wgpu::TextureViewDescriptor {
|
||||
format: Some(format),
|
||||
..Default::default()
|
||||
});
|
||||
self.blit(device, encoder, &texture, &target, format);
|
||||
}
|
||||
|
||||
/// `frame` (a 16-bit frame) in an 8-bit texture of `format`, clamped to 0..1
|
||||
/// as an 8-bit frame holds it, for a screenshot to read back.
|
||||
pub fn unorm_copy(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
frame: &wgpu::Texture,
|
||||
format: wgpu::TextureFormat,
|
||||
) -> wgpu::Texture {
|
||||
let copy = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("Horizon screenshot copy"),
|
||||
size: frame.size(),
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let target = copy.create_view(&Default::default());
|
||||
self.blit(device, encoder, frame, &target, format);
|
||||
copy
|
||||
}
|
||||
|
||||
fn blit(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
source: &wgpu::Texture,
|
||||
target: &wgpu::TextureView,
|
||||
format: wgpu::TextureFormat,
|
||||
) {
|
||||
if self.blitter.as_ref().is_none_or(|b| b.0 != format) {
|
||||
self.blitter = Some((format, wgpu::util::TextureBlitter::new(device, format)));
|
||||
}
|
||||
let source = source.create_view(&Default::default());
|
||||
let source = texture.create_view(&Default::default());
|
||||
if let Some((_, blitter)) = &self.blitter {
|
||||
blitter.copy(device, encoder, &source, target);
|
||||
blitter.copy(device, encoder, &source, &target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,8 +84,6 @@ impl Readback {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const F: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8Unorm;
|
||||
|
||||
fn device() -> Option<(wgpu::Device, wgpu::Queue)> {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
|
||||
@@ -142,7 +101,7 @@ mod tests {
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: F,
|
||||
format: wgpu::TextureFormat::Bgra8Unorm,
|
||||
usage,
|
||||
view_formats: &[],
|
||||
})
|
||||
@@ -197,7 +156,7 @@ mod tests {
|
||||
&device,
|
||||
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
);
|
||||
let blitter = wgpu::util::TextureBlitter::new(&device, F);
|
||||
let blitter = wgpu::util::TextureBlitter::new(&device, wgpu::TextureFormat::Bgra8Unorm);
|
||||
let mut readback = Readback::default();
|
||||
let colors = [
|
||||
(1.0, [0, 0, 255, 255]),
|
||||
@@ -214,7 +173,7 @@ mod tests {
|
||||
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
let frame = readback
|
||||
.target(&device, &surface, F, true)
|
||||
.target(&device, &surface, true)
|
||||
.unwrap_or_else(|| surface.clone());
|
||||
clear(&mut encoder, &frame, color);
|
||||
let buffer = crate::gfx::copy_frame(&device, &mut encoder, &frame, [70, 3]);
|
||||
@@ -246,70 +205,9 @@ mod tests {
|
||||
&device,
|
||||
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
);
|
||||
assert!(readback.target(&device, &readable, F, true).is_none());
|
||||
assert!(readback.target(&device, &readable, true).is_none());
|
||||
let surface = texture(&device, wgpu::TextureUsages::RENDER_ATTACHMENT);
|
||||
assert!(readback.target(&device, &surface, F, false).is_none());
|
||||
assert!(readback.target(&device, &surface, false).is_none());
|
||||
assert!(readback.texture.is_none() && !readback.drawn);
|
||||
}
|
||||
|
||||
/// A 16-bit frame with HDR output off draws into a 16-bit texture of its own, and both
|
||||
/// the swapchain and a screenshot's 8-bit copy get it clamped as an 8-bit frame holds it.
|
||||
fn reaches_an_8_bit_swapchain(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
format: wgpu::TextureFormat,
|
||||
color: wgpu::Color,
|
||||
expected: [u8; 4],
|
||||
) {
|
||||
let surface = texture(
|
||||
device,
|
||||
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
);
|
||||
let mut readback = Readback::default();
|
||||
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
let frame = readback.target(device, &surface, format, false).unwrap();
|
||||
assert_eq!(frame.format(), format);
|
||||
clear(&mut encoder, &frame, color);
|
||||
let copy = readback.unorm_copy(device, &mut encoder, &frame, F);
|
||||
let screenshot = crate::gfx::copy_frame(device, &mut encoder, ©, [70, 3]);
|
||||
readback.present(device, &mut encoder, &surface);
|
||||
let presented = crate::gfx::copy_frame(device, &mut encoder, &surface, [70, 3]);
|
||||
queue.submit([encoder.finish()]);
|
||||
if let Some(error) = pollster::block_on(scope.pop()) {
|
||||
panic!("{error}");
|
||||
}
|
||||
assert_eq!(last_pixel(device, &screenshot), expected);
|
||||
assert_eq!(last_pixel(device, &presented), expected);
|
||||
}
|
||||
|
||||
/// `Rgba16Unorm`, the frame HDR output draws into where the device can
|
||||
/// (`Renderer::frame_format`).
|
||||
#[test]
|
||||
fn a_16_bit_unorm_frame_reaches_an_8_bit_swapchain() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
|
||||
return;
|
||||
};
|
||||
let wide = wgpu::Features::TEXTURE_FORMAT_16BIT_NORM
|
||||
| wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES;
|
||||
if !adapter.features().contains(wide) {
|
||||
return;
|
||||
}
|
||||
let descriptor = wgpu::DeviceDescriptor {
|
||||
required_features: wide,
|
||||
..Default::default()
|
||||
};
|
||||
let Ok((device, queue)) = pollster::block_on(adapter.request_device(&descriptor)) else {
|
||||
return;
|
||||
};
|
||||
let color = wgpu::Color {
|
||||
r: 1.0,
|
||||
g: 0.2,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
};
|
||||
let unorm = wgpu::TextureFormat::Rgba16Unorm;
|
||||
reaches_an_8_bit_swapchain(&device, &queue, unorm, color, [0, 51, 255, 255]);
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,6 @@ struct Column {
|
||||
z: i32,
|
||||
/// Section y (from the world's bottom) and slot, in no particular order.
|
||||
sections: Vec<(i32, u32)>,
|
||||
/// Those of `sections` that are marked (`Resident::insert_marked`), in no
|
||||
/// particular order.
|
||||
marked: Vec<(i32, u32)>,
|
||||
/// The lowest and highest section y of `sections`.
|
||||
low: i32,
|
||||
high: i32,
|
||||
@@ -39,9 +36,6 @@ pub struct Resident<T> {
|
||||
min_y: i32,
|
||||
items: Vec<(SectionKey, T)>,
|
||||
bounds: Vec<Option<[Vec3; 2]>>,
|
||||
/// Per slot, whether the section is marked: within the render distance,
|
||||
/// `walk` lists only those when `marked_only_near`.
|
||||
marked: Vec<bool>,
|
||||
slots: FxHashMap<SectionKey, u32>,
|
||||
columns: Vec<Column>,
|
||||
column_index: FxHashMap<(i32, i32), u32>,
|
||||
@@ -53,7 +47,6 @@ impl<T> Default for Resident<T> {
|
||||
min_y: crate::chunk::MIN_Y,
|
||||
items: Vec::new(),
|
||||
bounds: Vec::new(),
|
||||
marked: Vec::new(),
|
||||
slots: FxHashMap::default(),
|
||||
columns: Vec::new(),
|
||||
column_index: FxHashMap::default(),
|
||||
@@ -72,13 +65,12 @@ impl<T> Resident<T> {
|
||||
use std::mem::size_of;
|
||||
self.items.capacity() * size_of::<(SectionKey, T)>()
|
||||
+ self.bounds.capacity() * size_of::<Option<[Vec3; 2]>>()
|
||||
+ self.marked.capacity() * size_of::<bool>()
|
||||
+ self.slots.capacity() * (size_of::<(SectionKey, u32)>() + 1)
|
||||
+ self.columns.capacity() * size_of::<Column>()
|
||||
+ self
|
||||
.columns
|
||||
.iter()
|
||||
.map(|c| (c.sections.capacity() + c.marked.capacity()) * size_of::<(i32, u32)>())
|
||||
.map(|c| c.sections.capacity() * size_of::<(i32, u32)>())
|
||||
.sum::<usize>()
|
||||
+ self.column_index.capacity() * (size_of::<((i32, i32), u32)>() + 1)
|
||||
}
|
||||
@@ -91,23 +83,14 @@ impl<T> Resident<T> {
|
||||
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
self.items.iter_mut().map(|(_, v)| v)
|
||||
}
|
||||
/// Every section's key, value and bounds.
|
||||
/// Every section's key and value.
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn iter(&self) -> impl Iterator<Item = (SectionKey, &T, Option<[Vec3; 2]>)> {
|
||||
let items = self.items.iter().zip(&self.bounds);
|
||||
items.map(|((key, value), bounds)| (*key, value, *bounds))
|
||||
pub fn iter(&self) -> impl Iterator<Item = (SectionKey, &T)> {
|
||||
self.items.iter().map(|(key, value)| (*key, value))
|
||||
}
|
||||
/// Every section's key, value and bounds.
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (SectionKey, &mut T, Option<[Vec3; 2]>)> {
|
||||
let items = self.items.iter_mut().zip(&self.bounds);
|
||||
items.map(|((key, value), bounds)| (*key, value, *bounds))
|
||||
}
|
||||
/// Whether the section at `key` is marked (`insert_marked`).
|
||||
#[cfg(any(test, debug_assertions))]
|
||||
pub fn is_marked(&self, key: SectionKey) -> bool {
|
||||
self.slots
|
||||
.get(&key)
|
||||
.is_some_and(|&slot| self.marked[slot as usize])
|
||||
/// Every section's key and value.
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (SectionKey, &mut T)> {
|
||||
self.items.iter_mut().map(|(key, value)| (*key, value))
|
||||
}
|
||||
/// `key`'s value, when it is resident.
|
||||
pub fn find(&self, key: SectionKey) -> Option<&T> {
|
||||
@@ -135,7 +118,6 @@ impl<T> Resident<T> {
|
||||
pub fn clear(&mut self) {
|
||||
self.items.clear();
|
||||
self.bounds.clear();
|
||||
self.marked.clear();
|
||||
self.slots.clear();
|
||||
self.columns.clear();
|
||||
self.column_index.clear();
|
||||
@@ -145,31 +127,16 @@ impl<T> Resident<T> {
|
||||
pub fn insert(&mut self, key: SectionKey, value: T) {
|
||||
self.insert_with_bounds(key, value, None);
|
||||
}
|
||||
/// Adds or replaces `key`'s value with optional tight geometry bounds, unmarked.
|
||||
#[cfg(test)]
|
||||
/// Adds or replaces `key`'s value with optional tight geometry bounds.
|
||||
pub fn insert_with_bounds(&mut self, key: SectionKey, value: T, bounds: Option<[Vec3; 2]>) {
|
||||
self.insert_marked(key, value, bounds, false);
|
||||
}
|
||||
/// Adds or replaces `key`'s value with optional tight geometry bounds, and
|
||||
/// whether it is marked: `walk` with `marked_only_near` lists the sections
|
||||
/// within the render distance that are.
|
||||
pub fn insert_marked(
|
||||
&mut self,
|
||||
key: SectionKey,
|
||||
value: T,
|
||||
bounds: Option<[Vec3; 2]>,
|
||||
marked: bool,
|
||||
) {
|
||||
if let Some(&slot) = self.slots.get(&key) {
|
||||
self.items[slot as usize].1 = value;
|
||||
self.bounds[slot as usize] = bounds;
|
||||
self.mark_slot(slot, marked);
|
||||
return;
|
||||
}
|
||||
let slot = self.items.len() as u32;
|
||||
self.items.push((key, value));
|
||||
self.bounds.push(bounds);
|
||||
self.marked.push(marked);
|
||||
self.slots.insert(key, slot);
|
||||
let column = match self.column_index.get(&(key.0, key.2)) {
|
||||
Some(&c) => &mut self.columns[c as usize],
|
||||
@@ -180,7 +147,6 @@ impl<T> Resident<T> {
|
||||
x: key.0,
|
||||
z: key.2,
|
||||
sections: Vec::new(),
|
||||
marked: Vec::new(),
|
||||
low: key.1,
|
||||
high: key.1,
|
||||
});
|
||||
@@ -188,39 +154,15 @@ impl<T> Resident<T> {
|
||||
}
|
||||
};
|
||||
column.sections.push((key.1, slot));
|
||||
if marked {
|
||||
column.marked.push((key.1, slot));
|
||||
}
|
||||
column.low = column.low.min(key.1);
|
||||
column.high = column.high.max(key.1);
|
||||
}
|
||||
/// Marks or unmarks the section in `slot`, in its column's list too.
|
||||
fn mark_slot(&mut self, slot: u32, marked: bool) {
|
||||
let was = std::mem::replace(&mut self.marked[slot as usize], marked);
|
||||
if was == marked {
|
||||
return;
|
||||
}
|
||||
let (x, y, z) = self.items[slot as usize].0;
|
||||
let column = &mut self.columns[self.column_index[&(x, z)] as usize];
|
||||
if marked {
|
||||
column.marked.push((y, slot));
|
||||
} else {
|
||||
let at = column
|
||||
.marked
|
||||
.iter()
|
||||
.position(|s| s.1 == slot)
|
||||
.expect("marked section in its column's list");
|
||||
column.marked.swap_remove(at);
|
||||
}
|
||||
}
|
||||
/// Removes every section of the `gone` columns, the others keeping their order and
|
||||
/// marks: one pass over the store, where `remove` moves a slot and looks its column
|
||||
/// up for each.
|
||||
/// Removes every section of the `gone` columns, the others keeping their order: one
|
||||
/// pass over the store, where `remove` moves a slot and looks its column up for each.
|
||||
pub fn remove_columns(&mut self, gone: &FxHashSet<(i32, i32)>) -> Vec<T> {
|
||||
// Every column (a far teleport): emptied, without a lookup per section.
|
||||
if self.columns.iter().all(|c| gone.contains(&(c.x, c.z))) {
|
||||
self.bounds.clear();
|
||||
self.marked.clear();
|
||||
self.slots.clear();
|
||||
self.columns.clear();
|
||||
self.column_index.clear();
|
||||
@@ -231,23 +173,21 @@ impl<T> Resident<T> {
|
||||
}
|
||||
let items = std::mem::take(&mut self.items);
|
||||
let bounds = std::mem::take(&mut self.bounds);
|
||||
let marked = std::mem::take(&mut self.marked);
|
||||
self.slots.clear();
|
||||
self.columns.clear();
|
||||
self.column_index.clear();
|
||||
let mut removed = Vec::new();
|
||||
for (((key, value), bounds), marked) in items.into_iter().zip(bounds).zip(marked) {
|
||||
for ((key, value), bounds) in items.into_iter().zip(bounds) {
|
||||
if gone.contains(&(key.0, key.2)) {
|
||||
removed.push(value);
|
||||
} else {
|
||||
self.insert_marked(key, value, bounds, marked);
|
||||
self.insert_with_bounds(key, value, bounds);
|
||||
}
|
||||
}
|
||||
removed
|
||||
}
|
||||
pub fn remove(&mut self, key: SectionKey) -> Option<T> {
|
||||
let slot = self.slots.remove(&key)?;
|
||||
self.mark_slot(slot, false);
|
||||
let c = self.column_index[&(key.0, key.2)] as usize;
|
||||
let column = &mut self.columns[c];
|
||||
let at = column
|
||||
@@ -266,10 +206,8 @@ impl<T> Resident<T> {
|
||||
column.bounds();
|
||||
}
|
||||
let _ = self.bounds.swap_remove(slot as usize);
|
||||
let _ = self.marked.swap_remove(slot as usize);
|
||||
let (_, value) = self.items.swap_remove(slot as usize);
|
||||
// The last item now sits in `slot`: point its key and column, and its
|
||||
// column's marked list if it is marked, at it.
|
||||
// The last item now sits in `slot`: point its key and column at it.
|
||||
if let Some(&(moved, _)) = self.items.get(slot as usize) {
|
||||
let last = self.items.len() as u32;
|
||||
self.slots.insert(moved, slot);
|
||||
@@ -280,14 +218,6 @@ impl<T> Resident<T> {
|
||||
.find(|s| s.1 == last)
|
||||
.expect("moved section in its column");
|
||||
entry.1 = slot;
|
||||
if self.marked[slot as usize] {
|
||||
let entry = column
|
||||
.marked
|
||||
.iter_mut()
|
||||
.find(|s| s.1 == last)
|
||||
.expect("moved marked section in its column's list");
|
||||
entry.1 = slot;
|
||||
}
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
@@ -398,22 +328,6 @@ impl<T> Resident<T> {
|
||||
reachable: impl Fn(SectionKey) -> bool,
|
||||
beyond: impl Fn(i32, i32) -> bool,
|
||||
out: &mut Vec<Visible>,
|
||||
) {
|
||||
self.walk(camera, radius, frustum, reachable, beyond, false, out);
|
||||
}
|
||||
/// `visible`, less the sections within the render distance that are not
|
||||
/// marked (`insert_marked`) when `marked_only_near`, in the same order: GPU
|
||||
/// culling's frames, where the GPU draws those. A column's box spans all of
|
||||
/// its sections either way, so both decide alike on those they share.
|
||||
pub fn walk(
|
||||
&self,
|
||||
camera: DVec3,
|
||||
radius: i32,
|
||||
frustum: &impl Cull,
|
||||
reachable: impl Fn(SectionKey) -> bool,
|
||||
beyond: impl Fn(i32, i32) -> bool,
|
||||
marked_only_near: bool,
|
||||
out: &mut Vec<Visible>,
|
||||
) {
|
||||
let from = camera_column(camera);
|
||||
for column in &self.columns {
|
||||
@@ -421,14 +335,6 @@ impl<T> Resident<T> {
|
||||
if !near && !beyond(column.x, column.z) {
|
||||
continue;
|
||||
}
|
||||
let list = if marked_only_near && near {
|
||||
&column.marked
|
||||
} else {
|
||||
&column.sections
|
||||
};
|
||||
if list.is_empty() {
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
@@ -445,7 +351,7 @@ impl<T> Resident<T> {
|
||||
let xz_sq = dx8 * dx8 + dz8 * dz8;
|
||||
|
||||
if inside {
|
||||
for &(y, slot) in list {
|
||||
for &(y, slot) in &column.sections {
|
||||
let key = (column.x, y, column.z);
|
||||
if near && !reachable(key) {
|
||||
continue;
|
||||
@@ -462,7 +368,7 @@ impl<T> Resident<T> {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for &(y, slot) in list {
|
||||
for &(y, slot) in &column.sections {
|
||||
let key = (column.x, y, column.z);
|
||||
if near && !reachable(key) {
|
||||
continue;
|
||||
@@ -613,10 +519,9 @@ mod tests {
|
||||
assert_eq!(fast, slow);
|
||||
}
|
||||
|
||||
/// Every key reaches its value and mark through its slot and column, every
|
||||
/// column's bounds and sections match the keys, and its marked list holds
|
||||
/// its marked sections.
|
||||
fn check(r: &Resident<u32>, model: &HashMap<SectionKey, (u32, bool)>) {
|
||||
/// Every key reaches its value through its slot and column, and every
|
||||
/// column's bounds and sections match the keys.
|
||||
fn check(r: &Resident<u32>, model: &HashMap<SectionKey, u32>) {
|
||||
assert_eq!(r.len(), model.len());
|
||||
let mut seen = 0;
|
||||
for column in &r.columns {
|
||||
@@ -627,24 +532,13 @@ mod tests {
|
||||
for &(y, slot) in &column.sections {
|
||||
let (key, value) = &r.items[slot as usize];
|
||||
assert_eq!(*key, (column.x, y, column.z));
|
||||
assert_eq!(model[key], (*value, r.marked[slot as usize]));
|
||||
assert_eq!(model[key], *value);
|
||||
seen += 1;
|
||||
}
|
||||
let mut marked: Vec<(i32, u32)> = column
|
||||
.sections
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&(_, slot)| r.marked[slot as usize])
|
||||
.collect();
|
||||
let mut listed = column.marked.clone();
|
||||
marked.sort_unstable();
|
||||
listed.sort_unstable();
|
||||
assert_eq!(listed, marked, "column ({}, {})", column.x, column.z);
|
||||
}
|
||||
assert_eq!(seen, model.len());
|
||||
for (key, &(_, marked)) in model {
|
||||
for key in model.keys() {
|
||||
assert!(r.contains(*key));
|
||||
assert_eq!(r.is_marked(*key), marked, "{key:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,12 +546,7 @@ mod tests {
|
||||
fn slots_and_columns_follow_inserts_and_removals() {
|
||||
let mut r = Resident::default();
|
||||
let mut model = HashMap::new();
|
||||
// Bounds that tell values apart.
|
||||
let bounds = |value: u32| {
|
||||
(!value.is_multiple_of(3)).then(|| [Vec3::splat((value % 5) as f32), Vec3::splat(16.0)])
|
||||
};
|
||||
// A deterministic mix of inserts, replacements and removals, marked at
|
||||
// random: a replacement can mark or unmark its section.
|
||||
// A deterministic mix of inserts, replacements and removals.
|
||||
let mut seed = 0x2545_f491_u32;
|
||||
for step in 0..4000u32 {
|
||||
seed ^= seed << 13;
|
||||
@@ -674,130 +563,47 @@ mod tests {
|
||||
.flat_map(|x| (-2..=2).map(move |z| (x, z)))
|
||||
.collect();
|
||||
let mut removed = r.remove_columns(&gone);
|
||||
let mut expected: Vec<u32> = model.drain().map(|(_, (value, _))| value).collect();
|
||||
let mut expected: Vec<u32> = model.drain().map(|(_, v)| v).collect();
|
||||
removed.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(removed, expected);
|
||||
assert!(r.columns.is_empty() && r.column_index.is_empty());
|
||||
} else if (seed >> 12).is_multiple_of(97) {
|
||||
// Whole columns at once: the column of `key` and the next one along x.
|
||||
// The others keep their values, bounds and marks (`check`).
|
||||
let gone: FxHashSet<(i32, i32)> =
|
||||
[(key.0, key.2), (key.0 + 1, key.2)].into_iter().collect();
|
||||
let mut removed = r.remove_columns(&gone);
|
||||
let mut expected: Vec<u32> = model
|
||||
.extract_if(|k: &SectionKey, _| gone.contains(&(k.0, k.2)))
|
||||
.map(|(_, (value, _))| value)
|
||||
.map(|(_, v)| v)
|
||||
.collect();
|
||||
removed.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(removed, expected);
|
||||
} else if (seed >> 12).is_multiple_of(3) {
|
||||
assert_eq!(r.remove(key), model.remove(&key).map(|(value, _)| value));
|
||||
assert_eq!(r.remove(key), model.remove(&key));
|
||||
} else {
|
||||
let marked = (seed >> 20).is_multiple_of(2);
|
||||
r.insert_marked(key, step, bounds(step), marked);
|
||||
model.insert(key, (step, marked));
|
||||
r.insert(key, step);
|
||||
model.insert(key, step);
|
||||
}
|
||||
check(&r, &model);
|
||||
// `iter_mut` yields every key once, with its value and bounds.
|
||||
// `iter_mut` yields every key once, with its value.
|
||||
let mut seen = HashSet::new();
|
||||
for (key, value, b) in r.iter_mut() {
|
||||
assert_eq!(*value, model[&key].0, "{key:?}");
|
||||
assert_eq!(b, bounds(*value), "{key:?}");
|
||||
for (key, value) in r.iter_mut() {
|
||||
assert_eq!(*value, model[&key], "{key:?}");
|
||||
assert!(seen.insert(key), "{key:?} twice");
|
||||
}
|
||||
assert_eq!(seen.len(), model.len());
|
||||
}
|
||||
for (_, value, _) in r.iter_mut() {
|
||||
for (_, value) in r.iter_mut() {
|
||||
*value += 1;
|
||||
}
|
||||
for (key, (value, _)) in &model {
|
||||
for (key, value) in &model {
|
||||
assert_eq!(r.find(*key), Some(&(value + 1)));
|
||||
}
|
||||
r.clear();
|
||||
assert_eq!(r.len(), 0);
|
||||
assert!(r.columns.is_empty() && r.marked.is_empty());
|
||||
}
|
||||
|
||||
/// GPU frames' walk (`walk` with `marked_only_near`) is `visible` less the
|
||||
/// sections within the render distance that are not marked, in the same
|
||||
/// order: with marks at random, bounds reaching out of their cells, short
|
||||
/// columns entirely inside the frustum, cave reach, and columns past the
|
||||
/// distance kept for far terrain (GPU frames have no water mirror,
|
||||
/// `gpu_frame`).
|
||||
#[test]
|
||||
fn the_marked_walk_is_the_walk_less_unmarked_sections_within_the_distance() {
|
||||
let mut r = Resident::default();
|
||||
let mut seed = 0x9e37_79b9_u32;
|
||||
let mut next = || {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 17;
|
||||
seed ^= seed << 5;
|
||||
seed
|
||||
};
|
||||
for x in -14i32..=14 {
|
||||
for z in -14i32..=14 {
|
||||
// Short columns ahead of the first camera: most lie entirely inside.
|
||||
let rows = if z > 0 && x.abs() <= 12 { 3..6 } else { 0..24 };
|
||||
for y in rows {
|
||||
let bits = next();
|
||||
if bits.is_multiple_of(5) {
|
||||
continue;
|
||||
}
|
||||
let bounds = match (bits >> 4) % 4 {
|
||||
0 => None,
|
||||
1 => Some([Vec3::splat(-1.5), Vec3::splat(17.5)]),
|
||||
2 => Some([Vec3::new(0.0, 18.0, 0.0), Vec3::new(16.0, 20.0, 16.0)]),
|
||||
_ => Some([Vec3::new(2.0, 0.0, 3.0), Vec3::new(14.0, 5.5, 16.0)]),
|
||||
};
|
||||
r.insert_marked((x, y, z), (), bounds, (bits >> 8).is_multiple_of(3));
|
||||
}
|
||||
}
|
||||
}
|
||||
let reachable = |k: SectionKey| (3 * k.0 + k.1 + 5 * k.2).rem_euclid(7) != 0;
|
||||
let beyond = |x: i32, z: i32| (x - z).rem_euclid(3) == 0;
|
||||
let projection = super::super::world_projection(70.0, 16.0 / 9.0, 512.0);
|
||||
let (mut kept, mut dropped, mut inside) = (0, 0, 0);
|
||||
for (camera, yaw, pitch) in [
|
||||
(DVec3::new(0.5, -8.0, 0.5), 0.0, 5.0),
|
||||
(DVec3::new(-37.25, 130.5, 21.75), 135.0, 35.0),
|
||||
(DVec3::new(100.0, -60.0, -3.0), 270.0, -80.0),
|
||||
(DVec3::new(8.0, 64.0, 8.0), 45.0, 89.0),
|
||||
] {
|
||||
let view = Frustum {
|
||||
planes: super::super::frustum(projection * crate::camera::view_matrix(yaw, pitch)),
|
||||
};
|
||||
inside += r
|
||||
.columns
|
||||
.iter()
|
||||
.filter(|c| {
|
||||
let min = origin((c.x, c.low, c.z), r.min_y, camera).as_vec3();
|
||||
let top = origin((c.x, c.high, c.z), r.min_y, camera).as_vec3();
|
||||
view.contains(min, top + Vec3::splat(16.0))
|
||||
})
|
||||
.count();
|
||||
for radius in [3, 7, 12] {
|
||||
let (mut all, mut marked) = (Vec::new(), Vec::new());
|
||||
r.visible(camera, radius, &view, reachable, beyond, &mut all);
|
||||
r.walk(camera, radius, &view, reachable, beyond, true, &mut marked);
|
||||
let from = camera_column(camera);
|
||||
let near = |v: &Visible| in_view_distance(from, radius, v.key.0, v.key.2);
|
||||
let expected: Vec<Visible> = all
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|v| r.is_marked(v.key) || !near(v))
|
||||
.collect();
|
||||
assert_eq!(marked, expected, "{camera} {yaw} {pitch} {radius}");
|
||||
kept += marked.len();
|
||||
dropped += all.len() - marked.len();
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
kept > 100 && dropped > 100,
|
||||
"{kept} kept, {dropped} dropped"
|
||||
);
|
||||
assert!(inside > 50, "{inside} columns entirely inside");
|
||||
assert!(r.columns.is_empty());
|
||||
}
|
||||
|
||||
/// The old walk (every key of a hash map, sorted by distance, then two
|
||||
|
||||
@@ -1,417 +0,0 @@
|
||||
//! A visibility buffer for solid terrain, opt-in with `HORIZON_VISBUFFER=1` (read once, at
|
||||
//! startup): pass 1 rasterises the solid flat draws (near sections and far tiles, phase 1 and,
|
||||
//! with occlusion culling, phase 2) writing only depth and the ID of the quad each pixel shows
|
||||
//! (`visbuffer.wgsl`'s `vis_id`: arena page and quad); cutout and fading
|
||||
//! quads keep their alpha test and dither there, the rest never discard. Pass 2 is one
|
||||
//! full-screen draw with up to `PAGE_SLOTS` arena pages bound at once (more pages, more draws)
|
||||
//! that reads the quad back from its page and its section's origin (through a copy of every
|
||||
//! drawn page's block table in one buffer), and runs terrain.wgsl's
|
||||
//! vertex work and solid shading once per pixel with analytic screen gradients. Every solid
|
||||
//! pixel is shaded once, however many quads were drawn over it and however small they are.
|
||||
//! A draw per page instead made each GPU thread group run the shading once for every page
|
||||
//! among its pixels, with most lanes idle: the pages' sections lie all over the screen.
|
||||
//!
|
||||
//! It runs only in frames drawn flat (`flat`: Metal past `flat::MIN_SECTIONS`, or
|
||||
//! `HORIZON_FLAT_DRAWS=1`) and falls back to the normal solid passes in frames with a shader
|
||||
//! pack drawing terrain, one of Horizon's own packs with a material stage or a shadow pass, a
|
||||
//! pack's surface target, and on the title screen. The water mirror's own pass stays as it is.
|
||||
use super::arena::Arena;
|
||||
use super::flat::Flat;
|
||||
use super::pack_timing::PassTiming;
|
||||
|
||||
/// Arena pages one resolve draw reads (`visbuffer.wgsl`'s `vis_page_0`...).
|
||||
pub const PAGE_SLOTS: u32 = 16;
|
||||
/// Arena pages an ID names (`visbuffer.wgsl`'s `vis_id`): frames with more draw without it.
|
||||
pub const MAX_PAGES: u32 = 64;
|
||||
/// The resolve binds that many pages, the block tables and the origins by section id.
|
||||
const STORAGE_BUFFERS: u32 = PAGE_SLOTS + 2;
|
||||
const IDS: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint;
|
||||
|
||||
/// Whether the visibility buffer is asked for, and the device binds storage buffers enough.
|
||||
pub fn wanted(device: &wgpu::Device) -> bool {
|
||||
std::env::var("HORIZON_VISBUFFER").as_deref() == Ok("1")
|
||||
&& device.limits().max_storage_buffers_per_shader_stage >= STORAGE_BUFFERS
|
||||
}
|
||||
|
||||
pub struct Visbuffer {
|
||||
/// Pass 1 over the quads `fs_opaque` draws (no discard) and the rest.
|
||||
opaque: wgpu::RenderPipeline,
|
||||
cutout: wgpu::RenderPipeline,
|
||||
resolve: wgpu::RenderPipeline,
|
||||
/// Pass 2's group 1: the block tables (binding 0), the origins by section id (1),
|
||||
/// `PAGE_SLOTS` pages' vertices (from 2), the IDs (after them).
|
||||
page_layout: wgpu::BindGroupLayout,
|
||||
/// Every page's block table, page after page (`Flat::blocks_per_page` entries each),
|
||||
/// copied each frame from the drawn pages' own, and its size in pages.
|
||||
blocks: Option<(wgpu::Buffer, u32)>,
|
||||
blocks_per_page: u64,
|
||||
/// The IDs, the size of the world's depth.
|
||||
ids: Option<(wgpu::TextureView, [u32; 2])>,
|
||||
}
|
||||
|
||||
impl Visbuffer {
|
||||
/// `terrain_layout`: terrain.wgsl's group 0; `flat_layout`: the flat draws' group 1;
|
||||
/// `color`: the world pass's colour format; `constants`: the sprite arrays' pipeline
|
||||
/// constants.
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
terrain_layout: &wgpu::BindGroupLayout,
|
||||
flat: &Flat,
|
||||
color: wgpu::TextureFormat,
|
||||
constants: &[(&str, f64)],
|
||||
) -> Self {
|
||||
let flat_layout = &flat.draw_layout;
|
||||
let blocks_per_page = flat.blocks_per_page();
|
||||
let resolve_constants: Vec<(&str, f64)> = constants
|
||||
.iter()
|
||||
.copied()
|
||||
.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(
|
||||
(include_str!("../terrain.wgsl").to_owned() + include_str!("../visbuffer.wgsl"))
|
||||
.into(),
|
||||
),
|
||||
});
|
||||
let fragment = wgpu::ShaderStages::FRAGMENT;
|
||||
let storage = |binding| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: fragment,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
};
|
||||
let entries: Vec<_> = (0..2 + PAGE_SLOTS)
|
||||
.map(storage)
|
||||
.chain([wgpu::BindGroupLayoutEntry {
|
||||
binding: 2 + PAGE_SLOTS,
|
||||
visibility: fragment,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Uint,
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
}])
|
||||
.collect();
|
||||
let page_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Visibility buffer pages"),
|
||||
entries: &entries,
|
||||
});
|
||||
let id_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Visibility buffer IDs"),
|
||||
bind_group_layouts: &[Some(terrain_layout), Some(flat_layout)],
|
||||
..Default::default()
|
||||
});
|
||||
let resolve_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Visibility buffer resolve"),
|
||||
bind_group_layouts: &[Some(terrain_layout), Some(&page_layout)],
|
||||
..Default::default()
|
||||
});
|
||||
let ids = |label: &'static str, vertex: &'static str, fragment: &'static str| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(&id_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &module,
|
||||
entry_point: Some(vertex),
|
||||
compilation_options: Default::default(),
|
||||
// `section_corner` reads the arena page (`flat::ARENA_BINDING`).
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: Some(true),
|
||||
depth_compare: Some(wgpu::CompareFunction::GreaterEqual),
|
||||
stencil: Default::default(),
|
||||
bias: Default::default(),
|
||||
}),
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some(fragment),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants,
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: IDS,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let opaque = ids("Visibility buffer opaque", "vs_vis_opaque", "fs_vis_id");
|
||||
let cutout = ids("Visibility buffer cutout", "vs_vis_cutout", "fs_vis_cutout");
|
||||
let resolve = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Visibility buffer resolve"),
|
||||
layout: Some(&resolve_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &module,
|
||||
entry_point: Some("vs_vis_resolve"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some("fs_vis_resolve"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &resolve_constants,
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: color,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
Self {
|
||||
opaque,
|
||||
cutout,
|
||||
resolve,
|
||||
page_layout,
|
||||
blocks: None,
|
||||
blocks_per_page,
|
||||
ids: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes the IDs the world depth's `size`, and the block tables hold the pages `flat`
|
||||
/// draws this frame.
|
||||
pub fn fit(&mut self, device: &wgpu::Device, size: [u32; 2], flat: &Flat) {
|
||||
let pages = flat.calls[..flat.solid_calls]
|
||||
.iter()
|
||||
.map(|c| c.page + 1)
|
||||
.max()
|
||||
.unwrap_or(1);
|
||||
if self.blocks.as_ref().is_none_or(|(_, n)| *n < pages) {
|
||||
let pages = pages.next_power_of_two();
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Visibility buffer block tables"),
|
||||
size: u64::from(pages) * self.blocks_per_page * 4,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
self.blocks = Some((buffer, pages));
|
||||
}
|
||||
if self.ids.as_ref().is_some_and(|(_, s)| *s == size) {
|
||||
return;
|
||||
}
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("Visibility buffer IDs"),
|
||||
size: wgpu::Extent3d {
|
||||
width: size[0].max(1),
|
||||
height: size[1].max(1),
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: IDS,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
self.ids = Some((texture.create_view(&Default::default()), size));
|
||||
}
|
||||
|
||||
/// Pass 1's render pass over the world's `depth`: the IDs cleared (`clear`, phase 1) or
|
||||
/// kept (phase 2 of occlusion culling).
|
||||
pub fn id_pass<'e>(
|
||||
&self,
|
||||
encoder: &'e mut wgpu::CommandEncoder,
|
||||
depth: &wgpu::TextureView,
|
||||
clear: bool,
|
||||
timing: Option<&mut PassTiming>,
|
||||
) -> wgpu::RenderPass<'e> {
|
||||
let (ids, _) = self.ids.as_ref().expect("fitted before the world pass");
|
||||
let name = if clear {
|
||||
"Visibility IDs"
|
||||
} else {
|
||||
"Visibility IDs after culling"
|
||||
};
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Horizon visibility buffer"),
|
||||
timestamp_writes: timing.and_then(|t| t.writes(name)),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: ids,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: if clear {
|
||||
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT)
|
||||
} else {
|
||||
wgpu::LoadOp::Load
|
||||
},
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: depth,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Phase 1's solid calls into the IDs, as the normal passes draw them: the opaque
|
||||
/// pipeline over `calls[..pass_calls[0]]`, the cutout one over
|
||||
/// `calls[pass_calls[1]..phase1_calls]` (each leaves out the other's quads).
|
||||
pub fn draw_ids<'a>(
|
||||
&self,
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
flat: &Flat,
|
||||
vertices: impl Fn(u32) -> wgpu::BufferSlice<'a> + Copy,
|
||||
) {
|
||||
let [opaque, cutout, _] = flat.pass_calls;
|
||||
pass.set_pipeline(&self.opaque);
|
||||
flat.draw_paged(pass, &flat.calls[..opaque], vertices);
|
||||
pass.set_pipeline(&self.cutout);
|
||||
flat.draw_paged(pass, &flat.calls[cutout..flat.phase1_calls], vertices);
|
||||
}
|
||||
|
||||
/// Phase 2's calls as `Flat::flatten_phase2` left them (their first instance is their
|
||||
/// page: `Flat::page_instances`), through both pipelines.
|
||||
pub fn draw_ids_phase2<'a>(
|
||||
&self,
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
flat: &Flat,
|
||||
vertices: impl Fn(u32) -> wgpu::BufferSlice<'a> + Copy,
|
||||
) {
|
||||
// Phase 2 lists each section's opaque runs first, its cutout runs after.
|
||||
let split = flat.pass_calls[2] - flat.phase1_calls;
|
||||
let total = flat.solid_calls - flat.phase1_calls;
|
||||
pass.set_pipeline(&self.opaque);
|
||||
flat.draw_phase2(pass, 0..split, vertices);
|
||||
pass.set_pipeline(&self.cutout);
|
||||
flat.draw_phase2(pass, split..total, vertices);
|
||||
}
|
||||
|
||||
/// Bytes of its block tables and IDs.
|
||||
pub fn bytes(&self) -> u64 {
|
||||
let blocks = self.blocks.as_ref().map_or(0, |(b, _)| b.size());
|
||||
let ids = self
|
||||
.ids
|
||||
.as_ref()
|
||||
.map_or(0, |(_, [w, h])| u64::from(*w) * u64::from(*h) * 4);
|
||||
blocks + ids
|
||||
}
|
||||
|
||||
/// Pass 2: shades the solid terrain the IDs show into `target`, the world's colour,
|
||||
/// over the sky drawn before. `terrain`: terrain.wgsl's group 0.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn resolve(
|
||||
&self,
|
||||
device: &wgpu::Device,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
target: &wgpu::TextureView,
|
||||
terrain: &wgpu::BindGroup,
|
||||
flat: &Flat,
|
||||
arena: &Arena,
|
||||
timing: Option<&mut PassTiming>,
|
||||
) {
|
||||
let (Some((ids, _)), Some((blocks, _))) = (&self.ids, &self.blocks) else {
|
||||
return;
|
||||
};
|
||||
let mut pages: Vec<u32> = flat.calls[..flat.solid_calls]
|
||||
.iter()
|
||||
.map(|c| c.page)
|
||||
.collect();
|
||||
pages.sort_unstable();
|
||||
pages.dedup();
|
||||
let Some(&any) = pages.first() else {
|
||||
return;
|
||||
};
|
||||
let table = self.blocks_per_page * 4;
|
||||
for &page in &pages {
|
||||
if let Some(from) = flat.block_table(page) {
|
||||
encoder.copy_buffer_to_buffer(from, 0, blocks, u64::from(page) * table, table);
|
||||
}
|
||||
}
|
||||
// Pages in groups of `PAGE_SLOTS` by number: a page's slot is its number's remainder.
|
||||
// Slots no drawn page fills take one that is, which no pixel's ID names.
|
||||
let mut chunks: Vec<u32> = pages.iter().map(|p| p / PAGE_SLOTS).collect();
|
||||
chunks.dedup();
|
||||
let groups: Vec<(u32, wgpu::BindGroup)> = chunks
|
||||
.into_iter()
|
||||
.map(|chunk| {
|
||||
let mut entries = vec![
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: blocks.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: flat.origins().as_entire_binding(),
|
||||
},
|
||||
];
|
||||
entries.extend((0..PAGE_SLOTS).map(|slot| {
|
||||
let page = chunk * PAGE_SLOTS + slot;
|
||||
let page = if pages.binary_search(&page).is_ok() {
|
||||
page
|
||||
} else {
|
||||
any
|
||||
};
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2 + slot,
|
||||
resource: arena.buffer(page).as_entire_binding(),
|
||||
}
|
||||
}));
|
||||
entries.push(wgpu::BindGroupEntry {
|
||||
binding: 2 + PAGE_SLOTS,
|
||||
resource: wgpu::BindingResource::TextureView(ids),
|
||||
});
|
||||
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Visibility buffer pages"),
|
||||
layout: &self.page_layout,
|
||||
entries: &entries,
|
||||
});
|
||||
(chunk, group)
|
||||
})
|
||||
.collect();
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Horizon visibility buffer resolve"),
|
||||
timestamp_writes: timing.and_then(|t| t.writes("Visibility resolve")),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: target,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
..Default::default()
|
||||
});
|
||||
pass.set_pipeline(&self.resolve);
|
||||
pass.set_bind_group(0, terrain, &[]);
|
||||
// The group of pages goes as the instance: each draw shades the pixels whose quad is
|
||||
// in one of its pages (one draw, up to `PAGE_SLOTS` pages).
|
||||
for (chunk, group) in &groups {
|
||||
pass.set_bind_group(1, group, &[]);
|
||||
pass.draw(0..3, *chunk..*chunk + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,44 +128,6 @@ impl EntityCullingMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Particle visibility strategy, independent of Entity Culling.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum ParticleCullingMode {
|
||||
#[default]
|
||||
Off,
|
||||
Synchronous,
|
||||
Asynchronous,
|
||||
}
|
||||
impl ParticleCullingMode {
|
||||
pub const ALL: [ParticleCullingMode; 3] = [
|
||||
ParticleCullingMode::Off,
|
||||
ParticleCullingMode::Synchronous,
|
||||
ParticleCullingMode::Asynchronous,
|
||||
];
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Off => "off",
|
||||
Self::Synchronous => "sync",
|
||||
Self::Asynchronous => "async",
|
||||
}
|
||||
}
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Off => "Off",
|
||||
Self::Synchronous => "Synchronous",
|
||||
Self::Asynchronous => "Asynchronous",
|
||||
}
|
||||
}
|
||||
fn parse(s: &str) -> Self {
|
||||
match s {
|
||||
"synchronous" | "sync" => Self::Synchronous,
|
||||
"asynchronous" | "async" => Self::Asynchronous,
|
||||
"off" => Self::Off,
|
||||
_ => Self::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How the swapchain presents frames.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum PresentMode {
|
||||
@@ -371,13 +333,6 @@ pub enum LeafCullingMode {
|
||||
/// Every leaf face, as vanilla's fancy leaves draw them.
|
||||
#[default]
|
||||
Off,
|
||||
/// Drops a face between two leaves when the leaf it faces has a leaf or
|
||||
/// a full opaque cube behind it too: that face only shows through two
|
||||
/// cutout layers. The faces behind the outer layer stay, so a crown
|
||||
/// looks the same from outside. The faced leaf must be in the section
|
||||
/// (as `Hollow`), so the block behind it is at most a border cell of the
|
||||
/// next section, whose edits remesh this one.
|
||||
Hidden,
|
||||
/// Leaves surrounded on all six sides by leaves or full opaque cubes draw
|
||||
/// in the opaque layer, so their cutouts no longer show the layers behind.
|
||||
Solid,
|
||||
@@ -389,9 +344,8 @@ pub enum LeafCullingMode {
|
||||
Hollow,
|
||||
}
|
||||
impl LeafCullingMode {
|
||||
pub const ALL: [LeafCullingMode; 5] = [
|
||||
pub const ALL: [LeafCullingMode; 4] = [
|
||||
LeafCullingMode::Off,
|
||||
LeafCullingMode::Hidden,
|
||||
LeafCullingMode::Solid,
|
||||
LeafCullingMode::SolidAggressive,
|
||||
LeafCullingMode::Hollow,
|
||||
@@ -399,7 +353,6 @@ impl LeafCullingMode {
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
LeafCullingMode::Off => "off",
|
||||
LeafCullingMode::Hidden => "hidden",
|
||||
LeafCullingMode::Solid => "solid",
|
||||
LeafCullingMode::SolidAggressive => "solid_aggressive",
|
||||
LeafCullingMode::Hollow => "hollow",
|
||||
@@ -408,7 +361,6 @@ impl LeafCullingMode {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
LeafCullingMode::Off => "Off",
|
||||
LeafCullingMode::Hidden => "Hidden Faces",
|
||||
LeafCullingMode::Solid => "Solid",
|
||||
LeafCullingMode::SolidAggressive => "Solid Aggressive",
|
||||
LeafCullingMode::Hollow => "Hollow",
|
||||
@@ -521,17 +473,6 @@ pub fn translucent_batching_offered() -> bool {
|
||||
TRANSLUCENT_BATCHING_OFFERED.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
static SMOOTH_GRADIENTS_OFFERED: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(false);
|
||||
/// Whether the device can draw, blend and filter the 16-bit frame (`Graphics::hdr_smooth_gradients`):
|
||||
/// set when the renderer starts. Where it can't, the settings grey its switch out.
|
||||
pub fn offer_smooth_gradients(on: bool) {
|
||||
SMOOTH_GRADIENTS_OFFERED.store(on, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
pub fn smooth_gradients_offered() -> bool {
|
||||
SMOOTH_GRADIENTS_OFFERED.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// The farthest LOD Distance, in chunks, on integrated GPUs other than
|
||||
/// Apple's: they share the CPU's memory bandwidth, and past it far terrain
|
||||
/// costs them more frame time than it is worth.
|
||||
@@ -610,8 +551,6 @@ pub struct Graphics {
|
||||
pub cave_culling: bool,
|
||||
/// Skip drawing entities hidden behind opaque terrain, synchronously or asynchronously.
|
||||
pub entity_culling: EntityCullingMode,
|
||||
/// Skip particles outside the view or hidden behind opaque terrain.
|
||||
pub particle_culling: ParticleCullingMode,
|
||||
/// Terrain leaf faces inside a canopy (see [`LeafCullingMode`]); `Off` draws
|
||||
/// vanilla's fancy leaves. Ignored with a resource pack that may replace
|
||||
/// the vanilla leaf models ([`Graphics::leaf_culling_effective`]).
|
||||
@@ -695,11 +634,6 @@ pub struct Graphics {
|
||||
/// `batchTranslucent`: `translucentBatching`, which every save wrote as false while it was
|
||||
/// off by default and had no switch, stays ignored.
|
||||
pub translucent_batching: bool,
|
||||
/// Smooth Gradients (HDR page, on by default): with HDR output on, the frame is drawn in
|
||||
/// 16 bits, so dark gradients don't band (`Renderer::frame_format`). Read when a renderer
|
||||
/// is made (start, resource-pack reload), like `translucent_batching`, where it is offered
|
||||
/// (`smooth_gradients_offered`).
|
||||
pub hdr_smooth_gradients: bool,
|
||||
/// The shader pack picked in `shaderpacks/` (`shader_pack_list`); empty: none. Its key is
|
||||
/// not `shaderPack`, which versions before the clean-room rewrite saved and which stays
|
||||
/// ignored.
|
||||
@@ -714,7 +648,6 @@ impl Default for Graphics {
|
||||
blue_map: crate::bluemap::Settings::default(),
|
||||
cave_culling: true,
|
||||
entity_culling: EntityCullingMode::Off,
|
||||
particle_culling: ParticleCullingMode::Off,
|
||||
leaf_culling: LeafCullingMode::Off,
|
||||
cache_shulker_boxes: false,
|
||||
// Sign text has used its renderer cache independently of the new model caches.
|
||||
@@ -752,7 +685,6 @@ impl Default for Graphics {
|
||||
shadow_cache: false,
|
||||
api: GraphicsApi::Auto,
|
||||
translucent_batching: true,
|
||||
hdr_smooth_gradients: true,
|
||||
shader_pack: String::new(),
|
||||
}
|
||||
}
|
||||
@@ -809,7 +741,6 @@ impl Graphics {
|
||||
"blueMapAutoDetect" => g.blue_map.auto_detect = flag(g.blue_map.auto_detect),
|
||||
"caveCulling" => g.cave_culling = flag(g.cave_culling),
|
||||
"entityCulling" => g.entity_culling = EntityCullingMode::parse(value),
|
||||
"particleCulling" => g.particle_culling = ParticleCullingMode::parse(value),
|
||||
"leafCulling" => g.leaf_culling = LeafCullingMode::parse(value),
|
||||
"blockEntityCache" => {
|
||||
let enabled = flag(false);
|
||||
@@ -884,7 +815,6 @@ impl Graphics {
|
||||
"batteryMode" => g.battery = flag(g.battery),
|
||||
"shadowCache" => g.shadow_cache = flag(g.shadow_cache),
|
||||
"batchTranslucent" => g.translucent_batching = flag(g.translucent_batching),
|
||||
"hdrSmoothGradients" => g.hdr_smooth_gradients = flag(g.hdr_smooth_gradients),
|
||||
"graphicsApi" => g.api = GraphicsApi::from_name(value).unwrap_or_default(),
|
||||
"pickedShaderPack" => g.shader_pack = value.to_owned(),
|
||||
"overlay" => {
|
||||
@@ -901,13 +831,12 @@ impl Graphics {
|
||||
}
|
||||
pub fn serialize(&self) -> String {
|
||||
let mut text = format!(
|
||||
"preset:{}\nlodDistance:{}\nfarDetail:{}\ncaveCulling:{}\nentityCulling:{}\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:{}\nhdrKey:{}\nbatteryMode:{}\nshadowCache:{}\ngraphicsApi:{}\npickedShaderPack:{}\nbatchTranslucent:{}\nhdrSmoothGradients:{}\n",
|
||||
"preset:{}\nlodDistance:{}\nfarDetail:{}\ncaveCulling:{}\nentityCulling:{}\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:{}\nhdrKey:{}\nbatteryMode:{}\nshadowCache:{}\ngraphicsApi:{}\npickedShaderPack:{}\nbatchTranslucent:{}\n",
|
||||
self.preset.name(),
|
||||
self.lod_distance,
|
||||
self.far_detail.name(),
|
||||
bool_text(self.cave_culling),
|
||||
self.entity_culling.name(),
|
||||
self.particle_culling.name(),
|
||||
self.leaf_culling.name(),
|
||||
bool_text(self.cache_shulker_boxes),
|
||||
bool_text(self.cache_signs),
|
||||
@@ -952,7 +881,6 @@ impl Graphics {
|
||||
self.api.name(),
|
||||
self.shader_pack,
|
||||
bool_text(self.translucent_batching),
|
||||
bool_text(self.hdr_smooth_gradients),
|
||||
);
|
||||
text += &self.blue_map.serialize();
|
||||
text
|
||||
@@ -968,11 +896,6 @@ impl Graphics {
|
||||
pub fn save(&self, dir: &Path) {
|
||||
let _ = fs::write(dir.join(FILE), self.serialize());
|
||||
}
|
||||
/// Whether a renderer made now draws its frame in 16 bits (`Renderer::new`'s `wide_frame`):
|
||||
/// HDR on, with Smooth Gradients.
|
||||
pub fn wide_frame(&self) -> bool {
|
||||
self.hdr && self.hdr_smooth_gradients
|
||||
}
|
||||
/// Paper white, in nits: the player's, or on Auto the desktop's SDR white as the
|
||||
/// system reports it, else 203 (BT.2408's reference white).
|
||||
pub fn paper_white_nits(&self) -> u32 {
|
||||
@@ -1092,7 +1015,6 @@ mod tests {
|
||||
},
|
||||
cave_culling: false,
|
||||
entity_culling: EntityCullingMode::Synchronous,
|
||||
particle_culling: ParticleCullingMode::Asynchronous,
|
||||
leaf_culling: LeafCullingMode::SolidAggressive,
|
||||
cache_shulker_boxes: true,
|
||||
cache_signs: true,
|
||||
@@ -1129,7 +1051,6 @@ mod tests {
|
||||
shadow_cache: true,
|
||||
api: GraphicsApi::Dx12,
|
||||
translucent_batching: false,
|
||||
hdr_smooth_gradients: false,
|
||||
shader_pack: "Golden Hour".into(),
|
||||
};
|
||||
assert_eq!(Graphics::parse(&g.serialize()), g);
|
||||
@@ -1140,16 +1061,6 @@ mod tests {
|
||||
};
|
||||
assert_eq!(Graphics::parse(&graphics.serialize()), graphics);
|
||||
}
|
||||
for mode in ParticleCullingMode::ALL {
|
||||
let graphics = Graphics {
|
||||
particle_culling: mode,
|
||||
..Graphics::default()
|
||||
};
|
||||
assert_eq!(
|
||||
Graphics::parse(&graphics.serialize()).particle_culling,
|
||||
mode
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
Graphics::parse("entityCulling:true\n").entity_culling,
|
||||
EntityCullingMode::Synchronous
|
||||
@@ -1171,10 +1082,6 @@ mod tests {
|
||||
// off by default, not a choice.
|
||||
assert!(Graphics::default().translucent_batching);
|
||||
assert!(!Graphics::parse("batchTranslucent:false\n").translucent_batching);
|
||||
// Smooth Gradients is on, and draws a 16-bit frame only with HDR on.
|
||||
assert!(Graphics::default().hdr_smooth_gradients && !Graphics::default().wide_frame());
|
||||
assert!(Graphics::parse("hdr:true\n").wide_frame());
|
||||
assert!(!Graphics::parse("hdr:true\nhdrSmoothGradients:false\n").wide_frame());
|
||||
assert!(Graphics::parse("translucentBatching:false\n").translucent_batching);
|
||||
// 0 is Auto; anything else is a peak the slider reaches.
|
||||
assert_eq!(Graphics::parse("hdrPeak:0\n").hdr_peak, 0);
|
||||
|
||||
@@ -209,29 +209,7 @@ pub fn button(
|
||||
}
|
||||
let color = if live { WHITE } else { MUTED };
|
||||
let ty = y + ((h - 8.0) / 2.0).floor();
|
||||
scrolling_label(batch, atlas, label, [x + 2.0, ty, w - 4.0], (y, h), color);
|
||||
}
|
||||
/// A widget label of `[left, y, width]` inside the widget's `(top, height)`: centred when it fits,
|
||||
/// else slid (vanilla `AbstractWidget.extractScrollingStringOverContents`) and clipped to the box.
|
||||
fn scrolling_label(
|
||||
batch: &mut HudBatch,
|
||||
atlas: &HudAtlas,
|
||||
text: &Text,
|
||||
[left, y, width]: [f32; 3],
|
||||
(top, height): (f32, f32),
|
||||
color: u32,
|
||||
) {
|
||||
ui::scrolling_text(
|
||||
batch,
|
||||
atlas,
|
||||
text,
|
||||
left,
|
||||
left + width,
|
||||
top,
|
||||
top + height,
|
||||
y,
|
||||
color,
|
||||
);
|
||||
ui::centered_text(batch, atlas, label, x + 2.0, ty, w - 4.0, color);
|
||||
}
|
||||
/// A slider: its value over a thin track filled in the accent up to the handle at `position`.
|
||||
pub fn slider(
|
||||
@@ -244,14 +222,7 @@ pub fn slider(
|
||||
) {
|
||||
let [x, y, w, h] = rect;
|
||||
let color = if live { WHITE } else { MUTED };
|
||||
scrolling_label(
|
||||
batch,
|
||||
atlas,
|
||||
value,
|
||||
[x + 2.0, y + 1.0, w - 4.0],
|
||||
(y, h),
|
||||
color,
|
||||
);
|
||||
ui::centered_text(batch, atlas, value, x + 2.0, y + 1.0, w - 4.0, color);
|
||||
let line = y + h - 3.0;
|
||||
let at = x + (position.clamp(0.0, 1.0) * (w - 4.0)).round();
|
||||
fill(batch, atlas, [x, line, w, 2.0], TRACK);
|
||||
@@ -289,7 +260,7 @@ pub fn cycler(
|
||||
(true, true) => WHITE,
|
||||
(true, false) => SOFT,
|
||||
};
|
||||
scrolling_label(batch, atlas, value, [x + 9.0, ty, w - 18.0], (y, h), color);
|
||||
batch.text_styled(atlas, value, x + 9.0, ty, color, true);
|
||||
}
|
||||
/// A screen's title, centred at the top (split-screen profiles).
|
||||
pub fn title(batch: &mut HudBatch, atlas: &HudAtlas, text: &Text) {
|
||||
@@ -300,76 +271,3 @@ pub fn title(batch: &mut HudBatch, atlas: &HudAtlas, text: &Text) {
|
||||
pub fn cost_color(cost: Cost) -> u32 {
|
||||
hex(cost.rgb(), 255)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn atlas() -> HudAtlas {
|
||||
HudAtlas::build(std::path::Path::new("/nonexistent/horizon-test-assets")).unwrap()
|
||||
}
|
||||
|
||||
fn x_span(batch: &HudBatch, plain: &HudBatch) -> (f32, f32) {
|
||||
let label = |v: &&crate::hud::HudVertex| !plain.vertices.iter().any(|p| p.pos == v.pos);
|
||||
batch
|
||||
.vertices
|
||||
.iter()
|
||||
.filter(label)
|
||||
.map(|v| v.pos[0])
|
||||
.fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), x| {
|
||||
(lo.min(x), hi.max(x))
|
||||
})
|
||||
}
|
||||
|
||||
/// A label wider than its widget slides inside the widget's box instead of being cut by
|
||||
/// characters; a short one stays centred.
|
||||
#[test]
|
||||
fn long_labels_stay_in_the_widget_and_short_ones_centred() {
|
||||
let atlas = atlas();
|
||||
let rect = [10.0, 20.0, 100.0, 20.0];
|
||||
let long = Text::literal("Graphics API: Prefer Vulkan (Experimental)");
|
||||
assert!(atlas.font.width_text(&long.resolved()) > rect[2]);
|
||||
let short = Text::literal("Done");
|
||||
let short_w = atlas.font.width_text(&short.resolved());
|
||||
let draws: [(&str, fn(&mut HudBatch, &HudAtlas, [f32; 4], &Text)); 3] = [
|
||||
("button", |b, a, r, t| {
|
||||
button(b, a, r, t, (false, true, false))
|
||||
}),
|
||||
("slider", |b, a, r, t| {
|
||||
slider(b, a, r, t, 0.5, (false, true))
|
||||
}),
|
||||
("cycler", |b, a, r, t| cycler(b, a, r, t, (false, true))),
|
||||
];
|
||||
for (name, draw) in draws {
|
||||
let mut plain = HudBatch::new([640, 480]);
|
||||
draw(&mut plain, &atlas, rect, &Text::empty());
|
||||
let base = plain.vertices.len();
|
||||
let mut batch = HudBatch::new([640, 480]);
|
||||
draw(&mut batch, &atlas, rect, &long);
|
||||
assert!(batch.vertices.len() > base, "{name}: long label drawn");
|
||||
// The widget's own fill, track and handle also draw: only the label's vertices count.
|
||||
let label = batch
|
||||
.vertices
|
||||
.iter()
|
||||
.filter(|v| !plain.vertices.iter().any(|p| p.pos == v.pos));
|
||||
for v in label {
|
||||
assert!(
|
||||
(rect[0]..=rect[0] + rect[2]).contains(&v.pos[0])
|
||||
&& (rect[1]..=rect[1] + rect[3]).contains(&v.pos[1]),
|
||||
"{name}: {:?} outside the widget",
|
||||
v.pos
|
||||
);
|
||||
}
|
||||
// The label's own glyphs (past the widget's fill) start centred.
|
||||
let mut batch = HudBatch::new([640, 480]);
|
||||
draw(&mut batch, &atlas, rect, &short);
|
||||
let (lo, hi) = x_span(&batch, &plain);
|
||||
let middle = (lo + hi) / 2.0;
|
||||
let want = rect[0] + rect[2] / 2.0;
|
||||
assert!(
|
||||
(middle - want).abs() <= short_w / 2.0 + 2.0 && (middle - want).abs() < 3.0,
|
||||
"{name}: centre {middle}, want {want}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
use crate::{
|
||||
graphics::{
|
||||
EntityCullingMode, FarDetail, FrameGeneration, GraphicsApi, LeafCullingMode, Overlay,
|
||||
Pacing, ParticleCullingMode, PresentMode, Preset, Upscaling,
|
||||
Pacing, PresentMode, Preset, Upscaling,
|
||||
},
|
||||
graphics_style::{self as style, Cost},
|
||||
hud::{HudAtlas, HudBatch},
|
||||
@@ -66,7 +66,6 @@ pub enum H {
|
||||
BlueMapRadius,
|
||||
CaveCulling,
|
||||
EntityCulling,
|
||||
ParticleCulling,
|
||||
LeafCulling,
|
||||
CacheShulkerBoxes,
|
||||
CacheSigns,
|
||||
@@ -104,7 +103,6 @@ pub enum H {
|
||||
ShadowCache,
|
||||
GraphicsApi,
|
||||
TranslucentBatching,
|
||||
HdrSmoothGradients,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -381,11 +379,6 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
|
||||
None,
|
||||
"Hides entities behind opaque blocks.",
|
||||
),
|
||||
h(
|
||||
H::ParticleCulling,
|
||||
None,
|
||||
"Skips offscreen, subpixel and terrain-hidden particles.",
|
||||
),
|
||||
h(
|
||||
H::LeafCulling,
|
||||
Saves,
|
||||
@@ -497,11 +490,6 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
|
||||
None,
|
||||
"Lava, fire and sunlight shine brighter than white. Needs an HDR display.",
|
||||
),
|
||||
h(
|
||||
H::HdrSmoothGradients,
|
||||
Low,
|
||||
"16-bit frame: dark skies and shadows don't band. A bit more VRAM. Next start.",
|
||||
),
|
||||
h(
|
||||
H::HdrCalibrate,
|
||||
None,
|
||||
@@ -632,9 +620,6 @@ fn description(item: Item, s: &Settings, text: &'static str) -> &'static str {
|
||||
},
|
||||
(Item::Horizon(H::LeafCulling), _) => match s.horizon.leaf_culling {
|
||||
LeafCullingMode::Off => "Every leaf face. Vanilla's fancy leaves.",
|
||||
LeafCullingMode::Hidden => {
|
||||
"Drops leaf faces behind two leaf layers: faster, same crowns."
|
||||
}
|
||||
LeafCullingMode::Solid => {
|
||||
"Leaves fully inside a canopy draw solid: fewer see-through layers."
|
||||
}
|
||||
@@ -643,15 +628,6 @@ fn description(item: Item, s: &Settings, text: &'static str) -> &'static str {
|
||||
}
|
||||
LeafCullingMode::Hollow => "Drops faces between leaves: faster, but gaps look through.",
|
||||
},
|
||||
(Item::Horizon(H::ParticleCulling), _) => match s.horizon.particle_culling {
|
||||
ParticleCullingMode::Off => "Draws every particle.",
|
||||
ParticleCullingMode::Synchronous => {
|
||||
"Skips offscreen, subpixel and terrain-hidden particles."
|
||||
}
|
||||
ParticleCullingMode::Asynchronous => {
|
||||
"Skips offscreen, subpixel and terrain-hidden particles with less render-thread work."
|
||||
}
|
||||
},
|
||||
_ => text,
|
||||
}
|
||||
}
|
||||
@@ -743,7 +719,6 @@ impl H {
|
||||
),
|
||||
H::CaveCulling => labelled("Cave Culling", on_off(g.cave_culling)),
|
||||
H::EntityCulling => labelled("Entity Culling", g.entity_culling.label()),
|
||||
H::ParticleCulling => labelled("Particle Culling", g.particle_culling.label()),
|
||||
H::LeafCulling => labelled("Leaf Culling", g.leaf_culling.label()),
|
||||
H::CacheShulkerBoxes => labelled("Shulker Boxes", on_off(g.cache_shulker_boxes)),
|
||||
H::CacheSigns => labelled("Signs", on_off(g.cache_signs)),
|
||||
@@ -830,16 +805,12 @@ impl H {
|
||||
H::BatteryMode => labelled("Battery Mode", on_off(g.battery)),
|
||||
H::ShadowCache => labelled("Shadow Cache", on_off(g.shadow_cache)),
|
||||
H::GraphicsApi => labelled("Graphics API", g.api.label()),
|
||||
// These two are greyed out where the device can't (`Item::blocked`): Off, as
|
||||
// they are there, while the saved value waits for a device that can.
|
||||
// Greyed out where the device can't (`Item::blocked`): Off, as it is there,
|
||||
// while the saved value waits for a device that can.
|
||||
H::TranslucentBatching => labelled(
|
||||
"Translucent Batching",
|
||||
on_off(g.translucent_batching && crate::graphics::translucent_batching_offered()),
|
||||
),
|
||||
H::HdrSmoothGradients => labelled(
|
||||
"Smooth Gradients",
|
||||
on_off(g.hdr_smooth_gradients && crate::graphics::smooth_gradients_offered()),
|
||||
),
|
||||
H::Overlay => labelled(
|
||||
"Performance Overlay",
|
||||
match g.overlay {
|
||||
@@ -900,9 +871,6 @@ impl H {
|
||||
H::EntityCulling => {
|
||||
g.entity_culling = step(&EntityCullingMode::ALL, g.entity_culling, back)
|
||||
}
|
||||
H::ParticleCulling => {
|
||||
g.particle_culling = step(&ParticleCullingMode::ALL, g.particle_culling, back)
|
||||
}
|
||||
H::LeafCulling => {
|
||||
g.leaf_culling = step(&LeafCullingMode::ALL, g.leaf_culling, back);
|
||||
}
|
||||
@@ -952,7 +920,6 @@ impl H {
|
||||
H::ShadowCache => g.shadow_cache = !g.shadow_cache,
|
||||
H::GraphicsApi => g.api = step(GraphicsApi::choices(), g.api, back),
|
||||
H::TranslucentBatching => g.translucent_batching = !g.translucent_batching,
|
||||
H::HdrSmoothGradients => g.hdr_smooth_gradients = !g.hdr_smooth_gradients,
|
||||
H::Pacing => {
|
||||
g.pacing = match g.pacing {
|
||||
Pacing::Efficient => Pacing::Precise,
|
||||
@@ -984,8 +951,7 @@ impl Item {
|
||||
/// folder, a display swapped for an SDR one: the renderer leaves HDR off there, and
|
||||
/// the saved values wait for an HDR display), and its sliders do something only
|
||||
/// while it is on; Translucent Batching switches only where the device can batch
|
||||
/// (`graphics::translucent_batching_offered`), its saved value waiting for one that can,
|
||||
/// and Smooth Gradients only where it can draw the 16-bit frame, the same way.
|
||||
/// (`graphics::translucent_batching_offered`), its saved value waiting for one that can.
|
||||
fn blocked(self, s: &Settings) -> Option<&'static str> {
|
||||
if !self.available() {
|
||||
return Some("This feature is not yet available in Horizon.");
|
||||
@@ -997,23 +963,16 @@ impl Item {
|
||||
| H::HdrBlackLevel
|
||||
| H::HdrPeak
|
||||
| H::HdrPaperWhite
|
||||
| H::HdrUiBrightness
|
||||
| H::HdrSmoothGradients,
|
||||
| H::HdrUiBrightness,
|
||||
) if !crate::graphics::hdr_offered() => {
|
||||
Some("Your display or system doesn't offer HDR output.")
|
||||
}
|
||||
Item::Horizon(H::HdrSmoothGradients)
|
||||
if !crate::graphics::smooth_gradients_offered() =>
|
||||
{
|
||||
Some("Your GPU or graphics API doesn't support it.")
|
||||
}
|
||||
Item::Horizon(
|
||||
H::HdrCalibrate
|
||||
| H::HdrBlackLevel
|
||||
| H::HdrPeak
|
||||
| H::HdrPaperWhite
|
||||
| H::HdrUiBrightness
|
||||
| H::HdrSmoothGradients,
|
||||
| H::HdrUiBrightness,
|
||||
) if !s.horizon.hdr => Some("Turn HDR on to use it."),
|
||||
Item::Horizon(H::TranslucentBatching)
|
||||
if !crate::graphics::translucent_batching_offered() =>
|
||||
@@ -1051,9 +1010,6 @@ impl Item {
|
||||
Item::Horizon(H::BatteryMode) => g.battery,
|
||||
Item::Horizon(H::ShadowCache) => g.shadow_cache,
|
||||
Item::Horizon(H::TranslucentBatching) => g.translucent_batching,
|
||||
Item::Horizon(H::HdrSmoothGradients) => {
|
||||
g.hdr_smooth_gradients && crate::graphics::smooth_gradients_offered()
|
||||
}
|
||||
Item::Vanilla(Opt::ViewBobbing) => s.view_bobbing,
|
||||
Item::Vanilla(Opt::Vsync) => s.vsync,
|
||||
Item::Vanilla(Opt::Fullscreen) => s.fullscreen,
|
||||
@@ -1120,7 +1076,6 @@ impl Item {
|
||||
| H::ShadowCache
|
||||
| H::GraphicsApi
|
||||
| H::TranslucentBatching
|
||||
| H::HdrSmoothGradients
|
||||
) | Item::Vanilla(
|
||||
Opt::GuiScale
|
||||
| Opt::Fullscreen
|
||||
@@ -1947,7 +1902,6 @@ impl GraphicsUi {
|
||||
framebuffer: [i32; 2],
|
||||
drag: bool,
|
||||
right: bool,
|
||||
shift: bool,
|
||||
) -> Vec<Action> {
|
||||
self.keep(s);
|
||||
let (cols, lines, content) = self.layout(s, w, h);
|
||||
@@ -2025,7 +1979,7 @@ impl GraphicsUi {
|
||||
return Vec::new();
|
||||
}
|
||||
self.focus = Some(Target::Item(item));
|
||||
let actions = self.activate(s, item, shift, framebuffer, Some((x, &cols)));
|
||||
let actions = self.activate(s, item, false, framebuffer, Some((x, &cols)));
|
||||
self.mark(&actions);
|
||||
actions
|
||||
}
|
||||
@@ -2404,15 +2358,7 @@ mod tests {
|
||||
[cols.list_x, ui.row_y(&cols, &line), cols.list_w, style::ROW]
|
||||
}
|
||||
fn press(ui: &mut GraphicsUi, s: &mut Settings, (x, y): (f32, f32)) -> Vec<Action> {
|
||||
ui.click(s, (x, y), SIZE, FB, false, false, false)
|
||||
}
|
||||
fn press_shift(
|
||||
ui: &mut GraphicsUi,
|
||||
s: &mut Settings,
|
||||
(x, y): (f32, f32),
|
||||
shift: bool,
|
||||
) -> Vec<Action> {
|
||||
ui.click(s, (x, y), SIZE, FB, false, false, shift)
|
||||
ui.click(s, (x, y), SIZE, FB, false, false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2503,15 +2449,7 @@ mod tests {
|
||||
assert!(y >= 0.0 && x + w <= size.0);
|
||||
// Undo, over Done, stays clear of the page list.
|
||||
assert!(cols.button(1)[1] >= y + h);
|
||||
ui.click(
|
||||
&mut s,
|
||||
(x + w / 2.0, y + h / 2.0),
|
||||
size,
|
||||
FB,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
ui.click(&mut s, (x + w / 2.0, y + h / 2.0), size, FB, false, false);
|
||||
let (_, lines, _) = ui.layout(&s, size.0, size.1);
|
||||
assert_eq!(ui.current(&lines), Some(Page::Hdr));
|
||||
}
|
||||
@@ -2628,7 +2566,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(H::ChunkUpdates.slider(&s), Some(1.0));
|
||||
assert_eq!(
|
||||
ui.click(&mut s, (-100.0, -100.0), SIZE, FB, true, false, false),
|
||||
ui.click(&mut s, (-100.0, -100.0), SIZE, FB, true, false),
|
||||
[Action::SliderDrag]
|
||||
);
|
||||
assert_eq!(H::ChunkUpdates.slider(&s), Some(0.0));
|
||||
@@ -2636,31 +2574,6 @@ mod tests {
|
||||
assert!(!ui.release());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smooth_gradients_wait_for_a_device_that_draws_the_16_bit_frame() {
|
||||
let _offer = HDR_OFFER.lock().unwrap_or_else(|e| e.into_inner());
|
||||
crate::graphics::offer_hdr(true);
|
||||
let mut s = Settings::default();
|
||||
s.horizon.hdr = true;
|
||||
let item = Item::Horizon(H::HdrSmoothGradients);
|
||||
// Where the device can't, the saved On reads Off.
|
||||
crate::graphics::offer_smooth_gradients(false);
|
||||
assert_eq!(
|
||||
item.blocked(&s),
|
||||
Some("Your GPU or graphics API doesn't support it.")
|
||||
);
|
||||
assert_eq!(item.label(&s).plain(), "Smooth Gradients: Off");
|
||||
assert_eq!(item.switch(&s), Some(false));
|
||||
assert!(s.horizon.hdr_smooth_gradients);
|
||||
// Where it can, it waits only for HDR, like the rest of the page.
|
||||
crate::graphics::offer_smooth_gradients(true);
|
||||
assert_eq!(item.blocked(&s), None);
|
||||
assert_eq!(item.label(&s).plain(), "Smooth Gradients: On");
|
||||
assert_eq!(item.switch(&s), Some(true));
|
||||
s.horizon.hdr = false;
|
||||
assert_eq!(item.blocked(&s), Some("Turn HDR on to use it."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hdr_waits_for_a_display_that_offers_it_and_its_sliders_for_the_switch() {
|
||||
let _offer = HDR_OFFER.lock().unwrap_or_else(|e| e.into_inner());
|
||||
@@ -2725,7 +2638,7 @@ mod tests {
|
||||
let (cols, _, content) = ui.layout(&s, SIZE.0, SIZE.1);
|
||||
let x = cols.bar_x() + 2.0;
|
||||
let drag = |ui: &mut GraphicsUi, s: &mut Settings, y, drag| {
|
||||
ui.click(s, (x, y), SIZE, FB, drag, false, false)
|
||||
ui.click(s, (x, y), SIZE, FB, drag, false)
|
||||
};
|
||||
assert!(drag(&mut ui, &mut s, cols.top + 5.0, false).is_empty());
|
||||
drag(&mut ui, &mut s, cols.top + 15.0, true);
|
||||
@@ -2745,13 +2658,13 @@ mod tests {
|
||||
let mut ui = GraphicsUi::default();
|
||||
let (cols, ..) = ui.layout(&s, SIZE.0, SIZE.1);
|
||||
let x = cols.bar_x() + 2.0;
|
||||
ui.click(&mut s, (x, cols.top + 5.0), SIZE, FB, false, false, false);
|
||||
ui.click(&mut s, (x, cols.top + 5.0), SIZE, FB, false, false);
|
||||
assert!(ui.scroll.held());
|
||||
// A 130-high GUI leaves the list under the 40 px the scroller's thumb needs.
|
||||
let short = (SIZE.0, 130.0);
|
||||
assert!(!Columns::of(short.0, short.1).scrolls());
|
||||
assert!(
|
||||
ui.click(&mut s, (x, 80.0), short, FB, true, false, false)
|
||||
ui.click(&mut s, (x, 80.0), short, FB, true, false)
|
||||
.is_empty()
|
||||
);
|
||||
assert!(!ui.scroll.held());
|
||||
@@ -3061,92 +2974,6 @@ mod tests {
|
||||
assert_eq!(item.label(&s).plain(), "Entity Culling: Asynchronous");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn particle_culling_modes_are_opt_in_and_cycle_from_its_row() {
|
||||
let mut s = Settings::default();
|
||||
let item = Item::Horizon(H::ParticleCulling);
|
||||
assert_eq!(s.horizon.particle_culling, ParticleCullingMode::Off);
|
||||
assert_eq!(item.label(&s).plain(), "Particle Culling: Off");
|
||||
for mode in ParticleCullingMode::ALL {
|
||||
s.horizon.particle_culling = mode;
|
||||
for preset in Preset::ALL {
|
||||
crate::graphics::apply_preset(preset, &mut s);
|
||||
assert_eq!(s.horizon.particle_culling, mode);
|
||||
}
|
||||
assert_eq!(
|
||||
crate::graphics::Graphics::parse(&s.horizon.serialize()).particle_culling,
|
||||
mode
|
||||
);
|
||||
}
|
||||
s.horizon.particle_culling = ParticleCullingMode::Off;
|
||||
let mut ui = GraphicsUi::default();
|
||||
let r = row(&mut ui, &s, item);
|
||||
for (mode, label) in [
|
||||
(ParticleCullingMode::Synchronous, "Synchronous"),
|
||||
(ParticleCullingMode::Asynchronous, "Asynchronous"),
|
||||
(ParticleCullingMode::Off, "Off"),
|
||||
] {
|
||||
s.horizon.preset = Preset::Balanced;
|
||||
assert_eq!(
|
||||
press(&mut ui, &mut s, (r[0] + 4.0, r[1] + 4.0)),
|
||||
[Action::ButtonClick, Action::Changed]
|
||||
);
|
||||
assert_eq!(s.horizon.particle_culling, mode);
|
||||
assert_eq!(s.horizon.preset, Preset::Custom);
|
||||
assert_eq!(item.label(&s).plain(), format!("Particle Culling: {label}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphics_cycle_button_cycles_forward_and_shift_click_cycles_backward_with_wrapping() {
|
||||
let mut s = Settings::default();
|
||||
let item = Item::Horizon(H::EntityCulling);
|
||||
let mut ui = GraphicsUi::default();
|
||||
let r = row(&mut ui, &s, item);
|
||||
let at = (r[0] + 4.0, r[1] + 4.0);
|
||||
|
||||
// 1. Feature without shift (normal click): cycles forward (+1)
|
||||
assert_eq!(s.horizon.entity_culling, EntityCullingMode::Off);
|
||||
assert_eq!(
|
||||
press_shift(&mut ui, &mut s, at, false),
|
||||
[Action::ButtonClick, Action::Changed]
|
||||
);
|
||||
assert_eq!(s.horizon.entity_culling, EntityCullingMode::Synchronous);
|
||||
|
||||
assert_eq!(
|
||||
press_shift(&mut ui, &mut s, at, false),
|
||||
[Action::ButtonClick, Action::Changed]
|
||||
);
|
||||
assert_eq!(s.horizon.entity_culling, EntityCullingMode::Asynchronous);
|
||||
|
||||
// 3. Wrap-around forward (at max value, normal click wraps to min / 0)
|
||||
assert_eq!(
|
||||
press_shift(&mut ui, &mut s, at, false),
|
||||
[Action::ButtonClick, Action::Changed]
|
||||
);
|
||||
assert_eq!(s.horizon.entity_culling, EntityCullingMode::Off);
|
||||
|
||||
// 4. Wrap-around backward (at min / 0, shift-click wraps to max)
|
||||
assert_eq!(
|
||||
press_shift(&mut ui, &mut s, at, true),
|
||||
[Action::ButtonClick, Action::Changed]
|
||||
);
|
||||
assert_eq!(s.horizon.entity_culling, EntityCullingMode::Asynchronous);
|
||||
|
||||
// 2. Feature with shift: cycles backward (-1)
|
||||
assert_eq!(
|
||||
press_shift(&mut ui, &mut s, at, true),
|
||||
[Action::ButtonClick, Action::Changed]
|
||||
);
|
||||
assert_eq!(s.horizon.entity_culling, EntityCullingMode::Synchronous);
|
||||
|
||||
assert_eq!(
|
||||
press_shift(&mut ui, &mut s, at, true),
|
||||
[Action::ButtonClick, Action::Changed]
|
||||
);
|
||||
assert_eq!(s.horizon.entity_culling, EntityCullingMode::Off);
|
||||
}
|
||||
|
||||
/// Translucent Batching: a switch on the Advanced page, on by default, no part of a
|
||||
/// preset, and greyed out, reading Off, where the device can't batch.
|
||||
#[test]
|
||||
@@ -3204,7 +3031,7 @@ mod tests {
|
||||
let item = Item::Horizon(H::LeafCulling);
|
||||
assert_eq!(item.switch(&s), None);
|
||||
assert_eq!(item.label(&s).plain(), "Leaf Culling: Off");
|
||||
for label in ["Hidden Faces", "Solid", "Solid Aggressive", "Hollow", "Off"] {
|
||||
for label in ["Solid", "Solid Aggressive", "Hollow", "Off"] {
|
||||
item.cycle(&mut s, false, FB);
|
||||
assert_eq!(item.label(&s).plain(), format!("Leaf Culling: {label}"));
|
||||
assert_eq!(
|
||||
@@ -3223,12 +3050,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(culling.0, Page::Performance);
|
||||
let block: Vec<Item> = culling.1.iter().map(|r| r.item).collect();
|
||||
for h in [
|
||||
H::CaveCulling,
|
||||
H::EntityCulling,
|
||||
H::ParticleCulling,
|
||||
H::LeafCulling,
|
||||
] {
|
||||
for h in [H::CaveCulling, H::EntityCulling, H::LeafCulling] {
|
||||
assert!(block.contains(&Item::Horizon(h)), "{h:?}");
|
||||
}
|
||||
// The block-entity caches are the group after it, in this order.
|
||||
@@ -3279,7 +3101,7 @@ mod tests {
|
||||
press(&mut ui, &mut s, (r[0] + 4.0, r[1] + 4.0)),
|
||||
[Action::ButtonClick, Action::Changed]
|
||||
);
|
||||
assert_eq!(s.horizon.leaf_culling, LeafCullingMode::Hidden);
|
||||
assert_eq!(s.horizon.leaf_culling, LeafCullingMode::Solid);
|
||||
assert_eq!(s.horizon.preset, Preset::Custom);
|
||||
}
|
||||
|
||||
@@ -3444,47 +3266,36 @@ mod tests {
|
||||
let mut s = Settings::default();
|
||||
s.horizon.leaf_culling = LeafCullingMode::Hollow;
|
||||
s.horizon.entity_culling = EntityCullingMode::Asynchronous;
|
||||
s.horizon.particle_culling = ParticleCullingMode::Asynchronous;
|
||||
for (code, leaf, entity, leaf_text, particle, particle_text) in [
|
||||
for (code, leaf, entity, leaf_text) in [
|
||||
(
|
||||
"de_de",
|
||||
"Laub-Culling: Hohl",
|
||||
"Objekt-Culling: Asynchron",
|
||||
"Lässt Flächen zwischen Blättern weg: schneller, aber Lücken sind durchsichtig.",
|
||||
"Partikel-Culling: Asynchron",
|
||||
"Überspringt Partikel außerhalb des Bildes, unterhalb eines Pixels oder hinter Terrain und entlastet den Render-Thread.",
|
||||
),
|
||||
(
|
||||
"pl_pl",
|
||||
"Pomijanie liści: Puste",
|
||||
"Pomijanie bytów: Asynchroniczne",
|
||||
"Pomija ściany między liśćmi: szybciej, ale luki są przezroczyste.",
|
||||
"Pomijanie cząsteczek: Asynchroniczne",
|
||||
"Pomija cząsteczki poza ekranem, mniejsze niż piksel i zasłonięte przez teren, odciążając wątek renderowania.",
|
||||
),
|
||||
(
|
||||
"uk_ua",
|
||||
"Відсікання листя: Порожнє",
|
||||
"Відсік. сутностей: Асинхронно",
|
||||
"Прибирає грані між листям: швидше, але прогалини прозорі.",
|
||||
"Відсікання частинок: Асинхронно",
|
||||
"Пропускає частинки поза екраном, менші за піксель і закриті місцевістю, зменшуючи роботу потоку рендерингу.",
|
||||
),
|
||||
(
|
||||
"zh_cn",
|
||||
"树叶剔除: 镂空",
|
||||
"实体剔除: 异步",
|
||||
"省略树叶之间的面:更快,但缝隙会透视。",
|
||||
"粒子剔除: 异步",
|
||||
"跳过屏幕外、小于一个像素和被地形遮挡的粒子,减少渲染线程的工作量。",
|
||||
),
|
||||
(
|
||||
"fr_fr",
|
||||
"Occlusion feuillage: Creux",
|
||||
"Occlusion des entités: Asynchrone",
|
||||
"Supprime les faces entre feuilles : plus rapide, mais on voit à travers les trous.",
|
||||
"Occlusion des particules: Asynchrone",
|
||||
"Ignore les particules hors écran, trop petites ou masquées par le terrain avec moins de travail sur le thread de rendu.",
|
||||
),
|
||||
] {
|
||||
let (shown, _) = lang::scoped(lang::Language::load(&[], code), || {
|
||||
@@ -3493,19 +3304,11 @@ mod tests {
|
||||
item(H::LeafCulling).label(&s).plain(),
|
||||
item(H::EntityCulling).label(&s).plain(),
|
||||
lang::get(description(item(H::LeafCulling), &s, "")),
|
||||
item(H::ParticleCulling).label(&s).plain(),
|
||||
lang::get(description(item(H::ParticleCulling), &s, "")),
|
||||
)
|
||||
});
|
||||
assert_eq!(
|
||||
shown,
|
||||
(
|
||||
leaf.into(),
|
||||
entity.into(),
|
||||
leaf_text.into(),
|
||||
particle.into(),
|
||||
particle_text.into(),
|
||||
),
|
||||
(leaf.into(), entity.into(), leaf_text.into()),
|
||||
"{code}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,14 +58,6 @@ fn shows_world(c: vec3<f32>, w: vec3<f32>) -> bool {
|
||||
return f <= 1.01 && f > 0.02 && all(abs(c - f * w) <= 0.03 * w + 1.0 / 255.0);
|
||||
}
|
||||
|
||||
// A texel with its alpha as an 8-bit frame holds it. A 16-bit frame
|
||||
// (Renderer::frame_format) keeps alpha between 8-bit steps, where the marks below
|
||||
// would read differently (an alpha of 0.001 would count as the hand); its colour
|
||||
// keeps the gradients 8 bits would band. On an 8-bit frame nothing changes.
|
||||
fn frame_texel(v: vec4<f32>) -> vec4<f32> {
|
||||
return vec4(saturate(v.rgb), round(saturate(v.a) * 255.0) / 255.0);
|
||||
}
|
||||
|
||||
// Linear BT.709 to BT.2020 (ITU-R BT.2087), columns first.
|
||||
const BT709_TO_BT2020 = mat3x3<f32>(
|
||||
vec3(0.6274, 0.0691, 0.0164),
|
||||
@@ -160,7 +152,7 @@ fn calibration_nits(p: vec2<f32>, size: vec2<f32>) -> f32 {
|
||||
}
|
||||
return vec4(nits / 80.0, 1.0);
|
||||
}
|
||||
let f = frame_texel(textureLoad(frame, at, 0));
|
||||
let f = textureLoad(frame, at, 0);
|
||||
let c = f.rgb;
|
||||
// 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
|
||||
@@ -170,7 +162,7 @@ fn calibration_nits(p: vec2<f32>, size: vec2<f32>) -> f32 {
|
||||
let ui = select(f.a, 0.0, hand);
|
||||
// Gamma 2.2, as SDR displays and the compositors' SDR paths decode it.
|
||||
var light = pow(max(c, vec3(0.0)), vec3(2.2));
|
||||
let w = frame_texel(textureLoad(world, at, 0));
|
||||
let w = 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
|
||||
|
||||
@@ -96,9 +96,6 @@ pub struct HudAtlas {
|
||||
/// A resource pack's own `core/text` shader for the GUI's text, translated;
|
||||
/// none draws the text natively.
|
||||
pub text_shader: Option<std::sync::Arc<horizon_shaders::CoreProgram>>,
|
||||
/// 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' OptiFine random entity textures, whose variants this atlas packs.
|
||||
pub random_textures: horizon_optifine::random_textures::RandomTextures,
|
||||
/// Their emissive overlays (`_e`), packed here too.
|
||||
@@ -816,13 +813,6 @@ impl HudAtlas {
|
||||
pub fn build_over_with_image(root: &Path, defaults: &Path, mod_image: bool) -> Result<Self> {
|
||||
let mut atlas = Self::pack(root, defaults, WALKED_TEXTURE_FOLDERS, true, mod_image)?;
|
||||
atlas.text_shader = pack_text::load(root, defaults);
|
||||
atlas.lines_shader = pack_text::load_core(
|
||||
root,
|
||||
defaults,
|
||||
"rendertype_lines",
|
||||
&[],
|
||||
&crate::gfx::core_lines::GLUE,
|
||||
);
|
||||
Ok(atlas)
|
||||
}
|
||||
/// Empty slots written at run time: map textures (game/maps.rs), the
|
||||
@@ -951,7 +941,6 @@ impl HudAtlas {
|
||||
glint_armor: (vec![0; 4], 1, 1),
|
||||
white,
|
||||
text_shader: None,
|
||||
lines_shader: None,
|
||||
random_textures: Default::default(),
|
||||
emissive: Default::default(),
|
||||
blinks: Default::default(),
|
||||
@@ -1395,7 +1384,6 @@ impl HudAtlas {
|
||||
glint,
|
||||
glint_armor,
|
||||
text_shader: None,
|
||||
lines_shader: None,
|
||||
random_textures,
|
||||
emissive,
|
||||
blinks,
|
||||
@@ -3008,14 +2996,8 @@ mod tests {
|
||||
}
|
||||
/// A resource root holding one animated block texture: a strip of two
|
||||
/// 16x16 frames, red then blue, whose mcmeta starts on the blue one.
|
||||
/// Each call gets its own folder: tests run side by side, and one adds
|
||||
/// a strip of its own that another would otherwise find.
|
||||
fn animated_root() -> PathBuf {
|
||||
static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
|
||||
let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("horizon-atlas-anim-{}-{n}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
let root = std::env::temp_dir().join(format!("horizon-atlas-anim-{}", std::process::id()));
|
||||
let block = root.join("assets/minecraft/textures/block");
|
||||
fs::create_dir_all(&block).unwrap();
|
||||
write_strip(
|
||||
|
||||
@@ -29,47 +29,36 @@ const GLUE: Glue = Glue {
|
||||
vertex_id: "hz_vertex_id()",
|
||||
};
|
||||
|
||||
/// The packs' text shader, when a pack replaces either stage.
|
||||
pub(super) fn load(root: &Path, defaults: &Path) -> Option<Arc<CoreProgram>> {
|
||||
load_core(root, defaults, "text", &["IS_GUI"], &GLUE)
|
||||
}
|
||||
const VERTEX: &str = "assets/minecraft/shaders/core/text.vsh";
|
||||
const FRAGMENT: &str = "assets/minecraft/shaders/core/text.fsh";
|
||||
|
||||
/// The packs' core shader `name` (`shaders/core/<name>.vsh` and `.fsh`) over `defaults` (the
|
||||
/// game's own resources, which have vanilla's and its includes), when a pack replaces either
|
||||
/// stage.
|
||||
pub(super) fn load_core(
|
||||
root: &Path,
|
||||
defaults: &Path,
|
||||
name: &str,
|
||||
defines: &[&str],
|
||||
glue: &Glue,
|
||||
) -> Option<Arc<CoreProgram>> {
|
||||
/// The packs' text shader over `defaults` (the game's own resources, which have vanilla's and
|
||||
/// its includes), when a pack replaces either stage.
|
||||
pub(super) fn load(root: &Path, defaults: &Path) -> Option<Arc<CoreProgram>> {
|
||||
if root == defaults {
|
||||
return None;
|
||||
}
|
||||
let read = |base: &Path, path: &str| std::fs::read_to_string(base.join(path)).ok();
|
||||
let own =
|
||||
|path: &str| read(root, path).filter(|text| read(defaults, path).as_ref() != Some(text));
|
||||
let vertex_path = format!("assets/minecraft/shaders/core/{name}.vsh");
|
||||
let fragment_path = format!("assets/minecraft/shaders/core/{name}.fsh");
|
||||
let (own_vertex, own_fragment) = (own(&vertex_path), own(&fragment_path));
|
||||
let (own_vertex, own_fragment) = (own(VERTEX), own(FRAGMENT));
|
||||
if own_vertex.is_none() && own_fragment.is_none() {
|
||||
return None;
|
||||
}
|
||||
let vertex = own_vertex.or_else(|| read(defaults, &vertex_path))?;
|
||||
let fragment = own_fragment.or_else(|| read(defaults, &fragment_path))?;
|
||||
let vertex = own_vertex.or_else(|| read(defaults, VERTEX))?;
|
||||
let fragment = own_fragment.or_else(|| read(defaults, FRAGMENT))?;
|
||||
let include = |id: &str| {
|
||||
let (namespace, path) = id.split_once(':')?;
|
||||
let path = format!("assets/{namespace}/{path}");
|
||||
read(root, &path).or_else(|| read(defaults, &path))
|
||||
};
|
||||
match horizon_shaders::translate_core(&vertex, &fragment, defines, &include, glue) {
|
||||
match horizon_shaders::translate_core(&vertex, &fragment, &["IS_GUI"], &include, &GLUE) {
|
||||
Ok(program) => {
|
||||
log::info!("the resource pack's {name} shader is used");
|
||||
log::info!("the resource pack's text shader draws the GUI's text");
|
||||
Some(Arc::new(program))
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("the resource pack's {name} shader is not used: {e}");
|
||||
log::warn!("the resource pack's text shader is not used: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,27 +38,10 @@ const QUEUED_PER_SAMPLER: usize = 32;
|
||||
const MAX_REFRESHED: usize = 64;
|
||||
/// Threads that sample far tiles beside the mesher. Sampling is the cold-load
|
||||
/// bottleneck (each task evaluates a shared density lattice for 256 columns),
|
||||
/// so medium machines use two thirds of their cores, not half.
|
||||
///
|
||||
/// The mesher has little to do until tiles are sampled, so its threads sit
|
||||
/// idle through the part of a cold build that waits on sampling. Measured on
|
||||
/// a 12-thread machine building 4336 tiles at LOD distance 256, from the game
|
||||
/// answering the control socket until every target tile is sampled, four runs
|
||||
/// a configuration:
|
||||
///
|
||||
/// | samplers / meshers | sampling | whole build |
|
||||
/// |---|---|---|
|
||||
/// | 6 / 5 (before) | 15.0 s | 23.9 s |
|
||||
/// | 8 / 3 | 11.8 s | 18.6 s |
|
||||
/// | 9 / 2 | 10.6 s | 19.1 s |
|
||||
///
|
||||
/// Nine samplers finish sampling sooner still, but leave the mesher two
|
||||
/// threads and the whole build ends later. The cap keeps a large machine
|
||||
/// from starving its mesher, and `meshing_threads` keeps the frame and the
|
||||
/// ticks a core either way.
|
||||
/// so medium machines use half their cores, not a quarter.
|
||||
fn sampling_threads(cores: usize) -> usize {
|
||||
if cores >= 8 {
|
||||
(cores * 2 / 3).min(8)
|
||||
(cores / 2).min(8)
|
||||
} else {
|
||||
(cores / 4).clamp(1, 3)
|
||||
}
|
||||
@@ -86,9 +69,6 @@ const WORLD_START: i32 = 1024 * 16;
|
||||
/// How many tile widths away from the camera a tile of the world-wide terrain
|
||||
/// splits (`Plan::visit`: cells then span about 1/48 of the distance).
|
||||
const WORLD_SPLIT: i32 = 3;
|
||||
/// A split tile stays split until its distance passes the threshold by this
|
||||
/// ratio (`Plan::visit`): 1 / 0.95.
|
||||
const RETURN: (i64, i64) = (20, 19);
|
||||
/// The farthest `Plan` reaches, in chunks.
|
||||
pub const MAX_CHUNKS: u32 = 1 << 22;
|
||||
/// The coarsest tile level whose chunks are generated in full (trees,
|
||||
@@ -196,11 +176,7 @@ pub struct Column {
|
||||
}
|
||||
impl Column {
|
||||
/// Reduce an entire horizontal footprint, rather than inflating its corner
|
||||
/// column. A height is drawn where any column of the footprint holds a
|
||||
/// block there, in the material most of them hold: anything joined to
|
||||
/// the ground stays joined at every width. Drawing only heights most of
|
||||
/// the footprint held dropped trunks (one column of 16 or 256) under the
|
||||
/// crowns they hold up, and the crowns hung in the air as slabs.
|
||||
/// column (a single leaf used to become a whole distant tree-sized slab).
|
||||
pub fn reduced(columns: &[Column; 256], x: usize, z: usize, width: usize) -> Self {
|
||||
if width == 1 {
|
||||
return columns[z * 16 + x].clone();
|
||||
@@ -224,7 +200,7 @@ impl Column {
|
||||
let mut bottom = MIN_Y as i16;
|
||||
for (y, material, change) in events {
|
||||
if bottom < y {
|
||||
if occupied > 0
|
||||
if occupied * 2 >= (width * width) as i32
|
||||
&& let Some(&((state, biome), _)) =
|
||||
votes.iter().max_by_key(|&(material, count)| {
|
||||
(*count, std::cmp::Reverse((material.0.0, material.1)))
|
||||
@@ -296,6 +272,39 @@ impl Column {
|
||||
self.spans.push(span);
|
||||
}
|
||||
}
|
||||
/// Open water and its floor lit as open sky, as the seed's far columns
|
||||
/// are: a chunk's own light fades a level per block of water, so the
|
||||
/// chunks a player passed drew a darker band through the seed's sea.
|
||||
/// The water is one span again.
|
||||
fn light_open_water(&mut self, kinds: &[u8]) {
|
||||
// Water; lava glows.
|
||||
self.light_open_water_where(|state| {
|
||||
let kind = kinds.get(state.0 as usize).copied().unwrap_or(0);
|
||||
kind & FLUID != 0 && kind >> 4 == 0
|
||||
});
|
||||
}
|
||||
/// [`Column::light_open_water`] for a caller that knows which states are
|
||||
/// water without `BlockFacts`: the save decoder, whose palette already
|
||||
/// names water apart from lava. Saved columns skipping this lit their
|
||||
/// water a level darker per block, which drew a band through the sea
|
||||
/// where they met the generator's columns.
|
||||
pub fn light_open_water_where(&mut self, is_water: impl Fn(BlockStateId) -> bool) {
|
||||
let water = |span: &Span| is_water(span.state);
|
||||
if !self.spans.last().is_some_and(water) {
|
||||
return;
|
||||
}
|
||||
let floor = self
|
||||
.spans
|
||||
.iter()
|
||||
.rposition(|span| !water(span))
|
||||
.unwrap_or(0);
|
||||
for (i, mut span) in std::mem::take(&mut self.spans).into_iter().enumerate() {
|
||||
if i >= floor {
|
||||
span.light |= 0xf0;
|
||||
}
|
||||
self.push(span);
|
||||
}
|
||||
}
|
||||
/// `kinds`: `state_kinds`, per block state; `water`: `water_source(kinds)`,
|
||||
/// what blocks holding water (kelp, seagrass, waterlogged ones) are drawn as.
|
||||
fn from_chunk(
|
||||
@@ -360,6 +369,7 @@ impl Column {
|
||||
});
|
||||
}
|
||||
}
|
||||
column.light_open_water(kinds);
|
||||
let mut water = None;
|
||||
for span in column.spans.iter().rev() {
|
||||
if kind(span.state) & FLUID != 0 {
|
||||
@@ -660,36 +670,20 @@ impl Plan {
|
||||
}
|
||||
// Keep distant cells small on screen; finest tiles retain individual
|
||||
// blocks. Reduced keeps them twice as large (a quarter of the cells).
|
||||
let held = previous.split.contains(&key);
|
||||
let split = if held { 10 } else { 8 };
|
||||
let split = if previous.split.contains(&key) { 10 } else { 8 };
|
||||
// Beyond the old 1024-chunk limit the cells are as large as `reduced`'s:
|
||||
// the whole world is 22 levels, not all of them worth full cells.
|
||||
let world = key.distance(camera) >= WORLD_START;
|
||||
let threshold = i64::from(key.size())
|
||||
* i64::from(if world {
|
||||
let threshold = key.size()
|
||||
* if key.distance(camera) >= WORLD_START {
|
||||
WORLD_SPLIT
|
||||
} else if reduced {
|
||||
split / 2
|
||||
} else {
|
||||
split
|
||||
});
|
||||
// The 8 to 10 band above is wide already. The ring of full detail
|
||||
// around the camera and the world-wide threshold have none: a tile
|
||||
// split last plan stays split until it is 5% past them, or it
|
||||
// flickered between two levels as the camera moved a block to and
|
||||
// fro across the line.
|
||||
let hold = |bound: i64| {
|
||||
if held {
|
||||
bound * RETURN.0 / RETURN.1
|
||||
} else {
|
||||
bound
|
||||
}
|
||||
};
|
||||
let distance = i64::from(key.distance(camera));
|
||||
};
|
||||
if key.level > 0
|
||||
&& (distance < hold(i64::from(detail) * 16 + 16)
|
||||
|| distance.max(i64::from(self.elevation))
|
||||
< if world { hold(threshold) } else { threshold })
|
||||
&& (key.distance(camera) < detail * 16 + 16
|
||||
|| key.distance(camera).max(self.elevation) < threshold)
|
||||
{
|
||||
self.split.insert(key);
|
||||
for child in key.children() {
|
||||
@@ -829,10 +823,6 @@ struct Snapshot {
|
||||
samples: FxHashMap<TileKey, Arc<Vec<Column>>>,
|
||||
saved: Saved,
|
||||
reduced: Reduced,
|
||||
/// The plan's tiles, to tell terrain not drawn yet from terrain out of
|
||||
/// range (`Snapshot::planned_leaf`).
|
||||
nodes: FxHashSet<TileKey>,
|
||||
split: FxHashSet<TileKey>,
|
||||
}
|
||||
/// One column of a mesh input, by reference into the snapshot. Mesh jobs used
|
||||
/// to clone all 256 columns of a tile (each with its own span allocation);
|
||||
@@ -863,8 +853,6 @@ struct EdgeSource {
|
||||
start: i32,
|
||||
end: i32,
|
||||
column: ColumnSource,
|
||||
/// Planned but not drawn yet (`mesh::EdgeColumn::unknown`).
|
||||
unknown: bool,
|
||||
}
|
||||
struct MeshInput {
|
||||
/// The tile's samples, with `overrides` replacing some of them.
|
||||
@@ -897,7 +885,6 @@ impl MeshInput {
|
||||
start: edge.start,
|
||||
end: edge.end,
|
||||
column: edge.column.get(),
|
||||
unknown: edge.unknown,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
@@ -1024,18 +1011,6 @@ struct Shared {
|
||||
approximate: AtomicBool,
|
||||
/// On small machines, hold queued far meshing while near terrain loads.
|
||||
mesh_held: AtomicBool,
|
||||
/// The near terrain is loading: the far threads step behind it
|
||||
/// (`horizon_threads::far_terrain_behind`) and come back once it is in.
|
||||
near_loading: AtomicBool,
|
||||
}
|
||||
/// Puts the calling far terrain thread behind the near terrain while it
|
||||
/// loads, and back once it is in. `behind`: where the thread is now.
|
||||
fn step_behind(shared: &Shared, behind: &mut bool) {
|
||||
let loading = shared.near_loading.load(Ordering::Relaxed);
|
||||
if loading != *behind {
|
||||
horizon_threads::far_terrain_behind(loading);
|
||||
*behind = loading;
|
||||
}
|
||||
}
|
||||
/// A background worker, sampling and meshing threads, and a bounded queue of
|
||||
/// sample jobs (`sample_jobs`). Only the current view's quadtree and a
|
||||
@@ -1164,7 +1139,6 @@ impl Lod {
|
||||
stop: AtomicBool::new(false),
|
||||
approximate: AtomicBool::new(false),
|
||||
mesh_held: AtomicBool::new(false),
|
||||
near_loading: AtomicBool::new(false),
|
||||
});
|
||||
let worker = shared.clone();
|
||||
let (tx, results) = mpsc::channel();
|
||||
@@ -1184,8 +1158,7 @@ impl Lod {
|
||||
thread::Builder::new()
|
||||
.name("Far meshing".into())
|
||||
.spawn(move || {
|
||||
horizon_threads::far_terrain_current();
|
||||
let mut behind = true;
|
||||
horizon_threads::behind_server();
|
||||
let mut mesher = mesh::Mesher::default();
|
||||
loop {
|
||||
let job = {
|
||||
@@ -1216,7 +1189,6 @@ impl Lod {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
step_behind(&shared, &mut behind);
|
||||
let keep_finer = !job.finer.is_empty();
|
||||
let input = job.snapshot.mesh_input(job.key, job.finer);
|
||||
let columns = input.columns();
|
||||
@@ -1272,8 +1244,7 @@ impl Lod {
|
||||
.name("Far terrain".into())
|
||||
.spawn(move || {
|
||||
// Ticks and frames come first (horizon_threads).
|
||||
horizon_threads::far_terrain_current();
|
||||
let mut behind = true;
|
||||
horizon_threads::behind_server();
|
||||
let mut sampler = source.sampler();
|
||||
let count = sampling_threads(cores);
|
||||
let mut sampling = Vec::new();
|
||||
@@ -1287,8 +1258,7 @@ impl Lod {
|
||||
thread::Builder::new()
|
||||
.name("Far sampling".into())
|
||||
.spawn(move || {
|
||||
horizon_threads::far_terrain_current();
|
||||
let mut behind = true;
|
||||
horizon_threads::behind_server();
|
||||
loop {
|
||||
let key = {
|
||||
let mut jobs = shared.jobs.lock().unwrap();
|
||||
@@ -1311,7 +1281,6 @@ impl Lod {
|
||||
jobs = shared.wake.wait(jobs).unwrap();
|
||||
}
|
||||
};
|
||||
step_behind(&shared, &mut behind);
|
||||
let [x, _, z] = key.origin();
|
||||
sample.set_approximate(
|
||||
shared.approximate.load(Ordering::Relaxed),
|
||||
@@ -1377,7 +1346,6 @@ impl Lod {
|
||||
}
|
||||
};
|
||||
let Some(job) = job else { continue };
|
||||
step_behind(&worker, &mut behind);
|
||||
let done = match job {
|
||||
Job::Observe(key) => {
|
||||
let chunk = worker.observed.lock().unwrap().remove(&key);
|
||||
@@ -2123,7 +2091,6 @@ impl Lod {
|
||||
self.held_sample_at = std::time::Instant::now();
|
||||
}
|
||||
self.near_loading = loading;
|
||||
self.shared.near_loading.store(loading, Ordering::Relaxed);
|
||||
let held = self.held();
|
||||
if self.shared.mesh_held.swap(held, Ordering::Release) && !held {
|
||||
self.shared.mesh_wake.notify_all();
|
||||
@@ -2256,7 +2223,7 @@ impl Lod {
|
||||
/// Rebuilds the quadtree and the selection it implies. Only runs when the
|
||||
/// planned area changes (a chunk boundary), never per finished sample.
|
||||
fn replan(&mut self, camera: [i32; 2], detail: i32, distance: u32, elevation: i32) {
|
||||
let plan = Plan::with_previous(
|
||||
self.plan = Plan::with_previous(
|
||||
camera,
|
||||
detail,
|
||||
distance,
|
||||
@@ -2264,17 +2231,6 @@ impl Lod {
|
||||
elevation,
|
||||
self.reduced_detail,
|
||||
);
|
||||
// A tile that leaves the range before it was drawn stops being
|
||||
// unknown to its neighbours (`Snapshot::planned_leaf`): they remesh
|
||||
// with walls against it, as against any terrain out of range.
|
||||
let gone: Vec<_> = self
|
||||
.plan
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|key| !plan.nodes.contains(key) && !self.ready.contains_key(key))
|
||||
.copied()
|
||||
.collect();
|
||||
self.plan = plan;
|
||||
self.ready.retain(|key, _| self.plan.nodes.contains(key));
|
||||
// Queued tiles the view left are not sampled (a running one is
|
||||
// dropped when it finishes, `finish_sample`).
|
||||
@@ -2298,9 +2254,6 @@ impl Lod {
|
||||
for key in standins {
|
||||
self.refresh_path(key);
|
||||
}
|
||||
for key in gone {
|
||||
self.mark_side_neighbors_of(key);
|
||||
}
|
||||
}
|
||||
/// Recomputes every active tile after a plan change, then marks only the
|
||||
/// tiles whose selection changed (plus their neighbours).
|
||||
@@ -2606,8 +2559,6 @@ impl Lod {
|
||||
.collect(),
|
||||
saved: self.saved.clone(),
|
||||
reduced: self.reduced.clone(),
|
||||
nodes: self.plan.nodes.clone(),
|
||||
split: self.plan.split.clone(),
|
||||
});
|
||||
let covered = Arc::new(self.covered.clone());
|
||||
let mut queued = self.shared.mesh_jobs.lock().unwrap();
|
||||
@@ -2755,8 +2706,6 @@ impl Lod {
|
||||
.collect(),
|
||||
saved: self.saved.clone(),
|
||||
reduced: self.reduced.clone(),
|
||||
nodes: self.plan.nodes.clone(),
|
||||
split: self.plan.split.clone(),
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
@@ -2828,23 +2777,6 @@ impl Snapshot {
|
||||
}
|
||||
None
|
||||
}
|
||||
/// Whether block column (wx, wz) lies in a planned tile that is not
|
||||
/// split, whatever drew it (nothing, here): the finest planned tile
|
||||
/// there. Past a split tile's out-of-range children, or past the plan,
|
||||
/// there is no terrain to wait for.
|
||||
fn planned_leaf(&self, wx: i32, wz: i32) -> bool {
|
||||
(0..=MAX_LEVEL)
|
||||
.map(|level| {
|
||||
let size = 16 << level;
|
||||
TileKey {
|
||||
x: wx.div_euclid(size),
|
||||
z: wz.div_euclid(size),
|
||||
level,
|
||||
}
|
||||
})
|
||||
.find(|key| self.nodes.contains(key))
|
||||
.is_some_and(|key| !self.split.contains(&key))
|
||||
}
|
||||
fn mesh_input(&self, key: TileKey, finer: Vec<TileKey>) -> MeshInput {
|
||||
let stride = key.size() / CELLS as i32;
|
||||
let [ox, _, oz] = key.origin();
|
||||
@@ -2868,12 +2800,9 @@ impl Snapshot {
|
||||
2 => (ox + offset, oz - 1),
|
||||
_ => (ox + offset, oz + key.size()),
|
||||
};
|
||||
// Nothing drawn across the edge: still to come when planned,
|
||||
// so no wall is drawn against it; empty past the plan.
|
||||
let (column, neighbor_stride, unknown) = match self.neighbor_source(wx, wz) {
|
||||
Some((column, stride)) => (column, stride, false),
|
||||
None => (ColumnSource::Empty, stride, self.planned_leaf(wx, wz)),
|
||||
};
|
||||
let (column, neighbor_stride) = self
|
||||
.neighbor_source(wx, wz)
|
||||
.unwrap_or((ColumnSource::Empty, stride));
|
||||
let coordinate = if direction < 2 { wz } else { wx };
|
||||
let width = (neighbor_stride - coordinate.rem_euclid(neighbor_stride))
|
||||
.min(key.size() - offset);
|
||||
@@ -2881,7 +2810,6 @@ impl Snapshot {
|
||||
start: offset,
|
||||
end: offset + width,
|
||||
column,
|
||||
unknown,
|
||||
});
|
||||
offset += width;
|
||||
}
|
||||
@@ -3220,27 +3148,6 @@ mod tests {
|
||||
assert!(after.nodes.len() < 16000);
|
||||
}
|
||||
#[test]
|
||||
fn a_split_tile_stays_split_a_little_past_the_detail_ring() {
|
||||
// Detail 40 puts the ring of full detail at 656 blocks, past where
|
||||
// level 2 tiles split by their width (8 * 64 = 512): the ring alone
|
||||
// splits this one, 640 blocks away.
|
||||
let tile = TileKey {
|
||||
x: 10,
|
||||
z: 0,
|
||||
level: 2,
|
||||
};
|
||||
let before = Plan::new([0, 0], 40, 256);
|
||||
assert!(before.split.contains(&tile));
|
||||
// 670 blocks: past the ring, inside 5% of it.
|
||||
let held = Plan::with_previous([-30, 0], 40, 256, &before, 0, false);
|
||||
assert!(held.split.contains(&tile));
|
||||
// Not held without having been split: it was never in the ring.
|
||||
assert!(!Plan::new([-30, 0], 40, 256).split.contains(&tile));
|
||||
// 700 blocks: past the margin.
|
||||
let gone = Plan::with_previous([-60, 0], 40, 256, &before, 0, false);
|
||||
assert!(!gone.split.contains(&tile));
|
||||
}
|
||||
#[test]
|
||||
fn reduced_material_votes_ignore_light_and_retain_exposure() {
|
||||
let mut columns: [Column; 256] = std::array::from_fn(|_| Column::default());
|
||||
for i in 0..16 {
|
||||
@@ -3278,6 +3185,49 @@ mod tests {
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn open_water_and_its_floor_are_lit_as_the_seed_lights_them() {
|
||||
let span = |min, max, state, light| Span {
|
||||
min,
|
||||
max,
|
||||
state: BlockStateId(state),
|
||||
biome: 0,
|
||||
light,
|
||||
};
|
||||
// 0 air, 1 stone, 2 water, 3 lava.
|
||||
let kinds = [AIR, KEPT, KEPT | FLUID, KEPT | FLUID | 15 << 4];
|
||||
// A chunk's own light: a cave, the floor, water fading a level a block.
|
||||
let mut sea = Column {
|
||||
surface: None,
|
||||
spans: vec![
|
||||
span(30, 40, 1, 0),
|
||||
span(40, 48, 1, 0x10 | 2 << 12),
|
||||
span(48, 61, 2, 0x20),
|
||||
span(61, 62, 2, 0xe0),
|
||||
span(62, 63, 2, 0xf0),
|
||||
]
|
||||
.into(),
|
||||
};
|
||||
sea.light_open_water(&kinds);
|
||||
assert_eq!(
|
||||
sea.spans,
|
||||
[
|
||||
span(30, 40, 1, 0),
|
||||
span(40, 48, 1, 0xf0 | 2 << 12),
|
||||
span(48, 63, 2, 0xf0)
|
||||
]
|
||||
);
|
||||
// Land, and lava, keep their light.
|
||||
for top in [1, 3] {
|
||||
let mut column = Column {
|
||||
surface: None,
|
||||
spans: vec![span(40, 50, 2, 0x20), span(50, 63, top, 0x80)].into(),
|
||||
};
|
||||
let before = column.spans.clone();
|
||||
column.light_open_water(&kinds);
|
||||
assert_eq!(column.spans, before);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn nearly_full_blocks_are_kept() {
|
||||
assert!(nearly_full("minecraft:powder_snow"));
|
||||
assert!(nearly_full("minecraft:dirt_path"));
|
||||
@@ -3312,7 +3262,7 @@ mod tests {
|
||||
assert_eq!(kelp.surface.unwrap().height, 40);
|
||||
}
|
||||
#[test]
|
||||
fn coarse_voxels_keep_what_any_column_holds_so_crowns_stay_on_trunks() {
|
||||
fn coarse_voxels_reduce_the_area_instead_of_inflating_one_leaf() {
|
||||
let span = |min, max, state| Span {
|
||||
min,
|
||||
max,
|
||||
@@ -3324,8 +3274,8 @@ mod tests {
|
||||
surface: None,
|
||||
spans: vec![span(0, 4, 1)].into(),
|
||||
});
|
||||
// One trunk column under a crown over the whole footprint.
|
||||
columns[0].spans.push(span(4, 12, 2));
|
||||
columns[0].spans.push(span(8, 10, 2));
|
||||
assert_eq!(Column::reduced(&columns, 0, 0, 4).spans, [span(0, 4, 1)]);
|
||||
for z in 0..4 {
|
||||
for x in 0..4 {
|
||||
columns[z * 16 + x].spans.push(span(12, 14, 3));
|
||||
@@ -3333,20 +3283,17 @@ mod tests {
|
||||
}
|
||||
assert_eq!(
|
||||
Column::reduced(&columns, 0, 0, 4).spans,
|
||||
[span(0, 4, 1), span(4, 12, 2), span(12, 14, 3)]
|
||||
[span(0, 4, 1), span(12, 14, 3)]
|
||||
);
|
||||
assert_eq!(Column::reduced(&columns, 0, 0, 1), columns[0]);
|
||||
let reduced = ReducedColumns::new(Arc::new(columns));
|
||||
assert_eq!(
|
||||
reduced.get(0, 0, 4).spans,
|
||||
[span(0, 4, 1), span(4, 12, 2), span(12, 14, 3)]
|
||||
reduced.get(0, 0, 1).spans,
|
||||
[span(0, 4, 1), span(8, 10, 2), span(12, 14, 3)]
|
||||
);
|
||||
assert_eq!(reduced.get(0, 0, 4).spans, [span(0, 4, 1), span(12, 14, 3)]);
|
||||
assert_eq!(reduced.get(4, 8, 4).spans, [span(0, 4, 1)]);
|
||||
assert_eq!(
|
||||
reduced.get(0, 0, 16).spans,
|
||||
[span(0, 4, 1), span(4, 12, 2), span(12, 14, 3)],
|
||||
"a crown over one corner of a wide cell still sits on its trunk"
|
||||
);
|
||||
assert_eq!(reduced.get(0, 0, 16).spans, [span(0, 4, 1)]);
|
||||
}
|
||||
/// Detail read before a refresh over its chunk is dropped (asked
|
||||
/// again); elsewhere, or read after, it is kept.
|
||||
@@ -4052,22 +3999,9 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn extra_sampling_threads_only_on_large_machines() {
|
||||
// Small machines are unchanged: a quarter of their cores, never more
|
||||
// than three, so the near chunks and the frame keep theirs.
|
||||
assert_eq!([1, 2, 4].map(sampling_threads), [1, 1, 1]);
|
||||
// From eight, two thirds rather than half, still capped at eight.
|
||||
assert_eq!([8, 11, 12].map(sampling_threads), [5, 7, 8]);
|
||||
assert_eq!([16, 64].map(sampling_threads), [8, 8]);
|
||||
assert_eq!([2, 8, 12, 16].map(meshing_threads), [1, 2, 3, 7]);
|
||||
// From four cores up the two pools together leave one for the frame
|
||||
// and the ticks. Below that both clamp to one and there is nothing
|
||||
// to spare, which is the existing behaviour.
|
||||
for cores in [4usize, 8, 11, 12, 16, 64] {
|
||||
assert!(
|
||||
sampling_threads(cores) + meshing_threads(cores) < cores,
|
||||
"{cores} cores leaves nothing for the frame"
|
||||
);
|
||||
}
|
||||
assert_eq!([1, 2, 4, 8, 11].map(sampling_threads), [1, 1, 1, 4, 5]);
|
||||
assert_eq!([12, 16, 64].map(sampling_threads), [6, 8, 8]);
|
||||
assert_eq!([2, 8, 12, 16].map(meshing_threads), [1, 3, 5, 7]);
|
||||
}
|
||||
|
||||
/// Review point 3 on #4747: a result from a batch that is no longer the
|
||||
@@ -4126,87 +4060,6 @@ mod tests {
|
||||
assert_eq!(meshes.len(), 2, "a tile that left the view is dropped");
|
||||
}
|
||||
|
||||
/// A neighbour inside the plan that is not drawn yet is unknown, not
|
||||
/// empty, and its arrival remeshes the tile that faced it. One past the
|
||||
/// planned range is still empty.
|
||||
#[test]
|
||||
fn a_planned_neighbor_that_is_not_ready_is_unknown_not_empty() {
|
||||
let mut lod = Lod::scheduler_for_test();
|
||||
let a = TileKey {
|
||||
x: 0,
|
||||
z: 0,
|
||||
level: 0,
|
||||
};
|
||||
let b = TileKey {
|
||||
x: 1,
|
||||
z: 0,
|
||||
level: 0,
|
||||
};
|
||||
lod.plan = Plan::default();
|
||||
lod.plan.roots = vec![a, b];
|
||||
lod.plan.nodes.extend([a, b]);
|
||||
lod.finish_sample(a, vec![Column::default(); CELLS * CELLS]);
|
||||
assert!(lod.active.contains(&a));
|
||||
let sides = |lod: &Lod| {
|
||||
let input = lod.snapshot().mesh_input(a, Vec::new());
|
||||
[0, 1].map(|side| {
|
||||
input.neighbors[side]
|
||||
.iter()
|
||||
.map(|edge| (edge.unknown, matches!(edge.column, ColumnSource::Tile(..))))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
};
|
||||
let [west, east] = sides(&lod);
|
||||
assert!(west.iter().all(|&edge| edge == (false, false)), "range end");
|
||||
assert!(east.iter().all(|&edge| edge == (true, false)), "planned");
|
||||
|
||||
// Its arrival draws the neighbour and remeshes the tile facing it.
|
||||
lod.dirty.clear();
|
||||
lod.finish_sample(b, vec![Column::default(); CELLS * CELLS]);
|
||||
assert!(lod.dirty.contains_key(&a), "the tile facing it is remeshed");
|
||||
let [_, east] = sides(&lod);
|
||||
assert!(east.iter().all(|&edge| edge == (false, true)), "drawn");
|
||||
|
||||
// Past a split tile's out-of-range child there is nothing to wait for.
|
||||
let parent = TileKey {
|
||||
x: 4,
|
||||
z: 4,
|
||||
level: 1,
|
||||
};
|
||||
lod.plan.nodes.insert(parent);
|
||||
lod.plan.split.insert(parent);
|
||||
lod.plan.nodes.extend(parent.children().into_iter().take(3));
|
||||
let snapshot = lod.snapshot();
|
||||
let [x, _, z] = parent.origin();
|
||||
assert!(snapshot.planned_leaf(x + 1, z + 1), "a planned leaf");
|
||||
assert!(!snapshot.planned_leaf(x + 17, z + 17), "no child planned");
|
||||
assert!(!snapshot.planned_leaf(x - 40, z), "outside the plan");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_planned_neighbor_leaving_the_range_unready_remeshes_the_tile_that_waited() {
|
||||
let mut lod = Lod::scheduler_for_test();
|
||||
let a = TileKey {
|
||||
x: 0,
|
||||
z: 0,
|
||||
level: 0,
|
||||
};
|
||||
let b = TileKey {
|
||||
x: 1,
|
||||
z: 0,
|
||||
level: 0,
|
||||
};
|
||||
lod.plan = Plan::default();
|
||||
lod.plan.roots = vec![a, b];
|
||||
lod.plan.nodes.extend([a, b]);
|
||||
lod.finish_sample(a, vec![Column::default(); CELLS * CELLS]);
|
||||
lod.dirty.clear();
|
||||
// A range of one chunk around (-10, 0) keeps `a` and drops `b`.
|
||||
lod.replan([-10, 0], 0, 1, 0);
|
||||
assert!(lod.plan.nodes.contains(&a) && !lod.plan.nodes.contains(&b));
|
||||
assert!(lod.dirty.contains_key(&a), "its east wall is drawn now");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn far_meshing_holds_while_small_machine_near_terrain_loads() {
|
||||
let mut lod = Lod::scheduler_for_test();
|
||||
|
||||
@@ -16,9 +16,6 @@ pub(super) struct EdgeColumn<'a> {
|
||||
pub start: i32,
|
||||
pub end: i32,
|
||||
pub column: &'a Column,
|
||||
/// The neighbour is planned but not drawn yet: its terrain is not known
|
||||
/// to be empty, so the sides facing it are left out, not drawn as walls.
|
||||
pub unknown: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -322,7 +319,6 @@ fn mesh_columns_with_neighbors(
|
||||
start: row as i32 * stride,
|
||||
end: (row as i32 + 1) * stride,
|
||||
column,
|
||||
unknown: false,
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
@@ -351,17 +347,13 @@ fn heightmap(column: &Column, mut fluid: impl FnMut(&Span) -> bool) -> Column {
|
||||
..*span
|
||||
};
|
||||
let mut spans = Spans::with_capacity(2);
|
||||
if !fluid(top) {
|
||||
spans.push(ground(top));
|
||||
} else if let Some(under) = below.iter().rev().find(|&span| !fluid(span)) {
|
||||
// Shaded water is a span per light level; the bed is under all of it.
|
||||
spans.push(ground(under));
|
||||
spans.push(Span {
|
||||
min: under.max,
|
||||
..*top
|
||||
});
|
||||
} else {
|
||||
spans.push(*top);
|
||||
match below.last() {
|
||||
Some(under) if fluid(top) => {
|
||||
spans.push(ground(under));
|
||||
spans.push(*top);
|
||||
}
|
||||
_ if fluid(top) => spans.push(*top),
|
||||
_ => spans.push(ground(top)),
|
||||
}
|
||||
Column {
|
||||
surface: column.surface,
|
||||
@@ -404,7 +396,6 @@ fn mesh_columns_with_edges(
|
||||
start: edge.start,
|
||||
end: edge.end,
|
||||
column,
|
||||
unknown: edge.unknown,
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
@@ -669,9 +660,6 @@ fn mesh_columns_with_edges(
|
||||
// A coarse face can meet several finer columns.
|
||||
// Clip each interval independently to avoid missing walls.
|
||||
for (i, edge) in neighbors[edge_side].iter().enumerate() {
|
||||
if edge.unknown {
|
||||
continue;
|
||||
}
|
||||
let start = u0.max(edge.start as f32);
|
||||
let end = u1.min(edge.end as f32);
|
||||
if start < end {
|
||||
@@ -797,70 +785,6 @@ mod refinement_tests {
|
||||
}
|
||||
assert_eq!(area, (parent.size() * parent.size() - 256) as f32);
|
||||
}
|
||||
/// A neighbour that is still to come draws no wall against it; an empty
|
||||
/// one (out of range) does.
|
||||
#[test]
|
||||
fn unknown_neighbor_leaves_its_side_out() {
|
||||
let key = TileKey {
|
||||
x: 0,
|
||||
z: 0,
|
||||
level: 0,
|
||||
};
|
||||
let columns = vec![
|
||||
Column {
|
||||
surface: None,
|
||||
spans: vec![Span {
|
||||
min: 0,
|
||||
max: 8,
|
||||
state: BlockStateId(1),
|
||||
biome: 0,
|
||||
light: 240,
|
||||
}]
|
||||
.into()
|
||||
};
|
||||
256
|
||||
];
|
||||
let columns: Vec<&Column> = columns.iter().collect();
|
||||
let empty = Column::default();
|
||||
let east_quads = |unknown: bool| {
|
||||
let edge = |column| EdgeColumn {
|
||||
start: 0,
|
||||
end: 16,
|
||||
column,
|
||||
unknown,
|
||||
};
|
||||
let neighbors = [Vec::new(), vec![edge(&empty)], Vec::new(), Vec::new()];
|
||||
let mesh = mesh_columns_with_edges(
|
||||
key,
|
||||
&columns,
|
||||
&FxHashSet::default(),
|
||||
None,
|
||||
&neighbors,
|
||||
&[],
|
||||
|_, _, _| Material {
|
||||
faces: [
|
||||
Some(Paint {
|
||||
layer: 1,
|
||||
light: 240,
|
||||
color: u32::MAX,
|
||||
translucent: false,
|
||||
}),
|
||||
None,
|
||||
],
|
||||
fluid: 0,
|
||||
opaque: true,
|
||||
},
|
||||
);
|
||||
mesh.solid
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|quad| quad.iter().all(|v| v.pos[0] == 16.0))
|
||||
.count()
|
||||
};
|
||||
assert!(east_quads(false) > 0, "a wall faces the empty neighbour");
|
||||
assert_eq!(east_quads(true), 0, "none faces the unknown one");
|
||||
}
|
||||
}
|
||||
|
||||
/// The tile's columns with the gaps no open air reaches filled with the
|
||||
@@ -1518,7 +1442,6 @@ mod tests {
|
||||
start: 0,
|
||||
end: CELLS as i32,
|
||||
column: &hill_column,
|
||||
unknown: false,
|
||||
};
|
||||
let neighbors = [vec![hill()], vec![hill()], vec![hill()], vec![hill()]];
|
||||
let fill = |columns: &[Column]| -> Vec<Vec<Span>> {
|
||||
@@ -1660,7 +1583,6 @@ mod tests {
|
||||
start: 0,
|
||||
end: 32,
|
||||
column: &columns[0],
|
||||
unknown: false,
|
||||
}]
|
||||
});
|
||||
// One fine column is lower than both the coarse cell and its sibling.
|
||||
@@ -1674,19 +1596,16 @@ mod tests {
|
||||
start: 0,
|
||||
end: 1,
|
||||
column: &columns[0],
|
||||
unknown: false,
|
||||
},
|
||||
EdgeColumn {
|
||||
start: 1,
|
||||
end: 2,
|
||||
column: &low,
|
||||
unknown: false,
|
||||
},
|
||||
EdgeColumn {
|
||||
start: 2,
|
||||
end: 32,
|
||||
column: &columns[0],
|
||||
unknown: false,
|
||||
},
|
||||
];
|
||||
let columns: Vec<&Column> = columns.iter().collect();
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
mod fluid;
|
||||
mod leaf_culling;
|
||||
mod lighting;
|
||||
pub mod packed;
|
||||
mod sides;
|
||||
use crate::connected::{Pick, TileLayer};
|
||||
use crate::{
|
||||
@@ -266,12 +265,6 @@ pub struct SectionMesh {
|
||||
/// model order before.
|
||||
pub sides: Sides,
|
||||
pub translucent: Vec<TerrainVertex>,
|
||||
/// `solid` in the compact layout (`packed`), when the renderer reads it and it is
|
||||
/// smaller; empty otherwise. Made by `sort_solid`.
|
||||
pub packed: Vec<[u32; 6]>,
|
||||
/// The tint of each solid quad as the mesher multiplied it (`packed::UNKNOWN_TINT` where
|
||||
/// it does not say), for `packed`; dropped once that is made.
|
||||
pub(crate) solid_tints: Vec<u32>,
|
||||
/// VisGraph: bit `a * 6 + b` is set when faces `a` and `b` (Direction
|
||||
/// order) are connected through non-opaque blocks.
|
||||
pub visibility: u64,
|
||||
@@ -297,8 +290,6 @@ impl Default for SectionMesh {
|
||||
solid: Vec::new(),
|
||||
sides: Sides::default(),
|
||||
translucent: Vec::new(),
|
||||
packed: Vec::new(),
|
||||
solid_tints: Vec::new(),
|
||||
visibility: ALL_VISIBLE,
|
||||
block_entities: Vec::new(),
|
||||
lights: Vec::new(),
|
||||
@@ -331,8 +322,6 @@ impl SectionMesh {
|
||||
pub fn clear(&mut self) {
|
||||
self.solid.clear();
|
||||
self.translucent.clear();
|
||||
self.packed.clear();
|
||||
self.solid_tints.clear();
|
||||
self.block_entities.clear();
|
||||
self.lights.clear();
|
||||
self.sprite_layers.clear();
|
||||
@@ -344,27 +333,15 @@ impl SectionMesh {
|
||||
pub(crate) fn capacity_bytes(&self) -> usize {
|
||||
self.solid.capacity() * std::mem::size_of::<TerrainVertex>()
|
||||
+ self.translucent.capacity() * std::mem::size_of::<TerrainVertex>()
|
||||
+ self.packed.capacity() * std::mem::size_of::<[u32; 6]>()
|
||||
+ self.solid_tints.capacity() * std::mem::size_of::<u32>()
|
||||
+ self.block_entities.capacity() * std::mem::size_of::<([i32; 3], u32)>()
|
||||
+ self.lights.capacity() * std::mem::size_of::<([i32; 3], u32)>()
|
||||
+ self.sprite_layers.capacity() * std::mem::size_of::<u16>()
|
||||
}
|
||||
/// Sorts the solid quads by the side they face, for the renderer to
|
||||
/// leave out those facing away from the eye (`sides`).
|
||||
/// Then lays them out compactly (`packed`) while the renderer reads that.
|
||||
pub fn sort_solid(&mut self) {
|
||||
let quads = self.solid.len() / 4;
|
||||
if !self.solid_tints.is_empty() {
|
||||
self.solid_tints.resize(quads, packed::UNKNOWN_TINT);
|
||||
}
|
||||
self.sides = sides::sort_tinted(&mut self.solid, &mut self.solid_tints);
|
||||
self.sides = sides::sort(&mut self.solid);
|
||||
self.bounds = self.compute_bounds();
|
||||
self.packed.clear();
|
||||
if packed::packing() && quads > 0 && self.solid_tints.len() == quads {
|
||||
self.packed = packed::layout(&self.solid, &self.solid_tints);
|
||||
}
|
||||
self.solid_tints.clear();
|
||||
}
|
||||
|
||||
/// Computes the tight axis-aligned bounding box [min, max] of actual terrain geometry vertices.
|
||||
@@ -1542,24 +1519,12 @@ impl Mesher<'_> {
|
||||
let other = &info[neighbor as usize];
|
||||
// LeavesBlock.skipRendering with opaque leaves.
|
||||
let opaque_leaves = !self.cutout_leaves && state.leaves && other.leaves;
|
||||
let culled_leaf = match self.leaf_culling {
|
||||
LeafCullingMode::Hollow | LeafCullingMode::Hidden => {
|
||||
state.leaves
|
||||
&& other.leaves
|
||||
&& leaf_culling::in_section(x + dx, y + dy, z + dz)
|
||||
&& (self.leaf_culling == LeafCullingMode::Hollow
|
||||
|| leaf_culling::backed(
|
||||
pad,
|
||||
info,
|
||||
region,
|
||||
base_y,
|
||||
[x + 2 * dx, y + 2 * dy, z + 2 * dz],
|
||||
))
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let hollow_leaf = self.leaf_culling == LeafCullingMode::Hollow
|
||||
&& state.leaves
|
||||
&& other.leaves
|
||||
&& leaf_culling::in_section(x + dx, y + dy, z + dz);
|
||||
visible[d as usize] = !opaque_leaves
|
||||
&& !culled_leaf
|
||||
&& !hollow_leaf
|
||||
&& self
|
||||
.visibility
|
||||
.should_render_face(
|
||||
@@ -1805,8 +1770,8 @@ impl Mesher<'_> {
|
||||
TileLayer::Skip | TileLayer::Default => own,
|
||||
};
|
||||
match pick {
|
||||
Pick::Keep => self.quad(out, solid_leaves, own, corners.map(|c| c.0), tint),
|
||||
Pick::Tile(t) => self.quad(out, solid_leaves, tile(t), corners.map(|c| c.0), tint),
|
||||
Pick::Keep => self.quad(out, solid_leaves, own, corners.map(|c| c.0)),
|
||||
Pick::Tile(t) => self.quad(out, solid_leaves, tile(t), corners.map(|c| c.0)),
|
||||
Pick::Quarters(tiles) => {
|
||||
let cuts = cuts(&corners);
|
||||
for s in cuts.windows(2) {
|
||||
@@ -1814,8 +1779,7 @@ impl Mesher<'_> {
|
||||
let piece = [(s[0], t[0]), (s[1], t[0]), (s[1], t[1]), (s[0], t[1])]
|
||||
.map(|(s, t)| bilinear(&corners, s, t));
|
||||
let quarter = quarter_of(&piece);
|
||||
let piece = piece.map(|c| c.0);
|
||||
self.quad(out, solid_leaves, tile(tiles[quarter]), piece, tint);
|
||||
self.quad(out, solid_leaves, tile(tiles[quarter]), piece.map(|c| c.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1859,7 +1823,7 @@ impl Mesher<'_> {
|
||||
for t in cuts.windows(2) {
|
||||
let piece = [(s[0], t[0]), (s[1], t[0]), (s[1], t[1]), (s[0], t[1])]
|
||||
.map(|(s, t)| bilinear(&whole, s, t).0);
|
||||
self.quad_as(out, false, tile, piece, false, tint);
|
||||
self.quad_as(out, false, tile, piece, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1872,9 +1836,8 @@ impl Mesher<'_> {
|
||||
solid_leaves: bool,
|
||||
tile: TileLayer,
|
||||
v: [TerrainVertex; 4],
|
||||
tint: Option<u32>,
|
||||
) {
|
||||
self.quad_as(out, solid_leaves, tile, v, true, tint);
|
||||
self.quad_as(out, solid_leaves, tile, v, true);
|
||||
}
|
||||
/// Whether [`Mesher::quad`] draws `tile` in the translucent pass.
|
||||
fn translucent(&self, tile: TileLayer) -> bool {
|
||||
@@ -1893,8 +1856,7 @@ impl Mesher<'_> {
|
||||
}
|
||||
}
|
||||
/// [`Mesher::quad`], with the sprite's own translucency (`translucent_layers`) counted only
|
||||
/// when `by_sprite`: an overlay's pass is its rule's. `tint`: what its grey levels were
|
||||
/// multiplied by, for the compact layout (`packed`).
|
||||
/// when `by_sprite`: an overlay's pass is its rule's.
|
||||
fn quad_as(
|
||||
&self,
|
||||
out: &mut SectionMesh,
|
||||
@@ -1902,7 +1864,6 @@ impl Mesher<'_> {
|
||||
tile: TileLayer,
|
||||
mut v: [TerrainVertex; 4],
|
||||
by_sprite: bool,
|
||||
tint: Option<u32>,
|
||||
) {
|
||||
let TileLayer::Layer {
|
||||
layer,
|
||||
@@ -1936,12 +1897,6 @@ impl Mesher<'_> {
|
||||
vertex.layer = layer;
|
||||
}
|
||||
target.extend(v);
|
||||
if !translucent {
|
||||
// Quads other code put in `solid` (lava) have no tint known.
|
||||
let quad = out.solid.len() / 4 - 1;
|
||||
out.solid_tints.resize(quad, packed::UNKNOWN_TINT);
|
||||
out.solid_tints.push(tint.unwrap_or(packed::WHITE));
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The cuts of a `ctm_compact` face, in quad parameters, without allocating: the first `.1`
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
//! Aggressive` runs a multi-source breadth-first search from leaves touching
|
||||
//! openings, solidifying leaves with a single open face to thin the canopy shell
|
||||
//! by one layer. `Hollow` directly drops faces shared between two leaves within
|
||||
//! the section, preserving boundaries and cavity faces. `Hidden` drops only
|
||||
//! those whose faced leaf has another leaf or an opaque cube behind it
|
||||
//! ([`backed`]), so the layer seen through the canopy's outer holes stays.
|
||||
//! the section, preserving boundaries and cavity faces.
|
||||
|
||||
use super::{B, Info, P, Padded, pi};
|
||||
use crate::{blocks::RenderShape, chunk::Chunk, graphics::LeafCullingMode};
|
||||
@@ -231,21 +229,6 @@ pub(super) fn in_section(x: i32, y: i32, z: i32) -> bool {
|
||||
(0..16).contains(&x) && (0..16).contains(&y) && (0..16).contains(&z)
|
||||
}
|
||||
|
||||
/// `Hidden`: whether the padded cell `[x, y, z]`, the one behind a faced leaf,
|
||||
/// is a leaf or a full opaque cube. A cell without real data never is.
|
||||
#[inline]
|
||||
pub(super) fn backed(
|
||||
pad: &Padded,
|
||||
info: &[Info],
|
||||
region: &[[Option<&Chunk>; 3]; 3],
|
||||
base_y: i32,
|
||||
[x, y, z]: [i32; 3],
|
||||
) -> bool {
|
||||
info.get(pad.blocks[pi(x, y, z)] as usize)
|
||||
.is_some_and(|state| state.leaves || state.opaque_cube)
|
||||
&& known(region, base_y, x, y, z)
|
||||
}
|
||||
|
||||
/// Whether the padded cell `(x, y, z)` has real block data. A missing chunk
|
||||
/// does not count: `Padded::fill` copies the centre's edge column into it, which
|
||||
/// would make a canopy look enclosed. A Y outside the column counts as unknown
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
//! The compact layout of a section's solid quads in the arena: most of them are whole faces
|
||||
//! of a block's cube on the block grid, and those take one 24-byte record each instead of
|
||||
//! four 24-byte vertices. terrain.wgsl's `section_corner` expands a record into the very
|
||||
//! vertex words `TerrainVertex` would hold (`packed_vertex` here is the same on the CPU, and
|
||||
//! a record is kept only when it gives back the quad's four vertices bit for bit), so every
|
||||
//! pass draws the same picture from either layout.
|
||||
//!
|
||||
//! A mesh's solid range is then one record per quad in mesh order (so `Sides` and the draws
|
||||
//! count quads as before), padding to a multiple of 4 slots, and the quads no record fits as
|
||||
//! their four vertices: their record holds the distance to those (`FULL`). A record:
|
||||
//!
|
||||
//! - word 0: the tint (0xRRGGBB as the mesher multiplies it) and the colour's alpha byte;
|
||||
//! - word 1: each corner's grey level before the tint;
|
||||
//! - word 2: the UV rectangle in 1/32 of the sprite (u0, u1, v0, v1, 6 bits each) and, per
|
||||
//! corner, which edge of it its u and v take;
|
||||
//! - word 3: the layer, and corner 0's light (sky and block light over 4, 6 bits each);
|
||||
//! - word 4: corners 1 and 2's light;
|
||||
//! - word 5: corner 3's light, the face (block x, y, z, the axis it faces along, which side
|
||||
//! of the block, and where and which way its corners go round), and `FULL` clear.
|
||||
use super::TerrainVertex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
/// A solid quad's tint the mesher does not know (fluids): only untinted colours pack.
|
||||
pub const UNKNOWN_TINT: u32 = u32::MAX;
|
||||
/// The tint of an untinted quad, whose colour is its grey level.
|
||||
pub const WHITE: u32 = 0xFF_FFFF;
|
||||
/// Word 5's bit of a quad kept as four vertices; word 0 is then the slots from the record
|
||||
/// to its first vertex.
|
||||
pub const FULL: u32 = 1 << 31;
|
||||
/// The draws' base vertex of a compact mesh: `PACKED_BASE + 4 * slot + corner` is a corner of
|
||||
/// the quad whose record is at `slot`; below it, vertex indices name vertices.
|
||||
pub const PACKED_BASE: u32 = 1 << 28;
|
||||
|
||||
/// Whether meshes sorted from now on get the compact layout: set by the renderer while its
|
||||
/// passes read it (no shader pack).
|
||||
static PACK: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub fn pack_quads(on: bool) {
|
||||
PACK.store(on, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn packing() -> bool {
|
||||
PACK.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// The order corners go round a face: (du, dw) of each step.
|
||||
const ROUND: [[u32; 2]; 4] = [[0, 0], [1, 0], [1, 1], [0, 1]];
|
||||
|
||||
/// `quad`'s record, if one gives back its four vertices exactly. `tint`: what the mesher
|
||||
/// multiplied its grey levels by (`UNKNOWN_TINT` when it does not say).
|
||||
pub fn pack_quad(quad: &[TerrainVertex; 4], tint: u32) -> Option<[u32; 6]> {
|
||||
// Corners on the block grid of the section.
|
||||
let mut p = [[0u32; 3]; 4];
|
||||
for (to, v) in p.iter_mut().zip(quad) {
|
||||
for (c, &x) in to.iter_mut().zip(&v.pos) {
|
||||
if !(0.0..=16.0).contains(&x) || x.fract() != 0.0 {
|
||||
return None;
|
||||
}
|
||||
*c = x as u32;
|
||||
}
|
||||
}
|
||||
let axis = (0..3).find(|&a| p.iter().all(|c| c[a] == p[0][a]))?;
|
||||
let (u, w) = ((axis + 1) % 3, (axis + 2) % 3);
|
||||
let lo = [u, w].map(|k| p.iter().map(|c| c[k]).min().unwrap_or(0));
|
||||
if lo.iter().any(|&l| l > 15) {
|
||||
return None;
|
||||
}
|
||||
let mut steps = [0u32; 4];
|
||||
for (step, c) in steps.iter_mut().zip(&p) {
|
||||
let d = [c[u] - lo[0], c[w] - lo[1]];
|
||||
*step = ROUND.iter().position(|r| *r == d)? as u32;
|
||||
}
|
||||
let (start, dir) = (0..8u32).map(|n| (n & 3, n >> 2)).find(|&(start, dir)| {
|
||||
(0..4u32).all(|i| {
|
||||
let step = if dir == 1 { 4 - i } else { i };
|
||||
steps[i as usize] == (start + step) & 3
|
||||
})
|
||||
})?;
|
||||
let plane = p[0][axis];
|
||||
let mut block = [0u32; 3];
|
||||
block[axis] = plane.min(15);
|
||||
block[u] = lo[0];
|
||||
block[w] = lo[1];
|
||||
let side = plane - block[axis];
|
||||
let orient = block[0]
|
||||
| block[1] << 4
|
||||
| block[2] << 8
|
||||
| (axis as u32) << 12
|
||||
| side << 14
|
||||
| start << 15
|
||||
| dir << 17;
|
||||
// UVs in 1/32 of the sprite, at most two values each way.
|
||||
let mut ranges = [[u32::MAX, 0u32]; 2];
|
||||
let mut uvs = [[0u32; 2]; 4];
|
||||
for (uv, v) in uvs.iter_mut().zip(quad) {
|
||||
for k in 0..2 {
|
||||
let raw = u32::from(v.uv[k]);
|
||||
if raw & 1023 != 0 {
|
||||
return None;
|
||||
}
|
||||
uv[k] = raw >> 10;
|
||||
ranges[k] = [ranges[k][0].min(uv[k]), ranges[k][1].max(uv[k])];
|
||||
}
|
||||
}
|
||||
let mut select = 0u32;
|
||||
for (i, uv) in uvs.iter().enumerate() {
|
||||
for k in 0..2 {
|
||||
if uv[k] == ranges[k][1] && uv[k] != ranges[k][0] {
|
||||
select |= 1 << (2 * i + k);
|
||||
} else if uv[k] != ranges[k][0] {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let rect = ranges[0][0] | ranges[0][1] << 6 | ranges[1][0] << 12 | ranges[1][1] << 18;
|
||||
// Light in steps of 4 (smooth light averages four levels of 16).
|
||||
let mut light = [0u32; 4];
|
||||
for (l, v) in light.iter_mut().zip(quad) {
|
||||
let (sky, block) = (u32::from(v.light & 0xFF), u32::from(v.light >> 8));
|
||||
if sky & 3 != 0 || block & 3 != 0 {
|
||||
return None;
|
||||
}
|
||||
*l = sky >> 2 | (block >> 2) << 6;
|
||||
}
|
||||
// The colour: each channel the grey level times the tint's, over 255.
|
||||
let tint = if tint == UNKNOWN_TINT {
|
||||
WHITE
|
||||
} else {
|
||||
tint & WHITE
|
||||
};
|
||||
let alpha = quad[0].color & 0xFF00_0000;
|
||||
let mut grays = 0u32;
|
||||
for (i, v) in quad.iter().enumerate() {
|
||||
grays |= gray_of(v.color, tint)? << (8 * i);
|
||||
}
|
||||
let words = [
|
||||
tint | alpha,
|
||||
grays,
|
||||
rect | select << 24,
|
||||
u32::from(quad[0].layer) | light[0] << 16,
|
||||
light[1] | light[2] << 12,
|
||||
light[3] | orient << 12,
|
||||
];
|
||||
let exact = (0..4).all(|c| {
|
||||
let (a, b) = (packed_vertex(&words, c), quad[c]);
|
||||
bytemuck::bytes_of(&a) == bytemuck::bytes_of(&b)
|
||||
});
|
||||
exact.then_some(words)
|
||||
}
|
||||
|
||||
/// The tint's channels, as the mesher takes them for red, green and blue.
|
||||
fn channels(tint: u32) -> [u32; 3] {
|
||||
[tint >> 16 & 255, tint >> 8 & 255, tint & 255]
|
||||
}
|
||||
|
||||
/// A grey level that `tint` turns into `color`'s red, green and blue.
|
||||
fn gray_of(color: u32, tint: u32) -> Option<u32> {
|
||||
let want = [color & 255, color >> 8 & 255, color >> 16 & 255];
|
||||
let t = channels(tint);
|
||||
let gives = |g: u32| (0..3).all(|k| g * t[k] / 255 == want[k]);
|
||||
let k = (0..3).max_by_key(|&k| t[k]).unwrap_or(0);
|
||||
if t[k] == 0 {
|
||||
return gives(0).then_some(0);
|
||||
}
|
||||
let lo = (want[k] * 255).div_ceil(t[k]);
|
||||
let hi = ((want[k] * 255 + 254) / t[k]).min(255);
|
||||
(lo..=hi).find(|&g| gives(g))
|
||||
}
|
||||
|
||||
/// Corner `corner` of the quad `words` holds, as terrain.wgsl's `packed_corner` expands it.
|
||||
pub fn packed_vertex(words: &[u32; 6], corner: usize) -> TerrainVertex {
|
||||
let [w0, w1, w2, w3, w4, w5] = *words;
|
||||
let orient = w5 >> 12 & 0x3_FFFF;
|
||||
let block = [orient & 15, orient >> 4 & 15, orient >> 8 & 15];
|
||||
let axis = (orient >> 12 & 3) as usize;
|
||||
let side = orient >> 14 & 1;
|
||||
let (start, dir) = (orient >> 15 & 3, orient >> 17 & 1);
|
||||
let i = corner as u32;
|
||||
let step = (start + if dir == 1 { 4 - i } else { i }) & 3;
|
||||
let [du, dw] = ROUND[step as usize];
|
||||
let mut pos = block;
|
||||
pos[axis] += side;
|
||||
pos[(axis + 1) % 3] += du;
|
||||
pos[(axis + 2) % 3] += dw;
|
||||
let select = w2 >> 24;
|
||||
let us = select >> (2 * i) & 1;
|
||||
let vs = select >> (2 * i + 1) & 1;
|
||||
let uv = [w2 >> (6 * us) & 63, w2 >> (12 + 6 * vs) & 63].map(|k| (k << 10) as u16);
|
||||
let light = [w3 >> 16, w4, w4 >> 12, w5][corner] & 0xFFF;
|
||||
let light = ((light & 63) << 2 | (light >> 6 & 63) << 10) as u16;
|
||||
let g = w1 >> (8 * i) & 255;
|
||||
let [r, gr, b] = channels(w0).map(|t| g * t / 255);
|
||||
TerrainVertex {
|
||||
pos: pos.map(|c| c as f32),
|
||||
uv,
|
||||
layer: (w3 & 0xFFFF) as u16,
|
||||
light,
|
||||
color: r | gr << 8 | b << 16 | (w0 & 0xFF00_0000),
|
||||
}
|
||||
}
|
||||
|
||||
/// The compact layout of sorted solid quads with their tints (`SectionMesh::packed`), or
|
||||
/// nothing when it would be no smaller than their vertices.
|
||||
pub fn layout(solid: &[TerrainVertex], tints: &[u32]) -> Vec<[u32; 6]> {
|
||||
let quads = solid.as_chunks::<4>().0;
|
||||
let records: Vec<Option<[u32; 6]>> = quads
|
||||
.iter()
|
||||
.zip(tints)
|
||||
.map(|(q, &t)| pack_quad(q, t))
|
||||
.collect();
|
||||
let n = quads.len();
|
||||
let packed = records.iter().filter(|r| r.is_some()).count();
|
||||
let full = n - packed;
|
||||
let pad = if full > 0 {
|
||||
n.next_multiple_of(4) - n
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if n + pad + 4 * full >= 4 * n {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut out = Vec::with_capacity(n + pad + 4 * full);
|
||||
let mut next = n + pad;
|
||||
for (i, record) in records.iter().enumerate() {
|
||||
out.push(record.unwrap_or_else(|| {
|
||||
let at = next;
|
||||
next += 4;
|
||||
[(at - i) as u32, 0, 0, 0, 0, FULL]
|
||||
}));
|
||||
}
|
||||
out.resize(n + pad, [0; 6]);
|
||||
for (q, record) in quads.iter().zip(&records) {
|
||||
if record.is_none() {
|
||||
out.extend(
|
||||
q.iter()
|
||||
.map(|v| bytemuck::cast::<TerrainVertex, [u32; 6]>(*v)),
|
||||
);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Quads `layout` gave a record of their own.
|
||||
pub fn packed_quads(layout: &[[u32; 6]], quads: usize) -> usize {
|
||||
layout
|
||||
.iter()
|
||||
.take(quads)
|
||||
.filter(|w| w[5] & FULL == 0)
|
||||
.count()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const TINT: u32 = 0x7F_B238;
|
||||
|
||||
/// An opaque colour the mesher makes of grey level `g` times `tint`.
|
||||
fn tinted(g: u32, tint: u32) -> u32 {
|
||||
let [r, gr, b] = channels(tint).map(|t| g * t / 255);
|
||||
r | gr << 8 | b << 16 | 0xFF00_0000
|
||||
}
|
||||
|
||||
/// The top face of block (3, 15, 5), corners in `order`: tinted, smooth lit, with
|
||||
/// ambient occlusion darkening its corners differently.
|
||||
fn top(order: [usize; 4]) -> [TerrainVertex; 4] {
|
||||
let pos = [
|
||||
[3.0, 16.0, 5.0],
|
||||
[3.0, 16.0, 6.0],
|
||||
[4.0, 16.0, 6.0],
|
||||
[4.0, 16.0, 5.0],
|
||||
];
|
||||
let uv = [[0, 0], [16 << 10, 0], [16 << 10, 16 << 10], [0, 16 << 10]];
|
||||
let grays = [200, 180, 160, 200];
|
||||
order.map(|i| TerrainVertex {
|
||||
pos: pos[i],
|
||||
uv: uv[i],
|
||||
layer: 7,
|
||||
light: 0xF0 | 0x40 << 8,
|
||||
color: tinted(grays[i], TINT),
|
||||
})
|
||||
}
|
||||
|
||||
fn same(a: &TerrainVertex, b: &TerrainVertex) -> bool {
|
||||
bytemuck::bytes_of(a) == bytemuck::bytes_of(b)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_whole_cube_face_takes_one_record_either_way_round() {
|
||||
for order in [[0, 1, 2, 3], [2, 3, 0, 1], [0, 3, 2, 1]] {
|
||||
let quad = top(order);
|
||||
let words = pack_quad(&quad, TINT).expect("a whole face on the block grid packs");
|
||||
for (corner, v) in quad.iter().enumerate() {
|
||||
assert!(
|
||||
same(&packed_vertex(&words, corner), v),
|
||||
"{order:?} corner {corner}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quads_off_the_grid_or_the_steps_keep_their_vertices() {
|
||||
// Half a block up: a slab's top.
|
||||
let mut slab = top([0, 1, 2, 3]);
|
||||
for v in &mut slab {
|
||||
v.pos[1] = 8.5;
|
||||
}
|
||||
assert!(pack_quad(&slab, TINT).is_none());
|
||||
// A UV between the 1/32 steps.
|
||||
let mut uv = top([0, 1, 2, 3]);
|
||||
uv[2].uv[0] += 1;
|
||||
assert!(pack_quad(&uv, TINT).is_none());
|
||||
// Light between the steps of 4.
|
||||
let mut light = top([0, 1, 2, 3]);
|
||||
light[1].light += 1;
|
||||
assert!(pack_quad(&light, TINT).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_quad_without_a_record_points_at_its_own_vertices() {
|
||||
let mut odd = top([0, 1, 2, 3]);
|
||||
for v in &mut odd {
|
||||
v.pos[1] = 8.5;
|
||||
}
|
||||
let mut solid: Vec<TerrainVertex> = Vec::new();
|
||||
for _ in 0..4 {
|
||||
solid.extend(top([0, 1, 2, 3]));
|
||||
}
|
||||
solid.extend(odd);
|
||||
let out = layout(&solid, &[TINT; 5]);
|
||||
// Five records, padded to eight slots, then the odd quad's four vertices.
|
||||
assert_eq!(out.len(), 12);
|
||||
assert_eq!(packed_quads(&out, 5), 4);
|
||||
assert_eq!(out[4][5], FULL);
|
||||
let at = 4 + out[4][0] as usize;
|
||||
for (slot, v) in out[at..at + 4].iter().zip(&odd) {
|
||||
assert!(same(&bytemuck::cast::<[u32; 6], TerrainVertex>(*slot), v));
|
||||
}
|
||||
// Nothing smaller to gain: no layout at all.
|
||||
assert!(layout(&odd, &[TINT]).is_empty());
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ fn slot(grouped: bool, n: usize) -> (usize, bool) {
|
||||
|
||||
/// Where each part of each bucket of a mesh's solid quads ends, and the
|
||||
/// planes that tell whether a bucket can face the eye.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Sides {
|
||||
/// The solid quads up to the end of each part of each bucket, in mesh
|
||||
/// order (`slot`).
|
||||
@@ -205,21 +205,13 @@ fn bucket(quad: &[TerrainVertex; 4]) -> usize {
|
||||
/// cutout quads are also ordered front to back within each axis-facing
|
||||
/// bucket; quads that tie keep the mesher's order (the quad's index ends its
|
||||
/// sort key), so coplanar overlays still draw over their base.
|
||||
#[cfg(test)]
|
||||
fn sort(solid: &mut Vec<TerrainVertex>) -> Sides {
|
||||
sort_tinted(solid, &mut Vec::new())
|
||||
}
|
||||
|
||||
/// `sort`, moving each quad's tint in `tints` (one per quad, or none) with it.
|
||||
pub fn sort_tinted(solid: &mut Vec<TerrainVertex>, tints: &mut Vec<u32>) -> Sides {
|
||||
let grouped = GROUPED.load(Ordering::Relaxed);
|
||||
SORT_SCRATCH.with_borrow_mut(|scratch| sort_with(solid, tints, grouped, scratch))
|
||||
pub fn sort(solid: &mut Vec<TerrainVertex>) -> Sides {
|
||||
sort_parts(solid, GROUPED.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// `sort`, with all the opaque parts first when `grouped`.
|
||||
#[cfg(test)]
|
||||
fn sort_parts(solid: &mut Vec<TerrainVertex>, grouped: bool) -> Sides {
|
||||
SORT_SCRATCH.with_borrow_mut(|scratch| sort_with(solid, &mut Vec::new(), grouped, scratch))
|
||||
SORT_SCRATCH.with_borrow_mut(|scratch| sort_with(solid, grouped, scratch))
|
||||
}
|
||||
|
||||
/// `sort`'s buffers, kept by each meshing thread so sorting a mesh allocates
|
||||
@@ -230,7 +222,6 @@ struct SortScratch {
|
||||
slots: Vec<u8>,
|
||||
keys: Vec<u64>,
|
||||
sorted: Vec<[TerrainVertex; 4]>,
|
||||
tints: Vec<u32>,
|
||||
}
|
||||
thread_local! {
|
||||
static SORT_SCRATCH: std::cell::RefCell<SortScratch> = Default::default();
|
||||
@@ -242,15 +233,9 @@ fn total_order_key(v: f32) -> u32 {
|
||||
(bits ^ (((bits >> 31) as u32) >> 1) as i32) as u32 ^ 0x8000_0000
|
||||
}
|
||||
|
||||
fn sort_with(
|
||||
solid: &mut Vec<TerrainVertex>,
|
||||
tints: &mut Vec<u32>,
|
||||
grouped: bool,
|
||||
scratch: &mut SortScratch,
|
||||
) -> Sides {
|
||||
fn sort_with(solid: &mut Vec<TerrainVertex>, grouped: bool, scratch: &mut SortScratch) -> Sides {
|
||||
let (quads, rest) = solid.as_chunks::<4>();
|
||||
debug_assert!(rest.is_empty(), "solid vertices come in quads");
|
||||
let quads_len = quads.len();
|
||||
let cutout = |q: &[TerrainVertex; 4]| q[0].layer & OPAQUE_LAYER == 0;
|
||||
// Each quad's part, by its place in mesh order (`slot`).
|
||||
let position = |b: usize, cutout: bool| match grouped {
|
||||
@@ -312,12 +297,6 @@ fn sort_with(
|
||||
sorted.extend(keys.iter().map(|&k| quads[(k & 0xFF_FFFF) as usize]));
|
||||
solid.clear();
|
||||
solid.extend_from_slice(sorted.as_flattened());
|
||||
if tints.len() == quads_len {
|
||||
let sorted_tints = &mut scratch.tints;
|
||||
sorted_tints.clear();
|
||||
sorted_tints.extend(keys.iter().map(|&k| tints[(k & 0xFF_FFFF) as usize]));
|
||||
std::mem::swap(tints, sorted_tints);
|
||||
}
|
||||
}
|
||||
Sides {
|
||||
ends: slot_ends,
|
||||
|
||||
@@ -99,14 +99,9 @@ pub(crate) fn prepare_with_models(
|
||||
.filter(|name| registry.default_state(name).is_some())
|
||||
.map(|name| name.strip_prefix("minecraft:").unwrap_or(name))
|
||||
.collect();
|
||||
let mut catalog = {
|
||||
let _step = crate::spans::startup::step("core.bake_states");
|
||||
StateModels::from_facts_with_models(root, registry, &blocks, sprites, &facts, models)?
|
||||
};
|
||||
{
|
||||
let _step = crate::spans::startup::step("core.bake_standalone");
|
||||
catalog.bake_standalone(root, sprites, models, standalone())?;
|
||||
}
|
||||
let mut catalog =
|
||||
StateModels::from_facts_with_models(root, registry, &blocks, sprites, &facts, models)?;
|
||||
catalog.bake_standalone(root, sprites, models, standalone())?;
|
||||
let mut placements = Vec::new();
|
||||
let mut states = Vec::new();
|
||||
for (index, (name, properties)) in GALLERY.iter().enumerate() {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// the depth bounds stage, which sees the unfaded alpha.
|
||||
fn oit_cloud_color(in: Out, front: bool, fade: bool) -> vec4<f32> {
|
||||
let color = textureSample(atlas, atlas_sampler, in.uv) * select(in.back_color, in.color, front);
|
||||
let faded = 1.0 - linear_fog(in.fog_distance.x, 0.0, globals.fog_ends.y);
|
||||
let faded = 1.0 - linear_fog(in.spherical, 0.0, globals.fog_ends.y);
|
||||
return vec4(color.rgb, color.a * select(1.0, faded, fade));
|
||||
}
|
||||
@fragment fn fs_oit_cloud_bounds(in: Out, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
|
||||
@@ -289,7 +289,6 @@ impl Opt {
|
||||
"options.modelPart.right_pants_leg",
|
||||
"options.modelPart.hat",
|
||||
][i],
|
||||
Opt::SkinLayers3d => "horizon.options.three_d_skin_layers",
|
||||
Opt::MainHand => "options.mainHand",
|
||||
Opt::AllowServerListing => "options.allowServerListing",
|
||||
Opt::RealmsNotifications => "options.realmsNotifications.button",
|
||||
@@ -730,11 +729,6 @@ impl Opt {
|
||||
} else {
|
||||
"options.key.hold"
|
||||
}),
|
||||
Opt::SkinLayers3d => tr(if s.extras.three_d_skin_layers {
|
||||
"options.on"
|
||||
} else {
|
||||
"options.off"
|
||||
}),
|
||||
Opt::ToggleAttack => tr(if s.toggle_attack {
|
||||
"options.key.toggle"
|
||||
} else {
|
||||
@@ -1019,7 +1013,6 @@ impl Opt {
|
||||
Opt::ChunkBuilder => s.prioritize_chunk_updates = next(s.prioritize_chunk_updates, 3),
|
||||
Opt::ToggleCrouch => s.toggle_crouch = !s.toggle_crouch,
|
||||
Opt::ToggleSprint => s.toggle_sprint = !s.toggle_sprint,
|
||||
Opt::SkinLayers3d => s.extras.three_d_skin_layers = !s.extras.three_d_skin_layers,
|
||||
Opt::ToggleAttack => s.toggle_attack = !s.toggle_attack,
|
||||
Opt::ToggleUse => s.toggle_use = !s.toggle_use,
|
||||
Opt::AutoJump => s.auto_jump = !s.auto_jump,
|
||||
@@ -2619,8 +2612,6 @@ impl OptionsUi {
|
||||
Page::Skin => {
|
||||
// SkinCustomizationScreen: the seven layers, then Main Hand.
|
||||
let mut opts: Vec<Opt> = (0..MODEL_PART_KEYS.len()).map(Opt::ModelPart).collect();
|
||||
// Horizon's own: the outer layer as real pixels, not one box.
|
||||
opts.push(Opt::SkinLayers3d);
|
||||
opts.push(Opt::MainHand);
|
||||
pairs(&opts)
|
||||
}
|
||||
@@ -3604,12 +3595,6 @@ impl OptionsUi {
|
||||
let pointer_over = ui::hovered(cursor, wd.x, wd.y, wd.w, wd.h)
|
||||
&& (!wd.list || cursor.1 >= top && cursor.1 < bottom);
|
||||
let hover = pointer_over || is_focused;
|
||||
// The tooltip is the pointer's (`setTooltip`, drawn while hovered);
|
||||
// a keyboard-focused widget keeps it against the widget
|
||||
// (`MenuTooltipPositioner`). Focus a mouse click leaves behind
|
||||
// does not: the description goes when the mouse leaves, like
|
||||
// vanilla's.
|
||||
let tip_hover = pointer_over || (is_focused && self.focus_from_keyboard);
|
||||
// A pack's option: its label within the widget (the name shortened), and on hover
|
||||
// the whole label when it was, then what the pack says of it.
|
||||
let shader = s.shader_options.as_ref();
|
||||
@@ -3624,7 +3609,7 @@ impl OptionsUi {
|
||||
let fitted = shader.zip(option).map(|(o, name)| {
|
||||
o.option_label_within(name, wd.w - 8.0, |t| atlas.font.width_text(t))
|
||||
});
|
||||
if tip_hover && let Some(o) = shader {
|
||||
if hover && let Some(o) = shader {
|
||||
let comment = match (&wd.kind, option) {
|
||||
(_, Some(name)) => o.comment(&format!("option.{name}")),
|
||||
(
|
||||
@@ -3656,7 +3641,7 @@ impl OptionsUi {
|
||||
tooltip_rect = (!pointer_over).then_some([wd.x, wd.y, wd.w, wd.h]);
|
||||
}
|
||||
}
|
||||
if tip_hover && let Some(tip) = tooltip_text(s, wd) {
|
||||
if hover && let Some(tip) = tooltip_text(s, wd) {
|
||||
tooltip = Some(match &wd.kind {
|
||||
// The duplicate keybinds tooltip keeps its own lines.
|
||||
WKind::Button {
|
||||
@@ -3922,9 +3907,9 @@ impl OptionsUi {
|
||||
};
|
||||
return self.bind_mouse(s, button);
|
||||
}
|
||||
let actions =
|
||||
self.graphics
|
||||
.click(s, (x, y), size, framebuffer, drag, right, self.shift);
|
||||
let actions = self
|
||||
.graphics
|
||||
.click(s, (x, y), size, framebuffer, drag, right);
|
||||
return self.graphics_events(s, actions);
|
||||
}
|
||||
if self.page() == Page::Packs {
|
||||
@@ -4117,13 +4102,13 @@ impl OptionsUi {
|
||||
}
|
||||
Act::Done => events.extend(self.back(s)),
|
||||
Act::Cycle(opt) => {
|
||||
events.extend(self.cycle_option(s, opt, self.shift, framebuffer));
|
||||
events.extend(self.cycle_option(s, opt, right, framebuffer));
|
||||
}
|
||||
Act::Extra(item) => {
|
||||
if let Some(name) = item.key_name() {
|
||||
self.selected_key = Some(name);
|
||||
} else {
|
||||
item.cycle(&mut s.extras, self.shift);
|
||||
item.cycle(&mut s.extras, right);
|
||||
events.push(Event::Changed);
|
||||
}
|
||||
}
|
||||
@@ -6025,65 +6010,6 @@ mod tests {
|
||||
assert_eq!(s.file.fullscreen_resolution, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clicked_extras_tooltip_leaves_with_the_pointer() {
|
||||
let atlas = atlas();
|
||||
let mut s = Settings::default();
|
||||
let mut ui = OptionsUi::default();
|
||||
ui.show(Page::Extras);
|
||||
let (widgets, _, _, _) = ui.layout(&s, 480.0, 480.0);
|
||||
let extra = widgets
|
||||
.iter()
|
||||
.find(|w| {
|
||||
matches!(
|
||||
&w.kind,
|
||||
WKind::Button {
|
||||
act: Act::Extra(item),
|
||||
..
|
||||
} if !matches!(item, crate::extras::Item::SortKey)
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
ui.click(
|
||||
&mut s,
|
||||
&atlas,
|
||||
extra.x + 1.0,
|
||||
extra.y + 1.0,
|
||||
(480.0, 480.0),
|
||||
[960, 540],
|
||||
false,
|
||||
false,
|
||||
);
|
||||
// The pointer is still on the clicked widget: it carries the
|
||||
// description (`setTooltip`, drawn while hovered).
|
||||
let mut on = HudBatch::new([480, 480]);
|
||||
ui.draw(
|
||||
&s,
|
||||
&mut on,
|
||||
&atlas,
|
||||
(extra.x + extra.w / 2.0, extra.y + extra.h / 2.0),
|
||||
);
|
||||
// The pointer moved off: the description goes with it. A mouse
|
||||
// focus a click left behind (the look-up keeps the click's widget,
|
||||
// `focus_from_keyboard` false) does not keep the tooltip against
|
||||
// the widget, like vanilla's `MenuTooltipPositioner` does for a
|
||||
// keyboard focus.
|
||||
let mut off = HudBatch::new([480, 480]);
|
||||
ui.draw(
|
||||
&s,
|
||||
&mut off,
|
||||
&atlas,
|
||||
(extra.x + extra.w + 40.0, extra.y + extra.h + 40.0),
|
||||
);
|
||||
assert!(
|
||||
off.vertices.len() < on.vertices.len(),
|
||||
"a click left {} tooltip vertices behind when the pointer left ({} vs {})",
|
||||
on.vertices.len() - off.vertices.len(),
|
||||
on.vertices.len(),
|
||||
off.vertices.len(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclusive_fullscreen_and_its_tooltips_follow_the_option() {
|
||||
let mut s = Settings::default();
|
||||
@@ -7927,146 +7853,4 @@ mod tests {
|
||||
assert!(events.contains(&Event::Changed));
|
||||
assert!(s.extras.music_off.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_cycle_button_cycles_forward_and_shift_click_cycles_backward_with_wrapping() {
|
||||
let atlas = atlas();
|
||||
let mut s = Settings::default();
|
||||
let mut ui = OptionsUi::default();
|
||||
ui.show(Page::Chat);
|
||||
let size = (480.0, 480.0);
|
||||
let (widgets, _, _, _) = ui.layout(&s, size.0, size.1);
|
||||
let button = widgets
|
||||
.iter()
|
||||
.find(|wd| {
|
||||
matches!(
|
||||
wd.kind,
|
||||
WKind::Button {
|
||||
active: true,
|
||||
act: Act::Cycle(Opt::ChatVisibility),
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.expect("ChatVisibility must be a cycle button");
|
||||
let (x, y) = (button.x + 1.0, button.y + 1.0);
|
||||
|
||||
// Initial chat_visibility is 0
|
||||
assert_eq!(s.chat_visibility, 0);
|
||||
|
||||
// 1. Feature without shift (normal click): cycles forward (+1)
|
||||
assert_eq!(
|
||||
ui.click(&mut s, &atlas, x, y, size, [854, 480], false, false),
|
||||
vec![Event::ButtonClick, Event::Changed]
|
||||
);
|
||||
assert_eq!(s.chat_visibility, 1);
|
||||
|
||||
assert_eq!(
|
||||
ui.click(&mut s, &atlas, x, y, size, [854, 480], false, false),
|
||||
vec![Event::ButtonClick, Event::Changed]
|
||||
);
|
||||
assert_eq!(s.chat_visibility, 2);
|
||||
|
||||
// 3. Wrap-around forward (at max value, normal click wraps to min / 0)
|
||||
assert_eq!(
|
||||
ui.click(&mut s, &atlas, x, y, size, [854, 480], false, false),
|
||||
vec![Event::ButtonClick, Event::Changed]
|
||||
);
|
||||
assert_eq!(s.chat_visibility, 0);
|
||||
|
||||
// 4. Wrap-around backward (at min / 0, shift-click wraps to max)
|
||||
ui.shift = true;
|
||||
assert_eq!(
|
||||
ui.click(&mut s, &atlas, x, y, size, [854, 480], false, false),
|
||||
vec![Event::ButtonClick, Event::Changed]
|
||||
);
|
||||
assert_eq!(s.chat_visibility, 2);
|
||||
|
||||
// 2. Feature with shift: cycles backward (-1)
|
||||
assert_eq!(
|
||||
ui.click(&mut s, &atlas, x, y, size, [854, 480], false, false),
|
||||
vec![Event::ButtonClick, Event::Changed]
|
||||
);
|
||||
assert_eq!(s.chat_visibility, 1);
|
||||
|
||||
assert_eq!(
|
||||
ui.click(&mut s, &atlas, x, y, size, [854, 480], false, false),
|
||||
vec![Event::ButtonClick, Event::Changed]
|
||||
);
|
||||
assert_eq!(s.chat_visibility, 0);
|
||||
ui.shift = false;
|
||||
|
||||
// Also test Opt::Particles on Page::Video
|
||||
ui.show(Page::Video);
|
||||
let (widgets, _, _, _) = ui.layout(&s, size.0, size.1);
|
||||
let button = widgets
|
||||
.iter()
|
||||
.find(|wd| {
|
||||
matches!(
|
||||
wd.kind,
|
||||
WKind::Button {
|
||||
active: true,
|
||||
act: Act::Cycle(Opt::Particles),
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.expect("Particles must be a cycle button");
|
||||
let (px, py) = (button.x + 1.0, button.y + 1.0);
|
||||
|
||||
// Initial particles is 0
|
||||
assert_eq!(s.particles, 0);
|
||||
|
||||
// Forward
|
||||
ui.click(&mut s, &atlas, px, py, size, [854, 480], false, false);
|
||||
assert_eq!(s.particles, 1);
|
||||
ui.click(&mut s, &atlas, px, py, size, [854, 480], false, false);
|
||||
assert_eq!(s.particles, 2);
|
||||
// Wrap forward
|
||||
ui.click(&mut s, &atlas, px, py, size, [854, 480], false, false);
|
||||
assert_eq!(s.particles, 0);
|
||||
|
||||
// Wrap backward with shift
|
||||
ui.shift = true;
|
||||
ui.click(&mut s, &atlas, px, py, size, [854, 480], false, false);
|
||||
assert_eq!(s.particles, 2);
|
||||
// Backward with shift
|
||||
ui.click(&mut s, &atlas, px, py, size, [854, 480], false, false);
|
||||
assert_eq!(s.particles, 1);
|
||||
ui.click(&mut s, &atlas, px, py, size, [854, 480], false, false);
|
||||
assert_eq!(s.particles, 0);
|
||||
ui.shift = false;
|
||||
|
||||
// Also verify Act::Extra on Page::Extras
|
||||
ui.show(Page::Extras);
|
||||
let (widgets, _, _, _) = ui.layout(&s, size.0, size.1);
|
||||
if let Some(extra_btn) = widgets.iter().find(|wd| {
|
||||
matches!(
|
||||
wd.kind,
|
||||
WKind::Button {
|
||||
active: true,
|
||||
act: Act::Extra(crate::extras::Item::CosmeticsOthers),
|
||||
..
|
||||
}
|
||||
)
|
||||
}) {
|
||||
let (ex, ey) = (extra_btn.x + 1.0, extra_btn.y + 1.0);
|
||||
use horizon_cosmetics::prefs::Others;
|
||||
assert_eq!(s.extras.cosmetics_others, Others::Shown);
|
||||
|
||||
// Forward
|
||||
ui.click(&mut s, &atlas, ex, ey, size, [854, 480], false, false);
|
||||
assert_eq!(s.extras.cosmetics_others, Others::NameTags);
|
||||
|
||||
// Backward with shift
|
||||
ui.shift = true;
|
||||
ui.click(&mut s, &atlas, ex, ey, size, [854, 480], false, false);
|
||||
assert_eq!(s.extras.cosmetics_others, Others::Shown);
|
||||
|
||||
// Wrap-around backward with shift
|
||||
ui.click(&mut s, &atlas, ex, ey, size, [854, 480], false, false);
|
||||
assert_eq!(s.extras.cosmetics_others, Others::Hidden);
|
||||
ui.shift = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ use horizon_core::{
|
||||
};
|
||||
use horizon_world::block_collision::BlockCollisionRegistry;
|
||||
use std::{
|
||||
array,
|
||||
collections::{HashMap, HashSet},
|
||||
path::Path,
|
||||
};
|
||||
@@ -48,9 +47,6 @@ pub struct ParticleOptions {
|
||||
/// `GeyserParticleOptions.waterBlocks` and `GeyserBaseParticleOptions`'
|
||||
/// `waterBlocks` and `burstImpulseBase` (0 where the type has none).
|
||||
pub geyser: Option<(i32, f32)>,
|
||||
/// `VibrationParticleOption`: the listener the signal flies to and its
|
||||
/// arrival time in ticks (`ParticleEngine::vibration` is the same particle).
|
||||
pub vibration: Option<([f64; 3], i32)>,
|
||||
/// A native mod's own particle type, by its namespaced name (`mymod:spark`):
|
||||
/// `SimpleParticleType` plus the provider `particles/<name>.json` names.
|
||||
pub mod_particle: Option<String>,
|
||||
@@ -66,7 +62,6 @@ impl ParticleOptions {
|
||||
trail: None,
|
||||
item: None,
|
||||
geyser: None,
|
||||
vibration: None,
|
||||
mod_particle: None,
|
||||
}
|
||||
}
|
||||
@@ -92,13 +87,6 @@ impl ParticleOptions {
|
||||
..Self::simple(ty)
|
||||
}
|
||||
}
|
||||
/// `VibrationParticleOption(destination, arrivalInTicks)`.
|
||||
pub fn vibration(dest: [f64; 3], ticks: i32) -> Self {
|
||||
Self {
|
||||
vibration: Some((dest, ticks)),
|
||||
..Self::simple(ParticleType::Vibration)
|
||||
}
|
||||
}
|
||||
/// `ItemParticleOption`: the stack's layer particle sprites (`BreakingItemParticle`).
|
||||
pub fn item(layers: Vec<String>) -> Self {
|
||||
Self {
|
||||
@@ -528,7 +516,7 @@ pub fn breaking_position(
|
||||
}
|
||||
}
|
||||
// Vanilla consumes all three doubles before replacing the hit-face coordinate.
|
||||
let mut p = array::from_fn(|i| {
|
||||
let mut p = std::array::from_fn(|i| {
|
||||
f64::from(pos[i])
|
||||
+ random.next_double() * (bounds[i + 3] - bounds[i] - f64::from(0.2f32))
|
||||
+ f64::from(0.1f32)
|
||||
@@ -660,16 +648,18 @@ pub fn destroy_positions(
|
||||
mut emit: impl FnMut([f64; 3], [f64; 3]),
|
||||
) {
|
||||
for b in boxes {
|
||||
let widths: [f64; 3] = array::from_fn(|i| (b[i + 3] - b[i]).min(1.0));
|
||||
let widths: [f64; 3] = std::array::from_fn(|i| (b[i + 3] - b[i]).min(1.0));
|
||||
let counts: [usize; 3] = widths.map(|w| ((w / 0.25).ceil() as usize).max(2));
|
||||
for x in 0..counts[0] {
|
||||
for y in 0..counts[1] {
|
||||
for z in 0..counts[2] {
|
||||
let cells = [x, y, z];
|
||||
let relative: [f64; 3] =
|
||||
array::from_fn(|i| (cells[i] as f64 + 0.5) / counts[i] as f64);
|
||||
std::array::from_fn(|i| (cells[i] as f64 + 0.5) / counts[i] as f64);
|
||||
emit(
|
||||
array::from_fn(|i| f64::from(pos[i]) + (relative[i] * widths[i] + b[i])),
|
||||
std::array::from_fn(|i| {
|
||||
f64::from(pos[i]) + (relative[i] * widths[i] + b[i])
|
||||
}),
|
||||
relative.map(|v| v - 0.5),
|
||||
);
|
||||
}
|
||||
@@ -750,7 +740,7 @@ pub fn on_block_faces_between(
|
||||
let count = random.next_int_bound(max - min + 1).unwrap_or(0) + min;
|
||||
for _ in 0..count {
|
||||
// getRandomSpeedRanges, then spawnParticleOnFace with step factor 0.55.
|
||||
let speed: [f64; 3] = array::from_fn(|_| next_double_between(random, -0.5, 0.5));
|
||||
let speed: [f64; 3] = std::array::from_fn(|_| next_double_between(random, -0.5, 0.5));
|
||||
let mut p = [0.0; 3];
|
||||
let mut v = [0.0; 3];
|
||||
for i in 0..3 {
|
||||
@@ -779,12 +769,12 @@ pub fn along_axis(
|
||||
) {
|
||||
let count = random.next_int_bound(max - min + 1).unwrap_or(0) + min;
|
||||
for _ in 0..count {
|
||||
let p: [f64; 3] = array::from_fn(|i| {
|
||||
let p: [f64; 3] = std::array::from_fn(|i| {
|
||||
f64::from(pos[i])
|
||||
+ 0.5
|
||||
+ next_double_between(random, -1.0, 1.0) * if i == axis { 0.5 } else { 0.125 }
|
||||
});
|
||||
let v: [f64; 3] = array::from_fn(|i| {
|
||||
let v: [f64; 3] = std::array::from_fn(|i| {
|
||||
if i == axis {
|
||||
next_double_between(random, -1.0, 1.0)
|
||||
} else {
|
||||
@@ -1267,113 +1257,6 @@ impl ParticleLightCache {
|
||||
}
|
||||
}
|
||||
|
||||
const PARTICLE_SECTION_CACHE_SIZE: usize = 128;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum SectionFrustumResult {
|
||||
Outside,
|
||||
Intersects,
|
||||
Inside,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct CachedSectionFrustum {
|
||||
section: [i32; 3],
|
||||
radius_quarters: i32,
|
||||
result: SectionFrustumResult,
|
||||
}
|
||||
|
||||
struct ParticleSectionFrustumCache {
|
||||
entries: [Option<CachedSectionFrustum>; PARTICLE_SECTION_CACHE_SIZE],
|
||||
}
|
||||
|
||||
impl Default for ParticleSectionFrustumCache {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
entries: [None; PARTICLE_SECTION_CACHE_SIZE],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParticleSectionFrustumCache {
|
||||
fn result(
|
||||
&mut self,
|
||||
world: [f64; 3],
|
||||
camera: DVec3,
|
||||
radius: f64,
|
||||
frustum: &crate::gfx::Frustum,
|
||||
) -> SectionFrustumResult {
|
||||
let Some(section) = world_section(world) else {
|
||||
return SectionFrustumResult::Intersects;
|
||||
};
|
||||
if !radius.is_finite() || radius < 0.0 || radius * 4.0 > f64::from(i32::MAX) {
|
||||
return SectionFrustumResult::Intersects;
|
||||
}
|
||||
let radius_quarters = (radius * 4.0).ceil() as i32;
|
||||
let hash = (section[0] as u32).wrapping_mul(73_856_093)
|
||||
^ (section[1] as u32).wrapping_mul(19_349_663)
|
||||
^ (section[2] as u32).wrapping_mul(83_492_791)
|
||||
^ (radius_quarters as u32).wrapping_mul(2_654_435_761);
|
||||
let slot = hash as usize & (PARTICLE_SECTION_CACHE_SIZE - 1);
|
||||
if let Some(entry) = self.entries[slot]
|
||||
&& entry.section == section
|
||||
&& entry.radius_quarters == radius_quarters
|
||||
{
|
||||
return entry.result;
|
||||
}
|
||||
|
||||
let pad = f64::from(radius_quarters) * 0.25;
|
||||
let camera = camera.to_array();
|
||||
let min = array::from_fn(|axis| f64::from(section[axis]) * 16.0 - pad - camera[axis]);
|
||||
let max = array::from_fn(|axis| min[axis] + 16.0 + 2.0 * pad);
|
||||
let result = if min.into_iter().chain(max).any(|v| !v.is_finite()) {
|
||||
SectionFrustumResult::Intersects
|
||||
} else {
|
||||
let min = FVec3::from_array(min.map(|v| v as f32));
|
||||
let max = FVec3::from_array(max.map(|v| v as f32));
|
||||
if !min.is_finite() || !max.is_finite() {
|
||||
SectionFrustumResult::Intersects
|
||||
} else if frustum.contains(min, max) {
|
||||
SectionFrustumResult::Inside
|
||||
} else if frustum.intersects(min, max) {
|
||||
SectionFrustumResult::Intersects
|
||||
} else {
|
||||
SectionFrustumResult::Outside
|
||||
}
|
||||
};
|
||||
self.entries[slot] = Some(CachedSectionFrustum {
|
||||
section,
|
||||
radius_quarters,
|
||||
result,
|
||||
});
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn world_section(position: [f64; 3]) -> Option<[i32; 3]> {
|
||||
let mut block = [0; 3];
|
||||
for axis in 0..3 {
|
||||
let value = position[axis].floor();
|
||||
if !value.is_finite() || value < f64::from(i32::MIN) || value > f64::from(i32::MAX) {
|
||||
return None;
|
||||
}
|
||||
block[axis] = value as i32;
|
||||
}
|
||||
Some([block[0] >> 4, block[1] >> 4, block[2] >> 4])
|
||||
}
|
||||
|
||||
fn below_half_pixel(radius: f64, distance_squared: f64, pixel_focal_length: f32) -> bool {
|
||||
let projected_limit = 4.0 * radius * f64::from(pixel_focal_length);
|
||||
radius.is_finite()
|
||||
&& radius >= 0.0
|
||||
&& distance_squared.is_finite()
|
||||
&& distance_squared > 0.0
|
||||
&& pixel_focal_length.is_finite()
|
||||
&& pixel_focal_length > 0.0
|
||||
&& projected_limit.is_finite()
|
||||
&& projected_limit * projected_limit < distance_squared
|
||||
}
|
||||
|
||||
/// `ClientExplosionTracker.ExplosionInfo`.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct TrackedExplosion {
|
||||
@@ -1552,14 +1435,15 @@ impl ParticleEngine {
|
||||
self.particles.clear();
|
||||
self.pending.clear();
|
||||
}
|
||||
/// `VibrationSignalParticle`'s body: the signal leaves `from` aimed at the
|
||||
/// listener at `to` and lands there in `ticks`.
|
||||
fn vibration_particle(from: [f64; 3], to: [f64; 3], ticks: i32) -> Particle {
|
||||
/// A vibration signal flying from `from` to `to` in `ticks`
|
||||
/// (`VibrationParticleOption`).
|
||||
pub fn vibration(&mut self, from: [f64; 3], to: [f64; 3], ticks: i32) {
|
||||
let mut q = Particle::blank(ParticleType::Vibration, from);
|
||||
q.size = 0.3;
|
||||
q.lifetime = ticks;
|
||||
q.has_physics = false;
|
||||
q.light = Light::Block15;
|
||||
q.set = self.set_of(ParticleType::Vibration);
|
||||
let d = [from[0] - to[0], from[1] - to[1], from[2] - to[2]];
|
||||
let rot = d[0].atan2(d[2]) as f32;
|
||||
let pitch = d[1].atan2((d[0] * d[0] + d[2] * d[2]).sqrt()) as f32;
|
||||
@@ -1570,14 +1454,6 @@ impl ParticleEngine {
|
||||
pitch,
|
||||
pitch_o: pitch,
|
||||
};
|
||||
q
|
||||
}
|
||||
/// A vibration signal flying from `from` to `to` in `ticks`
|
||||
/// (`VibrationParticleOption`), as the client's own vibration system
|
||||
/// (`VibrationSystem.Ticker`) spawns one.
|
||||
pub fn vibration(&mut self, from: [f64; 3], to: [f64; 3], ticks: i32) {
|
||||
let mut q = Self::vibration_particle(from, to, ticks);
|
||||
q.set = self.set_of(ParticleType::Vibration);
|
||||
self.pending.push(q);
|
||||
}
|
||||
/// `ParticleEngine.add` (via `particlesToAdd`, applied on the next tick).
|
||||
@@ -1657,7 +1533,7 @@ impl ParticleEngine {
|
||||
for _ in 0..128 {
|
||||
let r = &mut self.level_random;
|
||||
let t = r.next_double();
|
||||
let velocity = array::from_fn(|_| f64::from((r.next_float() - 0.5) * 0.2));
|
||||
let velocity = std::array::from_fn(|_| f64::from((r.next_float() - 0.5) * 0.2));
|
||||
let position = [
|
||||
to[0] + t * (from[0] - to[0]) + (r.next_double() - 0.5) + 0.5,
|
||||
to[1] + t * (from[1] - to[1]) + r.next_double() - 0.5,
|
||||
@@ -1685,7 +1561,7 @@ impl ParticleEngine {
|
||||
for _ in 0..128 {
|
||||
let r = &mut self.level_random;
|
||||
let t = r.next_double();
|
||||
let velocity = array::from_fn(|_| f64::from((r.next_float() - 0.5) * 0.2));
|
||||
let velocity = std::array::from_fn(|_| f64::from((r.next_float() - 0.5) * 0.2));
|
||||
// Mth.lerp(t, to, from).
|
||||
let position = [
|
||||
to[0] + t * (from[0] - to[0]) + (r.next_double() - 0.5) * WIDTH * 2.0,
|
||||
@@ -2882,16 +2758,8 @@ impl ParticleEngine {
|
||||
}
|
||||
q
|
||||
}
|
||||
// VibrationSignalParticle.Provider: `VibrationParticleOption` carries
|
||||
// the listener the signal flies to and its arrival time; the sprite
|
||||
// is the set's own draw from the engine's random.
|
||||
T::Vibration => {
|
||||
let (dest, ticks) = o.vibration?;
|
||||
let mut q = Self::vibration_particle(p, dest, ticks);
|
||||
q.set = set;
|
||||
q.sprite = self.sprite_from_set(ty);
|
||||
q
|
||||
}
|
||||
// Vibration signals are created by `vibration`, aimed at a listener.
|
||||
T::Vibration => return None,
|
||||
// ShriekParticle.
|
||||
T::Shriek => {
|
||||
let mut q = Particle::moving(r, ty, p, [0.0; 3]);
|
||||
@@ -4497,7 +4365,8 @@ impl ParticleEngine {
|
||||
.filter(|p| p.rgba[3] > 0.0)
|
||||
.filter_map(|p| {
|
||||
let (_, rgb, reach) = shining.iter().find(|(ty, ..)| *ty == p.ty)?;
|
||||
let at = array::from_fn(|i| p.previous[i] + (p.position[i] - p.previous[i]) * f);
|
||||
let at =
|
||||
std::array::from_fn(|i| p.previous[i] + (p.position[i] - p.previous[i]) * f);
|
||||
Some((at, *rgb, *reach, p.rgba[3].clamp(0.0, 1.0)))
|
||||
})
|
||||
.collect()
|
||||
@@ -4512,70 +4381,11 @@ impl ParticleEngine {
|
||||
partial: f32,
|
||||
light_at: &dyn Fn([i32; 3]) -> u8,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
self.render_inner::<false>(
|
||||
camera,
|
||||
yaw,
|
||||
pitch,
|
||||
partial,
|
||||
0.0,
|
||||
None,
|
||||
&mut |_, _| false,
|
||||
light_at,
|
||||
out,
|
||||
);
|
||||
}
|
||||
|
||||
/// Render particles after rejecting view-frustum misses and any particles
|
||||
/// the caller proves hidden behind opaque terrain.
|
||||
pub fn render_culled(
|
||||
&self,
|
||||
camera: DVec3,
|
||||
yaw: f32,
|
||||
pitch: f32,
|
||||
partial: f32,
|
||||
pixel_focal_length: f32,
|
||||
culling_enabled: bool,
|
||||
frustum: Option<&crate::gfx::Frustum>,
|
||||
mut occluded: impl FnMut([f64; 3], [f64; 3]) -> bool,
|
||||
light_at: &dyn Fn([i32; 3]) -> u8,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
if !culling_enabled {
|
||||
self.render(camera, yaw, pitch, partial, light_at, out);
|
||||
return;
|
||||
}
|
||||
self.render_inner::<true>(
|
||||
camera,
|
||||
yaw,
|
||||
pitch,
|
||||
partial,
|
||||
pixel_focal_length,
|
||||
frustum,
|
||||
&mut occluded,
|
||||
light_at,
|
||||
out,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_inner<const CULL: bool>(
|
||||
&self,
|
||||
camera: DVec3,
|
||||
yaw: f32,
|
||||
pitch: f32,
|
||||
partial: f32,
|
||||
pixel_focal_length: f32,
|
||||
frustum: Option<&crate::gfx::Frustum>,
|
||||
occluded: &mut dyn FnMut([f64; 3], [f64; 3]) -> bool,
|
||||
light_at: &dyn Fn([i32; 3]) -> u8,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
// Camera.setRotation: rotationYXZ(PI - yRot, -xRot, 0).
|
||||
let base = Quat::from_rotation_y(std::f32::consts::PI - yaw.to_radians())
|
||||
* Quat::from_rotation_x(-pitch.to_radians());
|
||||
let mut light_cache = ParticleLightCache::default();
|
||||
let mut section_cache =
|
||||
(CULL && frustum.is_some()).then(ParticleSectionFrustumCache::default);
|
||||
for p in &self.particles {
|
||||
if p.no_render {
|
||||
continue;
|
||||
@@ -4587,39 +4397,6 @@ impl ParticleEngine {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if CULL && matches!(p.behaviour, Behaviour::Shriek { delay } if delay > 0) {
|
||||
continue;
|
||||
}
|
||||
let culled_geometry = if CULL {
|
||||
let a = f64::from(partial);
|
||||
let world = array::from_fn(|i| p.previous[i] + (p.position[i] - p.previous[i]) * a);
|
||||
let center = (DVec3::from_array(world) - camera).as_vec3();
|
||||
let size = p.quad_size(partial);
|
||||
// Every generated quad vertex is sqrt(2) * size from its center,
|
||||
// including the rotated two-quad particle types.
|
||||
let radius = f64::from(size.abs()) * std::f64::consts::SQRT_2 + 0.05;
|
||||
if let (Some(frustum), Some(section_cache)) = (frustum, section_cache.as_mut()) {
|
||||
match section_cache.result(world, camera, radius, frustum) {
|
||||
SectionFrustumResult::Outside => continue,
|
||||
SectionFrustumResult::Intersects
|
||||
if !frustum.intersects_sphere(center, radius as f32) =>
|
||||
{
|
||||
continue;
|
||||
}
|
||||
SectionFrustumResult::Inside | SectionFrustumResult::Intersects => {}
|
||||
}
|
||||
}
|
||||
let distance_squared = f64::from(center.length_squared());
|
||||
if below_half_pixel(radius, distance_squared, pixel_focal_length) {
|
||||
continue;
|
||||
}
|
||||
if occluded(world.map(|v| v - radius), world.map(|v| v + radius)) {
|
||||
continue;
|
||||
}
|
||||
Some((center, size))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// `SubmitNodeCollection.submitQuadParticleGroup`: the translucent
|
||||
// layer draws after translucent terrain, so a crumb of stained
|
||||
// glass cannot hide the glass behind it (`getLayer`).
|
||||
@@ -4635,20 +4412,19 @@ impl ParticleEngine {
|
||||
},
|
||||
};
|
||||
let Some(r) = rect else { continue };
|
||||
let (center, size) = culled_geometry.unwrap_or_else(|| {
|
||||
let a = f64::from(partial);
|
||||
let pos = DVec3::new(
|
||||
p.previous[0] + (p.position[0] - p.previous[0]) * a,
|
||||
p.previous[1] + (p.position[1] - p.previous[1]) * a,
|
||||
p.previous[2] + (p.position[2] - p.previous[2]) * a,
|
||||
) - camera;
|
||||
(pos.as_vec3(), p.quad_size(partial))
|
||||
});
|
||||
let a = f64::from(partial);
|
||||
let pos = DVec3::new(
|
||||
p.previous[0] + (p.position[0] - p.previous[0]) * a,
|
||||
p.previous[1] + (p.position[1] - p.previous[1]) * a,
|
||||
p.previous[2] + (p.position[2] - p.previous[2]) * a,
|
||||
) - camera;
|
||||
let center = pos.as_vec3();
|
||||
let rotation = if p.roll != 0.0 {
|
||||
base * Quat::from_rotation_z(p.o_roll + (p.roll - p.o_roll) * partial)
|
||||
} else {
|
||||
base
|
||||
};
|
||||
let size = p.quad_size(partial);
|
||||
let mut alpha = p.rgba[3];
|
||||
// OverlayParticle.extract: the flash fades as it pulses.
|
||||
if p.ty == ParticleType::Flash {
|
||||
@@ -4662,7 +4438,7 @@ impl ParticleEngine {
|
||||
match p.behaviour {
|
||||
// ShriekParticle.extract: two tilted quads, fading out.
|
||||
Behaviour::Shriek { delay } => {
|
||||
if !CULL && delay > 0 {
|
||||
if delay > 0 {
|
||||
continue;
|
||||
}
|
||||
alpha = 1.0 - ((p.age as f32 + partial) / p.lifetime as f32).clamp(0.0, 1.0);
|
||||
@@ -4705,7 +4481,7 @@ impl ParticleEngine {
|
||||
// DustColorTransitionParticle.lerpColors.
|
||||
Behaviour::Transition { from, to } => {
|
||||
let t = (p.age as f32 + partial) / (p.lifetime as f32 + 1.0);
|
||||
rgb = array::from_fn(|i| from[i] + (to[i] - from[i]) * t);
|
||||
rgb = std::array::from_fn(|i| from[i] + (to[i] - from[i]) * t);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -4840,86 +4616,6 @@ mod tests {
|
||||
assert_eq!(reads.get(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subpixel_culling_uses_the_frame_projection_and_fails_open() {
|
||||
assert!(below_half_pixel(0.01, 41.0_f64.powi(2), 1_000.0));
|
||||
assert!(!below_half_pixel(0.01, 40.0_f64.powi(2), 1_000.0));
|
||||
assert!(!below_half_pixel(0.01, 41.0_f64.powi(2), 0.0));
|
||||
assert!(!below_half_pixel(0.01, f64::NAN, 1_000.0));
|
||||
assert!(!below_half_pixel(f64::NAN, 41.0_f64.powi(2), 1_000.0));
|
||||
|
||||
for distance in [0.25_f32, 1.0, 8.0, 40.0, 128.0] {
|
||||
for radius in [0.01, 0.05, 0.2] {
|
||||
let center = FVec3::new(distance, distance * 0.3, -distance * 0.8);
|
||||
let old = 2.0 * radius * 900.0 / f64::from(center.length()) < 0.5;
|
||||
assert_eq!(
|
||||
below_half_pixel(radius, f64::from(center.length_squared()), 900.0),
|
||||
old
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_culled_render_matches_the_regular_particle_path() {
|
||||
let mut engine = ParticleEngine::new(Path::new(""), &HashMap::new(), &HashSet::new());
|
||||
let sprite = "test:smoke".to_owned();
|
||||
engine.atlas.insert(
|
||||
sprite.clone(),
|
||||
(
|
||||
Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 1.0,
|
||||
v1: 1.0,
|
||||
w: 16,
|
||||
h: 16,
|
||||
},
|
||||
false,
|
||||
),
|
||||
);
|
||||
let mut random = LegacyRandom::new(7);
|
||||
let mut particle =
|
||||
Particle::moving(&mut random, ParticleType::Smoke, [0.0, 0.0, -3.0], [0.0; 3]);
|
||||
particle.sprite = Sprite::Named(sprite);
|
||||
engine.particles.push(particle);
|
||||
|
||||
let light_at = |_: [i32; 3]| 0xf0;
|
||||
let mut regular = Vec::new();
|
||||
engine.render(DVec3::ZERO, 0.0, 0.0, 0.5, &light_at, &mut regular);
|
||||
let mut disabled = Vec::new();
|
||||
engine.render_culled(
|
||||
DVec3::ZERO,
|
||||
0.0,
|
||||
0.0,
|
||||
0.5,
|
||||
900.0,
|
||||
false,
|
||||
None,
|
||||
|_, _| panic!("disabled culling must not query particle visibility"),
|
||||
&light_at,
|
||||
&mut disabled,
|
||||
);
|
||||
|
||||
let attributes = |vertices: &[Vertex]| {
|
||||
vertices
|
||||
.iter()
|
||||
.map(|vertex| {
|
||||
(
|
||||
vertex.pos,
|
||||
vertex.uv,
|
||||
vertex.layer,
|
||||
vertex.color,
|
||||
vertex.light,
|
||||
vertex.normal,
|
||||
vertex.glint_uv,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(attributes(&disabled), attributes(®ular));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mod_particles_json_becomes_its_sprite_set() {
|
||||
let dir =
|
||||
@@ -5480,41 +5176,6 @@ mod tests {
|
||||
}
|
||||
assert_eq!(engine.pending.len(), 1010);
|
||||
}
|
||||
/// `VibrationSignalParticle.Provider.createParticle`: the provider path
|
||||
/// the client's packet handler reaches (`add_with`) makes the signal from
|
||||
/// the option's destination and arrival time, where it used to refuse it.
|
||||
#[test]
|
||||
fn a_vibration_option_makes_the_signal_its_provider_draws() {
|
||||
let mut engine = ParticleEngine::new(Path::new(""), &HashMap::new(), &HashSet::new());
|
||||
let options = ParticleOptions::vibration([10.5, 70.5, -3.5], 20);
|
||||
// Within the camera's 32 blocks, so only the provider decides.
|
||||
engine.add_with(&options, [0.5, 1.5, 0.5], [0.0; 3], false, false);
|
||||
assert_eq!(engine.made().get("vibration"), Some(&1));
|
||||
let q = engine.pending.first().expect("the signal is made");
|
||||
assert_eq!(q.ty, ParticleType::Vibration);
|
||||
assert_eq!(q.lifetime, 20);
|
||||
assert!((q.size - 0.3).abs() < 1e-6);
|
||||
assert!(!q.has_physics);
|
||||
match &q.behaviour {
|
||||
Behaviour::Vibration {
|
||||
dest, rot, pitch, ..
|
||||
} => {
|
||||
assert_eq!(*dest, [10.5, 70.5, -3.5]);
|
||||
// The constructor aims it from where it starts at the listener.
|
||||
let d: [f64; 3] = [0.5 - 10.5, 1.5 - 70.5, 0.5 + 3.5];
|
||||
assert!((*rot - d[0].atan2(d[2]) as f32).abs() < 1e-6);
|
||||
let flat = (d[0] * d[0] + d[2] * d[2]).sqrt();
|
||||
assert!((*pitch - d[1].atan2(flat) as f32).abs() < 1e-6);
|
||||
}
|
||||
_ => panic!("a vibration signal"),
|
||||
}
|
||||
// Only `VibrationParticleOption` carries a listener; a bare type has none.
|
||||
let bare = ParticleOptions::simple(ParticleType::Vibration);
|
||||
assert!(
|
||||
engine.make(&bare, [0.0; 3], [0.0; 3], 1).is_none(),
|
||||
"no destination, no signal"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn particle_names_round_trip() {
|
||||
for &ty in ParticleType::ALL {
|
||||
|
||||
@@ -490,7 +490,7 @@ pub struct Globals {
|
||||
|
||||
impl Globals {
|
||||
/// The std140 block: `ivec3, float, vec3, float, vec2, int, int`.
|
||||
pub(crate) fn bytes(&self, size: [u32; 2], menu_blur_radius: i32) -> [u8; 48] {
|
||||
fn bytes(&self, size: [u32; 2], menu_blur_radius: i32) -> [u8; 48] {
|
||||
let mut out = Vec::with_capacity(48);
|
||||
for v in self.camera_block_pos {
|
||||
out.extend_from_slice(&v.to_le_bytes());
|
||||
|
||||
@@ -337,28 +337,23 @@ impl Settings {
|
||||
.parent()
|
||||
.map(crate::gamepad::PadSettings::load)
|
||||
.unwrap_or_default();
|
||||
let extras_text = path
|
||||
let extras = path
|
||||
.parent()
|
||||
.and_then(|dir| fs::read_to_string(dir.join(crate::extras::FILE)).ok())
|
||||
.map(crate::extras::Extras::load)
|
||||
.unwrap_or_default();
|
||||
let mut extras = crate::extras::Extras::parse(&extras_text);
|
||||
// The enabled mods are the game folder's, shared by its players (`--player-dir`).
|
||||
let mods = path
|
||||
.parent()
|
||||
.map(|dir| crate::mod_list::ModList::load(&crate::logging::shared_dir(dir)))
|
||||
.unwrap_or_default();
|
||||
let settings = match fs::read(path) {
|
||||
Ok(bytes) => {
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
extras.read_old_options(&extras_text, &text);
|
||||
Self {
|
||||
horizon,
|
||||
controller,
|
||||
extras,
|
||||
mods,
|
||||
..Self::parse(&text)
|
||||
}
|
||||
}
|
||||
Ok(bytes) => Self {
|
||||
horizon,
|
||||
controller,
|
||||
extras,
|
||||
mods,
|
||||
..Self::parse(&String::from_utf8_lossy(&bytes))
|
||||
},
|
||||
Err(_) => Self {
|
||||
horizon,
|
||||
controller,
|
||||
@@ -813,8 +808,6 @@ pub enum Opt {
|
||||
PadKeyboard,
|
||||
/// `options.modelPart.*`: one skin layer (index into `options_file::MODEL_PARTS`).
|
||||
ModelPart(usize),
|
||||
/// 3D skin layers: the player's outer layer as extruded slabs.
|
||||
SkinLayers3d,
|
||||
MainHand,
|
||||
AllowServerListing,
|
||||
RealmsNotifications,
|
||||
@@ -851,31 +844,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn three_d_skin_layers_move_from_options_txt_to_the_extras_file() {
|
||||
// #8091 wrote the switch into options.txt; a player who had it on keeps
|
||||
// it, and the next options.txt is vanilla's again.
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"horizon-skin-layers-{}-{:?}",
|
||||
std::process::id(),
|
||||
std::thread::current().id()
|
||||
));
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
fs::write(dir.join("options.txt"), "fov:0.5\nthreeDSkinLayers:true\n").unwrap();
|
||||
let s = Settings::load(&dir.join("options.txt"));
|
||||
assert!(s.extras.three_d_skin_layers);
|
||||
assert_eq!(s.fov, 90.0);
|
||||
assert!(!s.serialize().contains("threeDSkinLayers"));
|
||||
assert!(crate::extras::Extras::parse(&s.extras.serialize()).three_d_skin_layers);
|
||||
// Once horizon-extras.txt has its own line, a stale options.txt one is ignored.
|
||||
fs::write(dir.join(crate::extras::FILE), "threeDSkinLayers:false\n").unwrap();
|
||||
assert!(
|
||||
!Settings::load(&dir.join("options.txt"))
|
||||
.extras
|
||||
.three_d_skin_layers
|
||||
);
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
#[test]
|
||||
fn an_option_set_keeps_horizon_files() {
|
||||
let mut s = Settings::parse("fov:70\n");
|
||||
s.mods.enabled.insert("zoom".into());
|
||||
|
||||
@@ -814,17 +814,6 @@ pub struct Downloader {
|
||||
/// without end; the disk cache holds everything it drops.
|
||||
const MAX_SKIN_IMAGES: usize = 1024;
|
||||
impl Downloader {
|
||||
/// The decoded 64×64 RGBA image an atlas location holds, when it is
|
||||
/// still in memory (3D skin layers builds its geometry from it, and the
|
||||
/// disk cache backs whatever was dropped).
|
||||
pub fn image(&self, location: &str) -> Option<&[u8]> {
|
||||
self.images.get(location).map(Vec::as_slice)
|
||||
}
|
||||
/// Keeps a decoded image under `location` for the images read back later
|
||||
/// (a launcher's offline skin, which skips `queue`).
|
||||
pub fn remember_image(&mut self, location: &str, rgba: &[u8]) {
|
||||
self.remember(location.to_owned(), rgba.to_vec());
|
||||
}
|
||||
pub fn new(root: std::path::PathBuf) -> Self {
|
||||
let (send, done) = std::sync::mpsc::channel();
|
||||
Self {
|
||||
|
||||
@@ -18,15 +18,8 @@ thread_local! {
|
||||
static HERE: Cell<bool> = const { Cell::new(false) };
|
||||
/// How many timed spans the thread is inside: the next span's [`Span::depth`].
|
||||
static DEPTH: Cell<u8> = const { Cell::new(0) };
|
||||
/// What this thread's slowest steps were busy with since it last took them ([`note`]).
|
||||
static NOTES: RefCell<Vec<(f32, String)>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
/// The shortest step a [`note`] keeps, in milliseconds.
|
||||
pub const NOTE_MS: f32 = 10.0;
|
||||
/// The notes a thread keeps between two [`take_notes`]: its slowest steps.
|
||||
const NOTES_KEPT: usize = 3;
|
||||
|
||||
/// A named timing: wall milliseconds and the thread's CPU milliseconds in it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Span {
|
||||
@@ -90,36 +83,6 @@ pub fn time<T>(name: &'static str, f: impl FnOnce() -> T) -> T {
|
||||
});
|
||||
out
|
||||
}
|
||||
/// Notes that one step inside a span took `ms` milliseconds doing `what` (one player's
|
||||
/// packets, say), when this thread records spans and the step took at least [`NOTE_MS`]: a
|
||||
/// span says where a slow tick went, its notes whose doing it was. Only the slowest few are
|
||||
/// kept, and `what` is built only for a step that is.
|
||||
pub fn note(ms: f32, what: impl FnOnce() -> String) {
|
||||
if ms < NOTE_MS || !recording_here() {
|
||||
return;
|
||||
}
|
||||
NOTES.with(|n| {
|
||||
let mut n = n.borrow_mut();
|
||||
if n.len() >= NOTES_KEPT {
|
||||
let Some((i, _)) = n
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, (kept, _))| *kept < ms)
|
||||
.min_by(|a, b| a.1.0.total_cmp(&b.1.0))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
n.swap_remove(i);
|
||||
}
|
||||
n.push((ms, what()));
|
||||
});
|
||||
}
|
||||
/// This thread's notes ([`note`]), slowest first; clears them.
|
||||
pub fn take_notes() -> Vec<(f32, String)> {
|
||||
let mut notes = NOTES.with(|n| std::mem::take(&mut *n.borrow_mut()));
|
||||
notes.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||
notes
|
||||
}
|
||||
/// Consecutive steps of one function timed as spans: each [`Laps::lap`] records the time since
|
||||
/// the previous one (or since [`laps`]) under its name. Nothing is read while spans are off.
|
||||
pub struct Laps(Option<(Instant, Option<Duration>)>);
|
||||
@@ -441,41 +404,6 @@ pub mod startup {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Span;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
/// Spans on for every thread ([`super::enable`]) is process-wide: a test that turns them on
|
||||
/// would make another, run beside it, record what it expects left out. Those tests take
|
||||
/// this lock.
|
||||
fn spans_switch() -> MutexGuard<'static, ()> {
|
||||
static SWITCH: Mutex<()> = Mutex::new(());
|
||||
SWITCH.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// A slow tick's notes are its slowest steps, slowest first; short steps and steps on a
|
||||
/// thread that records no spans leave none.
|
||||
#[test]
|
||||
fn notes_keep_the_slowest_steps() {
|
||||
let _switch = spans_switch();
|
||||
super::record_here(false);
|
||||
super::note(500.0, || "unrecorded".into());
|
||||
assert!(super::take_notes().is_empty());
|
||||
super::record_here(true);
|
||||
for (ms, what) in [
|
||||
(12.0, "a"),
|
||||
(900.0, "b"),
|
||||
(3.0, "short"),
|
||||
(40.0, "c"),
|
||||
(11.0, "d"),
|
||||
(60.0, "e"),
|
||||
] {
|
||||
super::note(ms, || what.into());
|
||||
}
|
||||
let notes: Vec<(f32, String)> = super::take_notes();
|
||||
super::record_here(false);
|
||||
let names: Vec<&str> = notes.iter().map(|(_, w)| w.as_str()).collect();
|
||||
assert_eq!(names, ["b", "e", "c"]);
|
||||
assert!(super::take_notes().is_empty(), "taking them clears them");
|
||||
}
|
||||
|
||||
/// Spans of one name add up; a span the thread spent mostly waiting
|
||||
/// shows its CPU time after the wall time, short ones are left out.
|
||||
@@ -587,7 +515,6 @@ mod tests {
|
||||
/// Laps record the steps between them while spans are on, and nothing while off.
|
||||
#[test]
|
||||
fn laps_time_consecutive_steps() {
|
||||
let _switch = spans_switch();
|
||||
let _ = super::take();
|
||||
let mut off = super::laps();
|
||||
off.lap("unseen");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Splitscreen's profile page (Horizon's Splitscreen setting): a joining player picks a profile,
|
||||
//! or makes one, before entering P1's world, and a player in it switches to another from its
|
||||
//! pause menu (Splitscreen Settings), playing on until the new one can join. A profile is a name
|
||||
//! (instance-local offline names and the launcher's Microsoft accounts), with its own options. The
|
||||
//! (kept in the game folder), with its own options. The
|
||||
//! game fills the list when the page opens (`filled`), takes the choice, and says why a pick
|
||||
//! can't play (`refused`). Built from vanilla's widgets.
|
||||
use crate::{
|
||||
@@ -56,12 +56,10 @@ pub fn wrap(text: &str, max: f32, width: impl Fn(&str) -> f32) -> Vec<String> {
|
||||
pub struct SplitProfiles {
|
||||
/// The game filled the page since it opened.
|
||||
pub filled: bool,
|
||||
/// The profiles; those playing now (none can be picked twice); the signed-in Microsoft
|
||||
/// accounts among them; and the one this player plays as (switching; none while joining).
|
||||
/// The profiles; those playing now (none can be picked twice); and the one this player plays
|
||||
/// as (switching; none while joining).
|
||||
pub names: Vec<String>,
|
||||
pub taken: Vec<String>,
|
||||
pub accounts: Vec<String>,
|
||||
pub offline_accounts: Vec<String>,
|
||||
pub current: Option<String>,
|
||||
/// Why the last pick can't play (an online server, an expired sign-in), until the next.
|
||||
pub refused: Option<String>,
|
||||
@@ -129,7 +127,7 @@ impl SplitProfiles {
|
||||
}
|
||||
|
||||
fn editable(&self, name: &str) -> bool {
|
||||
!self.accounts.contains(&name.to_owned()) && !self.taken.contains(&name.to_owned())
|
||||
!self.taken.contains(&name.to_owned())
|
||||
}
|
||||
|
||||
fn buttons(&self, w: f32, h: f32) -> Vec<Button> {
|
||||
@@ -181,34 +179,24 @@ impl SplitProfiles {
|
||||
.skip(self.page() * SHOWN)
|
||||
.take(SHOWN)
|
||||
{
|
||||
let microsoft = self.accounts.contains(name);
|
||||
let mut label = name.clone();
|
||||
if microsoft {
|
||||
label.push_str(&if self.offline_accounts.contains(name) {
|
||||
tr("horizon.splitscreen.profiles.offline", " (Offline)")
|
||||
} else {
|
||||
tr("horizon.splitscreen.profiles.microsoft", " (Microsoft)")
|
||||
});
|
||||
}
|
||||
if self.current.as_ref() == Some(name) {
|
||||
label.push_str(&tr("horizon.splitscreen.profiles.you", " - you"));
|
||||
}
|
||||
let y = 44.0 + (i % SHOWN) as f32 * 24.0;
|
||||
buttons.push((
|
||||
[x, y, if microsoft { 200.0 } else { 176.0 }, 20.0],
|
||||
[x, y, 176.0, 20.0],
|
||||
label,
|
||||
!self.taken.contains(name),
|
||||
Target::Profile(i),
|
||||
));
|
||||
if !microsoft {
|
||||
buttons.push((
|
||||
[x + 180.0, y, 20.0, 20.0],
|
||||
tr("horizon.splitscreen.profiles.forget_named", "Forget %s")
|
||||
.replacen("%s", name, 1),
|
||||
self.editable(name),
|
||||
Target::Remove(i),
|
||||
));
|
||||
}
|
||||
buttons.push((
|
||||
[x + 180.0, y, 20.0, 20.0],
|
||||
tr("horizon.splitscreen.profiles.forget_named", "Forget %s")
|
||||
.replacen("%s", name, 1),
|
||||
self.editable(name),
|
||||
Target::Remove(i),
|
||||
));
|
||||
}
|
||||
let mut y = 52.0 + self.names.len().min(SHOWN) as f32 * 24.0;
|
||||
if self.names.len() > SHOWN {
|
||||
@@ -334,19 +322,6 @@ impl SplitProfiles {
|
||||
for ([x, y, bw, bh], label, enabled, target) in &buttons {
|
||||
let hover = ui::hovered(cursor, *x, *y, *bw, *bh);
|
||||
let remove = matches!(target, Target::Remove(_));
|
||||
if hover && let Target::Profile(i) = target {
|
||||
let name = &self.names[*i];
|
||||
if self.accounts.contains(name) {
|
||||
tip = Some(if self.offline_accounts.contains(name) {
|
||||
tr(
|
||||
"horizon.splitscreen.profiles.sign_in",
|
||||
"Sign in again in the launcher",
|
||||
)
|
||||
} else {
|
||||
tr("horizon.splitscreen.profiles.online", "Microsoft online")
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut text = Text::literal(if remove { "" } else { label });
|
||||
if matches!(target, Target::Forget) {
|
||||
text = text.with_color(0xFF5555);
|
||||
@@ -609,31 +584,14 @@ mod tests {
|
||||
names: (0..11).map(|i| format!("Player{i}")).collect(),
|
||||
..Default::default()
|
||||
};
|
||||
p.accounts = vec!["Player10".into()];
|
||||
press(&mut p, &atlas, "Next");
|
||||
press(&mut p, &atlas, "Next");
|
||||
press(&mut p, &atlas, "Player10 (Microsoft)");
|
||||
press(&mut p, &atlas, "Player10");
|
||||
assert_eq!(p.take_choice(), Some(Some("Player10".into())));
|
||||
press(&mut p, &atlas, "Previous");
|
||||
assert!(p.buttons(427.0, 240.0).iter().any(|b| b.1 == "Player4"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_accounts_are_marked_and_cannot_be_forgotten() {
|
||||
let mut p = SplitProfiles {
|
||||
names: vec!["Cal".into()],
|
||||
accounts: vec!["Cal".into()],
|
||||
offline_accounts: vec!["Cal".into()],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!p.editable("Cal"));
|
||||
let buttons = p.buttons(427.0, 240.0);
|
||||
assert!(buttons.iter().any(|b| b.1 == "Cal (Offline)" && b.2));
|
||||
assert!(!buttons.iter().any(|b| b.1 == "Forget Cal"));
|
||||
press(&mut p, &atlas(), "Cal (Offline)");
|
||||
assert_eq!(p.take_choice(), Some(Some("Cal".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_names_are_player_names() {
|
||||
assert!(valid_name("Steve_2"));
|
||||
|
||||
@@ -62,15 +62,6 @@ impl SpriteAnimation {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
struct StillKey {
|
||||
frame: u64,
|
||||
pbr: u64,
|
||||
side: u32,
|
||||
settings: [u32; 2],
|
||||
flags: [bool; 4],
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Sprites {
|
||||
/// Edge length of every array layer and the layers' current frames, set
|
||||
@@ -99,7 +90,7 @@ pub struct Sprites {
|
||||
/// Still textures by their pixels, their LabPBR maps and mip settings: mods copy vanilla's
|
||||
/// textures under their own names, and one layer serves them all (the GPU's array may
|
||||
/// hold as few as 2048).
|
||||
still: std::collections::HashMap<StillKey, u32>,
|
||||
still: std::collections::HashMap<(Vec<u8>, u64, u32, [u32; 2], [bool; 4]), u32>,
|
||||
/// Decoded textures kept on disk between launches ([`Sprites::with_disk_cache`]).
|
||||
disk: Option<crate::sprite_cache::SpriteCache>,
|
||||
/// Textures [`Sprites::prefetch`] decoded ahead of their insert, by sprite name or
|
||||
@@ -656,20 +647,18 @@ impl Sprites {
|
||||
};
|
||||
let still = (animation.is_none() && frames.len() == 1).then(|| {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut frame = std::collections::hash_map::DefaultHasher::new();
|
||||
frames[0].hash(&mut frame);
|
||||
let mut maps = std::collections::hash_map::DefaultHasher::new();
|
||||
pbr.hash(&mut maps);
|
||||
StillKey {
|
||||
frame: frame.finish(),
|
||||
pbr: maps.finish(),
|
||||
(
|
||||
frames[0].clone(),
|
||||
maps.finish(),
|
||||
side,
|
||||
settings: [
|
||||
[
|
||||
settings.strategy as u32,
|
||||
settings.alpha_cutoff_bias.to_bits(),
|
||||
],
|
||||
flags: [settings.has_transparent, settings.item, opaque, translucent],
|
||||
}
|
||||
[settings.has_transparent, settings.item, opaque, translucent],
|
||||
)
|
||||
});
|
||||
if let Some(&same) = still.as_ref().and_then(|k| self.still.get(k)) {
|
||||
self.layers.insert(name.to_owned(), same);
|
||||
|
||||
@@ -1224,14 +1224,6 @@ impl Streaming {
|
||||
world.layout().sections() as i32,
|
||||
);
|
||||
}
|
||||
// `ChunkTaskPriorityQueue.release` for each of them (`ChunkTaskDispatcher`'s
|
||||
// `release(col, true)` from the chunk's unload): the jobs still queued for a
|
||||
// column go with it instead of running for a level they no longer belong to.
|
||||
if !gone.is_empty()
|
||||
&& let Ok(mut queue) = self.shared.queue.lock()
|
||||
{
|
||||
release_columns(&mut queue, gone);
|
||||
}
|
||||
let mut chunks = Vec::with_capacity(gone.len());
|
||||
for &(x, z) in gone {
|
||||
self.lit.remove(&(x, z));
|
||||
@@ -2887,25 +2879,6 @@ fn rank_again(queue: &mut BinaryHeap<Queued>, distance: impl Fn(i32, i32) -> i64
|
||||
}
|
||||
*queue = BinaryHeap::from(jobs);
|
||||
}
|
||||
/// `ChunkTaskPriorityQueue.release(pos, clearQueue=true)`: the jobs queued for the
|
||||
/// `gone` columns are dropped, so a view that moved or shrank does not wait behind the
|
||||
/// generation of the chunks it left. A job without a column (a save, meshes to free)
|
||||
/// stays, as vanilla keeps the tasks of a chunk that is still loaded. Returns how many
|
||||
/// jobs went.
|
||||
fn release_columns(queue: &mut BinaryHeap<Queued>, gone: &FxHashSet<(i32, i32)>) -> usize {
|
||||
if queue.is_empty() || gone.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let queued = std::mem::take(queue).into_vec();
|
||||
let before = queued.len();
|
||||
let kept: Vec<Queued> = queued
|
||||
.into_iter()
|
||||
.filter(|job| !job.at.is_some_and(|at| gone.contains(&at)))
|
||||
.collect();
|
||||
let released = before - kept.len();
|
||||
*queue = BinaryHeap::from(kept);
|
||||
released
|
||||
}
|
||||
/// Whether the 3x3 a light job for (x, z) reads is all there: every
|
||||
/// neighbour a finished column, or a decorated proto chunk past the area
|
||||
/// (the last FULL ring lights against neighbours at FEATURES, as
|
||||
@@ -3337,59 +3310,6 @@ mod tests {
|
||||
assert_eq!(queue.pop().and_then(|q| q.at), Some((20, 0)));
|
||||
}
|
||||
|
||||
/// `ChunkTaskPriorityQueue.release`: a column that unloaded keeps no queued job, so
|
||||
/// a teleport or a smaller render distance does not spend its workers on the world
|
||||
/// it left; the other columns' jobs and the jobs without a column (saves, meshes to
|
||||
/// free) stay, in the same order.
|
||||
#[test]
|
||||
fn a_released_column_keeps_no_queued_job() {
|
||||
let generate = |x, z, priority, order| Queued {
|
||||
priority,
|
||||
at: Some((x, z)),
|
||||
bias: 0,
|
||||
order,
|
||||
job: Job::Generate(x, z),
|
||||
};
|
||||
let save = |order| Queued {
|
||||
priority: i64::MIN,
|
||||
at: None,
|
||||
bias: 0,
|
||||
order,
|
||||
job: Job::Save(Vec::new()),
|
||||
};
|
||||
let queued = || {
|
||||
BinaryHeap::from(vec![
|
||||
generate(0, 0, 0, 1),
|
||||
generate(0, 0, 4, 2),
|
||||
generate(1, 0, 8, 3),
|
||||
generate(9, 9, 12, 4),
|
||||
save(5),
|
||||
])
|
||||
};
|
||||
let pops = |mut queue: BinaryHeap<Queued>| {
|
||||
std::iter::from_fn(move || queue.pop())
|
||||
.map(|q| q.at)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
// Nothing goes for a column that is still loaded, or for nothing at all.
|
||||
let mut queue = queued();
|
||||
assert_eq!(release_columns(&mut queue, &FxHashSet::default()), 0);
|
||||
assert_eq!(
|
||||
release_columns(&mut queue, &[(20, 20)].into_iter().collect()),
|
||||
0
|
||||
);
|
||||
assert_eq!(queue.len(), 5);
|
||||
// Both of (0, 0)'s jobs go, its neighbour's and the save stay.
|
||||
let gone: FxHashSet<(i32, i32)> = [(0, 0)].into_iter().collect();
|
||||
assert_eq!(release_columns(&mut queue, &gone), 2);
|
||||
assert_eq!(pops(queue), [None, Some((1, 0)), Some((9, 9))]);
|
||||
// Every column leaving at once: only the jobs without one are left.
|
||||
let mut queue = queued();
|
||||
let all: FxHashSet<(i32, i32)> = [(0, 0), (1, 0), (9, 9)].into_iter().collect();
|
||||
assert_eq!(release_columns(&mut queue, &all), 4);
|
||||
assert_eq!(pops(queue), [None]);
|
||||
}
|
||||
|
||||
/// The light and mesh stages submit bounded work, the nearest first:
|
||||
/// what exceeds the room stays dirty, to be chosen again — from the
|
||||
/// current centre — as capacity frees. Submitted whole, the queue once
|
||||
|
||||
@@ -32,14 +32,13 @@ struct Globals {
|
||||
// triangle's volume with the camera; then each rim vertex's normal against the apex.
|
||||
sunrise_fan: array<vec4<f32>, 32>,
|
||||
// x: Options.textureFiltering (0 none, 1 RGSS, 2 anisotropic), y: the sprite arrays' width
|
||||
// in texels (vanilla's TextureSize), z: the mip level at which a sprite is one texel when
|
||||
// the views reach it, else 0 (`rotated_grid`).
|
||||
// in texels (vanilla's TextureSize).
|
||||
filtering: vec4<f32>,
|
||||
// HDR output's tier windows (hdr.rs `Hdr::windows`): x..w low to very bright,
|
||||
// [0] where the lift starts, [1] where it is full.
|
||||
hdr_windows: array<vec4<f32>, 2>,
|
||||
// HDR lighting: x how strongly the sky lights the world now (`Sky::sky_light_factor`),
|
||||
// y what the sky and fog get: the same, but none under water; z 1 with HDR output on.
|
||||
// y what the sky and fog get: the same, but none under water.
|
||||
hdr_light: vec4<f32>,
|
||||
}
|
||||
@group(0) @binding(0) var<uniform> globals: Globals;
|
||||
@@ -125,34 +124,12 @@ fn sample_rgss(uv: vec2<f32>, layer: u32, pixel: vec2<f32>, du: vec2<f32>, dv: v
|
||||
let max_texel = max(texel_screen.x, texel_screen.y);
|
||||
let min_pixel = min(pixel.x, pixel.y);
|
||||
let blend = smoothstep(min_pixel, min_pixel * 2.0, max_texel);
|
||||
// Only the side of the mix that has weight is sampled: near texels (blend 0) take
|
||||
// sample_nearest's one tap alone, far ones (blend 1) the rotated grid alone.
|
||||
if blend <= 0.0 {
|
||||
return sample_nearest(uv, layer, pixel, du, dv, texel_screen, tiled);
|
||||
}
|
||||
let rgss = rotated_grid(uv, layer, pixel, du, dv);
|
||||
if blend >= 1.0 {
|
||||
return rgss;
|
||||
}
|
||||
return mix(sample_nearest(uv, layer, pixel, du, dv, texel_screen, tiled), rgss, blend);
|
||||
}
|
||||
fn rotated_grid(uv: vec2<f32>, layer: u32, pixel: vec2<f32>, du: vec2<f32>,
|
||||
dv: vec2<f32>) -> vec4<f32> {
|
||||
let min_pixel = min(pixel.x, pixel.y);
|
||||
let du_len = length(du);
|
||||
let dv_len = length(dv);
|
||||
let effective = sqrt(min(du_len, dv_len) * max(du_len, dv_len));
|
||||
let exact = max(0.0, log2(effective / min_pixel));
|
||||
let low = floor(exact);
|
||||
let mip_blend = fract(exact);
|
||||
// From this level on the sprite is a single texel (0: the views stop before one is), and
|
||||
// the sampler clamps any level past it to it: there every tap reads the same value, so
|
||||
// one tap gives the four's mean. Distant tiles sit there.
|
||||
let single = globals.filtering.z;
|
||||
if single > 0.0 && low >= single {
|
||||
return sprite_level(uv, layer, low);
|
||||
}
|
||||
let high_single = single > 0.0 && low + 1.0 >= single;
|
||||
var offsets = array<vec2<f32>, 4>(
|
||||
vec2(0.125, 0.375), vec2(-0.125, -0.375), vec2(0.375, -0.125), vec2(-0.375, 0.125));
|
||||
var sum_low = vec4(0.0);
|
||||
@@ -160,14 +137,10 @@ fn rotated_grid(uv: vec2<f32>, layer: u32, pixel: vec2<f32>, du: vec2<f32>,
|
||||
for (var i = 0; i < 4; i++) {
|
||||
let p = uv + offsets[i] * pixel;
|
||||
sum_low += sprite_level(p, layer, low);
|
||||
if !high_single {
|
||||
sum_high += sprite_level(p, layer, low + 1.0);
|
||||
}
|
||||
sum_high += sprite_level(p, layer, low + 1.0);
|
||||
}
|
||||
if high_single {
|
||||
sum_high = 4.0 * sprite_level(uv, layer, low + 1.0);
|
||||
}
|
||||
return mix(sum_low * 0.25, sum_high * 0.25, mip_blend);
|
||||
let rgss = mix(sum_low * 0.25, sum_high * 0.25, mip_blend);
|
||||
return mix(sample_nearest(uv, layer, pixel, du, dv, texel_screen, tiled), rgss, blend);
|
||||
}
|
||||
// Distant terrain tiles (lod/mesh.rs) count their UVs in blocks past 1: each block takes the
|
||||
// sprite again, as separate faces would under the clamped sampler. Sprite UVs in [0, 1] stay.
|
||||
@@ -175,13 +148,10 @@ fn tile_uv(uv: vec2<f32>) -> vec2<f32> {
|
||||
return uv - max(ceil(uv) - 1.0, vec2(0.0));
|
||||
}
|
||||
fn sprite(tiled: vec2<f32>, layer: u32) -> vec4<f32> {
|
||||
// The gradients come from the unwrapped UV, so the mip level holds across block seams.
|
||||
return sprite_at(tiled, layer, dpdx(tiled), dpdy(tiled));
|
||||
}
|
||||
// `sprite` with the unwrapped UV's screen gradients given: the visibility buffer's resolve
|
||||
// (visbuffer.wgsl) works them out from the quad, as its neighbours may be other quads.
|
||||
fn sprite_at(tiled: vec2<f32>, layer: u32, du: vec2<f32>, dv: vec2<f32>) -> vec4<f32> {
|
||||
// terrain.fsh's two sampling paths: sampleRGSS for RGSS, sampleNearest otherwise.
|
||||
// The gradients come from the unwrapped UV, so the mip level holds across block seams.
|
||||
let du = dpdx(tiled);
|
||||
let dv = dpdy(tiled);
|
||||
let uv = tile_uv(tiled);
|
||||
// The wrap shifts a face's UVs by whole blocks, so half of one tells a tile's face from a
|
||||
// block's (whose UVs stay inside its sprite).
|
||||
@@ -350,10 +320,8 @@ fn fade_code(w: f32) -> u32 {
|
||||
// (`mesher::Part`): first the quads with no cutout that are not fading, through
|
||||
// `fs_opaque`, which never discards, so a tiled GPU (Apple's) keeps hiding the pixels
|
||||
// behind them; then the rest through `fs_solid`. One discard anywhere in a pipeline's shader turned that hiding off for
|
||||
// all the terrain it drew. The draw lists give each pass only its own part; a mesh that
|
||||
// was not sorted (`Sides::unsorted`) gives both passes all of its quads, so each pass
|
||||
// still leaves out the other's: decided before any of the vertex's work, outside the
|
||||
// clip volume, it makes no pixel.
|
||||
// all the terrain it drew. A quad the pass leaves out is decided before any of the
|
||||
// vertex's work and lies outside the clip volume: it makes no pixel.
|
||||
fn opaque_quad(layer: u32, w: f32) -> bool {
|
||||
return (layer & 0x8000u) != 0u && fade_code(w) == 0u;
|
||||
}
|
||||
@@ -398,134 +366,6 @@ fn left_out() -> TerrainOut {
|
||||
return terrain_vertex(position, terrain_uv(uv, origin.w), layer, color, light, origin.xyz,
|
||||
origin.w);
|
||||
}
|
||||
// Solid section draws that read their vertices from the arena page (gfx.rs's `pulled`
|
||||
// pipelines; `mesher/packed.rs`): a draw's vertex indices below PACKED_BASE name vertices
|
||||
// (`TerrainVertex`, six words, as `TERRAIN_ATTRIBUTES` reads them), from PACKED_BASE on a
|
||||
// corner of the quad whose record is at that slot (`PACKED_BASE + 4 * slot + corner`).
|
||||
// Either way they come out as the six words of the vertex, so the work after is the same.
|
||||
// Binding 19: visbuffer.wgsl, in the same module, takes 2 to 18 of its own group 1.
|
||||
@group(1) @binding(19) var<storage, read> arena_page: array<vec2<u32>>;
|
||||
const PACKED_BASE: u32 = 0x10000000u;
|
||||
// A vertex's words: position bits, then UV, layer with light, colour.
|
||||
struct ArenaCorner {
|
||||
p: vec3<u32>,
|
||||
r: vec3<u32>,
|
||||
}
|
||||
fn arena_vertex(v: u32) -> ArenaCorner {
|
||||
let a = arena_page[v * 3u];
|
||||
let b = arena_page[v * 3u + 1u];
|
||||
let c = arena_page[v * 3u + 2u];
|
||||
return ArenaCorner(vec3(a.x, a.y, b.x), vec3(b.y, c.x, c.y));
|
||||
}
|
||||
// `packed::packed_vertex`: corner `corner` of the record whose words are lo (0 to 2) and
|
||||
// hi (3 to 5).
|
||||
fn packed_corner(lo: vec3<u32>, hi: vec3<u32>, corner: u32) -> ArenaCorner {
|
||||
let orient = (hi.z >> 12u) & 0x3ffffu;
|
||||
let block = vec3(orient & 15u, (orient >> 4u) & 15u, (orient >> 8u) & 15u);
|
||||
let axis = (orient >> 12u) & 3u;
|
||||
let side = (orient >> 14u) & 1u;
|
||||
let start = (orient >> 15u) & 3u;
|
||||
let dir = (orient >> 17u) & 1u;
|
||||
let step = (start + select(corner, 4u - corner, dir == 1u)) & 3u;
|
||||
let du = select(0u, 1u, step == 1u || step == 2u);
|
||||
let dw = select(0u, 1u, step >= 2u);
|
||||
var pos = block;
|
||||
pos[axis] = block[axis] + side;
|
||||
pos[(axis + 1u) % 3u] = block[(axis + 1u) % 3u] + du;
|
||||
pos[(axis + 2u) % 3u] = block[(axis + 2u) % 3u] + dw;
|
||||
let sel = lo.z >> 24u;
|
||||
let us = (sel >> (2u * corner)) & 1u;
|
||||
let vs = (sel >> (2u * corner + 1u)) & 1u;
|
||||
let u = (lo.z >> (6u * us)) & 63u;
|
||||
let v = (lo.z >> (12u + 6u * vs)) & 63u;
|
||||
var light = hi.z;
|
||||
if corner == 0u {
|
||||
light = hi.x >> 16u;
|
||||
} else if corner == 1u {
|
||||
light = hi.y;
|
||||
} else if corner == 2u {
|
||||
light = hi.y >> 12u;
|
||||
}
|
||||
light &= 0xfffu;
|
||||
let packed_light = ((light & 63u) << 2u) | (((light >> 6u) & 63u) << 10u);
|
||||
let g = (lo.y >> (8u * corner)) & 255u;
|
||||
let t = lo.x;
|
||||
let color = (g * ((t >> 16u) & 255u) / 255u) | ((g * ((t >> 8u) & 255u) / 255u) << 8u)
|
||||
| ((g * (t & 255u) / 255u) << 16u) | (t & 0xff000000u);
|
||||
return ArenaCorner(bitcast<vec3<u32>>(vec3<f32>(pos)),
|
||||
vec3((u << 10u) | (v << 26u), (hi.x & 0xffffu) | (packed_light << 16u), color));
|
||||
}
|
||||
// A solid draw's vertex `vertex`, and the slot its block is found by (`block_slots`, 256
|
||||
// slots a block): the vertex's own, or its quad's record's. `packed`: it has a record.
|
||||
struct SectionCorner {
|
||||
c: ArenaCorner,
|
||||
slot: u32,
|
||||
packed: bool,
|
||||
}
|
||||
fn section_corner(vertex: u32) -> SectionCorner {
|
||||
if vertex < PACKED_BASE {
|
||||
return SectionCorner(arena_vertex(vertex), vertex, false);
|
||||
}
|
||||
let slot = (vertex - PACKED_BASE) >> 2u;
|
||||
let corner = vertex & 3u;
|
||||
let a = arena_page[slot * 3u];
|
||||
let b = arena_page[slot * 3u + 1u];
|
||||
let c = arena_page[slot * 3u + 2u];
|
||||
// `packed::FULL`: the quad's four vertices, word 0 slots on.
|
||||
if (c.y & 0x80000000u) != 0u {
|
||||
return SectionCorner(arena_vertex(slot + a.x + corner), slot, true);
|
||||
}
|
||||
return SectionCorner(packed_corner(vec3(a.x, a.y, b.x), vec3(b.y, c.x, c.y), corner), slot,
|
||||
true);
|
||||
}
|
||||
// The colour as the vertex fetch converts a Unorm8x4 attribute, bit for bit (Apple's M1:
|
||||
// times 1/255; `unpack4x8unorm` and the correctly rounded c / 255 each differ from it for
|
||||
// some bytes, and moved a few pixels' colour or alpha cutout).
|
||||
fn unorm8x4(c: u32) -> vec4<f32> {
|
||||
return vec4<f32>(vec4(c & 255u, (c >> 8u) & 255u, (c >> 16u) & 255u, c >> 24u))
|
||||
* (1.0 / 255.0);
|
||||
}
|
||||
fn corner_layer(c: ArenaCorner) -> u32 {
|
||||
return c.r.y & 0xffffu;
|
||||
}
|
||||
fn corner_vertex(c: ArenaCorner, origin: vec4<f32>) -> TerrainOut {
|
||||
let uv = vec2(c.r.x & 0xffffu, c.r.x >> 16u);
|
||||
let light = vec2((c.r.y >> 16u) & 0xffu, c.r.y >> 24u);
|
||||
return terrain_vertex(bitcast<vec3<f32>>(c.p), terrain_uv(uv, origin.w), corner_layer(c),
|
||||
unorm8x4(c.r.z), light, origin.xyz, origin.w);
|
||||
}
|
||||
@vertex fn vs_section(@builtin(vertex_index) vertex: u32,
|
||||
@location(5) origin: vec4<f32>) -> TerrainOut {
|
||||
return corner_vertex(section_corner(vertex).c, origin);
|
||||
}
|
||||
@vertex fn vs_section_opaque(@builtin(vertex_index) vertex: u32,
|
||||
@location(5) origin: vec4<f32>) -> TerrainOut {
|
||||
let s = section_corner(vertex);
|
||||
if !opaque_quad(corner_layer(s.c), origin.w) { return left_out(); }
|
||||
return corner_vertex(s.c, origin);
|
||||
}
|
||||
@vertex fn vs_section_cutout(@builtin(vertex_index) vertex: u32,
|
||||
@location(5) origin: vec4<f32>) -> TerrainOut {
|
||||
let s = section_corner(vertex);
|
||||
if opaque_quad(corner_layer(s.c), origin.w) { return left_out(); }
|
||||
return corner_vertex(s.c, origin);
|
||||
}
|
||||
@vertex fn vs_section_flat(@builtin(vertex_index) vertex: u32) -> TerrainOut {
|
||||
let s = section_corner(vertex);
|
||||
return corner_vertex(s.c, slot_origins[block_slots[s.slot >> 8u]]);
|
||||
}
|
||||
@vertex fn vs_section_flat_opaque(@builtin(vertex_index) vertex: u32) -> TerrainOut {
|
||||
let s = section_corner(vertex);
|
||||
let origin = slot_origins[block_slots[s.slot >> 8u]];
|
||||
if !opaque_quad(corner_layer(s.c), origin.w) { return left_out(); }
|
||||
return corner_vertex(s.c, origin);
|
||||
}
|
||||
@vertex fn vs_section_flat_cutout(@builtin(vertex_index) vertex: u32) -> TerrainOut {
|
||||
let s = section_corner(vertex);
|
||||
let origin = slot_origins[block_slots[s.slot >> 8u]];
|
||||
if opaque_quad(corner_layer(s.c), origin.w) { return left_out(); }
|
||||
return corner_vertex(s.c, origin);
|
||||
}
|
||||
// HDR lighting: how far a surface's light carries it above paper white, 0 to 1. Only the
|
||||
// strongest light does, so dark stays as dark as in SDR; sky light by how the face meets the sun.
|
||||
fn hdr_scene(block: f32, sky: f32, facing: f32) -> f32 {
|
||||
@@ -608,9 +448,7 @@ fn hdr_mark(tier: u32, y: f32) -> f32 {
|
||||
}
|
||||
// `fs_solid` for the opaque pass (`opaque_quad`): no fade, no cutout, no discard.
|
||||
@fragment fn fs_opaque(in: TerrainOut) -> @location(0) vec4<f32> {
|
||||
if globals.hdr_light.z != 0.0 {
|
||||
hdr_normal = normalize(cross(dpdy(in.local), dpdx(in.local)));
|
||||
}
|
||||
hdr_normal = normalize(cross(dpdy(in.local), dpdx(in.local)));
|
||||
let texel = sprite(in.uv, in.layer & 0x7fffu);
|
||||
var color = texel * in.color;
|
||||
color = mix(globals.fog_color * vec4(1.0, 1.0, 1.0, color.a), color, in.visibility);
|
||||
@@ -625,9 +463,7 @@ fn hdr_mark(tier: u32, y: f32) -> f32 {
|
||||
fn solid_color(in: TerrainOut) -> vec4<f32> {
|
||||
let dx = dpdx(in.local);
|
||||
let dy = dpdy(in.local);
|
||||
if globals.hdr_light.z != 0.0 {
|
||||
hdr_normal = normalize(cross(dy, dx));
|
||||
}
|
||||
hdr_normal = normalize(cross(dy, dx));
|
||||
if faded(in.fade, in.local, dx, dy) { discard; }
|
||||
// Bit 15 of the layer marks opaque leaves (forceOpaque): no alpha cutout.
|
||||
let texel = sprite(in.uv, in.layer & 0x7fffu);
|
||||
@@ -639,11 +475,6 @@ fn solid_color(in: TerrainOut) -> vec4<f32> {
|
||||
}
|
||||
// The rest of a solid pixel's colour once it is kept: emission and fog.
|
||||
fn solid_finish(in: TerrainOut, texel: vec4<f32>, color: vec4<f32>) -> vec4<f32> {
|
||||
return solid_finish_at(in, texel, color, dpdx(in.uv), dpdy(in.uv));
|
||||
}
|
||||
// `solid_finish` with the UV's screen gradients given (`sprite_at`).
|
||||
fn solid_finish_at(in: TerrainOut, texel: vec4<f32>, color: vec4<f32>, du: vec2<f32>,
|
||||
dv: vec2<f32>) -> vec4<f32> {
|
||||
// Below 0.75, alpha tells HDR output (hdr.wgsl) a light source's pixel
|
||||
// shines, at which tier (mesher::hdr_tier) and how much (hdr_mark), by the
|
||||
// texel's own brightness, so its light level and face shading don't change
|
||||
@@ -651,31 +482,27 @@ fn solid_finish_at(in: TerrainOut, texel: vec4<f32>, color: vec4<f32>, du: vec2<
|
||||
// parts, or all of it. Nothing else reads the frame's alpha.
|
||||
// Fog turns a far surface into the fog's colour: it takes the fog's light with it, or a
|
||||
// fully fogged mountain would stand out from the fog behind it.
|
||||
// Without HDR output nothing reads the alpha, so its lighting is not worked out.
|
||||
var emissive = 1.0;
|
||||
if globals.hdr_light.z != 0.0 {
|
||||
let fogged = max(linear_fog(in.spherical, globals.fog.x, globals.fog.y),
|
||||
linear_fog(in.cylindrical, globals.fog.z, globals.fog.w)) * globals.fog_color.a;
|
||||
emissive = hdr_scene_alpha(mix(hdr_scene(in.scene.x, in.scene.y, hdr_facing()),
|
||||
globals.hdr_light.y, fogged));
|
||||
if in.emission > 0u {
|
||||
// A light source's own face shines as one or stays as drawn, without the lighting.
|
||||
emissive = 1.0;
|
||||
let whole = in.emission > 4u;
|
||||
let tier = select(in.emission, in.emission - 4u, whole);
|
||||
let l = in.layer & 0x7fffu;
|
||||
let t = textureLoad(emission_thresholds, vec2(i32(l % SPRITE_SPLIT), i32(l / SPRITE_SPLIT)), 0);
|
||||
let y = brightness(texel.rgb);
|
||||
// All over means all over: the sprite's range is one frame's, and an animated sprite's
|
||||
// other frames dip below it. Fog hides a light source as it hides everything else.
|
||||
if whole || y >= t.r {
|
||||
emissive = hdr_mark(tier, y * (1.0 - fogged));
|
||||
}
|
||||
let fogged = max(linear_fog(in.spherical, globals.fog.x, globals.fog.y),
|
||||
linear_fog(in.cylindrical, globals.fog.z, globals.fog.w)) * globals.fog_color.a;
|
||||
var emissive = hdr_scene_alpha(mix(hdr_scene(in.scene.x, in.scene.y, hdr_facing()),
|
||||
globals.hdr_light.y, fogged));
|
||||
if in.emission > 0u {
|
||||
// A light source's own face shines as one or stays as drawn, without the lighting.
|
||||
emissive = 1.0;
|
||||
let whole = in.emission > 4u;
|
||||
let tier = select(in.emission, in.emission - 4u, whole);
|
||||
let l = in.layer & 0x7fffu;
|
||||
let t = textureLoad(emission_thresholds, vec2(i32(l % SPRITE_SPLIT), i32(l / SPRITE_SPLIT)), 0);
|
||||
let y = brightness(texel.rgb);
|
||||
// All over means all over: the sprite's range is one frame's, and an animated sprite's
|
||||
// other frames dip below it. Fog hides a light source as it hides everything else.
|
||||
if whole || y >= t.r {
|
||||
emissive = hdr_mark(tier, y * (1.0 - fogged));
|
||||
}
|
||||
}
|
||||
if in.emissive_layer != 0xffffffffu {
|
||||
// The high bit: an OptiFine overlay, drawn cutout (opaque where alpha >= 0.1).
|
||||
var mask = sprite_at(in.uv, in.emissive_layer & 0x7fffffffu, du, dv);
|
||||
var mask = sprite(in.uv, in.emissive_layer & 0x7fffffffu);
|
||||
if (in.emissive_layer & 0x80000000u) != 0u {
|
||||
mask.a = select(0.0, 1.0, mask.a >= 0.1);
|
||||
}
|
||||
|
||||
@@ -1,427 +0,0 @@
|
||||
|
||||
// The visibility buffer for solid terrain (gfx/visbuffer.rs, `HORIZON_VISBUFFER=1`), after
|
||||
// terrain.wgsl in one module. Pass 1 rasterises the flat solid draws writing depth and, per
|
||||
// pixel, the quad it shows: its arena page and its quad in that page (`vis_id`). Pass 2, one draw for up to 16 pages, runs the shading of `fs_opaque`/`solid_color`
|
||||
// once per pixel from that ID: it reads the quad's corners from its page as the vertex shaders
|
||||
// do, runs `terrain_vertex` on the
|
||||
// triangle the pixel falls in, interpolates its outputs with perspective-correct barycentrics
|
||||
// and works out the screen gradients `dpdx`/`dpdy` would give, so the texel, mip level, light
|
||||
// and fog come out as in the normal path.
|
||||
|
||||
// Bit 31: terrain is here; bit 30: the quad has a record (`mesher/packed.rs`); bits 24 to 29:
|
||||
// the arena page (`visbuffer::MAX_PAGES`); bits 0 to 23: the quad's record's slot in the page,
|
||||
// else its first vertex over 4 (a page holds at most `arena::PAGE_BYTES` / 24 bytes =
|
||||
// 11 184 810 slots, under 2^24).
|
||||
fn vis_id(page: u32, s: SectionCorner) -> u32 {
|
||||
let at = 0x80000000u | ((page & 0x3fu) << 24u);
|
||||
if s.packed {
|
||||
return at | 0x40000000u | s.slot;
|
||||
}
|
||||
return at | (s.slot >> 2u);
|
||||
}
|
||||
|
||||
// `sprite_at` (terrain.wgsl's `sample_rgss` over `sample_nearest`), sampling only what its
|
||||
// blends keep: where RGSS has not faded in (the texels span two pixels or more) the nearest
|
||||
// texel alone, where it has fully, RGSS alone, and its upper mip level only where it blends one
|
||||
// in. A blend of 0 or 1 gives exactly the one side, so the result is `sprite_at`'s; near
|
||||
// faces take one tap instead of nine.
|
||||
fn vis_sprite(tiled: vec2<f32>, layer: u32, du: vec2<f32>, dv: vec2<f32>) -> vec4<f32> {
|
||||
let uv = tile_uv(tiled);
|
||||
let tiled_face = any(tiled - uv >= vec2(0.5));
|
||||
let pixel = vec2(1.0 / globals.filtering.y);
|
||||
let texel_screen = sqrt(du * du + dv * dv);
|
||||
if u32(globals.filtering.x) != 1u {
|
||||
return sample_nearest(uv, layer, pixel, du, dv, texel_screen, tiled_face);
|
||||
}
|
||||
let max_texel = max(texel_screen.x, texel_screen.y);
|
||||
let min_pixel = min(pixel.x, pixel.y);
|
||||
let blend = smoothstep(min_pixel, min_pixel * 2.0, max_texel);
|
||||
var nearest = vec4(0.0);
|
||||
if blend < 1.0 {
|
||||
nearest = sample_nearest(uv, layer, pixel, du, dv, texel_screen, tiled_face);
|
||||
}
|
||||
if blend <= 0.0 {
|
||||
return nearest;
|
||||
}
|
||||
let du_len = length(du);
|
||||
let dv_len = length(dv);
|
||||
let effective = sqrt(min(du_len, dv_len) * max(du_len, dv_len));
|
||||
let exact = max(0.0, log2(effective / min_pixel));
|
||||
let low = floor(exact);
|
||||
let mip_blend = fract(exact);
|
||||
var offsets = array<vec2<f32>, 4>(
|
||||
vec2(0.125, 0.375), vec2(-0.125, -0.375), vec2(0.375, -0.125), vec2(-0.375, 0.125));
|
||||
var sum_low = vec4(0.0);
|
||||
for (var i = 0; i < 4; i++) {
|
||||
sum_low += sprite_level(uv + offsets[i] * pixel, layer, low);
|
||||
}
|
||||
var rgss = sum_low * 0.25;
|
||||
if mip_blend > 0.0 {
|
||||
var sum_high = vec4(0.0);
|
||||
for (var i = 0; i < 4; i++) {
|
||||
sum_high += sprite_level(uv + offsets[i] * pixel, layer, low + 1.0);
|
||||
}
|
||||
rgss = mix(rgss, sum_high * 0.25, mip_blend);
|
||||
}
|
||||
return mix(nearest, rgss, blend);
|
||||
}
|
||||
|
||||
struct VisIdOut {
|
||||
@builtin(position) clip: vec4<f32>,
|
||||
@location(0) @interpolate(flat) id: u32,
|
||||
}
|
||||
// The quads with no cutout that are not fading (`opaque_quad`): no discard, so a tiled GPU
|
||||
// keeps hiding what lies behind them. `page`: the draw's instance, its arena page
|
||||
// (`Flat::draw_paged`).
|
||||
@vertex fn vs_vis_opaque(
|
||||
@builtin(vertex_index) vertex: u32, @builtin(instance_index) page: u32
|
||||
) -> VisIdOut {
|
||||
var out: VisIdOut;
|
||||
let s = section_corner(vertex);
|
||||
let origin = slot_origins[block_slots[s.slot >> 8u]];
|
||||
if !opaque_quad(corner_layer(s.c), origin.w) {
|
||||
out.clip = vec4(2.0, 2.0, 2.0, 1.0);
|
||||
return out;
|
||||
}
|
||||
let position = bitcast<vec3<f32>>(s.c.p);
|
||||
out.clip = fade_depth(globals.view_proj * vec4(position + origin.xyz, 1.0), origin.w);
|
||||
out.id = vis_id(page, s);
|
||||
return out;
|
||||
}
|
||||
@fragment fn fs_vis_id(in: VisIdOut) -> @location(0) u32 {
|
||||
return in.id;
|
||||
}
|
||||
|
||||
struct VisCutoutOut {
|
||||
@builtin(position) clip: vec4<f32>,
|
||||
@location(0) @interpolate(flat) id: u32,
|
||||
@location(1) uv: vec2<f32>,
|
||||
@location(2) @interpolate(flat) layer: u32,
|
||||
// The vertex colour's alpha (`terrain_vertex`'s `color.a`: the lightmap's).
|
||||
@location(3) alpha: f32,
|
||||
@location(4) @interpolate(flat) fade: u32,
|
||||
@location(5) local: vec3<f32>,
|
||||
}
|
||||
// The other quads: `solid_color`'s fade dither and alpha cutout, with no shading.
|
||||
@vertex fn vs_vis_cutout(
|
||||
@builtin(vertex_index) vertex: u32, @builtin(instance_index) page: u32
|
||||
) -> VisCutoutOut {
|
||||
var out: VisCutoutOut;
|
||||
let s = section_corner(vertex);
|
||||
let origin = slot_origins[block_slots[s.slot >> 8u]];
|
||||
let layer = corner_layer(s.c);
|
||||
if opaque_quad(layer, origin.w) {
|
||||
out.clip = vec4(2.0, 2.0, 2.0, 1.0);
|
||||
return out;
|
||||
}
|
||||
let position = bitcast<vec3<f32>>(s.c.p);
|
||||
let uv = vec2(s.c.r.x & 0xffffu, s.c.r.x >> 16u);
|
||||
let light = vec2((s.c.r.y >> 16u) & 0xffu, s.c.r.y >> 24u);
|
||||
let pos = position + origin.xyz;
|
||||
out.clip = fade_depth(globals.view_proj * vec4(pos, 1.0), origin.w);
|
||||
out.id = vis_id(page, s);
|
||||
out.uv = terrain_uv(uv, origin.w);
|
||||
out.layer = layer;
|
||||
let block = max(f32(light.y), dynamic_block_light(pos));
|
||||
out.alpha = sample_lightmap(f32(light.x) / 16.0, block / 16.0).a;
|
||||
out.fade = fade_code(origin.w);
|
||||
out.local = position;
|
||||
return out;
|
||||
}
|
||||
@fragment fn fs_vis_cutout(in: VisCutoutOut) -> @location(0) u32 {
|
||||
let dx = dpdx(in.local);
|
||||
let dy = dpdy(in.local);
|
||||
if faded(in.fade, in.local, dx, dy) { discard; }
|
||||
let alpha = vis_sprite(in.uv, in.layer & 0x7fffu, dpdx(in.uv), dpdy(in.uv)).a * in.alpha;
|
||||
if alpha < 0.5 && (in.layer & 0x8000u) == 0u { discard; }
|
||||
return in.id;
|
||||
}
|
||||
|
||||
// Pass 2. Up to 16 arena pages (mesher.rs's TerrainVertex, 24 bytes: position, UV as two u16,
|
||||
// layer and light as u16 each, RGBA8 colour), a quad's 96 bytes as six vec4s; at bindings 0 and
|
||||
// 1 the block tables of all pages, page after page, `VIS_BLOCKS_PER_PAGE` entries each
|
||||
// (`Visbuffer::resolve` copies them from the flat draws' own), and the origins by section id
|
||||
// (terrain.wgsl's flat draws); pass 1's IDs.
|
||||
override VIS_BLOCKS_PER_PAGE: u32 = 1u;
|
||||
@group(1) @binding(2) var<storage, read> vis_page_0: array<vec4<u32>>;
|
||||
@group(1) @binding(3) var<storage, read> vis_page_1: array<vec4<u32>>;
|
||||
@group(1) @binding(4) var<storage, read> vis_page_2: array<vec4<u32>>;
|
||||
@group(1) @binding(5) var<storage, read> vis_page_3: array<vec4<u32>>;
|
||||
@group(1) @binding(6) var<storage, read> vis_page_4: array<vec4<u32>>;
|
||||
@group(1) @binding(7) var<storage, read> vis_page_5: array<vec4<u32>>;
|
||||
@group(1) @binding(8) var<storage, read> vis_page_6: array<vec4<u32>>;
|
||||
@group(1) @binding(9) var<storage, read> vis_page_7: array<vec4<u32>>;
|
||||
@group(1) @binding(10) var<storage, read> vis_page_8: array<vec4<u32>>;
|
||||
@group(1) @binding(11) var<storage, read> vis_page_9: array<vec4<u32>>;
|
||||
@group(1) @binding(12) var<storage, read> vis_page_10: array<vec4<u32>>;
|
||||
@group(1) @binding(13) var<storage, read> vis_page_11: array<vec4<u32>>;
|
||||
@group(1) @binding(14) var<storage, read> vis_page_12: array<vec4<u32>>;
|
||||
@group(1) @binding(15) var<storage, read> vis_page_13: array<vec4<u32>>;
|
||||
@group(1) @binding(16) var<storage, read> vis_page_14: array<vec4<u32>>;
|
||||
@group(1) @binding(17) var<storage, read> vis_page_15: array<vec4<u32>>;
|
||||
@group(1) @binding(18) var vis_ids: texture_2d<u32>;
|
||||
|
||||
// A quad's 96 bytes, six 16-byte words from `i` on, of the page in `slot`: one switch for
|
||||
// all of them, so a thread group whose pixels show several pages walks its cases once.
|
||||
struct VisQuad {
|
||||
q: array<vec4<u32>, 6>,
|
||||
}
|
||||
fn vis_quad(slot: u32, i: u32) -> VisQuad {
|
||||
switch slot {
|
||||
case 0u: {
|
||||
return VisQuad(array(vis_page_0[i], vis_page_0[i + 1u], vis_page_0[i + 2u],
|
||||
vis_page_0[i + 3u], vis_page_0[i + 4u], vis_page_0[i + 5u]));
|
||||
}
|
||||
case 1u: {
|
||||
return VisQuad(array(vis_page_1[i], vis_page_1[i + 1u], vis_page_1[i + 2u],
|
||||
vis_page_1[i + 3u], vis_page_1[i + 4u], vis_page_1[i + 5u]));
|
||||
}
|
||||
case 2u: {
|
||||
return VisQuad(array(vis_page_2[i], vis_page_2[i + 1u], vis_page_2[i + 2u],
|
||||
vis_page_2[i + 3u], vis_page_2[i + 4u], vis_page_2[i + 5u]));
|
||||
}
|
||||
case 3u: {
|
||||
return VisQuad(array(vis_page_3[i], vis_page_3[i + 1u], vis_page_3[i + 2u],
|
||||
vis_page_3[i + 3u], vis_page_3[i + 4u], vis_page_3[i + 5u]));
|
||||
}
|
||||
case 4u: {
|
||||
return VisQuad(array(vis_page_4[i], vis_page_4[i + 1u], vis_page_4[i + 2u],
|
||||
vis_page_4[i + 3u], vis_page_4[i + 4u], vis_page_4[i + 5u]));
|
||||
}
|
||||
case 5u: {
|
||||
return VisQuad(array(vis_page_5[i], vis_page_5[i + 1u], vis_page_5[i + 2u],
|
||||
vis_page_5[i + 3u], vis_page_5[i + 4u], vis_page_5[i + 5u]));
|
||||
}
|
||||
case 6u: {
|
||||
return VisQuad(array(vis_page_6[i], vis_page_6[i + 1u], vis_page_6[i + 2u],
|
||||
vis_page_6[i + 3u], vis_page_6[i + 4u], vis_page_6[i + 5u]));
|
||||
}
|
||||
case 7u: {
|
||||
return VisQuad(array(vis_page_7[i], vis_page_7[i + 1u], vis_page_7[i + 2u],
|
||||
vis_page_7[i + 3u], vis_page_7[i + 4u], vis_page_7[i + 5u]));
|
||||
}
|
||||
case 8u: {
|
||||
return VisQuad(array(vis_page_8[i], vis_page_8[i + 1u], vis_page_8[i + 2u],
|
||||
vis_page_8[i + 3u], vis_page_8[i + 4u], vis_page_8[i + 5u]));
|
||||
}
|
||||
case 9u: {
|
||||
return VisQuad(array(vis_page_9[i], vis_page_9[i + 1u], vis_page_9[i + 2u],
|
||||
vis_page_9[i + 3u], vis_page_9[i + 4u], vis_page_9[i + 5u]));
|
||||
}
|
||||
case 10u: {
|
||||
return VisQuad(array(vis_page_10[i], vis_page_10[i + 1u], vis_page_10[i + 2u],
|
||||
vis_page_10[i + 3u], vis_page_10[i + 4u], vis_page_10[i + 5u]));
|
||||
}
|
||||
case 11u: {
|
||||
return VisQuad(array(vis_page_11[i], vis_page_11[i + 1u], vis_page_11[i + 2u],
|
||||
vis_page_11[i + 3u], vis_page_11[i + 4u], vis_page_11[i + 5u]));
|
||||
}
|
||||
case 12u: {
|
||||
return VisQuad(array(vis_page_12[i], vis_page_12[i + 1u], vis_page_12[i + 2u],
|
||||
vis_page_12[i + 3u], vis_page_12[i + 4u], vis_page_12[i + 5u]));
|
||||
}
|
||||
case 13u: {
|
||||
return VisQuad(array(vis_page_13[i], vis_page_13[i + 1u], vis_page_13[i + 2u],
|
||||
vis_page_13[i + 3u], vis_page_13[i + 4u], vis_page_13[i + 5u]));
|
||||
}
|
||||
case 14u: {
|
||||
return VisQuad(array(vis_page_14[i], vis_page_14[i + 1u], vis_page_14[i + 2u],
|
||||
vis_page_14[i + 3u], vis_page_14[i + 4u], vis_page_14[i + 5u]));
|
||||
}
|
||||
default: {
|
||||
return VisQuad(array(vis_page_15[i], vis_page_15[i + 1u], vis_page_15[i + 2u],
|
||||
vis_page_15[i + 3u], vis_page_15[i + 4u], vis_page_15[i + 5u]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct VisResolveOut {
|
||||
@builtin(position) clip: vec4<f32>,
|
||||
// The group of 16 pages this draw resolves: its instance (`Visbuffer::resolve`).
|
||||
@location(0) @interpolate(flat) chunk: u32,
|
||||
}
|
||||
@vertex fn vs_vis_resolve(@builtin(vertex_index) i: u32,
|
||||
@builtin(instance_index) chunk: u32) -> VisResolveOut {
|
||||
let p = vec2(f32(i32(i & 1u) * 4 - 1), f32(i32(i >> 1u) * 4 - 1));
|
||||
var out: VisResolveOut;
|
||||
out.clip = vec4(p, 0.0, 1.0);
|
||||
out.chunk = chunk;
|
||||
return out;
|
||||
}
|
||||
|
||||
// A framebuffer position's NDC (the world pass draws to the whole target).
|
||||
fn vis_ndc(at: vec2<f32>, size: vec2<f32>) -> vec2<f32> {
|
||||
return vec2(at.x / size.x * 2.0 - 1.0, 1.0 - at.y / size.y * 2.0);
|
||||
}
|
||||
|
||||
// Perspective-correct barycentrics of the point at NDC `p` on the triangle with clip-space
|
||||
// corners a, b, c (x, y, w): homogeneous 2D rasterisation (Olano and Greer). With M the matrix
|
||||
// of the corners as columns, the weights are M^-1 (p, 1) normalised, and the rows of M^-1 are
|
||||
// the corners' cross products over the determinant, which the normalisation cancels. Corners
|
||||
// behind the camera need no clipping. The rows are given (`vis_rows`), as three points share
|
||||
// them.
|
||||
fn vis_bary(rows: mat3x3<f32>, p: vec2<f32>) -> vec3<f32> {
|
||||
let l = vec3(p, 1.0) * rows;
|
||||
let sum = l.x + l.y + l.z;
|
||||
if abs(sum) < 1e-30 {
|
||||
return vec3(-1.0);
|
||||
}
|
||||
return l / sum;
|
||||
}
|
||||
fn vis_rows(a: vec3<f32>, b: vec3<f32>, c: vec3<f32>) -> mat3x3<f32> {
|
||||
return mat3x3(cross(b, c), cross(c, a), cross(a, b));
|
||||
}
|
||||
|
||||
// A corner's `terrain_vertex` from its six words: position, UV, layer and light, colour (the
|
||||
// vertex attributes `TERRAIN_ATTRIBUTES` in gfx.rs give: Float32x3, Uint16x2, Uint16, Uint8x2
|
||||
// with sky in the low byte, Unorm8x4).
|
||||
fn vis_corner(position: vec3<u32>, rest: vec3<u32>, origin: vec4<f32>) -> TerrainOut {
|
||||
let uv = vec2(rest.x & 0xffffu, rest.x >> 16u);
|
||||
let light = vec2((rest.y >> 16u) & 0xffu, rest.y >> 24u);
|
||||
return terrain_vertex(bitcast<vec3<f32>>(position), terrain_uv(uv, origin.w),
|
||||
rest.y & 0xffffu, unpack4x8unorm(rest.z), light, origin.xyz, origin.w);
|
||||
}
|
||||
|
||||
@fragment fn fs_vis_resolve(in: VisResolveOut) -> @location(0) vec4<f32> {
|
||||
let pixel = vec2<i32>(floor(in.clip.xy));
|
||||
let id = textureLoad(vis_ids, pixel, 0).x;
|
||||
let page = (id >> 24u) & 0x3fu;
|
||||
if (id & 0x80000000u) == 0u || page / 16u != in.chunk {
|
||||
discard;
|
||||
}
|
||||
let slot = page % 16u;
|
||||
let index = id & 0xffffffu;
|
||||
// The quad's first vertex (four of them from a multiple of 4) or its record.
|
||||
var first = 4u * index;
|
||||
var record = false;
|
||||
var lo = vec3(0u);
|
||||
var hi = vec3(0u);
|
||||
if (id & 0x40000000u) != 0u {
|
||||
first = index;
|
||||
// The record's six words from word 6 * index: two 16-byte words from the one it
|
||||
// starts in, at its start or its middle.
|
||||
let w = vis_quad(slot, (6u * index) >> 2u).q;
|
||||
let middle = ((6u * index) & 3u) != 0u;
|
||||
lo = select(w[0].xyz, vec3(w[0].zw, w[1].x), middle);
|
||||
hi = select(vec3(w[0].w, w[1].xy), w[1].yzw, middle);
|
||||
record = (hi.z & 0x80000000u) == 0u;
|
||||
if !record {
|
||||
// `packed::FULL`: its four vertices, word 0 slots on.
|
||||
first = index + lo.x;
|
||||
}
|
||||
}
|
||||
// The quad's block (256 slots: 64 quads of vertices, 256 records) names its section, as
|
||||
// for `vs_section_flat`.
|
||||
let block = select(index >> 6u, index >> 8u, (id & 0x40000000u) != 0u);
|
||||
let origin = slot_origins[block_slots[page * VIS_BLOCKS_PER_PAGE + block]];
|
||||
var p0: vec3<u32>;
|
||||
var r0: vec3<u32>;
|
||||
var p1: vec3<u32>;
|
||||
var r1: vec3<u32>;
|
||||
var p2: vec3<u32>;
|
||||
var r2: vec3<u32>;
|
||||
var p3: vec3<u32>;
|
||||
var r3: vec3<u32>;
|
||||
if record {
|
||||
let c0 = packed_corner(lo, hi, 0u);
|
||||
let c1 = packed_corner(lo, hi, 1u);
|
||||
let c2 = packed_corner(lo, hi, 2u);
|
||||
let c3 = packed_corner(lo, hi, 3u);
|
||||
p0 = c0.p;
|
||||
r0 = c0.r;
|
||||
p1 = c1.p;
|
||||
r1 = c1.r;
|
||||
p2 = c2.p;
|
||||
r2 = c2.r;
|
||||
p3 = c3.p;
|
||||
r3 = c3.r;
|
||||
} else {
|
||||
// Corner k is words 6k to 6k + 5 of the quad's 24.
|
||||
let words = vis_quad(slot, 6u * (first >> 2u)).q;
|
||||
let q0 = words[0];
|
||||
let q1 = words[1];
|
||||
let q2 = words[2];
|
||||
let q3 = words[3];
|
||||
let q4 = words[4];
|
||||
let q5 = words[5];
|
||||
p0 = q0.xyz;
|
||||
r0 = vec3(q0.w, q1.x, q1.y);
|
||||
p1 = vec3(q1.z, q1.w, q2.x);
|
||||
r1 = q2.yzw;
|
||||
p2 = q3.xyz;
|
||||
r2 = vec3(q3.w, q4.x, q4.y);
|
||||
p3 = vec3(q4.z, q4.w, q5.x);
|
||||
r3 = q5.yzw;
|
||||
}
|
||||
let c0 = (globals.view_proj * vec4(bitcast<vec3<f32>>(p0) + origin.xyz, 1.0)).xyw;
|
||||
let c1 = (globals.view_proj * vec4(bitcast<vec3<f32>>(p1) + origin.xyz, 1.0)).xyw;
|
||||
let c2 = (globals.view_proj * vec4(bitcast<vec3<f32>>(p2) + origin.xyz, 1.0)).xyw;
|
||||
let c3 = (globals.view_proj * vec4(bitcast<vec3<f32>>(p3) + origin.xyz, 1.0)).xyw;
|
||||
// The quad's triangles are 0 1 2 and 0 2 3 (`Renderer::quad_indices`, flatten.wgsl),
|
||||
// either side of the diagonal 0 2. Corner 1's weight in 0 1 2 is `vis_bary`'s row
|
||||
// cross(c2, c0) over the sum of the rows, whose sign is that of the triangle's determinant
|
||||
// (the sum is one over the point's w), dot(c1, cross(c2, c0)): the pixel lies in 0 1 2 when
|
||||
// the two agree. A degenerate 0 1 2 (no area, no pixels) leaves it to 0 2 3.
|
||||
let size = vec2<f32>(textureDimensions(vis_ids));
|
||||
let centre = in.clip.xy;
|
||||
let p = vis_ndc(centre, size);
|
||||
let diagonal = cross(c2, c0);
|
||||
let det = dot(c1, diagonal);
|
||||
let second = dot(vec3(p, 1.0), diagonal) * det < 0.0 || det == 0.0;
|
||||
let rows = vis_rows(c0, select(c1, c2, second), select(c2, c3, second));
|
||||
let b = vis_bary(rows, p);
|
||||
// dpdxFine and dpdyFine: the difference to the other pixel of this one's 2x2 quad in its
|
||||
// row and column, odd minus even, over the same triangle's plane.
|
||||
let odd = vec2<f32>(pixel & vec2(1));
|
||||
let toward = vec2(1.0) - 2.0 * odd;
|
||||
let bx = vis_bary(rows, vis_ndc(centre + vec2(toward.x, 0.0), size));
|
||||
let by = vis_bary(rows, vis_ndc(centre + vec2(0.0, toward.y), size));
|
||||
// `terrain_vertex` on the triangle's corners, one at a time into the sums, so that only
|
||||
// one corner's outputs are held at once; the first is the provoking one, whose flat
|
||||
// outputs (layer, emission, fade, emissive layer) the rasteriser keeps.
|
||||
var t = vis_corner(p0, r0, origin);
|
||||
let uv0 = t.uv;
|
||||
t.uv = b.x * uv0;
|
||||
var uv_x = bx.x * uv0;
|
||||
var uv_y = by.x * uv0;
|
||||
t.color = b.x * t.color;
|
||||
t.spherical = b.x * t.spherical;
|
||||
t.cylindrical = b.x * t.cylindrical;
|
||||
t.visibility = b.x * t.visibility;
|
||||
let t1 = vis_corner(select(p1, p2, second), select(r1, r2, second), origin);
|
||||
t.uv += b.y * t1.uv;
|
||||
uv_x += bx.y * t1.uv;
|
||||
uv_y += by.y * t1.uv;
|
||||
t.color += b.y * t1.color;
|
||||
t.spherical += b.y * t1.spherical;
|
||||
t.cylindrical += b.y * t1.cylindrical;
|
||||
t.visibility += b.y * t1.visibility;
|
||||
let t2 = vis_corner(select(p2, p3, second), select(r2, r3, second), origin);
|
||||
t.uv += b.z * t2.uv;
|
||||
uv_x += bx.z * t2.uv;
|
||||
uv_y += by.z * t2.uv;
|
||||
t.color += b.z * t2.color;
|
||||
t.spherical += b.z * t2.spherical;
|
||||
t.cylindrical += b.z * t2.cylindrical;
|
||||
t.visibility += b.z * t2.visibility;
|
||||
let du = (uv_x - t.uv) * toward.x;
|
||||
let dv = (uv_y - t.uv) * toward.y;
|
||||
// HDR output alone reads the light levels and the face's normal (as in `fs_opaque`):
|
||||
// `terrain_vertex` gives them as the block light over 16 and the sky light over 16, and the
|
||||
// section-relative position.
|
||||
if globals.hdr_light.z != 0.0 {
|
||||
let local = array(bitcast<vec3<f32>>(p0), bitcast<vec3<f32>>(select(p1, p2, second)),
|
||||
bitcast<vec3<f32>>(select(p2, p3, second)));
|
||||
// `t.scene` is still the first corner's.
|
||||
t.scene = b.x * t.scene + b.y * t1.scene + b.z * t2.scene;
|
||||
t.local = b.x * local[0] + b.y * local[1] + b.z * local[2];
|
||||
let dlx = (bx.x * local[0] + bx.y * local[1] + bx.z * local[2] - t.local) * toward.x;
|
||||
let dly = (by.x * local[0] + by.y * local[1] + by.z * local[2] - t.local) * toward.y;
|
||||
hdr_normal = normalize(cross(dly, dlx));
|
||||
}
|
||||
// `fs_opaque` and `solid_color` past their discards, which pass 1 already made.
|
||||
let texel = vis_sprite(t.uv, t.layer & 0x7fffu, du, dv);
|
||||
var color = texel * t.color;
|
||||
color = mix(globals.fog_color * vec4(1.0, 1.0, 1.0, color.a), color, t.visibility);
|
||||
return solid_finish_at(t, texel, color, du, dv);
|
||||
}
|
||||
@@ -386,7 +386,6 @@ impl World {
|
||||
chunk.motion_blocking(x & 15, z & 15, |v| blocks(BlockStateId(v)))
|
||||
})
|
||||
}
|
||||
#[inline]
|
||||
pub fn block(&self, x: i32, y: i32, z: i32) -> BlockStateId {
|
||||
self.chunks
|
||||
.get(&(x >> 4, z >> 4))
|
||||
|
||||
@@ -1,482 +0,0 @@
|
||||
{
|
||||
"horizon.options.controller": "Beheerder",
|
||||
"horizon.options.vibration": "Vibrasie",
|
||||
"horizon.options.adaptive_triggers": "Aanpasbare snellers",
|
||||
"horizon.options.lightbar": "Ligbalk",
|
||||
"horizon.options.look_sensitivity": "Kyksensitiwiteit",
|
||||
"horizon.options.invert_x": "Omgekeerde kyk X",
|
||||
"horizon.options.invert_y": "Omgekeerde kyk Y",
|
||||
"horizon.options.sneak_button": "Sluipknoppie",
|
||||
"horizon.options.move_deadzone": "Bewegingsdooiesone",
|
||||
"horizon.options.look_deadzone": "Kykdooiesone",
|
||||
"horizon.options.button_glyphs": "Knoppie Glyphs",
|
||||
"horizon.options.menu_navigation": "Kieslysnavigasie",
|
||||
"horizon.options.on_screen_keyboard": "Sleutelbord op die skerm",
|
||||
"horizon.options.chat_delay": "Chat Vertraging",
|
||||
"horizon.options.lightbar.health": "Gesondheid",
|
||||
"horizon.options.lightbar.dimension": "Dimensie",
|
||||
"horizon.options.glyphs.auto": "Outo",
|
||||
"horizon.options.menu_focus": "Fokus",
|
||||
"horizon.options.menu_pointer": "Wyser",
|
||||
"horizon.options.keyboard.auto": "Outo",
|
||||
"horizon.options.unavailable": "Hierdie kenmerk is nog nie in Horizon beskikbaar nie.",
|
||||
"horizon.options.search": "Soek instellings...",
|
||||
"horizon.pages.shader_packs": "Shader Pakke",
|
||||
"horizon.pages.shader_options": "Shader Opsies",
|
||||
"horizon.pages.controller": "Beheerderinstellings",
|
||||
"horizon.pages.controller_binds": "Kontroleerder bind",
|
||||
"horizon.pages.extras": "Horizon-ekstras",
|
||||
"horizon.pages.settings": "Horizon-instellings",
|
||||
"horizon.pages.agent": "KI Agent",
|
||||
"horizon.split.leave": "Los",
|
||||
"horizon.pages.split_settings": "Verdeelskerm-instellings",
|
||||
"horizon.pages.split_profiles": "Wie speel?",
|
||||
"horizon.pages.split_setup": "Verdeelskerm-opstelling",
|
||||
"horizon.pages.music": "Musiek",
|
||||
"horizon.pages.music.menu": "Spyskaart",
|
||||
"horizon.pages.music.overworld": "Oorwêreld",
|
||||
"horizon.pages.music.creative": "Kreatief",
|
||||
"horizon.pages.music.underwater": "Onderwater",
|
||||
"horizon.pages.music.end": "Die Einde",
|
||||
"horizon.pages.music.boss": "Baas",
|
||||
"horizon.pages.music.credits": "Krediete",
|
||||
"horizon.pages.music.overworld_biomes": "Oorwêreld biome",
|
||||
"horizon.pages.music.nether_biomes": "Onder biome",
|
||||
"horizon.pages.music.end_biomes": "Eindig biome",
|
||||
"horizon.pages.music.other": "Ander musiek",
|
||||
"horizon.pages.music.empty": "Geen musieksnitte nie: die speletjie se klanke is nie gelaai nie.",
|
||||
"horizon.pages.music.tooltip": "Die ingeboude snitte, volgens situasie: speel nou een, of skakel enige af sodat die speletjie ophou om dit te kies.",
|
||||
"horizon.pages.settings.tooltip": "Horizon se eie instellings: grafika, beheerders, ekstras en mods.",
|
||||
"horizon.pages.graphics.tooltip": "Grafika, eenvoudig: elke instelling sê wat dit kos.",
|
||||
"horizon.pages.graphics_advanced.tooltip": "Elke grafiese instelling, Horizon s'n ingesluit, met wat elkeen kos.",
|
||||
"horizon.pages.controller.tooltip": "Beheerders: hul knoppies, stokke, vibrasie en spyskaartnavigasie.",
|
||||
"horizon.pages.extras.tooltip": "Opsionele toevoegings, alles by verstek af: die speletjie bly vanielje totdat jy een aanskakel.",
|
||||
"horizon.pages.agent.tooltip": "Laat jou eie KI-agent speel: horizon-ctl en MCP-kliënte op hierdie rekenaar.",
|
||||
"horizon.pages.split_profiles.tooltip": "Wie speel: 'n profiel wat in hierdie speletjie-lêergids gehou word.",
|
||||
"horizon.pages.split_setup.tooltip": "Die vensters en monitors, en wie speel waar.",
|
||||
"horizon.pages.shader_packs.tooltip": "Shader-pakke vanaf die shaderpacks-lêergids: Horizon se eie en dié in die shaders/formaat. Een hardloop op 'n slag.",
|
||||
"horizon.pages.mods.tooltip": "Mods gemaak vir Horizon, vanaf die horizon-mods-lêergids. Elkeen loop in 'n sandbox en bly af totdat jy dit aanskakel.",
|
||||
"horizon.pages.open.graphics": "Grafika...",
|
||||
"horizon.pages.open.graphics_advanced": "Gevorderde grafika...",
|
||||
"horizon.pages.open.shader_packs": "Shader-pakke...",
|
||||
"horizon.pages.open.controller": "Beheerderinstellings...",
|
||||
"horizon.pages.open.split_setup": "Verdeelskerm...",
|
||||
"horizon.pages.open.extras": "Ekstras...",
|
||||
"horizon.pages.open.mods": "Modusse...",
|
||||
"horizon.pages.open.agent": "KI Agent...",
|
||||
"horizon.pages.open.horizon_graphics": "Horizon-grafika...",
|
||||
"horizon.pages.open.controller_binds": "Beheerder bind...",
|
||||
"horizon.pages.open.horizon_settings": "Horizon-instellings...",
|
||||
"horizon.pages.open.music": "Musiek...",
|
||||
"horizon.mods.empty": "Geen mods: plaas .hmod-lêers in die horizon-mods-lêergids",
|
||||
"horizon.mods.stopped": " (Gestop)",
|
||||
"horizon.mods.not_built": " (Nie gebou nie)",
|
||||
"horizon.mods.dev": " (Ontwikkelaar)",
|
||||
"horizon.mods.settings": "Instellings",
|
||||
"horizon.mods.in_world": " (los die wêreld om te verander)",
|
||||
"horizon.mods.open_folder": "Maak Mod-lêergids oop",
|
||||
"horizon.shader_packs.empty": "Geen shader packs: plaas dit in die shaderpacks-lêergids",
|
||||
"horizon.shader_options.open": "Shader-opsies...",
|
||||
"horizon.shader_options.profile": "Profiel",
|
||||
"horizon.shader_options.loading": "Lees tans die shader-pakket se opsies...",
|
||||
"horizon.agent.copy_details": "Kopieer verbindingbesonderhede",
|
||||
"horizon.multiplayer.join": "Sluit aan",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Kanselleer Herkoppel",
|
||||
"horizon.multiplayer.join_friend": "Sluit aan by 'n Vriend",
|
||||
"horizon.multiplayer.invite_code": "Uitnodigingskode",
|
||||
"horizon.multiplayer.relay": "Aflos (opsioneel)",
|
||||
"Preset": "Vooraf ingesteld",
|
||||
"LOD Distance": "LOD Afstand",
|
||||
"Far Detail": "Verre Detail",
|
||||
"Cave Culling": "Grotuitdunning",
|
||||
"Entity Culling": "Entiteit Uitdunning",
|
||||
"Upload Budget": "Laai begroting op",
|
||||
"Sky": "Hemel",
|
||||
"Fog": "Mis",
|
||||
"Weather": "Weer",
|
||||
"Animated Textures": "Geanimeerde teksture",
|
||||
"Present Mode": "Aanbiedingsmodus",
|
||||
"Frame Pacing": "Raamtempo",
|
||||
"Worker Threads": "Werkerdrade",
|
||||
"Resolution Scale": "Resolusieskaal",
|
||||
"Upscaling": "Opskaling",
|
||||
"Paper White": "Papier Wit",
|
||||
"Peak Brightness": "Piek helderheid",
|
||||
"Black Level": "Swart vlak",
|
||||
"UI Brightness": "UI Helderheid",
|
||||
"Battery Mode": "Batterymodus",
|
||||
"Graphics API": "Grafiese API",
|
||||
"Translucent Batching": "Deurskynende Batching",
|
||||
"Performance Overlay": "Prestasie-oorleg",
|
||||
"Full Detail Distance": "Volledige detail Afstand",
|
||||
"On": "Aan",
|
||||
"Off": "Af",
|
||||
"Unlimited": "Onbeperk",
|
||||
"Auto": "Outo",
|
||||
"Custom": "Pasgemaak",
|
||||
"Performance": "Prestasie",
|
||||
"Balanced": "Gebalanseerd",
|
||||
"Quality": "Kwaliteit",
|
||||
"Ultra Performance": "Ultra prestasie",
|
||||
"Native": "Inheems",
|
||||
"Full": "Vol",
|
||||
"Reduced": "Verminder",
|
||||
"Efficient": "Doeltreffend",
|
||||
"Precise": "Presies",
|
||||
"Auto (VSync)": "Outo (VSync)",
|
||||
"FIFO": "EIEU",
|
||||
"Mailbox": "Posbus",
|
||||
"Immediate": "Onmiddellik",
|
||||
"FPS and Graph": "FPS en grafiek",
|
||||
"No cost": "Geen koste nie",
|
||||
"Low cost": "Lae koste",
|
||||
"Medium cost": "Medium koste",
|
||||
"High cost": "Hoë koste",
|
||||
"Saves time": "Spaar tyd",
|
||||
"Graphics": "Grafika",
|
||||
"Done": "Klaar",
|
||||
"This feature is not yet available in Horizon.": "Hierdie kenmerk is nog nie in Horizon beskikbaar nie.",
|
||||
"Your display or system doesn't offer HDR output.": "Jou skerm of stelsel bied nie HDR-uitvoer nie.",
|
||||
"Turn HDR on to use it.": "Skakel HDR aan om dit te gebruik.",
|
||||
"Your GPU or graphics API doesn't support it.": "Jou GPU of grafiese API ondersteun dit nie.",
|
||||
"%s chunks": "%s stukke",
|
||||
"%s per frame": "%s per raam",
|
||||
"%s MiB/frame": "%s MiB/raam",
|
||||
"%s KiB/frame": "%s KiB/raam",
|
||||
"Auto (%s)": "Outo (%s)",
|
||||
"Full-detail terrain range. LOD Distance can extend local worlds farther.": "Volledige detail terreinreeks. LOD Distance kan plaaslike wêrelde verder uitbrei.",
|
||||
"Coarse distant terrain in local worlds. Integrated GPUs stop at 128 chunks.": "Growwe verre terrein in plaaslike wêrelde. Geïntegreerde GPU's stop by 128 stukke.",
|
||||
"Reduced draws far terrain coarser: faster on this integrated GPU.": "Verminder trek veel terrein growwer: vinniger op hierdie geïntegreerde GPU.",
|
||||
"Reduced draws far terrain coarser, for GPUs it slows down.": "Verminder trek veel terrein growwer, vir GPU's word dit stadiger.",
|
||||
"Field of view. Wider shows more terrain at once.": "Gesigsveld. Wyer wys meer terrein op een slag.",
|
||||
"Caps frames per second; a cap saves power and heat.": "Caps rame per sekonde; 'n pet spaar krag en hitte.",
|
||||
"Waits for the display's refresh: no tearing, a little latency.": "Wag vir die verversing van die skerm: geen skeur nie, 'n bietjie vertraging.",
|
||||
"Size of menus and the HUD.": "Grootte van spyskaarte en die HUD.",
|
||||
"Sets the options below together. Changing one makes it Custom.": "Stel die opsies hieronder saam. Om een te verander, maak dit Pasgemaak.",
|
||||
"Lava, fire and sunlight shine brighter than white. Needs an HDR display.": "Lava, vuur en sonlig skyn helderder as wit. Benodig 'n HDR-skerm.",
|
||||
"Your display's darkest black. 0 for OLED; LCDs glow a little.": "Jou skerm is die donkerste swart. 0 vir OLED; LCD's gloei 'n bietjie.",
|
||||
"How bright highlights get. Auto: the peak your system reports.": "Hoe helder hoogtepunte word. Outo: die hoogtepunt wat jou stelsel rapporteer.",
|
||||
"How bright white paper and menus look. Auto: your desktop's white.": "Hoe helder wit papier en spyskaarte lyk. Outo: jou lessenaar is wit.",
|
||||
"How bright the HUD and menus are, against paper white.": "Hoe helder die HUD en spyskaarte is, teenoor papierwit.",
|
||||
"How far mobs, crops and machines keep working. CPU time.": "Hoe ver skare, gewasse en masjiene aanhou werk. SVE tyd.",
|
||||
"How far away mobs and items are drawn, as a share of normal.": "Hoe ver weg skares en items getrek word, as 'n deel van normaal.",
|
||||
"Skips terrain hidden behind closed sections. Off costs a lot.": "Slaan terrein oor wat agter geslote gedeeltes versteek is. Af kos baie.",
|
||||
"Hides entities behind opaque blocks.": "Versteek entiteite agter ondeursigtige blokke.",
|
||||
"No entity occlusion culling.": "Geen entiteit-okklusion uitskot nie.",
|
||||
"CPU occlusion test runs this frame.": "SVE-okklusietoets loop hierdie raam.",
|
||||
"Occlusion checks run on a worker thread.": "Okklusiekontroles loop op 'n werkersdraad.",
|
||||
"Meshed sections uploaded per frame. Lower is smoother while loading.": "Maasafdelings per raam opgelaai. Laer is gladder tydens laai.",
|
||||
"Soft shadows in corners. Costs chunk building time only.": "Sagte skaduwees in hoeke. Dit kos slegs 'n stuk boutyd.",
|
||||
"Smaller copies of block textures for distant terrain: less shimmer.": "Kleiner kopieë van blokteksture vir verafgeleë terrein: minder glans.",
|
||||
"Blends grass and water colours across biomes when building chunks.": "Vermeng gras en waterkleure oor biome wanneer stukke gebou word.",
|
||||
"Cloud layer: off, flat (fast) or 3D (fancy).": "Wolklaag: af, plat (vinnig) of 3D (fancy).",
|
||||
"How far clouds reach, in chunks.": "Hoe ver wolke reik, in stukke.",
|
||||
"Draws the sky, sun, moon and stars.": "Teken die lug, son, maan en sterre.",
|
||||
"Distance fog. Off shows the edge of the loaded world.": "Afstand mis. Af wys die rand van die gelaaide wêreld.",
|
||||
"Draws rain and snow.": "Trek reën en sneeu.",
|
||||
"How far rain and snow fall around you.": "Hoe ver val reën en sneeu om jou.",
|
||||
"How many particles effects spawn.": "Hoeveel deeltjies effekte kuit.",
|
||||
"Round shadows under mobs and items.": "Ronde skaduwees onder skares en items.",
|
||||
"Leaves with see-through gaps. Off draws them solid.": "Blare met deursigtige gapings. Off trek hulle stewig.",
|
||||
"Sorts water and glass more exactly behind each other.": "Sorteer water en glas meer presies agter mekaar.",
|
||||
"Which chunk rebuilds wait for the frame: none, nearby or yours.": "Watter stuk herbou wag vir die raam: geen, naby of joune.",
|
||||
"Animates water, lava, fire and other moving textures.": "Animeer water, lawa, vuur en ander bewegende teksture.",
|
||||
"World pixels per screen pixel. Below 100% is faster on the GPU.": "Wêreldpixels per skermpiksel. Onder 100% is vinniger op die GPU.",
|
||||
"Smooths jagged edges. FXAA: softer. TAA: smoothest, moving things may trail.": "Maak gekartelde rande glad. FXAA: sagter. TAA: gladste, bewegende dinge kan loop.",
|
||||
"Renders the world smaller and rebuilds it with TAA. Faster, softer.": "Maak die wêreld kleiner en herbou dit met TAA. Vinniger, sagter.",
|
||||
"HDR output on an HDR display: lava, fire and sunlight shine brighter.": "HDR-uitset op 'n HDR-skerm: lawa, vuur en sonlig skyn helderder.",
|
||||
"How frames reach the screen while VSync is off.": "Hoe rame die skerm bereik terwyl VSync af is.",
|
||||
"Efficient sleeps until the next frame; Precise spins, like vanilla.": "Doeltreffende slaap tot die volgende raam; Presiese draaie, soos vanielje.",
|
||||
"Fills the screen (borderless).": "Vul die skerm (randloos).",
|
||||
"Brightens dark places.": "Verhelder donker plekke.",
|
||||
"Lowers the frame rate when minimised, or idle too.": "Verlaag die raamtempo wanneer dit geminimaliseer word, of ook ledig.",
|
||||
"Sways the view while walking.": "Swaai die uitsig terwyl jy stap.",
|
||||
"Fades newly loaded chunks in.": "Vervaag nuutgelaaide stukke in.",
|
||||
"Darkens the screen's edges.": "Maak die skerm se rande donkerder.",
|
||||
"Shows when the world saves.": "Wys wanneer die wêreld red.",
|
||||
"Shows attack cooldown.": "Toon aanvalafkoeling.",
|
||||
"60 FPS cap (30 in menus or unfocused), half the workers: less power.": "60 FPS pet (30 in spyskaarte of ongefokus), die helfte van die werkers: minder krag.",
|
||||
"Threads that generate, light and mesh chunks. Next start.": "Drade wat stukke genereer, lig en maas. Volgende begin.",
|
||||
"Vulkan, then DirectX 12 (Windows) or OpenGL (Linux) where it fails. Next start.": "Vulkan, dan DirectX 12 (Windows) of OpenGL (Linux) waar dit misluk. Volgende begin.",
|
||||
"Water and glass in one buffer: fewer draw calls, a bit more VRAM. Next start.": "Water en glas in een buffer: minder trekoproepe, 'n bietjie meer VRAM. Volgende begin.",
|
||||
"Mesh data sent to the GPU per frame. Lower is smoother while loading.": "Maasdata per raam na die GPU gestuur. Laer is gladder tydens laai.",
|
||||
"Frame rate and frame times over the game, with a graph.": "Raamtempo en raamtye oor die spel, met 'n grafiek.",
|
||||
"horizon.multiplayer.finding_friend": "Soek jou vriend se wêreld...",
|
||||
"horizon.multiplayer.not_signed_in": "Hierdie bediener is in aanlynmodus. Meld aan met jou Microsoft-rekening in die Horizon-lanseerder om daarby aan te sluit.",
|
||||
"horizon.multiplayer.invalid_address": "Ongeldige bedieneradres: %s",
|
||||
"horizon.multiplayer.connected_friend_direct": "Reguit gekoppel aan jou vriend...",
|
||||
"horizon.multiplayer.connected_friend_relay": "Gekoppel aan jou vriend deur die aflos...",
|
||||
"General": "Algemeen",
|
||||
"Advanced": "Gevorderd",
|
||||
"Undo": "Ontdoen",
|
||||
"Search options": "Soek opsies",
|
||||
"Shortcut": "Kortpad",
|
||||
"Distance": "Afstand",
|
||||
"Display": "Vertoon",
|
||||
"Interface": "Koppelvlak",
|
||||
"Sky and weather": "Lug en weer",
|
||||
"Lighting and entities": "Beligting en entiteite",
|
||||
"Textures": "Teksture",
|
||||
"Image": "Beeld",
|
||||
"Chunk loading": "Chunk laai",
|
||||
"Culling": "Uitskot",
|
||||
"Power": "Krag",
|
||||
"Frames": "Rame",
|
||||
"Overlay": "Oorleg",
|
||||
"Output": "Uitset",
|
||||
"Brightness": "Helderheid",
|
||||
"How far you see, the window and the HUD.": "Hoe ver jy sien, die venster en die HUD.",
|
||||
"The sky and weather, lighting, textures and the image.": "Die lug en weer, beligting, teksture en die beeld.",
|
||||
"Chunk loading, culling and power use.": "Chunk laai, uitskot en kraggebruik.",
|
||||
"How frames reach the screen, and the performance overlay.": "Hoe rame die skerm bereik, en die prestasie-oorleg.",
|
||||
"Brighter-than-white output on an HDR display.": "Helderder-as-wit uitset op 'n HDR-skerm.",
|
||||
"Results for \"%s\"": "Resultate vir \"%s\"",
|
||||
"No settings match.": "Geen instellings pas nie.",
|
||||
"horizon.title.not_affiliated": "Nie geaffilieer met Mojang of Microsoft nie",
|
||||
"horizon.extras.header.lighting": "Beligting",
|
||||
"horizon.extras.header.third_person": "Derde persoon",
|
||||
"horizon.extras.header.particles": "Deeltjies",
|
||||
"horizon.extras.header.inventory": "Voorraad",
|
||||
"horizon.extras.header.chat": "Gesels",
|
||||
"horizon.extras.header.multiplayer": "Multispeler",
|
||||
"horizon.extras.header.sound": "Klank",
|
||||
"horizon.extras.header.menus": "Spyskaarte",
|
||||
"horizon.extras.fullbright": "Vol helder",
|
||||
"horizon.extras.dynamic_lights": "Dinamiese ligte",
|
||||
"horizon.extras.own_name_tag": "Eie naamplaatjie",
|
||||
"horizon.extras.hide_block_particles": "Versteek blokbreekdeeltjies",
|
||||
"horizon.extras.auto_reconnect": "Herkoppel outomaties",
|
||||
"horizon.extras.ui_sound_reserve": "Hou kanale vir UI-klanke",
|
||||
"horizon.extras.music_page": "Musiek bladsy",
|
||||
"horizon.extras.mouse_tweaks": "Muis Tweaks kontroles",
|
||||
"horizon.extras.mouse_tweaks_wheel": "Rolwiel beweeg items",
|
||||
"horizon.extras.sort": "Sorteer knoppies",
|
||||
"horizon.extras.sort_key": "Sorteer sleutel",
|
||||
"horizon.extras.no_ai": "Geen KI",
|
||||
"horizon.extras.agent_control": "Agent beheer",
|
||||
"horizon.extras.discord_presence": "Ryk teenwoordigheid op Discord",
|
||||
"horizon.extras.splitscreen": "Verdeelskerm",
|
||||
"horizon.extras.chat_animation": "Glad gesels",
|
||||
"horizon.extras.chat_indicator": "Boodskap aanwyser",
|
||||
"horizon.extras.wheel_cycles_settings": "Rolwiel verander instellings",
|
||||
"horizon.extras.pack_browser": "Hulpbronpakketblaaier",
|
||||
"horizon.extras.fullbright.tooltip": "Wys blokke en entiteite in volle lig, selfs in donker areas.",
|
||||
"horizon.extras.dynamic_lights.tooltip": "Fakkels en ander ligbronne wat vasgehou, laat val word of aan die brand steek, steek die blokke om hulle aan.",
|
||||
"horizon.extras.own_name_tag.tooltip": "Wys jou naam bo jou kop in derde persoon, soos ander spelers dit sien. Versteek wanneer die bediener jou span se name versteek of jy onsigbaar is.",
|
||||
"horizon.extras.hide_block_particles.tooltip": "Versteek die puindeeltjies wat uit 'n gebreekte blok bars en verkrummel van die een wat jy myn. Ander deeltjies bly.",
|
||||
"horizon.extras.auto_reconnect.tooltip": "Sluit weer by 'n bediener aan na 'n verlore verbinding of 'n herbegin, nooit na 'n skop of verbod nie.",
|
||||
"horizon.extras.ui_sound_reserve.tooltip": "Klanke in die wêreld laat 16 van die 247 klankkanale vry, so 'n vloed van hulle ('n groot ontploffing) kan nie kieslysklikke stilmaak nie.",
|
||||
"horizon.extras.music_page.tooltip": "Musiek en Klanke kry 'n Musiek...-bladsy: speel nou enige ingeboude snit, of skakel een af sodat die speletjie ophou om dit te kies. Niks verander totdat jy 'n snit afskakel nie.",
|
||||
"horizon.extras.mouse_tweaks.tooltip": "Regs-sleep plaas een per gleuf, links-sleep versamel die gehou item, Shift + links-sleep skuif elke gleuf.",
|
||||
"horizon.extras.mouse_tweaks_wheel.tooltip": "Af stuur een item van die gleuf na die ander kant, op bring een terug.",
|
||||
"horizon.extras.sort.tooltip": "Sorteer die voorraad, kiste, vate, eindkiste en shulkerbokse in kreatiewe oortjievolgorde, en voeg stapels saam.",
|
||||
"horizon.extras.sort_key.tooltip": "Sorteer die deel onder die wyser. Klik en druk dan 'n sleutel; Ontvlugting ontbind dit.",
|
||||
"horizon.extras.no_ai.tooltip": "Versteek en skakel elke KI-kenmerk af: agentbeheer, horizon-ctl en die MCP-bedieners. Die lanseerder se skakelaar ook.",
|
||||
"horizon.extras.agent_control.tooltip": "Jou eie KI-agent kan hierdie speletjie deur horizon-ctl of 'n MCP-kliënt speel, slegs op hierdie rekenaar.",
|
||||
"horizon.extras.discord_presence.tooltip": "Wys Playing Horizon op jou Discord-profiel terwyl Discord loop: spyskaarte, enkelspeler of multispeler, die dimensie en die speeltyd. Nooit 'n wêreld se naam of 'n bediener se adres nie.",
|
||||
"Calibrate...": "Kalibreer...",
|
||||
"Calibrate Anyway...": "Kalibreer in elk geval...",
|
||||
"Use Automatic Setup": "Gebruik outomatiese opstelling",
|
||||
"Shortcut: %s": "Kortpad: %s",
|
||||
"Next": "Volgende",
|
||||
"HDR Calibration %s/%s: %s": "HDR-kalibrasie %s/%s: %s",
|
||||
"Sets the three below full screen, one test picture at a time.": "Stel die drie hieronder volskerm, een toetsprent op 'n slag.",
|
||||
"A key that opens this screen from the game. Not bound at first.": "'n Sleutel wat hierdie skerm vanaf die speletjie oopmaak. Eers nie gebonde nie.",
|
||||
"Your display didn't say how bright it gets: calibrate to set it up.": "Jou skerm het nie gesê hoe helder dit word nie: kalibreer om dit op te stel.",
|
||||
"Already set up for your display. Only calibrate if HDR looks wrong.": "Reeds opgestel vir jou vertoning. Kalibreer slegs as HDR verkeerd lyk.",
|
||||
"Puts Peak Brightness and Paper White back to your display's setup.": "Plaas Peak Brightness en Paper White terug na jou skerm se opstelling.",
|
||||
"Set up for your display. Leave on Auto unless it looks wrong.": "Stel op vir jou vertoning. Los op Auto tensy dit verkeerd lyk.",
|
||||
"Lower it until the logo just disappears into the black.": "Laat sak dit totdat die logo net in die swart verdwyn.",
|
||||
"Raise it until the logo just disappears into the bright square.": "Lig dit op totdat die logo net in die helder vierkant verdwyn.",
|
||||
"Make the card a white that's comfortable to look at for a long time, in the light you play in.": "Maak die kaart 'n wit wat gemaklik is om lank na te kyk, in die lig waarin jy speel.",
|
||||
"horizon.splitscreen.profiles.forget": "Vergeet",
|
||||
"horizon.splitscreen.profiles.create": "Skep",
|
||||
"horizon.splitscreen.profiles.you": " - jy",
|
||||
"horizon.splitscreen.profiles.forget_named": "Vergeet %s",
|
||||
"horizon.splitscreen.profiles.previous": "Vorige",
|
||||
"horizon.splitscreen.profiles.next": "Volgende",
|
||||
"horizon.splitscreen.profiles.new_button": "Nuwe profiel...",
|
||||
"horizon.splitscreen.profiles.forget_title": "Vergeet profiel?",
|
||||
"horizon.splitscreen.profiles.forget_leaves": "Dit verlaat hierdie instansie se lys.",
|
||||
"horizon.splitscreen.profiles.forget_kept": "Wêreldvordering en instellings word behou.",
|
||||
"horizon.splitscreen.profiles.new_title": "Nuwe profiel",
|
||||
"horizon.splitscreen.profiles.name_hint": "Naam (3 tot 16 letters, syfers, _)",
|
||||
"horizon.splitscreen.profiles.switch_title": "Wissel profiel",
|
||||
"horizon.splitscreen.profiles.offline_note": "Vanlyn profiele behoort aan hierdie geval",
|
||||
"horizon.splitscreen.profiles.move": "Beweeg waar jy speel",
|
||||
"horizon.splitscreen.profiles.move_place": "Beweeg waar jy speel: %s",
|
||||
"horizon.friends.title": "Speel met vriende",
|
||||
"horizon.friends.hint": "Vriende plak die kode na die titelskerm se Vriende-knoppie, in Direkte verbinding, of op die lanseerder se Speel-bladsy.",
|
||||
"horizon.friends.copied": "Gekopieer",
|
||||
"horizon.friends.copy_code": "Kopieer kode",
|
||||
"horizon.friends.stop_sharing": "Hou op deel",
|
||||
"horizon.friends.none_yet": "Nog geen vriende hier nie.",
|
||||
"horizon.friends.here_now": "Hier nou: %s",
|
||||
"horizon.friends.share_intro": "Deel hierdie wêreld agter 'n uitnodigingskode: dit maak oop na LAN, en vriende op die internet sluit aan met die kode. Geen bediener van ons is betrokke nie.",
|
||||
"horizon.friends.start_sharing": "Begin deel",
|
||||
"horizon.friends.troubleshooting": "Foutsporing …",
|
||||
"horizon.friends.stopped": "Het opgehou om die uitnodigingkode te deel.",
|
||||
"horizon.friends.short_code_changed": "Iemand het aanhou om 'n verkeerde kort kode te probeer, so dit het verander.",
|
||||
"horizon.friends.wrong_code": "Iemand het 'n verkeerde kode probeer.",
|
||||
"horizon.friends.code_ready": "Jou uitnodigingskode is gereed: Speletjie-kieslys, Speel met vriende. Kopieer dit en stuur dit aan jou vriende.",
|
||||
"horizon.friends.share_failed": "Kon nie die wêreld deel nie: %s",
|
||||
"horizon.friends.finding_route": "Vind tans hoe vriende jou kan bereik …",
|
||||
"horizon.osk.shift": "Verskuiwing",
|
||||
"horizon.osk.space": "Ruimte",
|
||||
"horizon.osk.delete": "Vee uit",
|
||||
"horizon.osk.type": "Tik",
|
||||
"horizon.osk.symbols": "Simbole",
|
||||
"horizon.osk.close": "Maak toe",
|
||||
"horizon.extras.chat_animation.tooltip": "'n Nuwe kletslyn gly van links af terwyl dit vervaag, in plaas daarvan om heel te lyk.",
|
||||
"horizon.extras.chat_indicator.tooltip": "Die gekleurde balk aan die linkerkant van 'n kletslyn.",
|
||||
"horizon.extras.wheel_cycles_settings.tooltip": "Die rolwiel oor 'n knoppie in die instellings verander die waarde daarvan, soos in vanielje. Af, die wiel blaai net deur die lys.",
|
||||
"horizon.extras.pack_browser.tooltip": "Select Resource Packs kry 'n Voeg by-knoppie wat Modrinth deursoek en 'n pakket in jou pakke-lêergids installeer. Af, die speletjie kontak nooit Modrinth nie.",
|
||||
"horizon.extras.splitscreen.tooltip": "In 'n wêreld kan 'n ander beheerder as 'n nuwe speler aansluit: druk A wanneer dit vra, kies dan 'n profiel en waar om te speel.",
|
||||
"horizon.splitscreen.setup.monitor_unknown": "'n monitor",
|
||||
"horizon.splitscreen.setup.window": "Venster %s",
|
||||
"horizon.splitscreen.setup.new_window": "Nuwe venster",
|
||||
"horizon.splitscreen.setup.fill_monitor": "Vul %s",
|
||||
"horizon.splitscreen.setup.done": "Klaar",
|
||||
"horizon.splitscreen.setup.on": "Aan",
|
||||
"horizon.splitscreen.setup.off": "Af",
|
||||
"horizon.splitscreen.setup.player": "Speler %s",
|
||||
"horizon.splitscreen.setup.revert": "Keer terug",
|
||||
"horizon.splitscreen.setup.apply": "Doen aansoek",
|
||||
"horizon.splitscreen.setup.warning.empty_main": "Niemand speel in Venster 1 nie: dit skuil",
|
||||
"horizon.splitscreen.setup.warning.empty_close": "Niemand speel in Venster %s nie: dit sluit op Toepassing",
|
||||
"horizon.splitscreen.setup.warning.empty_new": "Niemand speel in Venster %s nie: dit sal nie oopmaak nie",
|
||||
"horizon.splitscreen.setup.warning.on_main": "Venster %s vul Venster 1 se monitor: sy spelers speel in Venster 1",
|
||||
"horizon.splitscreen.setup.warning.unplugged": "Venster %s se monitor is nie gekoppel nie: sy spelers speel in Venster 1",
|
||||
"horizon.splitscreen.setup.status.off": "Verdeelskerm is af.",
|
||||
"horizon.splitscreen.setup.status.enable_hint": "Skakel dit aan en druk dan 'n knoppie op 'n ander beheerder in 'n wêreld.",
|
||||
"horizon.splitscreen.setup.status.join_hint": "Druk A om aan te sluit en kies 'n profiel.",
|
||||
"horizon.splitscreen.setup.status.idle": "Gedeelskerm ledig",
|
||||
"horizon.splitscreen.setup.status.alone": "Geen ander plaaslike spelers nie.",
|
||||
"horizon.splitscreen.setup.status.solo_controls": "Solo-kontroles en kontroleerder-terugvoer is onveranderd.",
|
||||
"horizon.splitscreen.setup.window_empty": "Venster %s: leeg",
|
||||
"horizon.splitscreen.setup.note.select": "Klik 'n speler, of sleep dit na 'n venster of 'n monitor.",
|
||||
"horizon.splitscreen.setup.note.apply": "Doen aansoek om op hierdie manier te speel, Keer terug om te ontdoen.",
|
||||
"horizon.splitscreen.setup.note.place": "Waar moet %s speel?",
|
||||
"horizon.splitscreen.setup.this_player": "hierdie speler",
|
||||
"horizon.splitscreen.setup.switch_disabled": "Splitscreen skakel af sodra die ander vertrek het (hul pousekieslys).",
|
||||
"horizon.splitscreen.setup.window_place": "Venster %s (%s van %s)",
|
||||
"horizon.splitscreen.setup.names": "Name",
|
||||
"horizon.splitscreen.setup.identify": "Identifiseer",
|
||||
"horizon.splitscreen.setup.layout.two": "2 deel: %s",
|
||||
"horizon.splitscreen.setup.layout.three": "3 deel: %s",
|
||||
"horizon.splitscreen.setup.layout.two.0": "Outo",
|
||||
"horizon.splitscreen.setup.layout.two.1": "Sy aan sy",
|
||||
"horizon.splitscreen.setup.layout.two.2": "Bo en onder",
|
||||
"horizon.splitscreen.setup.layout.three.0": "Een breed bo-op",
|
||||
"horizon.splitscreen.setup.layout.three.1": "Een lang aan die linkerkant",
|
||||
"horizon.splitscreen.setup.layout.three.2": "Sy aan sy",
|
||||
"horizon.splitscreen.setup.help.player": "Klik of sleep %s om hulle te skuif.",
|
||||
"horizon.splitscreen.setup.help.layout_auto": "%s (naas mekaar wanneer die venster wyd is). Druk om te verander.",
|
||||
"horizon.splitscreen.setup.help.layout": "%s. Druk om te verander.",
|
||||
"horizon.splitscreen.setup.help.switch": "Verdeelskerm vir hierdie rekenaar, aan of af.",
|
||||
"horizon.splitscreen.setup.help.identify": "Elke speler se aansig wys in watter venster dit is.",
|
||||
"horizon.splitscreen.setup.help.names": "Elke speler se naam in die hoek van hul aansig.",
|
||||
"horizon.splitscreen.setup.help.revert": "Ontdoen die veranderinge wat nog nie toegepas is nie.",
|
||||
"horizon.splitscreen.setup.help.apply": "Speel soos die kaart wys.",
|
||||
"horizon.splitscreen.setup.help.unchanged": "Niks het nog verander nie.",
|
||||
"horizon.splitscreen.setup.give_up_pad": "Gee op Pad",
|
||||
"horizon.splitscreen.prompt.controller_connected": "Beheerder gekoppel",
|
||||
"horizon.splitscreen.prompt.controller_disconnected": "Beheerder ontkoppel",
|
||||
"horizon.splitscreen.prompt.controller_disconnected.hint": "Koppel dit weer, of druk 'n knoppie op 'n ander",
|
||||
"horizon.splitscreen.prompt.join.one": "%s wil aansluit",
|
||||
"horizon.splitscreen.prompt.join.many": "%s beheerders wil aansluit",
|
||||
"horizon.splitscreen.prompt.join.yes": "Sluit aan",
|
||||
"horizon.splitscreen.prompt.join.as_p1": "Speel as P1",
|
||||
"horizon.splitscreen.prompt.join.no": "Nee",
|
||||
"horizon.splitscreen.prompt.lost": "%s: beheerder ontkoppel",
|
||||
"horizon.splitscreen.prompt.lost.use_another": "Gebruik 'n ander",
|
||||
"horizon.splitscreen.prompt.lost.leave": "Los",
|
||||
"horizon.splitscreen.setup.help.give_up_pad": "Jou beheerder hou op om jou te bestuur en word aangebied om aan te sluit (A).",
|
||||
"horizon.splitscreen.setup.note.held_at": "%s tot %s: A laat val dit hier, B sit dit terug.",
|
||||
"horizon.splitscreen.setup.note.held": "%s opgetel: links/regs kies waar, A laat val, B sit dit terug.",
|
||||
"Hides models behind opaque blocks. Async checks off-thread; entities tick.": "Versteek modelle agter ondeursigtige blokke. Async kontroleer af-draad; entiteite merk.",
|
||||
"Block Entities": "Blok Entiteite",
|
||||
"Reuses settled boxes; lid movement stays animated.": "Hergebruik afgewikkelde bokse; dekselbeweging bly geanimeerd.",
|
||||
"Signs": "Tekens",
|
||||
"Reuses sign text until text, light, or font changes.": "Hergebruik tekenteks totdat teks, lig of lettertipe verander.",
|
||||
"Decorated Pots": "Versierde potte",
|
||||
"Reuses still pots; break wobble and sherd changes redraw.": "Hergebruik stilpotte; breek wankel en skeur veranderinge herteken.",
|
||||
"Bells": "Klokke",
|
||||
"Reuses still bells; ringing stays animated.": "Hergebruik stilstaande klokke; lui bly geanimeerd.",
|
||||
"Chests": "Kiste",
|
||||
"Reuses closed chests; moving lids stay animated.": "Hergebruik toe kiste; bewegende deksels bly geanimeerd.",
|
||||
"Banners": "Baniere",
|
||||
"Reuses poles; flags and pattern layers still update.": "Hergebruik pale; vlae en patroonlae werk steeds by.",
|
||||
"Copper Golem Statues": "Koper Golem-standbeelde",
|
||||
"Reuses statue geometry until its block state changes.": "Hergebruik standbeeldgeometrie totdat die bloktoestand daarvan verander.",
|
||||
"Leaf Culling": "Blaaruitsny",
|
||||
"Every leaf face. Vanilla's fancy leaves.": "Elke blaar gesig. Vanielje se spoggerige blare.",
|
||||
"Distant Plant Dither": "Verre Plant Dither",
|
||||
"Keeps fine plants in high-res packs from fading far away.": "Hou fyn plante in hoë-resolusie-pakke om ver weg te vervaag.",
|
||||
"Screenshot Quality": "Skermkiekie Kwaliteit",
|
||||
"F2 screenshots at this many times the game's resolution. Higher takes longer.": "F2 skermkiekies teen hierdie baie keer die spel se resolusie. Hoër neem langer.",
|
||||
"Frame Generation": "Raam Generasie",
|
||||
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Toon 'n opgemaakte raam tussen elke twee regte: gladder, 'n bietjie meer vertraging.",
|
||||
"Shadow Cache": "Skadu-kas",
|
||||
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Hergebruik die pakskadukaart wanneer son, uitsig en wêreld ooreenstem. Af by verstek.",
|
||||
"Find BlueMap": "Vind BlueMap",
|
||||
"On servers, finds their BlueMap web map for far terrain. Next join.": "Op bedieners, vind hul BlueMap-webkaart vir ver terrein. Sluit volgende aan.",
|
||||
"BlueMap Radius": "Blou Kaart Radius",
|
||||
"How far around you BlueMap tiles are downloaded. Next join.": "Hoe ver om jou is BlueMap-teëls afgelaai. Sluit volgende aan.",
|
||||
"Synchronous": "Sinchronies",
|
||||
"Asynchronous": "Asinchronies",
|
||||
"Solid": "Solied",
|
||||
"Solid Aggressive": "Stewige aggressiewe",
|
||||
"Hollow": "Hol",
|
||||
"Leaves fully inside a canopy draw solid: fewer see-through layers.": "Blare volledig binne-in 'n blaredak trek solied: minder deurskynende lae.",
|
||||
"Leaves with one open side draw solid too: faster, chunkier edges.": "Blare met een oop kant trek ook solied: vinniger, bonkiger rande.",
|
||||
"Drops faces between leaves: faster, but gaps look through.": "Laat val gesigte tussen blare: vinniger, maar gapings kyk deur.",
|
||||
"Hotbar: Previous Slot": "Hotbar: Vorige Slot",
|
||||
"Hotbar: Next Slot": "Hotbar: Volgende Slot",
|
||||
"horizon.agent.connected.one": "Gekoppel: 1 agentverbinding",
|
||||
"horizon.agent.connected.many": "Gekoppel: %s agentverbindings",
|
||||
"horizon.agent.connected.one_ago": "Gekoppel: 1 agentverbinding, laaste opdrag %s s gelede",
|
||||
"horizon.agent.connected.many_ago": "Gekoppel: %s agentverbindings, laaste opdrag %s s gelede",
|
||||
"horizon.agent.waiting": "Wag vir 'n agent.",
|
||||
"horizon.agent.off": "Af: agente kan nie hierdie speletjie bereik nie.",
|
||||
"horizon.agent.session_opened": "Geopen vir hierdie sessie deur die program wat die speletjie begin het.",
|
||||
"horizon.agent.socket": "Sok: %s (kopieer die besonderhede vir sy volledige pad)",
|
||||
"horizon.agent.local_only": "Slegs plaaslik: agente op hierdie rekenaar, nooit die netwerk nie.",
|
||||
"%s nits": "%s nete",
|
||||
"Auto (%s nits)": "Outo (%s nete)",
|
||||
"%.2f nits": "%.2f nete",
|
||||
"Hides models behind opaque blocks. Entities update. Off with custom packs.": "Versteek modelle agter ondeursigtige blokke. Entiteite-opdatering. Af met pasgemaakte pakke.",
|
||||
"horizon.extras.repro_keys": "Opname kortpaaie",
|
||||
"horizon.extras.repro_keys.tooltip": "F3+R begin of stop 'n repro-opname en F3+E skryf sy bundel. Af by verstek: 'n opname kopieer die wêreld.",
|
||||
"horizon.agent.record_session": "Begin opnamesessie",
|
||||
"horizon.agent.record_stop": "Stop opnamesessie",
|
||||
"horizon.agent.export_repro": "Voer Repro-bundel uit",
|
||||
"horizon.repro.started": "Opname het begin; F3+R stop dit en skryf die bundel.",
|
||||
"horizon.repro.stopped": "Opname gestop: %s",
|
||||
"horizon.repro.bundle": "Repro-bundel: %s",
|
||||
"horizon.repro.no_recording": "Daar is nog geen opname om uit te voer nie.",
|
||||
"horizon.repro.replay_started": "Herhaling het begin: %s",
|
||||
"horizon.pack.addInfo": "Voeg 'n hulpbronpakket van Modrinth by",
|
||||
"horizon.pack.addWorking": "Vra vir Modrinth...",
|
||||
"horizon.pack.addNone": "Geen hulpbronpakke gevind nie",
|
||||
"horizon.pack.addTitle": "Voeg 'n hulpbronpakket by",
|
||||
"horizon.pack.install": "Installeer",
|
||||
"horizon.pack.installing": "Installeer tans",
|
||||
"horizon.pack.installed": "Geïnstalleer",
|
||||
"horizon.pack.search": "Soek",
|
||||
"horizon.pack.searchHint": "Soek Modrinth...",
|
||||
"horizon.pack.filter.realistic": "Realisties",
|
||||
"horizon.pack.filter.simplistic": "Simplistiese",
|
||||
"horizon.pack.filter.vanillaLike": "Vanielje-agtig",
|
||||
"horizon.pack.addFailed": "Die pakkie kon nie bygevoeg word nie: %s",
|
||||
"horizon.extras.header.cosmetics": "Skoonheidsmiddels",
|
||||
"horizon.extras.cosmetics_mine": "My skoonheidsmiddels",
|
||||
"horizon.extras.cosmetics_mine.tooltip": "Versteek: elke Horizon-speler sien jou sonder jou skoonheidsmiddels, jy ingesluit. Dieselfde skakelaar as die lanseerder se klerekas.",
|
||||
"horizon.extras.cosmetics_others": "Ander spelers se skoonheidsmiddels",
|
||||
"horizon.extras.cosmetics_others.tooltip": "Wat ander Horizon-spelers dra: alles, net hul naamplaatjiestyle, of niks. Versteek, die speletjie vra nooit wat hulle dra nie.",
|
||||
"horizon.extras.cosmetics_auras": "Ander se aura's en roetes",
|
||||
"horizon.extras.cosmetics_auras.tooltip": "Auras en roetes wat ander spelers dra. Af by verstek.",
|
||||
"horizon.extras.cosmetics.shown": "Getoon",
|
||||
"horizon.extras.cosmetics.hidden": "Versteek",
|
||||
"horizon.extras.cosmetics.name_tags": "Slegs naamplaatjies"
|
||||
}
|
||||
@@ -1,501 +0,0 @@
|
||||
{
|
||||
"horizon.options.controller": "المراقب المالي",
|
||||
"horizon.options.vibration": "الاهتزاز",
|
||||
"horizon.options.adaptive_triggers": "المشغلات التكيفية",
|
||||
"horizon.options.lightbar": "الشريط الضوئي",
|
||||
"horizon.options.look_sensitivity": "حساسية النظر",
|
||||
"horizon.options.invert_x": "عكس محور النظر X",
|
||||
"horizon.options.invert_y": "عكس محور النظر Y",
|
||||
"horizon.options.sneak_button": "زر التسلل",
|
||||
"horizon.options.move_deadzone": "المنطقة الميتة للحركة",
|
||||
"horizon.options.look_deadzone": "المنطقة الميتة للنظر",
|
||||
"horizon.options.button_glyphs": "الحروف الرسومية للأزرار",
|
||||
"horizon.options.menu_navigation": "التنقل في القائمة",
|
||||
"horizon.options.on_screen_keyboard": "لوحة المفاتيح على الشاشة",
|
||||
"horizon.options.chat_delay": "تأخير الدردشة",
|
||||
"horizon.options.lightbar.health": "الصحة",
|
||||
"horizon.options.lightbar.dimension": "البعد",
|
||||
"horizon.options.glyphs.auto": "تلقائي",
|
||||
"horizon.options.glyphs.xbox": "اكس بوكس",
|
||||
"horizon.options.glyphs.playstation": "بلاي ستيشن",
|
||||
"horizon.options.glyphs.nintendo": "نينتندو",
|
||||
"horizon.options.menu_focus": "التركيز",
|
||||
"horizon.options.menu_pointer": "المؤشر",
|
||||
"horizon.options.keyboard.auto": "تلقائي",
|
||||
"horizon.options.keyboard.steam": "البخار",
|
||||
"horizon.options.unavailable": "هذه الميزة غير متوفرة بعد في Horizon.",
|
||||
"horizon.options.search": "إعدادات البحث...",
|
||||
"horizon.pages.mods": "تعديل",
|
||||
"horizon.pages.shader_packs": "حزم شادر",
|
||||
"horizon.pages.shader_options": "خيارات التظليل",
|
||||
"horizon.pages.controller": "إعدادات وحدة التحكم",
|
||||
"horizon.pages.controller_binds": "ربط وحدة التحكم",
|
||||
"horizon.pages.extras": "إضافات Horizon",
|
||||
"horizon.pages.settings": "إعدادات Horizon",
|
||||
"horizon.pages.agent": "وكيل منظمة العفو الدولية",
|
||||
"horizon.split.leave": "غادر",
|
||||
"horizon.pages.split_settings": "إعدادات تقسيم الشاشة",
|
||||
"horizon.pages.split_profiles": "من يلعب؟",
|
||||
"horizon.pages.split_setup": "إعداد تقسيم الشاشة",
|
||||
"horizon.pages.music": "موسيقى",
|
||||
"horizon.pages.music.menu": "القائمة",
|
||||
"horizon.pages.music.overworld": "العالم الخارجي",
|
||||
"horizon.pages.music.creative": "إبداعي",
|
||||
"horizon.pages.music.underwater": "تحت الماء",
|
||||
"horizon.pages.music.end": "النهاية",
|
||||
"horizon.pages.music.boss": "رئيس",
|
||||
"horizon.pages.music.credits": "الاعتمادات",
|
||||
"horizon.pages.music.overworld_biomes": "المناطق الأحيائية في العالم الخارجي",
|
||||
"horizon.pages.music.nether_biomes": "المناطق الأحيائية السفلى",
|
||||
"horizon.pages.music.end_biomes": "نهاية المناطق الأحيائية",
|
||||
"horizon.pages.music.other": "موسيقى أخرى",
|
||||
"horizon.pages.music.empty": "لا توجد مقطوعات موسيقية: لا يتم تحميل أصوات اللعبة.",
|
||||
"horizon.pages.music.tooltip": "المسارات المضمنة، حسب الموقف: العب واحدة الآن، أو قم بإيقاف تشغيل أي منها حتى تتوقف اللعبة عن اختيارها.",
|
||||
"horizon.pages.settings.tooltip": "إعدادات Horizon الخاصة: الرسومات ووحدات التحكم والإضافات والتعديلات.",
|
||||
"horizon.pages.graphics.tooltip": "الرسومات ببساطة: كل إعداد يوضح تكلفته.",
|
||||
"horizon.pages.graphics_advanced.tooltip": "كل إعدادات الرسومات، بما في ذلك Horizon، وتكلفة كل منها.",
|
||||
"horizon.pages.controller.tooltip": "وحدات التحكم: الأزرار والعصي والاهتزاز والتنقل في القائمة.",
|
||||
"horizon.pages.extras.tooltip": "الإضافات الاختيارية، كلها متوقفة بشكل افتراضي: تظل اللعبة عادية حتى تقوم بتشغيلها.",
|
||||
"horizon.pages.agent.tooltip": "اسمح لوكيل الذكاء الاصطناعي الخاص بك باللعب: عملاء Horizon-ctl وMCP على هذا الكمبيوتر.",
|
||||
"horizon.pages.split_profiles.tooltip": "من يلعب: ملف تعريف محفوظ في مجلد اللعبة هذا.",
|
||||
"horizon.pages.split_setup.tooltip": "النوافذ والشاشات ومن يلعب أين.",
|
||||
"horizon.pages.shader_packs.tooltip": "حزم Shader من مجلد Shaderpacks: الخاصة بشركة Horizon وتلك الموجودة في تنسيق التظليل/. واحد يعمل في وقت واحد.",
|
||||
"horizon.pages.mods.tooltip": "التعديلات المعدة لـ Horizon، من مجلد Horizon-mods. يعمل كل منها في وضع الحماية ويظل مغلقًا حتى تقوم بتشغيله.",
|
||||
"horizon.pages.open.graphics": "الرسومات...",
|
||||
"horizon.pages.open.graphics_advanced": "رسومات متقدمة...",
|
||||
"horizon.pages.open.shader_packs": "حزم التظليل...",
|
||||
"horizon.pages.open.controller": "إعدادات وحدة التحكم...",
|
||||
"horizon.pages.open.split_setup": "شاشة مقسمة...",
|
||||
"horizon.pages.open.extras": "إضافات...",
|
||||
"horizon.pages.open.mods": "تعديل...",
|
||||
"horizon.pages.open.agent": "وكيل الذكاء الاصطناعي...",
|
||||
"horizon.pages.open.horizon_graphics": "رسومات Horizon...",
|
||||
"horizon.pages.open.controller_binds": "ربط وحدة التحكم...",
|
||||
"horizon.pages.open.horizon_settings": "إعدادات Horizon...",
|
||||
"horizon.pages.open.music": "موسيقى...",
|
||||
"horizon.mods.empty": "لا توجد تعديلات: ضع ملفات .hmod في مجلد Horizon-mods",
|
||||
"horizon.mods.stopped": " (متوقف)",
|
||||
"horizon.mods.not_built": " (لم يتم بناؤه)",
|
||||
"horizon.mods.dev": " (ديف)",
|
||||
"horizon.mods.settings": "الإعدادات",
|
||||
"horizon.mods.in_world": " (اترك العالم ليتغير)",
|
||||
"horizon.mods.open_folder": "افتح مجلد التعديل",
|
||||
"horizon.shader_packs.empty": "لا توجد حزم تظليل: ضعها في مجلدshaderpacks",
|
||||
"horizon.shader_options.open": "خيارات التظليل...",
|
||||
"horizon.shader_options.profile": "الملف الشخصي",
|
||||
"horizon.shader_options.loading": "قراءة خيارات حزمة التظليل...",
|
||||
"horizon.agent.copy_details": "نسخ تفاصيل الاتصال",
|
||||
"horizon.multiplayer.join": "انضم",
|
||||
"horizon.multiplayer.cancel_reconnecting": "إلغاء إعادة الاتصال",
|
||||
"horizon.multiplayer.join_friend": "انضم إلى صديق",
|
||||
"horizon.multiplayer.invite_code": "رمز الدعوة",
|
||||
"horizon.multiplayer.relay": "التتابع (اختياري)",
|
||||
"Preset": "محددة مسبقا",
|
||||
"LOD Distance": "مسافة اللد",
|
||||
"Far Detail": "تفاصيل بعيدة",
|
||||
"Cave Culling": "إعدام الكهف",
|
||||
"Entity Culling": "إعدام الكيان",
|
||||
"Chunk Updates": "تحديثات القطعة",
|
||||
"Upload Budget": "تحميل الميزانية",
|
||||
"Sky": "السماء",
|
||||
"Fog": "ضباب",
|
||||
"Weather": "الطقس",
|
||||
"Animated Textures": "القوام المتحركة",
|
||||
"Present Mode": "الوضع الحالي",
|
||||
"Frame Pacing": "سرعة الإطار",
|
||||
"Worker Threads": "خيوط العمال",
|
||||
"Resolution Scale": "مقياس القرار",
|
||||
"Anti-aliasing": "مكافحة التعرج",
|
||||
"Upscaling": "الارتقاء",
|
||||
"Paper White": "ورقة بيضاء",
|
||||
"Peak Brightness": "ذروة السطوع",
|
||||
"Black Level": "المستوى الأسود",
|
||||
"UI Brightness": "سطوع واجهة المستخدم",
|
||||
"Battery Mode": "وضع البطارية",
|
||||
"Graphics API": "واجهة برمجة تطبيقات الرسومات",
|
||||
"Translucent Batching": "الخلط الشفاف",
|
||||
"Performance Overlay": "تراكب الأداء",
|
||||
"Full Detail Distance": "مسافة التفاصيل الكاملة",
|
||||
"On": "على",
|
||||
"Off": "إيقاف",
|
||||
"Unlimited": "غير محدود",
|
||||
"Auto": "تلقائي",
|
||||
"Custom": "مخصص",
|
||||
"Performance": "الأداء",
|
||||
"Balanced": "متوازن",
|
||||
"Quality": "الجودة",
|
||||
"Ultra": "الترا",
|
||||
"Ultra Performance": "أداء فائق",
|
||||
"Native": "أصلي",
|
||||
"Full": "كامل",
|
||||
"Reduced": "مخفض",
|
||||
"Efficient": "كفاءة",
|
||||
"Precise": "دقيق",
|
||||
"Auto (VSync)": "تلقائي (VSync)",
|
||||
"FIFO": "يصرف أولاً",
|
||||
"Mailbox": "صندوق البريد",
|
||||
"Immediate": "فوري",
|
||||
"Vulkan": "فولكان",
|
||||
"DirectX 12": "دايركت اكس 12",
|
||||
"OpenGL": "برنامج OpenGL",
|
||||
"FPS and Graph": "FPS والرسم البياني",
|
||||
"FPS": "إطارا في الثانية",
|
||||
"TAA": "تأ",
|
||||
"FXAA": "فكسا",
|
||||
"No cost": "لا تكلفة",
|
||||
"Low cost": "تكلفة منخفضة",
|
||||
"Medium cost": "تكلفة متوسطة",
|
||||
"High cost": "تكلفة عالية",
|
||||
"Saves time": "يوفر الوقت",
|
||||
"HDR": "تقرير التنمية البشرية",
|
||||
"Graphics": "الرسومات",
|
||||
"Done": "تم",
|
||||
"This feature is not yet available in Horizon.": "هذه الميزة غير متوفرة بعد في Horizon.",
|
||||
"Your display or system doesn't offer HDR output.": "لا توفر شاشتك أو نظامك مخرجات HDR.",
|
||||
"Turn HDR on to use it.": "قم بتشغيل HDR لاستخدامه.",
|
||||
"Your GPU or graphics API doesn't support it.": "وحدة معالجة الرسومات (GPU) أو واجهة برمجة تطبيقات الرسومات الخاصة بك لا تدعمها.",
|
||||
"%s chunks": "%s قطع",
|
||||
"%s per frame": "%s لكل إطار",
|
||||
"%s MiB/frame": "%s MiB/الإطار",
|
||||
"%s KiB/frame": "%s كيلو بايت/الإطار",
|
||||
"Auto (%s)": "تلقائي (%s)",
|
||||
"Full-detail terrain range. LOD Distance can extend local worlds farther.": "نطاق التضاريس كامل التفاصيل. يمكن لمسافة LOD أن تمتد إلى عوالم محلية أبعد.",
|
||||
"Coarse distant terrain in local worlds. Integrated GPUs stop at 128 chunks.": "التضاريس البعيدة الخشنة في العوالم المحلية. تتوقف وحدات معالجة الرسومات المدمجة عند 128 قطعة.",
|
||||
"Reduced draws far terrain coarser: faster on this integrated GPU.": "تم تقليل الرسم على التضاريس البعيدة: بشكل أسرع على وحدة معالجة الرسومات المدمجة هذه.",
|
||||
"Reduced draws far terrain coarser, for GPUs it slows down.": "يتم تقليل الرسوم على التضاريس البعيدة بشكل أكثر خشونة، أما بالنسبة لوحدات معالجة الرسومات فإنها تبطئ.",
|
||||
"Field of view. Wider shows more terrain at once.": "مجال الرؤية. يُظهر العرض الأوسع المزيد من التضاريس في وقت واحد.",
|
||||
"Caps frames per second; a cap saves power and heat.": "قبعات الإطارات في الثانية الواحدة؛ غطاء يوفر الطاقة والحرارة.",
|
||||
"Waits for the display's refresh: no tearing, a little latency.": "في انتظار تحديث الشاشة: بدون تمزق، وقليل من زمن الوصول.",
|
||||
"Size of menus and the HUD.": "حجم القوائم وHUD.",
|
||||
"Sets the options below together. Changing one makes it Custom.": "يضبط الخيارات أدناه معًا. تغيير واحد يجعله مخصصًا.",
|
||||
"Lava, fire and sunlight shine brighter than white. Needs an HDR display.": "الحمم البركانية والنار وأشعة الشمس تتألق أكثر من اللون الأبيض. يحتاج إلى شاشة HDR.",
|
||||
"Your display's darkest black. 0 for OLED; LCDs glow a little.": "اللون الأسود الداكن لشاشتك. 0 لـ OLED؛ تتوهج شاشات LCD قليلاً.",
|
||||
"How bright highlights get. Auto: the peak your system reports.": "كيف تحصل على النقاط البارزة الساطعة. تلقائي: ذروة تقارير النظام الخاص بك.",
|
||||
"How bright white paper and menus look. Auto: your desktop's white.": "كيف تبدو الورق والقوائم البيضاء الزاهية. تلقائي: سطح المكتب الخاص بك باللون الأبيض.",
|
||||
"How bright the HUD and menus are, against paper white.": "مدى سطوع شاشة HUD والقوائم مقابل الورق الأبيض.",
|
||||
"How far mobs, crops and machines keep working. CPU time.": "إلى أي مدى يستمر الغوغاء والمحاصيل والآلات في العمل. وقت وحدة المعالجة المركزية.",
|
||||
"How far away mobs and items are drawn, as a share of normal.": "إلى أي مدى يتم رسم الغوغاء والعناصر، كنسبة من المعدل الطبيعي.",
|
||||
"Skips terrain hidden behind closed sections. Off costs a lot.": "يتخطى التضاريس المخفية خلف الأقسام المغلقة. قبالة تكاليف الكثير.",
|
||||
"Hides entities behind opaque blocks.": "يخفي الكيانات خلف الكتل غير الشفافة.",
|
||||
"No entity occlusion culling.": "لا يوجد إعدام انسداد الكيان.",
|
||||
"CPU occlusion test runs this frame.": "يقوم اختبار انسداد وحدة المعالجة المركزية بتشغيل هذا الإطار.",
|
||||
"Occlusion checks run on a worker thread.": "يتم تشغيل عمليات فحص الانسداد على مؤشر ترابط عامل.",
|
||||
"Meshed sections uploaded per frame. Lower is smoother while loading.": "تم تحميل المقاطع المتشابكة لكل إطار. الجزء السفلي أكثر سلاسة أثناء التحميل.",
|
||||
"Soft shadows in corners. Costs chunk building time only.": "ظلال ناعمة في الزوايا. تكاليف بناء قطعة قطعة فقط.",
|
||||
"Smaller copies of block textures for distant terrain: less shimmer.": "نسخ أصغر من أنسجة الكتل للتضاريس البعيدة: أقل وميضًا.",
|
||||
"Blends grass and water colours across biomes when building chunks.": "يمزج ألوان العشب والماء عبر المناطق الأحيائية عند بناء القطع.",
|
||||
"Cloud layer: off, flat (fast) or 3D (fancy).": "طبقة السحابة: معطلة، مسطحة (سريعة) أو ثلاثية الأبعاد (فاخرة).",
|
||||
"How far clouds reach, in chunks.": "إلى أي مدى تصل السحب، في قطع.",
|
||||
"Draws the sky, sun, moon and stars.": "يرسم السماء والشمس والقمر والنجوم.",
|
||||
"Distance fog. Off shows the edge of the loaded world.": "ضباب المسافة. يظهر Off حافة العالم المحمل.",
|
||||
"Draws rain and snow.": "يرسم المطر والثلج.",
|
||||
"How far rain and snow fall around you.": "إلى أي مدى تسقط الأمطار والثلوج من حولك.",
|
||||
"How many particles effects spawn.": "كم عدد الجسيمات التي تفرخ الآثار.",
|
||||
"Round shadows under mobs and items.": "ظلال مستديرة تحت الغوغاء والعناصر.",
|
||||
"Leaves with see-through gaps. Off draws them solid.": "الأوراق بها فجوات شفافة. قبالة يرسمهم الصلبة.",
|
||||
"Sorts water and glass more exactly behind each other.": "يقوم بفرز الماء والزجاج بشكل أكثر دقة خلف بعضهما البعض.",
|
||||
"Which chunk rebuilds wait for the frame: none, nearby or yours.": "أي قطعة سيتم إعادة بنائها تنتظر الإطار: لا شيء، قريب أو خاص بك.",
|
||||
"Animates water, lava, fire and other moving textures.": "ينعش الماء والحمم والنار وغيرها من القوام المتحرك.",
|
||||
"World pixels per screen pixel. Below 100% is faster on the GPU.": "بكسل العالم لكل بكسل الشاشة. أقل من 100% أسرع على وحدة معالجة الرسومات.",
|
||||
"Smooths jagged edges. FXAA: softer. TAA: smoothest, moving things may trail.": "ينعم الحواف الخشنة. FXAA: أكثر ليونة. TAA: قد تتبع الأشياء الأكثر سلاسة وتحريكًا.",
|
||||
"Renders the world smaller and rebuilds it with TAA. Faster, softer.": "يجعل العالم أصغر حجمًا ويعيد بنائه باستخدام TAA. أسرع وأكثر ليونة.",
|
||||
"HDR output on an HDR display: lava, fire and sunlight shine brighter.": "إخراج HDR على شاشة HDR: تتألق الحمم البركانية والنار وأشعة الشمس بشكل أكثر سطوعًا.",
|
||||
"How frames reach the screen while VSync is off.": "كيفية وصول الإطارات إلى الشاشة أثناء إيقاف تشغيل VSync.",
|
||||
"Efficient sleeps until the next frame; Precise spins, like vanilla.": "ينام بكفاءة حتى الإطار التالي؛ يدور بدقة، مثل الفانيليا.",
|
||||
"Fills the screen (borderless).": "يملأ الشاشة (بلا حدود).",
|
||||
"Brightens dark places.": "يضيء الأماكن الداكنة.",
|
||||
"Lowers the frame rate when minimised, or idle too.": "يخفض معدل الإطارات عند تصغيره أو في وضع الخمول أيضًا.",
|
||||
"Sways the view while walking.": "يتأرجح المنظر أثناء المشي.",
|
||||
"Fades newly loaded chunks in.": "يتلاشى القطع المحملة حديثًا.",
|
||||
"Darkens the screen's edges.": "لتغميق حواف الشاشة.",
|
||||
"Shows when the world saves.": "يظهر عندما ينقذ العالم.",
|
||||
"Shows attack cooldown.": "يظهر تباطؤ الهجوم.",
|
||||
"60 FPS cap (30 in menus or unfocused), half the workers: less power.": "60 إطارًا في الثانية (30 في القوائم أو غير مركزة)، نصف العمال: طاقة أقل.",
|
||||
"Threads that generate, light and mesh chunks. Next start.": "الخيوط التي تولد القطع الخفيفة والشبكية. البداية التالية.",
|
||||
"Vulkan, then DirectX 12 (Windows) or OpenGL (Linux) where it fails. Next start.": "Vulkan، ثم DirectX 12 (Windows) أو OpenGL (Linux) حيث يفشل. البداية التالية.",
|
||||
"Water and glass in one buffer: fewer draw calls, a bit more VRAM. Next start.": "الماء والزجاج في مخزن مؤقت واحد: عدد أقل من مكالمات السحب، والمزيد من VRAM. البداية التالية.",
|
||||
"Mesh data sent to the GPU per frame. Lower is smoother while loading.": "يتم إرسال بيانات الشبكة إلى وحدة معالجة الرسومات لكل إطار. الجزء السفلي أكثر سلاسة أثناء التحميل.",
|
||||
"Frame rate and frame times over the game, with a graph.": "معدل الإطارات وأوقات الإطار خلال اللعبة، مع رسم بياني.",
|
||||
"horizon.multiplayer.finding_friend": "البحث عن عالم صديقك...",
|
||||
"horizon.multiplayer.not_signed_in": "هذا الخادم في وضع الاتصال بالإنترنت. قم بتسجيل الدخول باستخدام حساب Microsoft الخاص بك في Horizon Launcher للانضمام إليه.",
|
||||
"horizon.multiplayer.invalid_address": "عنوان الخادم غير صالح: %s",
|
||||
"horizon.multiplayer.connected_friend_direct": "تواصل مباشرة مع صديقك..",
|
||||
"horizon.multiplayer.connected_friend_relay": "متصل بصديقك من خلال التتابع...",
|
||||
"General": "عام",
|
||||
"Advanced": "متقدم",
|
||||
"Undo": "تراجع",
|
||||
"Search options": "خيارات البحث",
|
||||
"Shortcut": "الاختصار",
|
||||
"Distance": "المسافة",
|
||||
"Display": "عرض",
|
||||
"Interface": "واجهة",
|
||||
"Sky and weather": "السماء والطقس",
|
||||
"Lighting and entities": "الإضاءة والكيانات",
|
||||
"Textures": "القوام",
|
||||
"Image": "صورة",
|
||||
"Chunk loading": "تحميل قطعة",
|
||||
"Culling": "إعدام",
|
||||
"Power": "القوة",
|
||||
"Frames": "إطارات",
|
||||
"Overlay": "تراكب",
|
||||
"Output": "الإخراج",
|
||||
"Brightness": "السطوع",
|
||||
"How far you see, the window and the HUD.": "إلى أي مدى ترى النافذة وشاشة HUD.",
|
||||
"The sky and weather, lighting, textures and the image.": "السماء والطقس والإضاءة والقوام والصورة.",
|
||||
"Chunk loading, culling and power use.": "تحميل القطع وإعدامها واستخدام الطاقة.",
|
||||
"How frames reach the screen, and the performance overlay.": "كيفية وصول الإطارات إلى الشاشة وتراكب الأداء.",
|
||||
"Brighter-than-white output on an HDR display.": "إخراج أكثر سطوعًا من اللون الأبيض على شاشة HDR.",
|
||||
"Results for \"%s\"": "نتائج ل \"%s\"",
|
||||
"No settings match.": "لا توجد إعدادات متطابقة.",
|
||||
"horizon.title.not_affiliated": "لا ينتمي إلى Mojang أو Microsoft",
|
||||
"horizon.extras.header.lighting": "الإضاءة",
|
||||
"horizon.extras.header.third_person": "شخص ثالث",
|
||||
"horizon.extras.header.particles": "الجسيمات",
|
||||
"horizon.extras.header.inventory": "المخزون",
|
||||
"horizon.extras.header.chat": "الدردشة",
|
||||
"horizon.extras.header.multiplayer": "متعددة اللاعبين",
|
||||
"horizon.extras.header.sound": "الصوت",
|
||||
"horizon.extras.header.menus": "القوائم",
|
||||
"horizon.extras.fullbright": "فولبرايت",
|
||||
"horizon.extras.dynamic_lights": "أضواء ديناميكية",
|
||||
"horizon.extras.own_name_tag": "علامة الاسم الخاصة",
|
||||
"horizon.extras.hide_block_particles": "إخفاء جزيئات كسر الكتلة",
|
||||
"horizon.extras.auto_reconnect": "إعادة الاتصال التلقائي",
|
||||
"horizon.extras.ui_sound_reserve": "احتفظ بقنوات لأصوات واجهة المستخدم",
|
||||
"horizon.extras.music_page": "صفحة الموسيقى",
|
||||
"horizon.extras.mouse_tweaks": "الضوابط بتعديل الماوس",
|
||||
"horizon.extras.mouse_tweaks_wheel": "عجلة التمرير تنقل العناصر",
|
||||
"horizon.extras.sort": "أزرار الفرز",
|
||||
"horizon.extras.sort_key": "مفتاح الفرز",
|
||||
"horizon.extras.no_ai": "لا يوجد ذكاء اصطناعي",
|
||||
"horizon.extras.agent_control": "مراقبة الوكيل",
|
||||
"horizon.extras.discord_presence": "وجود الخلاف الغني",
|
||||
"horizon.extras.splitscreen": "تقسيم الشاشة",
|
||||
"horizon.extras.chat_animation": "دردشة سلسة",
|
||||
"horizon.extras.chat_indicator": "مؤشر الرسالة",
|
||||
"horizon.extras.wheel_cycles_settings": "عجلة التمرير تغير الإعدادات",
|
||||
"horizon.extras.pack_browser": "متصفح حزمة الموارد",
|
||||
"horizon.extras.fullbright.tooltip": "إظهار الكتل والكيانات في الضوء الكامل، حتى في المناطق المظلمة.",
|
||||
"horizon.extras.dynamic_lights.tooltip": "تم حمل المشاعل ومصادر الضوء الأخرى أو إسقاطها أو إشعال النار فيها على الكتل المحيطة بها.",
|
||||
"horizon.extras.own_name_tag.tooltip": "يظهر اسمك فوق رأسك بصيغة الغائب، كما يراه اللاعبون الآخرون. يكون مخفيًا عندما يخفي الخادم أسماء فريقك أو عندما تكون غير مرئي.",
|
||||
"horizon.extras.hide_block_particles.tooltip": "يخفي جزيئات الحطام التي تنفجر من كتلة مكسورة وتتفتت من تلك التي قمت بتعدينها. وتبقى الجزيئات الأخرى.",
|
||||
"horizon.extras.auto_reconnect.tooltip": "إعادة الانضمام إلى الخادم بعد انقطاع الاتصال أو إعادة التشغيل، وليس بعد ركلة أو حظر أبدًا.",
|
||||
"horizon.extras.ui_sound_reserve.tooltip": "تترك الأصوات في العالم 16 قناة صوتية من أصل 247 قناة مجانية، لذا فإن طوفانًا منها (انفجار كبير) لا يمكنه إسكات نقرات القائمة.",
|
||||
"horizon.extras.music_page.tooltip": "تحصل الموسيقى والأصوات على صفحة موسيقى...: قم بتشغيل أي مسار مضمن الآن، أو قم بإيقاف تشغيله حتى تتوقف اللعبة عن اختياره. لا شيء يتغير حتى تقوم بإيقاف تشغيل المسار.",
|
||||
"horizon.extras.mouse_tweaks.tooltip": "يؤدي السحب الأيمن إلى وضع عنصر واحد في كل فتحة، بينما يقوم السحب الأيسر بتجميع العنصر المحفوظ، بينما يقوم Shift + السحب الأيسر بنقل كل فتحة.",
|
||||
"horizon.extras.mouse_tweaks_wheel.tooltip": "يرسل الجزء السفلي عنصرًا واحدًا من الفتحة إلى الجانب الآخر، بينما يقوم الجزء العلوي بإحضار عنصر واحد مرة أخرى.",
|
||||
"horizon.extras.sort.tooltip": "يقوم بفرز المخزون والصناديق والبراميل وصناديق النهاية وصناديق الشولكر بترتيب علامات تبويب إبداعية، ودمج الأكوام.",
|
||||
"horizon.extras.sort_key.tooltip": "فرز الجزء الموجود تحت المؤشر. انقر، ثم اضغط على مفتاح؛ الهروب يفككه.",
|
||||
"horizon.extras.no_ai.tooltip": "يخفي ويوقف تشغيل كل ميزات الذكاء الاصطناعي: التحكم بالوكيل، Horizon-ctl وخوادم MCP. مفتاح المشغل أيضًا.",
|
||||
"horizon.extras.agent_control.tooltip": "يمكن لوكيل الذكاء الاصطناعي الخاص بك لعب هذه اللعبة من خلال Horizon-ctl أو عميل MCP، على هذا الكمبيوتر فقط.",
|
||||
"horizon.extras.discord_presence.tooltip": "يعرض Playing Horizon على ملف تعريف Discord الخاص بك أثناء تشغيل Discord: القوائم، أو اللاعب الفردي أو المتعدد، والبعد ووقت اللعب. لا يوجد أبدًا اسم عالمي أو عنوان خادم.",
|
||||
"Calibrate...": "معايرة...",
|
||||
"Calibrate Anyway...": "معايرة على أية حال...",
|
||||
"Use Automatic Setup": "استخدم الإعداد التلقائي",
|
||||
"Shortcut: %s": "الاختصار: %s",
|
||||
"Next": "التالي",
|
||||
"HDR Calibration %s/%s: %s": "معايرة HDR %s/%s: %s",
|
||||
"Sets the three below full screen, one test picture at a time.": "لتعيين الصور الثلاثة أدناه بملء الشاشة، صورة اختبارية واحدة في كل مرة.",
|
||||
"A key that opens this screen from the game. Not bound at first.": "المفتاح الذي يفتح هذه الشاشة من اللعبة. غير ملزمة في البداية.",
|
||||
"Your display didn't say how bright it gets: calibrate to set it up.": "لم توضح شاشتك مدى سطوعها: قم بالمعايرة لإعدادها.",
|
||||
"Already set up for your display. Only calibrate if HDR looks wrong.": "تم إعداده بالفعل للعرض الخاص بك. قم بالمعايرة فقط إذا كان تقرير HDR يبدو خاطئًا.",
|
||||
"Puts Peak Brightness and Paper White back to your display's setup.": "يعيد ذروة السطوع والورق الأبيض إلى إعداد شاشتك.",
|
||||
"Set up for your display. Leave on Auto unless it looks wrong.": "قم بإعداد العرض الخاص بك. اتركه على الوضع التلقائي ما لم يبدو خاطئًا.",
|
||||
"Lower it until the logo just disappears into the black.": "قم بخفضه حتى يختفي الشعار باللون الأسود.",
|
||||
"Raise it until the logo just disappears into the bright square.": "ارفعه حتى يختفي الشعار في المربع المشرق.",
|
||||
"Make the card a white that's comfortable to look at for a long time, in the light you play in.": "اجعل البطاقة بيضاء اللون بحيث تكون مريحة عند النظر إليها لفترة طويلة، في الضوء الذي تلعب فيه.",
|
||||
"horizon.splitscreen.profiles.forget": "ننسى",
|
||||
"horizon.splitscreen.profiles.create": "إنشاء",
|
||||
"horizon.splitscreen.profiles.you": " - أنت",
|
||||
"horizon.splitscreen.profiles.forget_named": "ننسى %s",
|
||||
"horizon.splitscreen.profiles.previous": "السابق",
|
||||
"horizon.splitscreen.profiles.next": "التالي",
|
||||
"horizon.splitscreen.profiles.new_button": "ملف تعريف جديد...",
|
||||
"horizon.splitscreen.profiles.forget_title": "هل نسيت الملف الشخصي؟",
|
||||
"horizon.splitscreen.profiles.forget_leaves": "فإنه يترك قائمة هذا المثيل.",
|
||||
"horizon.splitscreen.profiles.forget_kept": "يتم الاحتفاظ بالتقدم العالمي والإعدادات.",
|
||||
"horizon.splitscreen.profiles.new_title": "ملف تعريف جديد",
|
||||
"horizon.splitscreen.profiles.name_hint": "الاسم (من 3 إلى 16 حرفًا ورقمًا _)",
|
||||
"horizon.splitscreen.profiles.switch_title": "تبديل الملف الشخصي",
|
||||
"horizon.splitscreen.profiles.offline_note": "تنتمي الملفات الشخصية غير المتصلة بالإنترنت إلى هذه الحالة",
|
||||
"horizon.splitscreen.profiles.move": "تحرك حيث تلعب",
|
||||
"horizon.splitscreen.profiles.move_place": "تحرك حيث تلعب: %s",
|
||||
"horizon.friends.title": "العب مع الأصدقاء",
|
||||
"horizon.friends.hint": "يقوم الأصدقاء بلصق الرمز بعد زر الأصدقاء على شاشة العنوان، أو في الاتصال المباشر، أو على صفحة التشغيل الخاصة بالمشغل.",
|
||||
"horizon.friends.copied": "منقول",
|
||||
"horizon.friends.copy_code": "نسخ الرمز",
|
||||
"horizon.friends.stop_sharing": "توقف عن المشاركة",
|
||||
"horizon.friends.none_yet": "لا يوجد أصدقاء هنا بعد.",
|
||||
"horizon.friends.here_now": "هنا الآن: %s",
|
||||
"horizon.friends.share_intro": "شارك هذا العالم خلف رمز الدعوة: فهو يفتح على شبكة LAN، وينضم الأصدقاء على الإنترنت باستخدام الرمز. لا يوجد خادم خاص بنا متورط.",
|
||||
"horizon.friends.start_sharing": "ابدأ المشاركة",
|
||||
"horizon.friends.troubleshooting": "استكشاف الأخطاء وإصلاحها...",
|
||||
"horizon.friends.stopped": "تم إيقاف مشاركة رمز الدعوة.",
|
||||
"horizon.friends.short_code_changed": "استمر شخص ما في تجربة رمز قصير خاطئ، لذلك تغير.",
|
||||
"horizon.friends.wrong_code": "حاول شخص ما رمزا خاطئا.",
|
||||
"horizon.friends.code_ready": "رمز الدعوة الخاص بك جاهز: قائمة الألعاب، العب مع الأصدقاء. انسخها وأرسلها إلى أصدقائك.",
|
||||
"horizon.friends.share_failed": "لا يمكن مشاركة العالم: %s",
|
||||
"horizon.friends.finding_route": "التعرف على كيفية وصول الأصدقاء إليك...",
|
||||
"horizon.osk.shift": "التحول",
|
||||
"horizon.osk.space": "الفضاء",
|
||||
"horizon.osk.delete": "حذف",
|
||||
"horizon.osk.type": "اكتب",
|
||||
"horizon.osk.symbols": "الرموز",
|
||||
"horizon.osk.close": "إغلاق",
|
||||
"horizon.extras.chat_animation.tooltip": "ينزلق خط الدردشة الجديد من اليسار بينما يتلاشى للداخل، بدلاً من أن يظهر كاملاً.",
|
||||
"horizon.extras.chat_indicator.tooltip": "الشريط الملون الموجود على يسار خط الدردشة.",
|
||||
"horizon.extras.wheel_cycles_settings.tooltip": "عجلة التمرير فوق زر في الإعدادات تغير قيمته، كما هو الحال في الفانيليا. مع إيقاف التشغيل، تقوم العجلة بتمرير القائمة فقط.",
|
||||
"horizon.extras.pack_browser.tooltip": "تحديد حزم الموارد يحصل على زر \"إضافة\" الذي يبحث في Modrinth ويقوم بتثبيت حزمة في مجلد الحزم الخاص بك. مع إيقاف التشغيل، لن تتصل اللعبة أبدًا بـ Modrinth.",
|
||||
"horizon.extras.splitscreen.tooltip": "في عالم ما، يمكن لوحدة تحكم أخرى الانضمام كلاعب جديد: اضغط على A عندما يُطلب منك ذلك، ثم اختر ملفًا شخصيًا ومكان اللعب.",
|
||||
"horizon.splitscreen.setup.monitor": "مراقب %s",
|
||||
"horizon.splitscreen.setup.monitor_unknown": "شاشة",
|
||||
"horizon.splitscreen.setup.window": "نافذة %s",
|
||||
"horizon.splitscreen.setup.new_window": "نافذة جديدة",
|
||||
"horizon.splitscreen.setup.fill_monitor": "املأ %s",
|
||||
"horizon.splitscreen.setup.done": "تم",
|
||||
"horizon.splitscreen.setup.on": "على",
|
||||
"horizon.splitscreen.setup.off": "إيقاف",
|
||||
"horizon.splitscreen.setup.player": "اللاعب %s",
|
||||
"horizon.splitscreen.setup.revert": "العودة",
|
||||
"horizon.splitscreen.setup.apply": "تطبيق",
|
||||
"horizon.splitscreen.setup.warning.empty_main": "لا أحد يلعب في النافذة 1: إنه يخفي",
|
||||
"horizon.splitscreen.setup.warning.empty_close": "لا أحد يلعب في النافذة %s: يتم إغلاقها عند تطبيق",
|
||||
"horizon.splitscreen.setup.warning.empty_new": "لا أحد يلعب في النافذة %s: لن يتم فتحها",
|
||||
"horizon.splitscreen.setup.warning.on_main": "النافذة %s تملأ شاشة النافذة 1: يلعب لاعبوها في النافذة 1",
|
||||
"horizon.splitscreen.setup.warning.unplugged": "شاشة النافذة %s غير متصلة: يتم تشغيل مشغلاتها في النافذة 1",
|
||||
"horizon.splitscreen.setup.status.off": "تقسيم الشاشة معطل.",
|
||||
"horizon.splitscreen.setup.status.enable_hint": "قم بتشغيله، ثم اضغط على زر على وحدة تحكم أخرى في العالم.",
|
||||
"horizon.splitscreen.setup.status.join_hint": "اضغط على A للانضمام واختيار ملف التعريف.",
|
||||
"horizon.splitscreen.setup.status.idle": "تقسيم الشاشة خاملاً",
|
||||
"horizon.splitscreen.setup.status.alone": "لا يوجد لاعبين محليين آخرين.",
|
||||
"horizon.splitscreen.setup.status.solo_controls": "لم تتغير عناصر التحكم الفردية وملاحظات وحدة التحكم.",
|
||||
"horizon.splitscreen.setup.window_empty": "النافذة %s: فارغة",
|
||||
"horizon.splitscreen.setup.note.select": "انقر فوق أحد اللاعبين، أو اسحبه إلى نافذة أو شاشة.",
|
||||
"horizon.splitscreen.setup.note.apply": "تقدم للعب بهذه الطريقة، ثم ارجع للتراجع.",
|
||||
"horizon.splitscreen.setup.note.place": "أين يجب أن يلعب %s؟",
|
||||
"horizon.splitscreen.setup.this_player": "هذا اللاعب",
|
||||
"horizon.splitscreen.setup.switch_disabled": "يتم إيقاف تشغيل الشاشة المنقسمة بمجرد مغادرة الآخرين (قائمة الإيقاف المؤقت الخاصة بهم).",
|
||||
"horizon.splitscreen.setup.window_place": "النافذة %s (%s من %s)",
|
||||
"horizon.splitscreen.setup.names": "الأسماء",
|
||||
"horizon.splitscreen.setup.identify": "تحديد",
|
||||
"horizon.splitscreen.setup.layout.two": "2 المشاركة: %s",
|
||||
"horizon.splitscreen.setup.layout.three": "3 المشاركة: %s",
|
||||
"horizon.splitscreen.setup.layout.two.0": "تلقائي",
|
||||
"horizon.splitscreen.setup.layout.two.1": "جنبا إلى جنب",
|
||||
"horizon.splitscreen.setup.layout.two.2": "أعلى وأسفل",
|
||||
"horizon.splitscreen.setup.layout.three.0": "واحدة واسعة في الأعلى",
|
||||
"horizon.splitscreen.setup.layout.three.1": "واحد طويل القامة على اليسار",
|
||||
"horizon.splitscreen.setup.layout.three.2": "جنبا إلى جنب",
|
||||
"horizon.splitscreen.setup.help.player": "انقر أو اسحب %s لتحريكها.",
|
||||
"horizon.splitscreen.setup.help.layout_auto": "%s (جنبًا إلى جنب عندما تكون النافذة واسعة). اضغط للتغيير.",
|
||||
"horizon.splitscreen.setup.help.layout": "%s. اضغط للتغيير.",
|
||||
"horizon.splitscreen.setup.help.switch": "تقسيم الشاشة لهذا الكمبيوتر، أو إيقاف تشغيله.",
|
||||
"horizon.splitscreen.setup.help.identify": "تُظهر طريقة عرض كل لاعب النافذة التي يوجد بها.",
|
||||
"horizon.splitscreen.setup.help.names": "اسم كل لاعب في زاوية رؤيته.",
|
||||
"horizon.splitscreen.setup.help.revert": "التراجع عن التغييرات التي لم يتم تطبيقها بعد.",
|
||||
"horizon.splitscreen.setup.help.apply": "العب بالطريقة التي تظهر بها الخريطة.",
|
||||
"horizon.splitscreen.setup.help.unchanged": "لم يتغير شيء بعد.",
|
||||
"horizon.splitscreen.setup.give_up_pad": "التخلي عن الوسادة",
|
||||
"horizon.splitscreen.prompt.controller_connected": "وحدة التحكم متصلة",
|
||||
"horizon.splitscreen.prompt.controller_disconnected": "وحدة التحكم مفصولة",
|
||||
"horizon.splitscreen.prompt.controller_disconnected.hint": "أعد توصيله، أو اضغط على زر على آخر",
|
||||
"horizon.splitscreen.prompt.join.one": "%s يريد الانضمام",
|
||||
"horizon.splitscreen.prompt.join.many": "%s وحدات التحكم تريد الانضمام",
|
||||
"horizon.splitscreen.prompt.join.yes": "انضم",
|
||||
"horizon.splitscreen.prompt.join.as_p1": "العب بشخصية P1",
|
||||
"horizon.splitscreen.prompt.join.no": "لا",
|
||||
"horizon.splitscreen.prompt.lost": "%s: تم فصل وحدة التحكم",
|
||||
"horizon.splitscreen.prompt.lost.use_another": "استخدم آخر",
|
||||
"horizon.splitscreen.prompt.lost.leave": "غادر",
|
||||
"horizon.splitscreen.setup.help.give_up_pad": "تتوقف وحدة التحكم الخاصة بك عن قيادتك ويُعرض عليك الانضمام (أ).",
|
||||
"horizon.splitscreen.setup.note.held_at": "%s إلى %s: يسقطها A هنا، ويعيدها B.",
|
||||
"horizon.splitscreen.setup.note.held": "%s تم الالتقاط: يسار/يمين يختار المكان، يسقط A، ويعيده B.",
|
||||
"Hides models behind opaque blocks. Async checks off-thread; entities tick.": "يخفي النماذج خلف الكتل المعتمة. غير متزامن يتحقق من خارج الموضوع؛ علامة الكيانات.",
|
||||
"Block Entities": "حظر الكيانات",
|
||||
"Shulker Boxes": "صناديق شوكر",
|
||||
"Reuses settled boxes; lid movement stays animated.": "إعادة استخدام الصناديق المستقرة؛ تبقى حركة الغطاء متحركة.",
|
||||
"Signs": "علامات",
|
||||
"Reuses sign text until text, light, or font changes.": "يعيد استخدام نص التوقيع حتى يتغير النص أو الضوء أو الخط.",
|
||||
"Decorated Pots": "الأواني المزخرفة",
|
||||
"Reuses still pots; break wobble and sherd changes redraw.": "إعادة استخدام الأواني الثابتة؛ كسر تمايل وتغييرات الكسر إعادة رسم.",
|
||||
"Bells": "أجراس",
|
||||
"Reuses still bells; ringing stays animated.": "إعادة استخدام الأجراس الثابتة؛ يبقى الرنين متحركًا.",
|
||||
"Chests": "الصناديق",
|
||||
"Reuses closed chests; moving lids stay animated.": "إعادة استخدام الصناديق المغلقة؛ الأغطية المتحركة تبقى متحركة.",
|
||||
"Banners": "لافتات",
|
||||
"Reuses poles; flags and pattern layers still update.": "إعادة استخدام الأعمدة؛ لا يزال يتم تحديث الأعلام وطبقات الأنماط.",
|
||||
"Copper Golem Statues": "تماثيل النحاس جوليم",
|
||||
"Reuses statue geometry until its block state changes.": "يعيد استخدام هندسة التمثال حتى تتغير حالة الكتلة الخاصة به.",
|
||||
"Leaf Culling": "إعدام الأوراق",
|
||||
"Every leaf face. Vanilla's fancy leaves.": "وجه كل ورقة. أوراق الفانيليا الفاخرة.",
|
||||
"Distant Plant Dither": "ثبات النبات البعيد",
|
||||
"Keeps fine plants in high-res packs from fading far away.": "يحافظ على النباتات الجميلة في عبوات عالية الدقة من البهتان بعيدًا.",
|
||||
"Screenshot Quality": "جودة لقطة الشاشة",
|
||||
"F2 screenshots at this many times the game's resolution. Higher takes longer.": "لقطات شاشة F2 بهذا دقة اللعبة عدة مرات. أعلى يستغرق وقتا أطول.",
|
||||
"Frame Generation": "جيل الإطار",
|
||||
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "يُظهر إطارًا مكوّنًا بين كل إطارين حقيقيين: أكثر سلاسة وتأخرًا قليلًا.",
|
||||
"Shadow Cache": "ذاكرة التخزين المؤقت الظل",
|
||||
"Reuses the pack shadow map when sun, view and world match. Off by default.": "يعيد استخدام خريطة ظل الحزمة عندما تتطابق الشمس والعرض والعالم. إيقاف بشكل افتراضي.",
|
||||
"Find BlueMap": "ابحث عن بلو ماب",
|
||||
"On servers, finds their BlueMap web map for far terrain. Next join.": "على الخوادم، يتم العثور على خريطة الويب BlueMap الخاصة بهم للتضاريس البعيدة. الانضمام التالي.",
|
||||
"BlueMap Radius": "نصف قطر الخريطة الزرقاء",
|
||||
"How far around you BlueMap tiles are downloaded. Next join.": "إلى أي مدى يتم تنزيل بلاط BlueMap من حولك. الانضمام التالي.",
|
||||
"Synchronous": "متزامن",
|
||||
"Asynchronous": "غير متزامن",
|
||||
"Solid": "صلب",
|
||||
"Solid Aggressive": "العدوانية الصلبة",
|
||||
"Hollow": "جوفاء",
|
||||
"Leaves fully inside a canopy draw solid: fewer see-through layers.": "يتم رسم الأوراق بالكامل داخل المظلة بشكل صلب: طبقات شفافة أقل.",
|
||||
"Leaves with one open side draw solid too: faster, chunkier edges.": "الأوراق ذات جانب واحد مفتوح ترسم بشكل صلب أيضًا: حواف أسرع وأكثر كثافة.",
|
||||
"Drops faces between leaves: faster, but gaps look through.": "يسقط الوجوه بين الأوراق: بشكل أسرع، لكن الفجوات تبدو من خلالها.",
|
||||
"Hotbar: Previous Slot": "Hotbar: الفتحة السابقة",
|
||||
"Hotbar: Next Slot": "Hotbar: الفتحة التالية",
|
||||
"horizon.agent.connected.one": "متصل: اتصال وكيل واحد",
|
||||
"horizon.agent.connected.many": "متصل: %s اتصالات الوكيل",
|
||||
"horizon.agent.connected.one_ago": "متصل: اتصال وكيل واحد، آخر أمر %s مضت",
|
||||
"horizon.agent.connected.many_ago": "متصل: %s اتصالات الوكيل، آخر أمر %s مضت",
|
||||
"horizon.agent.waiting": "في انتظار وكيل.",
|
||||
"horizon.agent.off": "إيقاف: لا يمكن للعملاء الوصول إلى هذه اللعبة.",
|
||||
"horizon.agent.session_opened": "تم فتحه لهذه الجلسة بواسطة البرنامج الذي بدأ اللعبة.",
|
||||
"horizon.agent.socket": "المقبس: %s (انسخ التفاصيل لمساره الكامل)",
|
||||
"horizon.agent.local_only": "محلي فقط: الوكلاء على هذا الكمبيوتر، وليس الشبكة أبدًا.",
|
||||
"%s nits": "%s القمل",
|
||||
"Auto (%s nits)": "تلقائي (%s شمعة في المتر المربع)",
|
||||
"%.2f nits": "%.2f القمل",
|
||||
"Hides models behind opaque blocks. Entities update. Off with custom packs.": "يخفي النماذج خلف الكتل المعتمة. تحديث الكيانات مع حزم مخصصة.",
|
||||
"horizon.extras.repro_keys": "تسجيل الاختصارات",
|
||||
"horizon.extras.repro_keys.tooltip": "يقوم F3+R ببدء التسجيل النسخي أو إيقافه ويقوم F3+E بكتابة الحزمة الخاصة به. متوقف بشكل افتراضي: تسجيل ينسخ العالم.",
|
||||
"horizon.agent.record_session": "ابدأ جلسة التسجيل",
|
||||
"horizon.agent.record_stop": "إيقاف جلسة التسجيل",
|
||||
"horizon.agent.export_repro": "تصدير حزمة النسخ المتماثل",
|
||||
"horizon.repro.started": "بدأ التسجيل؛ F3+R يوقفه ويكتب الحزمة.",
|
||||
"horizon.repro.stopped": "توقف التسجيل: %s",
|
||||
"horizon.repro.bundle": "حزمة النسخ: %s",
|
||||
"horizon.repro.error": "استنساخ: %s",
|
||||
"horizon.repro.no_recording": "لا يوجد تسجيل للتصدير حتى الآن.",
|
||||
"horizon.repro.replay_started": "بدأت إعادة التشغيل: %s",
|
||||
"horizon.pack.addInfo": "إضافة حزمة موارد من Modrinth",
|
||||
"horizon.pack.addWorking": "يسأل مودرنث...",
|
||||
"horizon.pack.addNone": "لم يتم العثور على حزم الموارد",
|
||||
"horizon.pack.addTitle": "أضف حزمة الموارد",
|
||||
"horizon.pack.install": "تثبيت",
|
||||
"horizon.pack.installing": "التثبيت",
|
||||
"horizon.pack.installed": "تم التثبيت",
|
||||
"horizon.pack.search": "بحث",
|
||||
"horizon.pack.searchHint": "بحث مودرينث...",
|
||||
"horizon.pack.filters": "المرشحات",
|
||||
"horizon.pack.filter.realistic": "واقعية",
|
||||
"horizon.pack.filter.simplistic": "تبسيط",
|
||||
"horizon.pack.filter.vanillaLike": "تشبه الفانيليا",
|
||||
"horizon.pack.addFailed": "لا يمكن إضافة الحزمة: %s",
|
||||
"horizon.extras.header.cosmetics": "مستحضرات التجميل",
|
||||
"horizon.extras.cosmetics_mine": "مستحضرات التجميل الخاصة بي",
|
||||
"horizon.extras.cosmetics_mine.tooltip": "مخفي: يراك كل لاعب في Horizon بدون مستحضرات التجميل الخاصة بك، بما في ذلك أنت. نفس مفتاح خزانة المشغل.",
|
||||
"horizon.extras.cosmetics_others": "مستحضرات التجميل للاعبين الآخرين",
|
||||
"horizon.extras.cosmetics_others.tooltip": "ما يرتديه لاعبو Horizon الآخرون: كل شيء، فقط أنماط بطاقة الاسم الخاصة بهم، أو لا شيء. مخفيًا، اللعبة لا تسأل أبدًا عما يرتدونه.",
|
||||
"horizon.extras.cosmetics_auras": "هالات ومسارات الآخرين",
|
||||
"horizon.extras.cosmetics_auras.tooltip": "الهالات والمسارات التي يرتديها اللاعبون الآخرون. إيقاف بشكل افتراضي.",
|
||||
"horizon.extras.cosmetics.shown": "معروض",
|
||||
"horizon.extras.cosmetics.hidden": "مخفي",
|
||||
"horizon.extras.cosmetics.name_tags": "علامات الاسم فقط"
|
||||
}
|
||||
@@ -1,473 +0,0 @@
|
||||
{
|
||||
"horizon.options.controller": "Nəzarətçi",
|
||||
"horizon.options.vibration": "Vibrasiya",
|
||||
"horizon.options.adaptive_triggers": "Adaptiv tətiklər",
|
||||
"horizon.options.lightbar": "İşıq çubuğu",
|
||||
"horizon.options.look_sensitivity": "Baxış həssaslığı",
|
||||
"horizon.options.invert_x": "Tərs baxış X",
|
||||
"horizon.options.invert_y": "Tərs baxış Y",
|
||||
"horizon.options.sneak_button": "Gizlənmə düyməsi",
|
||||
"horizon.options.move_deadzone": "Hərəkət ölü zonası",
|
||||
"horizon.options.look_deadzone": "Baxış ölü zonası",
|
||||
"horizon.options.button_glyphs": "Düymə Qlifləri",
|
||||
"horizon.options.menu_navigation": "Menyu Naviqasiyası",
|
||||
"horizon.options.on_screen_keyboard": "Ekran Klaviaturası",
|
||||
"horizon.options.chat_delay": "Söhbət Gecikməsi",
|
||||
"horizon.options.lightbar.health": "Sağlamlıq",
|
||||
"horizon.options.lightbar.dimension": "Ölçü",
|
||||
"horizon.options.glyphs.auto": "Avtomatik",
|
||||
"horizon.options.menu_focus": "Fokus",
|
||||
"horizon.options.menu_pointer": "Göstərici",
|
||||
"horizon.options.keyboard.auto": "Avtomatik",
|
||||
"horizon.options.unavailable": "Bu funksiya hələ Horizon ərazisində mövcud deyil.",
|
||||
"horizon.options.search": "Axtarış parametrləri...",
|
||||
"horizon.pages.mods": "Modlar",
|
||||
"horizon.pages.shader_packs": "Shader Paketləri",
|
||||
"horizon.pages.shader_options": "Shader Seçimləri",
|
||||
"horizon.pages.controller": "Nəzarətçi Parametrləri",
|
||||
"horizon.pages.controller_binds": "Nəzarətçi Bağlayır",
|
||||
"horizon.pages.extras": "Horizon Əlavələr",
|
||||
"horizon.pages.settings": "Horizon Parametrlər",
|
||||
"horizon.pages.agent": "AI agenti",
|
||||
"horizon.split.leave": "tərk et",
|
||||
"horizon.pages.split_settings": "Splitscreen Parametrləri",
|
||||
"horizon.pages.split_profiles": "Kim oynayır?",
|
||||
"horizon.pages.split_setup": "Splitscreen Quraşdırma",
|
||||
"horizon.pages.music": "Musiqi",
|
||||
"horizon.pages.music.menu": "Menyu",
|
||||
"horizon.pages.music.overworld": "Yerüstü dünya",
|
||||
"horizon.pages.music.creative": "Yaradıcı",
|
||||
"horizon.pages.music.underwater": "Sualtı",
|
||||
"horizon.pages.music.end": "Son",
|
||||
"horizon.pages.music.boss": "patron",
|
||||
"horizon.pages.music.credits": "Kreditlər",
|
||||
"horizon.pages.music.overworld_biomes": "Yerüstü biomlar",
|
||||
"horizon.pages.music.nether_biomes": "Nether biomları",
|
||||
"horizon.pages.music.end_biomes": "Biomları bitir",
|
||||
"horizon.pages.music.other": "Digər musiqi",
|
||||
"horizon.pages.music.empty": "Musiqi parçaları yoxdur: oyunun səsləri yüklənmir.",
|
||||
"horizon.pages.music.tooltip": "Vəziyyətə görə quraşdırılmış treklər: birini indi oynayın və ya hər hansı birini söndürün ki, oyun onu seçməyi dayandırsın.",
|
||||
"horizon.pages.settings.tooltip": "Horizon öz parametrləri: qrafika, nəzarətçilər, əlavələr və modlar.",
|
||||
"horizon.pages.graphics.tooltip": "Qrafika, sadəcə olaraq: hər parametr nəyə başa gəldiyini bildirir.",
|
||||
"horizon.pages.graphics_advanced.tooltip": "Hər bir qrafik parametri, Horizon daxil edilir, hər birinin dəyəri ilə.",
|
||||
"horizon.pages.controller.tooltip": "Nəzarətçilər: onların düymələri, çubuqları, vibrasiya və menyu naviqasiyası.",
|
||||
"horizon.pages.extras.tooltip": "Könüllü əlavələr, hamısı defolt olaraq söndürülür: siz birini yandırana qədər oyun vanil olaraq qalır.",
|
||||
"horizon.pages.agent.tooltip": "Öz AI agentinizin oynamasına icazə verin: bu kompüterdə horizon-ctl və MCP müştəriləri.",
|
||||
"horizon.pages.split_profiles.tooltip": "Kim oynayır: bu oyun qovluğunda saxlanılan profil.",
|
||||
"horizon.pages.split_setup.tooltip": "Pəncərələr və monitorlar və kim harada oynayır.",
|
||||
"horizon.pages.shader_packs.tooltip": "shaderpacks qovluğundan şader paketləri: Horizon öz və shaders/formatında olanlar. Bir dəfə qaçır.",
|
||||
"horizon.pages.mods.tooltip": "Horizon-mods qovluğundan Horizon üçün hazırlanmış modlar. Hər biri sandboxda işləyir və siz onu yandırana qədər sönür.",
|
||||
"horizon.pages.open.graphics": "Qrafika...",
|
||||
"horizon.pages.open.graphics_advanced": "Qabaqcıl Qrafika...",
|
||||
"horizon.pages.open.shader_packs": "Shader Paketləri...",
|
||||
"horizon.pages.open.controller": "Nəzarətçi Parametrləri...",
|
||||
"horizon.pages.open.split_setup": "Ayrılmış ekran...",
|
||||
"horizon.pages.open.extras": "Əlavələr...",
|
||||
"horizon.pages.open.mods": "Modlar...",
|
||||
"horizon.pages.open.horizon_graphics": "Horizon Qrafika...",
|
||||
"horizon.pages.open.controller_binds": "Nəzarətçi Bağlayır...",
|
||||
"horizon.pages.open.horizon_settings": "Horizon Parametrlər...",
|
||||
"horizon.pages.open.music": "Musiqi...",
|
||||
"horizon.mods.empty": "Mod yoxdur: .hmod fayllarını horizon-mods qovluğuna qoyun",
|
||||
"horizon.mods.stopped": " (Dayandı)",
|
||||
"horizon.mods.not_built": " (tikilməyib)",
|
||||
"horizon.mods.settings": "Parametrlər",
|
||||
"horizon.mods.in_world": " (Dəyişmək üçün dünyanı tərk et)",
|
||||
"horizon.mods.open_folder": "Mod Qovluğunu açın",
|
||||
"horizon.shader_packs.empty": "Şeyder paketləri yoxdur: onları shaderpacks qovluğuna qoyun",
|
||||
"horizon.shader_options.open": "Shader Seçimləri...",
|
||||
"horizon.shader_options.profile": "Profil",
|
||||
"horizon.shader_options.loading": "Şeyder paketinin seçimləri oxunur...",
|
||||
"horizon.agent.copy_details": "Bağlantı təfərrüatlarını kopyalayın",
|
||||
"horizon.multiplayer.join": "Qoşulun",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Yenidən qoşulmanı ləğv edin",
|
||||
"horizon.multiplayer.join_friend": "Dosta Qoşulun",
|
||||
"horizon.multiplayer.invite_code": "Dəvət Kodu",
|
||||
"horizon.multiplayer.relay": "Rele (isteğe bağlı)",
|
||||
"Preset": "Əvvəlcədən təyin",
|
||||
"LOD Distance": "LOD məsafəsi",
|
||||
"Far Detail": "Uzaq Detal",
|
||||
"Cave Culling": "Mağaraların kəsilməsi",
|
||||
"Entity Culling": "Müəssisə Culling",
|
||||
"Chunk Updates": "Parça Yeniləmələri",
|
||||
"Upload Budget": "Büdcə yükləyin",
|
||||
"Sky": "Göy",
|
||||
"Fog": "Duman",
|
||||
"Weather": "Hava",
|
||||
"Animated Textures": "Animasiyalı Dokular",
|
||||
"Present Mode": "İndiki rejim",
|
||||
"Worker Threads": "İşçi ipləri",
|
||||
"Resolution Scale": "Qətnamə Ölçüsü",
|
||||
"Upscaling": "Təkmilləşdirmə",
|
||||
"Paper White": "Kağız Ağ",
|
||||
"Peak Brightness": "Pik Parlaqlıq",
|
||||
"Black Level": "Qara Səviyyə",
|
||||
"UI Brightness": "UI Parlaqlığı",
|
||||
"Battery Mode": "Batareya rejimi",
|
||||
"Graphics API": "Qrafik API",
|
||||
"Translucent Batching": "Şəffaf Batching",
|
||||
"Performance Overlay": "Performans örtüyü",
|
||||
"Full Detail Distance": "Tam Detal Məsafə",
|
||||
"On": "Aktiv",
|
||||
"Off": "Söndür",
|
||||
"Unlimited": "Limitsiz",
|
||||
"Auto": "Avtomatik",
|
||||
"Custom": "Xüsusi",
|
||||
"Performance": "Performans",
|
||||
"Balanced": "Balanslı",
|
||||
"Quality": "Keyfiyyət",
|
||||
"Ultra Performance": "Ultra Performans",
|
||||
"Native": "Yerli",
|
||||
"Full": "Tam",
|
||||
"Reduced": "Azaldılmış",
|
||||
"Efficient": "Səmərəli",
|
||||
"Precise": "Dəqiq",
|
||||
"Auto (VSync)": "Avtomatik (VSync)",
|
||||
"FIFO": "FİFO",
|
||||
"Mailbox": "Poçt qutusu",
|
||||
"Immediate": "Dərhal",
|
||||
"FPS and Graph": "FPS və Qrafik",
|
||||
"No cost": "Xərc yoxdur",
|
||||
"Low cost": "Aşağı qiymət",
|
||||
"Medium cost": "Orta qiymət",
|
||||
"High cost": "Yüksək qiymət",
|
||||
"Saves time": "Vaxta qənaət edir",
|
||||
"Graphics": "Qrafika",
|
||||
"Done": "Bitdi",
|
||||
"This feature is not yet available in Horizon.": "Bu funksiya hələ Horizon ərazisində mövcud deyil.",
|
||||
"Your display or system doesn't offer HDR output.": "Ekranınız və ya sisteminiz HDR çıxışı təklif etmir.",
|
||||
"Turn HDR on to use it.": "İstifadə etmək üçün HDR-ni yandırın.",
|
||||
"Your GPU or graphics API doesn't support it.": "GPU və ya qrafik API onu dəstəkləmir.",
|
||||
"%s chunks": "%s parçalar",
|
||||
"%s per frame": "%s hər çərçivə üçün",
|
||||
"%s MiB/frame": "%s MiB/çərçivə",
|
||||
"%s KiB/frame": "%s KiB/çərçivə",
|
||||
"Auto (%s)": "Avtomatik (%s)",
|
||||
"Full-detail terrain range. LOD Distance can extend local worlds farther.": "Tam detallı ərazi diapazonu. LOD məsafəsi yerli dünyaları daha da uzada bilər.",
|
||||
"Coarse distant terrain in local worlds. Integrated GPUs stop at 128 chunks.": "Yerli dünyalarda kobud uzaq ərazi. İnteqrasiya edilmiş GPU-lar 128 hissədə dayanır.",
|
||||
"Reduced draws far terrain coarser: faster on this integrated GPU.": "Azaldılmış ərazini daha qaba çəkir: bu inteqrasiya olunmuş GPU-da daha sürətli.",
|
||||
"Reduced draws far terrain coarser, for GPUs it slows down.": "Azaldılmış ərazini daha qaba çəkir, GPU-lar üçün isə yavaşlayır.",
|
||||
"Field of view. Wider shows more terrain at once.": "Baxış sahəsi. Daha geniş bir anda daha çox ərazini göstərir.",
|
||||
"Caps frames per second; a cap saves power and heat.": "Saniyədə kadrların sayı; qapaq enerjiyə və istiliyə qənaət edir.",
|
||||
"Waits for the display's refresh: no tearing, a little latency.": "Ekranın təzələnməsini gözləyir: cırılma, bir az gecikmə.",
|
||||
"Size of menus and the HUD.": "Menyuların ölçüsü və HUD.",
|
||||
"Sets the options below together. Changing one makes it Custom.": "Aşağıdakı seçimləri birlikdə təyin edir. Birinin dəyişdirilməsi onu Xüsusi edir.",
|
||||
"Lava, fire and sunlight shine brighter than white. Needs an HDR display.": "Lava, od və günəş işığı ağdan daha parlaq parlayır. HDR displey lazımdır.",
|
||||
"Your display's darkest black. 0 for OLED; LCDs glow a little.": "Ekranınız ən tünd qaradır. OLED üçün 0; LCD-lər bir az parlayır.",
|
||||
"How bright highlights get. Auto: the peak your system reports.": "Parlaq məqamlar necə əldə edilir. Avtomatik: sisteminizin ən yüksək hesabatı.",
|
||||
"How bright white paper and menus look. Auto: your desktop's white.": "Ağ kağız və menyular necə parlaq görünür. Avtomatik: masaüstünüz ağ rəngdədir.",
|
||||
"How bright the HUD and menus are, against paper white.": "HUD və menyular ağ kağıza qarşı nə qədər parlaqdır.",
|
||||
"How far mobs, crops and machines keep working. CPU time.": "Kütlələr, məhsullar və maşınlar nə qədər işləməyə davam edir. CPU vaxtı.",
|
||||
"How far away mobs and items are drawn, as a share of normal.": "Nə qədər uzağa mob və əşyalar çəkilir, normal bir pay olaraq.",
|
||||
"Skips terrain hidden behind closed sections. Off costs a lot.": "Qapalı hissələrin arxasında gizlənmiş ərazini ötür. Sıxılma çox baha başa gəlir.",
|
||||
"Hides entities behind opaque blocks.": "Obyektləri qeyri-şəffaf blokların arxasında gizlədir.",
|
||||
"No entity occlusion culling.": "Heç bir obyektin okklyuziyası məhv edilmir.",
|
||||
"CPU occlusion test runs this frame.": "CPU tıxanma testi bu çərçivəni idarə edir.",
|
||||
"Occlusion checks run on a worker thread.": "Tıxanma yoxlanışı işçi ipində aparılır.",
|
||||
"Meshed sections uploaded per frame. Lower is smoother while loading.": "Meshed bölmələr hər çərçivəyə yüklənir. Yükləyərkən aşağı daha hamar olur.",
|
||||
"Soft shadows in corners. Costs chunk building time only.": "Künclərdə yumşaq kölgələr. Yalnız tikinti vaxtı xərcləri.",
|
||||
"Smaller copies of block textures for distant terrain: less shimmer.": "Uzaq ərazilər üçün blok dokuların daha kiçik nüsxələri: daha az parıltı.",
|
||||
"Blends grass and water colours across biomes when building chunks.": "Parçalar qurarkən biomlar arasında ot və su rənglərini qarışdırır.",
|
||||
"Cloud layer: off, flat (fast) or 3D (fancy).": "Bulud təbəqəsi: sönük, düz (sürətli) və ya 3D (fantastik).",
|
||||
"How far clouds reach, in chunks.": "Buludlar nə qədər uzağa çatır, parça-parça.",
|
||||
"Draws the sky, sun, moon and stars.": "Göyü, günəşi, ayı və ulduzları çəkir.",
|
||||
"Distance fog. Off shows the edge of the loaded world.": "Məsafə dumanı. Off yüklənmiş dünyanın kənarını göstərir.",
|
||||
"Draws rain and snow.": "Yağış və qar çəkir.",
|
||||
"How far rain and snow fall around you.": "Ətrafınıza yağış və qar nə qədər yağır.",
|
||||
"How many particles effects spawn.": "Nə qədər hissəcik effektləri kürü verir.",
|
||||
"Round shadows under mobs and items.": "Kütlələrin və əşyaların altında dairəvi kölgələr.",
|
||||
"Leaves with see-through gaps. Off draws them solid.": "Yarpaqları göz qabağındadır. Off onları möhkəm çəkir.",
|
||||
"Sorts water and glass more exactly behind each other.": "Suyu və şüşəni bir-birinin arxasında daha dəqiq şəkildə sıralayır.",
|
||||
"Which chunk rebuilds wait for the frame: none, nearby or yours.": "Hansı hissənin yenidən qurulması çərçivəni gözləyir: heç biri, yaxınlıqda və ya sizin.",
|
||||
"Animates water, lava, fire and other moving textures.": "Su, lava, od və digər hərəkətli dokuları canlandırır.",
|
||||
"Smooths jagged edges. FXAA: softer. TAA: smoothest, moving things may trail.": "Kəsik kənarları hamarlayır. FXAA: daha yumşaq. TAA: ən hamar, hərəkətli şeylər cığır çəkə bilər.",
|
||||
"Renders the world smaller and rebuilds it with TAA. Faster, softer.": "Dünyanı daha kiçik edir və TAA ilə yenidən qurur. Daha sürətli, daha yumşaq.",
|
||||
"HDR output on an HDR display: lava, fire and sunlight shine brighter.": "HDR displeydə HDR çıxışı: lava, yanğın və günəş işığı daha parlaq parlayır.",
|
||||
"How frames reach the screen while VSync is off.": "VSync söndürüldükdə çərçivələr ekrana necə çatır.",
|
||||
"Efficient sleeps until the next frame; Precise spins, like vanilla.": "Növbəti kadra qədər səmərəli yuxular; Vanil kimi dəqiq spinlər.",
|
||||
"Fills the screen (borderless).": "Ekranı doldurur (sərhədsiz).",
|
||||
"Brightens dark places.": "Qaranlıq yerləri işıqlandırır.",
|
||||
"Lowers the frame rate when minimised, or idle too.": "Minimumlaşdırıldıqda və ya boş olduqda kadr sürətini azaldır.",
|
||||
"Sways the view while walking.": "Gəzərkən mənzərəni yelləyir.",
|
||||
"Fades newly loaded chunks in.": "Yeni yüklənmiş parçaları sönür.",
|
||||
"Darkens the screen's edges.": "Ekranın kənarlarını qaraldır.",
|
||||
"Shows when the world saves.": "Dünyanın nə vaxt xilas etdiyini göstərir.",
|
||||
"Shows attack cooldown.": "Hücumun soyumasını göstərir.",
|
||||
"60 FPS cap (30 in menus or unfocused), half the workers: less power.": "60 FPS qapağı (menyularda 30 və ya diqqətsiz), işçilərin yarısı: daha az güc.",
|
||||
"Threads that generate, light and mesh chunks. Next start.": "Yaradan iplər, yüngül və mesh parçaları. Növbəti başlanğıc.",
|
||||
"Vulkan, then DirectX 12 (Windows) or OpenGL (Linux) where it fails. Next start.": "Vulkan, sonra uğursuz olduğu DirectX 12 (Windows) və ya OpenGL (Linux). Növbəti başlanğıc.",
|
||||
"Water and glass in one buffer: fewer draw calls, a bit more VRAM. Next start.": "Bir buferdə su və şüşə: daha az çəkiliş zəngi, bir az daha çox VRAM. Növbəti başlanğıc.",
|
||||
"Mesh data sent to the GPU per frame. Lower is smoother while loading.": "Hər kadra GPU-ya göndərilən mesh datası. Yükləyərkən aşağı daha hamar olur.",
|
||||
"Frame rate and frame times over the game, with a graph.": "Qrafik ilə oyun üzrə çərçivə sürəti və kadr vaxtları.",
|
||||
"horizon.multiplayer.finding_friend": "Dostunuzun dünyasını tapmaq...",
|
||||
"horizon.multiplayer.not_signed_in": "Bu server onlayn rejimdədir. Qoşulmaq üçün Horizon başlatma cihazında Microsoft hesabınızla daxil olun.",
|
||||
"horizon.multiplayer.invalid_address": "Yanlış server ünvanı: %s",
|
||||
"horizon.multiplayer.connected_friend_direct": "Dostunuza birbaşa qoşulun...",
|
||||
"horizon.multiplayer.connected_friend_relay": "Rele vasitəsilə dostunuza qoşulun...",
|
||||
"Advanced": "Qabaqcıl",
|
||||
"Undo": "Geri alın",
|
||||
"Search options": "Axtarış seçimləri",
|
||||
"Shortcut": "Qısayol",
|
||||
"Distance": "Məsafə",
|
||||
"Display": "Ekran",
|
||||
"Interface": "İnterfeys",
|
||||
"Sky and weather": "Səma və hava",
|
||||
"Lighting and entities": "İşıqlandırma və obyektlər",
|
||||
"Textures": "Dokular",
|
||||
"Image": "Şəkil",
|
||||
"Chunk loading": "Parça yükləmə",
|
||||
"Power": "Güc",
|
||||
"Frames": "Çərçivələr",
|
||||
"Output": "Çıxış",
|
||||
"Brightness": "Parlaqlıq",
|
||||
"How far you see, the window and the HUD.": "Nə qədər görürsən, pəncərə və HUD.",
|
||||
"The sky and weather, lighting, textures and the image.": "Səma və hava, işıqlandırma, faktura və görüntü.",
|
||||
"Chunk loading, culling and power use.": "Parçaların yüklənməsi, kəsilməsi və enerji istifadəsi.",
|
||||
"How frames reach the screen, and the performance overlay.": "Çərçivələrin ekrana necə çatması və performans örtüyü.",
|
||||
"Brighter-than-white output on an HDR display.": "HDR displeydə ağdan daha parlaq çıxış.",
|
||||
"Results for \"%s\"": "\"%s\" üçün nəticələr",
|
||||
"No settings match.": "Heç bir parametr uyğun gəlmir.",
|
||||
"horizon.title.not_affiliated": "Mojang və ya Microsoft ilə əlaqəli deyil",
|
||||
"horizon.extras.header.lighting": "İşıqlandırma",
|
||||
"horizon.extras.header.third_person": "Üçüncü şəxs",
|
||||
"horizon.extras.header.particles": "hissəciklər",
|
||||
"horizon.extras.header.inventory": "İnventar",
|
||||
"horizon.extras.header.chat": "Söhbət",
|
||||
"horizon.extras.header.sound": "Səs",
|
||||
"horizon.extras.header.menus": "Menyular",
|
||||
"horizon.extras.fullbright": "Tam parlaq",
|
||||
"horizon.extras.dynamic_lights": "Dinamik işıqlar",
|
||||
"horizon.extras.own_name_tag": "Öz ad etiketi",
|
||||
"horizon.extras.hide_block_particles": "Blok qırılan hissəcikləri gizlədin",
|
||||
"horizon.extras.auto_reconnect": "Avtomatik yenidən qoşulun",
|
||||
"horizon.extras.ui_sound_reserve": "UI səsləri üçün kanalları saxlayın",
|
||||
"horizon.extras.music_page": "Musiqi səhifəsi",
|
||||
"horizon.extras.mouse_tweaks": "Mouse Tweaks nəzarətləri",
|
||||
"horizon.extras.mouse_tweaks_wheel": "Sürüşdürmə çarxı elementləri hərəkət etdirir",
|
||||
"horizon.extras.sort": "Çeşidləmə düymələri",
|
||||
"horizon.extras.sort_key": "Sıralama açarı",
|
||||
"horizon.extras.no_ai": "AI yoxdur",
|
||||
"horizon.extras.agent_control": "Agent Nəzarəti",
|
||||
"horizon.extras.chat_animation": "Rahat söhbət",
|
||||
"horizon.extras.chat_indicator": "Mesaj göstəricisi",
|
||||
"horizon.extras.wheel_cycles_settings": "Sürüşdürmə çarxı parametrləri dəyişir",
|
||||
"horizon.extras.pack_browser": "Resurs paketi brauzeri",
|
||||
"horizon.extras.fullbright.tooltip": "Blokları və obyektləri hətta qaranlıq yerlərdə də tam işıqda göstərin.",
|
||||
"horizon.extras.dynamic_lights.tooltip": "Məşəllər və digər işıq mənbələri ətrafdakı blokları tutmuş, yerə atmış və ya odda yandırır.",
|
||||
"horizon.extras.own_name_tag.tooltip": "Başqa oyunçuların gördüyü kimi adınızı başınızın üstündə üçüncü şəxsdə göstərir. Server komandanızın adlarını gizlətdikdə və ya siz görünməz olduğunuzda gizlidir.",
|
||||
"horizon.extras.hide_block_particles.tooltip": "Sınıq blokdan qopan və mənimsədiyiniz blokdan parçalanan zibil hissəciklərini gizlədir. Digər hissəciklər qalır.",
|
||||
"horizon.extras.auto_reconnect.tooltip": "Heç bir zərbə və ya qadağadan sonra itən əlaqə və ya yenidən başladıqdan sonra serverə yenidən qoşulur.",
|
||||
"horizon.extras.ui_sound_reserve.tooltip": "Dünyadakı səslər 247 səs kanalından 16-nı pulsuz buraxır, buna görə də onların selinin (böyük partlayış) menyu kliklərini susdura bilməz.",
|
||||
"horizon.extras.music_page.tooltip": "Musiqi və Səslər Musiqi... səhifəsi əldə edir: hər hansı daxili treki indi çalın və ya birini söndürün ki, oyun onu seçməyi dayandırsın. Siz treki söndürməyincə heç nə dəyişməyəcək.",
|
||||
"horizon.extras.mouse_tweaks.tooltip": "Sağa sürükləmək hər yuvaya bir yerləşdirir, sola sürükləmək saxlanılan elementi toplayır, Shift + sola sürükləmək hər bir yuvaya köçür.",
|
||||
"horizon.extras.mouse_tweaks_wheel.tooltip": "Aşağı yuvanın bir elementini digər tərəfə göndərir, yuxarı birini geri gətirir.",
|
||||
"horizon.extras.sort.tooltip": "İnventar, sandıqlar, çəlləklər, ender sandıqlar və şulker qutularını yaradıcı nişan sırası ilə çeşidləyir, yığınları birləşdirin.",
|
||||
"horizon.extras.sort_key.tooltip": "Kursorun altındakı hissəni çeşidləyir. Basın, sonra düyməni basın; Escape onu açır.",
|
||||
"horizon.extras.no_ai.tooltip": "Hər bir AI funksiyasını gizlədir və söndürür: agent nəzarəti, horizon-ctl və MCP serverləri. Başlatıcının açarı da.",
|
||||
"horizon.extras.agent_control.tooltip": "Öz AI agentiniz bu oyunu horizon-ctl və ya MCP müştəri vasitəsilə yalnız bu kompüterdə oynaya bilər.",
|
||||
"horizon.extras.discord_presence.tooltip": "Discord işləyərkən Discord profilinizdə Horizon Playing göstərilir: menyular, tək oyunçu və ya multiplayer, ölçü və oynanılan vaxt. Heç vaxt dünyanın adı və ya server ünvanı.",
|
||||
"Calibrate...": "Kalibrləmə...",
|
||||
"Calibrate Anyway...": "İstənilən halda kalibrləyin...",
|
||||
"Use Automatic Setup": "Avtomatik Quraşdırmadan istifadə edin",
|
||||
"Shortcut: %s": "Qısayol: %s",
|
||||
"Next": "Sonrakı",
|
||||
"HDR Calibration %s/%s: %s": "HDR Kalibrləmə %s/%s: %s",
|
||||
"Sets the three below full screen, one test picture at a time.": "Aşağıdakı üç tam ekranı, hər dəfə bir sınaq şəklini təyin edir.",
|
||||
"A key that opens this screen from the game. Not bound at first.": "Oyundan bu ekranı açan açar. Əvvəlcə bağlı deyil.",
|
||||
"Your display didn't say how bright it gets: calibrate to set it up.": "Ekranınız nə qədər parlaq olduğunu deməyib: onu qurmaq üçün kalibrləyin.",
|
||||
"Already set up for your display. Only calibrate if HDR looks wrong.": "Ekranınız üçün artıq qurulub. Yalnız HDR səhv görünürsə kalibrləyin.",
|
||||
"Puts Peak Brightness and Paper White back to your display's setup.": "Pik Parlaqlığı və Kağız Ağı ekranınızın quraşdırılmasına qaytarır.",
|
||||
"Set up for your display. Leave on Auto unless it looks wrong.": "Ekranınız üçün qurun. Səhv görünməsə, Auto-da buraxın.",
|
||||
"Lower it until the logo just disappears into the black.": "Loqo qara rəngdə yox olana qədər onu endirin.",
|
||||
"Raise it until the logo just disappears into the bright square.": "Loqo parlaq kvadratda yox olana qədər onu qaldırın.",
|
||||
"Make the card a white that's comfortable to look at for a long time, in the light you play in.": "Kartı oynadığınız işıqda uzun müddət baxmaq üçün rahat olan ağ rəngə çevirin.",
|
||||
"horizon.splitscreen.profiles.forget": "Unut",
|
||||
"horizon.splitscreen.profiles.create": "Yaradın",
|
||||
"horizon.splitscreen.profiles.you": " - sən",
|
||||
"horizon.splitscreen.profiles.forget_named": "Unut %s",
|
||||
"horizon.splitscreen.profiles.previous": "Əvvəlki",
|
||||
"horizon.splitscreen.profiles.next": "Sonrakı",
|
||||
"horizon.splitscreen.profiles.new_button": "Yeni Profil...",
|
||||
"horizon.splitscreen.profiles.forget_title": "Profili unutmusunuz?",
|
||||
"horizon.splitscreen.profiles.forget_leaves": "Bu nümunənin siyahısını tərk edir.",
|
||||
"horizon.splitscreen.profiles.forget_kept": "Dünya tərəqqisi və parametrlər saxlanılır.",
|
||||
"horizon.splitscreen.profiles.new_title": "Yeni Profil",
|
||||
"horizon.splitscreen.profiles.name_hint": "Ad (3-16 hərf, rəqəm, _)",
|
||||
"horizon.splitscreen.profiles.switch_title": "Profilə keçin",
|
||||
"horizon.splitscreen.profiles.offline_note": "Oflayn profillər bu instansiyaya aiddir",
|
||||
"horizon.splitscreen.profiles.move": "Oynadığınız yerdə hərəkət edin",
|
||||
"horizon.splitscreen.profiles.move_place": "Oynadığınız yerə köçürün: %s",
|
||||
"horizon.friends.title": "Dostlarla oynayın",
|
||||
"horizon.friends.hint": "Dostlar kodu başlıq ekranının Dostlar düyməsindən sonra, Birbaşa Bağlantıda və ya başlatma qurğusunun Play səhifəsində yerləşdirin.",
|
||||
"horizon.friends.copied": "Kopyalandı",
|
||||
"horizon.friends.copy_code": "Kodu kopyalayın",
|
||||
"horizon.friends.stop_sharing": "Paylaşmağı dayandırın",
|
||||
"horizon.friends.none_yet": "Burada hələ dost yoxdur.",
|
||||
"horizon.friends.here_now": "İndi burada: %s",
|
||||
"horizon.friends.share_intro": "Bu dünyanı dəvət kodunun arxasında paylaşın: o, LAN-a açılır və internetdəki dostlar kodla qoşulur. Heç bir serverimiz iştirak etmir.",
|
||||
"horizon.friends.start_sharing": "Paylaşmağa başlayın",
|
||||
"horizon.friends.troubleshooting": "Problem həll olunur...",
|
||||
"horizon.friends.stopped": "Dəvət kodunu paylaşmağı dayandırdı.",
|
||||
"horizon.friends.short_code_changed": "Kimsə səhv qısa kodu sınamağa davam etdi, ona görə də dəyişdi.",
|
||||
"horizon.friends.wrong_code": "Kimsə səhv kodu sınadı.",
|
||||
"horizon.friends.code_ready": "Dəvət kodunuz hazırdır: Oyun Menyu, Dostlarla Oyna. Kopyalayın və dostlarınıza göndərin.",
|
||||
"horizon.friends.share_failed": "Dünyanı paylaşmaq mümkün olmadı: %s",
|
||||
"horizon.friends.finding_route": "Dostların sizinlə necə əlaqə saxlaya biləcəyi tapılır...",
|
||||
"horizon.osk.space": "Kosmos",
|
||||
"horizon.osk.delete": "Sil",
|
||||
"horizon.osk.type": "Növ",
|
||||
"horizon.osk.symbols": "Simvollar",
|
||||
"horizon.osk.close": "Bağlayın",
|
||||
"horizon.extras.chat_animation.tooltip": "Yeni söhbət xətti bütöv görünmək əvəzinə soldan içəri sürüşür.",
|
||||
"horizon.extras.chat_indicator.tooltip": "Söhbət xəttinin solunda olan rəngli bar.",
|
||||
"horizon.extras.wheel_cycles_settings.tooltip": "Parametrlərdəki düymənin üzərindəki sürüşdürmə çarxı vanildə olduğu kimi dəyərini dəyişir. Söndürülmüş, təkər yalnız siyahını sürüşdürür.",
|
||||
"horizon.extras.pack_browser.tooltip": "Resurs Paketlərini seçin Modrinth-də axtarış aparan və paketlər qovluğuna paket quraşdıran Əlavə et düyməsini alır. Off, oyun Modrinthlə heç vaxt əlaqə saxlamır.",
|
||||
"horizon.extras.splitscreen.tooltip": "Dünyada başqa bir nəzarətçi yeni oyunçu kimi qoşula bilər: soruşduqda A düyməsini basın, sonra profil və harada oynayacağınızı seçin.",
|
||||
"horizon.splitscreen.setup.monitor_unknown": "monitor",
|
||||
"horizon.splitscreen.setup.window": "Pəncərə %s",
|
||||
"horizon.splitscreen.setup.new_window": "Yeni pəncərə",
|
||||
"horizon.splitscreen.setup.fill_monitor": "%s doldurun",
|
||||
"horizon.splitscreen.setup.done": "Bitdi",
|
||||
"horizon.splitscreen.setup.on": "Aktiv",
|
||||
"horizon.splitscreen.setup.off": "Söndür",
|
||||
"horizon.splitscreen.setup.player": "Oyunçu %s",
|
||||
"horizon.splitscreen.setup.revert": "Geri qaytarın",
|
||||
"horizon.splitscreen.setup.apply": "Müraciət edin",
|
||||
"horizon.splitscreen.setup.warning.empty_main": "Window 1-də heç kim oynamır: gizlənir",
|
||||
"horizon.splitscreen.setup.warning.empty_close": "%s Pəncərəsində heç kim oynamır: Tətbiq edərkən bağlanır",
|
||||
"horizon.splitscreen.setup.warning.empty_new": "%s pəncərəsində heç kim oynamır: açılmayacaq",
|
||||
"horizon.splitscreen.setup.warning.on_main": "Pəncərə %s Pəncərə 1-in monitorunu doldurur: onun oyunçuları Pəncərə 1-də oynayır",
|
||||
"horizon.splitscreen.setup.warning.unplugged": "Pəncərənin %s monitoru bağlı deyil: onun oyunçuları Window 1-də oynayır",
|
||||
"horizon.splitscreen.setup.status.off": "Splitscreen deaktivdir.",
|
||||
"horizon.splitscreen.setup.status.enable_hint": "Onu yandırın, sonra dünyanın başqa bir nəzarətçisində düyməni basın.",
|
||||
"horizon.splitscreen.setup.status.join_hint": "Qoşulmaq və profil seçmək üçün A düyməsini basın.",
|
||||
"horizon.splitscreen.setup.status.idle": "Splitscreen boş",
|
||||
"horizon.splitscreen.setup.status.alone": "Başqa yerli oyunçu yoxdur.",
|
||||
"horizon.splitscreen.setup.status.solo_controls": "Solo nəzarət və nəzarətçi rəyi dəyişməzdir.",
|
||||
"horizon.splitscreen.setup.window_empty": "Pəncərə %s: boş",
|
||||
"horizon.splitscreen.setup.note.select": "Oyunçuya klikləyin və ya onu pəncərəyə və ya monitora sürükləyin.",
|
||||
"horizon.splitscreen.setup.note.apply": "Bu şəkildə oynamaq üçün müraciət edin, ləğv etmək üçün geri qaytarın.",
|
||||
"horizon.splitscreen.setup.note.place": "%s harada oynamalıdır?",
|
||||
"horizon.splitscreen.setup.this_player": "bu oyunçu",
|
||||
"horizon.splitscreen.setup.switch_disabled": "Digərləri ayrıldıqdan sonra Splitscreen sönür (pauza menyusu).",
|
||||
"horizon.splitscreen.setup.window_place": "Pəncərə %s (%s/%s)",
|
||||
"horizon.splitscreen.setup.names": "Adlar",
|
||||
"horizon.splitscreen.setup.identify": "Müəyyən etmək",
|
||||
"horizon.splitscreen.setup.layout.two": "2 paylaşım: %s",
|
||||
"horizon.splitscreen.setup.layout.three": "3 paylaşım: %s",
|
||||
"horizon.splitscreen.setup.layout.two.0": "Avtomatik",
|
||||
"horizon.splitscreen.setup.layout.two.1": "Yan-yana",
|
||||
"horizon.splitscreen.setup.layout.two.2": "Üst və alt",
|
||||
"horizon.splitscreen.setup.layout.three.0": "Üstündə biri geniş",
|
||||
"horizon.splitscreen.setup.layout.three.1": "Solda biri hündür",
|
||||
"horizon.splitscreen.setup.layout.three.2": "Yan-yana",
|
||||
"horizon.splitscreen.setup.help.player": "Onları köçürmək üçün %s üzərinə klikləyin və ya dartın.",
|
||||
"horizon.splitscreen.setup.help.layout_auto": "%s (pəncərə geniş olduqda yan-yana). Dəyişmək üçün basın.",
|
||||
"horizon.splitscreen.setup.help.layout": "%s. Dəyişmək üçün basın.",
|
||||
"horizon.splitscreen.setup.help.switch": "Bu kompüter üçün bölünmüş ekran, aktiv və ya söndürülür.",
|
||||
"horizon.splitscreen.setup.help.identify": "Hər bir oyunçunun görünüşü onun hansı pəncərədə olduğunu göstərir.",
|
||||
"horizon.splitscreen.setup.help.names": "Görünüşünün küncündə hər bir oyunçunun adı.",
|
||||
"horizon.splitscreen.setup.help.revert": "Hələ tətbiq olunmamış dəyişiklikləri geri alın.",
|
||||
"horizon.splitscreen.setup.help.apply": "Xəritənin göstərdiyi şəkildə oynayın.",
|
||||
"horizon.splitscreen.setup.help.unchanged": "Hələ heç nə dəyişməyib.",
|
||||
"horizon.splitscreen.setup.give_up_pad": "Pasxadan imtina et",
|
||||
"horizon.splitscreen.prompt.controller_connected": "Nəzarətçi qoşulub",
|
||||
"horizon.splitscreen.prompt.controller_disconnected": "Nəzarətçi ayrıldı",
|
||||
"horizon.splitscreen.prompt.controller_disconnected.hint": "Onu yenidən birləşdirin və ya digərində düyməni basın",
|
||||
"horizon.splitscreen.prompt.join.one": "%s qoşulmaq istəyir",
|
||||
"horizon.splitscreen.prompt.join.many": "%s nəzarətçilər qoşulmaq istəyirlər",
|
||||
"horizon.splitscreen.prompt.join.yes": "Qoşulun",
|
||||
"horizon.splitscreen.prompt.join.as_p1": "P1 kimi oynayın",
|
||||
"horizon.splitscreen.prompt.join.no": "yox",
|
||||
"horizon.splitscreen.prompt.lost": "%s: nəzarətçi ayrıldı",
|
||||
"horizon.splitscreen.prompt.lost.use_another": "Başqasını istifadə edin",
|
||||
"horizon.splitscreen.prompt.lost.leave": "tərk et",
|
||||
"horizon.splitscreen.setup.help.give_up_pad": "Nəzarətçiniz sizi idarə etməyi dayandırır və ona qoşulmaq təklif olunur (A).",
|
||||
"horizon.splitscreen.setup.note.held_at": "%s to %s: A onu bura düşür, B onu geri qoyur.",
|
||||
"horizon.splitscreen.setup.note.held": "%s götürdü: sol/sağ yeri seçir, A düşür, B onu geri qoyur.",
|
||||
"Hides models behind opaque blocks. Async checks off-thread; entities tick.": "Modelləri qeyri-şəffaf blokların arxasında gizlədir. Async-dən kənar yoxlayır; qurumlar işarələyir.",
|
||||
"Block Entities": "Blok obyektləri",
|
||||
"Shulker Boxes": "Shulker qutuları",
|
||||
"Reuses settled boxes; lid movement stays animated.": "Yerləşmiş qutuları təkrar istifadə edir; qapaq hərəkəti canlı qalır.",
|
||||
"Signs": "İşarələr",
|
||||
"Reuses sign text until text, light, or font changes.": "Mətn, işıq və ya şrift dəyişənə qədər işarə mətnindən təkrar istifadə edir.",
|
||||
"Decorated Pots": "Bəzəkli qablar",
|
||||
"Reuses still pots; break wobble and sherd changes redraw.": "Qazan qabları təkrar istifadə edir; yırğalanmaq və parçalanma dəyişiklikləri yenidən çəkmək.",
|
||||
"Bells": "Zənglər",
|
||||
"Reuses still bells; ringing stays animated.": "Hərəkətsiz zəngləri təkrar istifadə edir; Zəng animasiyalı qalır.",
|
||||
"Chests": "Sinələr",
|
||||
"Reuses closed chests; moving lids stay animated.": "Qapalı sandıqları təkrar istifadə edir; hərəkət edən qapaqlar canlı qalır.",
|
||||
"Banners": "Bannerlər",
|
||||
"Reuses poles; flags and pattern layers still update.": "Dirəkləri təkrar istifadə edir; bayraqlar və naxış qatları hələ də yenilənir.",
|
||||
"Copper Golem Statues": "Mis Qolem heykəlləri",
|
||||
"Reuses statue geometry until its block state changes.": "Blok vəziyyəti dəyişənə qədər heykəlin həndəsəsindən təkrar istifadə edir.",
|
||||
"Leaf Culling": "Yarpaqların kəsilməsi",
|
||||
"Every leaf face. Vanilla's fancy leaves.": "Hər yarpaq üzü. Vanilin gözəl yarpaqları.",
|
||||
"Distant Plant Dither": "Uzaq Bitki Dither",
|
||||
"Keeps fine plants in high-res packs from fading far away.": "Yüksək dəqiqlikli paketlərdəki incə bitkilərin uzaqdan solmasını qarşısını alır.",
|
||||
"Screenshot Quality": "Ekran Şotunun Keyfiyyəti",
|
||||
"F2 screenshots at this many times the game's resolution. Higher takes longer.": "F2 ekran görüntüləri bu dəfələrlə oyunun həlli. Yüksək daha uzun çəkir.",
|
||||
"Frame Generation": "Çərçivə Yaradılması",
|
||||
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Hər iki real arasında düzəldilmiş çərçivəni göstərir: daha hamar, bir az daha gecikmə.",
|
||||
"Shadow Cache": "Kölgə önbelleği",
|
||||
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Günəş, görünüş və dünya uyğunlaşdıqda paket kölgə xəritəsindən təkrar istifadə edir. Defolt olaraq söndürülür.",
|
||||
"Find BlueMap": "BlueMap tapın",
|
||||
"On servers, finds their BlueMap web map for far terrain. Next join.": "Serverlərdə uzaq ərazilər üçün BlueMap veb xəritəsini tapır. Növbəti qoşulun.",
|
||||
"How far around you BlueMap tiles are downloaded. Next join.": "Ətrafınızda BlueMap plitələri nə qədər yüklənir. Növbəti qoşulun.",
|
||||
"Synchronous": "Sinxron",
|
||||
"Asynchronous": "Asinxron",
|
||||
"Solid": "Möhkəm",
|
||||
"Solid Aggressive": "Möhkəm Aqressiv",
|
||||
"Hollow": "Boş",
|
||||
"Leaves fully inside a canopy draw solid: fewer see-through layers.": "Tam örtünün içindəki yarpaqlar möhkəm çəkilir: daha az keçirici təbəqə.",
|
||||
"Leaves with one open side draw solid too: faster, chunkier edges.": "Bir tərəfi açıq olan yarpaqlar da bərk çəkir: daha sürətli, daha sıx kənarlar.",
|
||||
"Drops faces between leaves: faster, but gaps look through.": "Yarpaqlar arasında üzləri düşür: daha sürətli, lakin boşluqlar içəri baxır.",
|
||||
"Hotbar: Previous Slot": "İsti panel: Əvvəlki yuva",
|
||||
"Hotbar: Next Slot": "İsti panel: Növbəti Slot",
|
||||
"horizon.agent.connected.one": "Qoşuldu: 1 agent bağlantısı",
|
||||
"horizon.agent.connected.many": "Qoşuldu: %s agent əlaqələri",
|
||||
"horizon.agent.connected.one_ago": "Qoşuldu: 1 agent bağlantısı, son əmr %s s əvvəl",
|
||||
"horizon.agent.connected.many_ago": "Qoşuldu: %s agent əlaqələri, son əmr %s s əvvəl",
|
||||
"horizon.agent.waiting": "Agent gözləyir.",
|
||||
"horizon.agent.off": "Off: agentlər bu oyuna daxil ola bilməz.",
|
||||
"horizon.agent.session_opened": "Oyuna başlayan proqram tərəfindən bu sessiya üçün açıldı.",
|
||||
"horizon.agent.socket": "Soket: %s (tam yolu üçün təfərrüatları kopyalayın)",
|
||||
"horizon.agent.local_only": "Yalnız yerli: bu kompüterdəki agentlər, heç vaxt şəbəkə.",
|
||||
"Auto (%s nits)": "Avtomatik (%s nits)",
|
||||
"Hides models behind opaque blocks. Entities update. Off with custom packs.": "Modelləri qeyri-şəffaf blokların arxasında gizlədir. Müəssisələrin yenilənməsi. Fərdi paketlərlə endirim.",
|
||||
"horizon.extras.repro_keys": "Qısayolların qeyd edilməsi",
|
||||
"horizon.extras.repro_keys.tooltip": "F3+R repro qeydini başlayır və ya dayandırır və F3+E öz paketini yazır. Defolt olaraq söndürülür: yazı dünyanı kopyalayır.",
|
||||
"horizon.agent.record_session": "Səs yazmağa başlayın",
|
||||
"horizon.agent.record_stop": "Səs yazmağı dayandırın",
|
||||
"horizon.agent.export_repro": "Repro paketini ixrac edin",
|
||||
"horizon.repro.started": "Qeydiyyat başladı; F3+R onu dayandırır və paketi yazır.",
|
||||
"horizon.repro.stopped": "Qeydiyyat dayandırıldı: %s",
|
||||
"horizon.repro.bundle": "Repro paketi: %s",
|
||||
"horizon.repro.error": "Reproduksiya: %s",
|
||||
"horizon.repro.no_recording": "İxrac etmək üçün hələ heç bir qeyd yoxdur.",
|
||||
"horizon.repro.replay_started": "Təkrar başladı: %s",
|
||||
"horizon.pack.addInfo": "Modrinth-dən resurs paketi əlavə edin",
|
||||
"horizon.pack.addWorking": "Modrinthdən soruşur...",
|
||||
"horizon.pack.addNone": "Heç bir resurs paketi tapılmadı",
|
||||
"horizon.pack.addTitle": "Resurs Paketi əlavə edin",
|
||||
"horizon.pack.install": "Quraşdırın",
|
||||
"horizon.pack.installing": "Quraşdırılır",
|
||||
"horizon.pack.installed": "Quraşdırılıb",
|
||||
"horizon.pack.search": "Axtar",
|
||||
"horizon.pack.searchHint": "Modrinth axtar...",
|
||||
"horizon.pack.filters": "Filtrlər",
|
||||
"horizon.pack.filter.realistic": "Realist",
|
||||
"horizon.pack.filter.simplistic": "Sadə",
|
||||
"horizon.pack.filter.vanillaLike": "Vanil kimi",
|
||||
"horizon.pack.addFailed": "Paketi əlavə etmək mümkün olmadı: %s",
|
||||
"horizon.extras.header.cosmetics": "Kosmetika",
|
||||
"horizon.extras.cosmetics_mine": "Kosmetikam",
|
||||
"horizon.extras.cosmetics_mine.tooltip": "Gizli: hər Horizon oyunçu sizi kosmetikanız olmadan görür. Başlatma şkafı ilə eyni açar.",
|
||||
"horizon.extras.cosmetics_others": "Digər oyunçuların kosmetikası",
|
||||
"horizon.extras.cosmetics_others.tooltip": "Digər Horizon oyunçular nə geyinirlər: hər şey, yalnız ad etiketi üslubları və ya heç nə. Gizli, oyun heç vaxt nə geyindiklərini soruşmur.",
|
||||
"horizon.extras.cosmetics_auras": "Başqalarının auraları və izləri",
|
||||
"horizon.extras.cosmetics_auras.tooltip": "Digər oyunçuların geydiyi auralar və izlər. Defolt olaraq söndürülür.",
|
||||
"horizon.extras.cosmetics.shown": "Göstərildi",
|
||||
"horizon.extras.cosmetics.hidden": "Gizli",
|
||||
"horizon.extras.cosmetics.name_tags": "Yalnız ad etiketləri"
|
||||
}
|
||||
@@ -1,499 +0,0 @@
|
||||
{
|
||||
"horizon.options.controller": "Контролер",
|
||||
"horizon.options.three_d_skin_layers": "3D скин ҡатламдары",
|
||||
"horizon.options.vibration": "Вибрация",
|
||||
"horizon.options.adaptive_triggers": "Адаптив триггерҙар",
|
||||
"horizon.options.lightbar": "Лайтбар",
|
||||
"horizon.options.look_sensitivity": "Ҡарау һиҙгерлеге",
|
||||
"horizon.options.invert_x": "Ҡараш инверсияһы X",
|
||||
"horizon.options.invert_y": "Ҡараш инверсияһы Y",
|
||||
"horizon.options.sneak_button": "Боҫоу төймәһе",
|
||||
"horizon.options.move_deadzone": "Хәрәкәт үле зонаһы",
|
||||
"horizon.options.look_deadzone": "Ҡараш үле зонаһы",
|
||||
"horizon.options.button_glyphs": "Төймә глифтары",
|
||||
"horizon.options.menu_navigation": "Навигация меню",
|
||||
"horizon.options.on_screen_keyboard": "Экрандағы клавиатура",
|
||||
"horizon.options.chat_delay": "Чат тотҡарланыуы",
|
||||
"horizon.options.lightbar.health": "Һаулыҡ",
|
||||
"horizon.options.lightbar.dimension": "Үлсәм",
|
||||
"horizon.options.glyphs.auto": "Авто",
|
||||
"horizon.options.menu_focus": "Фокус",
|
||||
"horizon.options.menu_pointer": "Күрһәткес",
|
||||
"horizon.options.keyboard.auto": "Авто",
|
||||
"horizon.options.unavailable": "Был функция Horizon-ла әлегә юҡ.",
|
||||
"horizon.options.search": "Эҙләү параметрҙары...",
|
||||
"horizon.pages.mods": "Модтар",
|
||||
"horizon.pages.shader_packs": "Шейдер пакеттары",
|
||||
"horizon.pages.shader_options": "Шейдер параметрҙары",
|
||||
"horizon.pages.controller": "Контроллер параметрҙары",
|
||||
"horizon.pages.controller_binds": "Контроллер бәйләй",
|
||||
"horizon.pages.extras": "Horizon Өҫтәмә",
|
||||
"horizon.pages.settings": "Horizon Көйләүҙәр",
|
||||
"horizon.pages.agent": "ЯИ агенты",
|
||||
"horizon.split.leave": "Ҡалдырырға",
|
||||
"horizon.pages.split_settings": "Сплит-экран параметрҙары",
|
||||
"horizon.pages.split_profiles": "Кем уйнай?",
|
||||
"horizon.pages.split_setup": "Сплит-экранды ҡуйыу",
|
||||
"horizon.pages.music": "Музыка",
|
||||
"horizon.pages.music.menu": "Меню",
|
||||
"horizon.pages.music.overworld": "Өҫтөн донъя",
|
||||
"horizon.pages.music.creative": "Ижади .",
|
||||
"horizon.pages.music.underwater": "Һыу аҫтында .",
|
||||
"horizon.pages.music.end": "Аҙағы",
|
||||
"horizon.pages.music.boss": "Етәксе",
|
||||
"horizon.pages.music.credits": "Кредиттар",
|
||||
"horizon.pages.music.overworld_biomes": "Донъя өҫтөндәге биомдар",
|
||||
"horizon.pages.music.nether_biomes": "Недер биомдары",
|
||||
"horizon.pages.music.end_biomes": "Аҙаҡҡы биомдар",
|
||||
"horizon.pages.music.other": "Башҡа музыка .",
|
||||
"horizon.pages.music.empty": "Музыка тректары юҡ: уйындың тауыштары тейәлмәгән.",
|
||||
"horizon.pages.music.tooltip": "Встроенный тректар, хәл буйынса: хәҙер бер уйнай, йәки теләһә ниндәй һүндерергә, шулай итеп, уйын уны йыйыу туҡтата.",
|
||||
"horizon.pages.settings.tooltip": "Horizon үҙ параметрҙары: графика, контроллерҙар, өҫтәмәләр һәм модтар.",
|
||||
"horizon.pages.graphics.tooltip": "Графика, ябай: һәр параметр нимәгә төшкәнен әйтә.",
|
||||
"horizon.pages.graphics_advanced.tooltip": "Һәр график параметр, Horizon’s инә, һәр береһе нимә менән.",
|
||||
"horizon.pages.controller.tooltip": "Контроллерҙар: уларҙың төймәләре, таяҡсалар, вибрация һәм меню навигацияһы.",
|
||||
"horizon.pages.extras.tooltip": "Факультатив өҫтәүҙәр, бөтәһе лә өҫтөндә ғәҙәттәгесә: уйын ваниль ҡала, тик һеҙ бер өҫтөндә.",
|
||||
"horizon.pages.agent.tooltip": "Һеҙҙең үҙ AI агент уйнай: horizon-ctl һәм MCP клиенттар был компьютерҙа.",
|
||||
"horizon.pages.split_profiles.tooltip": "Кем уйнай: был уйын папкаһында һаҡланған профиль.",
|
||||
"horizon.pages.split_setup.tooltip": "Тәҙрәләр һәм мониторҙар, ә кем ҡайҙа уйнай.",
|
||||
"horizon.pages.shader_packs.tooltip": "Шейдер пакеттары шейдерпакеттар папкаһынан: Horizon үҙ һәм шейдерҙар/ форматындағылар. Берәм-берәм йүгерә.",
|
||||
"horizon.pages.mods.tooltip": "Horizon өсөн эшләнгән модтар, горизонт-модтар папкаһынан. Һәр береһе ҡом йәшниктә йүгерә һәм һеҙ уны тоҡандырғансы һүндерелә.",
|
||||
"horizon.pages.open.graphics": "Графика...",
|
||||
"horizon.pages.open.graphics_advanced": "Алдынғы графика...",
|
||||
"horizon.pages.open.shader_packs": "Шейдер пакеттары...",
|
||||
"horizon.pages.open.controller": "Контроллер параметрҙары...",
|
||||
"horizon.pages.open.split_setup": "Бүленгән экран...",
|
||||
"horizon.pages.open.extras": "Өҫтәмәләр...",
|
||||
"horizon.pages.open.mods": "Модтар...",
|
||||
"horizon.pages.open.agent": "ЯИ агенты...",
|
||||
"horizon.pages.open.horizon_graphics": "Horizon Графика...",
|
||||
"horizon.pages.open.controller_binds": "Контроллер бәйләй...",
|
||||
"horizon.pages.open.horizon_settings": "Horizon Көйләүҙәр...",
|
||||
"horizon.pages.open.music": "Музыка...",
|
||||
"horizon.mods.empty": "Модтар юҡ: .hmod файлдарын горизонт-модтар папкаһына ҡуйығыҙ",
|
||||
"horizon.mods.stopped": " (Туҡтатылған)",
|
||||
"horizon.mods.not_built": " (Төҙөлмәгән)",
|
||||
"horizon.mods.dev": " (Дев)",
|
||||
"horizon.mods.settings": "Көйләүҙәр",
|
||||
"horizon.mods.in_world": " (донъяны үҙгәртергә ҡалдырырға)",
|
||||
"horizon.mods.open_folder": "Мод папкаһын асыу",
|
||||
"horizon.shader_packs.empty": "Шейдер пакеттары юҡ: уларҙы шейдерпакеттар папкаһына һалығыҙ",
|
||||
"horizon.shader_options.open": "Шейдер варианттары...",
|
||||
"horizon.shader_options.profile": "Профиль",
|
||||
"horizon.shader_options.loading": "Шейдер пакетының варианттарын уҡыу...",
|
||||
"horizon.agent.copy_details": "Тоташыу тураһында мәғлүмәтте күсерергә",
|
||||
"horizon.multiplayer.join": "Ҡушылыу",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Яңынан тоташтырыуҙы юҡҡа сығарыу",
|
||||
"horizon.multiplayer.join_friend": "Дуҫҡа ҡушыл",
|
||||
"horizon.multiplayer.invite_code": "Саҡырыу коды",
|
||||
"horizon.multiplayer.relay": "Реле (теләк буйынса)",
|
||||
"Preset": "Алдан ҡуйылған",
|
||||
"LOD Distance": "LOD арауығы",
|
||||
"Far Detail": "Алыҫ деталь",
|
||||
"Cave Culling": "Мәмерйә ҡырҡыу",
|
||||
"Entity Culling": "Субъектты ҡырҡыу",
|
||||
"Chunk Updates": "Яңыртыу өлөштәре",
|
||||
"Upload Budget": "Бюджет тейәү",
|
||||
"Sky": "Күк",
|
||||
"Fog": "Томан",
|
||||
"Weather": "Һауа торошо",
|
||||
"Animated Textures": "Йәнле текстуралар",
|
||||
"Present Mode": "Хәҙерге режим",
|
||||
"Frame Pacing": "Кадр темпы",
|
||||
"Worker Threads": "Эшсе ептәр",
|
||||
"Resolution Scale": "Асыҡлыҡ шкалаһы",
|
||||
"Anti-aliasing": "Анти-псевдоним",
|
||||
"Upscaling": "Масштаблаштырыу",
|
||||
"Paper White": "Аҡ ҡағыҙ",
|
||||
"Peak Brightness": "Иң юғары яҡтылыҡ",
|
||||
"Black Level": "Ҡара кимәл",
|
||||
"UI Brightness": "Яҡтылыҡ",
|
||||
"Battery Mode": "Аккумулятор режимы",
|
||||
"Graphics API": "Графика API",
|
||||
"Translucent Batching": "Ярым үтә күренмәле партиялау",
|
||||
"Performance Overlay": "Һөҙөмтәлелек өҫтөнлөктәре",
|
||||
"Full Detail Distance": "Тулы ентекле арауыҡ",
|
||||
"On": "Өҫтөндә",
|
||||
"Off": "Һүндерелгән",
|
||||
"Unlimited": "Сикләнмәгән",
|
||||
"Auto": "Авто",
|
||||
"Custom": "Ҡулланыусы",
|
||||
"Performance": "Башҡарыу",
|
||||
"Balanced": "Баланслы",
|
||||
"Quality": "Сифат",
|
||||
"Ultra": "Ультра",
|
||||
"Ultra Performance": "Ультра етештереүсәнлеге",
|
||||
"Native": "Натив",
|
||||
"Full": "Тулы",
|
||||
"Reduced": "Кәметелгән",
|
||||
"Efficient": "Һөҙөмтәле",
|
||||
"Precise": "Теүәл",
|
||||
"Auto (VSync)": "Авто (ВСинхронизация)",
|
||||
"FIFO": "ФИФО",
|
||||
"Mailbox": "Почта йәшниге",
|
||||
"Immediate": "Шунда уҡ",
|
||||
"FPS and Graph": "FPS һәм график",
|
||||
"TAA": "ТАА",
|
||||
"FXAA": "ФХАА",
|
||||
"No cost": "Хаҡы юҡ",
|
||||
"Low cost": "Түбән хаҡ",
|
||||
"Medium cost": "Уртаса хаҡ",
|
||||
"High cost": "Юғары хаҡ",
|
||||
"Saves time": "Ваҡытты экономиялай",
|
||||
"Graphics": "Графика",
|
||||
"Done": "Бөттө",
|
||||
"This feature is not yet available in Horizon.": "Был функция Horizon-ла әлегә юҡ.",
|
||||
"Your display or system doesn't offer HDR output.": "Һеҙҙең дисплей йәки система тәҡдим итмәй HDR сығыш.",
|
||||
"Turn HDR on to use it.": "Уны ҡулланыу өсөн HDR ҡабыҙығыҙ.",
|
||||
"Your GPU or graphics API doesn't support it.": "Һеҙҙең GPU йәки график API уны ҡабул итмәй.",
|
||||
"%s chunks": "%s өлөштәр",
|
||||
"%s per frame": "%s бер кадрға",
|
||||
"%s MiB/frame": "%s МиБ/кадр",
|
||||
"%s KiB/frame": "%s КиБ/кадр",
|
||||
"Auto (%s)": "Авто (%s)",
|
||||
"Full-detail terrain range. LOD Distance can extend local worlds farther.": "Тулы деталь ер диапазоны. LOD Дистанция урындағы донъяларҙы алыҫҡараҡ һуҙырға мөмкин.",
|
||||
"Coarse distant terrain in local worlds. Integrated GPUs stop at 128 chunks.": "Урындағы донъяларҙа ҡырыҫ алыҫ ерҙәр. Интеграцияланған график процессорҙар 128 өлөштә туҡтай.",
|
||||
"Reduced draws far terrain coarser: faster on this integrated GPU.": "Ҡыҫҡартылған тарта алыҫ ер ҡырыҫ: тиҙерәк был интеграцияланған GPU.",
|
||||
"Reduced draws far terrain coarser, for GPUs it slows down.": "Кәметелгән тарта алыҫ ер ҡырыҫыраҡ, GPUs өсөн ул яйлатып.",
|
||||
"Field of view. Wider shows more terrain at once.": "Күренеш өлкәһе. Киңерәк бер юлы күберәк ерҙе күрһәтә.",
|
||||
"Caps frames per second; a cap saves power and heat.": "Ҡапҡастар кадрҙар секундына; ҡапҡас энергияны һәм йылылыҡты һаҡлай.",
|
||||
"Waits for the display's refresh: no tearing, a little latency.": "Дисплейҙы яңыртыуҙы көтә: йыртыу юҡ, бер аҙ тотҡарлыҡ.",
|
||||
"Size of menus and the HUD.": "Размер меню һәм HUD.",
|
||||
"Sets the options below together. Changing one makes it Custom.": "Түбәндәге варианттарҙы бергә ҡуя. Бер үҙгәртеү уны Ҡулланыусы итә.",
|
||||
"Lava, fire and sunlight shine brighter than white. Needs an HDR display.": "Лава, ут һәм ҡояш нурҙары аҡтан да сағыуыраҡ балҡый. HDR дисплей кәрәк.",
|
||||
"Your display's darkest black. 0 for OLED; LCDs glow a little.": "Һеҙҙең дисплей’ы иң ҡара ҡара. 0 өсөн OLED; ЖК-дисплейҙар бер аҙ балҡый.",
|
||||
"How bright highlights get. Auto: the peak your system reports.": "Нисек сағыу өҫтөнлөктәр ала. Авто: пик һеҙҙең система отчеттар.",
|
||||
"How bright white paper and menus look. Auto: your desktop's white.": "Нисек сағыу аҡ ҡағыҙ, менюлар күренә. Авто: һеҙҙең өҫтәл’ы аҡ.",
|
||||
"How bright the HUD and menus are, against paper white.": "Нисек сағыу HUD һәм менюлар, ҡағыҙ аҡ ҡаршы.",
|
||||
"How far mobs, crops and machines keep working. CPU time.": "Күпме алыҫҡа мобтар, культуралар һәм машиналар эшләп тора. Процессор ваҡыты.",
|
||||
"How far away mobs and items are drawn, as a share of normal.": "Нисек алыҫҡа мобтар һәм әйберҙәр тартыла, өлөшө булараҡ нормаль.",
|
||||
"Skips terrain hidden behind closed sections. Off costs a lot.": "Ябыҡ участкалар артында йәшеренгән ерҙәрҙе һикерә. Офф күп сығымдар талап итә.",
|
||||
"Hides entities behind opaque blocks.": "Һүҙ асыҡ булмаған блоктар артында субъекттарҙы йәшерә.",
|
||||
"No entity occlusion culling.": "Юҡ субъект окклюзия ҡырҡыу.",
|
||||
"CPU occlusion test runs this frame.": "Процессор окклюзия һынау был кадрҙа эшләй.",
|
||||
"Occlusion checks run on a worker thread.": "Окклюзия тикшерелеүҙәре эшсе ептә эшләй.",
|
||||
"Meshed sections uploaded per frame. Lower is smoother while loading.": "Бер кадрға тейәлгән селтәрле бүлектәр. Түбәнге тейәгәндә шымараҡ.",
|
||||
"Soft shadows in corners. Costs chunk building time only.": "Мөйөштәрҙә йомшаҡ күләгәләр. Сығымдар өлөшө төҙөү ваҡыты ғына.",
|
||||
"Smaller copies of block textures for distant terrain: less shimmer.": "Алыҫ ерҙәр өсөн блок текстураларының бәләкәйерәк күсермәләре: ялтырап тороу аҙыраҡ.",
|
||||
"Blends grass and water colours across biomes when building chunks.": "Үлән һәм һыу төҫтәрен биомдар буйынса ҡатнаштырып, өлөштәр төҙөгәндә.",
|
||||
"Cloud layer: off, flat (fast) or 3D (fancy).": "Болот ҡатламы: өҙөлгән, яҫы (тиҙ) йәки 3D (фантазия).",
|
||||
"How far clouds reach, in chunks.": "Болоттар ни тиклем алыҫҡа етә, өлөшләп.",
|
||||
"Draws the sky, sun, moon and stars.": "Күкте, ҡояшты, айҙы, йондоҙҙарҙы һүрәтләй.",
|
||||
"Distance fog. Off shows the edge of the loaded world.": "Дистанция томан. Off йөкләнгән донъяның ситен күрһәтә.",
|
||||
"Draws rain and snow.": "Ямғыр һәм ҡар тарта.",
|
||||
"How far rain and snow fall around you.": "Тирә-яғыңда ни тиклем алыҫҡа ямғыр, ҡар яуа.",
|
||||
"How many particles effects spawn.": "Күпме киҫәксә эффекттары үрсетә.",
|
||||
"Round shadows under mobs and items.": "Мобтар һәм әйберҙәр аҫтында түңәрәк күләгәләр.",
|
||||
"Leaves with see-through gaps. Off draws them solid.": "Япраҡтары үтә күренмәле тишектәр менән. Офф уларҙы ныҡлы тарта.",
|
||||
"Sorts water and glass more exactly behind each other.": "Һыу менән быяланы бер-береһе артында анығыраҡ сорттарға айыра.",
|
||||
"Which chunk rebuilds wait for the frame: none, nearby or yours.": "Ниндәй өлөшө яңынан төҙөй көтә рамка: бер ниндәй ҙә, яҡында йәки һеҙҙең.",
|
||||
"Animates water, lava, fire and other moving textures.": "Һыу, лава, ут һәм башҡа хәрәкәт итеүсе текстураларҙы йәнләндерә.",
|
||||
"World pixels per screen pixel. Below 100% is faster on the GPU.": "Донъя пикселдәре экран пикселе. 100% түбән тиҙерәк GPU.",
|
||||
"Smooths jagged edges. FXAA: softer. TAA: smoothest, moving things may trail.": "Тешле ситтәрен тигеҙләй. ФХАА: йомшағыраҡ. ТАА: иң шыма, хәрәкәт итеүсе әйберҙәр эҙҙәре булыуы мөмкин.",
|
||||
"Renders the world smaller and rebuilds it with TAA. Faster, softer.": "Донъяны бәләкәйерәк итә һәм уны ТАА менән яңынан төҙөй. Тиҙерәк, йомшағыраҡ.",
|
||||
"HDR output on an HDR display: lava, fire and sunlight shine brighter.": "HDR дисплейында HDR сығышы: лава, ут һәм ҡояш нурҙары сағыуыраҡ балҡый.",
|
||||
"How frames reach the screen while VSync is off.": "Нисек кадрҙар экранға етә, ә VSync өҙөлгән.",
|
||||
"Efficient sleeps until the next frame; Precise spins, like vanilla.": "Һөҙөмтәле йоҡлай сираттағы кадрға тиклем; Теүәл әйләнештәр, ваниль кеүек.",
|
||||
"Fills the screen (borderless).": "Экранды тултыра (сикһеҙ).",
|
||||
"Brightens dark places.": "Ҡараңғы урындарҙы яҡтырта.",
|
||||
"Lowers the frame rate when minimised, or idle too.": "Кадрҙар йышлығын түбәнәйтә, ҡасан минималь, йәки буш ла.",
|
||||
"Sways the view while walking.": "Йөрөгәндә ҡарашты һелкетә.",
|
||||
"Fades newly loaded chunks in.": "Яңы тейәлгән өлөштәрҙе һүнә.",
|
||||
"Darkens the screen's edges.": "Экрандың ситтәрен ҡарайта.",
|
||||
"Shows when the world saves.": "Донъя ҡотҡарғанда күрһәтә.",
|
||||
"Shows attack cooldown.": "Һөжүм итеү ваҡытын күрһәтә.",
|
||||
"60 FPS cap (30 in menus or unfocused), half the workers: less power.": "60 FPS ҡапҡас (30 менюларҙа йәки нефокусированный), яртыһы эшселәр: ҡөҙрәте аҙыраҡ.",
|
||||
"Threads that generate, light and mesh chunks. Next start.": "Ептәр, генерациялау, яҡтылыҡ һәм селтәр өлөштәре. Киләһе старт.",
|
||||
"Vulkan, then DirectX 12 (Windows) or OpenGL (Linux) where it fails. Next start.": "Vulkan, унан һуң DirectX 12 (Windows) йәки OpenGL (Linux) ҡайҙа уңышһыҙлыҡҡа осрай. Киләһе старт.",
|
||||
"Water and glass in one buffer: fewer draw calls, a bit more VRAM. Next start.": "Һыу һәм быяла бер буферҙа: низағ шылтыратыуҙар аҙыраҡ, бер аҙ күберәк VRAM. Киләһе старт.",
|
||||
"Mesh data sent to the GPU per frame. Lower is smoother while loading.": "Селтәр мәғлүмәттәре ебәрелгән GPU бер кадр. Түбәнге тейәгәндә шымараҡ.",
|
||||
"Frame rate and frame times over the game, with a graph.": "Кадрҙар тиҙлеге һәм кадрҙар ваҡыты өҫтөндә уйын, график менән.",
|
||||
"horizon.multiplayer.finding_friend": "Дуҫыңдың донъяһын табыу...",
|
||||
"horizon.multiplayer.not_signed_in": "Был сервер онлайн режимда. Уға ҡушылыу өсөн Microsoft иҫәп яҙмағыҙ менән Horizon стартерында инегеҙ.",
|
||||
"horizon.multiplayer.invalid_address": "Дөрөҫ булмаған сервер адресы: %s",
|
||||
"horizon.multiplayer.connected_friend_direct": "Һеҙҙең дуҫығыҙға туранан-тура тоташтырылған...",
|
||||
"horizon.multiplayer.connected_friend_relay": "Һеҙҙең дуҫығыҙға реле аша тоташтырылған...",
|
||||
"General": "Дөйөм",
|
||||
"Advanced": "Алдынғы",
|
||||
"Undo": "Кире ҡайтарыу",
|
||||
"Search options": "Эҙләү варианттары",
|
||||
"Shortcut": "Ярлыҡ",
|
||||
"Distance": "Арауыҡ",
|
||||
"Display": "Дисплей",
|
||||
"Interface": "Интерфейс",
|
||||
"Sky and weather": "Күк йөҙө һәм һауа торошо",
|
||||
"Lighting and entities": "Яҡтыртыу һәм субъекттар",
|
||||
"Textures": "Текстуралар",
|
||||
"Image": "Һүрәт",
|
||||
"Chunk loading": "Ҡыҫҡа тейәү",
|
||||
"Culling": "Куллинг",
|
||||
"Power": "Власть",
|
||||
"Frames": "Рамкалар",
|
||||
"Overlay": "Ҡаплау",
|
||||
"Output": "Сығыш",
|
||||
"Brightness": "Яҡтылыҡ",
|
||||
"How far you see, the window and the HUD.": "Күпме алыҫ күрәһең, тәҙрә һәм HUD.",
|
||||
"The sky and weather, lighting, textures and the image.": "Күк һәм һауа торошо, яҡтыртыу, текстуралар һәм образ.",
|
||||
"Chunk loading, culling and power use.": "Ҡырҡылған тейәү, ҡырҡыу һәм ҡөҙрәт ҡулланыу.",
|
||||
"How frames reach the screen, and the performance overlay.": "Нисек кадрҙар экранға етә, һәм башҡарыу өҫтәү.",
|
||||
"Brighter-than-white output on an HDR display.": "HDR дисплейында аҡтан сағыуыраҡ сығыш.",
|
||||
"Results for \"%s\"": "\"%s\" өсөн һөҙөмтәләр.",
|
||||
"No settings match.": "Бер ниндәй ҙә параметрҙар тап килмәй.",
|
||||
"horizon.title.not_affiliated": "Mojang йәки Microsoft менән бәйле түгел",
|
||||
"horizon.extras.header.lighting": "Яҡтыртыу",
|
||||
"horizon.extras.header.third_person": "Өсөнсө зат .",
|
||||
"horizon.extras.header.particles": "Киҫәксәләр",
|
||||
"horizon.extras.header.inventory": "Инвентаризация",
|
||||
"horizon.extras.header.chat": "Чат",
|
||||
"horizon.extras.header.multiplayer": "Күп уйынсылар",
|
||||
"horizon.extras.header.sound": "Тауыш .",
|
||||
"horizon.extras.header.menus": "Менюлар",
|
||||
"horizon.extras.fullbright": "Фуллбрайт",
|
||||
"horizon.extras.dynamic_lights": "Динамик яҡтылыҡ",
|
||||
"horizon.extras.own_name_tag": "Үҙ исем билдәһе",
|
||||
"horizon.extras.hide_block_particles": "Блок-өҙөү киҫәксәләрен йәшерергә",
|
||||
"horizon.extras.auto_reconnect": "Авто-яңынан тоташтырыу",
|
||||
"horizon.extras.ui_sound_reserve": "Ҡулланыусылар интерфейсы тауыштары өсөн каналдар һаҡлау",
|
||||
"horizon.extras.music_page": "Музыка бите .",
|
||||
"horizon.extras.mouse_tweaks": "Сысҡандың идара итеү элементтарын үҙгәртә",
|
||||
"horizon.extras.mouse_tweaks_wheel": "Прокрутка тәгәрмәсе әйберҙәрҙе күсерә",
|
||||
"horizon.extras.sort": "Сортлау төймәләре",
|
||||
"horizon.extras.sort_key": "Сортлау асҡысы",
|
||||
"horizon.extras.no_ai": "ЯИ юҡ",
|
||||
"horizon.extras.agent_control": "Агент контроле",
|
||||
"horizon.extras.discord_presence": "Discord Бай булыу",
|
||||
"horizon.extras.splitscreen": "Бүлгән экран",
|
||||
"horizon.extras.chat_animation": "Тигеҙ һөйләшеү",
|
||||
"horizon.extras.chat_indicator": "Хәбәр күрһәткесе",
|
||||
"horizon.extras.wheel_cycles_settings": "Прокрутка тәгәрмәсе параметрҙарҙы үҙгәртә",
|
||||
"horizon.extras.pack_browser": "Ресурс пакеты браузеры",
|
||||
"horizon.extras.fullbright.tooltip": "Блоктарҙы һәм субъекттарҙы тулы яҡтылыҡта, хатта ҡараңғы урындарҙа ла күрһәтегеҙ.",
|
||||
"horizon.extras.dynamic_lights.tooltip": "Факелдар һәм башҡа яҡтылыҡ сығанаҡтары тотолған, төшөрөлгән йәки ут өҫтөндә улар тирәһендәге блоктарҙы яҡтырта.",
|
||||
"horizon.extras.own_name_tag.tooltip": "Һеҙҙең исемде баш өҫтөндә өсөнсө заттан күрһәтә, башҡа уйынсылар күргәнсә. Йәшерен ҡасан сервер һеҙҙең команданың исемдәрен йәшерә йәки һеҙ’күренмәй.",
|
||||
"horizon.extras.hide_block_particles.tooltip": "Йәшерә сүп-сар киҫәксәләре, улар өҙөлгән блоктан ярыла һәм һеҙ шахта өҫтөндә емерек. Башҡа киҫәксәләр ҡала.",
|
||||
"horizon.extras.auto_reconnect.tooltip": "Серверға тоташыу юғалғандан йәки ҡабаттан башлағандан һуң ҡабаттан ҡушыла, бер ҡасан да тибеүҙән йәки тыйыуҙан һуң.",
|
||||
"horizon.extras.ui_sound_reserve.tooltip": "Донъялағы тауыштар 247 тауыш каналының 16-һын бушлай ҡалдыра, шуға күрә уларҙы ташҡын (ҙур шартлау) меню баҫыуҙарын тындыра алмай.",
|
||||
"horizon.extras.music_page.tooltip": "Музыка & Тауыштар ала Музыка... бит: уйнау теләһә ниндәй встроенный трек хәҙер, йәки бер һүндереү, шулай итеп, уйын уны йыйыу туҡтата. Тректы һүндермәйенсә бер нәмә лә үҙгәрмәй.",
|
||||
"horizon.extras.mouse_tweaks.tooltip": "Уң-һөйрәп урындар бер слот, һул-һөйрәп йыя үткәрелгән әйбер, Shift + һул-һөйрәп һәр слот күсә.",
|
||||
"horizon.extras.mouse_tweaks_wheel.tooltip": "Аҫҡа слоттың бер әйберен икенсе яҡҡа ебәрә, өҫкә бер кире килтерә.",
|
||||
"horizon.extras.sort.tooltip": "Сортировать инвентарь, һандыҡтар, бочкалар, эндер һандыҡтар һәм шулкер йәшниктәр ижади вкладка тәртибендә, стека берләштереү.",
|
||||
"horizon.extras.sort_key.tooltip": "Курсор аҫтындағы өлөштө сорттарға бүлә. Баҫығыҙ, һуңынан бер төймәгә баҫығыҙ; Ҡасыу уны сисеп ебәрә.",
|
||||
"horizon.extras.no_ai.tooltip": "Йәшерә һәм һүндерә һәр AI функцияһы: агент менән идара итеү, горизонт-ctl һәм MCP серверҙары. Пускатель’ы переключатель да.",
|
||||
"horizon.extras.agent_control.tooltip": "Һеҙҙең үҙ AI агент был уйынды уйнай ала аша horizon-ctl йәки MCP клиент, был компьютерҙа ғына.",
|
||||
"horizon.extras.discord_presence.tooltip": "Һеҙҙең Discord профилендә Horizon уйнауҙы күрһәтә, ә Discord йүгерә: менюлар, бер уйынсы йәки күп уйынсы, үлсәм һәм уйнаған ваҡыт. Бер ҡасан да донъяның исеме йәки сервер адресы.",
|
||||
"Calibrate...": "Калибрлау...",
|
||||
"Calibrate Anyway...": "Калибрлау барыбер...",
|
||||
"Use Automatic Setup": "Автоматик ҡуйыуҙы ҡулланыу",
|
||||
"Shortcut: %s": "Ҡыҫҡа юл: %s",
|
||||
"Next": "Артабан",
|
||||
"HDR Calibration %s/%s: %s": "HDR калибровкаһы %s/%s: %s",
|
||||
"Sets the three below full screen, one test picture at a time.": "Өс аҫтында тулы экран ҡуя, бер һынау һүрәте бер юлы.",
|
||||
"A key that opens this screen from the game. Not bound at first.": "Уйындан был экранды асҡан асҡыс. Тәүҙә бәйләнмәгән.",
|
||||
"Your display didn't say how bright it gets: calibrate to set it up.": "Һеҙҙең дисплей нисек яҡты ала, тип әйтмәне: калибровка, уны ҡуйырға.",
|
||||
"Already set up for your display. Only calibrate if HDR looks wrong.": "Һеҙҙең дисплей өсөн инде ҡуйылған. Тик калибровка, әгәр HDR дөрөҫ түгел күренә.",
|
||||
"Puts Peak Brightness and Paper White back to your display's setup.": "Ҡуя Пик Яркость һәм Ҡағыҙ Аҡ кире һеҙҙең дисплей’ы ҡороу.",
|
||||
"Set up for your display. Leave on Auto unless it looks wrong.": "Һеҙҙең дисплей өсөн ҡуйырға. Авто өҫтөндә ҡалдырырға, әгәр ҙә ул дөрөҫ күренмәһә.",
|
||||
"Lower it until the logo just disappears into the black.": "Түбән уны тиклем логотип ҡара ғына юғала.",
|
||||
"Raise it until the logo just disappears into the bright square.": "Логотип яҡты квадратҡа ғына юғалғансы уны күтәрегеҙ.",
|
||||
"Make the card a white that's comfortable to look at for a long time, in the light you play in.": "Картаны аҡ төҫтә эшләгеҙ, оҙаҡ ҡарарға уңайлы, һеҙ уйнаған яҡтылыҡта.",
|
||||
"horizon.splitscreen.profiles.forget": "Онот .",
|
||||
"horizon.splitscreen.profiles.create": "Ижад итеү",
|
||||
"horizon.splitscreen.profiles.you": " - һин",
|
||||
"horizon.splitscreen.profiles.forget_named": "Онот %s .",
|
||||
"horizon.splitscreen.profiles.previous": "Алдағы",
|
||||
"horizon.splitscreen.profiles.next": "Артабан",
|
||||
"horizon.splitscreen.profiles.new_button": "Яңы профиль...",
|
||||
"horizon.splitscreen.profiles.forget_title": "Профильде оноттоғоҙмо?",
|
||||
"horizon.splitscreen.profiles.forget_leaves": "Ул был инстанцияның исемлегенән сыға.",
|
||||
"horizon.splitscreen.profiles.forget_kept": "Донъя прогресс һәм параметрҙар һаҡлана.",
|
||||
"horizon.splitscreen.profiles.new_title": "Яңы профиль",
|
||||
"horizon.splitscreen.profiles.name_hint": "Исем (3-тән 16-ға тиклем хәреф, һан, _)",
|
||||
"horizon.splitscreen.profiles.switch_title": "Профиль алмаштырыу",
|
||||
"horizon.splitscreen.profiles.offline_note": "Офлайн профилдәр был данаға ҡарай",
|
||||
"horizon.splitscreen.profiles.move": "Уйнаған ергә күсерегеҙ .",
|
||||
"horizon.splitscreen.profiles.move_place": "Уйнаған урынға күсегеҙ: %s .",
|
||||
"horizon.friends.title": "Дуҫтар менән уйнау .",
|
||||
"horizon.friends.hint": "Дуҫтар кодты титул экранының «Дуҫтар» төймәһенән һуң, туранан-тура бәйләнештә йәки стартердың «Уйнау» битендә йәбештерә.",
|
||||
"horizon.friends.copied": "Күсерелгән",
|
||||
"horizon.friends.copy_code": "Код күсермәһе",
|
||||
"horizon.friends.stop_sharing": "Бүлешеүҙе туҡтатығыҙ",
|
||||
"horizon.friends.none_yet": "Бында әлегә дуҫтар юҡ.",
|
||||
"horizon.friends.here_now": "Бында хәҙер: %s",
|
||||
"horizon.friends.share_intro": "Был донъя менән бүлешергә артында саҡырыу коды: ул LAN асыла, һәм дуҫтар интернетта код менән ҡушыла. Беҙҙең бер сервер ҙа ҡатнашмай.",
|
||||
"horizon.friends.start_sharing": "Бүлешергә башлағыҙ",
|
||||
"horizon.friends.troubleshooting": "Неисправность...",
|
||||
"horizon.friends.stopped": "Саҡырыу коды менән бүлешеүҙе туҡтатты.",
|
||||
"horizon.friends.short_code_changed": "Кемдер дөрөҫ булмаған ҡыҫҡа кодты һынап ҡараны, шуға күрә ул үҙгәрҙе.",
|
||||
"horizon.friends.wrong_code": "Кемдер дөрөҫ булмаған кодты ҡулланып ҡараған.",
|
||||
"horizon.friends.code_ready": "Һеҙҙең саҡырыу коды әҙер: Уйын Менюһы, Дуҫтар менән уйнау. Күсереп яҙып, дуҫтарығыҙға ебәрегеҙ.",
|
||||
"horizon.friends.share_failed": "Донъяны бүлешә алманым: %s",
|
||||
"horizon.friends.finding_route": "Һеҙгә дуҫтар нисек барып етә алыуын табыу...",
|
||||
"horizon.osk.shift": "Смена",
|
||||
"horizon.osk.space": "Йыһан",
|
||||
"horizon.osk.delete": "Юйырға",
|
||||
"horizon.osk.type": "Тип .",
|
||||
"horizon.osk.symbols": "Символдар",
|
||||
"horizon.osk.close": "Ябыҡ",
|
||||
"horizon.extras.chat_animation.tooltip": "Яңы чат һыҙығы һулдан шыуып инә, ә ул һүнә, урынына бөтөн күренә.",
|
||||
"horizon.extras.chat_indicator.tooltip": "Чат һыҙығының һул яғында төҫлө һыҙыҡ.",
|
||||
"horizon.extras.wheel_cycles_settings.tooltip": "Көйләүҙәрҙәге төймә өҫтөндәге прокрутка тәгәрмәсе үҙенең ҡиммәтен үҙгәртә, ванильдәге кеүек. Һүндерелгән, тәгәрмәс исемлекте генә йөрөтә.",
|
||||
"horizon.extras.pack_browser.tooltip": "Һайлау ресурс пакеттары ала өҫтәү төймәһе, эҙләү Modrinth һәм һеҙҙең пакеттар папкаһына пакет ҡуя. Һүндерелгән, уйын бер ҡасан да бәйләнешкә инә Modrinth.",
|
||||
"horizon.extras.splitscreen.tooltip": "Донъяла, икенсе контроллер яңы уйынсы булараҡ ҡушыла ала: баҫығыҙ А, ҡасан ул һорай, һуңынан профиль һайлап һәм ҡайҙа уйнау.",
|
||||
"horizon.splitscreen.setup.monitor": "Монитор %s",
|
||||
"horizon.splitscreen.setup.monitor_unknown": "монитор",
|
||||
"horizon.splitscreen.setup.window": "Тәҙрә %s",
|
||||
"horizon.splitscreen.setup.new_window": "Яңы тәҙрә",
|
||||
"horizon.splitscreen.setup.fill_monitor": "Тултырырға %s",
|
||||
"horizon.splitscreen.setup.done": "Бөттө",
|
||||
"horizon.splitscreen.setup.on": "Өҫтөндә",
|
||||
"horizon.splitscreen.setup.off": "Һүндерелгән",
|
||||
"horizon.splitscreen.setup.player": "Уйынсы %s",
|
||||
"horizon.splitscreen.setup.revert": "Кире ҡайтарыу",
|
||||
"horizon.splitscreen.setup.apply": "Ҡулланыу",
|
||||
"horizon.splitscreen.setup.warning.empty_main": "1-се тәҙрәлә бер кем дә уйнамай: ул йәшенә",
|
||||
"horizon.splitscreen.setup.warning.empty_close": "Тәҙрәлә бер кем дә уйнамай %s: ул Ҡулланыуҙа ябыла",
|
||||
"horizon.splitscreen.setup.warning.empty_new": "Тәҙрәлә бер кем дә уйнамай %s: ул асылмай",
|
||||
"horizon.splitscreen.setup.warning.on_main": "%s тәҙрә 1-се тәҙрә мониторын тултыра: уның уйынсылары 1-се тәҙрәлә уйнай",
|
||||
"horizon.splitscreen.setup.warning.unplugged": "Тәҙрә %s мониторы тоташтырылмаған: уның уйынсылары 1-се тәҙрәлә уйнай",
|
||||
"horizon.splitscreen.setup.status.off": "Сплитскрин һүндерелгән.",
|
||||
"horizon.splitscreen.setup.status.enable_hint": "Уны тоҡандырып, һуңынан бер донъяла икенсе контроллерҙағы төймәгә баҫығыҙ.",
|
||||
"horizon.splitscreen.setup.status.join_hint": "А баҫып, ҡушылырға һәм профиль һайларға.",
|
||||
"horizon.splitscreen.setup.status.idle": "Сплит-экран буш тора",
|
||||
"horizon.splitscreen.setup.status.alone": "Башҡа урындағы уйынсылар юҡ.",
|
||||
"horizon.splitscreen.setup.status.solo_controls": "Яңғыҙ идара итеү һәм контроллер кире бәйләнеш үҙгәрешһеҙ.",
|
||||
"horizon.splitscreen.setup.window_empty": "Тәҙрә %s: буш",
|
||||
"horizon.splitscreen.setup.note.select": "Плеерға баҫығыҙ, йәки уны тәҙрәгә йәки мониторға һөйрәгеҙ.",
|
||||
"horizon.splitscreen.setup.note.apply": "Ҡулланыу өсөн уйнау өсөн был ысул, Ҡайтарыу өсөн кире ҡайтарыу.",
|
||||
"horizon.splitscreen.setup.note.place": "%s ҡайҙа уйнарға тейеш?",
|
||||
"horizon.splitscreen.setup.this_player": "был уйынсы",
|
||||
"horizon.splitscreen.setup.switch_disabled": "Splitscreen һүнә бер тапҡыр башҡа ҡалдырҙы (уларҙы пауза менюһы).",
|
||||
"horizon.splitscreen.setup.window_place": "Тәҙрә %s (%s %s-нан)",
|
||||
"horizon.splitscreen.setup.names": "Исемдәр .",
|
||||
"horizon.splitscreen.setup.identify": "Билдәләү",
|
||||
"horizon.splitscreen.setup.layout.two": "2 бүлешеү: %s",
|
||||
"horizon.splitscreen.setup.layout.three": "3 бүлешеү: %s",
|
||||
"horizon.splitscreen.setup.layout.two.0": "Авто",
|
||||
"horizon.splitscreen.setup.layout.two.1": "Ян-яҡтан .",
|
||||
"horizon.splitscreen.setup.layout.two.2": "Өҫкө һәм аҫҡы .",
|
||||
"horizon.splitscreen.setup.layout.three.0": "Өҫтөндә бер киң",
|
||||
"horizon.splitscreen.setup.layout.three.1": "Һул яҡта бер бейек",
|
||||
"horizon.splitscreen.setup.layout.three.2": "Ян-яҡтан .",
|
||||
"horizon.splitscreen.setup.help.player": "Уларҙы күсерер өсөн %s баҫығыҙ йәки һөйрәгеҙ.",
|
||||
"horizon.splitscreen.setup.help.layout_auto": "%s (тәҙрә киң булғанда йәнәш). Үҙгәртергә баҫығыҙ.",
|
||||
"horizon.splitscreen.setup.help.layout": "%s. Үҙгәртергә баҫығыҙ.",
|
||||
"horizon.splitscreen.setup.help.switch": "Был компьютер өсөн экран бүленгән, өҫтөндә йәки өҫтөндә.",
|
||||
"horizon.splitscreen.setup.help.identify": "Һәр уйынсының ҡарашы ниндәй тәҙрәлә икәнен күрһәтә.",
|
||||
"horizon.splitscreen.setup.help.names": "Һәр уйынсының исеме мөйөшөндә уларҙы ҡарау.",
|
||||
"horizon.splitscreen.setup.help.revert": "Әле ҡулланылмаған үҙгәрештәрҙе кире ҡайтарығыҙ.",
|
||||
"horizon.splitscreen.setup.help.apply": "Картала күрһәтелгәнсә уйнағыҙ.",
|
||||
"horizon.splitscreen.setup.help.unchanged": "Әлегә бер нәмә лә үҙгәрмәне.",
|
||||
"horizon.splitscreen.setup.give_up_pad": "Падтан баш тартығыҙ",
|
||||
"horizon.splitscreen.prompt.controller_connected": "Контроллер тоташтырылған",
|
||||
"horizon.splitscreen.prompt.controller_disconnected": "Контроллер өҙөлгән",
|
||||
"horizon.splitscreen.prompt.controller_disconnected.hint": "Яңынан уны тоташтырырға, йәки икенсе төймәгә баҫығыҙ",
|
||||
"horizon.splitscreen.prompt.join.one": "%s ҡушылырға теләй",
|
||||
"horizon.splitscreen.prompt.join.many": "%s контроллерҙар ҡушылырға теләй",
|
||||
"horizon.splitscreen.prompt.join.yes": "Ҡушылыу",
|
||||
"horizon.splitscreen.prompt.join.as_p1": "Р1 булып уйнай.",
|
||||
"horizon.splitscreen.prompt.join.no": "Юҡ",
|
||||
"horizon.splitscreen.prompt.lost": "%s: контроллер өҙөлгән",
|
||||
"horizon.splitscreen.prompt.lost.use_another": "Икенсе ҡулланыу .",
|
||||
"horizon.splitscreen.prompt.lost.leave": "Ҡалдырырға",
|
||||
"horizon.splitscreen.setup.help.give_up_pad": "Һеҙҙең контролер һеҙҙе водитель туҡтата һәм ҡушылырға тәҡдим ителә (А).",
|
||||
"horizon.splitscreen.setup.note.held_at": "%s %s тиклем: А уны бында төшөрә, В кире һала.",
|
||||
"horizon.splitscreen.setup.note.held": "%s йыйып алынған: һул/уң ҡайҙа йыйып ала, А төшә, В кире һала.",
|
||||
"horizon.splitscreen.online_only": "Был сервер онлайн режимда: офлайн профилдәр уға ҡушыла алмай, һәр уйынсыға үҙ Minecraft иҫәбе кәрәк.",
|
||||
"horizon.splitscreen.own_account": "Был серверға һәр уйынсының үҙ Minecraft иҫәбе кәрәк",
|
||||
"horizon.splitscreen.local_only": "Башҡа уйынсылар был серверға үҙҙәренең Minecraft иҫәбенә генә ҡушыла ала: офлайн профилдәр һеҙҙең донъяла, LAN йәки һеҙҙең урындағы селтәрҙәге серверҙа уйнай.",
|
||||
"Hides models behind opaque blocks. Async checks off-thread; entities tick.": "Моделдәрҙе асыҡ булмаған блоктар артында йәшерә. Ассинхрон тикшерә тышҡы еп; субъекттар галочкой.",
|
||||
"Block Entities": "Блок субъекттары",
|
||||
"Shulker Boxes": "Шулкер йәшниктәре",
|
||||
"Reuses settled boxes; lid movement stays animated.": "Ҡабаттан ҡуллана урынлаштырылған йәшниктәр; ҡапҡас хәрәкәте йәнле ҡала.",
|
||||
"Signs": "Билдәләре",
|
||||
"Reuses sign text until text, light, or font changes.": "Текст, яҡтылыҡ йәки шрифт үҙгәргәнсе билдә тексын ҡабаттан ҡуллана.",
|
||||
"Decorated Pots": "Биҙәкле һауыттар .",
|
||||
"Reuses still pots; break wobble and sherd changes redraw.": "Һаман да ҡаҙандарҙы ҡабаттан ҡуллана; өҙөү качаться һәм шерд үҙгәрештәр перерисовать.",
|
||||
"Bells": "Ҡыңғырауҙар",
|
||||
"Reuses still bells; ringing stays animated.": "Ҡабаттан ҡуллана әле ҡыңғырауҙар; шылтыратыу йәнле ҡала.",
|
||||
"Chests": "Һандыҡтар",
|
||||
"Reuses closed chests; moving lids stay animated.": "Ябыҡ һандыҡтарҙы ҡабаттан ҡуллана; хәрәкәт итеүсе ҡапҡастар йәнле булып ҡала.",
|
||||
"Banners": "Баннерҙар",
|
||||
"Reuses poles; flags and pattern layers still update.": "Бағаналарҙы ҡабаттан ҡуллана; флагтар һәм өлгө ҡатламдары һаман да яңыртыла.",
|
||||
"Copper Golem Statues": "Баҡыр Голем статуялары",
|
||||
"Reuses statue geometry until its block state changes.": "Блок хәле үҙгәргәнсе статуя геометрияһын ҡабаттан ҡуллана.",
|
||||
"Leaf Culling": "Япраҡ ҡырҡыу",
|
||||
"Every leaf face. Vanilla's fancy leaves.": "Һәр япраҡ йөҙө. Ванильдың фантазия япраҡтары.",
|
||||
"Distant Plant Dither": "Алыҫ үҫемлек ҡалтыраныуы",
|
||||
"Keeps fine plants in high-res packs from fading far away.": "Юғары асыҡлыҡтағы пакеттарҙағы ваҡ үҫемлектәрҙе алыҫҡа һүнеүҙән һаҡлай.",
|
||||
"Screenshot Quality": "Скриншот сифаты",
|
||||
"F2 screenshots at this many times the game's resolution. Higher takes longer.": "F2 скриншоттар был күп тапҡыр уйын’ы ҡарар. Юғары оҙағыраҡ ваҡыт ала.",
|
||||
"Frame Generation": "Кадр быуын",
|
||||
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Һәр ике ысын араһында ҡоролған кадр күрһәтә: шымараҡ, бер аҙ күберәк лаг.",
|
||||
"Shadow Cache": "Күләгә кэш",
|
||||
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Ҡояш, ҡараш һәм донъя тап килгәндә пакет күләгә картаһын ҡабаттан ҡуллана. Һүндерелгән по умолчанию.",
|
||||
"Find BlueMap": "Зәңгәр картаны табығыҙ",
|
||||
"On servers, finds their BlueMap web map for far terrain. Next join.": "Серверҙарҙа, алыҫ ерҙәр өсөн уларҙы BlueMap веб-картаһын таба. Киләһе ҡушылыу.",
|
||||
"BlueMap Radius": "Зәңгәр карта радиусы",
|
||||
"How far around you BlueMap tiles are downloaded. Next join.": "Һеҙҙең тирәләй ни тиклем алыҫ BlueMap плиталар скачать. Киләһе ҡушылыу.",
|
||||
"Synchronous": "Синхрон",
|
||||
"Asynchronous": "Ассинхрон",
|
||||
"Solid": "Ҡаты",
|
||||
"Solid Aggressive": "Ҡаты агрессив",
|
||||
"Hollow": "Ҡыуыш",
|
||||
"Leaves fully inside a canopy draw solid: fewer see-through layers.": "Япраҡтар тулыһынса навес эсендә ҡаты тарта: үтә күренмәле ҡатламдар аҙыраҡ.",
|
||||
"Leaves with one open side draw solid too: faster, chunkier edges.": "Япраҡтар менән бер асыҡ яғы ҡаты тарта ла: тиҙерәк, chunkier ситтәре.",
|
||||
"Drops faces between leaves: faster, but gaps look through.": "Япраҡтар араһында йөҙҙәрен тамсылай: тиҙерәк, әммә тишектәр аша ҡарай.",
|
||||
"Hotbar: Previous Slot": "Ҡыҙыу панелдә: Алдағы слот",
|
||||
"Hotbar: Next Slot": "Ҡыҙыу панелдә: Киләһе слот",
|
||||
"horizon.agent.connected.one": "Тоташтырылған: 1 агент тоташыуы",
|
||||
"horizon.agent.connected.many": "Тоташтырылған: %s агент тоташыуҙары",
|
||||
"horizon.agent.connected.one_ago": "Тоташтырылған: 1 агент тоташыу, һуңғы команда %s с элек",
|
||||
"horizon.agent.connected.many_ago": "Тоташтырылған: %s агент тоташыуҙары, һуңғы команда %s с элек",
|
||||
"horizon.agent.waiting": "Агент көтөп.",
|
||||
"horizon.agent.off": "Һүндерелгән: агенттар был уйынға етә алмай.",
|
||||
"horizon.agent.session_opened": "Был сессия өсөн уйынды башлаған программа асты.",
|
||||
"horizon.agent.socket": "Розетка: %s (уның тулы юлы өсөн реквизиттарҙы күсерергә)",
|
||||
"horizon.agent.local_only": "Урындағы ғына: был компьютерҙа агенттар, бер ҡасан да селтәр.",
|
||||
"%s nits": "%s нит",
|
||||
"Auto (%s nits)": "Авто (%s нит)",
|
||||
"%.2f nits": "%.2f нит",
|
||||
"Hides models behind opaque blocks. Entities update. Off with custom packs.": "Моделдәрҙе асыҡ булмаған блоктар артында йәшерә. Яңыртыу субъекттары. Ҡулланыусылар пакеттары менән өҙөлгән.",
|
||||
"horizon.extras.repro_keys": "Яҙма ярлыҡтар",
|
||||
"horizon.extras.repro_keys.tooltip": "F3+R репро яҙманы башлай йәки туҡтата һәм F3+E уның өйөмөн яҙа. Ғәҙәттәгесә һүндерелгән: яҙма донъяны күсерә.",
|
||||
"horizon.agent.record_session": "Яҙҙырыу сеансын башлау",
|
||||
"horizon.agent.record_stop": "Яҙҙырыуҙы туҡтатыу",
|
||||
"horizon.agent.export_repro": "Экспортлау Repro йыйылмаһы",
|
||||
"horizon.repro.started": "Яҙҙырыу башланды; F3+R уны туҡтата һәм өйөмдө яҙа.",
|
||||
"horizon.repro.stopped": "Яҙма туҡтатылды: %s",
|
||||
"horizon.repro.bundle": "Репро пакеты: %s",
|
||||
"horizon.repro.error": "Ҡабатлау: %s",
|
||||
"horizon.repro.no_recording": "Әлегә экспортҡа сығарырлыҡ яҙма юҡ.",
|
||||
"horizon.repro.replay_started": "Ҡабатлау башланды: %s",
|
||||
"horizon.pack.addInfo": "Modrinth ресурстар пакетын өҫтәү",
|
||||
"horizon.pack.addWorking": "Һорау Modrinth...",
|
||||
"horizon.pack.addNone": "Ресурс пакеттары табылмаған",
|
||||
"horizon.pack.addTitle": "Ресурстар пакетын өҫтәү",
|
||||
"horizon.pack.install": "Ҡуйыу",
|
||||
"horizon.pack.installing": "Ҡуйыу",
|
||||
"horizon.pack.installed": "Ҡуйылған",
|
||||
"horizon.pack.search": "Эҙләү",
|
||||
"horizon.pack.searchHint": "Эҙләү Modrinth...",
|
||||
"horizon.pack.filters": "Фильтрҙар",
|
||||
"horizon.pack.filter.realistic": "Ысынбарлыҡҡа ярашлы",
|
||||
"horizon.pack.filter.simplistic": "Ябайлаштырылған",
|
||||
"horizon.pack.filter.vanillaLike": "Ваниль һымаҡ",
|
||||
"horizon.pack.filter.x16": "16х",
|
||||
"horizon.pack.filter.x32": "32х",
|
||||
"horizon.pack.filter.x64": "64х",
|
||||
"horizon.pack.addFailed": "Пакетты өҫтәп булманы: %s",
|
||||
"horizon.extras.header.cosmetics": "Косметика",
|
||||
"horizon.extras.cosmetics_mine": "Минең косметика",
|
||||
"horizon.extras.cosmetics_mine.tooltip": "Йәшерен: һәр Horizon уйынсы һеҙҙең косметикаһыҙ күрә, һеҙ ҙә индерелгән. Шул уҡ переключатель стартер’s Гардероб.",
|
||||
"horizon.extras.cosmetics_others": "Башҡа уйынсыларҙың косметикаһы",
|
||||
"horizon.extras.cosmetics_others.tooltip": "Башҡа ниндәй Horizon уйынсылар кейә: барыһы ла, тик уларҙы исем биркаһы стилдәре, йәки бер нәмә лә. Йәшерен, уйын бер ҡасан да һорамай, улар нимә кейә.",
|
||||
"horizon.extras.cosmetics_auras": "Башҡаларҙың ауралары һәм эҙҙәре",
|
||||
"horizon.extras.cosmetics_auras.tooltip": "Аура һәм эҙҙәр башҡа уйынсылар кейә. Һүндерелгән по умолчанию.",
|
||||
"horizon.extras.cosmetics.shown": "Күрһәтелгән",
|
||||
"horizon.extras.cosmetics.hidden": "Йәшерен",
|
||||
"horizon.extras.cosmetics.name_tags": "Исем билдәләре генә"
|
||||
}
|
||||
@@ -1,485 +0,0 @@
|
||||
{
|
||||
"horizon.options.controller": "Кантралёр",
|
||||
"horizon.options.vibration": "Вібрацыя",
|
||||
"horizon.options.adaptive_triggers": "Адаптыўныя трыгеры",
|
||||
"horizon.options.lightbar": "Светлавая панэль",
|
||||
"horizon.options.look_sensitivity": "Адчувальнасць агляду",
|
||||
"horizon.options.invert_x": "Інверсія агляду па X",
|
||||
"horizon.options.invert_y": "Інверсія агляду па Y",
|
||||
"horizon.options.sneak_button": "Кнопка красціся",
|
||||
"horizon.options.move_deadzone": "Мёртвая зона руху",
|
||||
"horizon.options.look_deadzone": "Мёртвая зона агляду",
|
||||
"horizon.options.button_glyphs": "Гліфы кнопак",
|
||||
"horizon.options.menu_navigation": "Навігацыя па меню",
|
||||
"horizon.options.on_screen_keyboard": "Экранная клавіятура",
|
||||
"horizon.options.chat_delay": "Затрымка чата",
|
||||
"horizon.options.lightbar.health": "здароўе",
|
||||
"horizon.options.lightbar.dimension": "Вымярэнне",
|
||||
"horizon.options.glyphs.auto": "Аўто",
|
||||
"horizon.options.menu_focus": "Фокус",
|
||||
"horizon.options.menu_pointer": "Паказальнік",
|
||||
"horizon.options.keyboard.auto": "Аўто",
|
||||
"horizon.options.unavailable": "Гэтая функцыя яшчэ недаступная ў Horizon.",
|
||||
"horizon.options.search": "Налады пошуку...",
|
||||
"horizon.pages.mods": "Моды",
|
||||
"horizon.pages.shader_packs": "Пакеты шэйдараў",
|
||||
"horizon.pages.shader_options": "Параметры шэйдара",
|
||||
"horizon.pages.controller": "Налады кантролера",
|
||||
"horizon.pages.controller_binds": "Прывязкі кантролера",
|
||||
"horizon.pages.extras": "Horizon Дадаткова",
|
||||
"horizon.pages.settings": "Horizon Налады",
|
||||
"horizon.pages.agent": "Агент штучнага інтэлекту",
|
||||
"horizon.split.leave": "Сыходзь",
|
||||
"horizon.pages.split_settings": "Налады падзеленага экрана",
|
||||
"horizon.pages.split_profiles": "Хто гуляе?",
|
||||
"horizon.pages.split_setup": "Налада падзеленага экрана",
|
||||
"horizon.pages.music": "Музыка",
|
||||
"horizon.pages.music.menu": "Меню",
|
||||
"horizon.pages.music.overworld": "Надсвет",
|
||||
"horizon.pages.music.creative": "Творчы",
|
||||
"horizon.pages.music.underwater": "Пад вадой",
|
||||
"horizon.pages.music.end": "Канец",
|
||||
"horizon.pages.music.boss": "Бос",
|
||||
"horizon.pages.music.credits": "Крэдыты",
|
||||
"horizon.pages.music.overworld_biomes": "Сусветныя біёмы",
|
||||
"horizon.pages.music.nether_biomes": "Ніжнія біёмы",
|
||||
"horizon.pages.music.end_biomes": "Канчатковыя біёмы",
|
||||
"horizon.pages.music.other": "Іншая музыка",
|
||||
"horizon.pages.music.empty": "Няма музычных трэкаў: не загружаюцца гукі гульні.",
|
||||
"horizon.pages.music.tooltip": "Убудаваныя трэкі ў залежнасці ад сітуацыі: прайграйце адзін зараз або адключыце любы, каб гульня перастала яго выбіраць.",
|
||||
"horizon.pages.settings.tooltip": "Уласныя налады Horizon: графіка, кантролеры, дадатковыя функцыі і моды.",
|
||||
"horizon.pages.graphics.tooltip": "Простая графіка: кожная налада паказвае, колькі яна каштуе.",
|
||||
"horizon.pages.graphics_advanced.tooltip": "Кожная налада графікі, у тым ліку Horizon, з коштам кожнай.",
|
||||
"horizon.pages.controller.tooltip": "Кантролеры: іх кнопкі, стыкі, вібрацыя і навігацыя па меню.",
|
||||
"horizon.pages.extras.tooltip": "Неабавязковыя дадаткі, усе выключаны па змаўчанні: гульня застаецца яркай, пакуль вы не ўключыце адну.",
|
||||
"horizon.pages.agent.tooltip": "Дазвольце вашаму ўласнаму агенту штучнага інтэлекту гуляць: кліенты horizon-ctl і MCP на гэтым кампутары.",
|
||||
"horizon.pages.split_profiles.tooltip": "Хто гуляе: профіль захоўваецца ў гэтай тэчцы гульні.",
|
||||
"horizon.pages.split_setup.tooltip": "Вокны і маніторы, і хто дзе гуляе.",
|
||||
"horizon.pages.shader_packs.tooltip": "Пакеты шэйдараў з папкі shaderpacks: уласныя Horizon і тыя, якія знаходзяцца ў фармаце shaders/. Бяжыць адзін за адным.",
|
||||
"horizon.pages.mods.tooltip": "Моды, зробленыя для Horizon, з папкі horizon-mods. Кожны працуе ў пясочніцы і застаецца выключаным, пакуль вы яго не ўключыце.",
|
||||
"horizon.pages.open.graphics": "Графіка...",
|
||||
"horizon.pages.open.graphics_advanced": "Пашыраная графіка...",
|
||||
"horizon.pages.open.shader_packs": "Пакеты шэйдэраў...",
|
||||
"horizon.pages.open.controller": "Налады кантролера...",
|
||||
"horizon.pages.open.split_setup": "Раздзелены экран...",
|
||||
"horizon.pages.open.extras": "Дадаткова...",
|
||||
"horizon.pages.open.mods": "Моды...",
|
||||
"horizon.pages.open.agent": "Агент штучнага інтэлекту...",
|
||||
"horizon.pages.open.horizon_graphics": "Horizon Графіка...",
|
||||
"horizon.pages.open.controller_binds": "Кантролер прывязвае...",
|
||||
"horizon.pages.open.horizon_settings": "Horizon Налады...",
|
||||
"horizon.pages.open.music": "музыка...",
|
||||
"horizon.mods.empty": "Няма модаў: змясціце файлы .hmod у тэчку horizon-mods",
|
||||
"horizon.mods.stopped": " (Спынена)",
|
||||
"horizon.mods.not_built": " (Не пабудаваны)",
|
||||
"horizon.mods.settings": "Налады",
|
||||
"horizon.mods.in_world": " (пакінуць свет змяніцца)",
|
||||
"horizon.mods.open_folder": "Адкрыць тэчку Mod",
|
||||
"horizon.shader_packs.empty": "Пакетаў шэйдэраў няма: змесціце іх у тэчку шэйдэраў",
|
||||
"horizon.shader_options.open": "Параметры шэйдара...",
|
||||
"horizon.shader_options.profile": "Профіль",
|
||||
"horizon.shader_options.loading": "Чытанне параметраў пакета шэйдараў...",
|
||||
"horizon.agent.copy_details": "Скапіруйце звесткі аб падключэнні",
|
||||
"horizon.multiplayer.join": "Далучайцеся",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Адмяніць паўторнае падключэнне",
|
||||
"horizon.multiplayer.join_friend": "Далучайцеся да сяброў",
|
||||
"horizon.multiplayer.invite_code": "Код запрашэння",
|
||||
"horizon.multiplayer.relay": "Рэле (неабавязкова)",
|
||||
"Preset": "Прадусталяваная",
|
||||
"LOD Distance": "LOD Адлегласць",
|
||||
"Far Detail": "Далёкая дэталь",
|
||||
"Cave Culling": "Выбракоўка пячор",
|
||||
"Entity Culling": "Выбракоўванне сутнасці",
|
||||
"Chunk Updates": "Абнаўленні чанка",
|
||||
"Upload Budget": "Бюджэт загрузкі",
|
||||
"Sky": "неба",
|
||||
"Fog": "Туман",
|
||||
"Weather": "Надвор'е",
|
||||
"Animated Textures": "Аніміраваныя тэкстуры",
|
||||
"Present Mode": "Рэжым сучаснасці",
|
||||
"Frame Pacing": "Тэмп кадраў",
|
||||
"Worker Threads": "Працоўныя тэмы",
|
||||
"Resolution Scale": "Шкала дазволу",
|
||||
"Anti-aliasing": "Згладжванне",
|
||||
"Upscaling": "Апскейлінг",
|
||||
"Paper White": "Папера белая",
|
||||
"Peak Brightness": "Пікавая яркасць",
|
||||
"Black Level": "Узровень чорнага",
|
||||
"UI Brightness": "Яркасць інтэрфейсу",
|
||||
"Battery Mode": "Рэжым батарэі",
|
||||
"Graphics API": "Графічны API",
|
||||
"Translucent Batching": "Напаўпразрысты дазаванне",
|
||||
"Performance Overlay": "Прадукцыйнасць Overlay",
|
||||
"Full Detail Distance": "Поўная адлегласць",
|
||||
"On": "Укл",
|
||||
"Off": "Выкл",
|
||||
"Unlimited": "Неабмежаваны",
|
||||
"Auto": "Аўто",
|
||||
"Custom": "Прыстасаваныя",
|
||||
"Performance": "Прадукцыйнасць",
|
||||
"Balanced": "Ураўнаважаны",
|
||||
"Quality": "Якасць",
|
||||
"Ultra": "Ультра",
|
||||
"Ultra Performance": "Ультрапрадукцыйнасць",
|
||||
"Native": "Натыўны",
|
||||
"Full": "Поўны",
|
||||
"Reduced": "Паменшаны",
|
||||
"Efficient": "Эфектыўная",
|
||||
"Precise": "Дакладны",
|
||||
"Auto (VSync)": "Аўта (VSync)",
|
||||
"Mailbox": "Паштовая скрыня",
|
||||
"Immediate": "Неадкладны",
|
||||
"FPS and Graph": "FPS і графік",
|
||||
"No cost": "Без выдаткаў",
|
||||
"Low cost": "Нізкі кошт",
|
||||
"Medium cost": "Сярэдні кошт",
|
||||
"High cost": "Высокі кошт",
|
||||
"Saves time": "Эканоміць час",
|
||||
"Graphics": "Графіка",
|
||||
"Done": "Гатова",
|
||||
"This feature is not yet available in Horizon.": "Гэтая функцыя яшчэ недаступная ў Horizon.",
|
||||
"Your display or system doesn't offer HDR output.": "Ваш дысплэй або сістэма не забяспечвае выхад HDR.",
|
||||
"Turn HDR on to use it.": "Уключыце HDR, каб выкарыстоўваць яго.",
|
||||
"Your GPU or graphics API doesn't support it.": "Ваш GPU або графічны API не падтрымлівае гэта.",
|
||||
"%s chunks": "%s кавалкаў",
|
||||
"%s per frame": "%s за кадр",
|
||||
"%s MiB/frame": "%s МіБ/кадр",
|
||||
"%s KiB/frame": "%s КіБ/кадр",
|
||||
"Auto (%s)": "Аўто (%s)",
|
||||
"Full-detail terrain range. LOD Distance can extend local worlds farther.": "Поўны дэталёвы дыяпазон мясцовасці. LOD Адлегласць можа пашырыць мясцовыя светы далей.",
|
||||
"Coarse distant terrain in local worlds. Integrated GPUs stop at 128 chunks.": "Грубая аддаленая мясцовасць у лакальных мірах. Убудаваныя графічныя працэсары спыняюцца на 128 кавалках.",
|
||||
"Reduced draws far terrain coarser: faster on this integrated GPU.": "Паменшанае малюнак больш грубай мясцовасці: хутчэй на гэтым убудаваным графічным працэсары.",
|
||||
"Reduced draws far terrain coarser, for GPUs it slows down.": "Зніжэнне малюе больш грубую мясцовасць, для графічных працэсараў гэта запавольваецца.",
|
||||
"Field of view. Wider shows more terrain at once.": "Поле зроку. Шырэй паказвае адразу больш мясцовасці.",
|
||||
"Caps frames per second; a cap saves power and heat.": "Колькасць кадраў у секунду; шапка эканоміць энергію і цяпло.",
|
||||
"Waits for the display's refresh: no tearing, a little latency.": "Чакае абнаўлення дысплея: без разрыву, з невялікай затрымкай.",
|
||||
"Size of menus and the HUD.": "Памер меню і HUD.",
|
||||
"Sets the options below together. Changing one makes it Custom.": "Разам усталёўвае параметры ніжэй. Змена аднаго робіць яго карыстальніцкім.",
|
||||
"Lava, fire and sunlight shine brighter than white. Needs an HDR display.": "Лава, агонь і сонечнае святло ззяюць ярчэй белага. Патрэбен дысплей HDR.",
|
||||
"Your display's darkest black. 0 for OLED; LCDs glow a little.": "Самы цёмны чорны на вашым дысплеі. 0 для OLED; ВК-дысплеі трохі свецяцца.",
|
||||
"How bright highlights get. Auto: the peak your system reports.": "Якія яркія моманты становяцца. Аўта: пік, пра які паведамляе ваша сістэма.",
|
||||
"How bright white paper and menus look. Auto: your desktop's white.": "Як ярка выглядаюць белая папера і меню. Аўтаматычна: ваш працоўны стол белы.",
|
||||
"How bright the HUD and menus are, against paper white.": "Наколькі яркія HUD і меню на фоне белай паперы.",
|
||||
"How far mobs, crops and machines keep working. CPU time.": "Як далёка працягваюць працаваць мобы, ураджай і машыны. працэсарны час.",
|
||||
"How far away mobs and items are drawn, as a share of normal.": "Наколькі далёка намаляваны мобы і прадметы ў долі звычайнага.",
|
||||
"Skips terrain hidden behind closed sections. Off costs a lot.": "Прапускае мясцовасць, схаваную за закрытымі ўчасткамі. Off каштуе шмат.",
|
||||
"Hides entities behind opaque blocks.": "Хавае аб'екты за непразрыстымі блокамі.",
|
||||
"No entity occlusion culling.": "Няма выбракоўвання аклюзіі сутнасці.",
|
||||
"CPU occlusion test runs this frame.": "Тэст аклюзіі ЦП запускае гэты кадр.",
|
||||
"Occlusion checks run on a worker thread.": "Праверкі аклюзіі выконваюцца ў працоўным патоку.",
|
||||
"Meshed sections uploaded per frame. Lower is smoother while loading.": "Сеткавыя раздзелы, загружаныя на кадр. Ніжняя больш плыўная падчас загрузкі.",
|
||||
"Soft shadows in corners. Costs chunk building time only.": "Мяккія цені ў кутах. Каштуе толькі час зборкі кавалкаў.",
|
||||
"Smaller copies of block textures for distant terrain: less shimmer.": "Меншыя копіі блочных тэкстур для далёкай мясцовасці: менш мігацення.",
|
||||
"Blends grass and water colours across biomes when building chunks.": "Змешвае колеру травы і вады ў біёмах пры стварэнні кавалкаў.",
|
||||
"Cloud layer: off, flat (fast) or 3D (fancy).": "Воблачны пласт: выключаны, плоскі (хуткі) або 3D (фантастычны).",
|
||||
"How far clouds reach, in chunks.": "Наколькі дасягаюць воблакі, кавалкамі.",
|
||||
"Draws the sky, sun, moon and stars.": "Малюе неба, сонца, месяц і зоркі.",
|
||||
"Distance fog. Off shows the edge of the loaded world.": "Далёкі туман. Off паказвае край загружанага свету.",
|
||||
"Draws rain and snow.": "Малюе дождж і снег.",
|
||||
"How far rain and snow fall around you.": "Як далёка вакол цябе дождж і снег.",
|
||||
"How many particles effects spawn.": "Колькі эфектаў часціц спараджае.",
|
||||
"Round shadows under mobs and items.": "Круглыя цені пад мобамі і прадметамі.",
|
||||
"Leaves with see-through gaps. Off draws them solid.": "Лісце з празрыстымі прасветамі. Off малюе іх цвёрда.",
|
||||
"Sorts water and glass more exactly behind each other.": "Сартуе ваду і шкло больш дакладна адзін за адным.",
|
||||
"Which chunk rebuilds wait for the frame: none, nearby or yours.": "Які кавалак перабудоўваецца ў чаканні кадра: ніякі, побач ці ваш.",
|
||||
"Animates water, lava, fire and other moving textures.": "Ажыўляе ваду, лаву, агонь і іншыя рухомыя тэкстуры.",
|
||||
"World pixels per screen pixel. Below 100% is faster on the GPU.": "Сусветныя пікселі на піксель экрана. Ніжэй за 100% хутчэй на графічным працэсары.",
|
||||
"Smooths jagged edges. FXAA: softer. TAA: smoothest, moving things may trail.": "Згладжвае няроўныя краю. FXAA: мякчэй. TAA: самыя гладкія рэчы, якія рухаюцца, могуць цягнуцца.",
|
||||
"Renders the world smaller and rebuilds it with TAA. Faster, softer.": "Памяншае свет і перабудоўвае яго з TAA. Хутчэй, мякчэй.",
|
||||
"HDR output on an HDR display: lava, fire and sunlight shine brighter.": "Вывад HDR на дысплеі HDR: лава, агонь і сонечнае святло ззяюць ярчэй.",
|
||||
"How frames reach the screen while VSync is off.": "Як кадры трапляюць на экран, калі VSync выключаны.",
|
||||
"Efficient sleeps until the next frame; Precise spins, like vanilla.": "Эфектыўнае спіць да наступнага кадра; Дакладныя спіны, як ваніль.",
|
||||
"Fills the screen (borderless).": "Запаўняе экран (без палёў).",
|
||||
"Brightens dark places.": "Асвятляе цёмныя месцы.",
|
||||
"Lowers the frame rate when minimised, or idle too.": "Зніжае частату кадраў пры згортванні або ў рэжыме чакання.",
|
||||
"Sways the view while walking.": "Качае погляд падчас хады.",
|
||||
"Fades newly loaded chunks in.": "Змяншае толькі што загружаныя кавалкі.",
|
||||
"Darkens the screen's edges.": "Зацямняе краю экрана.",
|
||||
"Shows when the world saves.": "Паказвае, калі свет ратуе.",
|
||||
"Shows attack cooldown.": "Паказвае час аднаўлення атакі.",
|
||||
"60 FPS cap (30 in menus or unfocused), half the workers: less power.": "Абмежаванне 60 кадраў у секунду (30 у меню або без увагі), палова рабочых: меншая магутнасць.",
|
||||
"Threads that generate, light and mesh chunks. Next start.": "Ніткі, якія ствараюць, лёгкія і сеткаватыя кавалкі. Наступны старт.",
|
||||
"Vulkan, then DirectX 12 (Windows) or OpenGL (Linux) where it fails. Next start.": "Vulkan, затым DirectX 12 (Windows) або OpenGL (Linux), дзе не атрымліваецца. Наступны старт.",
|
||||
"Water and glass in one buffer: fewer draw calls, a bit more VRAM. Next start.": "Вада і шкло ў адным буферы: менш выклікаў на розыгрыш, крыху больш VRAM. Наступны старт.",
|
||||
"Mesh data sent to the GPU per frame. Lower is smoother while loading.": "Даныя сеткі, якія адпраўляюцца ў графічны працэсар за кадр. Ніжняя больш плыўная падчас загрузкі.",
|
||||
"Frame rate and frame times over the game, with a graph.": "Частата кадраў і час кадраў у гульні з графікам.",
|
||||
"horizon.multiplayer.finding_friend": "Пошук свету вашага сябра...",
|
||||
"horizon.multiplayer.not_signed_in": "Гэты сервер знаходзіцца ў онлайн-рэжыме. Увайдзіце ў свой уліковы запіс Microsoft у праграме запуску Horizon, каб далучыцца да яе.",
|
||||
"horizon.multiplayer.invalid_address": "Няправільны адрас сервера: %s",
|
||||
"horizon.multiplayer.connected_friend_direct": "Падключаны непасрэдна да вашага сябра...",
|
||||
"horizon.multiplayer.connected_friend_relay": "Падключаны да вашага сябра праз рэле...",
|
||||
"General": "Генерал",
|
||||
"Advanced": "Пашыраны",
|
||||
"Undo": "Адмяніць",
|
||||
"Search options": "Параметры пошуку",
|
||||
"Shortcut": "Ярлык",
|
||||
"Distance": "Дыстанцыя",
|
||||
"Display": "Дысплей",
|
||||
"Interface": "Інтэрфейс",
|
||||
"Sky and weather": "Неба і надвор'е",
|
||||
"Lighting and entities": "Асвятленне і сутнасці",
|
||||
"Textures": "Тэкстуры",
|
||||
"Image": "Малюнак",
|
||||
"Chunk loading": "Загрузка чанка",
|
||||
"Culling": "Выбракоўка",
|
||||
"Power": "Магутнасць",
|
||||
"Frames": "Рамкі",
|
||||
"Overlay": "Накладка",
|
||||
"Output": "Выхад",
|
||||
"Brightness": "Яркасць",
|
||||
"How far you see, the window and the HUD.": "Як далёка вы бачыце, акно і HUD.",
|
||||
"The sky and weather, lighting, textures and the image.": "Неба і надвор'е, асвятленне, тэкстуры і малюнак.",
|
||||
"Chunk loading, culling and power use.": "Загрузка кавалкаў, выбракоўванне і выкарыстанне энергіі.",
|
||||
"How frames reach the screen, and the performance overlay.": "Як кадры трапляюць на экран і накладанне прадукцыйнасці.",
|
||||
"Brighter-than-white output on an HDR display.": "Ярчэйшы за белы выхад на HDR-дысплеі.",
|
||||
"Results for \"%s\"": "Вынікі для \"%s\"",
|
||||
"No settings match.": "Налады не супадаюць.",
|
||||
"horizon.title.not_affiliated": "Не звязаны з Mojang або Microsoft",
|
||||
"horizon.extras.header.lighting": "Асвятленне",
|
||||
"horizon.extras.header.third_person": "Трэцяя асоба",
|
||||
"horizon.extras.header.particles": "Часціцы",
|
||||
"horizon.extras.header.inventory": "Інвентар",
|
||||
"horizon.extras.header.chat": "Чат",
|
||||
"horizon.extras.header.multiplayer": "Мультыплэер",
|
||||
"horizon.extras.header.sound": "Гук",
|
||||
"horizon.extras.header.menus": "Меню",
|
||||
"horizon.extras.fullbright": "Фулбрайт",
|
||||
"horizon.extras.dynamic_lights": "Дынамічныя фары",
|
||||
"horizon.extras.own_name_tag": "Уласная бірка з імем",
|
||||
"horizon.extras.hide_block_particles": "Схаваць часціцы, якія разбіваюць блок",
|
||||
"horizon.extras.auto_reconnect": "Аўтаматычнае падключэнне",
|
||||
"horizon.extras.ui_sound_reserve": "Захоўвайце каналы для гукаў інтэрфейсу",
|
||||
"horizon.extras.music_page": "Музычная старонка",
|
||||
"horizon.extras.mouse_tweaks": "Элементы кіравання Mouse Tweaks",
|
||||
"horizon.extras.mouse_tweaks_wheel": "Кола пракруткі перамяшчае элементы",
|
||||
"horizon.extras.sort": "Кнопкі сартавання",
|
||||
"horizon.extras.sort_key": "Ключ сартавання",
|
||||
"horizon.extras.agent_control": "Агентскі кантроль",
|
||||
"horizon.extras.splitscreen": "Раздзелены экран",
|
||||
"horizon.extras.chat_animation": "Плаўны чат",
|
||||
"horizon.extras.chat_indicator": "Індыкатар паведамлення",
|
||||
"horizon.extras.wheel_cycles_settings": "Кола пракруткі змяняе налады",
|
||||
"horizon.extras.pack_browser": "Браўзэр Resource Pack",
|
||||
"horizon.extras.fullbright.tooltip": "Паказвайце блокі і аб'екты пры поўным асвятленні, нават у цёмных месцах.",
|
||||
"horizon.extras.dynamic_lights.tooltip": "Факелы і іншыя крыніцы святла, якія трымаюць, упускаюць або гараць, асвятляюць блокі вакол іх.",
|
||||
"horizon.extras.own_name_tag.tooltip": "Паказвае ваша імя над галавой ад трэцяй асобы, як яго бачаць іншыя гульцы. Схаваны, калі сервер хавае назвы вашай каманды або вы нябачныя.",
|
||||
"horizon.extras.hide_block_particles.tooltip": "Хавае часціцы смецця, якія вырываюцца з разбітага блока і рассыпаюцца на той, які вы здабываеце. Іншыя часціцы застаюцца.",
|
||||
"horizon.extras.auto_reconnect.tooltip": "Паўторна далучаецца да сервера пасля страты злучэння або перазагрузкі, ніколі пасля кіку або забароны.",
|
||||
"horizon.extras.ui_sound_reserve.tooltip": "Гукі ў свеце пакідаюць 16 з 247 гукавых каналаў свабоднымі, таму іх паток (вялікі выбух) не можа заглушыць націсканне меню.",
|
||||
"horizon.extras.music_page.tooltip": "«Музыка і гукі» атрымлівае старонку «Музыка...»: прайграйце любы ўбудаваны трэк зараз або адключыце адзін, каб гульня перастала яго выбіраць. Нічога не зменіцца, пакуль вы не адключыце трэк.",
|
||||
"horizon.extras.mouse_tweaks.tooltip": "Перацягванне правай кнопкай мышы змяшчае па адным у слот, перацягванне левай кнопкай збірае ўтрымоўваны прадмет, Shift + перацягванне левай кнопкай мышы перамяшчае кожны слот.",
|
||||
"horizon.extras.mouse_tweaks_wheel.tooltip": "Уніз адпраўляе адзін элемент слота на другі бок, уверх вяртае адзін назад.",
|
||||
"horizon.extras.sort.tooltip": "Сартуе інвентар, куфры, бочкі, эндэрскія куфры і шулкерныя скрыні ў творчым парадку ўкладак, аб'ядноўваючы стосы.",
|
||||
"horizon.extras.sort_key.tooltip": "Сартуе частку пад курсорам. Націсніце, затым націсніце клавішу; Уцёкі развязвае яго.",
|
||||
"horizon.extras.no_ai.tooltip": "Хавае і адключае ўсе функцыі штучнага інтэлекту: кіраванне агентамі, horizon-ctl і серверы MCP. Пераключальнік пускавы ўстаноўкі таксама.",
|
||||
"horizon.extras.agent_control.tooltip": "Ваш уласны агент штучнага інтэлекту можа гуляць у гэтую гульню праз horizon-ctl або кліент MCP толькі на гэтым камп'ютары.",
|
||||
"horizon.extras.discord_presence.tooltip": "Паказвае Playing Horizon у вашым профілі Discord падчас запуску Discord: меню, адзіночны або шматкарыстальніцкі рэжым, памер і час гульні. Ніколі не назва свету або адрас сервера.",
|
||||
"Calibrate...": "Адкалібраваць...",
|
||||
"Calibrate Anyway...": "Усё роўна адкалібраваць...",
|
||||
"Use Automatic Setup": "Выкарыстоўвайце аўтаматычную наладу",
|
||||
"Shortcut: %s": "Ярлык: %s",
|
||||
"Next": "Далей",
|
||||
"HDR Calibration %s/%s: %s": "Каліброўка HDR %s/%s: %s",
|
||||
"Sets the three below full screen, one test picture at a time.": "Устанаўлівае тры ніжэй на ўвесь экран, па адным тэставым малюнку.",
|
||||
"A key that opens this screen from the game. Not bound at first.": "Ключ, які адкрывае гэты экран з гульні. Спачатку не звязаны.",
|
||||
"Your display didn't say how bright it gets: calibrate to set it up.": "На вашым дысплеі не пазначана, наколькі ён яркім: адкалібруйце, каб наладзіць яго.",
|
||||
"Already set up for your display. Only calibrate if HDR looks wrong.": "Ужо наладжана для вашага дысплея. Калібруйце, толькі калі HDR выглядае няправільна.",
|
||||
"Puts Peak Brightness and Paper White back to your display's setup.": "Вяртае пікавую яркасць і папяровы белы ў налады вашага дысплея.",
|
||||
"Set up for your display. Leave on Auto unless it looks wrong.": "Наладзьце для вашага дысплея. Пакіньце на Auto, калі гэта не выглядае няправільна.",
|
||||
"Lower it until the logo just disappears into the black.": "Апускайце яго, пакуль лагатып проста не знікне ў чорным колеры.",
|
||||
"Raise it until the logo just disappears into the bright square.": "Падымайце яго, пакуль лагатып не знікне ў яркім квадраце.",
|
||||
"Make the card a white that's comfortable to look at for a long time, in the light you play in.": "Зрабіце карту белай, каб на яе было зручна глядзець на працягу доўгага часу пры святле, пры якім вы гуляеце.",
|
||||
"horizon.splitscreen.profiles.forget": "Забыць",
|
||||
"horizon.splitscreen.profiles.create": "Ствараць",
|
||||
"horizon.splitscreen.profiles.you": " - вы",
|
||||
"horizon.splitscreen.profiles.forget_named": "Забыць %s",
|
||||
"horizon.splitscreen.profiles.previous": "Папярэдні",
|
||||
"horizon.splitscreen.profiles.next": "Далей",
|
||||
"horizon.splitscreen.profiles.new_button": "Новы профіль...",
|
||||
"horizon.splitscreen.profiles.forget_title": "Забыць профіль?",
|
||||
"horizon.splitscreen.profiles.forget_leaves": "Ён пакідае спіс гэтага экземпляра.",
|
||||
"horizon.splitscreen.profiles.forget_kept": "Сусветны прагрэс і налады захоўваюцца.",
|
||||
"horizon.splitscreen.profiles.new_title": "Новы профіль",
|
||||
"horizon.splitscreen.profiles.name_hint": "Імя (ад 3 да 16 літар, лічбаў, _)",
|
||||
"horizon.splitscreen.profiles.switch_title": "Пераключыць профіль",
|
||||
"horizon.splitscreen.profiles.offline_note": "Афлайн-профілі належаць да гэтага асобніка",
|
||||
"horizon.splitscreen.profiles.move": "Рухайцеся туды, дзе гуляеце",
|
||||
"horizon.splitscreen.profiles.move_place": "Рухайцеся туды, дзе вы гуляеце: %s",
|
||||
"horizon.friends.title": "Гуляць з сябрамі",
|
||||
"horizon.friends.hint": "Сябры ўстаўце код пасля кнопкі \"Сябры\" на тытульным экране ў раздзеле \"Прамое злучэнне\" або на старонцы \"Прайграванне\" праграмы запуску.",
|
||||
"horizon.friends.copied": "Скапіравана",
|
||||
"horizon.friends.copy_code": "Скапіруйце код",
|
||||
"horizon.friends.stop_sharing": "Спыніць абагульванне",
|
||||
"horizon.friends.none_yet": "Тут пакуль няма сяброў.",
|
||||
"horizon.friends.here_now": "Вось зараз: %s",
|
||||
"horizon.friends.share_intro": "Падзяліцеся гэтым светам за кодам запрашэння: ён адкрываецца ў лакальнай сеткі, і сябры ў Інтэрнэце далучаюцца з кодам. Наш сервер не задзейнічаны.",
|
||||
"horizon.friends.start_sharing": "Пачніце абагульваць",
|
||||
"horizon.friends.troubleshooting": "Ліквідацыя непаладак...",
|
||||
"horizon.friends.stopped": "Абагульванне кода запрашэння спынена.",
|
||||
"horizon.friends.short_code_changed": "Хтосьці спрабаваў увесці няправільны кароткі код, таму ён змяніўся.",
|
||||
"horizon.friends.wrong_code": "Хтосьці паспрабаваў увесці няправільны код.",
|
||||
"horizon.friends.code_ready": "Ваш код запрашэння гатовы: меню гульні, гульня з сябрамі. Скапіруйце яго і адпраўце сваім сябрам.",
|
||||
"horizon.friends.share_failed": "Не атрымалася падзяліцца светам: %s",
|
||||
"horizon.friends.finding_route": "Пошук, як сябры могуць звязацца з вамі...",
|
||||
"horizon.osk.shift": "зрух",
|
||||
"horizon.osk.space": "космас",
|
||||
"horizon.osk.delete": "Выдаліць",
|
||||
"horizon.osk.type": "Тып",
|
||||
"horizon.osk.symbols": "Сімвалы",
|
||||
"horizon.osk.close": "Блізка",
|
||||
"horizon.extras.chat_animation.tooltip": "Новы радок чата слізгае злева, пакуль ён знікае, замест таго, каб выглядаць цалкам.",
|
||||
"horizon.extras.chat_indicator.tooltip": "Каляровая паласа злева ад радка чата.",
|
||||
"horizon.extras.wheel_cycles_settings.tooltip": "Кола пракруткі над кнопкай у наладах змяняе яе значэнне, як у vanilla. Выключана, кола толькі пракручвае спіс.",
|
||||
"horizon.extras.pack_browser.tooltip": "Select Resource Packs атрымлівае кнопку «Дадаць», якая выконвае пошук у Modrinth і ўсталёўвае пакет у вашу тэчку пакетаў. Выключана, гульня ніколі не звязваецца з Modrinth.",
|
||||
"horizon.extras.splitscreen.tooltip": "У свеце іншы кантролер можа далучыцца як новы гулец: націсніце A, калі ён запытае, затым выберыце профіль і дзе гуляць.",
|
||||
"horizon.splitscreen.setup.monitor": "Манітор %s",
|
||||
"horizon.splitscreen.setup.monitor_unknown": "манітор",
|
||||
"horizon.splitscreen.setup.window": "Акно %s",
|
||||
"horizon.splitscreen.setup.new_window": "Новае акно",
|
||||
"horizon.splitscreen.setup.fill_monitor": "Запоўніце %s",
|
||||
"horizon.splitscreen.setup.done": "Гатова",
|
||||
"horizon.splitscreen.setup.on": "Укл",
|
||||
"horizon.splitscreen.setup.off": "Выкл",
|
||||
"horizon.splitscreen.setup.player": "Гулец %s",
|
||||
"horizon.splitscreen.setup.revert": "Вярнуць",
|
||||
"horizon.splitscreen.setup.apply": "Ужыць",
|
||||
"horizon.splitscreen.setup.warning.empty_main": "Ніхто не гуляе ў Window 1: яна хаваецца",
|
||||
"horizon.splitscreen.setup.warning.empty_close": "Ніхто не гуляе ў Window %s: яно зачыняецца пры ўжыванні",
|
||||
"horizon.splitscreen.setup.warning.empty_new": "Ніхто не гуляе ў Window %s: яно не адчыняецца",
|
||||
"horizon.splitscreen.setup.warning.on_main": "Window %s запаўняе манітор Window 1: яго гульцы гуляюць у Window 1",
|
||||
"horizon.splitscreen.setup.warning.unplugged": "Манітор Window %s не падключаны: яго гульцы гуляюць у Window 1",
|
||||
"horizon.splitscreen.setup.status.off": "Splitscreen выключаны.",
|
||||
"horizon.splitscreen.setup.status.enable_hint": "Уключыце яго, затым націсніце кнопку на іншым кантролеры ў свеце.",
|
||||
"horizon.splitscreen.setup.status.join_hint": "Націсніце A, каб далучыцца і выбраць профіль.",
|
||||
"horizon.splitscreen.setup.status.idle": "Раздзелены экран у рэжыме чакання",
|
||||
"horizon.splitscreen.setup.status.alone": "Няма іншых мясцовых гульцоў.",
|
||||
"horizon.splitscreen.setup.status.solo_controls": "Індывідуальныя элементы кіравання і зваротная сувязь кантролера не змяніліся.",
|
||||
"horizon.splitscreen.setup.window_empty": "Акно %s: пустое",
|
||||
"horizon.splitscreen.setup.note.select": "Пстрыкніце прайгравальнік або перацягніце яго ў акно або на манітор.",
|
||||
"horizon.splitscreen.setup.note.apply": "Падайце заяўку, каб гуляць такім чынам, вярніце, каб адмяніць.",
|
||||
"horizon.splitscreen.setup.note.place": "Дзе гуляць %s?",
|
||||
"horizon.splitscreen.setup.this_player": "гэты гулец",
|
||||
"horizon.splitscreen.setup.switch_disabled": "Раздзелены экран выключаецца, як толькі іншыя сыдуць (іх меню паўзы).",
|
||||
"horizon.splitscreen.setup.window_place": "Акно %s (%s з %s)",
|
||||
"horizon.splitscreen.setup.names": "Імёны",
|
||||
"horizon.splitscreen.setup.identify": "Ідэнтыфікаваць",
|
||||
"horizon.splitscreen.setup.layout.two": "2 абмен: %s",
|
||||
"horizon.splitscreen.setup.layout.three": "3 абмен: %s",
|
||||
"horizon.splitscreen.setup.layout.two.0": "Аўто",
|
||||
"horizon.splitscreen.setup.layout.two.1": "Бок аб бок",
|
||||
"horizon.splitscreen.setup.layout.two.2": "Зверху і знізу",
|
||||
"horizon.splitscreen.setup.layout.three.0": "Адзін шырокі зверху",
|
||||
"horizon.splitscreen.setup.layout.three.1": "Адзін высокі злева",
|
||||
"horizon.splitscreen.setup.layout.three.2": "Бок аб бок",
|
||||
"horizon.splitscreen.setup.help.player": "Націсніце або перацягніце %s, каб перамясціць іх.",
|
||||
"horizon.splitscreen.setup.help.layout_auto": "%s (побач, калі акно шырокае). Націсніце, каб змяніць.",
|
||||
"horizon.splitscreen.setup.help.layout": "%s. Націсніце, каб змяніць.",
|
||||
"horizon.splitscreen.setup.help.switch": "Splitscreen для гэтага кампутара, уключаны або выключаны.",
|
||||
"horizon.splitscreen.setup.help.identify": "Погляд кожнага гульца паказвае, у якім акне ён знаходзіцца.",
|
||||
"horizon.splitscreen.setup.help.names": "Імя кожнага гульца ў куце іх агляду.",
|
||||
"horizon.splitscreen.setup.help.revert": "Адмяніць яшчэ не прымененыя змены.",
|
||||
"horizon.splitscreen.setup.help.apply": "Гуляйце так, як паказвае карта.",
|
||||
"horizon.splitscreen.setup.help.unchanged": "Пакуль нічога не змянілася.",
|
||||
"horizon.splitscreen.setup.give_up_pad": "Адмоўцеся ад Pad",
|
||||
"horizon.splitscreen.prompt.controller_connected": "Кантролер падключаны",
|
||||
"horizon.splitscreen.prompt.controller_disconnected": "Кантролер адключаны",
|
||||
"horizon.splitscreen.prompt.controller_disconnected.hint": "Зноў падключыце яго або націсніце кнопку на іншым",
|
||||
"horizon.splitscreen.prompt.join.one": "%s хоча далучыцца",
|
||||
"horizon.splitscreen.prompt.join.many": "%s кантралёраў хочуць далучыцца",
|
||||
"horizon.splitscreen.prompt.join.yes": "Далучайцеся",
|
||||
"horizon.splitscreen.prompt.join.as_p1": "Гуляць як P1",
|
||||
"horizon.splitscreen.prompt.join.no": "няма",
|
||||
"horizon.splitscreen.prompt.lost": "%s: кантролер адключаны",
|
||||
"horizon.splitscreen.prompt.lost.use_another": "Выкарыстоўвайце іншы",
|
||||
"horizon.splitscreen.prompt.lost.leave": "Сыходзь",
|
||||
"horizon.splitscreen.setup.help.give_up_pad": "Ваш кантролер перастае кіраваць вамі і прапануе далучыцца (A).",
|
||||
"horizon.splitscreen.setup.note.held_at": "%s да %s: А кідае яго сюды, Б ставіць назад.",
|
||||
"horizon.splitscreen.setup.note.held": "%s падняў: левы/правы выбіраюць, дзе, А кідае, Б ставіць назад.",
|
||||
"Hides models behind opaque blocks. Async checks off-thread; entities tick.": "Хавае мадэлі за непразрыстымі блокамі. Async правярае па-за патокам; суб'ектаў цік.",
|
||||
"Block Entities": "Блокавыя аб'екты",
|
||||
"Shulker Boxes": "Скрынкі Шулкера",
|
||||
"Reuses settled boxes; lid movement stays animated.": "Паўторна выкарыстоўвае адстойнікі; рух вечка застаецца ажыўленым.",
|
||||
"Signs": "Знакі",
|
||||
"Reuses sign text until text, light, or font changes.": "Паўторна выкарыстоўвае тэкст знака, пакуль не зменіцца тэкст, падсвятленне або шрыфт.",
|
||||
"Decorated Pots": "Упрыгожаныя гаршкі",
|
||||
"Reuses still pots; break wobble and sherd changes redraw.": "Паўторна выкарыстоўвае гаршкі; перапыніць хістанне і асколак змены перамаляваць.",
|
||||
"Bells": "Званы",
|
||||
"Reuses still bells; ringing stays animated.": "Паўторна выкарыстоўвае яшчэ званы; званок застаецца аніміраваным.",
|
||||
"Chests": "Сундукі",
|
||||
"Reuses closed chests; moving lids stay animated.": "Паўторна выкарыстоўвае закрытыя куфры; рухомыя павекі застаюцца ажыўленымі.",
|
||||
"Banners": "Банеры",
|
||||
"Reuses poles; flags and pattern layers still update.": "Паўторна выкарыстоўвае слупы; сцягі і ўзоры слаёў па-ранейшаму абнаўляюцца.",
|
||||
"Copper Golem Statues": "Медныя статуі голема",
|
||||
"Reuses statue geometry until its block state changes.": "Паўторна выкарыстоўвае геаметрыю статуі, пакуль не зменіцца яе стан блока.",
|
||||
"Leaf Culling": "Выбракоўванне лісця",
|
||||
"Every leaf face. Vanilla's fancy leaves.": "Кожны ліст твар. Вострыя лісце ванілі.",
|
||||
"Distant Plant Dither": "Дыстантны завод",
|
||||
"Keeps fine plants in high-res packs from fading far away.": "Захоўвае выдатныя расліны ў пакетах з высокім дазволам ад выцвітання.",
|
||||
"Screenshot Quality": "Якасць скрыншота",
|
||||
"F2 screenshots at this many times the game's resolution. Higher takes longer.": "Скрыншоты F2 з дазволам, які шмат разоў перавышае дазвол гульні. Вышэйшы займае больш часу.",
|
||||
"Frame Generation": "Генерацыя фрэймаў",
|
||||
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Паказвае прыдуманую рамку паміж двума сапраўднымі: больш гладкая, з крыху большым адставаннем.",
|
||||
"Shadow Cache": "Ценявы кэш",
|
||||
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Паўторна выкарыстоўвае карту ценяў зграі, калі сонца, выгляд і свет супадаюць. Выключана па змаўчанні.",
|
||||
"Find BlueMap": "Знайдзіце BlueMap",
|
||||
"On servers, finds their BlueMap web map for far terrain. Next join.": "На серверах знаходзіць вэб-карту BlueMap для далёкай мясцовасці. Далей далучыцца.",
|
||||
"BlueMap Radius": "Радыус BlueMap",
|
||||
"How far around you BlueMap tiles are downloaded. Next join.": "Як далёка вакол вас загружаюцца пліткі BlueMap. Далей далучыцца.",
|
||||
"Synchronous": "Сінхронныя",
|
||||
"Asynchronous": "Асінхронны",
|
||||
"Solid": "Цвёрды",
|
||||
"Solid Aggressive": "Суцэльны агрэсіўны",
|
||||
"Hollow": "Полая",
|
||||
"Leaves fully inside a canopy draw solid: fewer see-through layers.": "Лісце цалкам унутры полага малююць суцэльна: менш празрыстых слаёў.",
|
||||
"Leaves with one open side draw solid too: faster, chunkier edges.": "Лісце з адным адкрытым бокам таксама атрымліваюцца цвёрдымі: больш хуткія, каржакаватыя краю.",
|
||||
"Drops faces between leaves: faster, but gaps look through.": "Апускае грані паміж лісцікамі: хутчэй, але шчыліны праглядваюць.",
|
||||
"Hotbar: Previous Slot": "Гарачая панэль: папярэдні слот",
|
||||
"Hotbar: Next Slot": "Гарачая панэль: наступны слот",
|
||||
"horizon.agent.connected.one": "Падключана: 1 падключэнне агента",
|
||||
"horizon.agent.connected.many": "Падключана: %s агентскіх злучэнняў",
|
||||
"horizon.agent.connected.one_ago": "Падключана: 1 падключэнне агента, апошняя каманда %s с таму",
|
||||
"horizon.agent.connected.many_ago": "Падключана: %s злучэнняў агента, апошняя каманда %s с таму",
|
||||
"horizon.agent.waiting": "Чакаю агента.",
|
||||
"horizon.agent.off": "Выключана: агенты не могуць атрымаць доступ да гэтай гульні.",
|
||||
"horizon.agent.session_opened": "Адкрыты для гэтай сесіі праграмай, якая запусціла гульню.",
|
||||
"horizon.agent.socket": "Сокет: %s (скапіруйце дэталі для поўнага шляху)",
|
||||
"horizon.agent.local_only": "Толькі лакальна: агенты на гэтым кампутары, ніколі ў сетцы.",
|
||||
"%s nits": "%s ніт",
|
||||
"Auto (%s nits)": "Аўта (%s ніт)",
|
||||
"%.2f nits": "%.2f ніт",
|
||||
"Hides models behind opaque blocks. Entities update. Off with custom packs.": "Хавае мадэлі за непразрыстымі блокамі. Абнаўленне суб'ектаў. Выключыце спецыяльныя пакеты.",
|
||||
"horizon.extras.repro_keys": "Ярлыкі для запісу",
|
||||
"horizon.extras.repro_keys.tooltip": "F3+R запускае або спыняе прайгравальны запіс, а F3+E запісвае пакет. Выключана па змаўчанні: запіс капіруе свет.",
|
||||
"horizon.agent.record_session": "Пачаць сеанс запісу",
|
||||
"horizon.agent.record_stop": "Спыніць сеанс запісу",
|
||||
"horizon.agent.export_repro": "Экспарт Repro Bundle",
|
||||
"horizon.repro.started": "Запіс пачаўся; F3+R спыняе яго і запісвае пакет.",
|
||||
"horizon.repro.stopped": "Запіс спынены: %s",
|
||||
"horizon.repro.bundle": "Пакет рэпрадукцый: %s",
|
||||
"horizon.repro.error": "Паўтор: %s",
|
||||
"horizon.repro.no_recording": "Пакуль няма запісу для экспарту.",
|
||||
"horizon.repro.replay_started": "Паўтор пачаўся: %s",
|
||||
"horizon.pack.addInfo": "Дадайце пакет рэсурсаў з Modrinth",
|
||||
"horizon.pack.addWorking": "Запыт да Modrinth...",
|
||||
"horizon.pack.addNone": "Пакеты рэсурсаў не знойдзены",
|
||||
"horizon.pack.addTitle": "Дадайце пакет рэсурсаў",
|
||||
"horizon.pack.install": "Усталяваць",
|
||||
"horizon.pack.installing": "Ўстаноўка",
|
||||
"horizon.pack.installed": "Усталяваны",
|
||||
"horizon.pack.search": "Пошук",
|
||||
"horizon.pack.searchHint": "Пошук у Modrinth...",
|
||||
"horizon.pack.filters": "Фільтры",
|
||||
"horizon.pack.filter.realistic": "Рэалістычны",
|
||||
"horizon.pack.filter.simplistic": "Спрошчаны",
|
||||
"horizon.pack.filter.vanillaLike": "Ванільны",
|
||||
"horizon.extras.header.cosmetics": "Касметыка",
|
||||
"horizon.extras.cosmetics_mine": "Мая касметыка",
|
||||
"horizon.extras.cosmetics_mine.tooltip": "Схаваны: кожны гулец Horizon бачыць вас без вашай касметыкі, у тым ліку і вас. The same switch as the launcher's Wardrobe.",
|
||||
"horizon.extras.cosmetics_others": "Касметыка іншых гульцоў",
|
||||
"horizon.extras.cosmetics_others.tooltip": "Што носяць іншыя гульцы Horizon: усё, толькі стылі тэгаў з імёнамі або нічога. Hidden, the game never asks what they wear.",
|
||||
"horizon.extras.cosmetics_auras": "Чужыя аўры і шлейфы",
|
||||
"horizon.extras.cosmetics_auras.tooltip": "Auras and trails other players wear. Выключана па змаўчанні.",
|
||||
"horizon.extras.cosmetics.shown": "Паказана",
|
||||
"horizon.extras.cosmetics.hidden": "Схаваны",
|
||||
"horizon.extras.cosmetics.name_tags": "Толькі біркі з імёнамі"
|
||||
}
|
||||
@@ -1,481 +0,0 @@
|
||||
{
|
||||
"horizon.options.controller": "Контролер",
|
||||
"horizon.options.vibration": "Вибрация",
|
||||
"horizon.options.adaptive_triggers": "Адаптивни тригери",
|
||||
"horizon.options.lightbar": "Светлинна лента",
|
||||
"horizon.options.look_sensitivity": "Чувствителност на погледа",
|
||||
"horizon.options.invert_x": "Обръщане на оста X на погледа",
|
||||
"horizon.options.invert_y": "Обръщане на оста Y на погледа",
|
||||
"horizon.options.sneak_button": "Бутон за промъкване",
|
||||
"horizon.options.move_deadzone": "Мъртва зона на движението",
|
||||
"horizon.options.look_deadzone": "Мъртва зона на погледа",
|
||||
"horizon.options.button_glyphs": "Глифове на бутони",
|
||||
"horizon.options.menu_navigation": "Навигация в менюто",
|
||||
"horizon.options.on_screen_keyboard": "Екранна клавиатура",
|
||||
"horizon.options.chat_delay": "Забавяне на чата",
|
||||
"horizon.options.lightbar.health": "здраве",
|
||||
"horizon.options.lightbar.dimension": "Измерение",
|
||||
"horizon.options.glyphs.auto": "Авто",
|
||||
"horizon.options.menu_focus": "Фокус",
|
||||
"horizon.options.menu_pointer": "показалец",
|
||||
"horizon.options.keyboard.auto": "Авто",
|
||||
"horizon.options.keyboard.steam": "Пара",
|
||||
"horizon.options.unavailable": "Тази функция все още не е налична в Horizon.",
|
||||
"horizon.options.search": "Настройки за търсене...",
|
||||
"horizon.pages.mods": "Модификации",
|
||||
"horizon.pages.shader_packs": "Шейдърни пакети",
|
||||
"horizon.pages.shader_options": "Опции на шейдъра",
|
||||
"horizon.pages.controller": "Настройки на контролера",
|
||||
"horizon.pages.controller_binds": "Контролерът се свързва",
|
||||
"horizon.pages.agent": "AI агент",
|
||||
"horizon.split.leave": "тръгвай",
|
||||
"horizon.pages.split_settings": "Настройки за разделен екран",
|
||||
"horizon.pages.split_profiles": "Кой играе?",
|
||||
"horizon.pages.split_setup": "Настройка на разделен екран",
|
||||
"horizon.pages.music": "Музика",
|
||||
"horizon.pages.music.menu": "Меню",
|
||||
"horizon.pages.music.overworld": "Надземният свят",
|
||||
"horizon.pages.music.creative": "Творчески",
|
||||
"horizon.pages.music.underwater": "Под вода",
|
||||
"horizon.pages.music.end": "Краят",
|
||||
"horizon.pages.music.boss": "Шефе",
|
||||
"horizon.pages.music.credits": "Кредити",
|
||||
"horizon.pages.music.overworld_biomes": "Биоми от надземния свят",
|
||||
"horizon.pages.music.nether_biomes": "Долни биоми",
|
||||
"horizon.pages.music.end_biomes": "Крайни биоми",
|
||||
"horizon.pages.music.other": "Друга музика",
|
||||
"horizon.pages.music.empty": "Няма музикални записи: звуците на играта не се зареждат.",
|
||||
"horizon.pages.music.tooltip": "Вградените песни според ситуацията: пуснете една сега или изключете някоя, така че играта да спре да я избира.",
|
||||
"horizon.pages.settings.tooltip": "Собствени настройки на Horizon: графики, контролери, екстри и модове.",
|
||||
"horizon.pages.graphics.tooltip": "Графика просто: всяка настройка казва колко струва.",
|
||||
"horizon.pages.graphics_advanced.tooltip": "Всяка графична настройка, включително Horizon, с цената на всяка.",
|
||||
"horizon.pages.controller.tooltip": "Контролери: техните бутони, стикове, вибрация и навигация в менюто.",
|
||||
"horizon.pages.extras.tooltip": "Незадължителни добавки, всички изключени по подразбиране: играта остава ванилия, докато не включите някоя.",
|
||||
"horizon.pages.agent.tooltip": "Нека вашият собствен AI агент играе: horizon-ctl и MCP клиенти на този компютър.",
|
||||
"horizon.pages.split_profiles.tooltip": "Кой играе: профил, съхраняван в тази папка на играта.",
|
||||
"horizon.pages.split_setup.tooltip": "Прозорците и мониторите и кой къде играе.",
|
||||
"horizon.pages.shader_packs.tooltip": "Шейдърни пакети от папката shaderpacks: собствените на Horizon и тези във формата shaders/. Един тича наведнъж.",
|
||||
"horizon.pages.mods.tooltip": "Модификации, направени за Horizon, от папката horizon-mods. Всеки работи в пясъчна кутия и остава изключен, докато не го включите.",
|
||||
"horizon.pages.open.graphics": "Графика...",
|
||||
"horizon.pages.open.graphics_advanced": "Разширена графика...",
|
||||
"horizon.pages.open.shader_packs": "Шейдър пакети...",
|
||||
"horizon.pages.open.controller": "Настройки на контролера...",
|
||||
"horizon.pages.open.split_setup": "Разделен екран...",
|
||||
"horizon.pages.open.extras": "Екстри...",
|
||||
"horizon.pages.open.mods": "Модове...",
|
||||
"horizon.pages.open.agent": "AI агент...",
|
||||
"horizon.pages.open.controller_binds": "Контролерът се свързва...",
|
||||
"horizon.pages.open.horizon_settings": "Настройки на Horizon...",
|
||||
"horizon.pages.open.music": "музика...",
|
||||
"horizon.mods.empty": "Без модификации: поставете .hmod файлове в папката horizon-mods",
|
||||
"horizon.mods.stopped": " (спряно)",
|
||||
"horizon.mods.not_built": " (Не е построен)",
|
||||
"horizon.mods.settings": "Настройки",
|
||||
"horizon.mods.in_world": " (оставете света да се промени)",
|
||||
"horizon.mods.open_folder": "Отворете папката на Mod",
|
||||
"horizon.shader_packs.empty": "Без шейдър пакети: поставете ги в папката shaderpacks",
|
||||
"horizon.shader_options.open": "Опции за шейдър...",
|
||||
"horizon.shader_options.profile": "Профил",
|
||||
"horizon.shader_options.loading": "Четене на опциите на шейдър пакета...",
|
||||
"horizon.agent.copy_details": "Копиране на подробности за връзката",
|
||||
"horizon.multiplayer.join": "Присъединете се",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Отказ Повторно свързване",
|
||||
"horizon.multiplayer.join_friend": "Присъединете се към приятел",
|
||||
"horizon.multiplayer.invite_code": "Код за покана",
|
||||
"horizon.multiplayer.relay": "Реле (по избор)",
|
||||
"Preset": "Предварителна настройка",
|
||||
"LOD Distance": "LOD разстояние",
|
||||
"Far Detail": "Далечен детайл",
|
||||
"Cave Culling": "Пещерно избиване",
|
||||
"Entity Culling": "Отстраняване на субекти",
|
||||
"Chunk Updates": "Актуализации на парчета",
|
||||
"Upload Budget": "Бюджет за качване",
|
||||
"Sky": "небе",
|
||||
"Fog": "мъгла",
|
||||
"Weather": "Времето",
|
||||
"Animated Textures": "Анимирани текстури",
|
||||
"Present Mode": "Настоящ режим",
|
||||
"Frame Pacing": "Темпо на рамката",
|
||||
"Worker Threads": "Работни нишки",
|
||||
"Resolution Scale": "Скала на резолюцията",
|
||||
"Upscaling": "Премащабиране",
|
||||
"Paper White": "Бяла хартия",
|
||||
"Peak Brightness": "Пикова яркост",
|
||||
"Black Level": "Черно ниво",
|
||||
"UI Brightness": "Яркост на потребителския интерфейс",
|
||||
"Battery Mode": "Режим на батерия",
|
||||
"Graphics API": "Графичен API",
|
||||
"Translucent Batching": "Полупрозрачно пакетиране",
|
||||
"Performance Overlay": "Наслагване на ефективността",
|
||||
"Full Detail Distance": "Пълно подробно разстояние",
|
||||
"On": "включено",
|
||||
"Off": "Изкл",
|
||||
"Unlimited": "Неограничен",
|
||||
"Auto": "Авто",
|
||||
"Custom": "По поръчка",
|
||||
"Performance": "Изпълнение",
|
||||
"Balanced": "Балансиран",
|
||||
"Quality": "качество",
|
||||
"Ultra": "Ултра",
|
||||
"Ultra Performance": "Ултра производителност",
|
||||
"Native": "Роден",
|
||||
"Full": "Пълна",
|
||||
"Reduced": "Намалена",
|
||||
"Efficient": "Ефективно",
|
||||
"Precise": "Прецизен",
|
||||
"Auto (VSync)": "Автоматично (VSync)",
|
||||
"Mailbox": "Пощенска кутия",
|
||||
"Immediate": "Незабавно",
|
||||
"FPS and Graph": "FPS и графика",
|
||||
"No cost": "Без разходи",
|
||||
"Low cost": "Ниска цена",
|
||||
"Medium cost": "Средна цена",
|
||||
"High cost": "Висока цена",
|
||||
"Saves time": "Спестява време",
|
||||
"Graphics": "Графика",
|
||||
"Done": "Готово",
|
||||
"This feature is not yet available in Horizon.": "Тази функция все още не е налична в Horizon.",
|
||||
"Your display or system doesn't offer HDR output.": "Вашият дисплей или система не предлага HDR изход.",
|
||||
"Turn HDR on to use it.": "Включете HDR, за да го използвате.",
|
||||
"Your GPU or graphics API doesn't support it.": "Вашият GPU или графичен API не го поддържат.",
|
||||
"%s chunks": "%s парчета",
|
||||
"%s per frame": "%s на кадър",
|
||||
"%s MiB/frame": "%s MiB/кадър",
|
||||
"%s KiB/frame": "%s KiB/кадър",
|
||||
"Auto (%s)": "Авто (%s)",
|
||||
"Full-detail terrain range. LOD Distance can extend local worlds farther.": "Пълен детайлен обхват на терена. LOD Разстоянието може да разшири локалните светове по-далеч.",
|
||||
"Coarse distant terrain in local worlds. Integrated GPUs stop at 128 chunks.": "Груб далечен терен в местните светове. Интегрираните графични процесори спират на 128 парчета.",
|
||||
"Reduced draws far terrain coarser: faster on this integrated GPU.": "Намалено чертане на по-груб терен: по-бързо с този интегриран GPU.",
|
||||
"Reduced draws far terrain coarser, for GPUs it slows down.": "Намалено рисува далеч по-груб терен, за GPU се забавя.",
|
||||
"Field of view. Wider shows more terrain at once.": "Зрително поле. По-широко показва повече терен наведнъж.",
|
||||
"Caps frames per second; a cap saves power and heat.": "Максимални кадри в секунда; капачката спестява енергия и топлина.",
|
||||
"Waits for the display's refresh: no tearing, a little latency.": "Изчаква опресняване на дисплея: без разкъсване, малко забавяне.",
|
||||
"Size of menus and the HUD.": "Размер на менютата и HUD.",
|
||||
"Sets the options below together. Changing one makes it Custom.": "Задава заедно опциите по-долу. Промяната на един го прави персонализиран.",
|
||||
"Lava, fire and sunlight shine brighter than white. Needs an HDR display.": "Лавата, огънят и слънчевата светлина блестят по-ярко от бялото. Има нужда от HDR дисплей.",
|
||||
"Your display's darkest black. 0 for OLED; LCDs glow a little.": "Вашият дисплей е най-тъмно черен. 0 за OLED; LCD дисплеите светят малко.",
|
||||
"How bright highlights get. Auto: the peak your system reports.": "Колко ярки стават акцентите. Автоматично: пикът, който вашата система отчита.",
|
||||
"How bright white paper and menus look. Auto: your desktop's white.": "Колко ярки изглеждат бялата хартия и менютата. Автоматично: вашият работен плот е бял.",
|
||||
"How bright the HUD and menus are, against paper white.": "Колко ярки са HUD и менютата на бяла хартия.",
|
||||
"How far mobs, crops and machines keep working. CPU time.": "Докъде продължават да работят тълпите, културите и машините. процесорно време.",
|
||||
"How far away mobs and items are drawn, as a share of normal.": "Колко далеч са изтеглени тълпите и предметите, като дял от нормалното.",
|
||||
"Skips terrain hidden behind closed sections. Off costs a lot.": "Пропуска терен, скрит зад затворени участъци. Off струва много.",
|
||||
"Hides entities behind opaque blocks.": "Скрива обекти зад непрозрачни блокове.",
|
||||
"No entity occlusion culling.": "Без отстраняване на оклузия на обект.",
|
||||
"CPU occlusion test runs this frame.": "Тестът за запушване на процесора изпълнява този кадър.",
|
||||
"Occlusion checks run on a worker thread.": "Проверките за затваряне се изпълняват на работна нишка.",
|
||||
"Meshed sections uploaded per frame. Lower is smoother while loading.": "Мрежести секции, качени на кадър. Долната е по-плавна при зареждане.",
|
||||
"Soft shadows in corners. Costs chunk building time only.": "Меки сенки в ъглите. Разходи само време за изграждане на парче.",
|
||||
"Smaller copies of block textures for distant terrain: less shimmer.": "По-малки копия на блокови текстури за отдалечени терени: по-малко блясък.",
|
||||
"Blends grass and water colours across biomes when building chunks.": "Смесва тревни и водни цветове в биоми при изграждане на парчета.",
|
||||
"Cloud layer: off, flat (fast) or 3D (fancy).": "Облачен слой: изключен, плосък (бърз) или 3D (фантастичен).",
|
||||
"How far clouds reach, in chunks.": "Докъде достигат облаците, на парчета.",
|
||||
"Draws the sky, sun, moon and stars.": "Рисува небето, слънцето, луната и звездите.",
|
||||
"Distance fog. Off shows the edge of the loaded world.": "Дистанционна мъгла. Изключено показва ръба на заредения свят.",
|
||||
"Draws rain and snow.": "Рисува дъжд и сняг.",
|
||||
"How far rain and snow fall around you.": "Докъде валят дъжд и сняг около вас.",
|
||||
"How many particles effects spawn.": "Колко ефекти на частици хвърлят хайвера си.",
|
||||
"Round shadows under mobs and items.": "Кръгли сенки под тълпи и предмети.",
|
||||
"Leaves with see-through gaps. Off draws them solid.": "Листа с прозрачни празнини. Off ги рисува солидно.",
|
||||
"Sorts water and glass more exactly behind each other.": "Сортира водата и стъклото по-точно едно зад друго.",
|
||||
"Which chunk rebuilds wait for the frame: none, nearby or yours.": "Коя част се възстановява, чакайки рамката: нито една, наблизо или вашата.",
|
||||
"Animates water, lava, fire and other moving textures.": "Анимира вода, лава, огън и други движещи се текстури.",
|
||||
"World pixels per screen pixel. Below 100% is faster on the GPU.": "Световни пиксели на пиксел на екрана. Под 100% е по-бързо на GPU.",
|
||||
"Smooths jagged edges. FXAA: softer. TAA: smoothest, moving things may trail.": "Изглажда назъбените ръбове. FXAA: по-мек. TAA: най-плавните, движещи се неща могат да се проточат.",
|
||||
"Renders the world smaller and rebuilds it with TAA. Faster, softer.": "Прави света по-малък и го възстановява с TAA. По-бързо, по-меко.",
|
||||
"HDR output on an HDR display: lava, fire and sunlight shine brighter.": "HDR изход на HDR дисплей: лава, огън и слънчева светлина блестят по-ярко.",
|
||||
"How frames reach the screen while VSync is off.": "Как кадрите достигат до екрана, докато VSync е изключен.",
|
||||
"Efficient sleeps until the next frame; Precise spins, like vanilla.": "Ефективно заспиване до следващия кадър; Прецизни завъртания, като ванилия.",
|
||||
"Fills the screen (borderless).": "Запълва екрана (без граници).",
|
||||
"Brightens dark places.": "Осветява тъмните места.",
|
||||
"Lowers the frame rate when minimised, or idle too.": "Намалява честотата на кадрите, когато е минимизирана или неактивна.",
|
||||
"Sways the view while walking.": "Люлее гледката по време на ходене.",
|
||||
"Fades newly loaded chunks in.": "Избледнява новозаредените парчета.",
|
||||
"Darkens the screen's edges.": "Затъмнява ръбовете на екрана.",
|
||||
"Shows when the world saves.": "Показва кога светът спасява.",
|
||||
"Shows attack cooldown.": "Показва изчакване на атаката.",
|
||||
"60 FPS cap (30 in menus or unfocused), half the workers: less power.": "60 FPS капачка (30 в менюта или нефокусирани), половината работници: по-малко мощност.",
|
||||
"Threads that generate, light and mesh chunks. Next start.": "Нишки, които генерират, леки и мрежести парчета. Следващ старт.",
|
||||
"Vulkan, then DirectX 12 (Windows) or OpenGL (Linux) where it fails. Next start.": "Vulkan, след това DirectX 12 (Windows) или OpenGL (Linux), където не успее. Следващ старт.",
|
||||
"Water and glass in one buffer: fewer draw calls, a bit more VRAM. Next start.": "Вода и стъкло в един буфер: по-малко извиквания за теглене, малко повече VRAM. Следващ старт.",
|
||||
"Mesh data sent to the GPU per frame. Lower is smoother while loading.": "Мрежови данни, изпратени до GPU на кадър. Долната е по-плавна при зареждане.",
|
||||
"Frame rate and frame times over the game, with a graph.": "Скорост на кадрите и времена на кадрите в играта, с графика.",
|
||||
"horizon.multiplayer.finding_friend": "Намиране на света на вашия приятел...",
|
||||
"horizon.multiplayer.not_signed_in": "Този сървър е в онлайн режим. Влезте с вашия акаунт в Microsoft в стартовия панел на Horizon, за да се присъедините към него.",
|
||||
"horizon.multiplayer.invalid_address": "Невалиден адрес на сървъра: %s",
|
||||
"horizon.multiplayer.connected_friend_direct": "Свързан директно с вашия приятел...",
|
||||
"horizon.multiplayer.connected_friend_relay": "Свързан с вашия приятел чрез релето...",
|
||||
"General": "генерал",
|
||||
"Advanced": "Разширено",
|
||||
"Undo": "Отмяна",
|
||||
"Search options": "Опции за търсене",
|
||||
"Shortcut": "Пряк път",
|
||||
"Distance": "Разстояние",
|
||||
"Display": "Дисплей",
|
||||
"Interface": "Интерфейс",
|
||||
"Sky and weather": "Небе и време",
|
||||
"Lighting and entities": "Осветление и обекти",
|
||||
"Textures": "Текстури",
|
||||
"Image": "Изображение",
|
||||
"Chunk loading": "Зареждане на парчета",
|
||||
"Culling": "Бране",
|
||||
"Power": "Мощност",
|
||||
"Frames": "Рамки",
|
||||
"Overlay": "Наслагване",
|
||||
"Output": "Изход",
|
||||
"Brightness": "Яркост",
|
||||
"How far you see, the window and the HUD.": "Колко далеч виждате, прозореца и HUD.",
|
||||
"The sky and weather, lighting, textures and the image.": "Небето и времето, осветлението, текстурите и изображението.",
|
||||
"Chunk loading, culling and power use.": "Зареждане на парчета, отстраняване и използване на енергия.",
|
||||
"How frames reach the screen, and the performance overlay.": "Как рамките достигат до екрана и наслагването на ефективността.",
|
||||
"Brighter-than-white output on an HDR display.": "По-ярък от бялото изход на HDR дисплей.",
|
||||
"Results for \"%s\"": "Резултати за \"%s\"",
|
||||
"No settings match.": "Няма съответстващи настройки.",
|
||||
"horizon.title.not_affiliated": "Не е свързан с Mojang или Microsoft",
|
||||
"horizon.extras.header.lighting": "Осветление",
|
||||
"horizon.extras.header.third_person": "Трето лице",
|
||||
"horizon.extras.header.particles": "частици",
|
||||
"horizon.extras.header.inventory": "Инвентаризация",
|
||||
"horizon.extras.header.chat": "Чат",
|
||||
"horizon.extras.header.multiplayer": "Мултиплейър",
|
||||
"horizon.extras.header.sound": "Звук",
|
||||
"horizon.extras.header.menus": "Менюта",
|
||||
"horizon.extras.fullbright": "Фулбрайт",
|
||||
"horizon.extras.dynamic_lights": "Динамични светлини",
|
||||
"horizon.extras.own_name_tag": "Собствен етикет с име",
|
||||
"horizon.extras.hide_block_particles": "Скриване на частиците за разбиване на блокове",
|
||||
"horizon.extras.auto_reconnect": "Автоматично повторно свързване",
|
||||
"horizon.extras.ui_sound_reserve": "Запазете канали за звуци от потребителския интерфейс",
|
||||
"horizon.extras.music_page": "Музикална страница",
|
||||
"horizon.extras.mouse_tweaks": "Mouse Tweaks контроли",
|
||||
"horizon.extras.mouse_tweaks_wheel": "Колелото за превъртане премества елементи",
|
||||
"horizon.extras.sort": "Бутони за сортиране",
|
||||
"horizon.extras.sort_key": "Ключ за сортиране",
|
||||
"horizon.extras.no_ai": "Без AI",
|
||||
"horizon.extras.agent_control": "Агентски контрол",
|
||||
"horizon.extras.discord_presence": "Богато присъствие на Discord",
|
||||
"horizon.extras.splitscreen": "Разделен екран",
|
||||
"horizon.extras.chat_animation": "Плавен чат",
|
||||
"horizon.extras.chat_indicator": "Индикатор за съобщение",
|
||||
"horizon.extras.wheel_cycles_settings": "Колелото за превъртане променя настройките",
|
||||
"horizon.extras.pack_browser": "Браузър за пакети с ресурси",
|
||||
"horizon.extras.fullbright.tooltip": "Показвайте блокове и обекти при пълна светлина, дори в тъмни зони.",
|
||||
"horizon.extras.dynamic_lights.tooltip": "Факли и други източници на светлина, държани, изпуснати или запалени, осветяват блоковете около тях.",
|
||||
"horizon.extras.own_name_tag.tooltip": "Показва името ви над главата ви в трето лице, както го виждат другите играчи. Скрит, когато сървърът скрива имената на вашия отбор или сте невидими.",
|
||||
"horizon.extras.hide_block_particles.tooltip": "Скрива частиците отломки, които избухват от счупен блок и се разпадат от този, който копаете. Други частици остават.",
|
||||
"horizon.extras.auto_reconnect.tooltip": "Присъединява се отново към сървър след загубена връзка или рестартиране, никога след ритване или бан.",
|
||||
"horizon.extras.ui_sound_reserve.tooltip": "Звуците в света оставят 16 от 247 звукови канала свободни, така че потоп от тях (голяма експлозия) не може да заглуши щракванията в менюто.",
|
||||
"horizon.extras.music_page.tooltip": "Music & Sounds получава страница Music...: пуснете която и да е вградена песен сега или изключете някоя, така че играта да спре да я избира. Нищо не се променя, докато не изключите запис.",
|
||||
"horizon.extras.mouse_tweaks.tooltip": "Плъзгане с десен бутон поставя по един на слот, плъзгане с ляво събира задържания елемент, Shift + плъзгане наляво премества всеки слот.",
|
||||
"horizon.extras.mouse_tweaks_wheel.tooltip": "Надолу изпраща един елемент от слота от другата страна, нагоре връща един обратно.",
|
||||
"horizon.extras.sort.tooltip": "Сортира инвентара, сандъците, бъчвите, сандъците за ендер и кутиите за шулкери в творчески ред на раздели, сливайки стекове.",
|
||||
"horizon.extras.sort_key.tooltip": "Сортира частта под курсора. Щракнете, след това натиснете клавиш; Escape го развързва.",
|
||||
"horizon.extras.no_ai.tooltip": "Скрива и изключва всяка функция на AI: контрол на агента, horizon-ctl и MCP сървърите. Превключвателят на стартера също.",
|
||||
"horizon.extras.agent_control.tooltip": "Вашият собствен AI агент може да играе тази игра чрез horizon-ctl или MCP клиент само на този компютър.",
|
||||
"horizon.extras.discord_presence.tooltip": "Показва Playing Horizon във вашия Discord профил, докато Discord работи: менюта, един играч или мултиплейър, измерението и времето на игра. Никога име на света или адрес на сървър.",
|
||||
"Calibrate...": "Калибриране...",
|
||||
"Calibrate Anyway...": "Калибриране все пак...",
|
||||
"Use Automatic Setup": "Използвайте автоматична настройка",
|
||||
"Shortcut: %s": "Пряк път: %s",
|
||||
"Next": "Следваща",
|
||||
"HDR Calibration %s/%s: %s": "HDR калибриране %s/%s: %s",
|
||||
"Sets the three below full screen, one test picture at a time.": "Задава трите под цял екран, една тестова картина наведнъж.",
|
||||
"A key that opens this screen from the game. Not bound at first.": "Ключ, който отваря този екран от играта. В началото не е обвързан.",
|
||||
"Your display didn't say how bright it gets: calibrate to set it up.": "Вашият дисплей не каза колко ярък става: калибрирайте, за да го настроите.",
|
||||
"Already set up for your display. Only calibrate if HDR looks wrong.": "Вече е настроен за вашия дисплей. Калибрирайте само ако HDR изглежда грешно.",
|
||||
"Puts Peak Brightness and Paper White back to your display's setup.": "Връща Peak Brightness и Paper White обратно към настройката на вашия дисплей.",
|
||||
"Set up for your display. Leave on Auto unless it looks wrong.": "Настройте за вашия дисплей. Оставете на Auto, освен ако не изглежда грешно.",
|
||||
"Lower it until the logo just disappears into the black.": "Спуснете го, докато логото изчезне в черното.",
|
||||
"Raise it until the logo just disappears into the bright square.": "Повдигнете го, докато логото изчезне в яркия квадрат.",
|
||||
"Make the card a white that's comfortable to look at for a long time, in the light you play in.": "Направете картата бяла, удобна за гледане дълго време, на светлината, на която играете.",
|
||||
"horizon.splitscreen.profiles.forget": "Забравете",
|
||||
"horizon.splitscreen.profiles.create": "Създавайте",
|
||||
"horizon.splitscreen.profiles.you": " - ти",
|
||||
"horizon.splitscreen.profiles.forget_named": "Забравете %s",
|
||||
"horizon.splitscreen.profiles.previous": "Предишен",
|
||||
"horizon.splitscreen.profiles.next": "Следваща",
|
||||
"horizon.splitscreen.profiles.new_button": "Нов профил...",
|
||||
"horizon.splitscreen.profiles.forget_title": "Забравете профила?",
|
||||
"horizon.splitscreen.profiles.forget_leaves": "Напуска списъка на този екземпляр.",
|
||||
"horizon.splitscreen.profiles.forget_kept": "Световният напредък и настройки се запазват.",
|
||||
"horizon.splitscreen.profiles.new_title": "Нов профил",
|
||||
"horizon.splitscreen.profiles.name_hint": "Име (3 до 16 букви, цифри, _)",
|
||||
"horizon.splitscreen.profiles.switch_title": "Превключване на профил",
|
||||
"horizon.splitscreen.profiles.offline_note": "Офлайн профилите принадлежат към този екземпляр",
|
||||
"horizon.splitscreen.profiles.move": "Движете се там, където играете",
|
||||
"horizon.splitscreen.profiles.move_place": "Преместете се там, където играете: %s",
|
||||
"horizon.friends.title": "Играйте с приятели",
|
||||
"horizon.friends.hint": "Приятели поставете кода след бутона Приятели на заглавния екран, в Direct Connection или на страницата за възпроизвеждане на стартовия панел.",
|
||||
"horizon.friends.copied": "Копирано",
|
||||
"horizon.friends.copy_code": "Копиране на код",
|
||||
"horizon.friends.stop_sharing": "Спиране на споделянето",
|
||||
"horizon.friends.none_yet": "Тук все още няма приятели.",
|
||||
"horizon.friends.here_now": "Тук сега: %s",
|
||||
"horizon.friends.share_intro": "Споделете този свят зад код за покана: той се отваря към LAN и приятели в интернет се присъединяват с кода. Не е включен наш сървър.",
|
||||
"horizon.friends.start_sharing": "Започнете споделяне",
|
||||
"horizon.friends.troubleshooting": "Отстраняване на неизправности...",
|
||||
"horizon.friends.stopped": "Спря споделянето на кода за покана.",
|
||||
"horizon.friends.short_code_changed": "Някой продължаваше да опитва грешен кратък код, така че той се промени.",
|
||||
"horizon.friends.wrong_code": "Някой е опитал грешен код.",
|
||||
"horizon.friends.code_ready": "Вашият код за покана е готов: Меню за игри, Играйте с приятели. Копирайте го и го изпратете на приятелите си.",
|
||||
"horizon.friends.share_failed": "Не можах да споделя света: %s",
|
||||
"horizon.friends.finding_route": "Търся как приятелите могат да се свържат с вас...",
|
||||
"horizon.osk.space": "пространство",
|
||||
"horizon.osk.delete": "Изтриване",
|
||||
"horizon.osk.type": "Тип",
|
||||
"horizon.osk.symbols": "Символи",
|
||||
"horizon.osk.close": "затвори",
|
||||
"horizon.extras.chat_animation.tooltip": "Нова линия за чат се плъзга отляво, докато избледнява, вместо да се показва цяла.",
|
||||
"horizon.extras.chat_indicator.tooltip": "Цветната лента отляво на линия за чат.",
|
||||
"horizon.extras.wheel_cycles_settings.tooltip": "Колелцето за превъртане над бутон в настройките променя стойността му, както при vanilla. Изключено, колелото само превърта списъка.",
|
||||
"horizon.extras.pack_browser.tooltip": "Select Resource Packs получава бутон за добавяне, който търси в Modrinth и инсталира пакет във вашата папка с пакети. Off, играта никога не се свързва с Modrinth.",
|
||||
"horizon.extras.splitscreen.tooltip": "В един свят друг контролер може да се присъедини като нов играч: натиснете A, когато поиска, след това изберете профил и къде да играете.",
|
||||
"horizon.splitscreen.setup.monitor": "Монитор %s",
|
||||
"horizon.splitscreen.setup.monitor_unknown": "монитор",
|
||||
"horizon.splitscreen.setup.window": "Прозорец %s",
|
||||
"horizon.splitscreen.setup.new_window": "Нов прозорец",
|
||||
"horizon.splitscreen.setup.fill_monitor": "Попълнете %s",
|
||||
"horizon.splitscreen.setup.done": "Готово",
|
||||
"horizon.splitscreen.setup.on": "включено",
|
||||
"horizon.splitscreen.setup.off": "Изкл",
|
||||
"horizon.splitscreen.setup.player": "Играч %s",
|
||||
"horizon.splitscreen.setup.revert": "Връщане",
|
||||
"horizon.splitscreen.setup.apply": "Кандидатствайте",
|
||||
"horizon.splitscreen.setup.warning.empty_main": "Никой не играе в Window 1: той се скрива",
|
||||
"horizon.splitscreen.setup.warning.empty_close": "Никой не играе в Window %s: затваря се при Apply",
|
||||
"horizon.splitscreen.setup.warning.empty_new": "Никой не играе в Window %s: няма да се отвори",
|
||||
"horizon.splitscreen.setup.warning.on_main": "Window %s изпълва монитора на Window 1: неговите играчи играят в Window 1",
|
||||
"horizon.splitscreen.setup.warning.unplugged": "Мониторът на Window %s не е свързан: неговите играчи играят в Window 1",
|
||||
"horizon.splitscreen.setup.status.off": "Разделеният екран е изключен.",
|
||||
"horizon.splitscreen.setup.status.enable_hint": "Включете го, след това натиснете бутон на друг контролер в свят.",
|
||||
"horizon.splitscreen.setup.status.join_hint": "Натиснете A, за да се присъедините и изберете профил.",
|
||||
"horizon.splitscreen.setup.status.idle": "Разделен екран неактивен",
|
||||
"horizon.splitscreen.setup.status.alone": "Няма други местни играчи.",
|
||||
"horizon.splitscreen.setup.status.solo_controls": "Самостоятелните контроли и обратната връзка на контролера са непроменени.",
|
||||
"horizon.splitscreen.setup.window_empty": "Прозорец %s: празен",
|
||||
"horizon.splitscreen.setup.note.select": "Щракнете върху плейър или го плъзнете към прозорец или монитор.",
|
||||
"horizon.splitscreen.setup.note.apply": "Приложете, за да играете по този начин, Върнете, за да отмените.",
|
||||
"horizon.splitscreen.setup.note.place": "Къде трябва да играе %s?",
|
||||
"horizon.splitscreen.setup.this_player": "този играч",
|
||||
"horizon.splitscreen.setup.switch_disabled": "Splitscreen се изключва, след като другите напуснат (тяхното меню за пауза).",
|
||||
"horizon.splitscreen.setup.window_place": "Прозорец %s (%s от %s)",
|
||||
"horizon.splitscreen.setup.names": "имена",
|
||||
"horizon.splitscreen.setup.identify": "Идентифицирайте",
|
||||
"horizon.splitscreen.setup.layout.two": "2 споделяне: %s",
|
||||
"horizon.splitscreen.setup.layout.three": "3 споделяне: %s",
|
||||
"horizon.splitscreen.setup.layout.two.0": "Авто",
|
||||
"horizon.splitscreen.setup.layout.two.1": "Един до друг",
|
||||
"horizon.splitscreen.setup.layout.two.2": "Отгоре и отдолу",
|
||||
"horizon.splitscreen.setup.layout.three.0": "Един широк отгоре",
|
||||
"horizon.splitscreen.setup.layout.three.1": "Един висок отляво",
|
||||
"horizon.splitscreen.setup.layout.three.2": "Един до друг",
|
||||
"horizon.splitscreen.setup.help.player": "Кликнете или плъзнете %s, за да ги преместите.",
|
||||
"horizon.splitscreen.setup.help.layout_auto": "%s (един до друг, когато прозорецът е широк). Натиснете за промяна.",
|
||||
"horizon.splitscreen.setup.help.layout": "%s. Натиснете за промяна.",
|
||||
"horizon.splitscreen.setup.help.switch": "Разделен екран за този компютър, включен или изключен.",
|
||||
"horizon.splitscreen.setup.help.identify": "Изгледът на всеки играч показва в кой прозорец се намира.",
|
||||
"horizon.splitscreen.setup.help.names": "Името на всеки играч в ъгъла на неговия изглед.",
|
||||
"horizon.splitscreen.setup.help.revert": "Отменете промените, които все още не са приложени.",
|
||||
"horizon.splitscreen.setup.help.apply": "Играйте по начина, по който картата показва.",
|
||||
"horizon.splitscreen.setup.help.unchanged": "Все още нищо не се е променило.",
|
||||
"horizon.splitscreen.setup.give_up_pad": "Подложка за отказване",
|
||||
"horizon.splitscreen.prompt.controller_connected": "Контролерът е свързан",
|
||||
"horizon.splitscreen.prompt.controller_disconnected": "Контролерът е изключен",
|
||||
"horizon.splitscreen.prompt.controller_disconnected.hint": "Свържете го отново или натиснете бутон на друг",
|
||||
"horizon.splitscreen.prompt.join.one": "%s иска да се присъедини",
|
||||
"horizon.splitscreen.prompt.join.many": "%s контролери искат да се присъединят",
|
||||
"horizon.splitscreen.prompt.join.yes": "Присъединете се",
|
||||
"horizon.splitscreen.prompt.join.as_p1": "Играйте като P1",
|
||||
"horizon.splitscreen.prompt.join.no": "не",
|
||||
"horizon.splitscreen.prompt.lost": "%s: контролерът е изключен",
|
||||
"horizon.splitscreen.prompt.lost.use_another": "Използвайте друг",
|
||||
"horizon.splitscreen.prompt.lost.leave": "тръгвай",
|
||||
"horizon.splitscreen.setup.help.give_up_pad": "Вашият контролер спира да ви кара и получава предложение да се присъедини (A).",
|
||||
"horizon.splitscreen.setup.note.held_at": "%s до %s: A го пуска тук, B го връща обратно.",
|
||||
"horizon.splitscreen.setup.note.held": "%s взето: ляв/десен избира къде, A пуска, B го връща обратно.",
|
||||
"Hides models behind opaque blocks. Async checks off-thread; entities tick.": "Скрива модели зад непрозрачни блокове. Async проверява извън нишката; субекти отметка.",
|
||||
"Block Entities": "Блокиране на обекти",
|
||||
"Shulker Boxes": "Кутии Шулкер",
|
||||
"Reuses settled boxes; lid movement stays animated.": "Повторно използване на утаени кутии; движението на капака остава анимирано.",
|
||||
"Signs": "Знаци",
|
||||
"Reuses sign text until text, light, or font changes.": "Използва повторно текста на знака, докато текстът, светлината или шрифтът се променят.",
|
||||
"Decorated Pots": "Декорирани саксии",
|
||||
"Reuses still pots; break wobble and sherd changes redraw.": "Повторно използване на саксии; прекъсване на колебанията и отделяне на промени, преначертаване.",
|
||||
"Bells": "Камбани",
|
||||
"Reuses still bells; ringing stays animated.": "Повторно използване на неподвижни камбани; звъненето остава анимирано.",
|
||||
"Chests": "Сандъци",
|
||||
"Reuses closed chests; moving lids stay animated.": "Повторно използване на затворени сандъци; подвижните клепачи остават анимирани.",
|
||||
"Banners": "Банери",
|
||||
"Reuses poles; flags and pattern layers still update.": "Повторно използване на стълбове; флаговете и слоевете с шаблони все още се актуализират.",
|
||||
"Copper Golem Statues": "Медни статуи на голем",
|
||||
"Reuses statue geometry until its block state changes.": "Използва повторно геометрията на статуята, докато блоковото й състояние се промени.",
|
||||
"Leaf Culling": "Изрязване на листа",
|
||||
"Every leaf face. Vanilla's fancy leaves.": "Всеки лист лице. Елегантните листа на ванилия.",
|
||||
"Keeps fine plants in high-res packs from fading far away.": "Предпазва фините растения в пакети с висока разделителна способност от избледняване.",
|
||||
"Screenshot Quality": "Качество на екранната снимка",
|
||||
"F2 screenshots at this many times the game's resolution. Higher takes longer.": "Екранни снимки на F2 при толкова многократна разделителна способност на играта. По-високото отнема повече време.",
|
||||
"Frame Generation": "Генериране на рамка",
|
||||
"Shows a made-up frame between each two real ones: smoother, a little more lag.": "Показва измислен кадър между всеки два реални: по-гладък, с малко повече забавяне.",
|
||||
"Reuses the pack shadow map when sun, view and world match. Off by default.": "Използва повторно картата на сенките на глутницата, когато слънцето, гледката и света съвпадат. Изключено по подразбиране.",
|
||||
"Find BlueMap": "Намерете BlueMap",
|
||||
"On servers, finds their BlueMap web map for far terrain. Next join.": "На сървъри намира тяхната уеб карта BlueMap за далечен терен. Следващо присъединяване.",
|
||||
"BlueMap Radius": "Радиус на BlueMap",
|
||||
"How far around you BlueMap tiles are downloaded. Next join.": "На какво разстояние около вас се изтеглят плочките BlueMap. Следващо присъединяване.",
|
||||
"Synchronous": "Синхронен",
|
||||
"Asynchronous": "Асинхронен",
|
||||
"Solid": "Твърди",
|
||||
"Solid Aggressive": "Твърди агресивни",
|
||||
"Hollow": "Кух",
|
||||
"Leaves fully inside a canopy draw solid: fewer see-through layers.": "Листата изцяло вътре в сенник рисуват плътно: по-малко прозрачни слоеве.",
|
||||
"Leaves with one open side draw solid too: faster, chunkier edges.": "Листата с една отворена страна също рисуват плътно: по-бързи, по-едри ръбове.",
|
||||
"Drops faces between leaves: faster, but gaps look through.": "Капки лица между листата: по-бързо, но празнини прозират.",
|
||||
"Hotbar: Previous Slot": "Гореща лента: Предишен слот",
|
||||
"Hotbar: Next Slot": "Гореща лента: Следващ слот",
|
||||
"horizon.agent.connected.one": "Свързан: 1 връзка с агент",
|
||||
"horizon.agent.connected.many": "Свързани: %s агентни връзки",
|
||||
"horizon.agent.connected.one_ago": "Свързан: 1 връзка с агент, последна команда преди %s сек",
|
||||
"horizon.agent.connected.many_ago": "Свързани: %s агентни връзки, последна команда преди %s сек",
|
||||
"horizon.agent.waiting": "В очакване на агент.",
|
||||
"horizon.agent.off": "Изключено: агентите не могат да достигнат до тази игра.",
|
||||
"horizon.agent.session_opened": "Отворено за тази сесия от програмата, която стартира играта.",
|
||||
"horizon.agent.socket": "Сокет: %s (копирайте подробностите за пълния му път)",
|
||||
"horizon.agent.local_only": "Само локално: агенти на този компютър, никога в мрежата.",
|
||||
"%s nits": "%s нита",
|
||||
"Auto (%s nits)": "Автоматично (%s нита)",
|
||||
"%.2f nits": "%.2f нита",
|
||||
"Hides models behind opaque blocks. Entities update. Off with custom packs.": "Скрива модели зад непрозрачни блокове. Актуализация на обекти. Изключване с персонализирани пакети.",
|
||||
"horizon.extras.repro_keys": "Преки пътища за запис",
|
||||
"horizon.extras.repro_keys.tooltip": "F3+R стартира или спира възпроизвеждане на запис и F3+E записва неговия пакет. Изключено по подразбиране: записът копира света.",
|
||||
"horizon.agent.record_session": "Започнете запис на сесия",
|
||||
"horizon.agent.record_stop": "Спрете сесията за запис",
|
||||
"horizon.agent.export_repro": "Експортиране на Repro Bundle",
|
||||
"horizon.repro.started": "Записът започна; F3+R го спира и записва пакета.",
|
||||
"horizon.repro.stopped": "Записът е спрян: %s",
|
||||
"horizon.repro.bundle": "Repro пакет: %s",
|
||||
"horizon.repro.error": "Репро: %s",
|
||||
"horizon.repro.no_recording": "Все още няма запис за експортиране.",
|
||||
"horizon.repro.replay_started": "Стартирало повторение: %s",
|
||||
"horizon.pack.addInfo": "Добавете ресурсен пакет от Modrinth",
|
||||
"horizon.pack.addNone": "Няма намерени пакети с ресурси",
|
||||
"horizon.pack.addTitle": "Добавете пакет с ресурси",
|
||||
"horizon.pack.install": "Инсталирайте",
|
||||
"horizon.pack.installing": "Инсталиране",
|
||||
"horizon.pack.installed": "Инсталиран",
|
||||
"horizon.pack.search": "Търсене",
|
||||
"horizon.pack.searchHint": "Търсете Modrinth...",
|
||||
"horizon.pack.filters": "Филтри",
|
||||
"horizon.pack.filter.realistic": "Реалистичен",
|
||||
"horizon.pack.filter.simplistic": "Опростено",
|
||||
"horizon.pack.filter.vanillaLike": "Подобно на ванилия",
|
||||
"horizon.pack.addFailed": "Пакетът не можа да бъде добавен: %s",
|
||||
"horizon.extras.header.cosmetics": "Козметика",
|
||||
"horizon.extras.cosmetics_mine": "Моята козметика",
|
||||
"horizon.extras.cosmetics_mine.tooltip": "Скрит: всеки играч на Horizon ви вижда без вашата козметика, включително и вие. Същият превключвател като гардероба на стартера.",
|
||||
"horizon.extras.cosmetics_others": "Козметика за други играчи",
|
||||
"horizon.extras.cosmetics_others.tooltip": "Какво носят другите играчи на Horizon: всичко, само техните стилове на имената или нищо. Скрито, играта никога не пита какво носят.",
|
||||
"horizon.extras.cosmetics_auras": "Аури и следи на други",
|
||||
"horizon.extras.cosmetics_auras.tooltip": "Аури и пътеки, които другите играчи носят. Изключено по подразбиране.",
|
||||
"horizon.extras.cosmetics.shown": "Показано",
|
||||
"horizon.extras.cosmetics.hidden": "Скрити",
|
||||
"horizon.extras.cosmetics.name_tags": "Само етикети с имена"
|
||||
}
|
||||
Loaded 100 of 664 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user