mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-08 06:48:22 -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")
|
||||
}
|
||||
|
||||
@@ -173,16 +173,6 @@ impl Registries {
|
||||
fill(&mut self.templates, "templates");
|
||||
fill(&mut self.persistent_components, "persistent_components");
|
||||
fill(&mut self.learnable_recipes, "learnable_recipes");
|
||||
if self.summonable.is_empty() {
|
||||
self.summonable = strings(&r["summonable"]);
|
||||
self.summonable.sort();
|
||||
self.summonable.dedup();
|
||||
}
|
||||
if self.entity_types.is_empty() {
|
||||
self.entity_types = strings(&r["entity_types"]);
|
||||
self.entity_types.sort();
|
||||
self.entity_types.dedup();
|
||||
}
|
||||
const RELOADED: [&str; 5] = [
|
||||
"minecraft:advancement",
|
||||
"minecraft:recipe",
|
||||
@@ -246,16 +236,6 @@ impl Registries {
|
||||
fill(&mut self.templates, &e.templates);
|
||||
fill(&mut self.persistent_components, &e.persistent_components);
|
||||
fill(&mut self.learnable_recipes, &e.learnable_recipes);
|
||||
if self.summonable.is_empty() {
|
||||
self.summonable.clone_from(&e.summonable);
|
||||
self.summonable.sort();
|
||||
self.summonable.dedup();
|
||||
}
|
||||
if self.entity_types.is_empty() {
|
||||
self.entity_types.clone_from(&e.entity_types);
|
||||
self.entity_types.sort();
|
||||
self.entity_types.dedup();
|
||||
}
|
||||
for (name, ids) in &e.by_registry {
|
||||
if !self.by_registry.contains_key(name) {
|
||||
self.by_registry.insert(name.clone(), ids.clone());
|
||||
|
||||
@@ -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);
|
||||
@@ -378,7 +377,6 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
celestial: &[],
|
||||
stars: None,
|
||||
hand: &[],
|
||||
hand_items: &[],
|
||||
hand_fov_degrees: 70.0,
|
||||
screen: &[],
|
||||
pack: Default::default(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -11,7 +11,6 @@ use crate::{
|
||||
block_entity_anim::{self, EnchantingBook},
|
||||
breaking::entity_geometry,
|
||||
fast_hash::FxHashMap,
|
||||
font::Glyph,
|
||||
gfx::mark_oit,
|
||||
hud::Rect,
|
||||
mesher::{CULL_BACK, Vertex},
|
||||
@@ -661,7 +660,7 @@ fn sign_text_calls(
|
||||
} else {
|
||||
(dark, 0, vertex_light(packed))
|
||||
};
|
||||
let advance = |c: char| (scene.glyph)(c).advance;
|
||||
let advance = |c: char| (scene.glyph)(c).1;
|
||||
let width = |s: &str| s.chars().map(advance).sum::<f32>().ceil();
|
||||
text.lines
|
||||
.iter()
|
||||
@@ -700,19 +699,10 @@ fn draw_text(scene: &Scene, call: &TextCall, out: &mut Vec<Vertex>) {
|
||||
let mut x = call.x + dx;
|
||||
let y0 = call.y + dy;
|
||||
for ch in call.text.chars() {
|
||||
let g = (scene.glyph)(ch);
|
||||
if let Some(cell) = g.rect {
|
||||
// BakedSheetGlyph: the glyph's own box from the pen, not a
|
||||
// fixed 8x8 cell (a Unifont glyph is 0.5 units per texel).
|
||||
let (x0, x1) = (x + g.left, x + g.right);
|
||||
let (top, bottom) = (y0 + g.top, y0 + g.bottom);
|
||||
let corners = [(x0, bottom), (x1, bottom), (x1, top), (x0, top)];
|
||||
let uvs = [
|
||||
[cell.u0, cell.v1],
|
||||
[cell.u1, cell.v1],
|
||||
[cell.u1, cell.v0],
|
||||
[cell.u0, cell.v0],
|
||||
];
|
||||
let (cell, advance) = (scene.glyph)(ch);
|
||||
if let Some(r) = cell {
|
||||
let corners = [(x, y0 + 8.0), (x + 8.0, y0 + 8.0), (x + 8.0, y0), (x, y0)];
|
||||
let uvs = [[r.u0, r.v1], [r.u1, r.v1], [r.u1, r.v0], [r.u0, r.v0]];
|
||||
let c =
|
||||
0xFF00_0000 | (argb & 255) << 16 | (argb >> 8 & 255) << 8 | (argb >> 16 & 255);
|
||||
for ((cx, cy), uv) in corners.into_iter().zip(uvs) {
|
||||
@@ -731,7 +721,7 @@ fn draw_text(scene: &Scene, call: &TextCall, out: &mut Vec<Vertex>) {
|
||||
});
|
||||
}
|
||||
}
|
||||
x += g.advance;
|
||||
x += advance;
|
||||
}
|
||||
};
|
||||
if call.outline != 0 {
|
||||
@@ -828,8 +818,8 @@ pub struct Scene<'a> {
|
||||
/// profile → skin): a downloaded `skins/<sha1>` slot or the profile's
|
||||
/// `DefaultPlayerSkin` fallback; None keeps `SKIN_BY_TYPE` (`DefaultPlayerSkin.getDefaultTexture`, slim steve).
|
||||
pub skull: &'a dyn Fn([i32; 3]) -> Option<String>,
|
||||
/// Font glyph: atlas cell (none for blank glyphs such as the space), box and advance.
|
||||
pub glyph: &'a dyn Fn(char) -> Glyph,
|
||||
/// Font glyph cell (none for blank glyphs such as the space) and advance.
|
||||
pub glyph: &'a dyn Fn(char) -> (Option<Rect>, f32),
|
||||
/// Changes whenever `glyph` may answer differently (`Font::epoch`): sign
|
||||
/// text laid out under another value is laid out again.
|
||||
pub glyphs: u64,
|
||||
@@ -3352,7 +3342,7 @@ mod tests {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
// Compare the cached world path with the original model renderer,
|
||||
@@ -3620,13 +3610,7 @@ mod tests {
|
||||
w: 8,
|
||||
h: 8,
|
||||
};
|
||||
Glyph {
|
||||
rect: Some(rect),
|
||||
right: 8.0,
|
||||
bottom: 8.0,
|
||||
advance: 6.0,
|
||||
..Glyph::default()
|
||||
}
|
||||
(Some(rect), 6.0)
|
||||
};
|
||||
let scene = Scene {
|
||||
camera: DVec3::ZERO,
|
||||
@@ -3676,78 +3660,6 @@ mod tests {
|
||||
assert!(out.iter().all(|v| v.pos[2] == 0.0));
|
||||
}
|
||||
|
||||
/// BakedSheetGlyph draws a glyph over its own box from the pen
|
||||
/// (x + left .. x + right, y + up .. y + down), not a fixed 8x8 cell. With
|
||||
/// Force Unicode Font a Latin letter is an 8x16 Unifont glyph at half
|
||||
/// scale: 4 units wide and 8 tall, advancing 5 (#8699).
|
||||
#[test]
|
||||
fn sign_text_draws_each_glyph_over_its_own_box() {
|
||||
let world = World::new(BlockStateId(0), 1);
|
||||
let sprites = HashMap::new();
|
||||
let glyph = |_: char| Glyph {
|
||||
rect: Some(Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 0.0625,
|
||||
v1: 0.125,
|
||||
w: 8,
|
||||
h: 16,
|
||||
}),
|
||||
left: 0.0,
|
||||
top: 0.0,
|
||||
right: 4.0,
|
||||
bottom: 8.0,
|
||||
advance: 5.0,
|
||||
offset: 0.5,
|
||||
};
|
||||
let scene = Scene {
|
||||
camera: DVec3::ZERO,
|
||||
orientation: Quat::IDENTITY,
|
||||
anim: &|_| BlockAnim::default(),
|
||||
texts: None,
|
||||
partial: 0.0,
|
||||
game_time: 0,
|
||||
max_y: 320,
|
||||
world: &world,
|
||||
sprites: &sprites,
|
||||
emissive: &Default::default(),
|
||||
translucent_sprites: &HashSet::new(),
|
||||
atlas_revision: 1,
|
||||
nether: false,
|
||||
lid: &|_| 0.0,
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
};
|
||||
let call = TextCall {
|
||||
matrix: Mat4::IDENTITY,
|
||||
x: 10.0,
|
||||
y: 2.0,
|
||||
text: "ab".into(),
|
||||
color: 0xFF00_0000,
|
||||
outline: 0,
|
||||
light: 240 | 240 << 8,
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
draw_text(&scene, &call, &mut out);
|
||||
let boxes: Vec<_> = out
|
||||
.chunks(4)
|
||||
.map(|q| {
|
||||
let xs = q.iter().map(|v| v.pos[0]);
|
||||
let ys = q.iter().map(|v| v.pos[1]);
|
||||
[
|
||||
xs.clone().fold(f32::MAX, f32::min),
|
||||
xs.fold(f32::MIN, f32::max),
|
||||
ys.clone().fold(f32::MAX, f32::min),
|
||||
ys.fold(f32::MIN, f32::max),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(boxes, [[10.0, 14.0, 2.0, 10.0], [15.0, 19.0, 2.0, 10.0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_sign_text_matches_and_follows_changes() {
|
||||
let renderer = BlockEntityRenderer {
|
||||
@@ -3802,13 +3714,7 @@ mod tests {
|
||||
w: 8,
|
||||
h: 8,
|
||||
};
|
||||
Glyph {
|
||||
rect: (c != ' ').then_some(rect),
|
||||
right: 8.0,
|
||||
bottom: 8.0,
|
||||
advance: wide.get(),
|
||||
..Glyph::default()
|
||||
}
|
||||
((c != ' ').then_some(rect), wide.get())
|
||||
};
|
||||
let (first_sign, changed_sign, glowing_sign) = (
|
||||
|_| Some(&first_text),
|
||||
@@ -3973,7 +3879,7 @@ mod tests {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
let mut verts = Vec::new();
|
||||
@@ -4058,7 +3964,7 @@ mod tests {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
// A closed single chest of `block` drawn by `renderer`: positions and UVs.
|
||||
|
||||
@@ -4,7 +4,6 @@ use crate::{
|
||||
ModelPart, SIGN_TEXT_KEEP, Scene, StaticSectionRevision, classify,
|
||||
},
|
||||
chunk::Chunk,
|
||||
font::Glyph,
|
||||
hud::Rect,
|
||||
mesher::Vertex,
|
||||
signs::{SignData, SignText},
|
||||
@@ -356,7 +355,7 @@ fn batched_callbacks_preserve_entity_ranges_and_allow_immediate_culling() {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
// Reverse local-position order to check that cache indexing does not
|
||||
@@ -444,7 +443,7 @@ fn static_cache_qualifies_local_positions_by_section_and_drops_old_entries() {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
@@ -498,7 +497,7 @@ fn gpu_static_cache_uses_section_local_chests_and_keeps_visibility_separate() {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
let blocks = [([100, 70, -32], 0)];
|
||||
@@ -652,19 +651,18 @@ fn sign_text_cache_can_be_disabled_and_reenabled_without_changing_output() {
|
||||
};
|
||||
let sign_data = &data;
|
||||
let sign = move |_: [i32; 3]| Some(sign_data);
|
||||
let glyph = |_: char| Glyph {
|
||||
rect: Some(Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 0.0625,
|
||||
v1: 0.0625,
|
||||
w: 8,
|
||||
h: 8,
|
||||
}),
|
||||
right: 8.0,
|
||||
bottom: 8.0,
|
||||
advance: 6.0,
|
||||
..Glyph::default()
|
||||
let glyph = |_: char| {
|
||||
(
|
||||
Some(Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 0.0625,
|
||||
v1: 0.0625,
|
||||
w: 8,
|
||||
h: 8,
|
||||
}),
|
||||
6.0,
|
||||
)
|
||||
};
|
||||
let scene = Scene {
|
||||
camera: DVec3::new(2.25, 64.5, -3.75),
|
||||
@@ -737,7 +735,7 @@ fn shulker_cache_tracks_settled_poses_and_does_not_freeze_opening_animation() {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -838,7 +836,7 @@ fn shulker_cache_covers_all_colors_and_facing_directions() {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
@@ -892,7 +890,7 @@ fn cached_bells_keep_shaking_until_the_animation_ends() {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -953,7 +951,7 @@ fn cached_closed_chests_match_the_direct_model_and_rebuild_when_opening() {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -1048,7 +1046,7 @@ fn closed_chest_cache_covers_all_materials_and_atlas_reloads() {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
let pos = [0, 64, 0];
|
||||
@@ -1136,7 +1134,7 @@ fn double_chest_cache_bypasses_both_halves_while_either_lid_is_open() {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
let pair = [([0, 64, 0], 0), ([1, 64, 0], 1)];
|
||||
@@ -1211,7 +1209,7 @@ fn decorated_pot_cache_tracks_sherds_and_bypasses_wobble_animation() {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -1289,7 +1287,7 @@ fn banner_cache_keeps_the_pole_static_while_the_flag_and_patterns_update() {
|
||||
sign: &|_| None,
|
||||
banner: &banner_patterns,
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
@@ -1355,7 +1353,7 @@ fn cached_wall_banner_poles_use_the_wall_model() {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
let pos = [0, 64, 0];
|
||||
@@ -1402,7 +1400,7 @@ fn copper_statue_cache_rebuilds_when_its_pose_state_changes() {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
@@ -1507,7 +1505,7 @@ fn copper_statue_cache_covers_weathering_waxing_and_all_poses() {
|
||||
sign: &|_| None,
|
||||
banner: &|_| Vec::new(),
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyph: &|_| (None, 0.0),
|
||||
glyphs: 0,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
|
||||
@@ -58,24 +58,6 @@ impl DataPack {
|
||||
}
|
||||
out
|
||||
}
|
||||
/// The files of `data/<namespace>/<folder>/` in every namespace, as
|
||||
/// `list` names them; a folder pack walks only those folders.
|
||||
pub fn list_folder(&self, folder: &str) -> Vec<String> {
|
||||
let files = self.list_kinds(&[folder]);
|
||||
let Source::Zip(_) = &self.source else {
|
||||
return files;
|
||||
};
|
||||
files
|
||||
.into_iter()
|
||||
.filter(|f| {
|
||||
f.strip_prefix("data/")
|
||||
.and_then(|r| r.split_once('/'))
|
||||
.is_some_and(|(_, r)| {
|
||||
r.strip_prefix(folder).is_some_and(|r| r.starts_with('/'))
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
pub fn read(&self, path: &str) -> Option<Vec<u8>> {
|
||||
match &self.source {
|
||||
Source::Zip(z) => z.read(path).ok(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -24,7 +24,7 @@ pub const DEFAULT: &str = "minecraft:default";
|
||||
|
||||
/// A glyph ready to draw: atlas UVs and its box relative to the pen
|
||||
/// position (GlyphBitmap left/top/right/bottom), in GUI pixels.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Glyph {
|
||||
pub rect: Option<Rect>,
|
||||
pub left: 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
|
||||
|
||||
+237
-1563
File diff suppressed because it is too large.
Load diff
@@ -877,15 +877,12 @@ 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()),
|
||||
sorted_indices: None,
|
||||
flat_id: None,
|
||||
gpu_id: None,
|
||||
gpu_fading: false,
|
||||
fading: None,
|
||||
leaving: None,
|
||||
animated_layers: Box::new([]),
|
||||
@@ -933,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;
|
||||
@@ -961,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 {
|
||||
@@ -1017,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),
|
||||
@@ -1074,15 +1071,12 @@ 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()),
|
||||
sorted_indices: None,
|
||||
flat_id: Some(id),
|
||||
gpu_id: None,
|
||||
gpu_fading: false,
|
||||
fading: None,
|
||||
leaving: None,
|
||||
animated_layers: Box::new([]),
|
||||
@@ -1301,15 +1295,12 @@ 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()),
|
||||
sorted_indices: (translucent > 0).then_some(if indices { at } else { (7, 7) }),
|
||||
flat_id: None,
|
||||
gpu_id: None,
|
||||
gpu_fading: false,
|
||||
fading: None,
|
||||
leaving: None,
|
||||
animated_layers: Box::new([]),
|
||||
|
||||
@@ -1,575 +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: &[],
|
||||
hand_items: &[],
|
||||
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` leaves sections fading in or out to the CPU (`FADING`).
|
||||
// `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 its flags (`FADING`).
|
||||
// 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,15 +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;
|
||||
// In a section's `a.w`: it fades in or out, and the CPU draws it with its fade.
|
||||
const FADING: u32 = 1u;
|
||||
|
||||
// The count of `block`'s draws in `page` (`cull`), then where they start (`scan`).
|
||||
fn block_count(block: u32, page: u32) -> u32 {
|
||||
@@ -105,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.
|
||||
@@ -182,21 +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 || (s.a.w & FADING) != 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)]
|
||||
@@ -42,32 +40,18 @@ impl Output {
|
||||
}
|
||||
}
|
||||
|
||||
/// `HORIZON_HDR_OUTPUT`'s names, in order of preference.
|
||||
const NAMES: [(Output, &str); 2] = [(Output::Pq, "pq"), (Output::ScRgb, "scrgb")];
|
||||
|
||||
/// The HDR output `caps` can present: HDR10 where offered, else scRGB.
|
||||
/// `HORIZON_HDR_OUTPUT=pq` or `scrgb` allows only that one.
|
||||
pub fn output(caps: &wgpu::SurfaceCapabilities) -> Option<Output> {
|
||||
pick(caps, std::env::var("HORIZON_HDR_OUTPUT").ok().as_deref())
|
||||
}
|
||||
fn pick(caps: &wgpu::SurfaceCapabilities, only: Option<&str>) -> Option<Output> {
|
||||
NAMES
|
||||
[(Output::Pq, "pq"), (Output::ScRgb, "scrgb")]
|
||||
.into_iter()
|
||||
.filter(|(_, name)| only.is_none_or(|o| o == *name))
|
||||
.map(|(output, _)| output)
|
||||
.find(|o| caps.color_spaces(o.format()).contains(o.space()))
|
||||
}
|
||||
/// The HDR output an offscreen renderer (`--offscreen`) offers: it has no display to ask, so
|
||||
/// only the one `HORIZON_HDR_OUTPUT` names, for HDR screenshots without an HDR display.
|
||||
pub fn offscreen_output() -> Option<Output> {
|
||||
named(std::env::var("HORIZON_HDR_OUTPUT").ok().as_deref())
|
||||
}
|
||||
fn named(name: Option<&str>) -> Option<Output> {
|
||||
NAMES
|
||||
.into_iter()
|
||||
.find(|(_, n)| name == Some(*n))
|
||||
.map(|(output, _)| output)
|
||||
}
|
||||
|
||||
struct Targets {
|
||||
size: [u32; 2],
|
||||
@@ -317,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| {
|
||||
@@ -651,7 +635,7 @@ fn scene_gain(peak: f32, paper_white: f32) -> f32 {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Output, named, pick};
|
||||
use super::{Output, pick};
|
||||
use wgpu::{SurfaceColorSpaces as S, TextureFormat as F};
|
||||
|
||||
#[test]
|
||||
@@ -701,14 +685,6 @@ mod tests {
|
||||
assert_eq!(pick(&scrgb, Some("pq")), None);
|
||||
assert_eq!(pick(&caps(&[(F::Bgra8UnormSrgb, S::SRGB)]), None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offscreen_offers_only_the_named_output() {
|
||||
assert_eq!(named(Some("pq")), Some(Output::Pq));
|
||||
assert_eq!(named(Some("scrgb")), Some(Output::ScRgb));
|
||||
assert_eq!(named(Some("hdr10")), None);
|
||||
assert_eq!(named(None), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -76,14 +76,14 @@ pub struct WorldDrawn<'a> {
|
||||
/// the run's first vertex and the entity, `(1 << 32) | id` for a mob or a player and
|
||||
/// `(2 << 32) | id` for a dropped item (their entity ids), 0 for anything else. A run lasts
|
||||
/// until the next. Unlike `spans`, whose ids name a type, these follow one entity from
|
||||
/// frame to frame (API 371).
|
||||
/// frame to frame (API @API:world_drawn_motion@).
|
||||
pub motion: &'a [(u32, u64)],
|
||||
/// The shared atlas entity UVs point into.
|
||||
pub atlas: Option<&'a wgpu::Texture>,
|
||||
/// Quad indices (0 1 2 0 2 3 for each 4 vertices), for as many quads as `entities` holds.
|
||||
pub quad_indices: &'a wgpu::Buffer,
|
||||
/// The block textures the terrain samples, for drawing blocks with the player's own
|
||||
/// textures (`native_block_sprites` says where a block's are). API 380.
|
||||
/// textures (`native_block_sprites` says where a block's are). API @API:native_block_sprites@.
|
||||
pub block_sprites: BlockSpriteArrays<'a>,
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ pub fn native_world_drawn(world: &mut WorldDrawn<'_>) {
|
||||
}
|
||||
|
||||
/// The first-person hand is drawn (see [`HandDrawn`]), on every frame [`native_world_drawn`]
|
||||
/// runs, with no hand too. API 364.
|
||||
/// runs, with no hand too. API @API:hand_drawn@.
|
||||
#[inline(never)]
|
||||
pub fn native_hand_drawn(hand: &mut HandDrawn<'_>) {
|
||||
generated::dispatch_hand_drawn(RENDERERS.get().map_or(&[], |renderers| renderers), hand);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -26,37 +26,6 @@ impl PackAtlas {
|
||||
split: u32,
|
||||
layers: u32,
|
||||
sprite: u32,
|
||||
) -> Option<Self> {
|
||||
Self::with_levels(
|
||||
device,
|
||||
encoder,
|
||||
arrays,
|
||||
split,
|
||||
layers,
|
||||
sprite,
|
||||
arrays[0].mip_level_count(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Encoded material channels must select an existing texel, including at distance.
|
||||
pub fn material(
|
||||
device: &wgpu::Device,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
arrays: &[wgpu::Texture],
|
||||
split: u32,
|
||||
layers: u32,
|
||||
) -> Option<Self> {
|
||||
Self::with_levels(device, encoder, arrays, split, layers, arrays[0].width(), 1)
|
||||
}
|
||||
|
||||
fn with_levels(
|
||||
device: &wgpu::Device,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
arrays: &[wgpu::Texture],
|
||||
split: u32,
|
||||
layers: u32,
|
||||
sprite: u32,
|
||||
levels: u32,
|
||||
) -> Option<Self> {
|
||||
let layers = layers.max(1);
|
||||
let columns = (f64::from(layers).sqrt().ceil() as u32).max(1);
|
||||
@@ -66,7 +35,7 @@ impl PackAtlas {
|
||||
return None;
|
||||
}
|
||||
// Levels past a sprite's single pixel would mix neighbours: stop there.
|
||||
let levels = levels.min(sprite.ilog2() + 1);
|
||||
let levels = arrays[0].mip_level_count().min(sprite.ilog2() + 1);
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("Shader pack block atlas"),
|
||||
size: wgpu::Extent3d {
|
||||
@@ -283,7 +252,6 @@ impl PackAtlas {
|
||||
.collect(),
|
||||
after: String::new(),
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +58,7 @@ impl EntitySpan {
|
||||
}
|
||||
}
|
||||
|
||||
/// `HzDraw`'s size: `renderStage`, `entityId`, `blockEntityId`, `currentRenderedItemId`,
|
||||
/// `entityColor`.
|
||||
/// `HzDraw`'s size: `renderStage`, `entityId`, `blockEntityId`, a pad, `entityColor`.
|
||||
const SIZE: u32 = 32;
|
||||
|
||||
pub(super) struct DrawUniforms {
|
||||
@@ -74,10 +73,8 @@ impl DrawUniforms {
|
||||
let mut contents = vec![0u8; stride as usize * RENDER_STAGES.len()];
|
||||
for (i, slot) in contents.chunks_mut(stride as usize).enumerate() {
|
||||
slot[..4].copy_from_slice(&(i as i32).to_le_bytes());
|
||||
// `blockEntityId` outside block entities, as Iris has it; `currentRenderedItemId`
|
||||
// outside items, as Iris 1.11.7 gives it to terrain, water and the sky (observed).
|
||||
// `blockEntityId` outside block entities, as Iris has it.
|
||||
slot[8..12].copy_from_slice(&(-1i32).to_le_bytes());
|
||||
slot[12..16].copy_from_slice(&(-1i32).to_le_bytes());
|
||||
}
|
||||
let buffer = wgpu::util::DeviceExt::create_buffer_init(
|
||||
device,
|
||||
|
||||
@@ -101,7 +101,6 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
op: ">",
|
||||
reference: 0.1,
|
||||
}),
|
||||
item_id_declared: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +162,6 @@ pub fn cloud_inputs() -> horizon_shaders::GbuffersInputs {
|
||||
.collect(),
|
||||
after: String::new(),
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,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).
|
||||
@@ -379,89 +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;
|
||||
// Portal faces cast too (a pack's shadow pass sees block entities whole, as Iris
|
||||
// draws them); beams do not.
|
||||
let casts = part != Part::Beam && 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;
|
||||
@@ -508,7 +453,6 @@ impl PackEntities {
|
||||
pack: bool,
|
||||
casts: bool,
|
||||
overlay: u32,
|
||||
portal: bool,
|
||||
) {
|
||||
let (entity_id, block_id) = match program {
|
||||
BLOCK => (0, span.id),
|
||||
@@ -532,7 +476,6 @@ impl PackEntities {
|
||||
pack,
|
||||
shadow: casts,
|
||||
slot: self.draws.len() as u32,
|
||||
portal,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -561,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.
|
||||
@@ -735,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));
|
||||
@@ -802,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::*;
|
||||
@@ -1036,57 +791,6 @@ mod gpu_span_tests {
|
||||
assert_eq!(skin.spans.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_portal_faces_reach_the_shadow_pass_with_their_block_entity() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
|
||||
return;
|
||||
};
|
||||
let Ok((device, queue)) = pollster::block_on(adapter.request_device(&Default::default()))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let layout = super::super::pack_draw::DrawUniforms::new(&device);
|
||||
let mut geometry = PackEntities::new(&device, &layout.layout);
|
||||
// One block entity: a plain face, then an End portal face (overlay layer 9).
|
||||
let cpu: Vec<Vertex> = [0, 0, 0, 0, 9, 9, 9, 9]
|
||||
.into_iter()
|
||||
.map(|layer| Vertex {
|
||||
layer,
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
})
|
||||
.collect();
|
||||
let spans = [EntitySpan {
|
||||
start: 0,
|
||||
id: 99,
|
||||
stage: render_stage("BLOCK_ENTITIES"),
|
||||
..Default::default()
|
||||
}];
|
||||
let mut rest = Vec::new();
|
||||
geometry.prepare(
|
||||
&device,
|
||||
&queue,
|
||||
&layout.layout,
|
||||
&cpu,
|
||||
&spans,
|
||||
[true; PROGRAMS],
|
||||
([0.; 4], [1; 2]),
|
||||
Casters {
|
||||
blocks: true,
|
||||
..Default::default()
|
||||
},
|
||||
&mut rest,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
(geometry.draws.iter())
|
||||
.map(|d| (d.portal, d.shadow))
|
||||
.collect::<Vec<_>>(),
|
||||
[(false, true), (true, true)],
|
||||
"the portal face casts as its block entity does"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn posed_and_cpu_draws_keep_offsets_uniforms_and_special_fallbacks() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
|
||||
@@ -13,14 +13,9 @@ const HAND_DEPTH: f32 = 0.125;
|
||||
/// `HzHand`: the hand's projection and its inverse.
|
||||
const UNIFORM: u64 = 2 * 64;
|
||||
|
||||
/// `HzDraw`'s size (pack_draw.rs).
|
||||
const SLOT: u64 = 32;
|
||||
|
||||
/// What `gbuffers_hand` reads: the entity inputs, and the hand's projection from set 1.
|
||||
pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
let mut inputs = super::pack_entities::inputs();
|
||||
// Iris 1.11.7 declares `currentRenderedItemId` in the hand's programs (observed).
|
||||
inputs.item_id_declared = true;
|
||||
inputs.declarations.push_str(
|
||||
"layout(set = 1, binding = 1) uniform HzHand { mat4 hz_hand_projection; mat4 hz_hand_projection_inverse; };\n",
|
||||
);
|
||||
@@ -50,17 +45,10 @@ pub struct PackHand {
|
||||
capacity: u32,
|
||||
/// Quads uploaded this frame.
|
||||
pub quads: u32,
|
||||
/// `HzDraw` (set 2) of each run: the arm or one held item, and its `currentRenderedItemId`.
|
||||
slots: wgpu::Buffer,
|
||||
pub slot_group: wgpu::BindGroup,
|
||||
stride: u64,
|
||||
/// This frame's runs: first quad, quads, and the dynamic offset of their slot.
|
||||
pub runs: Vec<(u32, u32, u32)>,
|
||||
}
|
||||
|
||||
impl PackHand {
|
||||
/// `draws`: the layout of `HzDraw` (pack_draw.rs).
|
||||
pub fn new(device: &wgpu::Device, draws: &wgpu::BindGroupLayout) -> Self {
|
||||
pub fn new(device: &wgpu::Device) -> Self {
|
||||
let entry = |binding, ty| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
@@ -86,28 +74,6 @@ impl PackHand {
|
||||
});
|
||||
let capacity = 256;
|
||||
let (vertices, group) = Self::vertex_buffer(device, &layout, &uniform, capacity);
|
||||
let stride = SLOT.max(u64::from(
|
||||
device.limits().min_uniform_buffer_offset_alignment,
|
||||
));
|
||||
// The arm and the two hands' items: a few runs a frame.
|
||||
let slots = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Shader pack hand draws"),
|
||||
size: SLOTS * stride,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let slot_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Shader pack hand draws"),
|
||||
layout: draws,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||
buffer: &slots,
|
||||
offset: 0,
|
||||
size: wgpu::BufferSize::new(SLOT),
|
||||
}),
|
||||
}],
|
||||
});
|
||||
Self {
|
||||
indices: super::pack_entities::quad_indices(device, capacity),
|
||||
layout,
|
||||
@@ -116,10 +82,6 @@ impl PackHand {
|
||||
group,
|
||||
capacity,
|
||||
quads: 0,
|
||||
slots,
|
||||
slot_group,
|
||||
stride,
|
||||
runs: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,31 +116,18 @@ impl PackHand {
|
||||
(buffer, group)
|
||||
}
|
||||
|
||||
/// Uploads this frame's hand, its projection, and the `HzDraw` of each run `items` starts
|
||||
/// (first vertex, `currentRenderedItemId`; the slots past [`SLOTS`] share the last).
|
||||
/// Uploads this frame's hand and its projection.
|
||||
pub fn prepare(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
hand: &[Vertex],
|
||||
items: &[(u32, i32)],
|
||||
projection: Mat4,
|
||||
) {
|
||||
self.quads = (hand.len() / 4) as u32;
|
||||
self.runs.clear();
|
||||
if self.quads == 0 {
|
||||
return;
|
||||
}
|
||||
let mut contents = Vec::new();
|
||||
for (start, quads, id) in runs(self.quads, items) {
|
||||
let slot = (self.runs.len() as u64).min(SLOTS - 1);
|
||||
if slot as usize == self.runs.len() {
|
||||
contents.extend_from_slice(&draw_slot(id));
|
||||
contents.resize(((slot + 1) * self.stride) as usize, 0);
|
||||
}
|
||||
self.runs.push((start, quads, (slot * self.stride) as u32));
|
||||
}
|
||||
queue.write_buffer(&self.slots, 0, &contents);
|
||||
if self.quads > self.capacity {
|
||||
self.capacity = self.quads.next_power_of_two();
|
||||
(self.vertices, self.group) =
|
||||
@@ -195,54 +144,10 @@ impl PackHand {
|
||||
}
|
||||
}
|
||||
|
||||
/// The hand's runs a frame keeps slots for.
|
||||
const SLOTS: u64 = 8;
|
||||
|
||||
/// `HzDraw` of a hand run: `renderStage` the solid hand's, `blockEntityId` -1 as outside block
|
||||
/// entities, and `currentRenderedItemId`.
|
||||
fn draw_slot(id: i32) -> [u8; SLOT as usize] {
|
||||
let mut bytes = [0u8; SLOT as usize];
|
||||
let stage = horizon_shaders::render_stage("HAND_SOLID") as i32;
|
||||
bytes[0..4].copy_from_slice(&stage.to_le_bytes());
|
||||
bytes[8..12].copy_from_slice(&(-1i32).to_le_bytes());
|
||||
bytes[12..16].copy_from_slice(&id.to_le_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
/// The runs of `quads` quads that `items` starts (first vertex, id), as first quad, quads and
|
||||
/// id; geometry before the first start, or with no starts at all, reads 0, as the arm does.
|
||||
fn runs(quads: u32, items: &[(u32, i32)]) -> Vec<(u32, u32, i32)> {
|
||||
let mut starts: Vec<(u32, i32)> = vec![(0, 0)];
|
||||
for &(start, id) in items {
|
||||
let q = (start / 4).min(quads);
|
||||
match starts.last_mut() {
|
||||
Some(last) if last.0 == q => last.1 = id,
|
||||
_ => starts.push((q, id)),
|
||||
}
|
||||
}
|
||||
(0..starts.len())
|
||||
.map(|i| {
|
||||
let end = starts.get(i + 1).map_or(quads, |s| s.0);
|
||||
(starts[i].0, end - starts[i].0, starts[i].1)
|
||||
})
|
||||
.filter(|r| r.1 > 0)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Each item's quads draw with its id; geometry before the first item is the arm's (0).
|
||||
#[test]
|
||||
fn runs_split_the_hand_by_item() {
|
||||
assert_eq!(runs(3, &[]), [(0, 3, 0)]);
|
||||
assert_eq!(runs(5, &[(0, 11), (8, 65535)]), [(0, 2, 11), (2, 3, 65535)]);
|
||||
assert_eq!(runs(4, &[(4, 21)]), [(0, 1, 0), (1, 3, 21)]);
|
||||
// An item that drew nothing gives way to the next.
|
||||
assert_eq!(runs(2, &[(0, 11), (0, 22)]), [(0, 2, 22)]);
|
||||
}
|
||||
|
||||
/// The hand's window depth lies within `MC_HAND_DEPTH` of the middle (packs tell the hand
|
||||
/// apart by it), and x, y match the plain projection.
|
||||
#[test]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,7 +50,6 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
.collect(),
|
||||
after,
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
//! Original synthetic material transport oracle; no external shader algorithms or assets.
|
||||
use super::*;
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a host GPU; missing adapter is a failure"]
|
||||
fn terrain_material_samplers_preserve_surface_and_encoded_bytes() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
|
||||
eprintln!("Material transport GPU: {:?}", adapter.get_info());
|
||||
assert_ne!(adapter.get_info().device_type, wgpu::DeviceType::Cpu);
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: adapter.limits(),
|
||||
..Default::default()
|
||||
}))
|
||||
.unwrap();
|
||||
let root = std::env::temp_dir().join(format!("horizon-material-pack-{}", std::process::id()));
|
||||
std::fs::create_dir_all(root.join("shaders")).unwrap();
|
||||
std::fs::write(
|
||||
root.join("shaders/gbuffers_terrain.vsh"),
|
||||
"#version 120\nvoid main(){gl_Position=gl_Vertex;}",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
root.join("shaders/gbuffers_terrain.fsh"),
|
||||
r#"#version 120
|
||||
uniform sampler2D gtexture;
|
||||
uniform sampler2D normals;
|
||||
uniform sampler2D specular;
|
||||
void main() {
|
||||
int x = int(floor(gl_FragCoord.x));
|
||||
int layer = x - (x / 5) * 5;
|
||||
// Source coordinate (1.25,1.25): inside a texel, away from both its centre and boundary.
|
||||
vec2 uv = (vec2(float(layer - (layer / 3) * 3), float(layer / 3)) + 0.3125) / vec2(3.0, 2.0);
|
||||
if (x < 5) gl_FragData[0] = texture2DLod(specular, uv, 12.0);
|
||||
else if (x < 10) gl_FragData[0] = texture2DLod(normals, uv, 12.0);
|
||||
else gl_FragData[0] = texture2DLod(gtexture, uv, 0.0);
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let (split, layers) = (2u32, 5u32);
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
let atlases: Vec<_> = (0..3)
|
||||
.map(|kind| {
|
||||
let side = if kind == 0 { 4 } else { 2 };
|
||||
let levels = if kind == 0 { 3 } else { 1 };
|
||||
let arrays: Vec<_> = (0..layers.div_ceil(split))
|
||||
.map(|array| {
|
||||
let count = (layers - array * split).min(split);
|
||||
let pixels: Vec<u8> = (0..count)
|
||||
.flat_map(|l| {
|
||||
let layer = (array * split + l) as u8;
|
||||
(0..levels).flat_map(move |level| {
|
||||
let edge = side >> level;
|
||||
(0..edge * edge).flat_map(move |pixel| {
|
||||
if level != 0 {
|
||||
// Sampling unused source mip levels must fail the oracle.
|
||||
[211, 212, 213, 214]
|
||||
} else {
|
||||
match kind {
|
||||
0 if layer == 3 => crate::sprites::NO_SPECULAR,
|
||||
0 => [
|
||||
17 + layer,
|
||||
if pixel % 2 == 0 { 0 } else { 240 },
|
||||
65,
|
||||
if pixel % 2 == 0 { 0 } else { 255 },
|
||||
],
|
||||
1 if layer == 2 => crate::sprites::FLAT_NORMAL,
|
||||
1 => [128 + layer, 126, 255, 19 + layer],
|
||||
_ => [40 + layer, 50, 60, 255],
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
device.create_texture_with_data(
|
||||
&queue,
|
||||
&wgpu::TextureDescriptor {
|
||||
label: Some("Original fixture companion array"),
|
||||
size: wgpu::Extent3d {
|
||||
width: side,
|
||||
height: side,
|
||||
depth_or_array_layers: count,
|
||||
},
|
||||
mip_level_count: levels,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
},
|
||||
wgpu::util::TextureDataOrder::LayerMajor,
|
||||
&pixels,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
super::super::pack_atlas::PackAtlas::material(
|
||||
&device,
|
||||
&mut encoder,
|
||||
&arrays,
|
||||
split,
|
||||
layers,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
queue.submit([encoder.finish()]);
|
||||
let pack = horizon_shaders::Pack::open(&root).unwrap();
|
||||
let macros = horizon_shaders::Macros::standard();
|
||||
// Encoded zero alpha is data here and must remain observable in readback.
|
||||
let properties = horizon_shaders::Properties::parse("alphaTest.gbuffers_terrain=off", ¯os);
|
||||
let inputs = horizon_shaders::GameInputs {
|
||||
terrain: Some(horizon_shaders::GbuffersInputs {
|
||||
body: "vec2 p = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); hz_Vertex=vec4(p*2.0-1.0,0.0,1.0);".into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let post = horizon_shaders::load_post(&pack, "", &properties, ¯os, &inputs);
|
||||
assert!(post.terrain.is_some(), "{:?}", post.notes);
|
||||
let world = World {
|
||||
atlas: Some((atlases[2].view.clone(), atlases[2].size())),
|
||||
material_maps: Some([atlases[1].view.clone(), atlases[0].view.clone()]),
|
||||
lightmap: atlases[2].view.clone(),
|
||||
terrain: Some(Geometry {
|
||||
buffers: &[],
|
||||
table: None,
|
||||
depth_write: true,
|
||||
atlas: None,
|
||||
material_maps: true,
|
||||
ids: None,
|
||||
cull: false,
|
||||
}),
|
||||
block_ids: &[],
|
||||
celestial: None,
|
||||
gui_size: [1, 1],
|
||||
clouds: None,
|
||||
weather: false,
|
||||
hand: false,
|
||||
};
|
||||
let mut post = PackPost::new(
|
||||
&device,
|
||||
&queue,
|
||||
&adapter,
|
||||
post,
|
||||
wgpu::TextureFormat::Rgba8Unorm,
|
||||
world,
|
||||
);
|
||||
let mut pass = post
|
||||
.terrain
|
||||
.take()
|
||||
.unwrap_or_else(|| panic!("terrain pipeline must compile: {:?}", post.notes));
|
||||
// Request the existing copy usage for the fixture's colour readback.
|
||||
post.copied |= 1;
|
||||
// First draw preserves production Geometry -> Pass scope, so a routing regression fails.
|
||||
for case in 0..3 {
|
||||
if case == 1 {
|
||||
pass.material_maps = false;
|
||||
}
|
||||
if case == 2 {
|
||||
pass.material_maps = true;
|
||||
post.material_maps = None;
|
||||
}
|
||||
let maps_expected = case == 0;
|
||||
pass.groups.clear();
|
||||
post.resize(&device, [15, 1]);
|
||||
let read = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: 256,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
{
|
||||
let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Initialize fixture depth"),
|
||||
color_attachments: &[],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &post.targets.as_ref().unwrap().depth,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
let group = post.bind(&device, &queue, &mut encoder, &mut pass, [15, 1]);
|
||||
{
|
||||
let mut render = post.begin(&mut encoder, &pass);
|
||||
render.set_pipeline(pass.render());
|
||||
render.set_bind_group(0, &group, &[]);
|
||||
if pass.draw {
|
||||
post.draws.bind(&mut render, "TERRAIN_SOLID");
|
||||
}
|
||||
render.draw(0..3, 0..1);
|
||||
}
|
||||
let output = post.targets.as_ref().unwrap().color[0].as_ref().unwrap()[0].texture();
|
||||
encoder.copy_texture_to_buffer(
|
||||
output.as_image_copy(),
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &read,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
bytes_per_row: Some(256),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: 15,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
queue.submit([encoder.finish()]);
|
||||
read.slice(..).map_async(wgpu::MapMode::Read, |_| {});
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
let data = read.slice(..).get_mapped_range().unwrap();
|
||||
for i in 0..15usize {
|
||||
let layer = (i % 5) as u8;
|
||||
let expected = if i >= 10 {
|
||||
[40 + layer, 50, 60, 255]
|
||||
} else if i >= 5 && (!maps_expected || layer == 2) {
|
||||
crate::sprites::FLAT_NORMAL
|
||||
} else if i >= 5 {
|
||||
[128 + layer, 126, 255, 19 + layer]
|
||||
} else if !maps_expected || layer == 3 {
|
||||
crate::sprites::NO_SPECULAR
|
||||
} else {
|
||||
[17 + layer, 240, 65, 255]
|
||||
};
|
||||
assert_eq!(&data[i * 4..i * 4 + 4], &expected, "case={case}, pixel={i}");
|
||||
}
|
||||
eprintln!("Material oracle case={case}: 15 exact RGBA pixels pass");
|
||||
}
|
||||
// Fault injection: the same production binding must refuse a filtering sampler.
|
||||
// If a regression changed both the binding and sampler, the fractional positive
|
||||
// probes above would instead mix 0.75*240=180 and 0.75*255=191.25, not 240/255.
|
||||
pass.material_maps = true;
|
||||
post.material_maps = Some([atlases[1].view.clone(), atlases[0].view.clone()]);
|
||||
post.nearest_repeat = sampler(&device, wgpu::FilterMode::Linear, wgpu::AddressMode::Repeat);
|
||||
pass.groups.clear();
|
||||
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
let _invalid_group = post.bind(&device, &queue, &mut encoder, &mut pass, [15, 1]);
|
||||
let error = pollster::block_on(validation.pop())
|
||||
.expect("production material binding must reject a filtering sampler");
|
||||
assert!(
|
||||
error.to_string().to_ascii_lowercase().contains("filtering"),
|
||||
"{error}"
|
||||
);
|
||||
eprintln!("Material oracle negative filtering control: rejected by production binding");
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,111 +0,0 @@
|
||||
//! A buffer that isn't cleared, in passes that leave pixels alone or blend: what is under them.
|
||||
//! The expected values are Iris 1.11.7's, measured with probe packs of our own (#9009).
|
||||
use super::*;
|
||||
|
||||
/// colortex6 is written three times a frame, the first write leaving the right half alone:
|
||||
/// there it reads the previous frame's last write, 0.125 (Iris 1.11.7), not the 0.25 written into
|
||||
/// that texture before. colortex8 is written twice a frame the same way: its right half was never
|
||||
/// written and reads 0 (Iris 1.11.7). colortex10 is only blended onto (`ONE ONE`), 0.125 a frame:
|
||||
/// it grows by 0.125 every frame (Iris 1.11.7 adds once per frame).
|
||||
#[test]
|
||||
fn a_buffer_that_is_not_cleared_starts_the_frame_with_its_last_write() {
|
||||
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();
|
||||
};
|
||||
let fill = |v: f32| format!("vec4({v},{v},{v},1)");
|
||||
let programs = [
|
||||
(
|
||||
"composite",
|
||||
format!(
|
||||
"#version 120\nvarying vec2 uv;\n/* RENDERTARGETS:6 */\n\
|
||||
/*\nconst int colortex6Format=RGBA16F;\nconst int colortex7Format=RGBA16F;\n\
|
||||
const int colortex8Format=RGBA16F;\nconst int colortex9Format=RGBA16F;\n\
|
||||
const int colortex10Format=RGBA16F;\nconst bool colortex6Clear=false;\n\
|
||||
const bool colortex8Clear=false;\nconst bool colortex10Clear=false;\n*/\n\
|
||||
void main() {{ if(uv.x>0.5) discard; gl_FragData[0]={}; }}\n",
|
||||
fill(0.5)
|
||||
),
|
||||
),
|
||||
(
|
||||
"composite1",
|
||||
format!(
|
||||
"#version 120\nuniform sampler2D colortex6;\nvarying vec2 uv;\n/* RENDERTARGETS:7,6 */\n\
|
||||
void main() {{ gl_FragData[0]=texture2D(colortex6,uv); gl_FragData[1]={}; }}\n",
|
||||
fill(0.25)
|
||||
),
|
||||
),
|
||||
(
|
||||
"composite2",
|
||||
format!(
|
||||
"#version 120\n/* RENDERTARGETS:6 */\nvoid main() {{ gl_FragData[0]={}; }}\n",
|
||||
fill(0.125)
|
||||
),
|
||||
),
|
||||
(
|
||||
"composite3",
|
||||
format!(
|
||||
"#version 120\nvarying vec2 uv;\n/* RENDERTARGETS:8 */\n\
|
||||
void main() {{ if(uv.x>0.5) discard; gl_FragData[0]={}; }}\n",
|
||||
fill(0.5)
|
||||
),
|
||||
),
|
||||
(
|
||||
"composite4",
|
||||
format!(
|
||||
"#version 120\nuniform sampler2D colortex8;\nvarying vec2 uv;\n/* RENDERTARGETS:9,8 */\n\
|
||||
void main() {{ gl_FragData[0]=texture2D(colortex8,uv); gl_FragData[1]={}; }}\n",
|
||||
fill(0.125)
|
||||
),
|
||||
),
|
||||
(
|
||||
"composite5",
|
||||
"#version 120\n/* RENDERTARGETS:10 */\nvoid main() { gl_FragData[0]=vec4(0.125,0.125,0.125,0); }\n"
|
||||
.to_owned(),
|
||||
),
|
||||
(
|
||||
"final",
|
||||
"#version 120\nuniform sampler2D colortex7;\nuniform sampler2D colortex9;\n\
|
||||
uniform sampler2D colortex10;\nvarying vec2 uv;\n\
|
||||
void main() { gl_FragColor=vec4(texture2D(colortex7,uv).r,texture2D(colortex9,uv).r,texture2D(colortex10,uv).r,1); }\n"
|
||||
.to_owned(),
|
||||
),
|
||||
];
|
||||
for (program, fragment) in &programs {
|
||||
add(&format!("{program}.vsh"), VERTEX);
|
||||
add(&format!("{program}.fsh"), fragment);
|
||||
}
|
||||
add("shaders.properties", "blend.composite5=ONE ONE ONE ONE\n");
|
||||
let pack = horizon_shaders::Pack::from_zip_bytes(
|
||||
"clear-false-history",
|
||||
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..4u8 {
|
||||
let actual = gpu.render(&mut post, [16, 16]);
|
||||
let sum = 32 * (frame + 1);
|
||||
let wrong: Vec<_> = (actual.iter().enumerate())
|
||||
.filter(|(n, p)| {
|
||||
let expected = match (n % 16 < 8, frame) {
|
||||
(true, _) => [128, 128, sum, 255],
|
||||
(false, 0) => [0, 0, sum, 255],
|
||||
(false, _) => [32, 0, sum, 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,70 +0,0 @@
|
||||
//! A texel offset given by a `const` local reads the texel that far away, as a literal offset
|
||||
//! does (GLSL 4.60, 4.3.3 and 8.9.2, as #8462 quotes them). #8462's probe.
|
||||
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);
|
||||
// Each texel of colortex1 holds its own column and row, in eighths.
|
||||
add(
|
||||
"composite.fsh",
|
||||
"#version 120\n/* RENDERTARGETS: 1 */\n\
|
||||
void main() { gl_FragData[0] = vec4(floor(gl_FragCoord.xy) / 8.0, 0.0, 1.0); }\n",
|
||||
);
|
||||
add("final.vsh", VERTEX);
|
||||
// Red and green: what the texel two columns right and one row down holds; blue: this row.
|
||||
add(
|
||||
"final.fsh",
|
||||
"#version 330 compatibility\nuniform sampler2D colortex1;\nin vec2 uv;\n\
|
||||
vec4 f(vec2 p) { const ivec2 offset = ivec2(2, -1); return textureLodOffset(colortex1, p, 0.0, offset); }\n\
|
||||
void main() { gl_FragColor = vec4(f(uv).rg, floor(gl_FragCoord.y) / 8.0, 1.0); }\n",
|
||||
);
|
||||
let pack = horizon_shaders::Pack::from_zip_bytes(
|
||||
"original-const-offset",
|
||||
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 a_const_local_offset_reads_the_offset_texel() {
|
||||
let Some(gpu) = Fixture::new() else { return };
|
||||
let mut post = gpu.build(pack());
|
||||
let pixels = gpu.render(&mut post, [8, 8]);
|
||||
let eighth = |n: u8| (f32::from(n) * 255.0 / 8.0).round() as u8;
|
||||
let mut checked = 0;
|
||||
for (n, p) in pixels.iter().enumerate() {
|
||||
let x = (n % 8) as u8;
|
||||
// The bottom row (GL's row 0) and the two right columns read past the edge.
|
||||
if x >= 6 || p[2] == 0 {
|
||||
continue;
|
||||
}
|
||||
let row = (0..8)
|
||||
.find(|&r| eighth(r).abs_diff(p[2]) <= 1)
|
||||
.expect("row in blue");
|
||||
let want = [eighth(x + 2), eighth(row - 1)];
|
||||
assert!(
|
||||
p[0].abs_diff(want[0]) <= 1 && p[1].abs_diff(want[1]) <= 1,
|
||||
"pixel {n} (column {x}, GL row {row}): {p:?}, expected red and green {want:?}"
|
||||
);
|
||||
checked += 1;
|
||||
}
|
||||
assert_eq!(checked, 6 * 7);
|
||||
}
|
||||
@@ -315,21 +315,19 @@ fn check(sampler: &str, stage: Stage, scene: Scene, expected: [u8; 4]) {
|
||||
let hand = matches!(scene, Scene::Terrain { hand: true, .. });
|
||||
let water = matches!(scene, Scene::Terrain { water: true, .. } | Scene::WaterOnly);
|
||||
let inputs=horizon_shaders::GameInputs {
|
||||
terrain:terrain.then(|| horizon_shaders::GbuffersInputs { declarations:"layout(location=0)in vec3 fixture_position;\n".into(),body:"hz_Vertex=vec4(fixture_position,1.0);hz_Color=vec4(1.0);hz_MultiTexCoord0=vec4(0.0);hz_MultiTexCoord1=vec4(240.0);hz_MultiTexCoord2=vec4(240.0);hz_Normal=vec3(0.0,0.0,1.0);\n".into(),attributes:vec![],after:String::new(),alpha_test:None,item_id_declared:false }),
|
||||
terrain:terrain.then(|| horizon_shaders::GbuffersInputs { declarations:"layout(location=0)in vec3 fixture_position;\n".into(),body:"hz_Vertex=vec4(fixture_position,1.0);hz_Color=vec4(1.0);hz_MultiTexCoord0=vec4(0.0);hz_MultiTexCoord1=vec4(240.0);hz_MultiTexCoord2=vec4(240.0);hz_Normal=vec3(0.0,0.0,1.0);\n".into(),attributes:vec![],after:String::new(),alpha_test:None }),
|
||||
hand:hand.then(crate::gfx::pack_hand::inputs),..Default::default()
|
||||
};
|
||||
let mut post = gpu.load(
|
||||
shaders(sampler, stage, scene),
|
||||
World {
|
||||
atlas: None,
|
||||
material_maps: None,
|
||||
lightmap: color.clone(),
|
||||
terrain: terrain.then_some(Geometry {
|
||||
buffers: &layouts,
|
||||
table: None,
|
||||
depth_write: true,
|
||||
atlas: None,
|
||||
material_maps: false,
|
||||
ids: None,
|
||||
cull: false,
|
||||
}),
|
||||
@@ -619,7 +617,6 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
celestial: &[],
|
||||
stars: None,
|
||||
hand: &[],
|
||||
hand_items: &[],
|
||||
hand_fov_degrees: 70.0,
|
||||
screen: &[],
|
||||
pack: Default::default(),
|
||||
@@ -638,143 +635,3 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
post_effects: crate::post_chain::Request::none(),
|
||||
}
|
||||
}
|
||||
|
||||
/// shadowtex0 and shadowtex1 read through a plain sampler a quarter texel past a texel's centre,
|
||||
/// over a shadow map whose depth rises a sixteenth a texel: red and green are how far each
|
||||
/// moved toward the next texel, times four (0 nearest, 255 linear). `nearest` is a line for
|
||||
/// both stage files.
|
||||
fn shadow_depth_filtering(nearest: &str) -> Option<Vec<[u8; 4]>> {
|
||||
let gpu = Gpu::new()?;
|
||||
if !gpu
|
||||
.device
|
||||
.features()
|
||||
.contains(wgpu::Features::FLOAT32_FILTERABLE)
|
||||
{
|
||||
eprintln!("DEPTH_FIXTURE_NO_FLOAT32_FILTERABLE");
|
||||
return None;
|
||||
}
|
||||
let color = gpu.texture(T::Rgba8Unorm);
|
||||
let depth = gpu.texture(T::Depth32Float);
|
||||
gpu.clear(&color, &depth, 0.0);
|
||||
const ATTRS: [wgpu::VertexAttribute; 1] = wgpu::vertex_attr_array![0=>Float32x3];
|
||||
let layouts = [Some(wgpu::VertexBufferLayout {
|
||||
array_stride: 12,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &ATTRS,
|
||||
})];
|
||||
let inputs = horizon_shaders::GameInputs {
|
||||
terrain: Some(horizon_shaders::GbuffersInputs {
|
||||
declarations: "layout(location=0)in vec3 fixture_position;\n".into(),
|
||||
body: "hz_Vertex=vec4(fixture_position,1.0);hz_Color=vec4(1.0);\
|
||||
hz_MultiTexCoord0=vec4(0.0);hz_MultiTexCoord1=vec4(240.0);\
|
||||
hz_MultiTexCoord2=vec4(240.0);hz_Normal=vec3(0.0,0.0,1.0);\n"
|
||||
.into(),
|
||||
attributes: vec![],
|
||||
after: String::new(),
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let settings = format!("const int shadowMapResolution = 16;\n{nearest}\n");
|
||||
let files = vec![
|
||||
("final.vsh".to_string(), VERTEX.to_string()),
|
||||
(
|
||||
"final.fsh".into(),
|
||||
format!(
|
||||
"#version 330 compatibility\n{settings}\
|
||||
uniform sampler2D shadowtex0;\nuniform sampler2D shadowtex1;\n\
|
||||
float moved(sampler2D s, ivec2 t) {{\n\
|
||||
float a = texelFetch(s, t, 0).r;\n\
|
||||
return (texture(s, (vec2(t) + vec2(0.75, 0.5)) / 16.0).r - a) * 16.0;\n\
|
||||
}}\n\
|
||||
void main() {{\n\
|
||||
ivec2 t = ivec2(int(gl_FragCoord.x) % 15, 8);\n\
|
||||
gl_FragColor = vec4(moved(shadowtex0, t) * 4.0, moved(shadowtex1, t) * 4.0, 0.0, 1.0);\n\
|
||||
}}\n"
|
||||
),
|
||||
),
|
||||
// The shadow map's depth runs from 0 at its left edge to 1 at its right.
|
||||
(
|
||||
"shadow.vsh".into(),
|
||||
"#version 120\nvoid main(){gl_Position=vec4(gl_Vertex.xy,gl_Vertex.x,1.0);}".into(),
|
||||
),
|
||||
(
|
||||
"shadow.fsh".into(),
|
||||
format!("#version 120\n{settings}void main(){{gl_FragData[0]=vec4(1.0);}}"),
|
||||
),
|
||||
];
|
||||
let mut post = gpu.load(
|
||||
files,
|
||||
World {
|
||||
atlas: None,
|
||||
material_maps: None,
|
||||
lightmap: color.clone(),
|
||||
terrain: Some(Geometry {
|
||||
buffers: &layouts,
|
||||
table: None,
|
||||
depth_write: true,
|
||||
atlas: None,
|
||||
ids: None,
|
||||
cull: false,
|
||||
material_maps: false,
|
||||
}),
|
||||
block_ids: &[],
|
||||
celestial: None,
|
||||
gui_size: SIZE,
|
||||
clouds: None,
|
||||
weather: false,
|
||||
hand: false,
|
||||
},
|
||||
inputs,
|
||||
);
|
||||
assert!(post.has_shadow(), "{:?}", post.notes);
|
||||
let positions: [[f32; 3]; 6] = [
|
||||
[-1., -1., 0.],
|
||||
[1., -1., 0.],
|
||||
[1., 1., 0.],
|
||||
[-1., -1., 0.],
|
||||
[1., 1., 0.],
|
||||
[-1., 1., 0.],
|
||||
];
|
||||
let vertices = gpu
|
||||
.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Original shadow depth ramp"),
|
||||
contents: bytemuck::cast_slice(&positions),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let hud = HudBatch::new(SIZE);
|
||||
let frame = fixture_frame(&hud);
|
||||
Some(
|
||||
gpu.render(&mut post, &color, &depth, &frame, &mut |rp, list, _, _| {
|
||||
if list == TerrainList::ShadowSolid {
|
||||
rp.set_vertex_buffer(0, vertices.slice(..));
|
||||
rp.draw(0..6, 0..1);
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shadow_depth_is_read_linearly_through_a_plain_sampler() {
|
||||
// Iris 1.11.7 filters both (probe packs of our own; the PR has the readings).
|
||||
if let Some(pixels) = shadow_depth_filtering("") {
|
||||
all_pixels(&pixels, [255, 255, 0, 255], "shadowtex linear");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shadowtex_nearest_settings_read_the_nearest_texel() {
|
||||
for (line, expected) in [
|
||||
("const bool shadowtex0Nearest = true;", [0, 255, 0, 255]),
|
||||
("const bool shadowtexNearest = true;", [0, 255, 0, 255]),
|
||||
("const bool shadow0MinMagNearest = true;", [0, 255, 0, 255]),
|
||||
("const bool shadowtex1Nearest = true;", [255, 0, 0, 255]),
|
||||
("const bool shadow1MinMagNearest = true;", [255, 0, 0, 255]),
|
||||
] {
|
||||
if let Some(pixels) = shadow_depth_filtering(line) {
|
||||
all_pixels(&pixels, expected, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -208,7 +208,6 @@ impl Fixture {
|
||||
T::Rgba8Unorm,
|
||||
World {
|
||||
atlas: None,
|
||||
material_maps: None,
|
||||
lightmap,
|
||||
terrain: None,
|
||||
block_ids: &[],
|
||||
@@ -227,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);
|
||||
@@ -347,8 +346,8 @@ fn frame_clear_and_retained_history_apply_before_partial_post_writes() {
|
||||
let Some(gpu) = Fixture::new() else { return };
|
||||
for clear in [true, false] {
|
||||
let mut post = gpu.build(pack(Coverage::Full, false, false, Some(clear), false));
|
||||
// The other image is fully seeded red before the partial write, independent of its
|
||||
// initial clearing.
|
||||
// Every frame changes which physical image is main. The other image is fully
|
||||
// seeded red before the partial write, independent of its initial clearing.
|
||||
for frame in 0..3 {
|
||||
let actual = gpu.render(&mut post, [16, 16]);
|
||||
assert!(
|
||||
@@ -434,7 +433,6 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
celestial: &[],
|
||||
stars: None,
|
||||
hand: &[],
|
||||
hand_items: &[],
|
||||
hand_fov_degrees: 70.0,
|
||||
screen: &[],
|
||||
pack: Default::default(),
|
||||
@@ -456,73 +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:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[path = "clear_false_tests.rs"]
|
||||
mod clear_false_tests;
|
||||
@@ -1,73 +0,0 @@
|
||||
//! `flip.<program>.<buffer> = true` on a buffer the program writes (#9010). The expected values
|
||||
//! are Iris 1.11.7's, measured with probe packs of our own.
|
||||
use super::destination_tests::Fixture;
|
||||
use std::io::{Cursor, Write};
|
||||
|
||||
const VERTEX: &str = "#version 120\nvarying vec2 uv;\nvoid main() { uv=gl_MultiTexCoord0.xy; gl_Position=ftransform(); }\n";
|
||||
|
||||
/// `composite` writes 0.25 to colortex2 and colortex4 and forces both to flip: the forced flip
|
||||
/// cancels the write's own, so `composite1` samples colortex2's cleared side (Iris 1.11.7: 0),
|
||||
/// and `composite2`, blended `ONE ONE`, adds 0.5 onto colortex4's 0.25 (Iris 1.11.7: 0.749).
|
||||
#[test]
|
||||
fn a_forced_flip_of_a_written_buffer_leaves_it_unflipped() {
|
||||
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\n/* RENDERTARGETS:2,4 */\n\
|
||||
void main() { gl_FragData[0]=vec4(0.25,0.25,0.25,1); gl_FragData[1]=vec4(0.25,0.25,0.25,1); }\n",
|
||||
),
|
||||
(
|
||||
"composite1",
|
||||
"#version 120\nuniform sampler2D colortex2;\nvarying vec2 uv;\n/* RENDERTARGETS:3 */\n\
|
||||
void main() { gl_FragData[0]=texture2D(colortex2,uv); }\n",
|
||||
),
|
||||
(
|
||||
"composite2",
|
||||
"#version 120\n/* RENDERTARGETS:4 */\nvoid main() { gl_FragData[0]=vec4(0.5,0.5,0.5,0); }\n",
|
||||
),
|
||||
(
|
||||
"final",
|
||||
"#version 120\nuniform sampler2D colortex3;\nuniform sampler2D colortex4;\nvarying vec2 uv;\n\
|
||||
void main() { gl_FragColor=vec4(texture2D(colortex4,uv).r,texture2D(colortex3,uv).r,0,1); }\n",
|
||||
),
|
||||
] {
|
||||
add(&format!("{program}.vsh"), VERTEX);
|
||||
add(&format!("{program}.fsh"), fragment);
|
||||
}
|
||||
add(
|
||||
"shaders.properties",
|
||||
"flip.composite.colortex2=true\nflip.composite.colortex4=true\nblend.composite2=ONE ONE ONE ONE\n",
|
||||
);
|
||||
let pack = horizon_shaders::Pack::from_zip_bytes(
|
||||
"forced-flip-of-a-written-buffer",
|
||||
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..2 {
|
||||
let actual = gpu.render(&mut post, [16, 16]);
|
||||
let wrong: Vec<_> = (actual.iter().enumerate())
|
||||
.filter(|(_, p)| {
|
||||
p.iter()
|
||||
.zip([191, 0, 0, 255])
|
||||
.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,85 +0,0 @@
|
||||
//! A pack may give colortex0 an integer format (OptiFine's `shaders.txt`, "Texture Formats",
|
||||
//! lists `RGBA32UI`, as #8459 quotes it): the pack still loads, and reads back what it wrote
|
||||
//! there. #8459's probe; #8459 reports Iris 1.11.7 drawing (0, 200, 0) at every point
|
||||
//! sampled from it.
|
||||
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";
|
||||
|
||||
/// `depth`: the composite reads depthtex0, so the blit copies the world's depth; `last`: the
|
||||
/// pack has a `final` that shows colortex0 (without, Horizon shows colortex0 itself).
|
||||
fn pack(depth: bool, last: bool) -> 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);
|
||||
// With depthtex0: green where it holds the empty world's far plane (1), red elsewhere.
|
||||
let value = if depth {
|
||||
"texelFetch(depthtex0, ivec2(gl_FragCoord.xy), 0).r == 1.0 ? uvec4(0u, 200u, 0u, 255u) : uvec4(200u, 0u, 0u, 255u)"
|
||||
} else {
|
||||
"uvec4(0u, 200u, 0u, 255u)"
|
||||
};
|
||||
add(
|
||||
"composite.fsh",
|
||||
&format!(
|
||||
"#version 330 compatibility\n/* RENDERTARGETS: 0 */\n/*\nconst int colortex0Format = RGBA32UI;\n*/\n\
|
||||
uniform sampler2D depthtex0;\nlayout(location = 0) out uvec4 o;\nvoid main() {{ o = {value}; }}\n"
|
||||
),
|
||||
);
|
||||
if last {
|
||||
add("final.vsh", VERTEX);
|
||||
add(
|
||||
"final.fsh",
|
||||
"#version 330 compatibility\nuniform usampler2D colortex0;\n\
|
||||
void main() { gl_FragColor = vec4(texelFetch(colortex0, ivec2(gl_FragCoord.xy), 0)) / 255.0; }\n",
|
||||
);
|
||||
}
|
||||
let pack = horizon_shaders::Pack::from_zip_bytes(
|
||||
"original-integer-colortex0",
|
||||
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(),
|
||||
1 + usize::from(last),
|
||||
"{:?}",
|
||||
post.notes
|
||||
);
|
||||
post
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_integer_colortex0_keeps_what_the_pack_wrote() {
|
||||
let Some(gpu) = Fixture::new() else { return };
|
||||
for depth in [false, true] {
|
||||
let mut post = gpu.build(pack(depth, true));
|
||||
// Twice: the second frame's world blit runs over a colortex0 the first frame wrote.
|
||||
for frame in 0..2 {
|
||||
let pixels = gpu.render(&mut post, [8, 8]);
|
||||
let wrong: Vec<_> = pixels.iter().filter(|p| **p != [0, 200, 0, 255]).collect();
|
||||
assert!(wrong.is_empty(), "depth {depth}, frame {frame}: {wrong:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Without a `final`, there are no colours to show from an integer colortex0; what Iris shows
|
||||
/// then was not measured. The frame must still run.
|
||||
#[test]
|
||||
fn an_integer_colortex0_without_final_still_runs() {
|
||||
let Some(gpu) = Fixture::new() else { return };
|
||||
let mut post = gpu.build(pack(false, false));
|
||||
for _ in 0..2 {
|
||||
gpu.render(&mut post, [8, 8]);
|
||||
}
|
||||
}
|
||||
@@ -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,78 +0,0 @@
|
||||
//! `min3`, `max3` and `mid3` of GL_AMD_shader_trinary_minmax: the per-component minimum, maximum
|
||||
//! and median of three values, for float, int and uint vectors (the extension's additions to
|
||||
//! GLSL 4.30 Section 8.3). An original probe; the expected values are worked out by hand.
|
||||
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("final.vsh", VERTEX);
|
||||
// Pixel column x shows case x % 8 in red, green and blue.
|
||||
add(
|
||||
"final.fsh",
|
||||
"#version 330 compatibility\n#extension GL_AMD_shader_trinary_minmax : require\n\
|
||||
void main() {\n int x = int(gl_FragCoord.x) % 8;\n\
|
||||
vec3 a = vec3(0.1, 0.9, 0.4), b = vec3(0.6, 0.2, 0.8), c = vec3(0.3, 0.5, 0.0);\n\
|
||||
ivec3 i = ivec3(-3, 7, 2), j = ivec3(5, -1, 9), k = ivec3(0, 4, -6);\n\
|
||||
uvec3 u = uvec3(1u, 8u, 3u), v = uvec3(6u, 2u, 7u), w = uvec3(4u, 5u, 0u);\n\
|
||||
vec3 o = vec3(0.0);\n\
|
||||
switch (x) {\n\
|
||||
case 0: o = min3(a, b, c); break;\n\
|
||||
case 1: o = max3(a, b, c); break;\n\
|
||||
case 2: o = mid3(a, b, c); break;\n\
|
||||
case 3: o = vec3(mid3(i, j, k)) / 10.0; break;\n\
|
||||
case 4: o = vec3(max3(u, v, w)) / 10.0; break;\n\
|
||||
case 5: o = vec3(min3(i, j, k) + 6) / 10.0; break;\n\
|
||||
case 6: o = vec3(mid3(u.x, v.x, w.x), mid3(u.y, v.y, w.y), mid3(u.z, v.z, w.z)) / 10.0; break;\n\
|
||||
case 7: o = vec3(max3(a.x, b.x, c.x), min3(a.yz, b.yz, c.yz)); break;\n\
|
||||
}\n gl_FragColor = vec4(o, 1.0);\n}\n",
|
||||
);
|
||||
let pack = horizon_shaders::Pack::from_zip_bytes(
|
||||
"original-trinary",
|
||||
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(), 1, "{:?}", post.notes);
|
||||
post
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trinary_min_max_and_median_per_component() {
|
||||
let Some(gpu) = Fixture::new() else { return };
|
||||
let mut post = gpu.build(pack());
|
||||
let pixels = gpu.render(&mut post, [8, 8]);
|
||||
// Tenths, by hand: a = (1, 9, 4), b = (6, 2, 8), c = (3, 5, 0); i = (-3, 7, 2),
|
||||
// j = (5, -1, 9), k = (0, 4, -6); u = (1, 8, 3), v = (6, 2, 7), w = (4, 5, 0).
|
||||
let tenths: [[u8; 3]; 8] = [
|
||||
[1, 2, 0],
|
||||
[6, 9, 8],
|
||||
[3, 5, 4],
|
||||
[0, 4, 2],
|
||||
[6, 8, 7],
|
||||
[3, 5, 0],
|
||||
[4, 5, 3],
|
||||
[6, 2, 0],
|
||||
];
|
||||
let row: Vec<[u8; 3]> = pixels[..8].iter().map(|p| [p[0], p[1], p[2]]).collect();
|
||||
for (case, (got, want)) in row.iter().zip(tenths).enumerate() {
|
||||
let want = want.map(|t| (f32::from(t) * 25.5).round() as u8);
|
||||
assert!(
|
||||
got.iter().zip(want).all(|(g, w)| g.abs_diff(w) <= 1),
|
||||
"case {case}: {got:?}, expected {want:?}; row {row:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -215,7 +215,6 @@ fn pack(
|
||||
T::Rgba8Unorm,
|
||||
World {
|
||||
atlas: None,
|
||||
material_maps: None,
|
||||
lightmap: lightmap.clone(),
|
||||
terrain: None,
|
||||
block_ids: &[],
|
||||
@@ -352,7 +351,6 @@ fn frame(hud: &HudBatch) -> Frame<'_> {
|
||||
celestial: &[],
|
||||
stars: None,
|
||||
hand: &[],
|
||||
hand_items: &[],
|
||||
hand_fov_degrees: 70.0,
|
||||
screen: &[],
|
||||
pack: Default::default(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -35,17 +33,10 @@ pub(super) struct ShadowTargets {
|
||||
pub depth: (wgpu::Texture, wgpu::TextureView),
|
||||
/// `shadowtex1`: a copy of `shadowtex0` taken before translucent terrain, when read.
|
||||
pub depth1: Option<(wgpu::Texture, wgpu::TextureView)>,
|
||||
/// `shadowtex0`/`1` as a plain sampler filters them: Iris 1.11.7 reads them linearly unless
|
||||
/// a pack asks for the nearest texel (measured). wgpu filters a depth format only for a
|
||||
/// comparison, so these are `R32Float` copies of the final depths, when read that way.
|
||||
pub linear: [Option<wgpu::TextureView>; 2],
|
||||
/// The rows a depth copy goes through to `linear` (a depth format copies only to a buffer),
|
||||
/// and their stride in bytes.
|
||||
staging: Option<(wgpu::Buffer, u32)>,
|
||||
/// 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
|
||||
@@ -84,15 +75,13 @@ fn target(
|
||||
|
||||
impl ShadowTargets {
|
||||
/// The buffers of `shadow` the pack uses: `colors` (bit n: shadowcolor n), `depth1` when
|
||||
/// some program reads `shadowtex1`, `linear` (bit n: shadowtex n read through a filtering
|
||||
/// sampler). The resolution is kept within the device's limit.
|
||||
/// some program reads `shadowtex1`. The resolution is kept within the device's limit.
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
shadow: &horizon_shaders::Shadow,
|
||||
formats: [wgpu::TextureFormat; SHADOW_COLORS],
|
||||
formats: [wgpu::TextureFormat; 2],
|
||||
colors: u16,
|
||||
depth1: bool,
|
||||
linear: u8,
|
||||
) -> Self {
|
||||
let size = shadow
|
||||
.resolution
|
||||
@@ -114,43 +103,10 @@ impl ShadowTargets {
|
||||
})
|
||||
})
|
||||
});
|
||||
let linear = [0, 1].map(|n| {
|
||||
(linear & 1 << n != 0).then(|| {
|
||||
let label = format!("Shader pack shadowtex{n} (filtered)");
|
||||
device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some(&label),
|
||||
size: wgpu::Extent3d {
|
||||
width: size,
|
||||
height: size,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::R32Float,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
})
|
||||
.create_view(&Default::default())
|
||||
})
|
||||
});
|
||||
let staging = linear.iter().any(Option::is_some).then(|| {
|
||||
let stride = (size * 4).next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Shader pack shadow depth rows"),
|
||||
size: u64::from(stride) * u64::from(size),
|
||||
usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
(buffer, stride)
|
||||
});
|
||||
Self {
|
||||
size,
|
||||
depth: target(device, "Shader pack shadowtex0", size, depth, copy),
|
||||
depth1: depth1.then(|| target(device, "Shader pack shadowtex1", size, depth, copy)),
|
||||
linear,
|
||||
staging,
|
||||
color,
|
||||
clears: std::array::from_fn(|n| {
|
||||
let b = &shadow.colors[n];
|
||||
@@ -187,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;
|
||||
@@ -206,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 {
|
||||
@@ -220,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).
|
||||
@@ -256,38 +205,4 @@ impl ShadowTargets {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `linear`: the filtered copies of the depths the shadow pass left (`shadowtex1` from its
|
||||
/// copy before translucent geometry).
|
||||
pub fn copy_linear(&self, encoder: &mut wgpu::CommandEncoder) {
|
||||
let Some((buffer, stride)) = &self.staging else {
|
||||
return;
|
||||
};
|
||||
for (n, linear) in self.linear.iter().enumerate() {
|
||||
let Some(linear) = linear else {
|
||||
continue;
|
||||
};
|
||||
let depth = match (n, &self.depth1) {
|
||||
(1, Some(d)) => &d.0,
|
||||
_ => &self.depth.0,
|
||||
};
|
||||
let rows = wgpu::TexelCopyBufferInfo {
|
||||
buffer,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(*stride),
|
||||
rows_per_image: None,
|
||||
},
|
||||
};
|
||||
encoder.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
aspect: wgpu::TextureAspect::DepthOnly,
|
||||
..depth.as_image_copy()
|
||||
},
|
||||
rows,
|
||||
depth.size(),
|
||||
);
|
||||
encoder.copy_buffer_to_texture(rows, linear.texture().as_image_copy(), depth.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,6 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
// At the far plane: the sky shows only where nothing else drew, and it writes no depth.
|
||||
after: " gl_Position.z = gl_Position.w;\n".into(),
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +122,6 @@ pub fn celestial_inputs() -> horizon_shaders::GbuffersInputs {
|
||||
],
|
||||
after: " gl_Position.z = gl_Position.w;\n".into(),
|
||||
alpha_test: None,
|
||||
item_id_declared: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -1388,7 +1295,6 @@ mod initial_history_tests {
|
||||
celestial: &[],
|
||||
stars: None,
|
||||
hand: &[],
|
||||
hand_items: &[],
|
||||
hand_fov_degrees: 70.0,
|
||||
screen: &[],
|
||||
pack: Default::default(),
|
||||
|
||||
@@ -44,7 +44,6 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
|
||||
op: ">",
|
||||
reference: 0.1,
|
||||
}),
|
||||
item_id_declared: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,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,68 +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;
|
||||
};
|
||||
self.draw_onto(device, encoder, &texture, surface);
|
||||
}
|
||||
|
||||
/// Draws `frame` onto the swapchain image (or offscreen target) `surface`.
|
||||
pub fn draw_onto(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
frame: &wgpu::Texture,
|
||||
surface: &wgpu::Texture,
|
||||
) {
|
||||
// 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, frame, &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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,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()?;
|
||||
@@ -153,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: &[],
|
||||
})
|
||||
@@ -208,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]),
|
||||
@@ -225,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]);
|
||||
@@ -257,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.
|
||||
#[cfg(any(test, 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))
|
||||
/// Every section's key and value.
|
||||
#[cfg(debug_assertions)]
|
||||
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,45 +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 at `key`, if it is resident.
|
||||
pub fn set_marked(&mut self, key: SectionKey, marked: bool) {
|
||||
if let Some(&slot) = self.slots.get(&key) {
|
||||
self.mark_slot(slot, marked);
|
||||
}
|
||||
}
|
||||
/// 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();
|
||||
@@ -237,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
|
||||
@@ -272,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);
|
||||
@@ -286,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)
|
||||
}
|
||||
@@ -404,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 {
|
||||
@@ -427,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);
|
||||
@@ -451,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;
|
||||
@@ -468,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;
|
||||
@@ -619,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 {
|
||||
@@ -633,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:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,13 +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, and so can a
|
||||
// mark set alone (`set_marked`), which leaves a key not resident alone.
|
||||
// A deterministic mix of inserts, replacements and removals.
|
||||
let mut seed = 0x2545_f491_u32;
|
||||
for step in 0..4000u32 {
|
||||
seed ^= seed << 13;
|
||||
@@ -681,136 +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));
|
||||
} else if (seed >> 12).is_multiple_of(5) {
|
||||
let marked = (seed >> 20).is_multiple_of(2);
|
||||
r.set_marked(key, marked);
|
||||
if let Some(held) = model.get_mut(&key) {
|
||||
held.1 = marked;
|
||||
}
|
||||
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() =>
|
||||
@@ -1050,12 +1009,7 @@ impl Item {
|
||||
Item::Horizon(H::Hdr) => g.hdr,
|
||||
Item::Horizon(H::BatteryMode) => g.battery,
|
||||
Item::Horizon(H::ShadowCache) => g.shadow_cache,
|
||||
Item::Horizon(H::TranslucentBatching) => {
|
||||
g.translucent_batching && crate::graphics::translucent_batching_offered()
|
||||
}
|
||||
Item::Horizon(H::HdrSmoothGradients) => {
|
||||
g.hdr_smooth_gradients && crate::graphics::smooth_gradients_offered()
|
||||
}
|
||||
Item::Horizon(H::TranslucentBatching) => g.translucent_batching,
|
||||
Item::Vanilla(Opt::ViewBobbing) => s.view_bobbing,
|
||||
Item::Vanilla(Opt::Vsync) => s.vsync,
|
||||
Item::Vanilla(Opt::Fullscreen) => s.fullscreen,
|
||||
@@ -1122,7 +1076,6 @@ impl Item {
|
||||
| H::ShadowCache
|
||||
| H::GraphicsApi
|
||||
| H::TranslucentBatching
|
||||
| H::HdrSmoothGradients
|
||||
) | Item::Vanilla(
|
||||
Opt::GuiScale
|
||||
| Opt::Fullscreen
|
||||
@@ -1949,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);
|
||||
@@ -2027,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
|
||||
}
|
||||
@@ -2406,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]
|
||||
@@ -2505,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));
|
||||
}
|
||||
@@ -2630,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));
|
||||
@@ -2638,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());
|
||||
@@ -2727,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);
|
||||
@@ -2747,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());
|
||||
@@ -3063,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]
|
||||
@@ -3166,13 +2991,13 @@ mod tests {
|
||||
assert_eq!(page, Some(Page::Advanced));
|
||||
let mut s = Settings::default();
|
||||
s.horizon.preset = Preset::Balanced;
|
||||
assert_eq!(item.switch(&s), Some(true));
|
||||
let mut ui = GraphicsUi::default();
|
||||
// Where the device can't, the saved On reads Off, and neither a click nor Enter
|
||||
// after it does anything: no sound, no change, no save.
|
||||
crate::graphics::offer_translucent_batching(false);
|
||||
assert!(item.blocked(&s).is_some());
|
||||
assert_eq!(item.label(&s).plain(), "Translucent Batching: Off");
|
||||
assert_eq!(item.switch(&s), Some(false));
|
||||
let r = row(&mut ui, &s, item);
|
||||
let point = (r[0] + 4.0, r[1] + 4.0);
|
||||
assert!(press(&mut ui, &mut s, point).is_empty());
|
||||
@@ -3183,7 +3008,6 @@ mod tests {
|
||||
// Where it can, a click switches it and keeps the preset.
|
||||
crate::graphics::offer_translucent_batching(true);
|
||||
assert_eq!(item.label(&s).plain(), "Translucent Batching: On");
|
||||
assert_eq!(item.switch(&s), Some(true));
|
||||
assert_eq!(
|
||||
press(&mut ui, &mut s, point),
|
||||
[Action::ButtonClick, Action::Changed]
|
||||
@@ -3207,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!(
|
||||
@@ -3226,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.
|
||||
@@ -3282,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);
|
||||
}
|
||||
|
||||
@@ -3447,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), || {
|
||||
@@ -3496,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.
|
||||
@@ -796,10 +793,10 @@ impl HudAtlas {
|
||||
let g = self.font.glyph(ch);
|
||||
g.rect.map(|rect| (rect, g.advance))
|
||||
}
|
||||
/// A font glyph in the default font: its atlas cell (none for blank
|
||||
/// glyphs such as the space), its box and advance.
|
||||
pub fn glyph_cell(&self, ch: char) -> crate::font::Glyph {
|
||||
self.font.glyph(ch)
|
||||
/// A font glyph's atlas cell (none for blank glyphs such as the space) and advance.
|
||||
pub fn glyph_cell(&self, ch: char) -> (Option<Rect>, f32) {
|
||||
let g = self.font.glyph(ch);
|
||||
(g.rect, g.advance)
|
||||
}
|
||||
pub fn build(root: &Path) -> Result<Self> {
|
||||
Self::build_over(root, root)
|
||||
@@ -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.
|
||||
@@ -875,16 +863,7 @@ struct MeshInput {
|
||||
}
|
||||
struct ReducedColumns {
|
||||
columns: Arc<[Column; 256]>,
|
||||
/// The chunk reduced to cells 2, 4, 8 and 16 blocks wide, each built the
|
||||
/// first time a tile of that width asks for it.
|
||||
///
|
||||
/// One chunk is covered by one far tile, so one of the four is what gets
|
||||
/// read, and a chunk whose detail arrives while full-detail terrain still
|
||||
/// draws it reads none. Building all four up front cost 405 us of a far
|
||||
/// terrain thread for every chunk the world sent (`Column::reduced` walks
|
||||
/// every span of the footprint four times over), and held 85 columns
|
||||
/// beside the chunk's 256.
|
||||
levels: [std::sync::OnceLock<Vec<Column>>; 4],
|
||||
levels: [Vec<Column>; 4],
|
||||
}
|
||||
impl MeshInput {
|
||||
/// The column of every cell: the tile's own samples unless the snapshot
|
||||
@@ -906,7 +885,6 @@ impl MeshInput {
|
||||
start: edge.start,
|
||||
end: edge.end,
|
||||
column: edge.column.get(),
|
||||
unknown: edge.unknown,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
@@ -914,26 +892,25 @@ impl MeshInput {
|
||||
}
|
||||
impl ReducedColumns {
|
||||
fn new(columns: Arc<[Column; 256]>) -> Self {
|
||||
Self {
|
||||
columns,
|
||||
levels: std::array::from_fn(|_| std::sync::OnceLock::new()),
|
||||
}
|
||||
}
|
||||
fn get(&self, x: usize, z: usize, width: usize) -> &Column {
|
||||
if width == 1 {
|
||||
return &self.columns[z * 16 + x];
|
||||
}
|
||||
let level = width.trailing_zeros() as usize - 1;
|
||||
let reduced = self.levels[level].get_or_init(|| {
|
||||
let columns = &self.columns;
|
||||
let levels = std::array::from_fn(|level| {
|
||||
let width = 2 << level;
|
||||
(0..16 / width)
|
||||
.flat_map(|z| {
|
||||
let columns = &columns;
|
||||
(0..16 / width)
|
||||
.map(move |x| Column::reduced(columns, x * width, z * width, width))
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
&reduced[(z / width) * (16 / width) + x / width]
|
||||
Self { columns, levels }
|
||||
}
|
||||
fn get(&self, x: usize, z: usize, width: usize) -> &Column {
|
||||
if width == 1 {
|
||||
&self.columns[z * 16 + x]
|
||||
} else {
|
||||
&self.levels[width.trailing_zeros() as usize - 1]
|
||||
[(z / width) * (16 / width) + x / width]
|
||||
}
|
||||
}
|
||||
}
|
||||
enum Job {
|
||||
@@ -1034,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
|
||||
@@ -1174,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();
|
||||
@@ -1194,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 = {
|
||||
@@ -1226,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();
|
||||
@@ -1282,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();
|
||||
@@ -1297,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();
|
||||
@@ -1321,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),
|
||||
@@ -1387,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);
|
||||
@@ -2133,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();
|
||||
@@ -2266,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,
|
||||
@@ -2274,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`).
|
||||
@@ -2308,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).
|
||||
@@ -2616,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();
|
||||
@@ -2765,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)]
|
||||
@@ -2838,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();
|
||||
@@ -2878,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);
|
||||
@@ -2891,7 +2810,6 @@ impl Snapshot {
|
||||
start: offset,
|
||||
end: offset + width,
|
||||
column,
|
||||
unknown,
|
||||
});
|
||||
offset += width;
|
||||
}
|
||||
@@ -3230,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 {
|
||||
@@ -3288,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"));
|
||||
@@ -3322,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,
|
||||
@@ -3334,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));
|
||||
@@ -3343,58 +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"
|
||||
);
|
||||
}
|
||||
/// A chunk is reduced to the one cell width that reads it, when it does.
|
||||
///
|
||||
/// Reducing to all four widths up front cost 405 us of a far terrain
|
||||
/// thread for every chunk the world sent, where at most one is read.
|
||||
#[test]
|
||||
fn a_chunk_is_reduced_only_to_the_width_that_reads_it() {
|
||||
let built = |reduced: &ReducedColumns| {
|
||||
reduced
|
||||
.levels
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, level)| level.get().is_some())
|
||||
.map(|(level, _)| 2usize << level)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let reduced = ReducedColumns::new(Arc::new(std::array::from_fn(|i| Column {
|
||||
surface: None,
|
||||
spans: vec![Span {
|
||||
min: 0,
|
||||
max: 4 + (i % 3) as i16,
|
||||
state: BlockStateId(1),
|
||||
biome: 0,
|
||||
light: 240,
|
||||
}]
|
||||
.into(),
|
||||
})));
|
||||
assert!(built(&reduced).is_empty(), "nothing is reduced unread");
|
||||
// A tile of full-detail cells reads the chunk's own columns.
|
||||
reduced.get(3, 5, 1);
|
||||
assert!(built(&reduced).is_empty());
|
||||
reduced.get(4, 8, 4);
|
||||
assert_eq!(built(&reduced), [4]);
|
||||
// Read again, and the reduction is the one already built.
|
||||
reduced.get(0, 0, 4);
|
||||
assert_eq!(built(&reduced), [4]);
|
||||
reduced.get(0, 0, 16);
|
||||
assert_eq!(built(&reduced), [4, 16]);
|
||||
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.
|
||||
@@ -4100,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
|
||||
@@ -4174,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.
|
||||
@@ -1405,8 +1382,7 @@ fn pack_coords(coords: u32) -> u16 {
|
||||
pub struct Mesher<'a> {
|
||||
/// CardinalLighting.NETHER: brighter up/down faces.
|
||||
pub nether_shading: bool,
|
||||
/// A shader pack lights the faces (its `oldLighting` off): no face shading in the colours,
|
||||
/// only the dimension's up-face brightness (`lighting::pack_flat`).
|
||||
/// A shader pack lights the faces (its `oldLighting` off): no face shading in the colours.
|
||||
pub shader_pack_lighting: bool,
|
||||
/// OptiFine/Iris separate ambient occlusion: vertex RGB does not include AO, and AO factor is passed in vertex alpha.
|
||||
pub separate_ao: bool,
|
||||
@@ -1543,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(
|
||||
@@ -1661,10 +1625,8 @@ impl Mesher<'_> {
|
||||
None => ao,
|
||||
};
|
||||
let shade_face = q.shade_direction_override.unwrap_or(q.facing);
|
||||
let directional = if !q.shade {
|
||||
let directional = if self.shader_pack_lighting || !q.shade {
|
||||
1.0
|
||||
} else if self.shader_pack_lighting {
|
||||
lighting::pack_flat(self.nether_shading)
|
||||
} else {
|
||||
lighting::cardinal(self.nether_shading, shade_face)
|
||||
};
|
||||
@@ -1808,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) {
|
||||
@@ -1817,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1862,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1875,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 {
|
||||
@@ -1896,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,
|
||||
@@ -1905,7 +1864,6 @@ impl Mesher<'_> {
|
||||
tile: TileLayer,
|
||||
mut v: [TerrainVertex; 4],
|
||||
by_sprite: bool,
|
||||
tint: Option<u32>,
|
||||
) {
|
||||
let TileLayer::Layer {
|
||||
layer,
|
||||
@@ -1939,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`
|
||||
|
||||
@@ -189,9 +189,8 @@ impl FluidFlowWorld for FlowView<'_> {
|
||||
}
|
||||
}
|
||||
/// Emits one fluid cell at section-relative `[x, y, z]`.
|
||||
/// `(nether, shader_pack_lighting, separate_ao)`: the Nether's face shading, only the up-face value
|
||||
/// when a shader pack lights the faces (`Mesher::shader_pack_lighting`), and whether to keep pure
|
||||
/// alpha for separate AO.
|
||||
/// `(nether, shader_pack_lighting, separate_ao)`: the Nether's face shading, none when a shader pack
|
||||
/// lights the faces (`Mesher::shader_pack_lighting`), and whether to keep pure alpha for separate AO.
|
||||
pub(super) fn tesselate(
|
||||
tables: &MeshTables,
|
||||
(nether, shader_pack_lighting, separate_ao): (bool, bool, bool),
|
||||
@@ -203,7 +202,7 @@ pub(super) fn tesselate(
|
||||
use horizon_assets::model::Direction as D;
|
||||
let cardinal = |d: D| {
|
||||
if shader_pack_lighting {
|
||||
lighting::pack_flat(nether)
|
||||
1.0
|
||||
} else {
|
||||
lighting::cardinal(nether, d)
|
||||
}
|
||||
@@ -448,10 +447,7 @@ pub(super) fn tesselate(
|
||||
Some(layer) if face_state.overlay => (layer, true),
|
||||
_ => (flow, false),
|
||||
};
|
||||
// Vanilla's side is the up value times the side's; a shader pack's has no side factor.
|
||||
let side = if shader_pack_lighting {
|
||||
1.0
|
||||
} else if matches!(d, Direction::North | Direction::South) {
|
||||
let side = if matches!(d, Direction::North | Direction::South) {
|
||||
cardinal(D::North)
|
||||
} else {
|
||||
cardinal(D::West)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,12 +19,6 @@ pub(super) fn cardinal(nether: bool, face: D) -> f32 {
|
||||
D::West | D::East => 0.6,
|
||||
}
|
||||
}
|
||||
/// A face's brightness when a shader pack lights the faces (`Mesher::shader_pack_lighting`): no
|
||||
/// directional shading, every face at the dimension's up-face value (1.0, the Nether's 0.9).
|
||||
/// Our probe pack's Iris 1.11.7 values fit this: 0.9 on every Nether face, 1.0 in the overworld.
|
||||
pub(super) fn pack_flat(nether: bool) -> f32 {
|
||||
cardinal(nether, D::Up)
|
||||
}
|
||||
fn normal(face: D) -> [i32; 3] {
|
||||
match face {
|
||||
D::Down => [0, -1, 0],
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -159,60 +159,6 @@ fn a_shader_pack_lights_the_faces_itself() {
|
||||
assert_eq!(colours(true).len(), 1);
|
||||
}
|
||||
|
||||
/// Run explicitly with prepared assets. With a shader pack lighting the faces, every face of a
|
||||
/// block and of a lava source takes the dimension's up-face brightness, as Iris 1.11.7 gives our
|
||||
/// probe pack: 0.9 in the Nether (`CardinalLighting.NETHER`'s up, 229 of 255), 1.0 elsewhere.
|
||||
#[test]
|
||||
#[ignore = "requires prepared 26.3 resources (HORIZON_MESH_TEST_RESOURCES or .local/horizon-client/26.3)"]
|
||||
fn a_shader_pack_lights_every_nether_face_like_an_up_face() {
|
||||
let root = std::env::var_os("HORIZON_MESH_TEST_RESOURCES")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|| {
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client/26.3")
|
||||
});
|
||||
let assets = Assets::load(&root).unwrap();
|
||||
let facts = BlockFacts::load(&root, assets.registry).unwrap();
|
||||
let biomes = Biomes::load(&root).unwrap();
|
||||
let render = assets.reload(&root).unwrap();
|
||||
let visibility = crate::models::load_visibility(&root, assets.registry).unwrap();
|
||||
let tables = MeshTables::new(&assets, &render, &facts, visibility.clone()).unwrap();
|
||||
let state = |name| assets.registry.default_state(name).unwrap().0;
|
||||
let mut chunk = Chunk::empty(0, 0);
|
||||
chunk.set(4, MIN_Y + 8, 4, state("minecraft:netherrack"));
|
||||
chunk.set(11, MIN_Y + 8, 11, state("minecraft:lava"));
|
||||
let mut region = [[None; 3]; 3];
|
||||
region[1][1] = Some(&chunk);
|
||||
let colours = |nether_shading| {
|
||||
let mut mesher = Mesher {
|
||||
nether_shading,
|
||||
shader_pack_lighting: true,
|
||||
separate_ao: false,
|
||||
assets: &assets,
|
||||
render: &render,
|
||||
tables: &tables,
|
||||
biomes: &biomes,
|
||||
visibility: visibility.clone(),
|
||||
zoom: BiomeZoom::new(0),
|
||||
blend_radius: 0,
|
||||
smooth_lighting: false,
|
||||
cutout_leaves: true,
|
||||
translucent_layers: &[],
|
||||
leaf_culling: LeafCullingMode::Off,
|
||||
};
|
||||
let mesh = mesher.mesh(®ion, 0);
|
||||
// Both blocks' six faces: 2 × 6 quads of 4 vertices.
|
||||
let all: Vec<_> = mesh.solid.iter().chain(&mesh.translucent).collect();
|
||||
assert!(all.len() >= 48, "{} vertices", all.len());
|
||||
let mut colours: Vec<u32> = all.iter().map(|v| v.color & 0xFF_FFFF).collect();
|
||||
colours.sort_unstable();
|
||||
colours.dedup();
|
||||
colours
|
||||
};
|
||||
assert_eq!(colours(true), [0xE5_E5E5]);
|
||||
assert_eq!(colours(false), [0xFF_FFFF]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires prepared 26.3 resources (HORIZON_MESH_TEST_RESOURCES or .local/horizon-client/26.3)"]
|
||||
fn separate_ao_passes_ao_in_alpha_without_darkening_rgb() {
|
||||
|
||||
Loaded 100 of 819 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user