Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba332a16d8 | ||
|
|
0601a8a436 | ||
|
|
6d0b4fcd2f |
No files matched your search
@@ -44,6 +44,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check out source
|
- name: Check out source
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # all of main: the version shown is VERSION + its commit count
|
||||||
- name: Restore compiler cache
|
- name: Restore compiler cache
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
@@ -90,6 +92,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check out source
|
- name: Check out source
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # all of main: the version shown is VERSION + its commit count
|
||||||
- name: Download Frame Home payload
|
- name: Download Frame Home payload
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -131,6 +135,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check out source
|
- name: Check out source
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # all of main: the version shown is VERSION + its commit count
|
||||||
- name: Restore compiler cache
|
- name: Restore compiler cache
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
@@ -182,8 +188,9 @@ jobs:
|
|||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: .apt-cache/archives
|
path: .apt-cache/archives
|
||||||
key: apt-ubuntu-24.04-arm64-${{ hashFiles('.github/workflows/build.yml') }}
|
# (its own key: the Linux arm64 Studio job caches other packages, and the first to save a key keeps it)
|
||||||
restore-keys: apt-ubuntu-24.04-arm64-
|
key: apt-frame-home-ubuntu-24.04-arm64-${{ hashFiles('.github/workflows/build.yml') }}
|
||||||
|
restore-keys: apt-frame-home-ubuntu-24.04-arm64-
|
||||||
- name: Install Frame Home build dependencies
|
- name: Install Frame Home build dependencies
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -194,6 +201,11 @@ jobs:
|
|||||||
sudo chown -R "$(id -u):$(id -g)" "$APT_CACHE_DIR/archives"
|
sudo chown -R "$(id -u):$(id -g)" "$APT_CACHE_DIR/archives"
|
||||||
- name: Build payload and installer
|
- name: Build payload and installer
|
||||||
run: sh scripts/build-frame-home.sh --ci
|
run: sh scripts/build-frame-home.sh --ci
|
||||||
|
- name: Tests (tests_loco, ctest)
|
||||||
|
run: |
|
||||||
|
cmake -S . -B build/frame-home-cmake-ci -DFRAME_TESTS=ON
|
||||||
|
cmake --build build/frame-home-cmake-ci --target frame-tests --parallel
|
||||||
|
ctest --test-dir build/frame-home-cmake-ci --output-on-failure
|
||||||
- name: Show compiler cache statistics
|
- name: Show compiler cache statistics
|
||||||
run: ccache --show-stats
|
run: ccache --show-stats
|
||||||
- name: Upload Frame Home payload
|
- name: Upload Frame Home payload
|
||||||
@@ -225,7 +237,8 @@ jobs:
|
|||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
steps:
|
steps:
|
||||||
# Every push to main is also a numbered release: VERSION (major.minor) and the number of commits on main.
|
# Every push to main is also a numbered release: VERSION (major.minor) and the commits since VERSION last
|
||||||
|
# changed, so the commit that bumps it is .0 (cmake/frame_version.cmake works out the same for the apps).
|
||||||
- name: Check out history (for the version number)
|
- name: Check out history (for the version number)
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -235,7 +248,8 @@ jobs:
|
|||||||
- name: Version
|
- name: Version
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
run: |
|
run: |
|
||||||
version="v$(tr -d ' \r\n' < src/VERSION).$(git -C src rev-list --count HEAD)"
|
bump=$(git -C src log -1 --format=%H -- VERSION)
|
||||||
|
version="v$(tr -d ' \r\n' < src/VERSION).$(git -C src rev-list --count "$bump..HEAD")"
|
||||||
echo "VERSION_TAG=$version" >> "$GITHUB_ENV"
|
echo "VERSION_TAG=$version" >> "$GITHUB_ENV"
|
||||||
echo "$version"
|
echo "$version"
|
||||||
- name: Download Studio build artifacts
|
- name: Download Studio build artifacts
|
||||||
@@ -302,9 +316,12 @@ jobs:
|
|||||||
# and the files in this release.
|
# and the files in this release.
|
||||||
- name: latest.json
|
- name: latest.json
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail # (a payload file missing must fail here, not hash what's left)
|
||||||
home=$(cd "$RUNNER_TEMP/frame-home-payload" && sha256sum bin/frame-home bin/frame-switcher bin/fenv-tool layer/libframe_home_layer.so layer/env_ps.spv | cut -c1-64 | sha256sum | cut -c1-64)
|
home=$(cd "$RUNNER_TEMP/frame-home-payload" && sha256sum bin/frame-home bin/frame-switcher bin/fenv-tool layer/libframe_home_layer.so layer/env_ps.spv | cut -c1-64 | sha256sum | cut -c1-64)
|
||||||
files=$(ls dist | python3 -c 'import json, sys; print(json.dumps([l.strip() for l in sys.stdin if l.strip()]))')
|
files=$(ls dist | python3 -c 'import json, sys; print(json.dumps([l.strip() for l in sys.stdin if l.strip()]))')
|
||||||
printf '{"build": "%s", "home": "%s", "date": "%s", "files": %s}\n' "$GITHUB_SHA" "$home" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$files" > dist/latest.json
|
# Each file's SHA-256 as "sha256-<file>": Frame Studio checks its download against it before installing.
|
||||||
|
hashes=$(cd dist && for f in *; do if [ -f "$f" ] && [ "$f" != latest.json ]; then printf ', "sha256-%s": "%s"' "$f" "$(sha256sum "$f" | cut -c1-64)"; fi; done)
|
||||||
|
printf '{"build": "%s", "home": "%s", "date": "%s", "files": %s%s}\n' "$GITHUB_SHA" "$home" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$files" "$hashes" > dist/latest.json
|
||||||
cat dist/latest.json
|
cat dist/latest.json
|
||||||
- name: Attest Studio release archives
|
- name: Attest Studio release archives
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest-build-provenance@v2
|
||||||
@@ -316,21 +333,53 @@ jobs:
|
|||||||
subject-path: dist/FrameHome-installer.zip
|
subject-path: dist/FrameHome-installer.zip
|
||||||
- name: Version release (tag v*)
|
- name: Version release (tag v*)
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/v') && !contains(needs.*.result, 'failure') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/v') && !contains(needs.*.result, 'failure') }}
|
||||||
run: gh release create "${{ github.ref_name }}" dist/* --repo "$REPO" --title "Frame Studio ${{ github.ref_name }}" --generate-notes
|
run: |
|
||||||
|
# A version release has every platform: this job doesn't wait for macOS (see above), so without its build
|
||||||
|
# here it stops; run this job again once the macOS build has finished.
|
||||||
|
if ! ls dist/FrameStudio-macos-*.zip > /dev/null 2>&1; then
|
||||||
|
echo "::error::the macOS build isn't here yet: re-run this job once it has finished"; exit 1
|
||||||
|
fi
|
||||||
|
gh release create "${{ github.ref_name }}" dist/* --repo "$REPO" --title "Frame Studio ${{ github.ref_name }}" --generate-notes
|
||||||
# The numbered release (push to main): GitHub's "Latest release", with the changes since the previous numbered
|
# The numbered release (push to main): GitHub's "Latest release", with the changes since the previous numbered
|
||||||
# one. Only when no build failed; a re-run of the same commit finds it there and leaves it.
|
# one. Only when no build failed; a re-run of the same commit finds it there and leaves it.
|
||||||
- name: Numbered release (push to main)
|
- name: Numbered release (push to main)
|
||||||
if: ${{ github.ref == 'refs/heads/main' && !contains(needs.*.result, 'failure') }}
|
if: ${{ github.ref == 'refs/heads/main' && !contains(needs.*.result, 'failure') }}
|
||||||
run: |
|
run: |
|
||||||
if gh release view "$VERSION_TAG" --repo "$REPO" > /dev/null 2>&1; then echo "$VERSION_TAG is already released"; exit 0; fi
|
if gh release view "$VERSION_TAG" --repo "$REPO" > /dev/null 2>&1; then
|
||||||
|
# A re-run of this commit: done already. The tag on another commit (pushed by hand, the same name as this
|
||||||
|
# number): say so rather than skip, or this push would never be released.
|
||||||
|
if [ "$(git -C src rev-list -n 1 "$VERSION_TAG" 2>/dev/null)" = "$GITHUB_SHA" ]; then echo "$VERSION_TAG is already released"; exit 0; fi
|
||||||
|
echo "::error::$VERSION_TAG already exists on another commit (a tag made by hand?): bump VERSION or remove that tag"; exit 1
|
||||||
|
fi
|
||||||
previous=$(gh release list --repo "$REPO" --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // empty')
|
previous=$(gh release list --repo "$REPO" --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // empty')
|
||||||
|
# The notes open with CHANGELOG.md's top section when the changelog changed since the previous release
|
||||||
|
# (whatever number this push gets), then GitHub's list of changes. They're also what the Discord's #releases
|
||||||
|
# channel gets (the repository's release webhook).
|
||||||
|
: > "$RUNNER_TEMP/notes.md"
|
||||||
|
if [ -z "$previous" ] || ! git -C src diff --quiet "$previous" HEAD -- CHANGELOG.md 2>/dev/null; then
|
||||||
|
awk '/^## / { if (on) exit; on = 1; next } on' src/CHANGELOG.md > "$RUNNER_TEMP/notes.md"
|
||||||
|
fi
|
||||||
|
cat "$RUNNER_TEMP/notes.md"
|
||||||
gh release create "$VERSION_TAG" dist/* --repo "$REPO" --target "${{ github.sha }}" --latest \
|
gh release create "$VERSION_TAG" dist/* --repo "$REPO" --target "${{ github.sha }}" --latest \
|
||||||
--title "Frame Studio $VERSION_TAG" --generate-notes ${previous:+--notes-start-tag "$previous"}
|
--title "Frame Studio $VERSION_TAG" --notes-file "$RUNNER_TEMP/notes.md" --generate-notes ${previous:+--notes-start-tag "$previous"}
|
||||||
# The rolling "latest" pre-release: where installed Studios and headsets look for updates (latest.json).
|
# The rolling "latest" pre-release: where installed Studios and headsets look for updates (latest.json).
|
||||||
|
# Updated in place, never deleted first: a run cancelled midway (a newer push) or an update check meanwhile
|
||||||
|
# always finds a whole release. The files go up first and latest.json last, so it never names files that aren't
|
||||||
|
# there yet; then what this build no longer makes is removed and the tag moved to this commit.
|
||||||
- name: Latest build (push to main)
|
- name: Latest build (push to main)
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
run: |
|
run: |
|
||||||
gh release delete latest --repo "$REPO" --yes --cleanup-tag 2>/dev/null || true
|
set -euo pipefail
|
||||||
gh release create latest dist/* --repo "$REPO" --prerelease --target "${{ github.sha }}" \
|
title="Latest build (${GITHUB_SHA::7})"
|
||||||
--title "Latest build (${GITHUB_SHA::7})" \
|
notes="Built automatically from ${{ github.sha }} on $(date -u '+%Y-%m-%d %H:%M UTC'). Replaced by every push to main."
|
||||||
--notes "Built automatically from ${{ github.sha }} on $(date -u '+%Y-%m-%d %H:%M UTC'). Replaced by every push to main."
|
if ! gh release view latest --repo "$REPO" > /dev/null 2>&1; then
|
||||||
|
gh release create latest dist/* --repo "$REPO" --prerelease --target "${{ github.sha }}" --title "$title" --notes "$notes"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
find dist -maxdepth 1 -type f ! -name latest.json -print0 | xargs -0 gh release upload latest --repo "$REPO" --clobber
|
||||||
|
gh release upload latest dist/latest.json --repo "$REPO" --clobber
|
||||||
|
for asset in $(gh release view latest --repo "$REPO" --json assets --jq '.assets[].name'); do
|
||||||
|
[ -e "dist/$asset" ] || gh release delete-asset latest "$asset" --repo "$REPO" --yes
|
||||||
|
done
|
||||||
|
gh api -X PATCH "repos/$REPO/git/refs/tags/latest" -f sha="$GITHUB_SHA" -F force=true > /dev/null
|
||||||
|
gh release edit latest --repo "$REPO" --title "$title" --notes "$notes"
|
||||||
@@ -1,24 +1,65 @@
|
|||||||
# build output
|
# build output
|
||||||
/build*/
|
/build*/
|
||||||
/dist/
|
/dist/
|
||||||
|
/out/
|
||||||
/frame/payload/bin/
|
/frame/payload/bin/
|
||||||
/frame/payload/layer/libframe_home_layer.so
|
/frame/payload/layer/libframe_home_layer.so
|
||||||
/frame/payload/layer/env_ps.spv
|
/frame/payload/layer/env_ps.spv
|
||||||
/frame/payload/SOURCE
|
/frame/payload/SOURCE
|
||||||
studio/build*/
|
studio/build*/
|
||||||
tools/*/build*/
|
tools/*/build*/
|
||||||
|
CMakeUserPresets.json
|
||||||
|
CMakeCache.txt
|
||||||
|
CMakeFiles/
|
||||||
|
compile_commands.json
|
||||||
|
.cache/
|
||||||
|
.ccache/
|
||||||
|
.apt-cache/
|
||||||
|
# compiler and linker leftovers outside a build folder
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.a
|
||||||
|
*.lib
|
||||||
|
*.so
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.pdb
|
||||||
|
*.ilk
|
||||||
|
*.exp
|
||||||
|
*.dSYM/
|
||||||
# editor / OS clutter
|
# editor / OS clutter
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.vs/
|
||||||
.idea/
|
.idea/
|
||||||
|
.fleet/
|
||||||
*.swp
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
# never commit homes, environments or keys
|
desktop.ini
|
||||||
|
# scripts' scratch
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.part
|
||||||
|
*.orig
|
||||||
|
*.rej
|
||||||
|
# never commit homes, environments, bundles or keys
|
||||||
*.apk
|
*.apk
|
||||||
*.fenv
|
*.fenv
|
||||||
|
*.fbundle
|
||||||
*.pem
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
id_rsa*
|
id_rsa*
|
||||||
id_ed25519*
|
id_ed25519*
|
||||||
|
.env
|
||||||
*Zone.Identifier
|
*Zone.Identifier
|
||||||
# astcenc's own test framework (not built here)
|
# astcenc's own test framework (not built here)
|
||||||
third_party/astcenc/Source/GoogleTest/
|
third_party/astcenc/Source/GoogleTest/
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Newest first. When this file changed since the last release, the next release's notes start with the top section,
|
||||||
|
and the notes post to the Discord's #releases channel: add a section at the top in the push you want announced.
|
||||||
|
The version: VERSION (major.minor) and the commits since VERSION changed, so the commit that bumps VERSION is .0.
|
||||||
|
|
||||||
|
## v0.2.0
|
||||||
|
|
||||||
|
### Frame Studio
|
||||||
|
- Gaussian splats: open or add 3DGS captures (.ply, .splat, .spz from Polycam, Luma, Scaniverse, Postshot...) as splat objects: listed in the Outliner, picked in the view, moved, rotated and scaled with the gizmo (undoable), hidden or deleted, with a headset budget in Properties (over 400,000 may need Half frame rate; over a million is asked about on Save and Send). Saved into the .fenv as .spz files.
|
||||||
|
- Newer .spz splat files (version 4, the kind Niantic's current spz tools save) open too.
|
||||||
|
- A saved environment records which Frame Studio made it, shown in Properties > Environment, on the start screen and by fenv-tool info.
|
||||||
|
- The version is shown at the bottom left (and in the Help menu), the same number as the release.
|
||||||
|
- Properties tabs are labelled: each shows its icon and name across the top, the open one underlined.
|
||||||
|
- The open tab of an area (Properties, Outliner) and the open workspace are underlined, so you can tell which is which.
|
||||||
|
- Buttons in the World, Environment, Bundle and Headset tabs sit in rows at their own size, as in the Object tab,
|
||||||
|
instead of stretching across the panel.
|
||||||
|
- Moving things: click to select, then drag the selected item to move it. Clicking something no longer nudges it
|
||||||
|
(or a large mesh behind it) when the mouse moves a little.
|
||||||
|
- Outliner folders have colours, one for each level of nesting, instead of plain grey.
|
||||||
|
- Removed "Stop at Meta's invisible walls" from the World tab.
|
||||||
|
- Help › Join the Discord, and a Discord button on the start screen: https://discord.gg/yJDn59ErgK
|
||||||
|
- New app icon (a roof over a headset) on Windows, macOS and Linux.
|
||||||
|
- Opens STL (3D printing, CAD) and PLY (scans, photogrammetry) models, alongside glTF, FBX and OBJ.
|
||||||
|
- PLY meshes show their own vertex colours (with see-through alpha when the file has it), in Studio and on the
|
||||||
|
headset. glTF models' vertex colours show too. Models without colours look the same as before.
|
||||||
|
- Environment tab › Sky: pick a 360° panorama (.hdr, .jpg or .png, twice as wide as it is tall) to use as the sky
|
||||||
|
instead of the home's own. Remove puts the home's sky back. It's saved in the .fenv, so Frame Home shows it as it is.
|
||||||
|
- Pictures can also be TGA, GIF (its first frame) and PSD (the flattened image).
|
||||||
|
- Meta homes look right in more places (Studio and Frame Home):
|
||||||
|
- Textures stored uncompressed (8-bit or half-float) load properly. They came out as streaky noise or pixelated
|
||||||
|
(Focused) or black (Horror's lanterns and bats).
|
||||||
|
- A vista's meshes whose shaders ship in its footprint draw when the footprint is in the same folder (Horror's
|
||||||
|
front garden, a Calming boulder).
|
||||||
|
- Meshes with no material in the scene use the one they were exported with (Horror's sky and candle flame,
|
||||||
|
Oceanarium's light rays and surface waves).
|
||||||
|
- Vista (Cascadia)'s floors are matte again instead of mirror-like: its reflection map keeps the blurred levels Meta
|
||||||
|
made for rough surfaces. Convert an environment made from it before this again to get the fix.
|
||||||
|
- Things placed inside another placed group sit where they belong (Haven2025's teleport spots by each seat were
|
||||||
|
all in the middle of the room).
|
||||||
|
- Environments can say which newer features they need; a Frame Studio or Frame Home too old for one says so instead of
|
||||||
|
showing it wrong.
|
||||||
|
- Fixed a crash in the Bundle tab: taking an environment out of a bundle (✕) could close Frame Studio, because its
|
||||||
|
picture was freed while it was still being drawn.
|
||||||
|
- Opening an environment that already has a saved walkable area no longer marks it as changed: the area is shown
|
||||||
|
exactly as saved until you edit something, so just opening and closing it doesn't ask you to save.
|
||||||
|
- Dragging the gizmo can no longer leave an object, a splat or a world item broken (gone from view) when the gizmo
|
||||||
|
gives a bad result for a moment, such as a rotate ring dragged edge-on.
|
||||||
|
- Placed surfaces far from the middle of the world join up as reliably as ones near it.
|
||||||
|
- Updates are checked before they're installed: a download that was cut off, damaged or not the real file is refused
|
||||||
|
and nothing is changed. The files an update sets aside are cleaned up even when the old copy was still running.
|
||||||
|
- An environment named "." or ".." (typed, or in a .fenv someone sent) could make the 5-minute auto-save delete
|
||||||
|
environments in My Environments. Names like that, and Windows device names (CON, NUL...), are now changed into
|
||||||
|
safe file names.
|
||||||
|
- Saving an environment opened from inside a bundle saves it into My Environments; before, Save wrote into a scratch
|
||||||
|
copy that was emptied the next time a bundle member was opened.
|
||||||
|
- Object edits are found again when the environment's name, or your Windows user name, has letters like é or ü
|
||||||
|
(they were silently missing, and saving then dropped them), and the edits file is written whole or not at all.
|
||||||
|
- An environment whose splat placement or a carried-over setting became "infinity" or "not a number" could never be
|
||||||
|
opened again after saving: those values are now saved as safe ones. Typing such a number into a Location, Rotation
|
||||||
|
or Scale field leaves the value as it was.
|
||||||
|
- Saving a bundle over an existing one no longer loses both when the old file can't be replaced, and a bundle can't
|
||||||
|
be saved over one of its own environments.
|
||||||
|
- A bundle with many environments no longer leaves the 3D view blank after a resize.
|
||||||
|
- Running out of memory opening a huge model, or saving a huge home, shows a message instead of closing Frame Studio.
|
||||||
|
- An OBJ model whose .mtl file is missing opens (without its materials) instead of not at all.
|
||||||
|
- Damaged or hostile files are handled safely: .spz files that unpack to gigabytes, PLY files with impossible
|
||||||
|
counts or indices, glTF files with a broken index count, oversized pictures in a .fenv or bundle.
|
||||||
|
- No more freeze after an edit: the walkable area is worked out in the background after you move, delete or undo
|
||||||
|
something. Big homes used to stop the window for up to 1.5 seconds each time (Hand Tutorial 4).
|
||||||
|
- The 3D view follows the mouse a frame sooner: orbiting, panning, flying and gizmo drags show in the same frame,
|
||||||
|
instead of one refresh behind the rest of the window.
|
||||||
|
- Frames come at an even pace. On Windows they're timed to the display's refreshes, which cuts the wait between
|
||||||
|
your input and the screen.
|
||||||
|
- Selecting a big mesh no longer holds up a frame for as long (its outline builds about twice as fast).
|
||||||
|
- The Bundle tab costs a third as much time per frame.
|
||||||
|
- Two Frame Studio windows can no longer install an update at the same time (the second says another is installing
|
||||||
|
it), and an update cut off halfway, by a crash or a power cut, is finished or undone the next time Frame Studio
|
||||||
|
starts instead of leaving a mix of old and new files.
|
||||||
|
- Closing an environment in the same moment the 3D view is resized no longer shows a frame of garbage in the view.
|
||||||
|
- An update stopped at the worst moment (a crash or power cut while Frame Studio swaps in the new program) can no
|
||||||
|
longer leave no program to start: the old program is replaced by the new one in a single step.
|
||||||
|
- On Windows, homes, models and environments in folders over 260 characters deep open, import and save (where
|
||||||
|
Windows' long paths setting is on).
|
||||||
|
- With letters like é or ü in your Windows user name, compiled shaders are now kept between runs instead of being
|
||||||
|
compiled again every time Frame Studio starts.
|
||||||
|
- glTF models whose index data doesn't start on an even byte are read safely.
|
||||||
|
- Copy and paste: Ctrl+C, Ctrl+X (cut), Ctrl+V and Ctrl+D (duplicate), also in the Edit menu and the right-click
|
||||||
|
menus, for objects, splat objects, spawns, seats, teleport hotspots, surfaces, invisible walls and gravity zones.
|
||||||
|
Paste puts the copy where the view is looking (at the same height over the floor), or right beside the original if
|
||||||
|
you're looking at it; Duplicate always puts it beside. It works across environments opened in the same Frame Studio,
|
||||||
|
and world items also paste into another Frame Studio window. One Ctrl+Z undoes it. Shortcuts do nothing while
|
||||||
|
you're typing in a text box.
|
||||||
|
- A new seat's "stand up here" spot is now in front of it (the way its arrow points) instead of behind it.
|
||||||
|
|
||||||
|
### Frame Home
|
||||||
|
- Sitting down turns you to face the way the seat faces (its arrow in Frame Studio), wherever you were looking when
|
||||||
|
you pointed at it or teleported onto it. Before, you kept facing the way you were, so you could end up sitting
|
||||||
|
sideways or backwards.
|
||||||
|
- Gaussian splats: environments with splat captures draw them (sorted back to front as you move, both eyes in one pass); a .fenv that has splats needs this Frame Home (older ones refuse it rather than show it without them). The Environments panel shows which Frame Studio made each environment.
|
||||||
|
- The Debug page scrolls (mouse wheel and drag).
|
||||||
|
- Settings shows Frame Home's version at the bottom.
|
||||||
|
- The dashboard tab has the new icon.
|
||||||
|
- An environment made by a newer Frame Studio says "Needs a newer Frame Home" on its card.
|
||||||
|
- Smoother movement at any frame rate: walking and turning step by exactly one display frame's time per frame, so the
|
||||||
|
speed no longer wobbles with the frame timing.
|
||||||
|
- Half frame rate: frames are timed to the display (every other refresh, not a loose sleep), the frame shown twice is
|
||||||
|
turned on with your smooth turn instead of standing still, and while you walk, turn, dash or blink every frame is
|
||||||
|
drawn (when the home is light enough for that).
|
||||||
|
- A frame that comes late, or is shown twice, follows your smooth turning and (approximately) your walking, instead of
|
||||||
|
lagging behind for a refresh.
|
||||||
|
- Blink teleports fade to black and back in the system environment (they were an unfaded freeze, then a jump).
|
||||||
|
- Teleporting near chairs and stools goes where you aim: the arc takes a seat only when it comes down onto it, not when
|
||||||
|
it passes a stool on the way or lands beside a desk (Haven2025 kept sitting you in the desk chair). Every spot of
|
||||||
|
Haven2025's floor can be teleported to.
|
||||||
|
- Sitting down no longer throws you straight back out: the stick has to come back to the middle before pushing it
|
||||||
|
stands you up.
|
||||||
|
- While a game is on screen the home stops completely (no drawing, no sound, nothing read from the controllers), so
|
||||||
|
the GPU and CPU are all the game's. It keeps going while a game loads, and is back the moment the game closes.
|
||||||
|
- The Hand Tutorial homes draw faster: their hundreds of look-alike plants (see-through ones too) and other matching
|
||||||
|
parts are drawn together instead of one at a time. They look exactly the same. Hand Tutorial 7 takes about 20% less
|
||||||
|
GPU time a frame, the other Hand Tutorials 4 to 9% less.
|
||||||
|
- Updates are checked before they're installed: the downloaded installer must match the SHA-256 the release lists, so
|
||||||
|
a download cut short or damaged (or one the release lists no SHA-256 for) is never installed, and a failed update no longer says
|
||||||
|
"Frame Home is updated" the next time the panel starts. The installer is removed from the headset once unpacked.
|
||||||
|
- Installing never leaves new Frame Home programs beside an old compositor layer: everything is copied first, and a
|
||||||
|
full disk changes nothing. Installing from a computer reports an environment that didn't copy, and doesn't make a
|
||||||
|
missing one the environment shown.
|
||||||
|
- A damaged environment file can no longer hang or crash Frame Home while loading: walls and floors with absurd or
|
||||||
|
broken coordinates, shaders with types that contain themselves, texture data shorter than it claims and settings
|
||||||
|
that aren't numbers are caught, and a loader that runs out of memory fails that home instead of closing Frame Home.
|
||||||
|
- The system environment: no more single-frame flashes of Aurora (or of the home over passthrough) from a clock race
|
||||||
|
in the compositor layer, a frame carried on during a fast dash no longer swings sideways, and the layer refuses a
|
||||||
|
damaged hand-over instead of crashing the compositor.
|
||||||
|
- A tracking glitch that reports a broken (not-a-number) head pose is skipped for that frame instead of being fed to
|
||||||
|
walking and collisions.
|
||||||
|
- The system environment, while the home changes size (a resolution or foveation setting): the compositor no longer
|
||||||
|
shows a frame turned to the wrong angle (or half of one frame's angle and half of the next), and no longer redraws
|
||||||
|
with pictures it had already let go of. An older Frame Home with this compositor layer, or the other way round,
|
||||||
|
still works.
|
||||||
|
- A home with damaged shaders leaves out the parts that use them (and says so) instead of handing broken code to the
|
||||||
|
graphics driver. The check takes about 5 ms for all 172 of Haven's shaders.
|
||||||
|
- A home shown for more than 30 days is no longer deleted from the cache while it's on screen when another home is
|
||||||
|
opened (Frame Home and Frame Studio): a home still in use anywhere is skipped.
|
||||||
|
- Lower latency in the system environment: the head pose is read again just before each frame goes to the GPU (after
|
||||||
|
its drawing commands are made and the frame before is finished), so what you see is up to a millisecond or more
|
||||||
|
newer, and several milliseconds newer when the GPU is busy.
|
||||||
|
- Desert Terrace and other homes with cut-out foliage draw it a little faster (about 2% less GPU time in Desert
|
||||||
|
Terrace): the leaves' second pass tests depth before shading. They look exactly the same.
|
||||||
|
- The system environment: each frame Frame Home draws is now handed over to the compositor properly before the
|
||||||
|
compositor reads it (Vulkan's hand-over between programs), so a GPU driver that keeps image data in caches or
|
||||||
|
compressed can't show a stale or garbled frame. It costs no measurable GPU time, and an older Frame Home with this
|
||||||
|
compositor layer, or the other way round, still works.
|
||||||
|
- With foveation, the pass that smooths the low-detail edges now waits until its picture is ready to be drawn into
|
||||||
|
(a missing GPU wait that could, in principle, leave stray pixels).
|
||||||
@@ -70,6 +70,15 @@ add_library(lzma STATIC "${TP}/lzma/LzmaEnc.c" "${TP}/lzma/LzmaDec.c" "${TP}/lzm
|
|||||||
"${TP}/lzma/LzFindMt.c" "${TP}/lzma/Threads.c" "${TP}/lzma/CpuArch.c")
|
"${TP}/lzma/LzFindMt.c" "${TP}/lzma/Threads.c" "${TP}/lzma/CpuArch.c")
|
||||||
target_include_directories(lzma PUBLIC "${TP}/lzma")
|
target_include_directories(lzma PUBLIC "${TP}/lzma")
|
||||||
|
|
||||||
|
# zstd 1.5.6 (BSD), decompression only: version 4 .spz splat files (src/splat_import.cpp). Single-threaded, plain C
|
||||||
|
# (no amd64 assembly), so it builds the same everywhere.
|
||||||
|
add_library(zstd STATIC "${TP}/zstd/common/debug.c" "${TP}/zstd/common/entropy_common.c"
|
||||||
|
"${TP}/zstd/common/error_private.c" "${TP}/zstd/common/fse_decompress.c" "${TP}/zstd/common/xxhash.c"
|
||||||
|
"${TP}/zstd/common/zstd_common.c" "${TP}/zstd/decompress/huf_decompress.c" "${TP}/zstd/decompress/zstd_ddict.c"
|
||||||
|
"${TP}/zstd/decompress/zstd_decompress.c" "${TP}/zstd/decompress/zstd_decompress_block.c")
|
||||||
|
target_include_directories(zstd PUBLIC "${TP}/zstd")
|
||||||
|
target_compile_definitions(zstd PRIVATE ZSTD_DISABLE_ASM)
|
||||||
|
|
||||||
# The home loader: HSL / Clay / glTF homes, .fenv environments (baked scenes), Blender / Unity models.
|
# The home loader: HSL / Clay / glTF homes, .fenv environments (baked scenes), Blender / Unity models.
|
||||||
add_library(frame-scene STATIC
|
add_library(frame-scene STATIC
|
||||||
src/hsl_scene.cpp
|
src/hsl_scene.cpp
|
||||||
@@ -77,6 +86,8 @@ add_library(frame-scene STATIC
|
|||||||
src/anim_bake.cpp
|
src/anim_bake.cpp
|
||||||
src/fenv_lzma.cpp
|
src/fenv_lzma.cpp
|
||||||
src/model_import.cpp
|
src/model_import.cpp
|
||||||
|
src/splat_import.cpp
|
||||||
|
src/spv_opt.cpp
|
||||||
"${TP}/ufbx/ufbx.c"
|
"${TP}/ufbx/ufbx.c"
|
||||||
"${TP}/qhe/cook/hzanim_acl.cpp"
|
"${TP}/qhe/cook/hzanim_acl.cpp"
|
||||||
"${TP}/stb/stb_image_impl.c")
|
"${TP}/stb/stb_image_impl.c")
|
||||||
@@ -93,7 +104,15 @@ target_include_directories(frame-scene PUBLIC
|
|||||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||||
set_source_files_properties("${TP}/qhe/cook/hzanim_acl.cpp" PROPERTIES COMPILE_OPTIONS "-flax-vector-conversions")
|
set_source_files_properties("${TP}/qhe/cook/hzanim_acl.cpp" PROPERTIES COMPILE_OPTIONS "-flax-vector-conversions")
|
||||||
endif()
|
endif()
|
||||||
target_link_libraries(frame-scene PUBLIC miniz lzma smolv ${ASTCENC_LIB} pthread)
|
target_link_libraries(frame-scene PUBLIC miniz lzma zstd smolv ${ASTCENC_LIB} pthread)
|
||||||
|
# The SPIRV-Tools optimizer for src/spv_opt.h (FH_SPV_OPT=1 at run time). Off: on the Frame it made no shader any
|
||||||
|
# cheaper (Turnip's compiler already does what it does), and it is a large library to build everywhere.
|
||||||
|
option(FRAME_SPV_OPT "Build in the SPIRV-Tools shader optimizer (third_party/spirv-tools)" OFF)
|
||||||
|
if(FRAME_SPV_OPT)
|
||||||
|
add_subdirectory("${TP}/spirv-tools" spirv-tools EXCLUDE_FROM_ALL)
|
||||||
|
target_link_libraries(frame-scene PUBLIC spirv-tools-opt)
|
||||||
|
target_compile_definitions(frame-scene PUBLIC FH_HAVE_SPV_OPT)
|
||||||
|
endif()
|
||||||
# libdeflate when the system has it (the Frame, CI): APK scene.zips inflate several times faster than with miniz. Its
|
# libdeflate when the system has it (the Frame, CI): APK scene.zips inflate several times faster than with miniz. Its
|
||||||
# static library first, so a release build doesn't need it installed on the headset.
|
# static library first, so a release build doesn't need it installed on the headset.
|
||||||
find_library(LIBDEFLATE_LIB NAMES libdeflate.a deflate)
|
find_library(LIBDEFLATE_LIB NAMES libdeflate.a deflate)
|
||||||
@@ -122,6 +141,7 @@ target_link_libraries(opusfile PUBLIC opus ogg)
|
|||||||
add_executable(frame-home
|
add_executable(frame-home
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/hsl_renderer.cpp
|
src/hsl_renderer.cpp
|
||||||
|
src/hsl_renderer_splat.cpp
|
||||||
src/panorama.cpp
|
src/panorama.cpp
|
||||||
"${TP}/stb/stb_vorbis_impl.c"
|
"${TP}/stb/stb_vorbis_impl.c"
|
||||||
"${TP}/miniaudio/miniaudio_impl.c")
|
"${TP}/miniaudio/miniaudio_impl.c")
|
||||||
@@ -149,6 +169,11 @@ target_link_libraries(fenv-tool PRIVATE frame-scene)
|
|||||||
# frame-switcher: the SteamVR dashboard panel that switches the environment.
|
# frame-switcher: the SteamVR dashboard panel that switches the environment.
|
||||||
add_executable(frame-switcher src/switcher.cpp)
|
add_executable(frame-switcher src/switcher.cpp)
|
||||||
target_include_directories(frame-switcher PRIVATE src "${TP}/stb" "${TP}/qhe" "${TP}/miniz" "${TP}/openvr")
|
target_include_directories(frame-switcher PRIVATE src "${TP}/stb" "${TP}/qhe" "${TP}/miniz" "${TP}/openvr")
|
||||||
|
# Its Settings page shows the version: major.minor only (with the installed files' hash as the build), so a commit
|
||||||
|
# that doesn't touch Frame Home leaves its binaries, and the hash updates go by, the same.
|
||||||
|
file(READ "${CMAKE_SOURCE_DIR}/VERSION" FRAME_VERSION_BASE)
|
||||||
|
string(STRIP "${FRAME_VERSION_BASE}" FRAME_VERSION_BASE)
|
||||||
|
target_compile_definitions(frame-switcher PRIVATE FRAME_VERSION_BASE="${FRAME_VERSION_BASE}")
|
||||||
if(FRAME_HOME_PC)
|
if(FRAME_HOME_PC)
|
||||||
target_include_directories(frame-switcher PRIVATE "${TP}/vulkan-headers/include")
|
target_include_directories(frame-switcher PRIVATE "${TP}/vulkan-headers/include")
|
||||||
endif()
|
endif()
|
||||||
@@ -160,3 +185,33 @@ endif()
|
|||||||
if(NOT FRAME_HOME_PC)
|
if(NOT FRAME_HOME_PC)
|
||||||
set_target_properties(frame-switcher PROPERTIES BUILD_RPATH "${OPENVR_LIB_DIR}" INSTALL_RPATH "${OPENVR_LIB_DIR}")
|
set_target_properties(frame-switcher PROPERTIES BUILD_RPATH "${OPENVR_LIB_DIR}" INSTALL_RPATH "${OPENVR_LIB_DIR}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Tests (tests_loco/): -DFRAME_TESTS=ON, then build frame-tests and run ctest. Off by default: release builds skip them.
|
||||||
|
option(FRAME_TESTS "Build the tests in tests_loco (cmake --build <dir> --target frame-tests; ctest --test-dir <dir>)" OFF)
|
||||||
|
if(FRAME_TESTS)
|
||||||
|
enable_testing()
|
||||||
|
add_executable(loco_test tests_loco/loco_test.cpp) # locomotion: floors, seats, stairs, gravity
|
||||||
|
add_executable(clip_test tests_loco/clip_test.cpp) # locomotion never puts eyes in walls or feet through floors
|
||||||
|
foreach(t loco_test clip_test)
|
||||||
|
target_include_directories(${t} PRIVATE "${TP}/openvr")
|
||||||
|
target_link_libraries(${t} PRIVATE frame-scene ${FH_OPENVR})
|
||||||
|
if(NOT FRAME_HOME_PC)
|
||||||
|
set_target_properties(${t} PROPERTIES BUILD_RPATH "${OPENVR_LIB_DIR}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
add_executable(spv_check_test tests_loco/spv_check_test.cpp) # the shader checker, on every shader Frame Home ships
|
||||||
|
target_include_directories(spv_check_test PRIVATE src)
|
||||||
|
add_test(NAME loco COMMAND loco_test)
|
||||||
|
add_test(NAME clip COMMAND clip_test)
|
||||||
|
add_test(NAME spv_check COMMAND spv_check_test)
|
||||||
|
set(FRAME_TEST_TARGETS loco_test clip_test spv_check_test)
|
||||||
|
if(NOT WIN32) # the Frame Home <-> compositor layer shared state (Linux: memfd, the layer itself)
|
||||||
|
add_executable(env_share_test tests_loco/env_share_test.cpp)
|
||||||
|
target_include_directories(env_share_test PRIVATE src "${TP}/vulkan-headers/include")
|
||||||
|
target_link_libraries(env_share_test PRIVATE pthread)
|
||||||
|
add_test(NAME env_share_thread COMMAND env_share_test 2 thread)
|
||||||
|
add_test(NAME env_share_process COMMAND env_share_test 2 process)
|
||||||
|
list(APPEND FRAME_TEST_TARGETS env_share_test)
|
||||||
|
endif()
|
||||||
|
add_custom_target(frame-tests DEPENDS ${FRAME_TEST_TARGETS})
|
||||||
|
endif()
|
||||||
@@ -1,45 +1,80 @@
|
|||||||
# Frame Studio and Frame Home
|
# Frame Studio and Frame Home
|
||||||
|
|
||||||
**Frame Home** makes your homes the Steam Frame's environment. Meta Quest homes (HSL, Clay `.opa`, glTF) and models
|
Turn Meta Quest homes and 3D models into native environments for the Steam Frame.
|
||||||
become Frame-native `.fenv` environments, drawn in SteamVR's compositor in place of its own environment: at boot,
|
|
||||||
behind the Steam menu and while games load. **Frame Studio** is the desktop editor for them, on Windows, Linux and
|
| | Runs on | What it does |
|
||||||
macOS. It opens Meta homes, `.fenv` environments and models (glTF, FBX, OBJ), shows them with the same renderer the
|
|---|---|---|
|
||||||
headset runs, and lets you edit, save and send them to the headset.
|
| **Frame Home** | The Steam Frame | Draws your environment in SteamVR's compositor in place of its own: at boot, behind the Steam menu and while games load |
|
||||||
|
| **Frame Studio** | Windows, Linux, macOS, the Steam Frame | Opens Meta homes (HSL, Clay `.opa`, glTF), `.fenv` environments and models (glTF, FBX, OBJ), shows them with the same renderer the headset runs, and lets you edit, save and send them to the headset |
|
||||||
|
|
||||||
|
**Help and news:** the [Frame Studio Discord](https://discord.gg/yJDn59ErgK) for help, bug reports, feature requests,
|
||||||
|
new releases and environments people have made. What changed in each version: [CHANGELOG.md](CHANGELOG.md).
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Quick start](#quick-start)
|
||||||
|
- [Install only Frame Home](#install-only-frame-home)
|
||||||
|
- [Downloads](#downloads)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Guides](#guides)
|
||||||
|
- [Building from source](#building-from-source)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
1. **Download** Frame Studio for your computer from the Releases page (**Latest build**, or a version release) and
|
1. **Download** Frame Studio for your computer from the Releases page and unpack it.
|
||||||
unpack it. See [Getting started](docs/getting-started.md).
|
See [Getting started](docs/getting-started.md).
|
||||||
2. **Prepare the headset** once: developer mode, SSH and a key. See [Preparing the Steam Frame](docs/headset-setup.md).
|
2. **Prepare the headset** once: developer mode, SSH and a key. See [Preparing the Steam Frame](docs/headset-setup.md).
|
||||||
3. **Install Frame Home**: in Studio, **Headset > Install on Steam Frame...**, then **Install Frame Home**.
|
3. **Install Frame Home**: in Studio, **Headset > Install on Steam Frame...**, then **Install Frame Home**.
|
||||||
See [Installing Frame Home](docs/install-frame-home.md).
|
See [Installing Frame Home](docs/install-frame-home.md).
|
||||||
4. **Open a home**: **File > Open...** and pick a Meta home `.apk`, a `.fenv` or a model.
|
4. **Open a home**: **File > Open...** and pick a Meta home `.apk`, a `.fenv` or a model.
|
||||||
5. **Send it**: **Headset > Send to Frame**. Put the headset on: you are standing in it.
|
5. **Send it**: **Headset > Send to Frame**. Put the headset on: you are standing in it.
|
||||||
|
|
||||||
|
## Install only Frame Home
|
||||||
|
|
||||||
|
You don't need Frame Studio to run Frame Home. On the Steam Frame:
|
||||||
|
|
||||||
|
1. Switch to **Desktop Mode** and open **Konsole**.
|
||||||
|
2. Run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/Toastconcern/FrameStudio/main/frame/install-frame-home-online.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
This downloads Frame Home from the **Latest build** release, checks it and installs it. No SSH or computer needed.
|
||||||
|
3. Put your `.fenv` environments in `~/frame-home/envs`.
|
||||||
|
4. Switch back to Game Mode. Your environment shows at boot and behind the Steam menu; switch between environments
|
||||||
|
in the **Environments** panel.
|
||||||
|
|
||||||
|
To install from a computer instead, download `FrameHome-installer.zip` and run **Install Frame Home.bat** (Windows)
|
||||||
|
or `install-frame-home.sh` (Linux, macOS). That way needs SSH on the headset. Every method, plus updating and
|
||||||
|
uninstalling: [Installing Frame Home](docs/install-frame-home.md).
|
||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
|
|
||||||
Every push builds Frame Studio and its matching Frame Home payload. The **Latest build** release contains both Studio
|
Every release on the Releases page (**Latest build**, or a numbered `v*` version) contains Frame Studio for each
|
||||||
packages and `FrameHome-installer.zip`; each `v*` version release contains both products too. Studio updates from its
|
computer below and the matching Frame Home payload.
|
||||||
top bar, and Frame Home updates from the Environments panel on the headset. Both use the same release channel.
|
|
||||||
|
|
||||||
| Computer | Frame Studio |
|
| Computer | File |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Windows, 64-bit PCs | `FrameStudio-windows-x64.zip` |
|
| Windows, 64-bit PCs | `FrameStudio-windows-x64.zip` |
|
||||||
| Windows on ARM (Snapdragon, Surface Pro X...) | `FrameStudio-windows-arm64.zip` |
|
| Windows on ARM (Snapdragon, Surface Pro X...) | `FrameStudio-windows-arm64.zip` |
|
||||||
| Linux, 64-bit PCs | `FrameStudio-linux-x64.tar.gz` |
|
| Linux, 64-bit PCs | `FrameStudio-linux-x64.tar.gz` |
|
||||||
| Linux on ARM (Raspberry Pi 5, ARM laptops, the Steam Frame's desktop) | `FrameStudio-linux-arm64.tar.gz` |
|
| Linux on ARM (Raspberry Pi 5, ARM laptops) | `FrameStudio-linux-arm64.tar.gz` |
|
||||||
|
| Steam Frame (SteamOS ARM64) | `FrameStudio-linux-arm64.tar.gz` |
|
||||||
| Mac with Apple Silicon (M1 and later) | `FrameStudio-macos-arm64.zip` |
|
| Mac with Apple Silicon (M1 and later) | `FrameStudio-macos-arm64.zip` |
|
||||||
| Frame Home only, installed from any computer | `FrameHome-installer.zip` |
|
| Frame Home only, installed from any computer | `FrameHome-installer.zip` |
|
||||||
|
|
||||||
Studio needs a GPU with Vulkan 1.2: any recent NVIDIA, AMD or Intel driver on Windows and Linux. On a Mac, Vulkan
|
**Requirements.** Studio needs a GPU with Vulkan 1.2: any recent NVIDIA, AMD or Intel driver on Windows and Linux.
|
||||||
runs on Metal through MoltenVK, which comes inside the app. 32-bit Windows and Linux and Intel Macs have no ready-made
|
On a Mac, Vulkan runs on Metal through MoltenVK, which comes inside the app. 32-bit Windows and Linux and Intel Macs
|
||||||
download; Studio can still be built for them from source ([docs/building.md](docs/building.md)).
|
have no ready-made download, but Studio can still be [built from source](docs/building.md) for them.
|
||||||
|
|
||||||
See [Installing Frame Home](docs/install-frame-home.md) for how to install the runtime on a Steam Frame.
|
**Updates.** Studio updates from its top bar, and Frame Home from the Environments panel on the headset. Both follow
|
||||||
|
the same release channel.
|
||||||
|
|
||||||
## Highlights
|
## Features
|
||||||
|
|
||||||
**Frame Home (on the headset)**
|
### Frame Home, on the headset
|
||||||
|
|
||||||
- Takes SteamVR's environment slot natively: no app, no library entry, controllers and passthrough still work.
|
- Takes SteamVR's environment slot natively: no app, no library entry, controllers and passthrough still work.
|
||||||
- An **Environments** panel in the Steam menu: switch homes live, delete them, change settings, debug views.
|
- An **Environments** panel in the Steam menu: switch homes live, delete them, change settings, debug views.
|
||||||
@@ -48,18 +83,18 @@ See [Installing Frame Home](docs/install-frame-home.md) for how to install the r
|
|||||||
thermal governor that keeps the headset cool.
|
thermal governor that keeps the headset cool.
|
||||||
- Safe: a compositor layer that steps aside if Frame Home stops, and a one-file off switch.
|
- Safe: a compositor layer that steps aside if Frame Home stops, and a one-file off switch.
|
||||||
|
|
||||||
**Frame Studio (on your computer)**
|
### Frame Studio, on your computer
|
||||||
|
|
||||||
- Opens Meta homes (HSL, Clay, glTF), `.fenv` environments and Blender or Unity models; pairs rooms with vistas.
|
- **Open** Meta homes (HSL, Clay, glTF), `.fenv` environments and Blender or Unity models; pair rooms with vistas.
|
||||||
- Move, rotate, scale, hide and delete objects, with undo and auto-saved versions.
|
- **Edit**: move, rotate, scale, hide and delete objects, with undo and auto-saved versions. A navigation gizmo in
|
||||||
- Shape the walkable area: auto-generated surfaces, drawn floors, spawns, seats, hotspots, gravity zones.
|
the viewport corner orbits the view or snaps it to an axis.
|
||||||
- Add a background sound and a picture; set and calculate per-world settings.
|
- **Shape the walkable area**: auto-generated surfaces, drawn floors, spawns, seats, hotspots, gravity zones.
|
||||||
- Saves baked `.fenv` files and leaves out what can't be seen; sends them to the headset in one click.
|
- **Set the mood**: a background sound, a picture, and per-world settings you can set or calculate.
|
||||||
- Packs several environments into one `.fbundle` to share or install together ([bundles](docs/bundles.md)).
|
- **Save and send**: baked `.fenv` files that leave out what can't be seen, sent to the headset in one click, or
|
||||||
- Debug views (wireframe, meshes, GPU cost, overdraw), a home report, and the headset's settings and debug page
|
packed with others into one `.fbundle` to share or install together ([bundles](docs/bundles.md)).
|
||||||
from your desk.
|
- **Check it**: debug views (wireframe, meshes, GPU cost, overdraw), a home report, and the headset's settings and
|
||||||
- A navigation gizmo in the viewport corner: drag it to orbit, click an axis to snap to that view.
|
debug page from your desk.
|
||||||
- Themes, from dark to light, drawn from the MetaDB palette (**Window > Theme**).
|
- Themes, from dark to light (**Window > Theme**).
|
||||||
|
|
||||||
## Guides
|
## Guides
|
||||||
|
|
||||||
@@ -69,26 +104,26 @@ See [Installing Frame Home](docs/install-frame-home.md) for how to install the r
|
|||||||
| [Preparing the Steam Frame](docs/headset-setup.md) | Developer mode, SSH, keys, the `frame` address |
|
| [Preparing the Steam Frame](docs/headset-setup.md) | Developer mode, SSH, keys, the `frame` address |
|
||||||
| [Installing Frame Home](docs/install-frame-home.md) | Three ways to install, updating, uninstalling, the off switch |
|
| [Installing Frame Home](docs/install-frame-home.md) | Three ways to install, updating, uninstalling, the off switch |
|
||||||
| [Using Frame Home](docs/using-frame-home.md) | The Environments panel, moving around, every setting |
|
| [Using Frame Home](docs/using-frame-home.md) | The Environments panel, moving around, every setting |
|
||||||
| [Environments and .fenv](docs/environments-and-fenv.md) | The format, `fenv-tool`, per-world settings and tuning |
|
|
||||||
| [Performance](docs/performance.md) | Reaching 120 fps, checking it, fixing stutter |
|
|
||||||
| [Frame Studio guide](docs/studio-guide.md) | The Studio window, tools and panels |
|
| [Frame Studio guide](docs/studio-guide.md) | The Studio window, tools and panels |
|
||||||
|
| [Environments and .fenv](docs/environments-and-fenv.md) | The format, `fenv-tool`, per-world settings and tuning |
|
||||||
| [Bundles](docs/bundles.md) | Several environments in one `.fbundle` |
|
| [Bundles](docs/bundles.md) | Several environments in one `.fbundle` |
|
||||||
| [Building from source](docs/building.md) | Building on each system, CI, and Frame Home payload builds |
|
| [Performance](docs/performance.md) | Reaching 120 fps, checking it, fixing stutter |
|
||||||
| [Troubleshooting](docs/troubleshooting.md) | Symptoms and fixes, logs, the self-test |
|
| [Troubleshooting](docs/troubleshooting.md) | Symptoms and fixes, logs, the self-test |
|
||||||
|
| [Building from source](docs/building.md) | Building on each system, CI, and Frame Home payload builds |
|
||||||
|
|
||||||
## Building from source
|
## Building from source
|
||||||
|
|
||||||
Everything Studio needs is in `third_party/`; you need CMake 3.16+ and Ninja. In short:
|
Everything Studio needs is in `third_party/`; you need CMake 3.16+ and Ninja.
|
||||||
|
|
||||||
| On | Command |
|
| On | Command |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Windows (Visual Studio 2022 with C++) | `studio\build_windows.bat x64` (or `x86`, `arm64`) |
|
| Windows (Visual Studio 2022 with C++) | `studio\build_windows.bat x64` (or `x86`, `arm64`) |
|
||||||
| Linux, macOS | `scripts/build-studio.sh <target>...` or `scripts/build-studio.sh all` |
|
| Linux, macOS | `scripts/build-studio.sh <target>...` or `scripts/build-studio.sh all` |
|
||||||
|
|
||||||
Frame Home binaries are built in GitHub Actions and are not checked into `frame/payload/`. Full details, prerequisites
|
Frame Home binaries are built in GitHub Actions and are not checked into `frame/payload/`. Prerequisites, CI and the
|
||||||
and CI: [docs/building.md](docs/building.md).
|
headset side: [docs/building.md](docs/building.md).
|
||||||
|
|
||||||
## Repository layout
|
### Repository layout
|
||||||
|
|
||||||
| Path | What it holds |
|
| Path | What it holds |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -108,9 +143,8 @@ You may use, study, change and share them; if you share a changed version, or ru
|
|||||||
its source must be available under the same license.
|
its source must be available under the same license.
|
||||||
|
|
||||||
Vendored libraries in `third_party/` keep their own licenses (in each folder); Studio's interface font, Inter, is under
|
Vendored libraries in `third_party/` keep their own licenses (in each folder); Studio's interface font, Inter, is under
|
||||||
the SIL Open Font License (`third_party/inter/LICENSE.txt`). The home loaders in `third_party/qhe`
|
the SIL Open Font License (`third_party/inter/LICENSE.txt`). The home loaders in `third_party/qhe` started from Quest
|
||||||
started from Quest Home Editor and keep its MIT license for the original code; see
|
Home Editor and keep its MIT license for the original code; see [third_party/qhe/LICENSE.md](third_party/qhe/LICENSE.md).
|
||||||
[third_party/qhe/LICENSE.md](third_party/qhe/LICENSE.md).
|
|
||||||
|
|
||||||
Neither license covers Meta's homes or other assets: Frame Studio only works with files you already have, and no
|
Neither license covers Meta's homes or other assets: Frame Studio only works with files you already have, and no
|
||||||
Meta content is included in this repository or its downloads.
|
Meta content is included in this repository or its downloads.
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="152 115.2 720 720" width="1024" height="1024"><!-- Frame Studio / Frame Home dashboard glyph (concept 59, no tile): a white roof resting on a Steam blue headset. --><g transform='translate(512 486) scale(1.06) rotate(45) translate(-194 -230.4)'><path d='M0 44.5917C0 39.9653 3.75045 36.2148 8.37686 36.2148H388.313V119.984H83.7682V424.532H0V44.5917Z' fill='#f7f4ff'/></g><g transform='translate(512 640) scale(0.62) translate(-512 -512)'><path d='M352 332 H 672 C 772 332 832 382 832 470 V 540 C 832 636 772 692 690 692 C 642 692 622 680 602 650 L 566 594 C 544 562 480 562 458 594 L 422 650 C 402 680 382 692 334 692 C 252 692 192 636 192 540 V 470 C 192 382 252 332 352 332 Z M452 366 C 438 366 428 376 428 388 C 428 400 438 410 452 410 H 572 C 586 410 596 400 596 388 C 596 376 586 366 572 366 Z' fill='#1A9FFF' fill-rule='evenodd'/></g></svg>
|
||||||
|
After Width: | Height: | Size: 896 B |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 512 B |
|
After Width: | Height: | Size: 773 B |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 988 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024"><!-- Frame Studio / Frame Home square icon (concept 59, full-bleed, for Discord and other round crops): a white roof resting on a Steam blue headset, drawn 14% larger than concept 59. --><rect width='1024' height='1024' fill='#151515'/><g transform='translate(0 66.5) translate(512 540) scale(1.3) translate(-512 -548)'><g transform='translate(512 486) scale(1.06) rotate(45) translate(-194 -230.4)'><path d='M0 44.5917C0 39.9653 3.75045 36.2148 8.37686 36.2148H388.313V119.984H83.7682V424.532H0V44.5917Z' fill='#f7f4ff'/></g><g transform='translate(512 640) scale(0.62) translate(-512 -512)'><path d='M352 332 H 672 C 772 332 832 382 832 470 V 540 C 832 636 772 692 690 692 C 642 692 622 680 602 650 L 566 594 C 544 562 480 562 458 594 L 422 650 C 402 680 382 692 334 692 C 252 692 192 636 192 540 V 470 C 192 382 252 332 352 332 Z M452 366 C 438 366 428 376 428 388 C 428 400 438 410 452 410 H 572 C 586 410 596 400 596 388 C 596 376 586 366 572 366 Z' fill='#1A9FFF' fill-rule='evenodd'/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024"><!-- Frame Studio / Frame Home app icon (concept 59): a white roof resting on a Steam blue headset, drawn 14% larger than concept 59. --><rect x='32' y='32' width='960' height='960' rx='216' fill='#151515'/><g transform='translate(0 47.5) translate(512 548) scale(1.14) translate(-512 -548)'><g transform='translate(512 486) scale(1.06) rotate(45) translate(-194 -230.4)'><path d='M0 44.5917C0 39.9653 3.75045 36.2148 8.37686 36.2148H388.313V119.984H83.7682V424.532H0V44.5917Z' fill='#f7f4ff'/></g><g transform='translate(512 640) scale(0.62) translate(-512 -512)'><path d='M352 332 H 672 C 772 332 832 382 832 470 V 540 C 832 636 772 692 690 692 C 642 692 622 680 602 650 L 566 594 C 544 562 480 562 458 594 L 422 650 C 402 680 382 692 334 692 C 252 692 192 636 192 540 V 470 C 192 382 252 332 352 332 Z M452 366 C 438 366 428 376 428 388 C 428 400 438 410 452 410 H 572 C 586 410 596 400 596 388 C 596 376 586 366 572 366 Z' fill='#1A9FFF' fill-rule='evenodd'/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,32 @@
|
|||||||
|
# FRAME_VERSION: what the apps show as their version, the same name the release gets (.github/workflows/build.yml):
|
||||||
|
# "v" + VERSION (major.minor) + "." + the commits since VERSION last changed, so the commit that bumps VERSION is
|
||||||
|
# v<major.minor>.0. $FRAME_VERSION overrides it. Without the whole history (a shallow checkout, a copy that isn't a
|
||||||
|
# git repository) the count is unknown: "v0.2 (local)"; VERSION edited but not committed yet counts from 0.
|
||||||
|
file(READ "${CMAKE_CURRENT_LIST_DIR}/../VERSION" FRAME_VERSION_BASE)
|
||||||
|
string(STRIP "${FRAME_VERSION_BASE}" FRAME_VERSION_BASE)
|
||||||
|
set(FRAME_VERSION "$ENV{FRAME_VERSION}")
|
||||||
|
if(NOT FRAME_VERSION)
|
||||||
|
set(FRAME_VERSION "v${FRAME_VERSION_BASE} (local)")
|
||||||
|
set(FRAME_SRC "${CMAKE_CURRENT_LIST_DIR}/..")
|
||||||
|
find_package(Git QUIET)
|
||||||
|
if(GIT_FOUND)
|
||||||
|
execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse --is-shallow-repository WORKING_DIRECTORY "${FRAME_SRC}"
|
||||||
|
OUTPUT_VARIABLE FRAME_SHALLOW RESULT_VARIABLE FRAME_GIT_RC OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
|
||||||
|
if(FRAME_GIT_RC EQUAL 0 AND FRAME_SHALLOW STREQUAL "false")
|
||||||
|
execute_process(COMMAND "${GIT_EXECUTABLE}" diff --quiet HEAD -- VERSION WORKING_DIRECTORY "${FRAME_SRC}"
|
||||||
|
RESULT_VARIABLE FRAME_VERSION_EDITED ERROR_QUIET)
|
||||||
|
execute_process(COMMAND "${GIT_EXECUTABLE}" log -1 --format=%H -- VERSION WORKING_DIRECTORY "${FRAME_SRC}"
|
||||||
|
OUTPUT_VARIABLE FRAME_BUMP RESULT_VARIABLE FRAME_GIT_RC OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
|
||||||
|
if(FRAME_VERSION_EDITED EQUAL 1)
|
||||||
|
set(FRAME_VERSION "v${FRAME_VERSION_BASE}.0 (local)")
|
||||||
|
elseif(FRAME_GIT_RC EQUAL 0 AND FRAME_BUMP)
|
||||||
|
execute_process(COMMAND "${GIT_EXECUTABLE}" rev-list --count "${FRAME_BUMP}..HEAD" WORKING_DIRECTORY "${FRAME_SRC}"
|
||||||
|
OUTPUT_VARIABLE FRAME_COMMITS RESULT_VARIABLE FRAME_GIT_RC OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
|
||||||
|
if(FRAME_GIT_RC EQUAL 0 AND FRAME_COMMITS MATCHES "^[0-9]+$")
|
||||||
|
set(FRAME_VERSION "v${FRAME_VERSION_BASE}.${FRAME_COMMITS}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
message(STATUS "Version: ${FRAME_VERSION}")
|
||||||
@@ -111,7 +111,7 @@ edges, grey are floors you can't reach. **W** shows or hides it.
|
|||||||
| Item | What it does on the headset |
|
| Item | What it does on the headset |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Spawn Point** | where you start, facing the way it faces |
|
| **Spawn Point** | where you start, facing the way it faces |
|
||||||
| **Seat** | a place to sit: point at it and pull the trigger; its "stand up here" ring is where you get up |
|
| **Seat** | a place to sit: point at it and pull the trigger; you sit facing the way its arrow points; its "stand up here" ring is where you get up |
|
||||||
| **Teleport Hotspot** | a spot the teleport arc snaps to |
|
| **Teleport Hotspot** | a spot the teleport arc snaps to |
|
||||||
| **Surface** | a box you can scale into a floor, a step or a wall |
|
| **Surface** | a box you can scale into a floor, a step or a wall |
|
||||||
| **Invisible Wall** | stops you; never drawn on the headset. Scale and turn it to fit; where it stands is no longer walkable |
|
| **Invisible Wall** | stops you; never drawn on the headset. Scale and turn it to fit; where it stands is no longer walkable |
|
||||||
|
|||||||
@@ -33,9 +33,22 @@ $scp = Find-Tool "scp"
|
|||||||
if ($HostName -notmatch "@") { $HostName = "steamos@$HostName" }
|
if ($HostName -notmatch "@") { $HostName = "steamos@$HostName" }
|
||||||
|
|
||||||
# The name an environment gets on the headset: lowercase, spaces as dashes, only [a-z0-9._-] (as Studio names them).
|
# The name an environment gets on the headset: lowercase, spaces as dashes, only [a-z0-9._-] (as Studio names them).
|
||||||
|
# When that drops characters (accents, other scripts) a short hash of the real name is added, so two such names don't
|
||||||
|
# become the same file, and an all-non-Latin one doesn't become a bare ".fenv".
|
||||||
function Frame-Name($path) {
|
function Frame-Name($path) {
|
||||||
$n = ([IO.Path]::GetFileName($path)).ToLowerInvariant() -replace " ", "-"
|
$base = [IO.Path]::GetFileName($path)
|
||||||
return ($n -replace "[^a-z0-9._-]", "")
|
$low = $base.ToLowerInvariant() -replace " ", "-"
|
||||||
|
$n = $low -replace "[^a-z0-9._-]", ""
|
||||||
|
$dot = $n.LastIndexOf(".")
|
||||||
|
$stem = if ($dot -ge 0) { $n.Substring(0, $dot) } else { $n }
|
||||||
|
if ($n -ne $low -or $stem -eq "") {
|
||||||
|
$sha = [Security.Cryptography.SHA1]::Create().ComputeHash([Text.Encoding]::UTF8.GetBytes($base))
|
||||||
|
$h = -join ($sha[0..3] | ForEach-Object { $_.ToString("x2") })
|
||||||
|
$ext = if ($dot -ge 0) { $n.Substring($dot) } else { "" }
|
||||||
|
if ($stem -eq "") { $stem = "environment" }
|
||||||
|
$n = "$stem-$h$ext"
|
||||||
|
}
|
||||||
|
return $n
|
||||||
}
|
}
|
||||||
|
|
||||||
$payload = Join-Path $PSScriptRoot "payload"
|
$payload = Join-Path $PSScriptRoot "payload"
|
||||||
@@ -62,6 +75,11 @@ if ($Uninstall) {
|
|||||||
Say "copying $([IO.Path]::GetFileName($f)) -> $name"
|
Say "copying $([IO.Path]::GetFileName($f)) -> $name"
|
||||||
Push-Location (Split-Path -Parent (Resolve-Path $f))
|
Push-Location (Split-Path -Parent (Resolve-Path $f))
|
||||||
try { & $scp -q ([IO.Path]::GetFileName($f)) "${HostName}:frame-home/envs/$name.part" } finally { Pop-Location }
|
try { & $scp -q ([IO.Path]::GetFileName($f)) "${HostName}:frame-home/envs/$name.part" } finally { Pop-Location }
|
||||||
|
if ($LASTEXITCODE -ne 0) { # (no half-copied file left behind, and the one shown stays as it was)
|
||||||
|
& $ssh $HostName "rm -f frame-home/envs/$name.part"
|
||||||
|
Say "couldn't copy $([IO.Path]::GetFileName($f)) (the headset's disk is full, or the connection dropped)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
& $ssh $HostName "mv -f frame-home/envs/$name.part frame-home/envs/$name"
|
& $ssh $HostName "mv -f frame-home/envs/$name.part frame-home/envs/$name"
|
||||||
}
|
}
|
||||||
& $ssh $HostName "sed -i 's/\r`$//' frame-home/.install/frame/install.sh; sh frame-home/.install/frame/install.sh"
|
& $ssh $HostName "sed -i 's/\r`$//' frame-home/.install/frame/install.sh; sh frame-home/.install/frame/install.sh"
|
||||||
@@ -70,7 +88,7 @@ if ($Uninstall) {
|
|||||||
$first = $Environments | Where-Object { $_ -match "\.fenv$" } | Select-Object -First 1
|
$first = $Environments | Where-Object { $_ -match "\.fenv$" } | Select-Object -First 1
|
||||||
if ($first -and (Test-Path $first)) {
|
if ($first -and (Test-Path $first)) {
|
||||||
$name = Frame-Name $first
|
$name = Frame-Name $first
|
||||||
& $ssh $HostName "printf '%s\n' '$name' > frame-home/current"
|
& $ssh $HostName "if [ -f 'frame-home/envs/$name' ]; then printf '%s\n' '$name' > frame-home/current; fi"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Say "done"
|
Say "done"
|
||||||
@@ -26,6 +26,20 @@ $1"; shift ;;
|
|||||||
done
|
done
|
||||||
case "$HOST" in *@*) ;; *) HOST="steamos@$HOST" ;; esac
|
case "$HOST" in *@*) ;; *) HOST="steamos@$HOST" ;; esac
|
||||||
say() { echo "frame-home: $*"; }
|
say() { echo "frame-home: $*"; }
|
||||||
|
# The name a file gets on the headset: lowercase, spaces as dashes, only [a-z0-9._-] (as Studio names them). When
|
||||||
|
# that drops characters (accents, other scripts) a short hash of the real name is added, so two such names don't
|
||||||
|
# become the same file, and an all-non-Latin one doesn't become a bare ".fenv".
|
||||||
|
frame_name() {
|
||||||
|
base=$(basename "$1")
|
||||||
|
low=$(printf '%s' "$base" | tr 'A-Z ' 'a-z-')
|
||||||
|
n=$(printf '%s' "$low" | tr -cd 'a-z0-9._-')
|
||||||
|
if [ "$n" != "$low" ] || [ -z "${n%.*}" ]; then
|
||||||
|
h=$(printf '%s' "$base" | cksum | cut -d' ' -f1)
|
||||||
|
stem=${n%.*}
|
||||||
|
n="${stem:-environment}-$h.${n##*.}"
|
||||||
|
fi
|
||||||
|
printf '%s' "$n"
|
||||||
|
}
|
||||||
[ -f "$PAYLOAD/install.sh" ] || { say "the headset files are missing: expected $PAYLOAD/install.sh"; exit 1; }
|
[ -f "$PAYLOAD/install.sh" ] || { say "the headset files are missing: expected $PAYLOAD/install.sh"; exit 1; }
|
||||||
|
|
||||||
say "connecting to $HOST"
|
say "connecting to $HOST"
|
||||||
@@ -40,17 +54,18 @@ if [ "$MODE" = install ]; then
|
|||||||
printf '%s\n' "$ENVS" | while IFS= read -r f; do
|
printf '%s\n' "$ENVS" | while IFS= read -r f; do
|
||||||
[ -n "$f" ] || continue
|
[ -n "$f" ] || continue
|
||||||
[ -f "$f" ] || { say "no such file: $f"; continue; }
|
[ -f "$f" ] || { say "no such file: $f"; continue; }
|
||||||
# the name it gets on the headset: lowercase, spaces as dashes, only [a-z0-9._-] (as Studio names them)
|
name=$(frame_name "$f")
|
||||||
name=$(basename "$f" | tr 'A-Z ' 'a-z-' | tr -cd 'a-z0-9._-')
|
|
||||||
say "copying $(basename "$f") -> $name"
|
say "copying $(basename "$f") -> $name"
|
||||||
scp -q "$f" "$HOST:frame-home/envs/$name.part" && ssh "$HOST" "mv -f frame-home/envs/$name.part frame-home/envs/$name"
|
scp -q "$f" "$HOST:frame-home/envs/$name.part" && ssh "$HOST" "mv -f frame-home/envs/$name.part frame-home/envs/$name" || {
|
||||||
|
ssh "$HOST" "rm -f frame-home/envs/$name.part" || true; say "couldn't copy $(basename "$f") (the headset's disk is full, or the connection dropped)"; }
|
||||||
done
|
done
|
||||||
ssh "$HOST" "sed -i 's/\r\$//' frame-home/.install/frame/install.sh; sh frame-home/.install/frame/install.sh"
|
ssh "$HOST" "sed -i 's/\r\$//' frame-home/.install/frame/install.sh; sh frame-home/.install/frame/install.sh"
|
||||||
# the first environment given is the one shown (a bundle's are only known once the headset unpacks it: skipped)
|
# the first environment given is the one shown (a bundle's are only known once the headset unpacks it: skipped)
|
||||||
FIRST=$(printf '%s\n' "$ENVS" | grep -i '\.fenv$' | sed -n '1p')
|
FIRST=$(printf '%s\n' "$ENVS" | grep -i '\.fenv$' | sed -n '1p')
|
||||||
if [ -n "$FIRST" ] && [ -f "$FIRST" ]; then
|
if [ -n "$FIRST" ] && [ -f "$FIRST" ]; then
|
||||||
name=$(basename "$FIRST" | tr 'A-Z ' 'a-z-' | tr -cd 'a-z0-9._-')
|
name=$(frame_name "$FIRST")
|
||||||
ssh "$HOST" "printf '%s\n' '$name' > frame-home/current"
|
# (only when it got there: a copy that failed leaves the one shown as it was)
|
||||||
|
ssh "$HOST" "if [ -f 'frame-home/envs/$name' ]; then printf '%s\n' '$name' > frame-home/current; fi"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# (never installed from a copy like this one: Frame Home itself puts SteamVR's environment back)
|
# (never installed from a copy like this one: Frame Home itself puts SteamVR's environment back)
|
||||||
|
|||||||
@@ -63,10 +63,23 @@ if ! LD_LIBRARY_PATH="$P/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$P/bin/frame
|
|||||||
say "Frame Home can't run on this headset: ${missing:-unknown error} (nothing was changed)"; exit 1
|
say "Frame Home can't run on this headset: ${missing:-unknown error} (nothing was changed)"; exit 1
|
||||||
fi
|
fi
|
||||||
mkdir -p "$H/bin" "$H/envs" || exit 1
|
mkdir -p "$H/bin" "$H/envs" || exit 1
|
||||||
# New binaries beside the old ones, then renamed over them: a running copy keeps its file until it restarts.
|
# New binaries beside the old ones, then renamed over them: a running copy keeps its file until it restarts. All of
|
||||||
|
# them copied before any is renamed, so a copy that fails (a full disk) leaves the old set whole, never a mix.
|
||||||
for x in frame-home frame-switcher fenv-tool; do
|
for x in frame-home frame-switcher fenv-tool; do
|
||||||
[ -f "$P/bin/$x" ] || continue
|
[ -f "$P/bin/$x" ] || continue
|
||||||
cp -f "$P/bin/$x" "$H/bin/$x.new" && chmod +x "$H/bin/$x.new" && mv -f "$H/bin/$x.new" "$H/bin/$x" || { say "couldn't install $x"; exit 1; }
|
cp -f "$P/bin/$x" "$H/bin/$x.new" && chmod +x "$H/bin/$x.new" || { rm -f "$H"/bin/*.new; say "couldn't install $x (nothing was changed)"; exit 1; }
|
||||||
|
done
|
||||||
|
# The compositor layer and its shader are copied in the same round: new programs never go in beside an old layer
|
||||||
|
# because the layer's copy failed (Frame Home and the layer share the frames).
|
||||||
|
if [ -f "$P/layer/libframe_home_layer.so" ]; then
|
||||||
|
mkdir -p "$H/layer" || { rm -f "$H"/bin/*.new; say "couldn't install the compositor layer (nothing was changed)"; exit 1; }
|
||||||
|
for f in libframe_home_layer.so env_ps.spv; do
|
||||||
|
cp -f "$P/layer/$f" "$H/layer/.$f.new" || { rm -f "$H"/bin/*.new "$H"/layer/.*.new; say "couldn't install the compositor layer (nothing was changed)"; exit 1; }
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
for x in frame-home frame-switcher fenv-tool; do
|
||||||
|
[ -f "$H/bin/$x.new" ] || continue
|
||||||
|
mv -f "$H/bin/$x.new" "$H/bin/$x" || { say "couldn't install $x"; exit 1; }
|
||||||
done
|
done
|
||||||
# The compositor layer and its shader (renamed over the old ones: a running compositor keeps its copy).
|
# The compositor layer and its shader (renamed over the old ones: a running compositor keeps its copy).
|
||||||
layerChanged=0
|
layerChanged=0
|
||||||
@@ -74,7 +87,8 @@ if [ -f "$P/layer/libframe_home_layer.so" ]; then
|
|||||||
mkdir -p "$H/layer" "$(dirname "$LAYERJSON")"
|
mkdir -p "$H/layer" "$(dirname "$LAYERJSON")"
|
||||||
cmp -s "$P/layer/libframe_home_layer.so" "$H/layer/libframe_home_layer.so" || layerChanged=1
|
cmp -s "$P/layer/libframe_home_layer.so" "$H/layer/libframe_home_layer.so" || layerChanged=1
|
||||||
cmp -s "$P/layer/env_ps.spv" "$H/layer/env_ps.spv" || layerChanged=1
|
cmp -s "$P/layer/env_ps.spv" "$H/layer/env_ps.spv" || layerChanged=1
|
||||||
for f in libframe_home_layer.so env_ps.spv; do cp -f "$P/layer/$f" "$H/layer/.$f.new" && mv -f "$H/layer/.$f.new" "$H/layer/$f"; done
|
# (the library and its shader go together: both copied above, then both renamed)
|
||||||
|
for f in libframe_home_layer.so env_ps.spv; do mv -f "$H/layer/.$f.new" "$H/layer/$f" || { say "couldn't install the compositor layer"; exit 1; }; done
|
||||||
rm -f "$H/layer/off" "$H/layer/starting" "$H/layer/crashes"
|
rm -f "$H/layer/off" "$H/layer/starting" "$H/layer/crashes"
|
||||||
[ -f "$LAYERJSON" ] || layerChanged=1
|
[ -f "$LAYERJSON" ] || layerChanged=1
|
||||||
cat > "$LAYERJSON" <<JSON
|
cat > "$LAYERJSON" <<JSON
|
||||||
@@ -94,7 +108,15 @@ if [ -f "$P/layer/libframe_home_layer.so" ]; then
|
|||||||
JSON
|
JSON
|
||||||
fi
|
fi
|
||||||
# Libraries the binaries need that the headset may not have (none today; kept for future builds).
|
# Libraries the binaries need that the headset may not have (none today; kept for future builds).
|
||||||
[ -d "$P/lib" ] && { mkdir -p "$H/lib"; cp -f "$P"/lib/* "$H/lib/"; }
|
# (each copied beside and renamed over: writing into a library a running process has mapped would crash it)
|
||||||
|
if [ -d "$P/lib" ]; then
|
||||||
|
mkdir -p "$H/lib"
|
||||||
|
for f in "$P"/lib/*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
n=$(basename "$f")
|
||||||
|
cp -f "$f" "$H/lib/.$n.new" && mv -f "$H/lib/.$n.new" "$H/lib/$n" || { say "couldn't install $n"; exit 1; }
|
||||||
|
done
|
||||||
|
fi
|
||||||
# Environments that came with this install.
|
# Environments that came with this install.
|
||||||
for f in "$P"/envs/*.fenv; do [ -f "$f" ] && mv -f "$f" "$H/envs/"; done
|
for f in "$P"/envs/*.fenv; do [ -f "$f" ] && mv -f "$f" "$H/envs/"; done
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
// holds the eye position (_m11, as Aurora's floor shader reads it). The direction from the eye to the point is
|
// holds the eye position (_m11, as Aurora's floor shader reads it). The direction from the eye to the point is
|
||||||
// looked up in the newest frame Frame Home published: its projection * view rotation for that eye, so the picture
|
// looked up in the newest frame Frame Home published: its projection * view rotation for that eye, so the picture
|
||||||
// stays locked to the world as the head turns between Frame Home's frame and the compositor's (rotation reprojection).
|
// stays locked to the world as the head turns between Frame Home's frame and the compositor's (rotation reprojection).
|
||||||
|
// A frame shown again (half rate, or the next one late) is carried on by how the rig was moving (env_share.h Motion):
|
||||||
|
// its smooth turn exactly, its walking as if everything were walk.w metres away.
|
||||||
|
|
||||||
// SteamVR's per-frame block (set 0, binding 4), as far as the members used here.
|
// SteamVR's per-frame block (set 0, binding 4), as far as the members used here.
|
||||||
layout(set = 0, binding = 4, std140) uniform CompositorFrame {
|
layout(set = 0, binding = 4, std140) uniform CompositorFrame {
|
||||||
@@ -23,6 +25,8 @@ layout(set = 1, binding = 1, std140) uniform HomeFrame {
|
|||||||
mat4 viewProj[2]; // projection * view rotation, per eye (tracking space)
|
mat4 viewProj[2]; // projection * view rotation, per eye (tracking space)
|
||||||
vec4 eyePos[2]; // where each eye was when Frame Home rendered it
|
vec4 eyePos[2]; // where each eye was when Frame Home rendered it
|
||||||
vec4 params; // x: brightness (0 fades to black), y: 1 when there's something to show
|
vec4 params; // x: brightness (0 fades to black), y: 1 when there's something to show
|
||||||
|
vec4 turn; // xy: cos, sin of the rig's turn since the frame (about tracking-space up)
|
||||||
|
vec4 walk; // xyz: the rig's walk since the frame (frame's tracking space, m), w: depth taken (0: none)
|
||||||
} homeFrame;
|
} homeFrame;
|
||||||
|
|
||||||
layout(location = 1) in vec3 worldPos;
|
layout(location = 1) in vec3 worldPos;
|
||||||
@@ -30,8 +34,12 @@ layout(location = 0) out vec4 color;
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
const vec3 eye = frame.m11.xyz;
|
const vec3 eye = frame.m11.xyz;
|
||||||
const vec3 dir = normalize(worldPos - eye);
|
vec3 dir = normalize(worldPos - eye);
|
||||||
const int k = distance(eye, homeFrame.eyePos[0].xyz) <= distance(eye, homeFrame.eyePos[1].xyz) ? 0 : 1;
|
const int k = distance(eye, homeFrame.eyePos[0].xyz) <= distance(eye, homeFrame.eyePos[1].xyz) ? 0 : 1;
|
||||||
|
// Into the frame's tracking space: the rig turned since by this much (about the head), and walked.
|
||||||
|
const vec2 cs = homeFrame.turn.xy;
|
||||||
|
dir = vec3(cs.x * dir.x + cs.y * dir.z, dir.y, -cs.y * dir.x + cs.x * dir.z);
|
||||||
|
if (homeFrame.walk.w > 0.0) dir = normalize(dir * homeFrame.walk.w + homeFrame.walk.xyz);
|
||||||
const vec4 c = homeFrame.viewProj[k] * vec4(dir, 0.0);
|
const vec4 c = homeFrame.viewProj[k] * vec4(dir, 0.0);
|
||||||
vec3 rgb = vec3(0.0);
|
vec3 rgb = vec3(0.0);
|
||||||
if (c.w > 1e-5 && homeFrame.params.y > 0.5) {
|
if (c.w > 1e-5 && homeFrame.params.y > 0.5) {
|
||||||
|
|||||||
@@ -26,8 +26,10 @@
|
|||||||
#include <sys/un.h>
|
#include <sys/un.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
#include <cstdarg>
|
#include <cstdarg>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
@@ -138,13 +140,16 @@ struct InstanceData {
|
|||||||
X(DestroyImage) X(CreateImageView) X(DestroyImageView) X(GetImageMemoryRequirements) X(AllocateMemory) X(FreeMemory) \
|
X(DestroyImage) X(CreateImageView) X(DestroyImageView) X(GetImageMemoryRequirements) X(AllocateMemory) X(FreeMemory) \
|
||||||
X(BindImageMemory) X(CreateBuffer) X(DestroyBuffer) X(GetBufferMemoryRequirements) X(BindBufferMemory) X(MapMemory) \
|
X(BindImageMemory) X(CreateBuffer) X(DestroyBuffer) X(GetBufferMemoryRequirements) X(BindBufferMemory) X(MapMemory) \
|
||||||
X(CmdBindPipeline) X(CmdBindDescriptorSets) X(CmdDraw) X(CmdDrawIndexed) X(QueueSubmit) X(QueueSubmit2) \
|
X(CmdBindPipeline) X(CmdBindDescriptorSets) X(CmdDraw) X(CmdDrawIndexed) X(QueueSubmit) X(QueueSubmit2) \
|
||||||
X(QueueSubmit2KHR) X(QueueWaitIdle) X(DeviceWaitIdle)
|
X(QueueSubmit2KHR) X(QueueWaitIdle) X(DeviceWaitIdle) X(DestroyDescriptorPool) X(DestroyDescriptorSetLayout) X(DestroySampler) \
|
||||||
|
X(AllocateCommandBuffers) X(FreeCommandBuffers) X(BeginCommandBuffer) X(ResetCommandBuffer) X(ResetCommandPool) \
|
||||||
|
X(DestroyCommandPool) X(CreateCommandPool) X(EndCommandBuffer) X(CmdPipelineBarrier) X(GetDeviceQueue2)
|
||||||
|
|
||||||
struct DeviceData {
|
struct DeviceData {
|
||||||
VkDevice device = VK_NULL_HANDLE;
|
VkDevice device = VK_NULL_HANDLE;
|
||||||
VkPhysicalDevice physical = VK_NULL_HANDLE;
|
VkPhysicalDevice physical = VK_NULL_HANDLE;
|
||||||
InstanceData* inst = nullptr;
|
InstanceData* inst = nullptr;
|
||||||
PFN_vkGetDeviceProcAddr gdpa = nullptr;
|
PFN_vkGetDeviceProcAddr gdpa = nullptr;
|
||||||
|
PFN_vkSetDeviceLoaderData setLoaderData = nullptr; // our own command buffers get the loader's dispatch with it
|
||||||
bool samplesSubsampled = false; // VK_EXT_fragment_density_map is on: Frame Home's foveated frames read as made
|
bool samplesSubsampled = false; // VK_EXT_fragment_density_map is on: Frame Home's foveated frames read as made
|
||||||
#define FH_DECL(n) PFN_vk##n n = nullptr;
|
#define FH_DECL(n) PFN_vk##n n = nullptr;
|
||||||
FH_DEVICE_FUNCS(FH_DECL)
|
FH_DEVICE_FUNCS(FH_DECL)
|
||||||
@@ -155,6 +160,7 @@ std::mutex g_mutex;
|
|||||||
std::unordered_map<void*, InstanceData> g_instances;
|
std::unordered_map<void*, InstanceData> g_instances;
|
||||||
std::unordered_map<void*, InstanceData*> g_physToInstance; // physical device (dispatch key) -> instance data
|
std::unordered_map<void*, InstanceData*> g_physToInstance; // physical device (dispatch key) -> instance data
|
||||||
std::unordered_map<void*, DeviceData> g_devices;
|
std::unordered_map<void*, DeviceData> g_devices;
|
||||||
|
std::unordered_map<VkQueue, uint32_t> g_queueFamily; // (g_mutex) each queue the compositor got, its family
|
||||||
|
|
||||||
DeviceData* dev(const void* dispatchable) {
|
DeviceData* dev(const void* dispatchable) {
|
||||||
std::lock_guard<std::mutex> l(g_mutex);
|
std::lock_guard<std::mutex> l(g_mutex);
|
||||||
@@ -176,6 +182,12 @@ std::unordered_map<VkPipeline, Twin> g_twins; // Aurora pipeline ->
|
|||||||
std::unordered_set<VkPipeline> g_skip; // other Aurora pieces: not drawn while the home shows
|
std::unordered_set<VkPipeline> g_skip; // other Aurora pieces: not drawn while the home shows
|
||||||
std::unordered_set<VkPipeline> g_camera; // passthrough (the cameras' reprojection)
|
std::unordered_set<VkPipeline> g_camera; // passthrough (the cameras' reprojection)
|
||||||
std::atomic<uint64_t> g_cameraNs{0}; // when passthrough was last drawn
|
std::atomic<uint64_t> g_cameraNs{0}; // when passthrough was last drawn
|
||||||
|
// Passthrough drawn in the last 150 ms. Signed: another thread's command buffer may stamp g_cameraNs between our
|
||||||
|
// clock read and its load, and that is "just now", not 584 years ago (unsigned, it flashed the home over the cameras).
|
||||||
|
bool cameraRecent() {
|
||||||
|
const uint64_t c = g_cameraNs.load();
|
||||||
|
return c && static_cast<int64_t>(envshare::nowNs() - c) < 150000000ll;
|
||||||
|
}
|
||||||
// Aurora's stars are a render model (rendermodels/stars), drawn with the generic model pipeline the controllers use
|
// Aurora's stars are a render model (rendermodels/stars), drawn with the generic model pipeline the controllers use
|
||||||
// too: they're told apart by their index count, read from the model itself.
|
// too: they're told apart by their index count, read from the model itself.
|
||||||
uint32_t g_starsIndices = 0;
|
uint32_t g_starsIndices = 0;
|
||||||
@@ -197,33 +209,77 @@ std::unordered_map<VkCommandBuffer, VkPipeline> g_bound; // graphics pipeline
|
|||||||
std::unordered_map<VkPipeline, std::string> g_names; // vertex + fragment shader names (trace)
|
std::unordered_map<VkPipeline, std::string> g_names; // vertex + fragment shader names (trace)
|
||||||
std::atomic<int> g_traceDraws{0}; // ~/frame-home/layer/trace: log this many draws
|
std::atomic<int> g_traceDraws{0}; // ~/frame-home/layer/trace: log this many draws
|
||||||
std::atomic<bool> g_off{false};
|
std::atomic<bool> g_off{false};
|
||||||
|
std::atomic<bool> g_startedOff{false}; // off as this compositor started: none of our pipelines exist
|
||||||
|
|
||||||
// Our objects on the compositor's device.
|
// Our objects on the compositor's device.
|
||||||
|
//
|
||||||
|
// Each Hello's images are a generation (Gen): the images, their views, and descriptor sets of their own, written
|
||||||
|
// once when the generation is made and never again. A descriptor set bound in a recorded command buffer must not be
|
||||||
|
// rewritten or freed until that command buffer is done with (re-begun, reset or freed: the spec forbids any of those
|
||||||
|
// while it's still pending on the GPU, so by then the GPU is done with it too). So a new generation never touches the
|
||||||
|
// old one's sets; the old one is retired, and destroyed once no command buffer that drew with it is left.
|
||||||
|
//
|
||||||
|
// The images are external memory written on Frame Home's queue: each frame Frame Home finishes is released to
|
||||||
|
// VK_QUEUE_FAMILY_EXTERNAL (src/hsl_renderer.cpp), and the compositor's queue must acquire it before sampling it, or
|
||||||
|
// what it reads is undefined (on some GPUs, caches or compression state another process left behind). The acquire is
|
||||||
|
// one pipeline barrier per slot, recorded once per generation and queue family (acquire[]), and submitted just ahead of
|
||||||
|
// the first compositor command buffer that draws a frame newer than the slot last acquired (acquiredFrame[]): once per
|
||||||
|
// Frame Home frame, never per draw. GENERAL to GENERAL on both sides (no layout transition), so an older Frame Home
|
||||||
|
// that doesn't release pairs with this layer as before, and an older layer that doesn't acquire with a newer Frame Home.
|
||||||
|
struct Use { uint32_t slots = 0; uint64_t frame[envshare::kSlots] = {}; }; // a command buffer's draws: which slots, which frames
|
||||||
|
struct Gen {
|
||||||
|
uint32_t generation = 0;
|
||||||
|
uint32_t layers = 1;
|
||||||
|
bool subsampled = false; // drawn with the subsampled twins (and their set layout)
|
||||||
|
VkImage image[envshare::kSlots] = {};
|
||||||
|
VkDeviceMemory mem[envshare::kSlots] = {};
|
||||||
|
VkImageView view[envshare::kSlots] = {};
|
||||||
|
VkDescriptorPool pool = VK_NULL_HANDLE; // this generation's own (its sets go with it)
|
||||||
|
VkDescriptorSet set[envshare::kSlots] = {};
|
||||||
|
std::unordered_map<VkCommandBuffer, Use> users; // recorded with these sets and not yet re-begun/reset/freed
|
||||||
|
VkCommandBuffer acquire[envshare::kSlots] = {}; // the slot's acquire barrier (for acquireFamily[k])
|
||||||
|
uint32_t acquireFamily[envshare::kSlots] = {};
|
||||||
|
uint64_t acquiredFrame[envshare::kSlots] = {}; // the Frame Home frame last acquired in the slot (0: none)
|
||||||
|
};
|
||||||
|
constexpr size_t kMaxRetired = 2; // a compositor that never re-records a command buffer can't pile generations up
|
||||||
|
|
||||||
struct Ours {
|
struct Ours {
|
||||||
DeviceData* d = nullptr;
|
DeviceData* d = nullptr;
|
||||||
VkShaderModule fragment = VK_NULL_HANDLE;
|
VkShaderModule fragment = VK_NULL_HANDLE;
|
||||||
VkDescriptorSetLayout setLayout = VK_NULL_HANDLE;
|
VkDescriptorSetLayout setLayout = VK_NULL_HANDLE;
|
||||||
VkDescriptorSetLayout setLayoutSubsampled = VK_NULL_HANDLE; // (null: this device can't sample subsampled images)
|
VkDescriptorSetLayout setLayoutSubsampled = VK_NULL_HANDLE; // (null: this device can't sample subsampled images)
|
||||||
VkDescriptorPool pool = VK_NULL_HANDLE;
|
|
||||||
VkSampler sampler = VK_NULL_HANDLE, subsampledSampler = VK_NULL_HANDLE;
|
VkSampler sampler = VK_NULL_HANDLE, subsampledSampler = VK_NULL_HANDLE;
|
||||||
// The imported frames (one generation per Hello).
|
// Each slot's uniforms are a ring of kUboRing blocks, one per compositor frame (a dynamic offset picks it): what a
|
||||||
struct Slot { VkImage image = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE;
|
// frame is drawn with changes every compositor frame (the turn carried on, the fade), and the block the last one is
|
||||||
VkBuffer ubo = VK_NULL_HANDLE; VkDeviceMemory uboMem = VK_NULL_HANDLE; void* uboMapped = nullptr;
|
// still drawing with on the GPU mustn't change under it. Shared by every generation (only the offset is bound).
|
||||||
VkDescriptorSet set = VK_NULL_HANDLE, setSubsampled = VK_NULL_HANDLE; uint64_t uboFrame = ~0ull; };
|
struct Ubo { VkBuffer buffer = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; void* mapped = nullptr; };
|
||||||
Slot slot[envshare::kSlots];
|
Ubo ubo[envshare::kSlots];
|
||||||
bool imported = false;
|
Gen* cur = nullptr; // the generation drawn now
|
||||||
bool subsampled = false; // the imported images are subsampled (drawn with the subsampled twins)
|
std::vector<Gen*> retired; // older ones, until no command buffer uses them
|
||||||
uint32_t generation = 0; // of the imported images
|
uint32_t generation = 0; // of the imported images
|
||||||
int lastGood = -1; // newest slot of this generation drawn (shown while Frame Home resizes)
|
int lastGood = -1; // newest slot of this generation drawn (shown while Frame Home resizes)
|
||||||
|
// What each slot was rendered with, copied (consistently: env_share.h readSlot) when the layer last saw it newest
|
||||||
|
// in this generation. Drawn from this copy, never from the shared slot: after a resize Frame Home rewrites the
|
||||||
|
// slot the layer is still showing from the old images with the new generation's pose.
|
||||||
|
envshare::Slot shown[envshare::kSlots];
|
||||||
|
envshare::Motion shownMotion[envshare::kSlots];
|
||||||
envshare::State* state = nullptr;
|
envshare::State* state = nullptr;
|
||||||
size_t stateSize = 0;
|
size_t stateSize = 0;
|
||||||
|
bool hasMotion = false; // the State says how the view moves (a v6 Frame Home; Motion)
|
||||||
|
bool seqlocked = false; // the State's slots are written under sequence counts (a v7 State)
|
||||||
uint64_t healthyFrames = 0;
|
uint64_t healthyFrames = 0;
|
||||||
bool baseReady = false, baseFailed = false; // ensureBase(): all made / gave up (the rest of the session is Aurora's)
|
bool baseReady = false, baseFailed = false; // ensureBase(): all made / gave up (the rest of the session is Aurora's)
|
||||||
|
std::unordered_map<uint32_t, VkCommandPool> acquirePools; // per queue family: the generations' acquire[] command buffers
|
||||||
|
uint64_t acquires = 0; // frames acquired (logged as the device goes)
|
||||||
} g_ours;
|
} g_ours;
|
||||||
std::mutex g_oursMutex;
|
std::mutex g_oursMutex;
|
||||||
|
std::atomic<bool> g_imported{false}; // g_ours.cur is set (read unlocked by the draw hooks as a quick "nothing to do")
|
||||||
int g_latchSlot = -1; // frameSlot(): this compositor frame's pick, when it was made, and whether the
|
int g_latchSlot = -1; // frameSlot(): this compositor frame's pick, when it was made, and whether the
|
||||||
uint64_t g_latchNs = 0; // home was drawn with it (sampledNs stamped)
|
uint64_t g_latchNs = 0; // home was drawn with it (sampledNs stamped, its uniforms filled in)
|
||||||
bool g_latchStamped = false;
|
bool g_latchStamped = false;
|
||||||
|
constexpr uint32_t kUboRing = 8, kUboStride = 256; // uniform blocks per slot (compositor frames in flight are fewer)
|
||||||
|
uint32_t g_latchRing = 0; // this compositor frame's block
|
||||||
|
std::unordered_map<VkCommandBuffer, VkCommandPool> g_cbPool; // (g_oursMutex) which pool each command buffer is from
|
||||||
|
|
||||||
// A Hello from Frame Home waiting to be imported on the compositor's thread.
|
// A Hello from Frame Home waiting to be imported on the compositor's thread.
|
||||||
struct Pending { bool have = false; envshare::Hello hello; int fds[envshare::kSlots] = {-1, -1, -1, -1}; int stateFd = -1; } g_pending;
|
struct Pending { bool have = false; envshare::Hello hello; int fds[envshare::kSlots] = {-1, -1, -1, -1}; int stateFd = -1; } g_pending;
|
||||||
@@ -238,23 +294,95 @@ uint32_t memoryTypeFor(DeviceData* d, uint32_t bits, VkMemoryPropertyFlags want)
|
|||||||
return UINT32_MAX;
|
return UINT32_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
void releaseImports() {
|
// A descriptor pool with room for one generation's sets of the given kind, and the sets. A subsampled combined image
|
||||||
|
// sampler takes half again a plain one's room, and Turnip sizes a pool for that only when the device enables the
|
||||||
|
// fragmentDensityMap feature (left off: it would cost the compositor elsewhere). So the count asks for the room
|
||||||
|
// (2 * kSlots plain-sized ones covers kSlots subsampled).
|
||||||
|
bool makeSets(DeviceData* d, VkDescriptorSetLayout layout, VkDescriptorPool& pool, VkDescriptorSet* sets) {
|
||||||
|
VkDescriptorPoolSize ps[2] = {{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2 * envshare::kSlots}, {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, envshare::kSlots}};
|
||||||
|
VkDescriptorPoolCreateInfo pi = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||||
|
pi.maxSets = envshare::kSlots;
|
||||||
|
pi.poolSizeCount = 2;
|
||||||
|
pi.pPoolSizes = ps;
|
||||||
|
pool = VK_NULL_HANDLE;
|
||||||
|
if (d->CreateDescriptorPool(d->device, &pi, nullptr, &pool) != VK_SUCCESS) { pool = VK_NULL_HANDLE; return false; }
|
||||||
|
VkDescriptorSetLayout layouts[envshare::kSlots];
|
||||||
|
for (auto& l : layouts) l = layout;
|
||||||
|
VkDescriptorSetAllocateInfo dai = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||||
|
dai.descriptorPool = pool;
|
||||||
|
dai.descriptorSetCount = envshare::kSlots;
|
||||||
|
dai.pSetLayouts = layouts;
|
||||||
|
if (d->AllocateDescriptorSets(d->device, &dai, sets) != VK_SUCCESS) {
|
||||||
|
d->DestroyDescriptorPool(d->device, pool, nullptr);
|
||||||
|
pool = VK_NULL_HANDLE;
|
||||||
|
for (uint32_t i = 0; i < envshare::kSlots; ++i) sets[i] = VK_NULL_HANDLE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// (g_oursMutex held, once it has acquire command buffers) None of those is pending by now: each was submitted ahead
|
||||||
|
// of a command buffer among the generation's users, and those are done.
|
||||||
|
void destroyGen(DeviceData* d, Gen* g) {
|
||||||
|
if (!g) return;
|
||||||
|
for (uint32_t i = 0; i < envshare::kSlots; ++i)
|
||||||
|
if (g->acquire[i]) {
|
||||||
|
auto p = g_ours.acquirePools.find(g->acquireFamily[i]);
|
||||||
|
if (p != g_ours.acquirePools.end()) d->FreeCommandBuffers(d->device, p->second, 1, &g->acquire[i]);
|
||||||
|
}
|
||||||
|
if (g->pool) d->DestroyDescriptorPool(d->device, g->pool, nullptr); // (its sets with it)
|
||||||
|
for (uint32_t i = 0; i < envshare::kSlots; ++i) {
|
||||||
|
if (g->view[i]) d->DestroyImageView(d->device, g->view[i], nullptr);
|
||||||
|
if (g->image[i]) d->DestroyImage(d->device, g->image[i], nullptr);
|
||||||
|
if (g->mem[i]) d->FreeMemory(d->device, g->mem[i], nullptr);
|
||||||
|
}
|
||||||
|
delete g;
|
||||||
|
}
|
||||||
|
|
||||||
|
// (g_oursMutex held) Destroys the retired generations no command buffer uses any more. More than kMaxRetired still
|
||||||
|
// in use (command buffers recorded once and never re-recorded): the oldest goes after the device idles, as the layer
|
||||||
|
// always did before generations had their own sets.
|
||||||
|
void collectRetired() {
|
||||||
DeviceData* d = g_ours.d;
|
DeviceData* d = g_ours.d;
|
||||||
if (!d) return;
|
if (!d) return;
|
||||||
for (auto& s : g_ours.slot) {
|
auto& r = g_ours.retired;
|
||||||
if (s.view) d->DestroyImageView(d->device, s.view, nullptr);
|
for (size_t i = 0; i < r.size();) {
|
||||||
if (s.image) d->DestroyImage(d->device, s.image, nullptr);
|
if (r[i]->users.empty()) { destroyGen(d, r[i]); r.erase(r.begin() + static_cast<std::ptrdiff_t>(i)); }
|
||||||
if (s.mem) d->FreeMemory(d->device, s.mem, nullptr);
|
else ++i;
|
||||||
s.view = VK_NULL_HANDLE; s.image = VK_NULL_HANDLE; s.mem = VK_NULL_HANDLE;
|
|
||||||
s.uboFrame = ~0ull;
|
|
||||||
}
|
}
|
||||||
|
if (r.size() > kMaxRetired) {
|
||||||
|
logf("generation %u: %zu command buffers still hold it; waiting for the GPU to free it", r.front()->generation, r.front()->users.size());
|
||||||
|
d->DeviceWaitIdle(d->device);
|
||||||
|
destroyGen(d, r.front());
|
||||||
|
r.erase(r.begin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A command buffer re-begun, reset or freed (none of which may happen while it's pending): no generation is held by it.
|
||||||
|
void forgetCommandBuffer(VkCommandBuffer cb) {
|
||||||
|
std::lock_guard<std::mutex> ol(g_oursMutex);
|
||||||
|
if (g_ours.cur) g_ours.cur->users.erase(cb);
|
||||||
|
for (Gen* g : g_ours.retired) g->users.erase(cb);
|
||||||
|
}
|
||||||
|
void forgetPool(VkCommandPool pool, bool destroyed) {
|
||||||
|
std::lock_guard<std::mutex> ol(g_oursMutex);
|
||||||
|
auto drop = [&](Gen* g) { for (auto it = g->users.begin(); it != g->users.end();) { auto p = g_cbPool.find(it->first); if (p != g_cbPool.end() && p->second == pool) it = g->users.erase(it); else ++it; } };
|
||||||
|
if (g_ours.cur) drop(g_ours.cur);
|
||||||
|
for (Gen* g : g_ours.retired) drop(g);
|
||||||
|
if (destroyed) for (auto it = g_cbPool.begin(); it != g_cbPool.end();) it = it->second == pool ? g_cbPool.erase(it) : std::next(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (g_oursMutex held) The current generation stops being drawn: it waits among the retired for its command buffers.
|
||||||
|
void retireCurrent() {
|
||||||
|
if (g_ours.cur) g_ours.retired.push_back(g_ours.cur);
|
||||||
|
g_ours.cur = nullptr;
|
||||||
|
g_imported = false;
|
||||||
if (g_ours.state) ::munmap(g_ours.state, g_ours.stateSize);
|
if (g_ours.state) ::munmap(g_ours.state, g_ours.stateSize);
|
||||||
g_ours.state = nullptr;
|
g_ours.state = nullptr;
|
||||||
g_ours.imported = false;
|
|
||||||
g_ours.lastGood = -1;
|
g_ours.lastGood = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One-time objects (shader, set layout, pool, sampler, per-slot UBOs and sets) on the compositor's device.
|
// One-time objects (shader, set layouts, sampler, per-slot UBOs) on the compositor's device.
|
||||||
bool ensureBase(DeviceData* d) {
|
bool ensureBase(DeviceData* d) {
|
||||||
if (g_ours.baseReady) return g_ours.d == d; // made on the compositor's device: only that one draws
|
if (g_ours.baseReady) return g_ours.d == d; // made on the compositor's device: only that one draws
|
||||||
if (g_ours.baseFailed) return false;
|
if (g_ours.baseFailed) return false;
|
||||||
@@ -268,7 +396,7 @@ bool ensureBase(DeviceData* d) {
|
|||||||
if (d->CreateShaderModule(d->device, &smi, nullptr, &g_ours.fragment) != VK_SUCCESS) return false;
|
if (d->CreateShaderModule(d->device, &smi, nullptr, &g_ours.fragment) != VK_SUCCESS) return false;
|
||||||
VkDescriptorSetLayoutBinding b[2] = {};
|
VkDescriptorSetLayoutBinding b[2] = {};
|
||||||
b[0].binding = 0; b[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; b[0].descriptorCount = 1; b[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
b[0].binding = 0; b[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; b[0].descriptorCount = 1; b[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||||
b[1].binding = 1; b[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; b[1].descriptorCount = 1; b[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
b[1].binding = 1; b[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; b[1].descriptorCount = 1; b[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||||
VkDescriptorSetLayoutCreateInfo li = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
VkDescriptorSetLayoutCreateInfo li = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
||||||
li.bindingCount = 2;
|
li.bindingCount = 2;
|
||||||
li.pBindings = b;
|
li.pBindings = b;
|
||||||
@@ -287,47 +415,34 @@ bool ensureBase(DeviceData* d) {
|
|||||||
if (d->CreateDescriptorSetLayout(d->device, &sli, nullptr, &g_ours.setLayoutSubsampled) != VK_SUCCESS)
|
if (d->CreateDescriptorSetLayout(d->device, &sli, nullptr, &g_ours.setLayoutSubsampled) != VK_SUCCESS)
|
||||||
g_ours.setLayoutSubsampled = VK_NULL_HANDLE;
|
g_ours.setLayoutSubsampled = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
|
// Can a generation's worth of that kind really be allocated here? (no: no twins of that kind are made)
|
||||||
|
VkDescriptorPool probe = VK_NULL_HANDLE;
|
||||||
|
VkDescriptorSet probeSets[envshare::kSlots];
|
||||||
|
if (g_ours.setLayoutSubsampled && !makeSets(d, g_ours.setLayoutSubsampled, probe, probeSets)) {
|
||||||
|
d->DestroyDescriptorSetLayout(d->device, g_ours.setLayoutSubsampled, nullptr);
|
||||||
|
g_ours.setLayoutSubsampled = VK_NULL_HANDLE;
|
||||||
|
logf("subsampled frames: no descriptor set (plain frames only)");
|
||||||
|
}
|
||||||
|
if (probe) d->DestroyDescriptorPool(d->device, probe, nullptr);
|
||||||
logf(g_ours.setLayoutSubsampled ? "subsampled frames: readable" : "subsampled frames: no sampler/layout (plain frames only)");
|
logf(g_ours.setLayoutSubsampled ? "subsampled frames: readable" : "subsampled frames: no sampler/layout (plain frames only)");
|
||||||
}
|
}
|
||||||
// A subsampled combined image sampler takes half again a plain one's room, and Turnip sizes a pool for that only
|
|
||||||
// when the device enables the fragmentDensityMap feature (left off: it would cost the compositor elsewhere). So the
|
|
||||||
// count asks for the room: kSlots plain + kSlots subsampled = 2.5 * kSlots plain-sized ones (3 * kSlots to spare).
|
|
||||||
VkDescriptorPoolSize ps[2] = {{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 3 * envshare::kSlots}, {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2 * envshare::kSlots}};
|
|
||||||
VkDescriptorPoolCreateInfo pi = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
|
||||||
pi.maxSets = 2 * envshare::kSlots; // a set of each kind per slot
|
|
||||||
pi.poolSizeCount = 2;
|
|
||||||
pi.pPoolSizes = ps;
|
|
||||||
if (d->CreateDescriptorPool(d->device, &pi, nullptr, &g_ours.pool) != VK_SUCCESS) return false;
|
|
||||||
VkSamplerCreateInfo si = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
VkSamplerCreateInfo si = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
||||||
si.magFilter = si.minFilter = VK_FILTER_LINEAR;
|
si.magFilter = si.minFilter = VK_FILTER_LINEAR;
|
||||||
si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
if (d->CreateSampler(d->device, &si, nullptr, &g_ours.sampler) != VK_SUCCESS) return false;
|
if (d->CreateSampler(d->device, &si, nullptr, &g_ours.sampler) != VK_SUCCESS) return false;
|
||||||
for (auto& s : g_ours.slot) {
|
for (auto& u : g_ours.ubo) {
|
||||||
VkBufferCreateInfo bi = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
VkBufferCreateInfo bi = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||||
bi.size = 256;
|
bi.size = kUboRing * kUboStride; // (256: no device needs uniform offsets aligned to more)
|
||||||
bi.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
|
bi.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
|
||||||
if (d->CreateBuffer(d->device, &bi, nullptr, &s.ubo) != VK_SUCCESS) return false;
|
if (d->CreateBuffer(d->device, &bi, nullptr, &u.buffer) != VK_SUCCESS) return false;
|
||||||
VkMemoryRequirements req;
|
VkMemoryRequirements req;
|
||||||
d->GetBufferMemoryRequirements(d->device, s.ubo, &req);
|
d->GetBufferMemoryRequirements(d->device, u.buffer, &req);
|
||||||
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||||
ai.allocationSize = req.size;
|
ai.allocationSize = req.size;
|
||||||
ai.memoryTypeIndex = memoryTypeFor(d, req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
ai.memoryTypeIndex = memoryTypeFor(d, req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||||||
if (ai.memoryTypeIndex == UINT32_MAX || d->AllocateMemory(d->device, &ai, nullptr, &s.uboMem) != VK_SUCCESS) return false;
|
if (ai.memoryTypeIndex == UINT32_MAX || d->AllocateMemory(d->device, &ai, nullptr, &u.mem) != VK_SUCCESS) return false;
|
||||||
d->BindBufferMemory(d->device, s.ubo, s.uboMem, 0);
|
d->BindBufferMemory(d->device, u.buffer, u.mem, 0);
|
||||||
d->MapMemory(d->device, s.uboMem, 0, VK_WHOLE_SIZE, 0, &s.uboMapped);
|
if (d->MapMemory(d->device, u.mem, 0, VK_WHOLE_SIZE, 0, &u.mapped) != VK_SUCCESS) return false;
|
||||||
VkDescriptorSetAllocateInfo dai = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
|
||||||
dai.descriptorPool = g_ours.pool;
|
|
||||||
dai.descriptorSetCount = 1;
|
|
||||||
dai.pSetLayouts = &g_ours.setLayout;
|
|
||||||
if (d->AllocateDescriptorSets(d->device, &dai, &s.set) != VK_SUCCESS) return false;
|
|
||||||
if (g_ours.setLayoutSubsampled) {
|
|
||||||
dai.pSetLayouts = &g_ours.setLayoutSubsampled;
|
|
||||||
if (d->AllocateDescriptorSets(d->device, &dai, &s.setSubsampled) != VK_SUCCESS) {
|
|
||||||
s.setSubsampled = VK_NULL_HANDLE;
|
|
||||||
g_ours.setLayoutSubsampled = VK_NULL_HANDLE; // (no twins of that kind are made: plain frames only)
|
|
||||||
logf("subsampled frames: no descriptor set (plain frames only)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
g_ours.baseFailed = false;
|
g_ours.baseFailed = false;
|
||||||
g_ours.baseReady = true;
|
g_ours.baseReady = true;
|
||||||
@@ -335,7 +450,7 @@ bool ensureBase(DeviceData* d) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Imports a Hello's images (compositor thread, inside QueueSubmit: nothing of ours is in flight once the queue idles).
|
// Imports a Hello's images (compositor thread, inside QueueSubmit) as a new generation beside the one showing.
|
||||||
void armHealth();
|
void armHealth();
|
||||||
|
|
||||||
void importPending(DeviceData* d, VkQueue) {
|
void importPending(DeviceData* d, VkQueue) {
|
||||||
@@ -356,35 +471,44 @@ void importPending(DeviceData* d, VkQueue) {
|
|||||||
d->inst->GetPhysicalDeviceProperties2(d->physical, &pp);
|
d->inst->GetPhysicalDeviceProperties2(d->physical, &pp);
|
||||||
if (std::memcmp(idp.deviceUUID, p.hello.deviceUUID, 16) != 0) { logf("frames from another GPU: ignored"); closeAll(); return; }
|
if (std::memcmp(idp.deviceUUID, p.hello.deviceUUID, 16) != 0) { logf("frames from another GPU: ignored"); closeAll(); return; }
|
||||||
// The new frames are made beside the ones showing, which stay until all of them import: a failed import keeps
|
// The new frames are made beside the ones showing, which stay until all of them import: a failed import keeps
|
||||||
// the last ones (the sets in this very submit never point at destroyed images).
|
// the last ones.
|
||||||
const size_t stateSize = sizeof(envshare::State);
|
// How much State there is: a v5 Frame Home's ends before Motion, a v6 one's before the sequence counts (v7).
|
||||||
|
struct stat sst = {};
|
||||||
|
const bool statOk = ::fstat(p.stateFd, &sst) == 0 && sst.st_size >= 0;
|
||||||
|
const envshare::StateLayout layout = envshare::stateLayout(p.hello.version, statOk ? static_cast<uint64_t>(sst.st_size) : 0);
|
||||||
|
const bool hasMotion = layout.motion;
|
||||||
|
const size_t stateSize = layout.size;
|
||||||
|
// The State must really be that large: touching a mapping past the end of its file is SIGBUS, which would take
|
||||||
|
// the compositor down. And the images must be ones Vulkan can make (a memory type bit, layers to draw).
|
||||||
|
if (!statOk || !stateSize || p.hello.memoryType >= 32 || p.hello.layers < 1 ||
|
||||||
|
p.hello.layers > 2 || !p.hello.width || !p.hello.height) {
|
||||||
|
logf("bad hello from Frame Home (state %lld bytes, memory type %u, %u layers, %ux%u): ignored", static_cast<long long>(sst.st_size),
|
||||||
|
p.hello.memoryType, p.hello.layers, p.hello.width, p.hello.height);
|
||||||
|
closeAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
void* st = ::mmap(nullptr, stateSize, PROT_READ | PROT_WRITE, MAP_SHARED, p.stateFd, 0);
|
void* st = ::mmap(nullptr, stateSize, PROT_READ | PROT_WRITE, MAP_SHARED, p.stateFd, 0);
|
||||||
::close(p.stateFd);
|
::close(p.stateFd);
|
||||||
p.stateFd = -1;
|
p.stateFd = -1;
|
||||||
if (st == MAP_FAILED) { logf("cannot map the shared state"); closeAll(); return; }
|
if (st == MAP_FAILED) { logf("cannot map the shared state"); closeAll(); return; }
|
||||||
|
// Sequence counts only when the State is v7 throughout (an older Frame Home's slots are copied twice and compared).
|
||||||
|
const bool seqlocked = envshare::seqlocked(static_cast<envshare::State*>(st), stateSize);
|
||||||
// Subsampled frames need this device to sample them (the extension, the sampler and the twins made for them):
|
// Subsampled frames need this device to sample them (the extension, the sampler and the twins made for them):
|
||||||
// otherwise Frame Home is told, and sends plain ones.
|
// otherwise Frame Home is told, and sends plain ones.
|
||||||
const bool subsampled = (p.hello.flags & VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT) != 0;
|
const bool subsampled = (p.hello.flags & VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT) != 0;
|
||||||
bool subsampledSets = g_ours.setLayoutSubsampled != VK_NULL_HANDLE;
|
if (p.hello.flags & ~static_cast<uint32_t>(VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT) || (subsampled && !g_ours.setLayoutSubsampled)) {
|
||||||
for (const auto& s : g_ours.slot) subsampledSets &= s.setSubsampled != VK_NULL_HANDLE;
|
|
||||||
if (p.hello.flags & ~static_cast<uint32_t>(VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT) || (subsampled && !subsampledSets)) {
|
|
||||||
static_cast<envshare::State*>(st)->refusedFlags.store(p.hello.flags);
|
static_cast<envshare::State*>(st)->refusedFlags.store(p.hello.flags);
|
||||||
logf("frames with create flags 0x%x: can't read them here (Frame Home will send plain ones)", p.hello.flags);
|
logf("frames with create flags 0x%x: can't read them here (Frame Home will send plain ones)", p.hello.flags);
|
||||||
::munmap(st, stateSize);
|
::munmap(st, stateSize);
|
||||||
closeAll();
|
closeAll();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
struct Made { VkImage image = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; } made[envshare::kSlots];
|
Gen* g = new Gen();
|
||||||
auto dropMade = [&] {
|
g->generation = p.hello.generation;
|
||||||
for (Made& m : made) {
|
g->layers = p.hello.layers;
|
||||||
if (m.view) d->DestroyImageView(d->device, m.view, nullptr);
|
g->subsampled = subsampled;
|
||||||
if (m.image) d->DestroyImage(d->device, m.image, nullptr);
|
auto dropMade = [&] { destroyGen(d, g); ::munmap(st, stateSize); };
|
||||||
if (m.mem) d->FreeMemory(d->device, m.mem, nullptr);
|
|
||||||
}
|
|
||||||
::munmap(st, stateSize);
|
|
||||||
};
|
|
||||||
for (uint32_t i = 0; i < envshare::kSlots; ++i) {
|
for (uint32_t i = 0; i < envshare::kSlots; ++i) {
|
||||||
Made& s = made[i];
|
|
||||||
VkExternalMemoryImageCreateInfo ext = {VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
|
VkExternalMemoryImageCreateInfo ext = {VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
|
||||||
ext.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
|
ext.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
|
||||||
VkImageCreateInfo ci = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
VkImageCreateInfo ci = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||||
@@ -399,59 +523,70 @@ void importPending(DeviceData* d, VkQueue) {
|
|||||||
ci.usage = p.hello.usage; // exactly as Frame Home created them (the import must match)
|
ci.usage = p.hello.usage; // exactly as Frame Home created them (the import must match)
|
||||||
ci.flags = p.hello.flags;
|
ci.flags = p.hello.flags;
|
||||||
ci.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
ci.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
bool ok = d->CreateImage(d->device, &ci, nullptr, &s.image) == VK_SUCCESS;
|
bool ok = d->CreateImage(d->device, &ci, nullptr, &g->image[i]) == VK_SUCCESS;
|
||||||
|
if (!ok) g->image[i] = VK_NULL_HANDLE;
|
||||||
if (ok) {
|
if (ok) {
|
||||||
VkMemoryRequirements req;
|
VkMemoryRequirements req;
|
||||||
d->GetImageMemoryRequirements(d->device, s.image, &req);
|
d->GetImageMemoryRequirements(d->device, g->image[i], &req);
|
||||||
VkImportMemoryFdInfoKHR imp = {VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR};
|
VkImportMemoryFdInfoKHR imp = {VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR};
|
||||||
imp.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
|
imp.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
|
||||||
imp.fd = p.fds[i];
|
imp.fd = p.fds[i];
|
||||||
VkMemoryDedicatedAllocateInfo ded = {VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
|
VkMemoryDedicatedAllocateInfo ded = {VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
|
||||||
ded.image = s.image;
|
ded.image = g->image[i];
|
||||||
if (p.hello.dedicated) imp.pNext = &ded;
|
if (p.hello.dedicated) imp.pNext = &ded;
|
||||||
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||||
ai.pNext = &imp;
|
ai.pNext = &imp;
|
||||||
ai.allocationSize = p.hello.memorySize[i];
|
ai.allocationSize = p.hello.memorySize[i];
|
||||||
ai.memoryTypeIndex = p.hello.memoryType;
|
ai.memoryTypeIndex = p.hello.memoryType;
|
||||||
ok = (req.memoryTypeBits & (1u << p.hello.memoryType)) && req.size <= p.hello.memorySize[i] &&
|
ok = (req.memoryTypeBits & (1u << p.hello.memoryType)) && req.size <= p.hello.memorySize[i] &&
|
||||||
d->AllocateMemory(d->device, &ai, nullptr, &s.mem) == VK_SUCCESS;
|
d->AllocateMemory(d->device, &ai, nullptr, &g->mem[i]) == VK_SUCCESS;
|
||||||
if (ok) p.fds[i] = -1; // the driver owns it now
|
if (ok) p.fds[i] = -1; // the driver owns it now
|
||||||
ok = ok && d->BindImageMemory(d->device, s.image, s.mem, 0) == VK_SUCCESS;
|
else g->mem[i] = VK_NULL_HANDLE;
|
||||||
|
ok = ok && d->BindImageMemory(d->device, g->image[i], g->mem[i], 0) == VK_SUCCESS;
|
||||||
}
|
}
|
||||||
if (ok) {
|
if (ok) {
|
||||||
VkImageViewCreateInfo vi = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
VkImageViewCreateInfo vi = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||||
vi.image = s.image;
|
vi.image = g->image[i];
|
||||||
vi.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
vi.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||||
vi.format = ci.format;
|
vi.format = ci.format;
|
||||||
vi.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, p.hello.layers};
|
vi.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, p.hello.layers};
|
||||||
ok = d->CreateImageView(d->device, &vi, nullptr, &s.view) == VK_SUCCESS;
|
ok = d->CreateImageView(d->device, &vi, nullptr, &g->view[i]) == VK_SUCCESS;
|
||||||
|
if (!ok) g->view[i] = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
if (!ok) { logf("importing frame slot %u failed: keeping the last frames", i); dropMade(); closeAll(); return; }
|
if (!ok) { logf("importing frame slot %u failed: keeping the last frames", i); dropMade(); closeAll(); return; }
|
||||||
}
|
}
|
||||||
closeAll();
|
closeAll();
|
||||||
std::lock_guard<std::mutex> ol(g_oursMutex);
|
// This generation's own sets, written now, before any command buffer can bind them (and never written again).
|
||||||
// Swap: nothing of the compositor's (any queue) may still be reading the old frames.
|
if (!makeSets(d, subsampled ? g_ours.setLayoutSubsampled : g_ours.setLayout, g->pool, g->set)) {
|
||||||
if (g_ours.imported) { d->DeviceWaitIdle(d->device); releaseImports(); }
|
logf("no descriptor sets for the new frames: keeping the last frames");
|
||||||
g_ours.state = static_cast<envshare::State*>(st);
|
dropMade();
|
||||||
g_ours.stateSize = stateSize;
|
return;
|
||||||
|
}
|
||||||
for (uint32_t i = 0; i < envshare::kSlots; ++i) {
|
for (uint32_t i = 0; i < envshare::kSlots; ++i) {
|
||||||
auto& s = g_ours.slot[i];
|
VkDescriptorImageInfo ii = {g_ours.sampler, g->view[i], VK_IMAGE_LAYOUT_GENERAL}; // (the subsampled kind's sampler is in its layout)
|
||||||
s.image = made[i].image; s.mem = made[i].mem; s.view = made[i].view;
|
VkDescriptorBufferInfo bi = {g_ours.ubo[i].buffer, 0, kUboStride};
|
||||||
VkDescriptorImageInfo ii = {g_ours.sampler, s.view, VK_IMAGE_LAYOUT_GENERAL}; // (the subsampled kind's sampler is in its layout)
|
|
||||||
VkDescriptorBufferInfo bi = {s.ubo, 0, 256};
|
|
||||||
const VkDescriptorSet set = subsampled ? s.setSubsampled : s.set;
|
|
||||||
VkWriteDescriptorSet w[2] = {{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}, {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}};
|
VkWriteDescriptorSet w[2] = {{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}, {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}};
|
||||||
w[0].dstSet = set; w[0].dstBinding = 0; w[0].descriptorCount = 1; w[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; w[0].pImageInfo = ⅈ
|
w[0].dstSet = g->set[i]; w[0].dstBinding = 0; w[0].descriptorCount = 1; w[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; w[0].pImageInfo = ⅈ
|
||||||
w[1].dstSet = set; w[1].dstBinding = 1; w[1].descriptorCount = 1; w[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; w[1].pBufferInfo = &bi;
|
w[1].dstSet = g->set[i]; w[1].dstBinding = 1; w[1].descriptorCount = 1; w[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; w[1].pBufferInfo = &bi;
|
||||||
d->UpdateDescriptorSets(d->device, 2, w, 0, nullptr);
|
d->UpdateDescriptorSets(d->device, 2, w, 0, nullptr);
|
||||||
}
|
}
|
||||||
g_ours.imported = true;
|
std::lock_guard<std::mutex> ol(g_oursMutex);
|
||||||
g_ours.subsampled = subsampled;
|
// Swap: the old generation stays alive (images, views, sets) until the command buffers drawn with it are done.
|
||||||
logf("imported Frame Home's frames: %ux%u, usage 0x%x, flags 0x%x (generation %u)", p.hello.width, p.hello.height, p.hello.usage,
|
retireCurrent();
|
||||||
p.hello.flags, p.hello.generation);
|
g_ours.cur = g;
|
||||||
|
g_ours.state = static_cast<envshare::State*>(st);
|
||||||
|
g_ours.stateSize = stateSize;
|
||||||
|
g_ours.hasMotion = hasMotion;
|
||||||
|
g_ours.seqlocked = seqlocked;
|
||||||
g_ours.generation = p.hello.generation;
|
g_ours.generation = p.hello.generation;
|
||||||
g_ours.lastGood = -1;
|
g_ours.lastGood = -1;
|
||||||
|
for (auto& s : g_ours.shown) s = envshare::Slot();
|
||||||
|
for (auto& m : g_ours.shownMotion) m = envshare::Motion();
|
||||||
g_latchNs = 0;
|
g_latchNs = 0;
|
||||||
|
g_imported = true;
|
||||||
|
collectRetired();
|
||||||
|
logf("imported Frame Home's frames: %ux%u, usage 0x%x, flags 0x%x (generation %u%s)", p.hello.width, p.hello.height, p.hello.usage,
|
||||||
|
p.hello.flags, p.hello.generation, seqlocked ? "" : ", an older Frame Home: slots without sequence counts");
|
||||||
armHealth();
|
armHealth();
|
||||||
logf("Frame Home connected: %ux%u frames, %u slots", p.hello.width, p.hello.height, envshare::kSlots);
|
logf("Frame Home connected: %ux%u frames, %u slots", p.hello.width, p.hello.height, envshare::kSlots);
|
||||||
}
|
}
|
||||||
@@ -459,16 +594,27 @@ void importPending(DeviceData* d, VkQueue) {
|
|||||||
// The slot to draw now, or -1 for Aurora. While Frame Home is connected the home stays up: a late frame, a home
|
// The slot to draw now, or -1 for Aurora. While Frame Home is connected the home stays up: a late frame, a home
|
||||||
// loading or new images after a resize keep the last good frame on screen (Aurora flashing in looked broken).
|
// loading or new images after a resize keep the last good frame on screen (Aurora flashing in looked broken).
|
||||||
// Only Frame Home gone, or frozen for 5 s (hung), brings Aurora back.
|
// Only Frame Home gone, or frozen for 5 s (hung), brings Aurora back.
|
||||||
std::chrono::steady_clock::time_point g_disconnectedAt;
|
// (written by the socket thread, read by the compositor's: steady_clock ticks, atomic)
|
||||||
int liveSlot() {
|
std::atomic<std::chrono::steady_clock::rep> g_disconnectedAt{0};
|
||||||
if (g_off || !g_ours.imported || !g_ours.state) return -1;
|
int liveSlot() { // (g_oursMutex held)
|
||||||
|
if (g_off || !g_ours.cur || !g_ours.state) return -1;
|
||||||
if (!g_connected) // Frame Home restarting (a setting that rebuilds the renderer): hold its last frame a moment
|
if (!g_connected) // Frame Home restarting (a setting that rebuilds the renderer): hold its last frame a moment
|
||||||
return std::chrono::steady_clock::now() - g_disconnectedAt < std::chrono::seconds(3) ? g_ours.lastGood : -1;
|
return std::chrono::steady_clock::now() - std::chrono::steady_clock::time_point(std::chrono::steady_clock::duration(g_disconnectedAt.load())) <
|
||||||
|
std::chrono::seconds(3) ? g_ours.lastGood : -1;
|
||||||
envshare::State* s = g_ours.state;
|
envshare::State* s = g_ours.state;
|
||||||
const uint64_t now = envshare::nowNs();
|
const uint64_t now = envshare::nowNs();
|
||||||
if (now - s->publishNs.load() > 5000000000ull && now > s->busyUntilNs.load()) return -1;
|
// (signed: a frame published between reading the clock and reading publishNs is newer than `now`, not 584 years old)
|
||||||
|
if (static_cast<int64_t>(now - s->publishNs.load()) > 5000000000ll && now > s->busyUntilNs.load()) return -1;
|
||||||
const uint32_t k = s->latest.load(std::memory_order_acquire);
|
const uint32_t k = s->latest.load(std::memory_order_acquire);
|
||||||
if (k < envshare::kSlots && s->slot[k].generation == g_ours.generation) g_ours.lastGood = static_cast<int>(k);
|
// The newest slot, if it's of the images we have: copied whole (a slot being rewritten keeps the last copy).
|
||||||
|
envshare::Slot sl;
|
||||||
|
envshare::Motion m;
|
||||||
|
if (k < envshare::kSlots && envshare::readSlot(s, k, g_ours.seqlocked, g_ours.hasMotion, sl, m) && sl.frame &&
|
||||||
|
sl.generation == g_ours.generation) {
|
||||||
|
g_ours.shown[k] = sl;
|
||||||
|
g_ours.shownMotion[k] = m;
|
||||||
|
g_ours.lastGood = static_cast<int>(k);
|
||||||
|
}
|
||||||
return g_ours.lastGood;
|
return g_ours.lastGood;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,28 +628,45 @@ int frameSlot() {
|
|||||||
g_latchSlot = liveSlot();
|
g_latchSlot = liveSlot();
|
||||||
g_latchNs = now;
|
g_latchNs = now;
|
||||||
g_latchStamped = false;
|
g_latchStamped = false;
|
||||||
|
g_latchRing = (g_latchRing + 1) % kUboRing;
|
||||||
return g_latchSlot;
|
return g_latchSlot;
|
||||||
}
|
}
|
||||||
|
|
||||||
void fillUbo(int k) {
|
// Slot k's uniforms for this compositor frame (block `ring`): the frame's eye rotations, the brightness now, and how
|
||||||
auto& slot = g_ours.slot[k];
|
// far to carry the frame on: by the rig's turn (exactly) and its walking (roughly) since the sample it was made for.
|
||||||
|
// Frame Home late or stopped: carried on 40 ms at most, so a frame left behind never keeps turning.
|
||||||
|
// From the layer's own copy of the slot (liveSlot), which matches the images drawn.
|
||||||
|
void fillUbo(int k, uint32_t ring) {
|
||||||
envshare::State* s = g_ours.state;
|
envshare::State* s = g_ours.state;
|
||||||
const envshare::Slot& src = s->slot[k];
|
const envshare::Slot& src = g_ours.shown[k];
|
||||||
if (slot.uboFrame == src.frame) return;
|
float data[16 * 2 + 4 * 2 + 4 + 4 + 4] = {};
|
||||||
float data[16 * 2 + 4 * 2 + 4];
|
|
||||||
std::memcpy(data, src.viewProj, sizeof src.viewProj);
|
std::memcpy(data, src.viewProj, sizeof src.viewProj);
|
||||||
std::memcpy(data + 32, src.eyePos, sizeof src.eyePos);
|
std::memcpy(data + 32, src.eyePos, sizeof src.eyePos);
|
||||||
data[40] = s->fade.load() / 1000.0f;
|
data[40] = s->fade.load() / 1000.0f;
|
||||||
data[41] = s->show.load() ? 1.0f : 0.0f;
|
data[41] = s->show.load() ? 1.0f : 0.0f;
|
||||||
data[42] = data[43] = 0;
|
float turn = 0, depth = 0, dt = 0;
|
||||||
std::memcpy(slot.uboMapped, data, sizeof data);
|
const envshare::Motion m = g_ours.shownMotion[k];
|
||||||
slot.uboFrame = src.frame;
|
if (m.targetNs) {
|
||||||
|
dt = std::max(-0.02f, std::min(0.04f, static_cast<float>(static_cast<int64_t>(g_latchNs - m.targetNs) / 1e9)));
|
||||||
|
turn = m.yawRate * dt;
|
||||||
|
depth = m.depth;
|
||||||
|
}
|
||||||
|
data[44] = std::cos(turn);
|
||||||
|
data[45] = std::sin(turn);
|
||||||
|
for (int c = 0; c < 3; ++c) data[48 + c] = depth > 0 ? m.velocity[c] * dt : 0.0f;
|
||||||
|
// The walk carried on stays well short of the depth it's taken against (a dash is metres a second): the shader
|
||||||
|
// looks along dir * depth + walk, which turns sideways, then backwards, as the walk nears the depth.
|
||||||
|
const float walked = std::sqrt(data[48] * data[48] + data[49] * data[49] + data[50] * data[50]);
|
||||||
|
if (!(walked <= 0.5f * depth)) for (int c = 0; c < 3; ++c) data[48 + c] = walked > 0 && depth > 0 ? data[48 + c] * (0.5f * depth / walked) : 0.0f;
|
||||||
|
if (!std::isfinite(data[44]) || !std::isfinite(data[45])) { data[44] = 1; data[45] = 0; } // (a NaN turn rate: no turn)
|
||||||
|
data[51] = depth;
|
||||||
|
std::memcpy(static_cast<char*>(g_ours.ubo[k].mapped) + ring * kUboStride, data, sizeof data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── health: off switch and crash guard ──────────────────────────────────────────────────────────
|
// ── health: off switch and crash guard ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
void startHealth() {
|
void startHealth() {
|
||||||
if (exists(dir() + "/off")) { g_off = true; logf("off: Aurora as usual (delete %s/off to turn Frame Home back on)", dir().c_str()); return; }
|
if (exists(dir() + "/off")) { g_off = g_startedOff = true; logf("off: Aurora as usual (delete %s/off and restart SteamVR to turn Frame Home back on)", dir().c_str()); return; }
|
||||||
// `starting` left over = the last compositor died after the layer took Frame Home's frames in (armHealth) and
|
// `starting` left over = the last compositor died after the layer took Frame Home's frames in (armHealth) and
|
||||||
// before it got healthy showing them. A compositor that never got that far, or closed cleanly, doesn't count.
|
// before it got healthy showing them. A compositor that never got that far, or closed cleanly, doesn't count.
|
||||||
int crashes = 0;
|
int crashes = 0;
|
||||||
@@ -515,7 +678,7 @@ void startHealth() {
|
|||||||
if (crashes >= 2) {
|
if (crashes >= 2) {
|
||||||
if (FILE* f = std::fopen((dir() + "/off").c_str(), "w")) std::fclose(f);
|
if (FILE* f = std::fopen((dir() + "/off").c_str(), "w")) std::fclose(f);
|
||||||
::unlink((dir() + "/starting").c_str());
|
::unlink((dir() + "/starting").c_str());
|
||||||
g_off = true;
|
g_off = g_startedOff = true;
|
||||||
logf("the compositor stopped twice before getting healthy: turned myself off (Aurora as usual)");
|
logf("the compositor stopped twice before getting healthy: turned myself off (Aurora as usual)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -566,11 +729,11 @@ void serve() {
|
|||||||
if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_RIGHTS) {
|
if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_RIGHTS) {
|
||||||
const size_t cnt = (cm->cmsg_len - CMSG_LEN(0)) / sizeof(int);
|
const size_t cnt = (cm->cmsg_len - CMSG_LEN(0)) / sizeof(int);
|
||||||
const int* p = reinterpret_cast<const int*>(CMSG_DATA(cm));
|
const int* p = reinterpret_cast<const int*>(CMSG_DATA(cm));
|
||||||
fds.assign(p, p + cnt);
|
fds.insert(fds.end(), p, p + cnt); // (all of them: each is closed below if unused)
|
||||||
}
|
}
|
||||||
// v5: the images' create flags ride along too; v4: their usage (Frame Home draws straight into them), no
|
// v6: the State carries Motion (the Hello is v5's); v5: the images' create flags ride along too; v4: their
|
||||||
// flags; v3 (an older Frame Home): made for copying into.
|
// usage (Frame Home draws straight into them), no flags; v3 (an older Frame Home): made for copying into.
|
||||||
const bool v5 = n == static_cast<ssize_t>(sizeof h) && h.version == envshare::kVersion;
|
const bool v5 = n == static_cast<ssize_t>(sizeof h) && (h.version == envshare::kVersion || h.version == 5);
|
||||||
const bool v4 = n == static_cast<ssize_t>(envshare::kHelloV4Size) && h.version == 4;
|
const bool v4 = n == static_cast<ssize_t>(envshare::kHelloV4Size) && h.version == 4;
|
||||||
const bool v3 = n == static_cast<ssize_t>(envshare::kHelloV3Size) && h.version == 3;
|
const bool v3 = n == static_cast<ssize_t>(envshare::kHelloV3Size) && h.version == 3;
|
||||||
if (v3) h.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
if (v3) h.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||||
@@ -588,7 +751,7 @@ void serve() {
|
|||||||
for (uint32_t i = 0; i < envshare::kSlots; ++i) g_pending.fds[i] = fds[i];
|
for (uint32_t i = 0; i < envshare::kSlots; ++i) g_pending.fds[i] = fds[i];
|
||||||
g_pending.stateFd = fds[envshare::kSlots];
|
g_pending.stateFd = fds[envshare::kSlots];
|
||||||
}
|
}
|
||||||
g_disconnectedAt = std::chrono::steady_clock::now();
|
g_disconnectedAt = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||||
g_connected = false;
|
g_connected = false;
|
||||||
::close(c);
|
::close(c);
|
||||||
logf("Frame Home disconnected: Aurora until it's back");
|
logf("Frame Home disconnected: Aurora until it's back");
|
||||||
@@ -708,6 +871,7 @@ VKAPI_ATTR void VKAPI_CALL DestroyPipeline(VkDevice d, VkPipeline p, const VkAll
|
|||||||
if (it != g_twins.end()) { t = it->second; g_twins.erase(it); }
|
if (it != g_twins.end()) { t = it->second; g_twins.erase(it); }
|
||||||
g_skip.erase(p);
|
g_skip.erase(p);
|
||||||
g_camera.erase(p);
|
g_camera.erase(p);
|
||||||
|
g_names.erase(p); // (else every pipeline the compositor ever made keeps its name string)
|
||||||
}
|
}
|
||||||
for (const TwinPipeline& k : {t.plain, t.subsampled}) {
|
for (const TwinPipeline& k : {t.plain, t.subsampled}) {
|
||||||
if (k.pipeline) dd->DestroyPipeline(d, k.pipeline, nullptr);
|
if (k.pipeline) dd->DestroyPipeline(d, k.pipeline, nullptr);
|
||||||
@@ -724,9 +888,10 @@ VKAPI_ATTR void VKAPI_CALL CmdBindPipeline(VkCommandBuffer cb, VkPipelineBindPoi
|
|||||||
dev(cb)->CmdBindPipeline(cb, bp, p);
|
dev(cb)->CmdBindPipeline(cb, bp, p);
|
||||||
}
|
}
|
||||||
|
|
||||||
// What to do with a draw: 0 as is, 1 replace with the twin, 2 skip.
|
// What to do with a draw: 0 as is, 1 replace with the twin (with `set`, of the kind `subsampled`), 2 skip. A twin
|
||||||
int drawAction(VkCommandBuffer cb, Twin& t, int& slot) {
|
// drawn holds the generation whose set it binds until the command buffer is re-begun, reset or freed (Gen::users).
|
||||||
if (g_off || !g_ours.imported) return 0;
|
int drawAction(VkCommandBuffer cb, Twin& t, VkDescriptorSet& set, bool& subsampled, uint32_t& uboOffset) {
|
||||||
|
if (g_off || !g_imported) return 0;
|
||||||
VkPipeline p;
|
VkPipeline p;
|
||||||
bool skip;
|
bool skip;
|
||||||
{
|
{
|
||||||
@@ -741,29 +906,37 @@ int drawAction(VkCommandBuffer cb, Twin& t, int& slot) {
|
|||||||
}
|
}
|
||||||
if (!t.plain.pipeline && !skip) return 0;
|
if (!t.plain.pipeline && !skip) return 0;
|
||||||
std::lock_guard<std::mutex> ol(g_oursMutex);
|
std::lock_guard<std::mutex> ol(g_oursMutex);
|
||||||
slot = frameSlot();
|
const int slot = frameSlot();
|
||||||
if (slot < 0) return 0;
|
if (slot < 0 || !g_ours.cur) return 0;
|
||||||
// Passthrough on: the compositor would blend the home half over the cameras. Step aside for the real view.
|
// Passthrough on: the compositor would blend the home half over the cameras. Step aside for the real view.
|
||||||
if (envshare::nowNs() - g_cameraNs.load() < 150000000ull) return 2;
|
if (cameraRecent()) return 2;
|
||||||
if (!t.plain.pipeline) return 2;
|
if (!t.plain.pipeline) return 2;
|
||||||
if (g_ours.subsampled && !t.subsampled.pipeline) return 0; // (never imported so: import checks the twins can)
|
Gen* g = g_ours.cur;
|
||||||
fillUbo(slot);
|
if (g->subsampled && !t.subsampled.pipeline) return 0; // (never imported so: import checks the twins can)
|
||||||
if (!g_latchStamped) { g_latchStamped = true; g_ours.state->sampledNs.store(g_latchNs); } // the moment it took the frame
|
if (!g_latchStamped) { // the moment it took the frame
|
||||||
|
g_latchStamped = true;
|
||||||
|
g_ours.state->sampledNs.store(g_latchNs);
|
||||||
|
fillUbo(slot, g_latchRing);
|
||||||
|
}
|
||||||
|
uboOffset = g_latchRing * kUboStride;
|
||||||
|
set = g->set[slot];
|
||||||
|
subsampled = g->subsampled;
|
||||||
|
Use& u = g->users[cb];
|
||||||
|
u.slots |= 1u << slot;
|
||||||
|
u.frame[slot] = g_ours.shown[slot].frame; // (the frame it shows: acquired ahead of this command buffer)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawTwin(VkCommandBuffer cb, const Twin& t, int slot, const std::function<void()>& draw) {
|
void drawTwin(VkCommandBuffer cb, const Twin& t, VkDescriptorSet set, bool subsampled, uint32_t uboOffset, const std::function<void()>& draw) {
|
||||||
DeviceData* dd = dev(cb);
|
DeviceData* dd = dev(cb);
|
||||||
VkPipeline original;
|
VkPipeline original;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> l(g_mutex);
|
std::lock_guard<std::mutex> l(g_mutex);
|
||||||
original = g_bound[cb];
|
original = g_bound[cb];
|
||||||
}
|
}
|
||||||
const bool subsampled = g_ours.subsampled;
|
|
||||||
const TwinPipeline& k = subsampled ? t.subsampled : t.plain;
|
const TwinPipeline& k = subsampled ? t.subsampled : t.plain;
|
||||||
const VkDescriptorSet set = subsampled ? g_ours.slot[slot].setSubsampled : g_ours.slot[slot].set;
|
|
||||||
dd->CmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, k.pipeline);
|
dd->CmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, k.pipeline);
|
||||||
dd->CmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, k.layout, 1, 1, &set, 0, nullptr);
|
dd->CmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, k.layout, 1, 1, &set, 1, &uboOffset);
|
||||||
draw();
|
draw();
|
||||||
dd->CmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, original);
|
dd->CmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, original);
|
||||||
}
|
}
|
||||||
@@ -782,11 +955,13 @@ void traceDraw(VkCommandBuffer cb, const char* kind, uint32_t n, int action) {
|
|||||||
|
|
||||||
VKAPI_ATTR void VKAPI_CALL CmdDraw(VkCommandBuffer cb, uint32_t vc, uint32_t ic, uint32_t fv, uint32_t fi) {
|
VKAPI_ATTR void VKAPI_CALL CmdDraw(VkCommandBuffer cb, uint32_t vc, uint32_t ic, uint32_t fv, uint32_t fi) {
|
||||||
Twin t;
|
Twin t;
|
||||||
int slot = -1;
|
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||||
const int act = drawAction(cb, t, slot);
|
bool subsampled = false;
|
||||||
|
uint32_t off = 0;
|
||||||
|
const int act = drawAction(cb, t, set, subsampled, off);
|
||||||
traceDraw(cb, "draw", vc, act);
|
traceDraw(cb, "draw", vc, act);
|
||||||
switch (act) {
|
switch (act) {
|
||||||
case 1: drawTwin(cb, t, slot, [&] { dev(cb)->CmdDraw(cb, vc, ic, fv, fi); }); return;
|
case 1: drawTwin(cb, t, set, subsampled, off, [&] { dev(cb)->CmdDraw(cb, vc, ic, fv, fi); }); return;
|
||||||
case 2: return;
|
case 2: return;
|
||||||
default: dev(cb)->CmdDraw(cb, vc, ic, fv, fi);
|
default: dev(cb)->CmdDraw(cb, vc, ic, fv, fi);
|
||||||
}
|
}
|
||||||
@@ -794,15 +969,17 @@ VKAPI_ATTR void VKAPI_CALL CmdDraw(VkCommandBuffer cb, uint32_t vc, uint32_t ic,
|
|||||||
|
|
||||||
VKAPI_ATTR void VKAPI_CALL CmdDrawIndexed(VkCommandBuffer cb, uint32_t n, uint32_t ic, uint32_t fi, int32_t vo, uint32_t fin) {
|
VKAPI_ATTR void VKAPI_CALL CmdDrawIndexed(VkCommandBuffer cb, uint32_t n, uint32_t ic, uint32_t fi, int32_t vo, uint32_t fin) {
|
||||||
Twin t;
|
Twin t;
|
||||||
int slot = -1;
|
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||||
int act = drawAction(cb, t, slot);
|
bool subsampled = false;
|
||||||
if (act == 0 && g_starsIndices && n == g_starsIndices && g_ours.imported) { // the stars, while the home shows
|
uint32_t off = 0;
|
||||||
|
int act = drawAction(cb, t, set, subsampled, off);
|
||||||
|
if (act == 0 && g_starsIndices && n == g_starsIndices && g_imported) { // the stars, while the home shows
|
||||||
std::lock_guard<std::mutex> ol(g_oursMutex);
|
std::lock_guard<std::mutex> ol(g_oursMutex);
|
||||||
if (liveSlot() >= 0 && envshare::nowNs() - g_cameraNs.load() > 150000000ull) act = 2;
|
if (liveSlot() >= 0 && !cameraRecent()) act = 2;
|
||||||
}
|
}
|
||||||
traceDraw(cb, "drawIndexed", n, act);
|
traceDraw(cb, "drawIndexed", n, act);
|
||||||
switch (act) {
|
switch (act) {
|
||||||
case 1: drawTwin(cb, t, slot, [&] { dev(cb)->CmdDrawIndexed(cb, n, ic, fi, vo, fin); }); return;
|
case 1: drawTwin(cb, t, set, subsampled, off, [&] { dev(cb)->CmdDrawIndexed(cb, n, ic, fi, vo, fin); }); return;
|
||||||
case 2: return;
|
case 2: return;
|
||||||
default: dev(cb)->CmdDrawIndexed(cb, n, ic, fi, vo, fin);
|
default: dev(cb)->CmdDrawIndexed(cb, n, ic, fi, vo, fin);
|
||||||
}
|
}
|
||||||
@@ -817,23 +994,217 @@ void onSubmit(VkQueue q) {
|
|||||||
if (now - g_lastOffCheck > std::chrono::seconds(1)) {
|
if (now - g_lastOffCheck > std::chrono::seconds(1)) {
|
||||||
g_lastOffCheck = now;
|
g_lastOffCheck = now;
|
||||||
const bool off = exists(dir() + "/off");
|
const bool off = exists(dir() + "/off");
|
||||||
if (off != g_off) { g_off = off; logf(off ? "turned off: Aurora as usual" : "turned back on"); }
|
if (off != g_off) {
|
||||||
|
g_off = off;
|
||||||
|
// Off since the compositor started: no pipelines of ours were made, so nothing changes until it restarts.
|
||||||
|
logf(off ? "turned off: Aurora as usual" : g_startedOff ? "turned back on: shows after SteamVR restarts (off since the compositor started)" : "turned back on");
|
||||||
|
}
|
||||||
if (exists(dir() + "/trace")) { ::unlink((dir() + "/trace").c_str()); logf("=== trace: the next 1500 draws ==="); g_traceDraws = 1500; }
|
if (exists(dir() + "/trace")) { ::unlink((dir() + "/trace").c_str()); logf("=== trace: the next 1500 draws ==="); g_traceDraws = 1500; }
|
||||||
}
|
}
|
||||||
|
if (!g_off) importPending(dd, q);
|
||||||
|
std::lock_guard<std::mutex> ol(g_oursMutex); // (liveSlot updates lastGood, as the draw hooks do on their threads)
|
||||||
|
if (g_ours.d == dd) collectRetired(); // old generations no command buffer holds any more
|
||||||
if (g_off) return;
|
if (g_off) return;
|
||||||
importPending(dd, q);
|
if (g_ours.cur && liveSlot() >= 0 && ++g_ours.healthyFrames == 900) markHealthy(); // ~10 s showing the home
|
||||||
if (g_ours.imported && liveSlot() >= 0 && ++g_ours.healthyFrames == 900) markHealthy(); // ~10 s showing the home
|
}
|
||||||
|
|
||||||
|
// ── command buffers: when a generation's sets are no longer used ────────────────────────────────
|
||||||
|
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL AllocateCommandBuffers(VkDevice d, const VkCommandBufferAllocateInfo* ai, VkCommandBuffer* out) {
|
||||||
|
VkResult r = dev(d)->AllocateCommandBuffers(d, ai, out);
|
||||||
|
if (r == VK_SUCCESS) {
|
||||||
|
std::lock_guard<std::mutex> ol(g_oursMutex);
|
||||||
|
for (uint32_t i = 0; i < ai->commandBufferCount; ++i) g_cbPool[out[i]] = ai->commandPool;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
VKAPI_ATTR void VKAPI_CALL FreeCommandBuffers(VkDevice d, VkCommandPool pool, uint32_t n, const VkCommandBuffer* cbs) {
|
||||||
|
for (uint32_t i = 0; i < n; ++i) {
|
||||||
|
if (!cbs[i]) continue;
|
||||||
|
forgetCommandBuffer(cbs[i]);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> ol(g_oursMutex);
|
||||||
|
g_cbPool.erase(cbs[i]);
|
||||||
|
}
|
||||||
|
std::lock_guard<std::mutex> l(g_mutex);
|
||||||
|
g_bound.erase(cbs[i]); // (a handle the driver hands out again is a new command buffer)
|
||||||
|
}
|
||||||
|
dev(d)->FreeCommandBuffers(d, pool, n, cbs);
|
||||||
|
}
|
||||||
|
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL BeginCommandBuffer(VkCommandBuffer cb, const VkCommandBufferBeginInfo* bi) {
|
||||||
|
forgetCommandBuffer(cb); // (begin resets a recorded one: what it drew before is gone)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> l(g_mutex);
|
||||||
|
g_bound.erase(cb);
|
||||||
|
}
|
||||||
|
return dev(cb)->BeginCommandBuffer(cb, bi);
|
||||||
|
}
|
||||||
|
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL ResetCommandBuffer(VkCommandBuffer cb, VkCommandBufferResetFlags f) {
|
||||||
|
forgetCommandBuffer(cb);
|
||||||
|
return dev(cb)->ResetCommandBuffer(cb, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
VKAPI_ATTR VkResult VKAPI_CALL ResetCommandPool(VkDevice d, VkCommandPool pool, VkCommandPoolResetFlags f) {
|
||||||
|
forgetPool(pool, false);
|
||||||
|
return dev(d)->ResetCommandPool(d, pool, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
VKAPI_ATTR void VKAPI_CALL DestroyCommandPool(VkDevice d, VkCommandPool pool, const VkAllocationCallbacks* a) {
|
||||||
|
forgetPool(pool, true);
|
||||||
|
dev(d)->DestroyCommandPool(d, pool, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── acquiring Frame Home's frames (see Gen) ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// (g_oursMutex held) Slot k's acquire command buffer for queue family `family`, recorded once; null if it can't be
|
||||||
|
// made (or the slot's was made for another family: one compositor queue family draws the home).
|
||||||
|
VkCommandBuffer acquireCommandBuffer(DeviceData* d, Gen* g, uint32_t k, uint32_t family) {
|
||||||
|
if (g->acquire[k]) return g->acquireFamily[k] == family ? g->acquire[k] : VK_NULL_HANDLE;
|
||||||
|
auto pit = g_ours.acquirePools.find(family);
|
||||||
|
if (pit == g_ours.acquirePools.end()) {
|
||||||
|
VkCommandPoolCreateInfo pci = {VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||||
|
pci.queueFamilyIndex = family;
|
||||||
|
VkCommandPool pool = VK_NULL_HANDLE;
|
||||||
|
if (!d->CreateCommandPool || d->CreateCommandPool(d->device, &pci, nullptr, &pool) != VK_SUCCESS) return VK_NULL_HANDLE;
|
||||||
|
pit = g_ours.acquirePools.emplace(family, pool).first;
|
||||||
|
}
|
||||||
|
VkCommandBufferAllocateInfo ai = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
||||||
|
ai.commandPool = pit->second;
|
||||||
|
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||||
|
ai.commandBufferCount = 1;
|
||||||
|
VkCommandBuffer cb = VK_NULL_HANDLE;
|
||||||
|
if (d->AllocateCommandBuffers(d->device, &ai, &cb) != VK_SUCCESS) return VK_NULL_HANDLE;
|
||||||
|
// A command buffer a layer makes itself carries the loader's dispatch like the device's (layers below look it up).
|
||||||
|
if (!d->setLoaderData) *reinterpret_cast<void**>(cb) = *reinterpret_cast<void**>(d->device); // (an old loader)
|
||||||
|
else if (d->setLoaderData(d->device, cb) != VK_SUCCESS) { d->FreeCommandBuffers(d->device, pit->second, 1, &cb); return VK_NULL_HANDLE; }
|
||||||
|
VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
||||||
|
bi.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT; // (submitted again with each new frame in the slot)
|
||||||
|
VkImageMemoryBarrier b = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||||
|
b.oldLayout = b.newLayout = VK_IMAGE_LAYOUT_GENERAL; // as Frame Home released it (and as an older one left it)
|
||||||
|
b.srcAccessMask = 0; // (an acquire: the release made Frame Home's writes available)
|
||||||
|
b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||||
|
b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL;
|
||||||
|
b.dstQueueFamilyIndex = family;
|
||||||
|
b.image = g->image[k];
|
||||||
|
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, g->layers};
|
||||||
|
if (d->BeginCommandBuffer(cb, &bi) != VK_SUCCESS) { d->FreeCommandBuffers(d->device, pit->second, 1, &cb); return VK_NULL_HANDLE; }
|
||||||
|
d->CmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||||
|
if (d->EndCommandBuffer(cb) != VK_SUCCESS) { d->FreeCommandBuffers(d->device, pit->second, 1, &cb); return VK_NULL_HANDLE; }
|
||||||
|
g->acquire[k] = cb;
|
||||||
|
g->acquireFamily[k] = family;
|
||||||
|
static bool said = false;
|
||||||
|
if (!said) { said = true; logf("Frame Home's frames are acquired from its queue before they're sampled (queue family %u)", family); }
|
||||||
|
return cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The acquires to submit just ahead of a batch of the compositor's command buffers: one per slot whose frame one of
|
||||||
|
// them draws and that isn't acquired yet. None (the usual case: a frame is acquired once) appends nothing.
|
||||||
|
template <class CommandBufferAt>
|
||||||
|
void acquiresFor(DeviceData* d, uint32_t family, uint32_t n, CommandBufferAt at, std::vector<VkCommandBuffer>& out) {
|
||||||
|
auto visit = [&](Gen* g) {
|
||||||
|
if (g->users.empty()) return;
|
||||||
|
for (uint32_t i = 0; i < n; ++i) {
|
||||||
|
auto u = g->users.find(at(i));
|
||||||
|
if (u == g->users.end()) continue;
|
||||||
|
for (uint32_t k = 0; k < envshare::kSlots; ++k) {
|
||||||
|
if (!(u->second.slots & (1u << k)) || !u->second.frame[k] || g->acquiredFrame[k] == u->second.frame[k]) continue;
|
||||||
|
if (VkCommandBuffer a = acquireCommandBuffer(d, g, k, family)) { out.push_back(a); ++g_ours.acquires; }
|
||||||
|
g->acquiredFrame[k] = u->second.frame[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (Gen* g : g_ours.retired) visit(g);
|
||||||
|
if (g_ours.cur) visit(g_ours.cur);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t queueFamilyOf(VkQueue q) {
|
||||||
|
std::lock_guard<std::mutex> l(g_mutex);
|
||||||
|
auto it = g_queueFamily.find(q);
|
||||||
|
return it == g_queueFamily.end() ? VK_QUEUE_FAMILY_IGNORED : it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
VKAPI_ATTR VkResult VKAPI_CALL QueueSubmit(VkQueue q, uint32_t n, const VkSubmitInfo* si, VkFence f) {
|
VKAPI_ATTR VkResult VKAPI_CALL QueueSubmit(VkQueue q, uint32_t n, const VkSubmitInfo* si, VkFence f) {
|
||||||
onSubmit(q);
|
onSubmit(q);
|
||||||
return dev(q)->QueueSubmit(q, n, si, f);
|
DeviceData* dd = dev(q);
|
||||||
|
if (!g_imported) return dd->QueueSubmit(q, n, si, f); // (nothing of ours drawn: no frames imported)
|
||||||
|
const uint32_t family = queueFamilyOf(q);
|
||||||
|
std::vector<std::vector<VkCommandBuffer>> acq(n);
|
||||||
|
bool any = false;
|
||||||
|
if (family != VK_QUEUE_FAMILY_IGNORED) {
|
||||||
|
std::lock_guard<std::mutex> ol(g_oursMutex);
|
||||||
|
if (g_ours.d == dd)
|
||||||
|
for (uint32_t i = 0; i < n; ++i) {
|
||||||
|
acquiresFor(dd, family, si[i].commandBufferCount, [&](uint32_t c) { return si[i].pCommandBuffers[c]; }, acq[i]);
|
||||||
|
any |= !acq[i].empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!any) return dd->QueueSubmit(q, n, si, f);
|
||||||
|
// Each batch that needs some gets a batch of its acquires just before it (no semaphores: they wait on nothing,
|
||||||
|
// and the barriers order the batch's sampling after them in submission order).
|
||||||
|
std::vector<VkSubmitInfo> batches;
|
||||||
|
batches.reserve(2 * n);
|
||||||
|
for (uint32_t i = 0; i < n; ++i) {
|
||||||
|
if (!acq[i].empty()) {
|
||||||
|
VkSubmitInfo a = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||||
|
a.commandBufferCount = static_cast<uint32_t>(acq[i].size());
|
||||||
|
a.pCommandBuffers = acq[i].data();
|
||||||
|
batches.push_back(a);
|
||||||
|
}
|
||||||
|
batches.push_back(si[i]);
|
||||||
|
}
|
||||||
|
return dd->QueueSubmit(q, static_cast<uint32_t>(batches.size()), batches.data(), f);
|
||||||
}
|
}
|
||||||
|
|
||||||
VKAPI_ATTR VkResult VKAPI_CALL QueueSubmit2(VkQueue q, uint32_t n, const VkSubmitInfo2* si, VkFence f) {
|
VKAPI_ATTR VkResult VKAPI_CALL QueueSubmit2(VkQueue q, uint32_t n, const VkSubmitInfo2* si, VkFence f) {
|
||||||
onSubmit(q);
|
onSubmit(q);
|
||||||
DeviceData* dd = dev(q);
|
DeviceData* dd = dev(q);
|
||||||
return (dd->QueueSubmit2 ? dd->QueueSubmit2 : dd->QueueSubmit2KHR)(q, n, si, f);
|
auto submit = dd->QueueSubmit2 ? dd->QueueSubmit2 : dd->QueueSubmit2KHR;
|
||||||
|
if (!g_imported) return submit(q, n, si, f);
|
||||||
|
const uint32_t family = queueFamilyOf(q);
|
||||||
|
std::vector<std::vector<VkCommandBuffer>> acq(n);
|
||||||
|
bool any = false;
|
||||||
|
if (family != VK_QUEUE_FAMILY_IGNORED) {
|
||||||
|
std::lock_guard<std::mutex> ol(g_oursMutex);
|
||||||
|
if (g_ours.d == dd)
|
||||||
|
for (uint32_t i = 0; i < n; ++i) {
|
||||||
|
acquiresFor(dd, family, si[i].commandBufferInfoCount, [&](uint32_t c) { return si[i].pCommandBufferInfos[c].commandBuffer; }, acq[i]);
|
||||||
|
any |= !acq[i].empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!any) return submit(q, n, si, f);
|
||||||
|
std::vector<std::vector<VkCommandBufferSubmitInfo>> infos(n);
|
||||||
|
std::vector<VkSubmitInfo2> batches;
|
||||||
|
batches.reserve(2 * n);
|
||||||
|
for (uint32_t i = 0; i < n; ++i) {
|
||||||
|
if (!acq[i].empty()) {
|
||||||
|
for (VkCommandBuffer c : acq[i]) {
|
||||||
|
VkCommandBufferSubmitInfo ci = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO};
|
||||||
|
ci.commandBuffer = c;
|
||||||
|
infos[i].push_back(ci);
|
||||||
|
}
|
||||||
|
VkSubmitInfo2 a = {VK_STRUCTURE_TYPE_SUBMIT_INFO_2};
|
||||||
|
a.commandBufferInfoCount = static_cast<uint32_t>(infos[i].size());
|
||||||
|
a.pCommandBufferInfos = infos[i].data();
|
||||||
|
batches.push_back(a);
|
||||||
|
}
|
||||||
|
batches.push_back(si[i]);
|
||||||
|
}
|
||||||
|
return submit(q, static_cast<uint32_t>(batches.size()), batches.data(), f);
|
||||||
|
}
|
||||||
|
|
||||||
|
VKAPI_ATTR void VKAPI_CALL GetDeviceQueue(VkDevice d, uint32_t family, uint32_t index, VkQueue* q) {
|
||||||
|
dev(d)->GetDeviceQueue(d, family, index, q);
|
||||||
|
std::lock_guard<std::mutex> l(g_mutex);
|
||||||
|
if (*q) g_queueFamily[*q] = family;
|
||||||
|
}
|
||||||
|
|
||||||
|
VKAPI_ATTR void VKAPI_CALL GetDeviceQueue2(VkDevice d, const VkDeviceQueueInfo2* info, VkQueue* q) {
|
||||||
|
dev(d)->GetDeviceQueue2(d, info, q);
|
||||||
|
std::lock_guard<std::mutex> l(g_mutex);
|
||||||
|
if (*q) g_queueFamily[*q] = info->queueFamilyIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── instance / device plumbing ──────────────────────────────────────────────────────────────────
|
// ── instance / device plumbing ──────────────────────────────────────────────────────────────────
|
||||||
@@ -883,6 +1254,9 @@ VKAPI_ATTR VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceC
|
|||||||
PFN_vkGetInstanceProcAddr gipa = link->u.pLayerInfo->pfnNextGetInstanceProcAddr;
|
PFN_vkGetInstanceProcAddr gipa = link->u.pLayerInfo->pfnNextGetInstanceProcAddr;
|
||||||
PFN_vkGetDeviceProcAddr gdpa = link->u.pLayerInfo->pfnNextGetDeviceProcAddr;
|
PFN_vkGetDeviceProcAddr gdpa = link->u.pLayerInfo->pfnNextGetDeviceProcAddr;
|
||||||
link->u.pLayerInfo = link->u.pLayerInfo->pNext;
|
link->u.pLayerInfo = link->u.pLayerInfo->pNext;
|
||||||
|
PFN_vkSetDeviceLoaderData setLoaderData = nullptr;
|
||||||
|
for (auto* l = static_cast<const VkLayerDeviceCreateInfo*>(ci->pNext); l; l = static_cast<const VkLayerDeviceCreateInfo*>(l->pNext))
|
||||||
|
if (l->sType == VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO && l->function == VK_LOADER_DATA_CALLBACK) setLoaderData = l->u.pfnSetDeviceLoaderData;
|
||||||
auto create = reinterpret_cast<PFN_vkCreateDevice>(gipa(VK_NULL_HANDLE, "vkCreateDevice"));
|
auto create = reinterpret_cast<PFN_vkCreateDevice>(gipa(VK_NULL_HANDLE, "vkCreateDevice"));
|
||||||
// The compositor's device gets VK_EXT_fragment_density_map when the GPU has it, so it can read Frame Home's
|
// The compositor's device gets VK_EXT_fragment_density_map when the GPU has it, so it can read Frame Home's
|
||||||
// foveated frames as they were drawn (subsampled). If the device won't create with it, it's made as asked.
|
// foveated frames as they were drawn (subsampled). If the device won't create with it, it's made as asked.
|
||||||
@@ -924,6 +1298,7 @@ VKAPI_ATTR VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceC
|
|||||||
dd.device = *out;
|
dd.device = *out;
|
||||||
dd.physical = pd;
|
dd.physical = pd;
|
||||||
dd.gdpa = gdpa;
|
dd.gdpa = gdpa;
|
||||||
|
dd.setLoaderData = setLoaderData;
|
||||||
#define FH_LOAD(n) dd.n = reinterpret_cast<PFN_vk##n>(gdpa(*out, "vk" #n));
|
#define FH_LOAD(n) dd.n = reinterpret_cast<PFN_vk##n>(gdpa(*out, "vk" #n));
|
||||||
FH_DEVICE_FUNCS(FH_LOAD)
|
FH_DEVICE_FUNCS(FH_LOAD)
|
||||||
#undef FH_LOAD
|
#undef FH_LOAD
|
||||||
@@ -955,8 +1330,27 @@ VKAPI_ATTR void VKAPI_CALL DestroyDevice(VkDevice d, const VkAllocationCallbacks
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> ol(g_oursMutex);
|
std::lock_guard<std::mutex> ol(g_oursMutex);
|
||||||
if (g_ours.d && g_ours.d->device == d) {
|
if (g_ours.d && g_ours.d->device == d) {
|
||||||
releaseImports();
|
// (the device's work is all done by now: the spec requires it of vkDestroyDevice's caller)
|
||||||
|
retireCurrent();
|
||||||
|
for (Gen* g : g_ours.retired) destroyGen(g_ours.d, g);
|
||||||
|
g_ours.retired.clear();
|
||||||
|
g_cbPool.clear();
|
||||||
|
for (auto& p : g_ours.acquirePools) g_ours.d->DestroyCommandPool(d, p.second, nullptr); // (after the generations' command buffers)
|
||||||
|
if (g_ours.acquires) logf("%llu Frame Home frames acquired", static_cast<unsigned long long>(g_ours.acquires));
|
||||||
|
// ensureBase()'s objects go with the device (a compositor that makes a new device gets them made again).
|
||||||
|
DeviceData* od = g_ours.d;
|
||||||
|
for (auto& u : g_ours.ubo) {
|
||||||
|
if (u.buffer) od->DestroyBuffer(d, u.buffer, nullptr);
|
||||||
|
if (u.mem) od->FreeMemory(d, u.mem, nullptr); // (unmapped by freeing)
|
||||||
|
}
|
||||||
|
if (g_ours.setLayout) od->DestroyDescriptorSetLayout(d, g_ours.setLayout, nullptr);
|
||||||
|
if (g_ours.setLayoutSubsampled) od->DestroyDescriptorSetLayout(d, g_ours.setLayoutSubsampled, nullptr);
|
||||||
|
if (g_ours.sampler) od->DestroySampler(d, g_ours.sampler, nullptr);
|
||||||
|
if (g_ours.subsampledSampler) od->DestroySampler(d, g_ours.subsampledSampler, nullptr);
|
||||||
|
if (g_ours.fragment) od->DestroyShaderModule(d, g_ours.fragment, nullptr);
|
||||||
g_ours = Ours();
|
g_ours = Ours();
|
||||||
|
g_latchNs = 0;
|
||||||
|
g_latchSlot = -1;
|
||||||
// a clean close (SteamVR quitting or restarting the compositor): not a crash
|
// a clean close (SteamVR quitting or restarting the compositor): not a crash
|
||||||
if (g_armed.exchange(false)) ::unlink((dir() + "/starting").c_str());
|
if (g_armed.exchange(false)) ::unlink((dir() + "/starting").c_str());
|
||||||
}
|
}
|
||||||
@@ -975,6 +1369,8 @@ PFN_vkVoidFunction intercept(const char* name) {
|
|||||||
if (!isCompositor()) return nullptr;
|
if (!isCompositor()) return nullptr;
|
||||||
FH_HOOK(CreateShaderModule) FH_HOOK(CreatePipelineLayout) FH_HOOK(CreateGraphicsPipelines) FH_HOOK(DestroyPipeline)
|
FH_HOOK(CreateShaderModule) FH_HOOK(CreatePipelineLayout) FH_HOOK(CreateGraphicsPipelines) FH_HOOK(DestroyPipeline)
|
||||||
FH_HOOK(CmdBindPipeline) FH_HOOK(CmdDraw) FH_HOOK(CmdDrawIndexed) FH_HOOK(QueueSubmit)
|
FH_HOOK(CmdBindPipeline) FH_HOOK(CmdDraw) FH_HOOK(CmdDrawIndexed) FH_HOOK(QueueSubmit)
|
||||||
|
FH_HOOK(AllocateCommandBuffers) FH_HOOK(FreeCommandBuffers) FH_HOOK(BeginCommandBuffer) FH_HOOK(ResetCommandBuffer)
|
||||||
|
FH_HOOK(ResetCommandPool) FH_HOOK(DestroyCommandPool) FH_HOOK(GetDeviceQueue) FH_HOOK(GetDeviceQueue2)
|
||||||
#undef FH_HOOK
|
#undef FH_HOOK
|
||||||
if (!std::strcmp(name, "vkQueueSubmit2") || !std::strcmp(name, "vkQueueSubmit2KHR")) return reinterpret_cast<PFN_vkVoidFunction>(QueueSubmit2);
|
if (!std::strcmp(name, "vkQueueSubmit2") || !std::strcmp(name, "vkQueueSubmit2KHR")) return reinterpret_cast<PFN_vkVoidFunction>(QueueSubmit2);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|||||||
@@ -270,3 +270,79 @@ static const uint32_t kClaySkinnedVertSpirv[] = {
|
|||||||
0x00000027,0x0000003e,0x0004003d,0x00000007,0x0000002b,0x00000029,0x0003003e,0x0000002a,
|
0x00000027,0x0000003e,0x0004003d,0x00000007,0x0000002b,0x00000029,0x0003003e,0x0000002a,
|
||||||
0x0000002b,0x000100fd,0x00010038,
|
0x0000002b,0x000100fd,0x00010038,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static const uint32_t kClayVColorVertSpirv[] = {
|
||||||
|
0x07230203,0x00010000,0x00000000,0x0000004d,0x00000000,0x00020011,0x00000001,0x00020011,
|
||||||
|
0x00001157,0x0006000a,0x5f565053,0x5f52484b,0x746c756d,0x65697669,0x00000077,0x0003000e,
|
||||||
|
0x00000000,0x00000001,0x0010000f,0x00000000,0x00000001,0x6e69616d,0x00000000,0x00000020,
|
||||||
|
0x00000021,0x00000022,0x00000023,0x00000024,0x00000029,0x00000025,0x00000026,0x00000027,
|
||||||
|
0x0000002a,0x00000028,0x00040005,0x00000001,0x6e69616d,0x00000000,0x00060005,0x0000000c,
|
||||||
|
0x626f6c47,0x6e556c61,0x726f6669,0x0000736d,0x00060005,0x0000001e,0x626f6c67,0x6e556c61,
|
||||||
|
0x726f6669,0x0000736d,0x00060005,0x0000000d,0x68737550,0x736e6f43,0x746e6174,0x00000073,
|
||||||
|
0x00060005,0x0000001f,0x68737570,0x736e6f43,0x746e6174,0x00000073,0x00050005,0x00000020,
|
||||||
|
0x69736f70,0x6e6f6974,0x00000000,0x00030005,0x00000021,0x00007675,0x00040005,0x00000022,
|
||||||
|
0x6f6c6f63,0x00000072,0x00040005,0x00000023,0x6d726f6e,0x00006c61,0x00030005,0x00000025,
|
||||||
|
0x00565576,0x00040005,0x00000026,0x6c6f4376,0x0000726f,0x00040005,0x00000027,0x726f4e76,
|
||||||
|
0x006c616d,0x00050005,0x00000029,0x6867696c,0x70616d74,0x00007655,0x00040005,0x0000002a,
|
||||||
|
0x31565576,0x00000000,0x00070006,0x0000000c,0x00000000,0x70696c63,0x6d6f7246,0x6c726f57,
|
||||||
|
0x00000064,0x00070006,0x0000000d,0x00000000,0x6c726f77,0x6f724664,0x646f4d6d,0x00006c65,
|
||||||
|
0x00050006,0x0000000d,0x00000001,0x6f6c6f63,0x00000072,0x00070006,0x0000000d,0x00000002,
|
||||||
|
0x72547675,0x66736e61,0x556d726f,0x00000000,0x00070006,0x0000000d,0x00000003,0x72547675,
|
||||||
|
0x66736e61,0x566d726f,0x00000000,0x00040047,0x00000020,0x0000001e,0x00000000,0x00040047,
|
||||||
|
0x00000021,0x0000001e,0x00000001,0x00040047,0x00000022,0x0000001e,0x00000002,0x00040047,
|
||||||
|
0x00000023,0x0000001e,0x00000003,0x00040047,0x00000029,0x0000001e,0x00000004,0x00040047,
|
||||||
|
0x00000025,0x0000001e,0x00000000,0x00040047,0x00000026,0x0000001e,0x00000001,0x00040047,
|
||||||
|
0x00000027,0x0000001e,0x00000002,0x00040047,0x0000002a,0x0000001e,0x00000003,0x00040047,
|
||||||
|
0x00000028,0x0000000b,0x00000000,0x00040047,0x00000024,0x0000000b,0x00001158,0x00040047,
|
||||||
|
0x0000000b,0x00000006,0x00000040,0x00050048,0x0000000c,0x00000000,0x00000023,0x00000000,
|
||||||
|
0x00040048,0x0000000c,0x00000000,0x00000005,0x00050048,0x0000000c,0x00000000,0x00000007,
|
||||||
|
0x00000010,0x00030047,0x0000000c,0x00000002,0x00040047,0x0000001e,0x00000022,0x00000000,
|
||||||
|
0x00040047,0x0000001e,0x00000021,0x00000000,0x00050048,0x0000000d,0x00000000,0x00000023,
|
||||||
|
0x00000000,0x00040048,0x0000000d,0x00000000,0x00000005,0x00050048,0x0000000d,0x00000000,
|
||||||
|
0x00000007,0x00000010,0x00050048,0x0000000d,0x00000001,0x00000023,0x00000040,0x00050048,
|
||||||
|
0x0000000d,0x00000002,0x00000023,0x00000050,0x00050048,0x0000000d,0x00000003,0x00000023,
|
||||||
|
0x00000060,0x00030047,0x0000000d,0x00000002,0x00020013,0x00000002,0x00030021,0x00000003,
|
||||||
|
0x00000002,0x00030016,0x00000004,0x00000020,0x00040015,0x00000005,0x00000020,0x00000001,
|
||||||
|
0x00040015,0x00000006,0x00000020,0x00000000,0x00040017,0x00000007,0x00000004,0x00000002,
|
||||||
|
0x00040017,0x00000008,0x00000004,0x00000003,0x00040017,0x00000009,0x00000004,0x00000004,
|
||||||
|
0x00040018,0x0000000a,0x00000009,0x00000004,0x0004002b,0x00000006,0x0000001d,0x00000002,
|
||||||
|
0x0004001c,0x0000000b,0x0000000a,0x0000001d,0x0003001e,0x0000000c,0x0000000b,0x0006001e,
|
||||||
|
0x0000000d,0x0000000a,0x00000009,0x00000009,0x00000009,0x00040020,0x0000000e,0x00000002,
|
||||||
|
0x0000000c,0x00040020,0x0000000f,0x00000002,0x0000000a,0x00040020,0x00000010,0x00000009,
|
||||||
|
0x0000000d,0x00040020,0x00000011,0x00000009,0x0000000a,0x00040020,0x00000012,0x00000009,
|
||||||
|
0x00000009,0x00040020,0x00000013,0x00000001,0x00000008,0x00040020,0x00000014,0x00000001,
|
||||||
|
0x00000007,0x00040020,0x00000015,0x00000001,0x00000009,0x00040020,0x00000016,0x00000001,
|
||||||
|
0x00000005,0x00040020,0x00000017,0x00000003,0x00000007,0x00040020,0x00000018,0x00000003,
|
||||||
|
0x00000009,0x00040020,0x00000019,0x00000003,0x00000008,0x0004002b,0x00000004,0x0000001a,
|
||||||
|
0x3f800000,0x0004002b,0x00000005,0x0000001b,0x00000000,0x0004002b,0x00000005,0x0000001c,
|
||||||
|
0x00000001,0x0004002b,0x00000005,0x0000003f,0x00000002,0x0004002b,0x00000005,0x00000040,
|
||||||
|
0x00000003,0x0004003b,0x0000000e,0x0000001e,0x00000002,0x0004003b,0x00000010,0x0000001f,
|
||||||
|
0x00000009,0x0004003b,0x00000013,0x00000020,0x00000001,0x0004003b,0x00000014,0x00000021,
|
||||||
|
0x00000001,0x0004003b,0x00000015,0x00000022,0x00000001,0x0004003b,0x00000013,0x00000023,
|
||||||
|
0x00000001,0x0004003b,0x00000016,0x00000024,0x00000001,0x0004003b,0x00000017,0x00000025,
|
||||||
|
0x00000003,0x0004003b,0x00000018,0x00000026,0x00000003,0x0004003b,0x00000019,0x00000027,
|
||||||
|
0x00000003,0x0004003b,0x00000018,0x00000028,0x00000003,0x0004003b,0x00000014,0x00000029,
|
||||||
|
0x00000001,0x0004003b,0x00000017,0x0000002a,0x00000003,0x00050036,0x00000002,0x00000001,
|
||||||
|
0x00000000,0x00000003,0x000200f8,0x0000002c,0x0004003d,0x00000005,0x0000002d,0x00000024,
|
||||||
|
0x00060041,0x0000000f,0x0000002e,0x0000001e,0x0000001b,0x0000002d,0x0004003d,0x0000000a,
|
||||||
|
0x0000002f,0x0000002e,0x00050041,0x00000011,0x00000030,0x0000001f,0x0000001b,0x0004003d,
|
||||||
|
0x0000000a,0x00000031,0x00000030,0x00050041,0x00000012,0x00000032,0x0000001f,0x0000001c,
|
||||||
|
0x0004003d,0x00000009,0x00000033,0x00000032,0x0004003d,0x00000008,0x00000034,0x00000020,
|
||||||
|
0x00050051,0x00000004,0x00000035,0x00000034,0x00000000,0x00050051,0x00000004,0x00000036,
|
||||||
|
0x00000034,0x00000001,0x00050051,0x00000004,0x00000037,0x00000034,0x00000002,0x00070050,
|
||||||
|
0x00000009,0x00000038,0x00000035,0x00000036,0x00000037,0x0000001a,0x00050091,0x00000009,
|
||||||
|
0x00000039,0x00000031,0x00000038,0x00050091,0x00000009,0x0000003a,0x0000002f,0x00000039,
|
||||||
|
0x0003003e,0x00000028,0x0000003a,0x0004003d,0x00000007,0x0000003b,0x00000021,0x00050041,
|
||||||
|
0x00000012,0x00000041,0x0000001f,0x0000003f,0x0004003d,0x00000009,0x00000042,0x00000041,
|
||||||
|
0x00050041,0x00000012,0x00000043,0x0000001f,0x00000040,0x0004003d,0x00000009,0x00000044,
|
||||||
|
0x00000043,0x0008004f,0x00000008,0x00000045,0x00000042,0x00000042,0x00000000,0x00000001,
|
||||||
|
0x00000002,0x0008004f,0x00000008,0x00000046,0x00000044,0x00000044,0x00000000,0x00000001,
|
||||||
|
0x00000002,0x00050051,0x00000004,0x00000047,0x0000003b,0x00000000,0x00050051,0x00000004,
|
||||||
|
0x00000048,0x0000003b,0x00000001,0x00060050,0x00000008,0x00000049,0x00000047,0x00000048,
|
||||||
|
0x0000001a,0x00050094,0x00000004,0x0000004a,0x00000045,0x00000049,0x00050094,0x00000004,
|
||||||
|
0x0000004b,0x00000046,0x00000049,0x00050050,0x00000007,0x0000004c,0x0000004a,0x0000004b,
|
||||||
|
0x0003003e,0x00000025,0x0000004c,0x0004003d,0x00000009,0x0000003c,0x00000022,0x00050085,
|
||||||
|
0x00000009,0x0000003d,0x00000033,0x0000003c,0x0003003e,0x00000026,0x0000003d,0x0004003d,
|
||||||
|
0x00000008,0x0000003e,0x00000023,0x0003003e,0x00000027,0x0000003e,0x0004003d,0x00000007,
|
||||||
|
0x0000002b,0x00000029,0x0003003e,0x0000002a,0x0000002b,0x000100fd,0x00010038,
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// The dashboard tab icon: art/icon/frame-glyph.svg (the app icon's roof and headset, no tile) at 64 x 64, RGBA.
|
||||||
|
// Regenerate from art/icon/frame-glyph-64.png if the icon changes.
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
static constexpr int kDashIconSize = 64;
|
||||||
|
static const uint8_t kDashIcon[64 * 64 * 4] = {
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,244,255,69,248,244,255,71,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,245,255,74,247,244,255,251,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,245,255,74,247,244,255,251,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,245,255,74,247,244,255,251,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,245,255,74,247,244,255,251,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,245,255,74,247,244,255,251,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,245,255,74,247,244,255,251,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,251,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,85,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,188,247,244,255,181,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,91,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,191,255,255,255,6,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,191,255,255,255,6,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,191,255,255,255,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,191,255,255,255,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,254,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,254,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,254,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,247,244,255,93,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,254,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,246,244,255,180,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,247,244,255,93,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,254,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,3,246,244,255,180,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,247,244,255,93,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,254,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,3,246,244,255,180,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,247,244,255,93,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,254,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,3,246,244,255,180,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,247,244,255,93,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,3,246,244,255,180,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,3,246,244,255,180,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,251,247,244,255,255,247,244,255,255,246,244,255,180,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,170,255,9,24,158,255,42,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,158,255,42,28,170,255,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,3,246,244,255,180,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,245,255,74,247,244,255,251,246,244,255,180,255,255,255,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,158,255,76,25,159,255,185,25,159,255,231,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,230,25,158,255,182,26,158,255,68,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,3,246,244,255,180,247,244,255,251,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,246,255,59,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,22,160,255,35,26,159,255,189,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,230,26,158,255,129,26,159,255,128,26,159,255,128,26,159,255,128,26,159,255,128,26,159,255,128,26,159,255,128,26,158,255,129,25,159,255,233,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,180,18,164,255,28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,246,255,59,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,161,255,38,25,159,255,233,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,158,255,110,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,160,255,115,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,158,255,226,18,164,255,28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,158,255,198,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,158,255,190,19,161,255,27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,158,255,29,25,159,255,193,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,158,255,174,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,160,255,51,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,22,155,255,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,160,255,115,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,23,158,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,158,255,150,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,158,255,132,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,158,255,161,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,158,255,160,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,158,255,163,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,159,255,162,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,158,255,163,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,159,255,162,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,158,255,163,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,159,255,162,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,159,255,157,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,158,255,155,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,158,255,139,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,239,25,158,255,192,25,158,255,192,26,159,255,239,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,24,159,255,136,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,159,255,106,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,158,255,161,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,158,255,166,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,159,255,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,22,160,255,35,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,158,255,216,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,158,255,216,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,18,164,255,28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,158,255,187,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,159,255,252,22,157,255,47,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,159,255,53,25,159,255,252,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,158,255,176,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,157,255,52,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,24,159,255,136,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,159,255,136,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,24,158,255,42,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,159,255,98,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,158,255,203,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,159,255,8,25,159,255,204,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,247,24,159,255,85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,157,255,39,25,159,255,183,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,158,255,192,26,166,255,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,162,255,22,26,159,255,196,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,167,26,161,255,30,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,21,159,255,37,26,159,255,120,26,159,255,128,26,159,255,128,26,159,255,128,26,158,255,68,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,159,255,72,26,159,255,128,26,159,255,128,26,159,255,128,25,159,255,114,26,161,255,30,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
};
|
||||||
@@ -76,17 +76,20 @@ public:
|
|||||||
|
|
||||||
int beginFrame() { const int k = static_cast<int>(next_); next_ = (next_ + 1) % envshare::kSlots; return k; }
|
int beginFrame() { const int k = static_cast<int>(next_); next_ = (next_ + 1) % envshare::kSlots; return k; }
|
||||||
|
|
||||||
void publish(int k, const float viewProj[2][16], const float eyePos[2][3]) {
|
void publish(int k, const float viewProj[2][16], const float eyePos[2][3], const envshare::Motion& motion) {
|
||||||
envshare::Slot& s = state_->slot[k];
|
envshare::Slot s;
|
||||||
s.frame = ++frame_;
|
s.frame = ++frame_;
|
||||||
s.generation = generation_;
|
s.generation = generation_;
|
||||||
std::memcpy(s.viewProj, viewProj, sizeof s.viewProj);
|
std::memcpy(s.viewProj, viewProj, sizeof s.viewProj);
|
||||||
for (int e = 0; e < 2; ++e) { for (int c = 0; c < 3; ++c) s.eyePos[e][c] = eyePos[e][c]; s.eyePos[e][3] = 1; }
|
for (int e = 0; e < 2; ++e) { for (int c = 0; c < 3; ++c) s.eyePos[e][c] = eyePos[e][c]; s.eyePos[e][3] = 1; }
|
||||||
|
envshare::writeSlot(state_, static_cast<uint32_t>(k), s, motion); // (under its sequence count: env_share.h)
|
||||||
state_->published.store(frame_);
|
state_->published.store(frame_);
|
||||||
state_->latest.store(static_cast<uint32_t>(k), std::memory_order_release);
|
state_->latest.store(static_cast<uint32_t>(k), std::memory_order_release);
|
||||||
state_->publishNs.store(envshare::nowNs());
|
state_->publishNs.store(envshare::nowNs());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void still() { const uint32_t k = state_->latest.load(); if (k < envshare::kSlots) envshare::writeMotion(state_, k, envshare::Motion()); }
|
||||||
|
|
||||||
uint32_t refusedFlags() const { return 0; } // (Windows frames carry no Vulkan create flags for the layer to refuse)
|
uint32_t refusedFlags() const { return 0; } // (Windows frames carry no Vulkan create flags for the layer to refuse)
|
||||||
|
|
||||||
double sinceSampled() const {
|
double sinceSampled() const {
|
||||||
@@ -282,17 +285,23 @@ public:
|
|||||||
int beginFrame() { const int k = static_cast<int>(next_); next_ = (next_ + 1) % envshare::kSlots; return k; }
|
int beginFrame() { const int k = static_cast<int>(next_); next_ = (next_ + 1) % envshare::kSlots; return k; }
|
||||||
|
|
||||||
// The frame in slot k is done on the GPU: hand it to the layer with the eye poses it was rendered from.
|
// The frame in slot k is done on the GPU: hand it to the layer with the eye poses it was rendered from.
|
||||||
void publish(int k, const float viewProj[2][16], const float eyePos[2][3]) {
|
void publish(int k, const float viewProj[2][16], const float eyePos[2][3], const envshare::Motion& motion) {
|
||||||
envshare::Slot& s = state_->slot[k];
|
envshare::Slot s;
|
||||||
s.frame = ++frame_;
|
s.frame = ++frame_;
|
||||||
s.generation = generation_;
|
s.generation = generation_;
|
||||||
std::memcpy(s.viewProj, viewProj, sizeof s.viewProj);
|
std::memcpy(s.viewProj, viewProj, sizeof s.viewProj);
|
||||||
for (int e = 0; e < 2; ++e) { for (int k = 0; k < 3; ++k) s.eyePos[e][k] = eyePos[e][k]; s.eyePos[e][3] = 1; }
|
for (int e = 0; e < 2; ++e) { for (int c = 0; c < 3; ++c) s.eyePos[e][c] = eyePos[e][c]; s.eyePos[e][3] = 1; }
|
||||||
|
// Under the slot's sequence count (env_share.h): after a resize this rewrites the slot the layer may still be
|
||||||
|
// showing from the old images, and it must never read half of this pose and half of the old one.
|
||||||
|
envshare::writeSlot(state_, static_cast<uint32_t>(k), s, motion);
|
||||||
state_->published.store(frame_);
|
state_->published.store(frame_);
|
||||||
state_->latest.store(static_cast<uint32_t>(k), std::memory_order_release);
|
state_->latest.store(static_cast<uint32_t>(k), std::memory_order_release);
|
||||||
state_->publishNs.store(envshare::nowNs());
|
state_->publishNs.store(envshare::nowNs());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The newest frame stays as it is (the home stops for a game): no turning or walking carried on from it.
|
||||||
|
void still() { const uint32_t k = state_->latest.load(); if (k < envshare::kSlots) envshare::writeMotion(state_, k, envshare::Motion()); }
|
||||||
|
|
||||||
// The layer couldn't import the images with these create flags (0: none refused).
|
// The layer couldn't import the images with these create flags (0: none refused).
|
||||||
uint32_t refusedFlags() const { return state_->refusedFlags.load() & flags_; }
|
uint32_t refusedFlags() const { return state_->refusedFlags.load() & flags_; }
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,11 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
#include <intrin.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace envshare {
|
namespace envshare {
|
||||||
|
|
||||||
@@ -32,8 +36,9 @@ constexpr const char* kPipeName = "\\\\.\\pipe\\framehome-env"; // Win
|
|||||||
constexpr const char* kStateMapName = "Local\\framehome-env-state"; // Windows: the shared State mapping (+ a
|
constexpr const char* kStateMapName = "Local\\framehome-env-state"; // Windows: the shared State mapping (+ a
|
||||||
// generation suffix, so a new one is distinct)
|
// generation suffix, so a new one is distinct)
|
||||||
constexpr uint32_t kMagic = 0x46484556; // "FHEV"
|
constexpr uint32_t kMagic = 0x46484556; // "FHEV"
|
||||||
constexpr uint32_t kVersion = 5; // 4: Hello says the images' usage (Frame Home draws straight into them)
|
constexpr uint32_t kVersion = 6; // 4: Hello says the images' usage (Frame Home draws straight into them)
|
||||||
// 5: ...and their create flags (subsampled: the compositor smooths foveation)
|
// 5: ...and their create flags (subsampled: the compositor smooths foveation)
|
||||||
|
// 6: the State says how the view was moving with each frame (Motion)
|
||||||
// Four: the compositor reads the newest while Frame Home publishes the one before it finished and renders the next.
|
// Four: the compositor reads the newest while Frame Home publishes the one before it finished and renders the next.
|
||||||
constexpr uint32_t kSlots = 4;
|
constexpr uint32_t kSlots = 4;
|
||||||
|
|
||||||
@@ -85,8 +90,20 @@ struct Slot {
|
|||||||
float eyePos[2][4] = {}; // per eye: where it was, tracking space (xyz)
|
float eyePos[2][4] = {}; // per eye: where it was, tracking space (xyz)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// v6: how the view was moving when a frame was made, so the layer can carry a frame it shows again (Half rate, or a
|
||||||
|
// frame that came late) on to where you've got to since. The rig's smooth turn is a rotation about the head, so it's
|
||||||
|
// carried on exactly; walking moves the eye, which needs depth the layer hasn't got: everything is taken as `depth`
|
||||||
|
// metres away (what's in front of you), so walls and furniture keep up and only the far distance is a little off.
|
||||||
|
struct Motion {
|
||||||
|
uint64_t targetNs = 0; // the compositor sample the frame was made for (CLOCK_MONOTONIC; 0: as is)
|
||||||
|
float yawRate = 0; // the rig turning (smooth turn), rad/s, + = left, about tracking-space up
|
||||||
|
float depth = 0; // what walking parallax takes everything's distance as (m; 0: no correction)
|
||||||
|
float velocity[4] = {}; // the rig walking, tracking space of the frame (m/s, xyz)
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr uint32_t kStateVersion = 7; // State::version once Frame Home writes slots under `seq` (v6 wrote kVersion)
|
||||||
struct State {
|
struct State {
|
||||||
uint32_t magic = kMagic, version = kVersion;
|
uint32_t magic = kMagic, version = kStateVersion;
|
||||||
std::atomic<uint32_t> latest{0xFFFFFFFFu}; // newest finished slot (0xFFFFFFFF none)
|
std::atomic<uint32_t> latest{0xFFFFFFFFu}; // newest finished slot (0xFFFFFFFF none)
|
||||||
std::atomic<uint64_t> published{0}; // frame number of `latest`
|
std::atomic<uint64_t> published{0}; // frame number of `latest`
|
||||||
std::atomic<uint64_t> publishNs{0}; // CLOCK_MONOTONIC when it was published
|
std::atomic<uint64_t> publishNs{0}; // CLOCK_MONOTONIC when it was published
|
||||||
@@ -97,7 +114,106 @@ struct State {
|
|||||||
Slot slot[kSlots];
|
Slot slot[kSlots];
|
||||||
std::atomic<uint32_t> refusedFlags{0}; // the layer: Hello flags it can't import with (Frame Home then sends
|
std::atomic<uint32_t> refusedFlags{0}; // the layer: Hello flags it can't import with (Frame Home then sends
|
||||||
// images without them)
|
// images without them)
|
||||||
|
Motion motion[kSlots]; // v6: with each slot's frame (written before `latest`, as Slot is)
|
||||||
|
// State v7: a sequence count per slot (a seqlock over slot[k] and motion[k]). Odd while Frame Home rewrites them;
|
||||||
|
// the layer copies a slot only between two equal even counts, so it never takes half of one frame's pose and half
|
||||||
|
// of the next's (a resize rewrites the slot the layer is still showing from the old images). See writeSlot().
|
||||||
|
std::atomic<uint32_t> seq[kSlots] = {};
|
||||||
};
|
};
|
||||||
|
// An older Frame Home's State ends before the fields added since (a v5 Hello: before `motion`; a v6 State before
|
||||||
|
// `seq`). The layer maps only as much as it was sent: reading past the end of the shared memory would crash the
|
||||||
|
// compositor. The Hello stays v6 (its layout didn't change): `version` in the State says v7, and the memfd's size
|
||||||
|
// says whether `seq` is there at all, so an old layer with a new Frame Home (and the reverse) still pair up.
|
||||||
|
constexpr size_t kStateV5Size = offsetof(State, motion);
|
||||||
|
constexpr size_t kStateV6Size = offsetof(State, seq);
|
||||||
|
|
||||||
|
// The layer: how much of the State a Frame Home sent may be mapped (0: less than its Hello's version promises, so
|
||||||
|
// none), and whether it says how the view moves (v6 Motion). Whether its slots are under sequence counts is known
|
||||||
|
// once it's mapped: seqlocked().
|
||||||
|
struct StateLayout { size_t size = 0; bool motion = false; };
|
||||||
|
inline StateLayout stateLayout(uint32_t helloVersion, uint64_t fileSize) {
|
||||||
|
StateLayout l;
|
||||||
|
l.motion = helloVersion >= 6;
|
||||||
|
const size_t need = l.motion ? kStateV6Size : kStateV5Size;
|
||||||
|
if (fileSize >= need) l.size = l.motion && fileSize >= sizeof(State) ? sizeof(State) : need;
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
// A State mapped `mapped` bytes long has `seq` and Frame Home writes the slots under it: v7 in size and in what it
|
||||||
|
// says (a bigger State from something else is still read the v6 way).
|
||||||
|
inline bool seqlocked(const State* st, size_t mapped) {
|
||||||
|
return mapped >= sizeof(State) && st->magic == kMagic && st->version >= kStateVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Word-by-word relaxed atomic copies of the seqlocked slot data (plain memcpy racing the other process is a data
|
||||||
|
// race; these are the C++ memory model's way to say "may tear, the sequence count catches it").
|
||||||
|
inline void storeWords(void* dst, const void* src, size_t bytes) {
|
||||||
|
uint32_t* d = static_cast<uint32_t*>(dst);
|
||||||
|
const uint32_t* s = static_cast<const uint32_t*>(src);
|
||||||
|
for (size_t i = 0; i < bytes / 4; ++i) {
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
__iso_volatile_store32(reinterpret_cast<volatile __int32*>(d + i), static_cast<__int32>(s[i]));
|
||||||
|
#else
|
||||||
|
__atomic_store_n(d + i, s[i], __ATOMIC_RELAXED);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inline void loadWords(void* dst, const void* src, size_t bytes) {
|
||||||
|
uint32_t* d = static_cast<uint32_t*>(dst);
|
||||||
|
const uint32_t* s = static_cast<const uint32_t*>(src);
|
||||||
|
for (size_t i = 0; i < bytes / 4; ++i) {
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
d[i] = static_cast<uint32_t>(__iso_volatile_load32(reinterpret_cast<const volatile __int32*>(s + i)));
|
||||||
|
#else
|
||||||
|
d[i] = __atomic_load_n(s + i, __ATOMIC_RELAXED);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static_assert(sizeof(Slot) % 4 == 0 && sizeof(Motion) % 4 == 0, "slot data is copied as 32-bit words");
|
||||||
|
|
||||||
|
// Frame Home: rewrites slot k (and its Motion) under its sequence count. Only Frame Home writes, from one thread.
|
||||||
|
inline void writeSlot(State* st, uint32_t k, const Slot& slot, const Motion& motion) {
|
||||||
|
const uint32_t s = st->seq[k].load(std::memory_order_relaxed);
|
||||||
|
st->seq[k].store(s | 1u, std::memory_order_relaxed); // odd: being written
|
||||||
|
std::atomic_thread_fence(std::memory_order_release); // (the odd count is seen before any new word)
|
||||||
|
storeWords(&st->slot[k], &slot, sizeof slot);
|
||||||
|
storeWords(&st->motion[k], &motion, sizeof motion);
|
||||||
|
st->seq[k].store((s | 1u) + 1u, std::memory_order_release); // even again: whole
|
||||||
|
}
|
||||||
|
// Frame Home: only the Motion of slot k (still()).
|
||||||
|
inline void writeMotion(State* st, uint32_t k, const Motion& motion) {
|
||||||
|
Slot cur;
|
||||||
|
loadWords(&cur, &st->slot[k], sizeof cur); // (Frame Home is the only writer: this is what's there)
|
||||||
|
writeSlot(st, k, cur, motion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The layer: a consistent copy of slot k, or false (being rewritten right now, every try). `seqlocked`: the State is
|
||||||
|
// v7 (has `seq`); an older Frame Home has no count, so two copies that agree are the best that can be had.
|
||||||
|
inline bool readSlot(const State* st, uint32_t k, bool seqlocked, bool withMotion, Slot& slot, Motion& motion) {
|
||||||
|
motion = Motion();
|
||||||
|
for (int tries = 0; tries < 8; ++tries) {
|
||||||
|
if (seqlocked) {
|
||||||
|
const uint32_t s1 = st->seq[k].load(std::memory_order_acquire);
|
||||||
|
if (s1 & 1u) continue;
|
||||||
|
loadWords(&slot, &st->slot[k], sizeof slot);
|
||||||
|
if (withMotion) loadWords(&motion, &st->motion[k], sizeof motion);
|
||||||
|
std::atomic_thread_fence(std::memory_order_acquire); // (the words are read before the second count)
|
||||||
|
if (st->seq[k].load(std::memory_order_relaxed) == s1) return true;
|
||||||
|
} else {
|
||||||
|
Slot a, b;
|
||||||
|
Motion ma, mb;
|
||||||
|
loadWords(&a, &st->slot[k], sizeof a);
|
||||||
|
if (withMotion) loadWords(&ma, &st->motion[k], sizeof ma);
|
||||||
|
loadWords(&b, &st->slot[k], sizeof b);
|
||||||
|
if (withMotion) loadWords(&mb, &st->motion[k], sizeof mb);
|
||||||
|
if (std::memcmp(&a, &b, sizeof a) == 0 && (!withMotion || std::memcmp(&ma, &mb, sizeof ma) == 0)) {
|
||||||
|
slot = a;
|
||||||
|
motion = ma;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
inline uint64_t nowNs() {
|
inline uint64_t nowNs() {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|||||||
@@ -21,12 +21,15 @@
|
|||||||
#else
|
#else
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <sys/file.h>
|
#include <sys/file.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#endif
|
#endif
|
||||||
#include "fenv_lzma.h"
|
#include "fenv_lzma.h"
|
||||||
#include "miniz.h"
|
#include "miniz.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -35,6 +38,9 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -43,14 +49,31 @@ struct FenvSource {
|
|||||||
std::string file, role, format, package, version;
|
std::string file, role, format, package, version;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A Gaussian splat object (splat_import.h): its .spz member, its name, how many splats and where it's placed.
|
||||||
|
struct FenvSplats {
|
||||||
|
std::string file, name;
|
||||||
|
uint64_t count = 0;
|
||||||
|
float xform[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; // world from the splats' own frame, column-major
|
||||||
|
};
|
||||||
|
|
||||||
struct FenvManifest {
|
struct FenvManifest {
|
||||||
std::string name;
|
std::string name;
|
||||||
std::vector<FenvSource> sources; // footprint first, then the vista (or one whole home)
|
std::vector<FenvSource> sources; // footprint first, then the vista (or one whole home)
|
||||||
std::vector<std::string> models; // imported model files (member paths)
|
std::vector<std::string> models; // imported model files (member paths)
|
||||||
|
std::vector<FenvSplats> splats; // Gaussian splat objects (written with needs = {"splats"})
|
||||||
std::string edits, thumbnail; // member paths, empty when absent
|
std::string edits, thumbnail; // member paths, empty when absent
|
||||||
std::string scene; // the baked scene (scene_bake.h): when present it IS the home, sources are info
|
std::string scene; // the baked scene (scene_bake.h): when present it IS the home, sources are info
|
||||||
std::string world; // world.json (core/world.h): spawns, seats, walls, walkable areas
|
std::string world; // world.json (core/world.h): spawns, seats, walls, walkable areas
|
||||||
|
std::string studioVersion; // "studio": the Frame Studio that saved it ("v0.2.3"; "" for files from before v0.2)
|
||||||
WorldSettings settings; // the world's own render settings (world_settings.h): none set = the headset's
|
WorldSettings settings; // the world's own render settings (world_settings.h): none set = the headset's
|
||||||
|
// What it needs a reader to understand ("splats", ...): a reader that doesn't know one refuses the file with "needs
|
||||||
|
// a newer Frame Home" instead of showing it wrong. Written only when something is needed (older readers ignore it,
|
||||||
|
// so content they can't show must also change something they do check, e.g. the baked scene's version).
|
||||||
|
std::vector<std::string> needs;
|
||||||
|
// What a newer Frame Studio wrote that this one doesn't know: its version and its other top-level keys (as JSON
|
||||||
|
// ",\n "key": value" pairs), written back as they were when this one rewrites the manifest.
|
||||||
|
int64_t version = 1;
|
||||||
|
std::string unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace fenv_detail {
|
namespace fenv_detail {
|
||||||
@@ -65,8 +88,33 @@ inline std::string quote(const std::string& s) {
|
|||||||
return o + "\"";
|
return o + "\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A parsed value written out again (the keys this version doesn't know, kept as they were).
|
||||||
|
inline std::string toJson(const tinyjson::Value& v) {
|
||||||
|
char b[40];
|
||||||
|
switch (v.type) {
|
||||||
|
case tinyjson::Type::Bool: return v.boolVal ? "true" : "false";
|
||||||
|
case tinyjson::Type::Int: std::snprintf(b, sizeof b, "%lld", static_cast<long long>(v.intVal)); return b;
|
||||||
|
// (inf / nan aren't JSON: written so, the manifest would never parse again and the file wouldn't open)
|
||||||
|
case tinyjson::Type::Float: if (!std::isfinite(v.floatVal)) return "null"; std::snprintf(b, sizeof b, "%.17g", v.floatVal); return b;
|
||||||
|
case tinyjson::Type::String: return quote(v.strVal);
|
||||||
|
case tinyjson::Type::Array: {
|
||||||
|
std::string o = "[";
|
||||||
|
for (size_t i = 0; i < v.arrVal.size(); ++i) o += (i ? ", " : "") + toJson(v.arrVal[i]);
|
||||||
|
return o + "]";
|
||||||
|
}
|
||||||
|
case tinyjson::Type::Object: {
|
||||||
|
std::string o = "{";
|
||||||
|
bool first = true;
|
||||||
|
for (const auto& kv : v.objVal) { o += (first ? "" : ", ") + quote(kv.first) + ": " + toJson(kv.second); first = false; }
|
||||||
|
return o + "}";
|
||||||
|
}
|
||||||
|
default: return "null";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline std::string manifestJson(const FenvManifest& m) {
|
inline std::string manifestJson(const FenvManifest& m) {
|
||||||
std::string j = "{\n \"format\": \"fenv\",\n \"version\": 1,\n \"name\": " + quote(m.name) + ",\n \"sources\": [";
|
std::string j = "{\n \"format\": \"fenv\",\n \"version\": " + std::to_string(std::max<int64_t>(1, m.version)) + ",\n \"name\": " +
|
||||||
|
quote(m.name) + ",\n \"sources\": [";
|
||||||
for (size_t i = 0; i < m.sources.size(); ++i) {
|
for (size_t i = 0; i < m.sources.size(); ++i) {
|
||||||
const FenvSource& s = m.sources[i];
|
const FenvSource& s = m.sources[i];
|
||||||
j += std::string(i ? "," : "") + "\n { \"file\": " + quote(s.file) + ", \"role\": " + quote(s.role) + ", \"format\": " +
|
j += std::string(i ? "," : "") + "\n { \"file\": " + quote(s.file) + ", \"role\": " + quote(s.role) + ", \"format\": " +
|
||||||
@@ -75,19 +123,42 @@ inline std::string manifestJson(const FenvManifest& m) {
|
|||||||
j += "\n ],\n \"models\": [";
|
j += "\n ],\n \"models\": [";
|
||||||
for (size_t i = 0; i < m.models.size(); ++i) j += std::string(i ? "," : "") + "\n { \"file\": " + quote(m.models[i]) + " }";
|
for (size_t i = 0; i < m.models.size(); ++i) j += std::string(i ? "," : "") + "\n { \"file\": " + quote(m.models[i]) + " }";
|
||||||
j += "\n ]";
|
j += "\n ]";
|
||||||
|
if (!m.splats.empty()) {
|
||||||
|
j += ",\n \"splats\": [";
|
||||||
|
for (size_t i = 0; i < m.splats.size(); ++i) {
|
||||||
|
const FenvSplats& s = m.splats[i];
|
||||||
|
j += std::string(i ? "," : "") + "\n { \"file\": " + quote(s.file) + ", \"name\": " + quote(s.name) + ", \"count\": " +
|
||||||
|
std::to_string(s.count) + ", \"xform\": [";
|
||||||
|
// A placement that isn't all finite numbers ("nan" / "inf" aren't JSON: the file would never open again)
|
||||||
|
// is written as where the splats were captured.
|
||||||
|
static const float kIdentity[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
|
||||||
|
const float* x = std::all_of(std::begin(s.xform), std::end(s.xform), [](float v) { return std::isfinite(v); }) ? s.xform : kIdentity;
|
||||||
|
for (int k = 0; k < 16; ++k) { char b[24]; std::snprintf(b, sizeof b, "%s%.9g", k ? ", " : "", x[k]); j += b; }
|
||||||
|
j += "] }";
|
||||||
|
}
|
||||||
|
j += "\n ]";
|
||||||
|
}
|
||||||
if (!m.edits.empty()) j += ",\n \"edits\": " + quote(m.edits);
|
if (!m.edits.empty()) j += ",\n \"edits\": " + quote(m.edits);
|
||||||
if (!m.thumbnail.empty()) j += ",\n \"thumbnail\": " + quote(m.thumbnail);
|
if (!m.thumbnail.empty()) j += ",\n \"thumbnail\": " + quote(m.thumbnail);
|
||||||
if (!m.scene.empty()) j += ",\n \"scene\": " + quote(m.scene);
|
if (!m.scene.empty()) j += ",\n \"scene\": " + quote(m.scene);
|
||||||
if (!m.world.empty()) j += ",\n \"world\": " + quote(m.world);
|
if (!m.world.empty()) j += ",\n \"world\": " + quote(m.world);
|
||||||
|
if (!m.studioVersion.empty()) j += ",\n \"studio\": " + quote(m.studioVersion);
|
||||||
if (!m.settings.empty() || !m.settings.tuned.empty()) j += ",\n \"settings\": " + m.settings.toJson();
|
if (!m.settings.empty() || !m.settings.tuned.empty()) j += ",\n \"settings\": " + m.settings.toJson();
|
||||||
return j + "\n}\n";
|
if (!m.needs.empty()) {
|
||||||
|
j += ",\n \"requires\": [";
|
||||||
|
for (size_t i = 0; i < m.needs.size(); ++i) j += (i ? ", " : "") + quote(m.needs[i]);
|
||||||
|
j += "]";
|
||||||
|
}
|
||||||
|
return j + m.unknown + "\n}\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything one .fenv may unpack to (its members, LZMA ones as unpacked): a damaged or hostile file must not
|
// Everything one .fenv may unpack to (its members, LZMA ones as unpacked): a damaged or hostile file must not
|
||||||
// fill the disk or memory. The largest homes are a few hundred MB.
|
// fill the disk or memory. The largest homes are a few hundred MB.
|
||||||
constexpr uint64_t kExtractBudget = uint64_t(4) << 30;
|
constexpr uint64_t kExtractBudget = uint64_t(4) << 30;
|
||||||
|
|
||||||
// Empty when missing or larger than `limit` (the zip's declared size, which miniz holds the inflate to).
|
// Empty when missing, larger than `limit` (the zip's declared size, which miniz holds the inflate to) or damaged.
|
||||||
|
// The CRC is checked here: miniz is built without its own checks (APKs are read in place, and quickly), but a .fenv's
|
||||||
|
// members are copied out and kept, so a flipped bit (a bad card, a cut-off copy) must not pass as the real thing.
|
||||||
inline std::vector<uint8_t> readMember(mz_zip_archive& z, const char* name, uint64_t limit = uint64_t(64) << 20) {
|
inline std::vector<uint8_t> readMember(mz_zip_archive& z, const char* name, uint64_t limit = uint64_t(64) << 20) {
|
||||||
std::vector<uint8_t> out;
|
std::vector<uint8_t> out;
|
||||||
const int i = mz_zip_reader_locate_file(&z, name, nullptr, 0);
|
const int i = mz_zip_reader_locate_file(&z, name, nullptr, 0);
|
||||||
@@ -96,11 +167,51 @@ inline std::vector<uint8_t> readMember(mz_zip_archive& z, const char* name, uint
|
|||||||
size_t n = 0;
|
size_t n = 0;
|
||||||
void* p = mz_zip_reader_extract_to_heap(&z, static_cast<mz_uint>(i), &n, 0);
|
void* p = mz_zip_reader_extract_to_heap(&z, static_cast<mz_uint>(i), &n, 0);
|
||||||
if (!p) return out;
|
if (!p) return out;
|
||||||
try { out.assign(static_cast<uint8_t*>(p), static_cast<uint8_t*>(p) + n); } catch (const std::exception&) { out = {}; }
|
if (mz_crc32(MZ_CRC32_INIT, static_cast<const unsigned char*>(p), n) == st.m_crc32)
|
||||||
|
try { out.assign(static_cast<uint8_t*>(p), static_cast<uint8_t*>(p) + n); } catch (const std::exception&) { out = {}; }
|
||||||
mz_free(p);
|
mz_free(p);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `tmp` (a finished file) in place of `out`. Where a plain rename can't (Windows, `out` open in another program),
|
||||||
|
// the old one is set aside first and put back if `tmp` still can't take its place: the user's only copy is never
|
||||||
|
// removed before the new one is there. A `tmp` that couldn't go in is removed (no stray .part left behind).
|
||||||
|
inline bool replaceWith(const std::filesystem::path& tmp, const std::filesystem::path& out, std::error_code& ec) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
fs::rename(tmp, out, ec);
|
||||||
|
if (ec) {
|
||||||
|
std::error_code e2;
|
||||||
|
fs::path old = out;
|
||||||
|
old += ".old";
|
||||||
|
fs::remove(old, e2);
|
||||||
|
fs::rename(out, old, e2);
|
||||||
|
if (!e2) {
|
||||||
|
fs::rename(tmp, out, ec);
|
||||||
|
if (ec) fs::rename(old, out, e2);
|
||||||
|
else fs::remove(old, e2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ec) { std::error_code e2; fs::remove(tmp, e2); return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Member `i` into the file `out`, its CRC checked as it's written (false: unreadable, damaged, or the disk).
|
||||||
|
inline bool extractMember(mz_zip_archive& z, mz_uint i, const mz_zip_archive_file_stat& st, const std::filesystem::path& out) {
|
||||||
|
struct Sink { std::ofstream f; mz_ulong crc = MZ_CRC32_INIT; };
|
||||||
|
Sink s;
|
||||||
|
s.f.open(out, std::ios::binary);
|
||||||
|
if (!s.f) return false;
|
||||||
|
const auto write = [](void* o, mz_uint64, const void* p, size_t n) -> size_t {
|
||||||
|
Sink& k = *static_cast<Sink*>(o);
|
||||||
|
k.crc = mz_crc32(k.crc, static_cast<const unsigned char*>(p), n);
|
||||||
|
k.f.write(static_cast<const char*>(p), static_cast<std::streamsize>(n));
|
||||||
|
return k.f ? n : 0;
|
||||||
|
};
|
||||||
|
const bool ok = mz_zip_reader_extract_to_callback(&z, i, write, &s, 0) && s.crc == st.m_crc32;
|
||||||
|
s.f.close();
|
||||||
|
return ok && !s.f.fail();
|
||||||
|
}
|
||||||
|
|
||||||
// A short tag for a file's full path: two "haven.fenv" in different folders get different cache folders.
|
// A short tag for a file's full path: two "haven.fenv" in different folders get different cache folders.
|
||||||
inline std::string pathTag(const std::filesystem::path& p) {
|
inline std::string pathTag(const std::filesystem::path& p) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
@@ -138,6 +249,80 @@ struct ExtractLock {
|
|||||||
ExtractLock& operator=(const ExtractLock&) = delete;
|
ExtractLock& operator=(const ExtractLock&) = delete;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// "In use": a lock on <cache folder>/.inuse. Every process with the folder open holds it shared; whoever removes an
|
||||||
|
// old folder (a stale version, or one not opened for 30 days) must take it exclusively without waiting, and leaves
|
||||||
|
// the folder alone when it can't. So a Frame Home that has shown one home for over a month (or Studio, or the
|
||||||
|
// switcher's unpacker in another process) never has its files pulled from under it.
|
||||||
|
struct UseLock {
|
||||||
|
#ifdef _WIN32
|
||||||
|
HANDLE h = INVALID_HANDLE_VALUE;
|
||||||
|
OVERLAPPED ov = {};
|
||||||
|
// shared: waits (only a remover holds it exclusively, briefly); exclusive: doesn't (false: in use).
|
||||||
|
bool take(const std::filesystem::path& folder, bool exclusive) {
|
||||||
|
h = CreateFileW((folder / L".inuse").wstring().c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||||
|
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||||
|
if (h == INVALID_HANDLE_VALUE) return false;
|
||||||
|
if (!LockFileEx(h, exclusive ? LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY : 0, 0, MAXDWORD, MAXDWORD, &ov)) { CloseHandle(h); h = INVALID_HANDLE_VALUE; return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void release() { if (h != INVALID_HANDLE_VALUE) { UnlockFileEx(h, 0, MAXDWORD, MAXDWORD, &ov); CloseHandle(h); h = INVALID_HANDLE_VALUE; } }
|
||||||
|
#else
|
||||||
|
int fd = -1;
|
||||||
|
bool take(const std::filesystem::path& folder, bool exclusive) {
|
||||||
|
const std::filesystem::path p = folder / ".inuse";
|
||||||
|
fd = open(p.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600);
|
||||||
|
if (fd < 0) return false;
|
||||||
|
if (flock(fd, exclusive ? LOCK_EX | LOCK_NB : LOCK_SH) != 0) { close(fd); fd = -1; return false; }
|
||||||
|
// A remover may have had it just before us and removed the folder meanwhile: then this lock is on a file
|
||||||
|
// that's gone (or one a new unpack made since), and protects nothing.
|
||||||
|
struct stat a, b;
|
||||||
|
if (fstat(fd, &a) != 0 || ::stat(p.c_str(), &b) != 0 || a.st_ino != b.st_ino || a.st_dev != b.st_dev) { release(); return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void release() { if (fd >= 0) { flock(fd, LOCK_UN); close(fd); fd = -1; } }
|
||||||
|
#endif
|
||||||
|
UseLock() = default;
|
||||||
|
~UseLock() { release(); }
|
||||||
|
UseLock(const UseLock&) = delete;
|
||||||
|
UseLock& operator=(const UseLock&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The folders this process has open, newest first: a shared UseLock on each of the last kHeld it unpacked or opened
|
||||||
|
// (Frame Home shows one home at a time and loads the next beside it; older ones are released and may be pruned).
|
||||||
|
// Held until the process exits, or fenvRelease().
|
||||||
|
struct HeldFolders {
|
||||||
|
static constexpr size_t kHeld = 4;
|
||||||
|
std::mutex m;
|
||||||
|
std::vector<std::pair<std::string, std::unique_ptr<UseLock>>> held;
|
||||||
|
static HeldFolders& get() { static HeldFolders h; return h; }
|
||||||
|
void hold(const std::string& folder, std::unique_ptr<UseLock> l) {
|
||||||
|
if (!l) return;
|
||||||
|
std::lock_guard<std::mutex> g(m);
|
||||||
|
for (auto it = held.begin(); it != held.end(); ++it) if (it->first == folder) { held.erase(it); break; }
|
||||||
|
held.insert(held.begin(), {folder, std::move(l)});
|
||||||
|
if (held.size() > kHeld) held.resize(kHeld);
|
||||||
|
}
|
||||||
|
void releaseUnder(const std::string& root) {
|
||||||
|
std::lock_guard<std::mutex> g(m);
|
||||||
|
held.erase(std::remove_if(held.begin(), held.end(), [&](const auto& h) { return h.first.compare(0, root.size(), root) == 0; }), held.end());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Removes a cache folder unless something has it open (UseLock), and only if `doomed()` still says so with the
|
||||||
|
// lock held (it may have been opened, and its .complete touched, just before). True when it went.
|
||||||
|
inline bool removeUnused(const std::filesystem::path& folder, const std::function<bool()>& doomed = {}) {
|
||||||
|
std::error_code ec;
|
||||||
|
UseLock l;
|
||||||
|
if (!l.take(folder, true)) return false;
|
||||||
|
if (doomed && !doomed()) return false;
|
||||||
|
std::filesystem::remove_all(folder, ec);
|
||||||
|
#ifdef _WIN32
|
||||||
|
l.release(); // (Windows keeps the folder while its .inuse is open)
|
||||||
|
std::filesystem::remove_all(folder, ec);
|
||||||
|
#endif
|
||||||
|
return !std::filesystem::exists(folder, ec);
|
||||||
|
}
|
||||||
|
|
||||||
inline long processId() {
|
inline long processId() {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
return static_cast<long>(_getpid());
|
return static_cast<long>(_getpid());
|
||||||
@@ -146,9 +331,27 @@ inline long processId() {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// A member path is safe to extract: relative, no "..", no drive letters.
|
// Well-formed UTF-8 (fs::u8path throws on anything else on Windows: a damaged name must not take Studio down).
|
||||||
|
inline bool validUtf8(const std::string& s) {
|
||||||
|
for (size_t i = 0; i < s.size();) {
|
||||||
|
const unsigned char c = static_cast<unsigned char>(s[i]);
|
||||||
|
const size_t n = c < 0x80 ? 1 : (c >> 5) == 6 ? 2 : (c >> 4) == 14 ? 3 : (c >> 3) == 30 ? 4 : 0;
|
||||||
|
if (!n || i + n > s.size() || (n == 2 && c < 0xC2)) return false;
|
||||||
|
uint32_t cp = n == 1 ? c : c & (0x7F >> n);
|
||||||
|
for (size_t k = 1; k < n; ++k) {
|
||||||
|
const unsigned char d = static_cast<unsigned char>(s[i + k]);
|
||||||
|
if ((d & 0xC0) != 0x80) return false;
|
||||||
|
cp = cp << 6 | (d & 0x3F);
|
||||||
|
}
|
||||||
|
if ((n == 3 && (cp < 0x800 || (cp >= 0xD800 && cp <= 0xDFFF))) || (n == 4 && (cp < 0x10000 || cp > 0x10FFFF))) return false;
|
||||||
|
i += n;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A member path is safe to extract: relative, no "..", no drive letters, UTF-8, no NUL.
|
||||||
inline bool safeMember(const std::string& m) {
|
inline bool safeMember(const std::string& m) {
|
||||||
if (m.empty() || m[0] == '/' || m[0] == '\\' || m.find(':') != std::string::npos) return false;
|
if (m.empty() || m[0] == '/' || m[0] == '\\' || m.find(':') != std::string::npos || m.find('\0') != std::string::npos || !validUtf8(m)) return false;
|
||||||
for (size_t a = 0; a <= m.size();) {
|
for (size_t a = 0; a <= m.size();) {
|
||||||
size_t b = m.find_first_of("/\\", a);
|
size_t b = m.find_first_of("/\\", a);
|
||||||
if (b == std::string::npos) b = m.size();
|
if (b == std::string::npos) b = m.size();
|
||||||
@@ -161,11 +364,14 @@ inline bool safeMember(const std::string& m) {
|
|||||||
} // namespace fenv_detail
|
} // namespace fenv_detail
|
||||||
|
|
||||||
// Where opened .fenv files are unpacked: FRAME_FENV_CACHE, else %LOCALAPPDATA%\FrameStudio\cache (Windows) or
|
// Where opened .fenv files are unpacked: FRAME_FENV_CACHE, else %LOCALAPPDATA%\FrameStudio\cache (Windows) or
|
||||||
// $XDG_CACHE_HOME / ~/.cache /frame-home/fenv.
|
// $XDG_CACHE_HOME / ~/.cache /frame-home/fenv. UTF-8: on Windows read wide (the narrow getenv is the ANSI code
|
||||||
|
// page, which u8path throws on for a user name like "José").
|
||||||
inline std::string fenvCacheDir() {
|
inline std::string fenvCacheDir() {
|
||||||
if (const char* e = std::getenv("FRAME_FENV_CACHE")) return e;
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
if (const char* l = std::getenv("LOCALAPPDATA")) return std::string(l) + "\\FrameStudio\\cache";
|
if (const wchar_t* e = _wgetenv(L"FRAME_FENV_CACHE")) return std::filesystem::path(e).u8string();
|
||||||
|
if (const wchar_t* l = _wgetenv(L"LOCALAPPDATA")) return (std::filesystem::path(l) / L"FrameStudio" / L"cache").u8string();
|
||||||
|
#else
|
||||||
|
if (const char* e = std::getenv("FRAME_FENV_CACHE")) return e;
|
||||||
#endif
|
#endif
|
||||||
if (const char* x = std::getenv("XDG_CACHE_HOME")) return std::string(x) + "/frame-home/fenv";
|
if (const char* x = std::getenv("XDG_CACHE_HOME")) return std::string(x) + "/frame-home/fenv";
|
||||||
const char* h = std::getenv("HOME");
|
const char* h = std::getenv("HOME");
|
||||||
@@ -193,17 +399,48 @@ inline bool fenvParseManifest(const std::string& json, FenvManifest& m) {
|
|||||||
const std::string f = doc["models"][i]["file"].asString();
|
const std::string f = doc["models"][i]["file"].asString();
|
||||||
if (fenv_detail::safeMember(f)) m.models.push_back(f);
|
if (fenv_detail::safeMember(f)) m.models.push_back(f);
|
||||||
}
|
}
|
||||||
|
for (size_t i = 0; i < doc["splats"].size(); ++i) {
|
||||||
|
const auto& e = doc["splats"][i];
|
||||||
|
FenvSplats s;
|
||||||
|
s.file = e["file"].asString(); s.name = e["name"].asString();
|
||||||
|
s.count = static_cast<uint64_t>(std::max<int64_t>(0, e["count"].asInt()));
|
||||||
|
if (e["xform"].size() == 16)
|
||||||
|
for (size_t k = 0; k < 16; ++k) s.xform[k] = static_cast<float>(e["xform"][k].asFloat());
|
||||||
|
if (fenv_detail::safeMember(s.file)) m.splats.push_back(s);
|
||||||
|
}
|
||||||
if (doc.has("edits") && fenv_detail::safeMember(doc["edits"].asString())) m.edits = doc["edits"].asString();
|
if (doc.has("edits") && fenv_detail::safeMember(doc["edits"].asString())) m.edits = doc["edits"].asString();
|
||||||
if (doc.has("thumbnail") && fenv_detail::safeMember(doc["thumbnail"].asString())) m.thumbnail = doc["thumbnail"].asString();
|
if (doc.has("thumbnail") && fenv_detail::safeMember(doc["thumbnail"].asString())) m.thumbnail = doc["thumbnail"].asString();
|
||||||
if (doc.has("scene") && fenv_detail::safeMember(doc["scene"].asString())) m.scene = doc["scene"].asString();
|
if (doc.has("scene") && fenv_detail::safeMember(doc["scene"].asString())) m.scene = doc["scene"].asString();
|
||||||
if (doc.has("world") && fenv_detail::safeMember(doc["world"].asString())) m.world = doc["world"].asString();
|
if (doc.has("world") && fenv_detail::safeMember(doc["world"].asString())) m.world = doc["world"].asString();
|
||||||
if (doc.has("settings")) m.settings.fromValue(doc["settings"]);
|
if (doc.has("settings")) m.settings.fromValue(doc["settings"]);
|
||||||
return !m.sources.empty() || !m.models.empty() || !m.scene.empty();
|
m.studioVersion = doc["studio"].asString();
|
||||||
|
for (size_t i = 0; i < doc["requires"].size(); ++i) m.needs.push_back(doc["requires"][i].asString());
|
||||||
|
m.version = std::max<int64_t>(1, doc["version"].asInt());
|
||||||
|
static const char* kKnown[] = {"format", "version", "name", "sources", "models", "splats", "edits", "thumbnail", "scene", "world",
|
||||||
|
"settings", "requires", "studio"};
|
||||||
|
for (const auto& kv : doc.objVal)
|
||||||
|
if (std::none_of(std::begin(kKnown), std::end(kKnown), [&](const char* k) { return kv.first == k; }))
|
||||||
|
m.unknown += ",\n " + fenv_detail::quote(kv.first) + ": " + fenv_detail::toJson(kv.second);
|
||||||
|
return !m.sources.empty() || !m.models.empty() || !m.scene.empty() || !m.splats.empty();
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The features this build reads (FenvManifest::needs), and the ones a manifest asks for that it doesn't ("" none).
|
||||||
|
inline const std::vector<std::string>& fenvFeatures() {
|
||||||
|
static const std::vector<std::string> known = {"splats"}; // (sky panoramas, ... join as they're written)
|
||||||
|
return known;
|
||||||
|
}
|
||||||
|
// Who made it, as shown: "Frame Studio v0.2.3", or "made before v0.2" (the version wasn't written then).
|
||||||
|
inline std::string fenvMadeWith(const FenvManifest& m) { return m.studioVersion.empty() ? "made before v0.2" : "Frame Studio " + m.studioVersion; }
|
||||||
|
inline std::string fenvMissingFeatures(const FenvManifest& m) {
|
||||||
|
std::string missing;
|
||||||
|
for (const std::string& r : m.needs)
|
||||||
|
if (std::find(fenvFeatures().begin(), fenvFeatures().end(), r) == fenvFeatures().end()) missing += (missing.empty() ? "" : ", ") + r;
|
||||||
|
return missing;
|
||||||
|
}
|
||||||
|
|
||||||
// Reads fenv.json (and, if asked, the thumbnail bytes) without extracting anything else.
|
// Reads fenv.json (and, if asked, the thumbnail bytes) without extracting anything else.
|
||||||
inline bool fenvReadManifest(const std::string& path, FenvManifest& m, std::vector<uint8_t>* thumb = nullptr) {
|
inline bool fenvReadManifest(const std::string& path, FenvManifest& m, std::vector<uint8_t>* thumb = nullptr) {
|
||||||
mz_zip_archive z = {};
|
mz_zip_archive z = {};
|
||||||
@@ -216,7 +453,8 @@ inline bool fenvReadManifest(const std::string& path, FenvManifest& m, std::vect
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extracts every member into <cacheRoot>/<name>-<path tag>-<size>-<time>/ (once) and returns the sources' and
|
// Extracts every member into <cacheRoot>/<name>-<path tag>-<size>-<time>/ (once) and returns the sources' and
|
||||||
// models' paths there, plus the edits file ("" when the environment has none).
|
// models' paths there, plus the edits file ("" when the environment has none). The scene, world and splat members `m`
|
||||||
|
// names become their paths there too.
|
||||||
inline bool fenvExtract(const std::string& path, const std::string& cacheRoot, FenvManifest& m,
|
inline bool fenvExtract(const std::string& path, const std::string& cacheRoot, FenvManifest& m,
|
||||||
std::vector<std::string>& sources, std::vector<std::string>& models, std::string& editsPath,
|
std::vector<std::string>& sources, std::vector<std::string>& models, std::string& editsPath,
|
||||||
std::string& error) {
|
std::string& error) {
|
||||||
@@ -227,13 +465,22 @@ inline bool fenvExtract(const std::string& path, const std::string& cacheRoot, F
|
|||||||
// (a 128-bit count on some standard libraries: no to_string for it, and 64 bits hold any file's date)
|
// (a 128-bit count on some standard libraries: no to_string for it, and 64 bits hold any file's date)
|
||||||
const long long time = static_cast<long long>(fs::last_write_time(src, ec).time_since_epoch().count());
|
const long long time = static_cast<long long>(fs::last_write_time(src, ec).time_since_epoch().count());
|
||||||
if (!fenvReadManifest(path, m)) { error = "not a Frame environment (.fenv): " + path; return false; }
|
if (!fenvReadManifest(path, m)) { error = "not a Frame environment (.fenv): " + path; return false; }
|
||||||
|
if (const std::string missing = fenvMissingFeatures(m); !missing.empty()) {
|
||||||
|
error = "made by a newer Frame Studio: update Frame Studio and Frame Home to open it (it uses " + missing + ")";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const std::string stem = src.stem().u8string(), key = stem + "-" + fenv_detail::pathTag(src);
|
const std::string stem = src.stem().u8string(), key = stem + "-" + fenv_detail::pathTag(src);
|
||||||
const fs::path root = fs::u8path(cacheRoot);
|
const fs::path root = fs::u8path(cacheRoot);
|
||||||
const fs::path dir = root / (key + "-" + std::to_string(size) + "-" + std::to_string(time));
|
const fs::path dir = root / (key + "-" + std::to_string(size) + "-" + std::to_string(time));
|
||||||
const fs::path done = dir / ".complete";
|
const fs::path done = dir / ".complete";
|
||||||
fs::create_directories(root, ec);
|
fs::create_directories(root, ec);
|
||||||
const fenv_detail::ExtractLock lock(root / (key + ".lock"));
|
const fenv_detail::ExtractLock lock(root / (key + ".lock"));
|
||||||
if (!fs::exists(done, ec)) {
|
// In use from before it's looked at until this process is done with it (fenv_detail::UseLock): no other process's
|
||||||
|
// prune can take it away now. (None yet, or a remover got it first: unpacked again below.)
|
||||||
|
auto use = std::make_unique<fenv_detail::UseLock>();
|
||||||
|
if (!fs::is_directory(dir, ec) || !use->take(dir, false)) use.reset();
|
||||||
|
if (!use || !fs::exists(done, ec)) {
|
||||||
|
use.reset();
|
||||||
// Earlier versions of this environment (same file, other size / time) are stale now, as are folders of the
|
// Earlier versions of this environment (same file, other size / time) are stale now, as are folders of the
|
||||||
// naming before the path tag ("<name>-<size>-<time>") and unpacks that never finished (".tmp-<pid>": the
|
// naming before the path tag ("<name>-<size>-<time>") and unpacks that never finished (".tmp-<pid>": the
|
||||||
// lock is ours, so nobody is still writing one).
|
// lock is ours, so nobody is still writing one).
|
||||||
@@ -257,7 +504,7 @@ inline bool fenvExtract(const std::string& path, const std::string& cacheRoot, F
|
|||||||
ours = sizeTime(rest);
|
ours = sizeTime(rest);
|
||||||
}
|
}
|
||||||
const bool legacy = f.rfind(stem + "-", 0) == 0 && sizeTime(f.substr(stem.size() + 1));
|
const bool legacy = f.rfind(stem + "-", 0) == 0 && sizeTime(f.substr(stem.size() + 1));
|
||||||
if (ours || legacy) fs::remove_all(e.path(), ec);
|
if (ours || legacy) fenv_detail::removeUnused(e.path()); // (one still open somewhere stays: next time)
|
||||||
}
|
}
|
||||||
// Unpacked beside the cache folder and renamed into place once whole: a crash or a full disk midway
|
// Unpacked beside the cache folder and renamed into place once whole: a crash or a full disk midway
|
||||||
// leaves no folder that looks finished.
|
// leaves no folder that looks finished.
|
||||||
@@ -284,6 +531,7 @@ inline bool fenvExtract(const std::string& path, const std::string& cacheRoot, F
|
|||||||
const fs::path out = tmp / fs::u8path(name);
|
const fs::path out = tmp / fs::u8path(name);
|
||||||
fs::create_directories(out.parent_path(), ec);
|
fs::create_directories(out.parent_path(), ec);
|
||||||
if (name.size() > 3 && name.compare(name.size() - 3, 3, ".lz") == 0) {
|
if (name.size() > 3 && name.compare(name.size() - 3, 3, ".lz") == 0) {
|
||||||
|
if (!fenv_detail::safeMember(name.substr(0, name.size() - 3))) continue; // ("...lz" would unpack to "..")
|
||||||
std::string why;
|
std::string why;
|
||||||
try {
|
try {
|
||||||
std::vector<uint8_t> packed = fenv_detail::readMember(z, name.c_str(), fenv_detail::kExtractBudget - written), raw;
|
std::vector<uint8_t> packed = fenv_detail::readMember(z, name.c_str(), fenv_detail::kExtractBudget - written), raw;
|
||||||
@@ -305,8 +553,8 @@ inline bool fenvExtract(const std::string& path, const std::string& cacheRoot, F
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
written += std::max(st.m_comp_size, st.m_uncomp_size);
|
written += std::max(st.m_comp_size, st.m_uncomp_size);
|
||||||
ok = written <= fenv_detail::kExtractBudget && mz_zip_reader_extract_to_file(&z, i, out.u8string().c_str(), 0);
|
ok = written <= fenv_detail::kExtractBudget && fenv_detail::extractMember(z, i, st, out);
|
||||||
if (!ok) error = "couldn't extract " + name + " from " + path;
|
if (!ok) error = "couldn't extract " + name + " from " + path + " (damaged, or the disk is full)";
|
||||||
}
|
}
|
||||||
mz_zip_reader_end(&z);
|
mz_zip_reader_end(&z);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -322,7 +570,29 @@ inline bool fenvExtract(const std::string& path, const std::string& cacheRoot, F
|
|||||||
if (!ok) error = "couldn't finish the cache for " + path + ": " + ec.message();
|
if (!ok) error = "couldn't finish the cache for " + path + ": " + ec.message();
|
||||||
}
|
}
|
||||||
if (!ok) { fs::remove_all(tmp, ec); return false; }
|
if (!ok) { fs::remove_all(tmp, ec); return false; }
|
||||||
|
use = std::make_unique<fenv_detail::UseLock>();
|
||||||
|
if (!use->take(dir, false)) use.reset();
|
||||||
|
// Environments not opened for 30 days go (they unpack again when they're next opened): the cache doesn't
|
||||||
|
// grow by every .fenv ever opened. One still open somewhere stays, however long ago it was opened: a Frame
|
||||||
|
// Home showing the same home for a month holds it (UseLock), and so does every other process using one.
|
||||||
|
const auto unused = fs::file_time_type::clock::now() - std::chrono::hours(24 * 30);
|
||||||
|
std::vector<fs::path> old;
|
||||||
|
for (const auto& e : fs::directory_iterator(root, ec)) {
|
||||||
|
if (!e.is_directory(ec) || e.path() == dir) continue;
|
||||||
|
std::error_code tec;
|
||||||
|
const fs::file_time_type used = fs::last_write_time(e.path() / ".complete", tec);
|
||||||
|
if (!tec && used < unused) old.push_back(e.path());
|
||||||
|
}
|
||||||
|
for (const fs::path& o : old)
|
||||||
|
fenv_detail::removeUnused(o, [&] { // (still unused with the lock held: not opened just now)
|
||||||
|
std::error_code tec;
|
||||||
|
const fs::file_time_type used = fs::last_write_time(o / ".complete", tec);
|
||||||
|
return !tec && used < unused;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fs::last_write_time(done, fs::file_time_type::clock::now(), ec); // used now: the last to be pruned
|
||||||
}
|
}
|
||||||
|
fenv_detail::HeldFolders::get().hold(dir.u8string(), std::move(use));
|
||||||
sources.clear();
|
sources.clear();
|
||||||
models.clear();
|
models.clear();
|
||||||
for (const FenvSource& s : m.sources)
|
for (const FenvSource& s : m.sources)
|
||||||
@@ -331,9 +601,14 @@ inline bool fenvExtract(const std::string& path, const std::string& cacheRoot, F
|
|||||||
editsPath = m.edits.empty() ? std::string() : (dir / fs::u8path(m.edits)).u8string();
|
editsPath = m.edits.empty() ? std::string() : (dir / fs::u8path(m.edits)).u8string();
|
||||||
if (!m.scene.empty()) m.scene = (dir / fs::u8path(m.scene)).u8string(); // now the extracted file
|
if (!m.scene.empty()) m.scene = (dir / fs::u8path(m.scene)).u8string(); // now the extracted file
|
||||||
if (!m.world.empty()) m.world = (dir / fs::u8path(m.world)).u8string();
|
if (!m.world.empty()) m.world = (dir / fs::u8path(m.world)).u8string();
|
||||||
|
for (FenvSplats& s : m.splats) s.file = (dir / fs::u8path(s.file)).u8string();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This process is done with the cache folders it unpacked under `cacheRoot` (fenvExtract holds the last few open):
|
||||||
|
// they may be pruned, or removed, now (on Windows a folder can't be removed while its .inuse is held).
|
||||||
|
inline void fenvRelease(const std::string& cacheRoot) { fenv_detail::HeldFolders::get().releaseUnder(cacheRoot); }
|
||||||
|
|
||||||
// One member to write: its path inside the .fenv and either a file on disk or bytes.
|
// One member to write: its path inside the .fenv and either a file on disk or bytes.
|
||||||
struct FenvMember {
|
struct FenvMember {
|
||||||
std::string name, file;
|
std::string name, file;
|
||||||
@@ -349,8 +624,22 @@ inline bool fenvWrite(const std::string& outPath, const FenvManifest& m, const s
|
|||||||
const std::function<void(size_t, size_t)>& progress = {}) {
|
const std::function<void(size_t, size_t)>& progress = {}) {
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
const std::string tmp = outPath + ".part";
|
const std::string tmp = outPath + ".part";
|
||||||
|
// At most what it holds unpacked (as fenvExtract counts it): past the extract budget it could never be opened,
|
||||||
|
// so it isn't written; near 4 GB it's zip64 from the start (as fenv_bundle.h: miniz can't finish an archive whose
|
||||||
|
// member ends past 4 GB unless it was zip64 before that member began).
|
||||||
|
uint64_t total = 0;
|
||||||
|
for (const FenvMember& f : members) {
|
||||||
|
std::error_code ec;
|
||||||
|
total += f.file.empty() ? f.bytes.size() : fs::file_size(fs::u8path(f.file), ec);
|
||||||
|
}
|
||||||
|
if (total > fenv_detail::kExtractBudget) {
|
||||||
|
error = "too large: " + std::to_string(total >> 20) + " MB unpacked, over the " + std::to_string(fenv_detail::kExtractBudget >> 20) +
|
||||||
|
" MB an environment may hold";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const mz_uint zip64 = total + (uint64_t(1) << 20) >= 0xFFFFFFFFull ? MZ_ZIP_FLAG_WRITE_ZIP64 : 0;
|
||||||
mz_zip_archive z = {};
|
mz_zip_archive z = {};
|
||||||
if (!mz_zip_writer_init_file_v2(&z, tmp.c_str(), 0, 0)) { error = "can't write " + outPath; return false; }
|
if (!mz_zip_writer_init_file_v2(&z, tmp.c_str(), 0, zip64)) { error = "can't write " + outPath; return false; }
|
||||||
const std::string json = fenv_detail::manifestJson(m);
|
const std::string json = fenv_detail::manifestJson(m);
|
||||||
bool ok = mz_zip_writer_add_mem(&z, "fenv.json", json.data(), json.size(), MZ_DEFAULT_COMPRESSION);
|
bool ok = mz_zip_writer_add_mem(&z, "fenv.json", json.data(), json.size(), MZ_DEFAULT_COMPRESSION);
|
||||||
for (const FenvMember& f : members) {
|
for (const FenvMember& f : members) {
|
||||||
@@ -372,9 +661,7 @@ inline bool fenvWrite(const std::string& outPath, const FenvManifest& m, const s
|
|||||||
mz_zip_writer_end(&z);
|
mz_zip_writer_end(&z);
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (!ok) { fs::remove(fs::u8path(tmp), ec); if (error.empty()) error = "couldn't finish " + outPath; return false; }
|
if (!ok) { fs::remove(fs::u8path(tmp), ec); if (error.empty()) error = "couldn't finish " + outPath; return false; }
|
||||||
fs::rename(fs::u8path(tmp), fs::u8path(outPath), ec);
|
if (!fenv_detail::replaceWith(fs::u8path(tmp), fs::u8path(outPath), ec)) { error = "couldn't replace " + outPath + ": " + ec.message(); return false; }
|
||||||
if (ec) { fs::remove(fs::u8path(outPath), ec); fs::rename(fs::u8path(tmp), fs::u8path(outPath), ec); }
|
|
||||||
if (ec) { error = "couldn't replace " + outPath + ": " + ec.message(); return false; }
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +672,11 @@ inline bool fenvRewriteManifest(const std::string& path, const FenvManifest& m,
|
|||||||
mz_zip_archive in = {}, out = {};
|
mz_zip_archive in = {}, out = {};
|
||||||
if (!mz_zip_reader_init_file(&in, path.c_str(), 0)) { error = "can't read " + path; return false; }
|
if (!mz_zip_reader_init_file(&in, path.c_str(), 0)) { error = "can't read " + path; return false; }
|
||||||
const std::string tmp = path + ".part";
|
const std::string tmp = path + ".part";
|
||||||
if (!mz_zip_writer_init_file_v2(&out, tmp.c_str(), 0, 0)) { mz_zip_reader_end(&in); error = "can't write " + tmp; return false; }
|
// Zip64 again when the one read is (or nearly so): miniz can't finish an archive past 4 GB that didn't start as zip64.
|
||||||
|
std::error_code sizeEc;
|
||||||
|
const uint64_t inSize = std::filesystem::file_size(std::filesystem::u8path(path), sizeEc);
|
||||||
|
const mz_uint zip64 = mz_zip_is_zip64(&in) || (!sizeEc && inSize + (uint64_t(1) << 20) >= 0xFFFFFFFFull) ? MZ_ZIP_FLAG_WRITE_ZIP64 : 0;
|
||||||
|
if (!mz_zip_writer_init_file_v2(&out, tmp.c_str(), 0, zip64)) { mz_zip_reader_end(&in); error = "can't write " + tmp; return false; }
|
||||||
const std::string json = fenv_detail::manifestJson(m);
|
const std::string json = fenv_detail::manifestJson(m);
|
||||||
bool ok = mz_zip_writer_add_mem(&out, "fenv.json", json.data(), json.size(), MZ_DEFAULT_COMPRESSION);
|
bool ok = mz_zip_writer_add_mem(&out, "fenv.json", json.data(), json.size(), MZ_DEFAULT_COMPRESSION);
|
||||||
const mz_uint n = mz_zip_reader_get_num_files(&in);
|
const mz_uint n = mz_zip_reader_get_num_files(&in);
|
||||||
@@ -401,8 +692,7 @@ inline bool fenvRewriteManifest(const std::string& path, const FenvManifest& m,
|
|||||||
mz_zip_reader_end(&in);
|
mz_zip_reader_end(&in);
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (!ok) { fs::remove(fs::u8path(tmp), ec); if (error.empty()) error = "couldn't finish " + tmp; return false; }
|
if (!ok) { fs::remove(fs::u8path(tmp), ec); if (error.empty()) error = "couldn't finish " + tmp; return false; }
|
||||||
fs::rename(fs::u8path(tmp), fs::u8path(path), ec);
|
if (!fenv_detail::replaceWith(fs::u8path(tmp), fs::u8path(path), ec)) { error = "couldn't replace " + path + ": " + ec.message(); return false; }
|
||||||
if (ec) { fs::remove(fs::u8path(tmp), ec); error = "couldn't replace " + path + ": " + ec.message(); return false; }
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,12 +704,9 @@ inline WorldSettings worldSettingsFor(const std::string& homePath) {
|
|||||||
if (fenvReadManifest(homePath, m)) w = m.settings;
|
if (fenvReadManifest(homePath, m)) w = m.settings;
|
||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
std::FILE* f = std::fopen(worldSettingsPathFor(homePath).c_str(), "rb");
|
std::ifstream f(std::filesystem::u8path(worldSettingsPathFor(homePath)), std::ios::binary); // (by its UTF-8 name)
|
||||||
if (!f) return w;
|
if (!f) return w;
|
||||||
std::string j;
|
const std::string j((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||||
char b[4096];
|
|
||||||
for (size_t k; (k = std::fread(b, 1, sizeof b, f)) > 0;) j.append(b, k);
|
|
||||||
std::fclose(f);
|
|
||||||
if (!w.fromJson(j)) w = WorldSettings();
|
if (!w.fromJson(j)) w = WorldSettings();
|
||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
@@ -436,9 +723,12 @@ inline bool setWorldSettings(const std::string& homePath, const WorldSettings& w
|
|||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (w.empty() && w.tuned.empty()) { std::filesystem::remove(std::filesystem::u8path(p), ec); return true; }
|
if (w.empty() && w.tuned.empty()) { std::filesystem::remove(std::filesystem::u8path(p), ec); return true; }
|
||||||
const std::string j = w.toJson() + "\n", tmp = p + ".part";
|
const std::string j = w.toJson() + "\n", tmp = p + ".part";
|
||||||
std::FILE* f = std::fopen(tmp.c_str(), "wb");
|
{
|
||||||
if (!f || std::fwrite(j.data(), 1, j.size(), f) != j.size()) { if (f) std::fclose(f); error = "can't write " + p; return false; }
|
std::ofstream f(std::filesystem::u8path(tmp), std::ios::binary | std::ios::trunc);
|
||||||
std::fclose(f);
|
f.write(j.data(), static_cast<std::streamsize>(j.size()));
|
||||||
|
f.close();
|
||||||
|
if (!f) { error = "can't write " + p; return false; }
|
||||||
|
}
|
||||||
std::filesystem::rename(std::filesystem::u8path(tmp), std::filesystem::u8path(p), ec);
|
std::filesystem::rename(std::filesystem::u8path(tmp), std::filesystem::u8path(p), ec);
|
||||||
if (ec) { error = "can't write " + p + ": " + ec.message(); return false; }
|
if (ec) { error = "can't write " + p + ": " + ec.message(); return false; }
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -242,9 +242,9 @@ inline bool fenvBundleWrite(const std::string& outPath, const std::vector<std::s
|
|||||||
mz_zip_writer_end(&zip);
|
mz_zip_writer_end(&zip);
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (!ok) { fs::remove(fs::u8path(tmp), ec); return false; }
|
if (!ok) { fs::remove(fs::u8path(tmp), ec); return false; }
|
||||||
fs::rename(fs::u8path(tmp), fs::u8path(outPath), ec);
|
// (the old bundle set aside, not deleted, until the new one is in its place: removing it first and then failing
|
||||||
if (ec) { fs::remove(fs::u8path(outPath), ec); fs::rename(fs::u8path(tmp), fs::u8path(outPath), ec); }
|
// the rename lost both)
|
||||||
if (ec) { fs::remove(fs::u8path(tmp), ec); error = "couldn't replace " + outPath + ": " + ec.message(); return false; }
|
if (!fenv_detail::replaceWith(fs::u8path(tmp), fs::u8path(outPath), ec)) { error = "couldn't replace " + outPath + ": " + ec.message(); return false; }
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,17 +92,36 @@ int pack(const std::string& out, const std::vector<std::string>& inputs, std::st
|
|||||||
b.packing = g_packing;
|
b.packing = g_packing;
|
||||||
members.push_back(std::move(b));
|
members.push_back(std::move(b));
|
||||||
}
|
}
|
||||||
|
// Member names taken so far: two inputs of the same file name (a footprint and a vista both pulled as base.apk,
|
||||||
|
// two models called chair) each get their own, "2-base.apk", rather than one overwriting the other.
|
||||||
|
std::set<std::string> taken;
|
||||||
|
const auto unique = [&](const std::string& folder, const std::string& name) {
|
||||||
|
std::string n = folder + name;
|
||||||
|
for (int k = 2; !taken.insert(n).second; ++k) n = folder + std::to_string(k) + "-" + name;
|
||||||
|
return n;
|
||||||
|
};
|
||||||
for (size_t i = 0; i < inputs.size(); ++i) {
|
for (size_t i = 0; i < inputs.size(); ++i) {
|
||||||
const HslSourceInfo info = i < scene.sources.size() ? scene.sources[i] : HslSourceInfo{};
|
const HslSourceInfo info = i < scene.sources.size() ? scene.sources[i] : HslSourceInfo{};
|
||||||
|
if (info.role == "Splats") continue; // below, as splat members
|
||||||
if (info.role == "Model") {
|
if (info.role == "Model") {
|
||||||
if (baked) continue; // its meshes are in scene.bin
|
if (baked) continue; // its meshes are in scene.bin
|
||||||
const fs::path base = fs::u8path(inputs[i]).parent_path();
|
const std::string dir = unique("models/", stem(inputs[i])) + "/";
|
||||||
const std::string dir = "models/" + stem(inputs[i]) + "/";
|
|
||||||
const std::vector<std::string>& files = i < scene.sourceFiles.size() ? scene.sourceFiles[i] : std::vector<std::string>{inputs[i]};
|
const std::vector<std::string>& files = i < scene.sourceFiles.size() ? scene.sourceFiles[i] : std::vector<std::string>{inputs[i]};
|
||||||
|
// Kept as laid out on disk, from the folder all its files share (a texture in ../textures stays where the
|
||||||
|
// model's own path to it points); files with nothing in common (another drive) go by their name alone.
|
||||||
|
fs::path base = fs::u8path(inputs[i]).parent_path();
|
||||||
|
for (const std::string& f : files) {
|
||||||
|
const fs::path p = fs::u8path(f).parent_path();
|
||||||
|
for (;;) {
|
||||||
|
const std::string r = p.lexically_relative(base).u8string(); // ("" for another drive)
|
||||||
|
if (base.empty() || base == base.root_path() || (!r.empty() && r.rfind("..", 0) != 0)) break;
|
||||||
|
base = base.parent_path();
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const std::string& f : files) {
|
for (const std::string& f : files) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
fs::path rel = fs::relative(fs::u8path(f), base, ec);
|
fs::path rel = fs::u8path(f).lexically_relative(base);
|
||||||
if (ec || rel.empty() || rel.u8string().find("..") == 0) rel = fs::u8path(f).filename();
|
if (rel.empty() || rel.u8string().rfind("..", 0) == 0 || rel.is_absolute()) rel = fs::u8path(f).filename();
|
||||||
FenvMember mm;
|
FenvMember mm;
|
||||||
mm.name = dir + rel.generic_u8string();
|
mm.name = dir + rel.generic_u8string();
|
||||||
mm.file = f;
|
mm.file = f;
|
||||||
@@ -112,7 +131,7 @@ int pack(const std::string& out, const std::vector<std::string>& inputs, std::st
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
FenvSource src;
|
FenvSource src;
|
||||||
src.file = "apks/" + fs::u8path(inputs[i]).filename().u8string();
|
src.file = unique("apks/", fs::u8path(inputs[i]).filename().u8string());
|
||||||
src.role = info.role; src.format = info.format; src.package = info.package; src.version = info.version;
|
src.role = info.role; src.format = info.format; src.package = info.package; src.version = info.version;
|
||||||
m.sources.push_back(src);
|
m.sources.push_back(src);
|
||||||
if (!baked || keepApks) {
|
if (!baked || keepApks) {
|
||||||
@@ -123,12 +142,13 @@ int pack(const std::string& out, const std::vector<std::string>& inputs, std::st
|
|||||||
members.push_back(a);
|
members.push_back(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
addSplatMembers(scene.splats, m, members);
|
||||||
if (!editsFile.empty()) { m.edits = "edits.json"; FenvMember e; e.name = m.edits; e.file = editsFile; members.push_back(e); }
|
if (!editsFile.empty()) { m.edits = "edits.json"; FenvMember e; e.name = m.edits; e.file = editsFile; members.push_back(e); }
|
||||||
if (!thumbFile.empty()) { m.thumbnail = "thumb.png"; FenvMember t; t.name = m.thumbnail; t.file = thumbFile; t.store = true; members.push_back(t); }
|
if (!thumbFile.empty()) { m.thumbnail = "thumb.png"; FenvMember t; t.name = m.thumbnail; t.file = thumbFile; t.store = true; members.push_back(t); }
|
||||||
if (!fenvWrite(out, m, members, err)) { std::fprintf(stderr, "fenv-tool: %s\n", err.c_str()); return 1; }
|
if (!fenvWrite(out, m, members, err)) { std::fprintf(stderr, "fenv-tool: %s\n", err.c_str()); return 1; }
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
const double s = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
const double s = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
||||||
std::printf("%s: \"%s\", %zu meshes, %s, %.0f MB, %.1f s\n", out.c_str(), m.name.c_str(), scene.meshes.size(),
|
std::printf("%s: \"%s\", %zu meshes, %zu splat objects, %s, %.0f MB, %.1f s\n", out.c_str(), m.name.c_str(), scene.meshes.size(), scene.splats.size(),
|
||||||
baked ? "baked" : ("from its homes (" + err + ")").c_str(), fs::file_size(fs::u8path(out), ec) / 1048576.0, s);
|
baked ? "baked" : ("from its homes (" + err + ")").c_str(), fs::file_size(fs::u8path(out), ec) / 1048576.0, s);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -179,6 +199,16 @@ int info(const std::string& path) {
|
|||||||
std::printf("%s\n name: %s\n %s\n", path.c_str(), m.name.c_str(), m.scene.empty() ? "loads from its homes" : "baked scene");
|
std::printf("%s\n name: %s\n %s\n", path.c_str(), m.name.c_str(), m.scene.empty() ? "loads from its homes" : "baked scene");
|
||||||
for (const FenvSource& s : m.sources) std::printf(" %-9s %-17s %s %s\n", s.role.c_str(), s.format.c_str(), s.package.c_str(), s.version.c_str());
|
for (const FenvSource& s : m.sources) std::printf(" %-9s %-17s %s %s\n", s.role.c_str(), s.format.c_str(), s.package.c_str(), s.version.c_str());
|
||||||
for (const std::string& f : m.models) std::printf(" model %s\n", f.c_str());
|
for (const std::string& f : m.models) std::printf(" model %s\n", f.c_str());
|
||||||
|
for (const FenvSplats& sp : m.splats)
|
||||||
|
std::printf(" splats %-17s %llu splats, %s, at %.2f %.2f %.2f\n", sp.name.c_str(), static_cast<unsigned long long>(sp.count), sp.file.c_str(),
|
||||||
|
sp.xform[12], sp.xform[13], sp.xform[14]);
|
||||||
|
if (!m.needs.empty()) {
|
||||||
|
std::string needs;
|
||||||
|
for (const std::string& r : m.needs) needs += (needs.empty() ? "" : ", ") + r;
|
||||||
|
const std::string missing = fenvMissingFeatures(m);
|
||||||
|
std::printf(" requires: %s%s\n", needs.c_str(), missing.empty() ? "" : (" (this build lacks " + missing + ")").c_str());
|
||||||
|
}
|
||||||
|
std::printf(" made with: %s\n", fenvMadeWith(m).c_str());
|
||||||
std::printf(" edits: %s, thumbnail: %s\n", m.edits.empty() ? "no" : "yes", thumb.empty() ? "no" : "yes");
|
std::printf(" edits: %s, thumbnail: %s\n", m.edits.empty() ? "no" : "yes", thumb.empty() ? "no" : "yes");
|
||||||
std::printf(" settings: %s%s\n", m.settings.describe().c_str(), m.settings.tuned.empty() ? "" : (" (" + m.settings.tuned + ")").c_str());
|
std::printf(" settings: %s%s\n", m.settings.describe().c_str(), m.settings.tuned.empty() ? "" : (" (" + m.settings.tuned + ")").c_str());
|
||||||
return 0;
|
return 0;
|
||||||
@@ -213,7 +243,8 @@ int upgrade(const std::string& path) {
|
|||||||
if (!bakeScene(scene, blob, err, optimized ? &opt : nullptr)) { std::fprintf(stderr, "fenv-tool: %s: %s\n", path.c_str(), err.c_str()); return 1; }
|
if (!bakeScene(scene, blob, err, optimized ? &opt : nullptr)) { std::fprintf(stderr, "fenv-tool: %s: %s\n", path.c_str(), err.c_str()); return 1; }
|
||||||
std::vector<FenvMember> members;
|
std::vector<FenvMember> members;
|
||||||
const std::string world = m.world;
|
const std::string world = m.world;
|
||||||
if (fromHomes) { m.scene = "scene.bin"; m.world.clear(); m.models.clear(); }
|
// (rebuilt from its homes: what a newer Studio added to the manifest may name members that aren't kept)
|
||||||
|
if (fromHomes) { m.scene = "scene.bin"; m.world.clear(); m.models.clear(); m.unknown.clear(); m.version = 1; }
|
||||||
FenvMember b;
|
FenvMember b;
|
||||||
b.name = m.scene;
|
b.name = m.scene;
|
||||||
b.bytes = std::move(blob);
|
b.bytes = std::move(blob);
|
||||||
@@ -450,9 +481,12 @@ int main(int argc, char** argv) {
|
|||||||
}
|
}
|
||||||
if (argc >= 4 && !std::strcmp(argv[1], "unbundle")) return unbundle(argv[2], argv[3]);
|
if (argc >= 4 && !std::strcmp(argv[1], "unbundle")) return unbundle(argv[2], argv[3]);
|
||||||
if (argc >= 3 && !std::strcmp(argv[1], "upgrade")) {
|
if (argc >= 3 && !std::strcmp(argv[1], "upgrade")) {
|
||||||
for (int i = 2; i < argc; ++i) if (!std::strcmp(argv[i], "--no-optimize")) g_optimize = false;
|
for (int i = 2; i < argc; ++i) {
|
||||||
|
if (!std::strcmp(argv[i], "--no-optimize")) g_optimize = false;
|
||||||
|
else if (!std::strcmp(argv[i], "--fast")) g_packing = FenvPacking::Fast;
|
||||||
|
}
|
||||||
int rc = 0;
|
int rc = 0;
|
||||||
for (int i = 2; i < argc; ++i) if (std::strcmp(argv[i], "--no-optimize")) rc = std::max(rc, upgrade(argv[i]));
|
for (int i = 2; i < argc; ++i) if (std::strcmp(argv[i], "--no-optimize") && std::strcmp(argv[i], "--fast")) rc = std::max(rc, upgrade(argv[i]));
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
if (argc >= 3 && !std::strcmp(argv[1], "report")) {
|
if (argc >= 3 && !std::strcmp(argv[1], "report")) {
|
||||||
@@ -485,6 +519,7 @@ int main(int argc, char** argv) {
|
|||||||
for (int i = 3; i < argc; ++i) {
|
for (int i = 3; i < argc; ++i) {
|
||||||
const std::string a = argv[i];
|
const std::string a = argv[i];
|
||||||
if (a == "--no-optimize") { g_optimize = false; continue; }
|
if (a == "--no-optimize") { g_optimize = false; continue; }
|
||||||
|
if (a == "--fast") { g_packing = FenvPacking::Fast; continue; }
|
||||||
const std::vector<std::string> group = homeFiles(a);
|
const std::vector<std::string> group = homeFiles(a);
|
||||||
std::string base;
|
std::string base;
|
||||||
for (const auto& g : group) base += (base.empty() ? "" : "+") + stem(g);
|
for (const auto& g : group) base += (base.empty() ? "" : "+") + stem(g);
|
||||||
|
|||||||
@@ -232,7 +232,11 @@ private:
|
|||||||
mutable std::vector<uint32_t> stamp_;
|
mutable std::vector<uint32_t> stamp_;
|
||||||
mutable uint32_t gen_ = 0;
|
mutable uint32_t gen_ = 0;
|
||||||
|
|
||||||
static int cell(float v) { return static_cast<int>(std::floor(v / kCell)); }
|
// (clamped, NaN as 0: a damaged file's 1e30 or NaN vertex must not overflow the ints the cell loops count in)
|
||||||
|
static int cell(float v) {
|
||||||
|
const float c = std::floor(v / kCell);
|
||||||
|
return c > -1e6f && c < 1e6f ? static_cast<int>(c) : c >= 1e6f ? 1000000 : c <= -1e6f ? -1000000 : 0;
|
||||||
|
}
|
||||||
static int64_t key(int x, int y, int z) { return (static_cast<int64_t>(x & 0x1FFFFF) << 42) | (static_cast<int64_t>(y & 0x1FFFFF) << 21) | (z & 0x1FFFFF); }
|
static int64_t key(int x, int y, int z) { return (static_cast<int64_t>(x & 0x1FFFFF) << 42) | (static_cast<int64_t>(y & 0x1FFFFF) << 21) | (z & 0x1FFFFF); }
|
||||||
// n: the surface's normal on the side being approached. Facing up (for this up) to stand on it: the underside
|
// n: the surface's normal on the side being approached. Facing up (for this up) to stand on it: the underside
|
||||||
// of a ceiling or a beam is never floor.
|
// of a ceiling or a beam is never floor.
|
||||||
@@ -381,7 +385,7 @@ public:
|
|||||||
}
|
}
|
||||||
origin_ = origin_ + d;
|
origin_ = origin_ + d;
|
||||||
hw = hw + d;
|
hw = hw + d;
|
||||||
walkV_ = d * (1 / dt); // what a wall left of it: no pent-up push
|
if (dt > 0) walkV_ = d * (1 / dt); // what a wall left of it: no pent-up push (a 0 s frame: NaN)
|
||||||
}
|
}
|
||||||
// Stand on the ground, or fall. The ground is looked for from knee height (a step), not from the head: a
|
// Stand on the ground, or fall. The ground is looked for from knee height (a step), not from the head: a
|
||||||
// head inside a cabinet or under a beam would otherwise find that and pop you up onto it.
|
// head inside a cabinet or under a beam would otherwise find that and pop you up onto it.
|
||||||
|
|||||||
@@ -19,9 +19,11 @@
|
|||||||
// "title": "Rainy Haven", "image": "haven.apk.image-1a2b3c4d.jpg" (the image beside this file, like the sound)
|
// "title": "Rainy Haven", "image": "haven.apk.image-1a2b3c4d.jpg" (the image beside this file, like the sound)
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -178,14 +180,25 @@ inline bool saveHomeEdits(const std::string& path, const HomeEdits& e) {
|
|||||||
if (!e.title.empty()) o << ",\n \"title\": " << quote(e.title);
|
if (!e.title.empty()) o << ",\n \"title\": " << quote(e.title);
|
||||||
if (!e.image.empty()) o << ",\n \"image\": " << quote(e.image);
|
if (!e.image.empty()) o << ",\n \"image\": " << quote(e.image);
|
||||||
o << "\n}\n";
|
o << "\n}\n";
|
||||||
std::ofstream f(path, std::ios::binary);
|
// By its UTF-8 name (a narrow path is the ANSI code page on Windows: C:\Users\José\... wasn't found), written
|
||||||
if (!f) return false;
|
// beside it and renamed over it once whole: a crash or a full disk midway never leaves a cut-off edits file.
|
||||||
f << o.str();
|
namespace fs = std::filesystem;
|
||||||
return static_cast<bool>(f);
|
const fs::path target = fs::u8path(path), part = fs::u8path(path + ".part");
|
||||||
|
{
|
||||||
|
std::ofstream f(part, std::ios::binary | std::ios::trunc);
|
||||||
|
if (!f) return false;
|
||||||
|
f << o.str();
|
||||||
|
f.close();
|
||||||
|
if (!f) { std::error_code ec; fs::remove(part, ec); return false; }
|
||||||
|
}
|
||||||
|
std::error_code ec;
|
||||||
|
fs::rename(part, target, ec);
|
||||||
|
if (ec) { fs::remove(part, ec); return false; }
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool loadHomeEdits(const std::string& path, HomeEdits& out) {
|
inline bool loadHomeEdits(const std::string& path, HomeEdits& out) {
|
||||||
std::ifstream f(path, std::ios::binary);
|
std::ifstream f(std::filesystem::u8path(path), std::ios::binary);
|
||||||
if (!f) return false;
|
if (!f) return false;
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
ss << f.rdbuf();
|
ss << f.rdbuf();
|
||||||
@@ -213,7 +226,8 @@ inline bool loadHomeEdits(const std::string& path, HomeEdits& out) {
|
|||||||
std::string k;
|
std::string k;
|
||||||
if (!r.str(k) || !r.eat(':')) return false;
|
if (!r.str(k) || !r.eat(':')) return false;
|
||||||
double d;
|
double d;
|
||||||
bool ok = k == "index" ? (r.num(d) && (x.index = static_cast<int>(d), true))
|
// (an index past int's range, or NaN, is no mesh: -1, never an out-of-range cast)
|
||||||
|
bool ok = k == "index" ? (r.num(d) && (x.index = d >= 0 && d < 2147483647.0 ? static_cast<int>(d) : -1, true))
|
||||||
: k == "name" ? r.str(x.name)
|
: k == "name" ? r.str(x.name)
|
||||||
: k == "deleted" ? r.boolean(x.deleted)
|
: k == "deleted" ? r.boolean(x.deleted)
|
||||||
: k == "pivot" ? r.floats(x.pivot, 16)
|
: k == "pivot" ? r.floats(x.pivot, 16)
|
||||||
@@ -234,8 +248,9 @@ inline bool loadHomeEdits(const std::string& path, HomeEdits& out) {
|
|||||||
double d = 0;
|
double d = 0;
|
||||||
const bool ok = k == "file" ? r.str(out.sound.file)
|
const bool ok = k == "file" ? r.str(out.sound.file)
|
||||||
: k == "name" ? r.str(out.sound.name)
|
: k == "name" ? r.str(out.sound.name)
|
||||||
: k == "volume" ? (r.num(d) && (out.sound.volume = static_cast<float>(std::clamp(d, 0.0, 1.0)), true))
|
// (strtod reads "nan", which clamp lets through to the audio gain: a NaN keeps the default)
|
||||||
: k == "homeVolume" ? (r.num(d) && (out.sound.homeVolume = static_cast<float>(std::clamp(d, 0.0, 1.0)), true))
|
: k == "volume" ? (r.num(d) && (std::isnan(d) || (out.sound.volume = static_cast<float>(std::clamp(d, 0.0, 1.0)), true)))
|
||||||
|
: k == "homeVolume" ? (r.num(d) && (std::isnan(d) || (out.sound.homeVolume = static_cast<float>(std::clamp(d, 0.0, 1.0)), true)))
|
||||||
: r.skip();
|
: r.skip();
|
||||||
if (!ok) return false;
|
if (!ok) return false;
|
||||||
} while (r.eat(','));
|
} while (r.eat(','));
|
||||||
|
|||||||
@@ -257,6 +257,8 @@ inline int run(const std::vector<std::string>& args, int nice, bool wait) {
|
|||||||
argv.push_back(nullptr);
|
argv.push_back(nullptr);
|
||||||
const pid_t pid = fork();
|
const pid_t pid = fork();
|
||||||
if (pid == 0) {
|
if (pid == 0) {
|
||||||
|
// Not waited for: started from a child that exits at once, so init reaps it (no zombie left behind)
|
||||||
|
if (!wait && fork() != 0) _exit(0);
|
||||||
if (nice > 0) setpriority(PRIO_PROCESS, 0, nice);
|
if (nice > 0) setpriority(PRIO_PROCESS, 0, nice);
|
||||||
const int null = ::open("/dev/null", O_RDWR);
|
const int null = ::open("/dev/null", O_RDWR);
|
||||||
if (null >= 0) { dup2(null, 1); dup2(null, 2); }
|
if (null >= 0) { dup2(null, 1); dup2(null, 2); }
|
||||||
@@ -264,8 +266,8 @@ inline int run(const std::vector<std::string>& args, int nice, bool wait) {
|
|||||||
_exit(127);
|
_exit(127);
|
||||||
}
|
}
|
||||||
if (pid < 0) return -1;
|
if (pid < 0) return -1;
|
||||||
if (!wait) return 0;
|
|
||||||
int st = 0;
|
int st = 0;
|
||||||
|
if (!wait) { waitpid(pid, &st, 0); return 0; } // (the go-between, gone straight away)
|
||||||
return waitpid(pid, &st, 0) == pid && WIFEXITED(st) ? WEXITSTATUS(st) : -1;
|
return waitpid(pid, &st, 0) == pid && WIFEXITED(st) ? WEXITSTATUS(st) : -1;
|
||||||
}
|
}
|
||||||
// This program again with the same arguments, in this process (SteamVR knows it by its path: still the environment).
|
// This program again with the same arguments, in this process (SteamVR knows it by its path: still the environment).
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
#include "core/parallel.h" // par::forEach: animated meshes rebuilt across cores
|
#include "core/parallel.h" // par::forEach: animated meshes rebuilt across cores
|
||||||
#include "fov_spirv.h" // the smooth-foveation pass
|
#include "fov_spirv.h" // the smooth-foveation pass
|
||||||
#include "shader_bake.h" // prepared and specialized shaders (made at bake, or here for APK homes)
|
#include "shader_bake.h" // prepared and specialized shaders (made at bake, or here for APK homes)
|
||||||
|
#include "spv_opt.h" // the SPIR-V optimizer over every program's shaders
|
||||||
|
#include "spv_check.h" // SPIR-V from a .fenv checked before the driver sees it
|
||||||
#include "tex_cache.h" // mip chains for textures shipped without one, built once
|
#include "tex_cache.h" // mip chains for textures shipped without one, built once
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
@@ -26,6 +28,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <tuple>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#define LOG(...) do { std::fprintf(stderr, "[render] " __VA_ARGS__); std::fputc('\n', stderr); } while (0)
|
#define LOG(...) do { std::fprintf(stderr, "[render] " __VA_ARGS__); std::fputc('\n', stderr); } while (0)
|
||||||
@@ -33,6 +36,15 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// SPIR-V a home brought (its .fenv, or made from it) fit to hand the driver: spv_check.h. A malformed module is said
|
||||||
|
// and its meshes skipped, as a module the driver refuses is.
|
||||||
|
bool shaderOk(const std::vector<uint32_t>& code, const std::string& program) {
|
||||||
|
std::string why;
|
||||||
|
if (spvcheck::check(code, &why)) return true;
|
||||||
|
LOG("shader of %s is malformed SPIR-V (%s): not handed to the driver", program.c_str(), why.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
std::string lower(std::string s) {
|
std::string lower(std::string s) {
|
||||||
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
return s;
|
return s;
|
||||||
@@ -387,6 +399,21 @@ static void imageBarrier(VkCommandBuffer c, VkImage img, VkImageAspectFlags aspe
|
|||||||
vkCmdPipelineBarrier(c, srcS, dstS, 0, 0, nullptr, 0, nullptr, 1, &b);
|
vkCmdPipelineBarrier(c, srcS, dstS, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A queue family ownership transfer of a frame shared with the compositor (all its layers, GENERAL throughout): the
|
||||||
|
// release half (from our family to VK_QUEUE_FAMILY_EXTERNAL) or the acquire half (the reverse).
|
||||||
|
static void ownershipBarrier(VkCommandBuffer c, VkImage img, uint32_t layers, uint32_t fromFamily, uint32_t toFamily,
|
||||||
|
VkAccessFlags srcA, VkAccessFlags dstA, VkPipelineStageFlags srcS, VkPipelineStageFlags dstS) {
|
||||||
|
VkImageMemoryBarrier b = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||||
|
b.oldLayout = b.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||||
|
b.srcAccessMask = srcA;
|
||||||
|
b.dstAccessMask = dstA;
|
||||||
|
b.srcQueueFamilyIndex = fromFamily;
|
||||||
|
b.dstQueueFamilyIndex = toFamily;
|
||||||
|
b.image = img;
|
||||||
|
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layers};
|
||||||
|
vkCmdPipelineBarrier(c, srcS, dstS, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||||
|
}
|
||||||
|
|
||||||
HslRenderer::Image HslRenderer::createImage2D(uint32_t w, uint32_t h, uint32_t layers, uint32_t mips,
|
HslRenderer::Image HslRenderer::createImage2D(uint32_t w, uint32_t h, uint32_t layers, uint32_t mips,
|
||||||
VkSampleCountFlagBits samples, VkFormat fmt, VkImageUsageFlags usage,
|
VkSampleCountFlagBits samples, VkFormat fmt, VkImageUsageFlags usage,
|
||||||
VkImageAspectFlags aspect, VkImageViewType viewType,
|
VkImageAspectFlags aspect, VkImageViewType viewType,
|
||||||
@@ -938,7 +965,7 @@ bool HslRenderer::init(uint32_t eyeWidth, uint32_t eyeHeight) {
|
|||||||
{ // Pipeline cache: compiled shader pipelines persist across runs.
|
{ // Pipeline cache: compiled shader pipelines persist across runs.
|
||||||
std::vector<char> data;
|
std::vector<char> data;
|
||||||
if (!pipelineCachePath.empty()) {
|
if (!pipelineCachePath.empty()) {
|
||||||
if (FILE* f = std::fopen(pipelineCachePath.c_str(), "rb")) {
|
if (FILE* f = texcache::open(std::filesystem::u8path(pipelineCachePath), false)) { // (UTF-8: wide on Windows)
|
||||||
std::fseek(f, 0, SEEK_END);
|
std::fseek(f, 0, SEEK_END);
|
||||||
const long n = std::ftell(f);
|
const long n = std::ftell(f);
|
||||||
std::fseek(f, 0, SEEK_SET);
|
std::fseek(f, 0, SEEK_SET);
|
||||||
@@ -959,6 +986,7 @@ bool HslRenderer::init(uint32_t eyeWidth, uint32_t eyeHeight) {
|
|||||||
createTargets();
|
createTargets();
|
||||||
createSharedResources();
|
createSharedResources();
|
||||||
createGuidePipelines();
|
createGuidePipelines();
|
||||||
|
createSplatPipeline();
|
||||||
ring_ = createBuffer(kRingSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true);
|
ring_ = createBuffer(kRingSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true);
|
||||||
// Per-draw GPU timestamps: a slow frame names the objects that cost it.
|
// Per-draw GPU timestamps: a slow frame names the objects that cost it.
|
||||||
if (props.limits.timestampComputeAndGraphics && props.limits.timestampPeriod > 0 && !std::getenv("FH_NO_PROF")) {
|
if (props.limits.timestampComputeAndGraphics && props.limits.timestampPeriod > 0 && !std::getenv("FH_NO_PROF")) {
|
||||||
@@ -1057,11 +1085,13 @@ void HslRenderer::createTargets() {
|
|||||||
dep.srcSubpass = 0;
|
dep.srcSubpass = 0;
|
||||||
dep.dstSubpass = VK_SUBPASS_EXTERNAL;
|
dep.dstSubpass = VK_SUBPASS_EXTERNAL;
|
||||||
dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||||
dep.dstStageMask = external ? VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT : VK_PIPELINE_STAGE_TRANSFER_BIT;
|
// One dependency for every kind (copied out, read by the smoothing pass, or the compositor's): render passes are
|
||||||
if (finalColor == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) dep.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
// compatible only when their dependencies are identical too, and the same pipelines draw in all of them
|
||||||
|
// (different ones were VUID-vkCmdDrawIndexed-renderPass-02684 on every export-pass draw).
|
||||||
|
(void)external;
|
||||||
|
dep.dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
|
||||||
dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||||
dep.dstAccessMask = external ? VK_ACCESS_MEMORY_READ_BIT : VK_ACCESS_TRANSFER_READ_BIT;
|
dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_MEMORY_READ_BIT;
|
||||||
if (finalColor == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) dep.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
||||||
const uint32_t viewMask = (1u << viewCount) - 1, correlation = viewMask; // both eyes in one pass (VR), or one view (editor)
|
const uint32_t viewMask = (1u << viewCount) - 1, correlation = viewMask; // both eyes in one pass (VR), or one view (editor)
|
||||||
VkRenderPassMultiviewCreateInfo mv = {VK_STRUCTURE_TYPE_RENDER_PASS_MULTIVIEW_CREATE_INFO};
|
VkRenderPassMultiviewCreateInfo mv = {VK_STRUCTURE_TYPE_RENDER_PASS_MULTIVIEW_CREATE_INFO};
|
||||||
mv.subpassCount = 1;
|
mv.subpassCount = 1;
|
||||||
@@ -1195,18 +1225,20 @@ VkRenderPass HslRenderer::colorOnlyPass(VkImageLayout finalLayout) const {
|
|||||||
sub.colorAttachmentCount = 1;
|
sub.colorAttachmentCount = 1;
|
||||||
sub.pColorAttachments = &cref;
|
sub.pColorAttachments = &cref;
|
||||||
VkSubpassDependency deps[2] = {};
|
VkSubpassDependency deps[2] = {};
|
||||||
deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; // the foveated pass's writes before this one reads them
|
deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; // the foveated pass's writes before this one reads them, and the
|
||||||
deps[0].dstSubpass = 0;
|
deps[0].dstSubpass = 0; // target's layout transition before this pass writes it
|
||||||
deps[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
deps[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||||
deps[0].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
deps[0].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||||
deps[0].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
deps[0].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||||
deps[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
deps[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||||
deps[1].srcSubpass = 0;
|
deps[1].srcSubpass = 0;
|
||||||
deps[1].dstSubpass = VK_SUBPASS_EXTERNAL;
|
deps[1].dstSubpass = VK_SUBPASS_EXTERNAL;
|
||||||
deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||||
deps[1].dstStageMask = finalLayout == VK_IMAGE_LAYOUT_GENERAL ? VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT : VK_PIPELINE_STAGE_TRANSFER_BIT;
|
// (the same whichever the final layout: passes differing in a dependency aren't compatible, and one pipeline draws
|
||||||
|
// in both kinds)
|
||||||
|
deps[1].dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
|
||||||
deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||||
deps[1].dstAccessMask = finalLayout == VK_IMAGE_LAYOUT_GENERAL ? VK_ACCESS_MEMORY_READ_BIT : VK_ACCESS_TRANSFER_READ_BIT;
|
deps[1].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_MEMORY_READ_BIT;
|
||||||
const uint32_t viewMask = (1u << viewCount) - 1;
|
const uint32_t viewMask = (1u << viewCount) - 1;
|
||||||
VkRenderPassMultiviewCreateInfo mv = {VK_STRUCTURE_TYPE_RENDER_PASS_MULTIVIEW_CREATE_INFO};
|
VkRenderPassMultiviewCreateInfo mv = {VK_STRUCTURE_TYPE_RENDER_PASS_MULTIVIEW_CREATE_INFO};
|
||||||
mv.subpassCount = 1;
|
mv.subpassCount = 1;
|
||||||
@@ -1534,20 +1566,23 @@ int HslRenderer::programFor(const ShaderProgramSrc& src) {
|
|||||||
plci.pPushConstantRanges = &pcr;
|
plci.pPushConstantRanges = &pcr;
|
||||||
vkCreatePipelineLayout(device_, &plci, nullptr, &p.layout);
|
vkCreatePipelineLayout(device_, &plci, nullptr, &p.layout);
|
||||||
|
|
||||||
auto module = [&](const std::vector<u32>& code) {
|
// (through the SPIR-V optimizer first: spv_opt.h; a module a .fenv carries made is used as it is)
|
||||||
|
auto module = [&](const std::vector<u32>& shipped) {
|
||||||
|
const std::vector<u32> code = spvopt::run(shipped, p.path);
|
||||||
VkShaderModuleCreateInfo ci = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
VkShaderModuleCreateInfo ci = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||||
ci.codeSize = code.size() * 4;
|
ci.codeSize = code.size() * 4;
|
||||||
ci.pCode = code.data();
|
ci.pCode = code.data();
|
||||||
VkShaderModule sm = VK_NULL_HANDLE;
|
VkShaderModule sm = VK_NULL_HANDLE;
|
||||||
if (code.empty() || vkCreateShaderModule(device_, &ci, nullptr, &sm) != VK_SUCCESS) sm = VK_NULL_HANDLE;
|
// (a .fenv's SPIR-V is checked before the driver sees it: spv_check.h)
|
||||||
|
if (code.empty() || !shaderOk(code, p.path) || vkCreateShaderModule(device_, &ci, nullptr, &sm) != VK_SUCCESS) sm = VK_NULL_HANDLE;
|
||||||
return sm;
|
return sm;
|
||||||
};
|
};
|
||||||
// (prepared by the bake for a .fenv: shader_bake.h)
|
// (prepared by the bake for a .fenv: shader_bake.h)
|
||||||
p.vs = module(src.prepared ? src.vertInvariant : shaderbake::invariantPosition(src.vert));
|
p.vs = module(src.prepared && shaderbake::bakedUsable(src.vertInvariant) ? src.vertInvariant : shaderbake::invariantPosition(src.vert));
|
||||||
p.fs = module(src.frag);
|
p.fs = module(src.frag);
|
||||||
p.src = &src;
|
p.src = &src;
|
||||||
p.foldBlocks = spvspec::blocksRead(src.frag);
|
p.foldBlocks = spvspec::blocksRead(src.frag);
|
||||||
const std::vector<u32> alpha = src.prepared ? src.fragAlpha : shaderbake::alphaOnly(src.frag);
|
const std::vector<u32> alpha = src.prepared && shaderbake::bakedUsable(src.fragAlpha) ? src.fragAlpha : shaderbake::alphaOnly(src.frag);
|
||||||
if (!alpha.empty()) p.fsAlpha = module(alpha);
|
if (!alpha.empty()) p.fsAlpha = module(alpha);
|
||||||
if (!p.vs || !p.fs) { // SPIR-V the driver rejects: this program's meshes are skipped
|
if (!p.vs || !p.fs) { // SPIR-V the driver rejects: this program's meshes are skipped
|
||||||
LOG("shader module failed: %s", src.path.c_str());
|
LOG("shader module failed: %s", src.path.c_str());
|
||||||
@@ -1688,6 +1723,16 @@ VkPipeline HslRenderer::pipelineFor(Program& p, int blend, bool cull, bool sky,
|
|||||||
dsi.depthTestEnable = sky && skyFirst() ? VK_FALSE : VK_TRUE;
|
dsi.depthTestEnable = sky && skyFirst() ? VK_FALSE : VK_TRUE;
|
||||||
dsi.depthWriteEnable = blend == 0 && !sky && cut != kCutShade ? VK_TRUE : VK_FALSE;
|
dsi.depthWriteEnable = blend == 0 && !sky && cut != kCutShade ? VK_TRUE : VK_FALSE;
|
||||||
dsi.depthCompareOp = VK_COMPARE_OP_GREATER_OR_EQUAL; // reversed-Z: clear 0, near -> 1
|
dsi.depthCompareOp = VK_COMPARE_OP_GREATER_OR_EQUAL; // reversed-Z: clear 0, near -> 1
|
||||||
|
// A cut-out's shading pass: the samples whose depth is its own (the depth pass wrote it there through the same
|
||||||
|
// alpha-to-coverage mask, and nothing nearer covered it) are exactly the ones alpha-to-coverage with GREATER_OR_EQUAL
|
||||||
|
// let through, so EQUAL alone picks them: the same picture, and without alpha-to-coverage the GPU tests depth
|
||||||
|
// before shading (Turnip ran it after: every frond layer was shaded). Desert Terrace 2-4% less GPU time.
|
||||||
|
// FH_CUT_GE=1: the old way (A/B).
|
||||||
|
static const bool cutEqual = std::getenv("FH_CUT_GE") == nullptr;
|
||||||
|
if (cutEqual && cut == kCutShade && msi.alphaToCoverageEnable) {
|
||||||
|
msi.alphaToCoverageEnable = VK_FALSE;
|
||||||
|
dsi.depthCompareOp = VK_COMPARE_OP_EQUAL;
|
||||||
|
}
|
||||||
VkPipelineColorBlendAttachmentState cba = {};
|
VkPipelineColorBlendAttachmentState cba = {};
|
||||||
cba.colorWriteMask = cut == kCutDepth ? 0 : 0xF;
|
cba.colorWriteMask = cut == kCutDepth ? 0 : 0xF;
|
||||||
if (blend != 0) {
|
if (blend != 0) {
|
||||||
@@ -2351,23 +2396,32 @@ void HslRenderer::setSelection(int mesh) {
|
|||||||
if (!nv || md.indices.size() < 3) return;
|
if (!nv || md.indices.size() < 3) return;
|
||||||
std::vector<float> v(nv * 5, 0.0f);
|
std::vector<float> v(nv * 5, 0.0f);
|
||||||
for (size_t i = 0; i < nv; ++i) std::memcpy(&v[i * 5], &md.positions[i * 3], 12);
|
for (size_t i = 0; i < nv; ++i) std::memcpy(&v[i * 5], &md.positions[i * 3], 12);
|
||||||
// Unique edges (shared by two triangles: drawn once).
|
// Unique edges (shared by two triangles: drawn once). Sorted keys, not a hash set: a few times faster, and
|
||||||
|
// selecting a big mesh in Studio held up a frame for it.
|
||||||
std::vector<u32> edges, tris;
|
std::vector<u32> edges, tris;
|
||||||
std::unordered_set<uint64_t> seen;
|
std::vector<uint64_t> keys;
|
||||||
seen.reserve(md.indices.size());
|
keys.reserve(md.indices.size());
|
||||||
|
tris.reserve(md.indices.size());
|
||||||
for (size_t t = 0; t + 2 < md.indices.size(); t += 3) {
|
for (size_t t = 0; t + 2 < md.indices.size(); t += 3) {
|
||||||
const u32 idx[3] = {md.indices[t], md.indices[t + 1], md.indices[t + 2]};
|
const u32 idx[3] = {md.indices[t], md.indices[t + 1], md.indices[t + 2]};
|
||||||
if (idx[0] >= nv || idx[1] >= nv || idx[2] >= nv) continue;
|
if (idx[0] >= nv || idx[1] >= nv || idx[2] >= nv) continue;
|
||||||
tris.insert(tris.end(), idx, idx + 3);
|
tris.insert(tris.end(), idx, idx + 3);
|
||||||
for (int e = 0; e < 3; ++e) {
|
for (int e = 0; e < 3; ++e) {
|
||||||
const u32 a = std::min(idx[e], idx[(e + 1) % 3]), b = std::max(idx[e], idx[(e + 1) % 3]);
|
const u32 a = std::min(idx[e], idx[(e + 1) % 3]), b = std::max(idx[e], idx[(e + 1) % 3]);
|
||||||
if (seen.insert(static_cast<uint64_t>(a) << 32 | b).second) { edges.push_back(a); edges.push_back(b); }
|
keys.push_back(static_cast<uint64_t>(a) << 32 | b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tris.empty()) return;
|
if (tris.empty()) return;
|
||||||
|
std::sort(keys.begin(), keys.end());
|
||||||
|
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
||||||
|
edges.reserve(keys.size() * 2);
|
||||||
|
for (uint64_t k : keys) { edges.push_back(static_cast<u32>(k >> 32)); edges.push_back(static_cast<u32>(k)); }
|
||||||
|
const bool batch = !loadCmd_ && !recordInto_; // the three uploads in one submit (one wait, not three)
|
||||||
|
if (batch) beginUploadBatch();
|
||||||
sel_.vbo = createDeviceBuffer(v.data(), v.size() * 4, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
sel_.vbo = createDeviceBuffer(v.data(), v.size() * 4, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
||||||
sel_.edges = createDeviceBuffer(edges.data(), edges.size() * 4, VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
|
sel_.edges = createDeviceBuffer(edges.data(), edges.size() * 4, VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
|
||||||
sel_.tris = createDeviceBuffer(tris.data(), tris.size() * 4, VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
|
sel_.tris = createDeviceBuffer(tris.data(), tris.size() * 4, VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
|
||||||
|
if (batch) flushUploadBatch();
|
||||||
if (!sel_.vbo.buf || !sel_.edges.buf || !sel_.tris.buf) { destroy(sel_.vbo); destroy(sel_.edges); destroy(sel_.tris); return; }
|
if (!sel_.vbo.buf || !sel_.edges.buf || !sel_.tris.buf) { destroy(sel_.vbo); destroy(sel_.edges); destroy(sel_.tris); return; }
|
||||||
sel_.edgeCount = static_cast<uint32_t>(edges.size());
|
sel_.edgeCount = static_cast<uint32_t>(edges.size());
|
||||||
sel_.triCount = static_cast<uint32_t>(tris.size());
|
sel_.triCount = static_cast<uint32_t>(tris.size());
|
||||||
@@ -2442,7 +2496,8 @@ void HslRenderer::setStaticGuide(const std::vector<GuideVertex>& tris) {
|
|||||||
if (!device_) return;
|
if (!device_) return;
|
||||||
if (guide_.staticVbo.buf) { vkDeviceWaitIdle(device_); destroy(guide_.staticVbo); }
|
if (guide_.staticVbo.buf) { vkDeviceWaitIdle(device_); destroy(guide_.staticVbo); }
|
||||||
guide_.staticVertexCount = 0;
|
guide_.staticVertexCount = 0;
|
||||||
if (tris.empty() || !guide_.visible) return;
|
static const bool noGuide = std::getenv("FH_NO_GUIDE") != nullptr; // diagnostics: the home alone (no walk-area overlay)
|
||||||
|
if (tris.empty() || !guide_.visible || noGuide) return;
|
||||||
guide_.staticVbo = createDeviceBuffer(tris.data(), tris.size() * sizeof(GuideVertex), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
guide_.staticVbo = createDeviceBuffer(tris.data(), tris.size() * sizeof(GuideVertex), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
||||||
if (guide_.staticVbo.buf) guide_.staticVertexCount = static_cast<uint32_t>(tris.size());
|
if (guide_.staticVbo.buf) guide_.staticVertexCount = static_cast<uint32_t>(tris.size());
|
||||||
}
|
}
|
||||||
@@ -3042,6 +3097,7 @@ bool HslRenderer::uploadMesh(const MeshData& md, int sceneIndex) {
|
|||||||
// objects came into view (the pipeline cache makes it near-instant on later opens).
|
// objects came into view (the pipeline cache makes it near-instant on later opens).
|
||||||
measureMesh(gm, md, nv);
|
measureMesh(gm, md, nv);
|
||||||
std::map<uint32_t, std::vector<u8>> set2Bytes; // its constant uniform buffers, for variantFor
|
std::map<uint32_t, std::vector<u8>> set2Bytes; // its constant uniform buffers, for variantFor
|
||||||
|
std::vector<uint64_t> lookSig; // what its material buffers and textures are (duplicate check)
|
||||||
|
|
||||||
// set2: per-material resources, routed by name.
|
// set2: per-material resources, routed by name.
|
||||||
VkDescriptorSetAllocateInfo ai = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
VkDescriptorSetAllocateInfo ai = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||||
@@ -3099,6 +3155,12 @@ bool HslRenderer::uploadMesh(const MeshData& md, int sceneIndex) {
|
|||||||
gm.ubos.push_back(b);
|
gm.ubos.push_back(b);
|
||||||
if (!skinning && r.kind == spv::ResKind::UniformBuffer && r.size)
|
if (!skinning && r.kind == spv::ResKind::UniformBuffer && r.size)
|
||||||
set2Bytes[r.binding].assign(static_cast<const u8*>(b.mapped), static_cast<const u8*>(b.mapped) + r.size);
|
set2Bytes[r.binding].assign(static_cast<const u8*>(b.mapped), static_cast<const u8*>(b.mapped) + r.size);
|
||||||
|
if (!skinning) {
|
||||||
|
uint64_t h = 0xcbf29ce484222325ull ^ r.binding;
|
||||||
|
const u8* bytes = static_cast<const u8*>(b.mapped);
|
||||||
|
for (size_t k = 0, n = std::min<size_t>(r.size, static_cast<size_t>(b.size)); k < n; ++k) h = (h ^ bytes[k]) * 0x100000001B3ull;
|
||||||
|
lookSig.push_back(h);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
VkDescriptorImageInfo ii = {};
|
VkDescriptorImageInfo ii = {};
|
||||||
ii.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
ii.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||||
@@ -3107,7 +3169,10 @@ bool HslRenderer::uploadMesh(const MeshData& md, int sceneIndex) {
|
|||||||
if (r.kind != spv::ResKind::Sampler) {
|
if (r.kind != spv::ResKind::Sampler) {
|
||||||
slotPending_ = false;
|
slotPending_ = false;
|
||||||
ii.imageView = textureFor(r, gm, md);
|
ii.imageView = textureFor(r, gm, md);
|
||||||
|
// (a placeholder stands for the texture it waits for: two meshes are alike when they wait for the same one)
|
||||||
|
lookSig.push_back(reinterpret_cast<uintptr_t>(ii.imageView) ^ (static_cast<uint64_t>(r.binding) << 56));
|
||||||
if (slotPending_) { // a placeholder for now: patch this binding when it lands
|
if (slotPending_) { // a placeholder for now: patch this binding when it lands
|
||||||
|
lookSig.push_back(reinterpret_cast<uintptr_t>(pendingKey_.first) * 2 + (pendingKey_.second ? 1 : 0));
|
||||||
waiting_[pendingKey_].push_back({meshes_.size(), r.binding, r.count, descriptorType(r.kind), ii.sampler});
|
waiting_[pendingKey_].push_back({meshes_.size(), r.binding, r.count, descriptorType(r.kind), ii.sampler});
|
||||||
slotPending_ = false;
|
slotPending_ = false;
|
||||||
}
|
}
|
||||||
@@ -3128,7 +3193,56 @@ bool HslRenderer::uploadMesh(const MeshData& md, int sceneIndex) {
|
|||||||
// objects came into view (the pipeline cache makes it near-instant on later opens).
|
// objects came into view (the pipeline cache makes it near-instant on later opens).
|
||||||
const auto tp0 = std::chrono::steady_clock::now();
|
const auto tp0 = std::chrono::steady_clock::now();
|
||||||
pipelineFor(p, gm.blend, gm.cull, gm.skybox, gm.alphaToCoverage, kCutNone, gm.variant);
|
pipelineFor(p, gm.blend, gm.cull, gm.skybox, gm.alphaToCoverage, kCutNone, gm.variant);
|
||||||
|
if (gm.alphaToCoverage && gm.blend == 0 && (gm.variant >= 0 ? p.variants[gm.variant].fsAlpha : p.fsAlpha)) {
|
||||||
|
// ...and a cut-out's two passes (render(): its depth from the alpha-only shader, then shaded): made on first
|
||||||
|
// sight, they were a compile in the middle of a frame the first time a fern came into view.
|
||||||
|
pipelineFor(p, gm.blend, gm.cull, gm.skybox, gm.alphaToCoverage, kCutDepth, gm.variant);
|
||||||
|
pipelineFor(p, gm.blend, gm.cull, gm.skybox, gm.alphaToCoverage, kCutShade, gm.variant);
|
||||||
|
}
|
||||||
g_loadPipeMs = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - tp0).count();
|
g_loadPipeMs = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - tp0).count();
|
||||||
|
// An exact copy of a solid mesh already in (the same vertices in the same place, the same shader, material
|
||||||
|
// values and textures): some APKs ship a part twice (the hand tutorials' chapters and "archive" copies), and the
|
||||||
|
// copy, at the same depth, shaded every one of its pixels a second time to the same colour. It stays loaded (a
|
||||||
|
// home edit can hide or move the first one) but isn't drawn while the first shows it (render()).
|
||||||
|
static const bool noDedupe = std::getenv("FH_NO_DEDUPE") != nullptr; // A/B: every copy drawn
|
||||||
|
if (!noDedupe && !gm.dynamic && !gm.skybox && gm.blend == 0 && gm.skinUbo < 0 && !md.hasVat) {
|
||||||
|
uint64_t h = 0xcbf29ce484222325ull;
|
||||||
|
auto mix = [&h](const void* p, size_t n) {
|
||||||
|
const u8* b = static_cast<const u8*>(p);
|
||||||
|
for (size_t k = 0; k < n; ++k) h = (h ^ b[k]) * 0x100000001B3ull;
|
||||||
|
};
|
||||||
|
mix(vbo.data(), vbo.size());
|
||||||
|
mix(indices->data(), indices->size() * 4);
|
||||||
|
mix(gm.world, 64); mix(gm.color, 16); mix(gm.uvXform, sizeof gm.uvXform);
|
||||||
|
const int state[4] = {gm.program, gm.variant, gm.alphaToCoverage ? 1 : 0, gm.cull ? 1 : 0};
|
||||||
|
mix(state, sizeof state);
|
||||||
|
mix(lookSig.data(), lookSig.size() * 8);
|
||||||
|
const auto it = dedupe_.find(h);
|
||||||
|
if (it == dedupe_.end()) {
|
||||||
|
dedupe_[h] = meshes_.size();
|
||||||
|
} else { // (the hash matched: the geometry checked in full before trusting it)
|
||||||
|
const GpuMesh& o = meshes_[it->second];
|
||||||
|
const MeshData& om = scene_->meshes[o.sceneIndex];
|
||||||
|
if (o.program == gm.program && o.variant == gm.variant && o.indexCount == gm.indexCount && !std::memcmp(o.world, gm.world, 64) &&
|
||||||
|
om.positions == md.positions && om.uvs == md.uvs && om.indices == md.indices) {
|
||||||
|
gm.dupOf = static_cast<int>(it->second);
|
||||||
|
++loadDuplicates_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// (see-through ones too: the hand tutorials' thousand-odd plants are alpha-blended; FH_NO_BLEND_BATCH=1 for A/B)
|
||||||
|
static const bool noBlendBatch = std::getenv("FH_NO_BLEND_BATCH") != nullptr;
|
||||||
|
if (p.clay && !gm.dynamic && !gm.skybox && (gm.blend == 0 || !noBlendBatch) && gm.skinUbo < 0 && !md.hasVat && gm.dupOf < 0) { // (buildBatches)
|
||||||
|
uint64_t h = 0xcbf29ce484222325ull;
|
||||||
|
auto mix = [&h](const void* q, size_t n) {
|
||||||
|
const u8* b = static_cast<const u8*>(q);
|
||||||
|
for (size_t k = 0; k < n; ++k) h = (h ^ b[k]) * 0x100000001B3ull;
|
||||||
|
};
|
||||||
|
const int state[5] = {gm.program, gm.variant, gm.alphaToCoverage ? 1 : 0, gm.cull ? 1 : 0, gm.blend};
|
||||||
|
mix(state, sizeof state); mix(gm.color, 16); mix(gm.uvXform, sizeof gm.uvXform);
|
||||||
|
mix(lookSig.data(), lookSig.size() * 8);
|
||||||
|
gm.lookHash = h | 1;
|
||||||
|
}
|
||||||
sceneToMesh_[sceneIndex] = meshes_.size();
|
sceneToMesh_[sceneIndex] = meshes_.size();
|
||||||
meshes_.push_back(std::move(gm));
|
meshes_.push_back(std::move(gm));
|
||||||
return true;
|
return true;
|
||||||
@@ -3149,12 +3263,13 @@ int HslRenderer::variantFor(Program& p, const std::map<uint32_t, std::vector<u8>
|
|||||||
const spvspec::Fetch fetch = shaderbake::fetchFor(sceneConsts_, p.sets, set2);
|
const spvspec::Fetch fetch = shaderbake::fetchFor(sceneConsts_, p.sets, set2);
|
||||||
const uint64_t key = shaderbake::variantKey(p.foldBlocks, fetch, floats) ^ (limit >= 0 ? 0x9E3779B97F4A7C15ull * (limit + 1) : 0);
|
const uint64_t key = shaderbake::variantKey(p.foldBlocks, fetch, floats) ^ (limit >= 0 ? 0x9E3779B97F4A7C15ull * (limit + 1) : 0);
|
||||||
if (const auto it = p.variantFor.find(key); it != p.variantFor.end()) return it->second;
|
if (const auto it = p.variantFor.find(key); it != p.variantFor.end()) return it->second;
|
||||||
auto module = [&](const std::vector<u32>& c) {
|
auto module = [&](const std::vector<u32>& specialized) {
|
||||||
|
const std::vector<u32> c = spvopt::run(specialized, p.path); // (spv_opt.h)
|
||||||
VkShaderModuleCreateInfo ci = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
VkShaderModuleCreateInfo ci = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||||
ci.codeSize = c.size() * 4;
|
ci.codeSize = c.size() * 4;
|
||||||
ci.pCode = c.data();
|
ci.pCode = c.data();
|
||||||
VkShaderModule sm = VK_NULL_HANDLE;
|
VkShaderModule sm = VK_NULL_HANDLE;
|
||||||
if (c.empty() || vkCreateShaderModule(device_, &ci, nullptr, &sm) != VK_SUCCESS) sm = VK_NULL_HANDLE;
|
if (c.empty() || !shaderOk(c, p.path) || vkCreateShaderModule(device_, &ci, nullptr, &sm) != VK_SUCCESS) sm = VK_NULL_HANDLE;
|
||||||
return sm;
|
return sm;
|
||||||
};
|
};
|
||||||
std::vector<u32> made, madeAlpha;
|
std::vector<u32> made, madeAlpha;
|
||||||
@@ -3162,7 +3277,7 @@ int HslRenderer::variantFor(Program& p, const std::map<uint32_t, std::vector<u8>
|
|||||||
const std::vector<u32>* alpha = nullptr;
|
const std::vector<u32>* alpha = nullptr;
|
||||||
bool baked = false;
|
bool baked = false;
|
||||||
for (const auto& bv : p.src->variants)
|
for (const auto& bv : p.src->variants)
|
||||||
if (bv.key == key && limit < 0) { code = &bv.frag; alpha = &bv.fragAlpha; baked = true; break; }
|
if (bv.key == key && limit < 0 && shaderbake::bakedUsable(bv.frag)) { code = &bv.frag; alpha = &bv.fragAlpha; baked = true; break; }
|
||||||
uint32_t folded = 0;
|
uint32_t folded = 0;
|
||||||
if (!baked) {
|
if (!baked) {
|
||||||
made = spvspec::specialize(p.src->frag, fetch, &folded, limit, floats);
|
made = spvspec::specialize(p.src->frag, fetch, &folded, limit, floats);
|
||||||
@@ -3200,7 +3315,7 @@ int HslRenderer::variantFor(Program& p, const std::map<uint32_t, std::vector<u8>
|
|||||||
bool HslRenderer::load(const HslScene& scene) {
|
bool HslRenderer::load(const HslScene& scene) {
|
||||||
beginLoad(scene);
|
beginLoad(scene);
|
||||||
while (!loadStep(1e9)) {}
|
while (!loadStep(1e9)) {}
|
||||||
return !meshes_.empty();
|
return !meshes_.empty() || !splatObjs_.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void HslRenderer::beginLoad(const HslScene& scene) {
|
void HslRenderer::beginLoad(const HslScene& scene) {
|
||||||
@@ -3233,8 +3348,9 @@ void HslRenderer::beginLoad(const HslScene& scene) {
|
|||||||
const uint32_t size = sky->reflCubeSize, mips = sky->reflCubeMips;
|
const uint32_t size = sky->reflCubeSize, mips = sky->reflCubeMips;
|
||||||
const auto t0 = std::chrono::steady_clock::now();
|
const auto t0 = std::chrono::steady_clock::now();
|
||||||
// The shader picks a level by roughness over every level the cube has: Meta's cubes are cooked down to 16^2
|
// The shader picks a level by roughness over every level the cube has: Meta's cubes are cooked down to 16^2
|
||||||
// (256^2, 5 levels). One shipped with fewer (Cascadia's 1024^2 has mip 0 alone, every metal a sharp mirror)
|
// (256^2, 5 levels; a streamed one like Cascadia's has 1024^2 and 512^2 in its streams, which the loader puts
|
||||||
// gets the rest of that chain blitted.
|
// in front). One that still arrives with fewer (an environment baked before the loader did that: mip 0 alone,
|
||||||
|
// every floor a mirror) gets the rest of that chain blitted.
|
||||||
uint32_t levels = 1;
|
uint32_t levels = 1;
|
||||||
while ((size >> levels) >= 16) ++levels;
|
while ((size >> levels) >= 16) ++levels;
|
||||||
const TexSource* src = sky->reflCubeSrc.get();
|
const TexSource* src = sky->reflCubeSrc.get();
|
||||||
@@ -3284,6 +3400,8 @@ void HslRenderer::beginLoad(const HslScene& scene) {
|
|||||||
if (!sel_.lines) createSelectionPipelines(); // now, not on the first click (a 70 ms hitch)
|
if (!sel_.lines) createSelectionPipelines(); // now, not on the first click (a 70 ms hitch)
|
||||||
loadNext_ = 0;
|
loadNext_ = 0;
|
||||||
loadSkipped_ = 0;
|
loadSkipped_ = 0;
|
||||||
|
loadDuplicates_ = 0;
|
||||||
|
dedupe_.clear();
|
||||||
loadStart_ = std::chrono::steady_clock::now();
|
loadStart_ = std::chrono::steady_clock::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3308,16 +3426,45 @@ bool HslRenderer::loadStep(double budgetMs) {
|
|||||||
}
|
}
|
||||||
flushUploadBatch();
|
flushUploadBatch();
|
||||||
if (loadNext_ < scene_->meshes.size()) return false;
|
if (loadNext_ < scene_->meshes.size()) return false;
|
||||||
|
if (!splatsUploaded_) uploadSplats(); // (hsl_renderer_splat.cpp)
|
||||||
if (loadNext_ == scene_->meshes.size() && !loadDone_) {
|
if (loadNext_ == scene_->meshes.size() && !loadDone_) {
|
||||||
loadDone_ = true;
|
loadDone_ = true;
|
||||||
|
buildBatches();
|
||||||
size_t variants = 0;
|
size_t variants = 0;
|
||||||
for (const auto& p : programs_) variants += p.variants.size();
|
for (const auto& p : programs_) variants += p.variants.size();
|
||||||
LOG("uploaded %zu meshes (%zu skipped), %zu shader programs (%zu specialized: %zu baked, %zu made now) in %.1fs", meshes_.size(),
|
size_t batched = 0;
|
||||||
loadSkipped_, programs_.size(), variants, specBaked_, specMade_, std::chrono::duration<double>(std::chrono::steady_clock::now() - loadStart_).count());
|
for (const Batch& b : batches_) batched += b.members.size();
|
||||||
|
LOG("uploaded %zu meshes (%zu skipped, %zu exact copies of another not drawn, %zu drawn as %zu batches), %zu shader programs (%zu specialized: %zu baked, %zu made now) in %.1fs",
|
||||||
|
meshes_.size(), loadSkipped_, loadDuplicates_, batched, batches_.size(), programs_.size(), variants, specBaked_, specMade_, std::chrono::duration<double>(std::chrono::steady_clock::now() - loadStart_).count());
|
||||||
|
if (spvopt::flags()) {
|
||||||
|
const spvopt::Stats st = spvopt::stats();
|
||||||
|
LOG("spir-v optimizer (flags %d): %u modules made in %.0f ms, %u reused, %u failed", spvopt::flags(), st.made, st.ms, st.cached, st.failed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int HslRenderer::addMesh(int sceneIndex) {
|
||||||
|
if (!device_ || !scene_ || !loadDone_ || recordInto_ || sceneIndex < 0 || sceneIndex >= static_cast<int>(scene_->meshes.size())) return -1;
|
||||||
|
beginUploadBatch();
|
||||||
|
const bool ok = uploadMesh(scene_->meshes[sceneIndex], sceneIndex);
|
||||||
|
flushUploadBatch();
|
||||||
|
loadNext_ = scene_->meshes.size(); // (nothing more for loadStep to bring in)
|
||||||
|
if (!ok) return -1;
|
||||||
|
GpuMesh& gm = meshes_.back();
|
||||||
|
gm.batch = -1; // drawn alone (batches are made once, at load)
|
||||||
|
gm.dupOf = -1; // (a copy pasted over its original still draws once moved)
|
||||||
|
requestTextures(gm);
|
||||||
|
return static_cast<int>(meshes_.size()) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HslRenderer::reloadSplats() {
|
||||||
|
if (!device_ || !scene_) return;
|
||||||
|
vkDeviceWaitIdle(device_);
|
||||||
|
destroySplats();
|
||||||
|
uploadSplats();
|
||||||
|
}
|
||||||
|
|
||||||
float HslRenderer::loadProgress() const {
|
float HslRenderer::loadProgress() const {
|
||||||
if (!scene_ || scene_->meshes.empty()) return 1.0f;
|
if (!scene_ || scene_->meshes.empty()) return 1.0f;
|
||||||
return static_cast<float>(loadNext_) / static_cast<float>(scene_->meshes.size());
|
return static_cast<float>(loadNext_) / static_cast<float>(scene_->meshes.size());
|
||||||
@@ -3328,6 +3475,7 @@ void HslRenderer::unload() {
|
|||||||
vkDeviceWaitIdle(device_);
|
vkDeviceWaitIdle(device_);
|
||||||
for (Buffer& b : frameStaging_) destroy(b);
|
for (Buffer& b : frameStaging_) destroy(b);
|
||||||
frameStaging_.clear();
|
frameStaging_.clear();
|
||||||
|
destroySplats();
|
||||||
for (auto& m : meshes_) {
|
for (auto& m : meshes_) {
|
||||||
destroy(m.vbo);
|
destroy(m.vbo);
|
||||||
destroy(m.ibo);
|
destroy(m.ibo);
|
||||||
@@ -3339,6 +3487,12 @@ void HslRenderer::unload() {
|
|||||||
sel_.mesh = -1;
|
sel_.mesh = -1;
|
||||||
sel_.edgeCount = sel_.triCount = 0;
|
sel_.edgeCount = sel_.triCount = 0;
|
||||||
meshes_.clear();
|
meshes_.clear();
|
||||||
|
for (Batch& b : batches_) { // (its set2 is its first part's, freed above; a sorted one's ibo is one of its sortIbo)
|
||||||
|
destroy(b.gm.vbo);
|
||||||
|
if (b.sorted) { for (Buffer& sb : b.sortIbo) destroy(sb); } else destroy(b.gm.ibo);
|
||||||
|
}
|
||||||
|
batches_.clear();
|
||||||
|
dedupe_.clear();
|
||||||
cost_.clear();
|
cost_.clear();
|
||||||
costMin_.clear(); costSeen_.clear(); std::fill(std::begin(costBaseRing_), std::end(costBaseRing_), 1e9f);
|
costMin_.clear(); costSeen_.clear(); std::fill(std::begin(costBaseRing_), std::end(costBaseRing_), 1e9f);
|
||||||
doubled_ = -1;
|
doubled_ = -1;
|
||||||
@@ -3381,6 +3535,7 @@ void HslRenderer::unload() {
|
|||||||
|
|
||||||
void HslRenderer::setMeshWorld(size_t i, const float world[16]) {
|
void HslRenderer::setMeshWorld(size_t i, const float world[16]) {
|
||||||
GpuMesh& gm = meshes_[i];
|
GpuMesh& gm = meshes_[i];
|
||||||
|
if (gm.batch >= 0 && std::memcmp(gm.baseWorld, world, 64) != 0) batches_[gm.batch].active = false; // (drawn alone again)
|
||||||
std::memcpy(gm.baseWorld, world, 64);
|
std::memcpy(gm.baseWorld, world, 64);
|
||||||
placeMesh(gm);
|
placeMesh(gm);
|
||||||
}
|
}
|
||||||
@@ -3410,6 +3565,159 @@ void HslRenderer::placeMesh(GpuMesh& gm) {
|
|||||||
updateBounds(gm);
|
updateBounds(gm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Static batching (Batch, hsl_renderer.h): the solid Clay meshes uploadMesh gave a lookHash, grouped by that look and by
|
||||||
|
// the 4 m cell their centre is in, each group of two or more made one mesh: their vertices moved into the world (the
|
||||||
|
// shader then places them with an identity matrix), their indices one after another. Big meshes stay alone (one draw
|
||||||
|
// is nothing next to their pixels, and they'd be held twice in memory).
|
||||||
|
void HslRenderer::buildBatches() {
|
||||||
|
batches_.clear();
|
||||||
|
static const bool off = std::getenv("FH_NO_BATCH") != nullptr; // A/B: every mesh its own draw
|
||||||
|
if (off || !scene_) return;
|
||||||
|
constexpr u32 kMaxPartVerts = 16384, kMaxBatchVerts = 262144;
|
||||||
|
// See-through parts are sorted back to front as one: a smaller cell keeps that order close to theirs one by one.
|
||||||
|
static const float kSolidCell = 4.0f, kBlendCell = std::getenv("FH_BLEND_CELL") ? static_cast<float>(std::atof(std::getenv("FH_BLEND_CELL"))) : 2.0f;
|
||||||
|
std::map<std::tuple<uint64_t, int, int, int>, std::vector<size_t>> groups;
|
||||||
|
for (size_t i = 0; i < meshes_.size(); ++i) {
|
||||||
|
const GpuMesh& gm = meshes_[i];
|
||||||
|
if (!gm.lookHash || gm.dupOf >= 0 || !gm.visible || gm.sceneIndex < 0 || static_cast<size_t>(gm.sceneIndex) >= scene_->meshes.size()) continue;
|
||||||
|
if (meshVerts(scene_->meshes[gm.sceneIndex]) > kMaxPartVerts) continue;
|
||||||
|
const float kCell = gm.blend ? std::max(0.25f, kBlendCell) : kSolidCell;
|
||||||
|
groups[{gm.lookHash, static_cast<int>(std::floor(gm.centroid[0] / kCell)), static_cast<int>(std::floor(gm.centroid[1] / kCell)),
|
||||||
|
static_cast<int>(std::floor(gm.centroid[2] / kCell))}].push_back(i);
|
||||||
|
}
|
||||||
|
std::vector<u8> vbo, all;
|
||||||
|
std::vector<float> normals;
|
||||||
|
std::vector<u32> indices;
|
||||||
|
for (auto& g : groups) {
|
||||||
|
std::vector<size_t>& parts = g.second;
|
||||||
|
if (parts.size() < 2) continue;
|
||||||
|
const GpuMesh& first = meshes_[parts.front()];
|
||||||
|
const Program& p = programs_[first.program];
|
||||||
|
int posOff = -1, nrmOff = -1; // only float positions (and normals) can be moved; anything else stays alone
|
||||||
|
bool movable = true;
|
||||||
|
for (const auto& a : p.attrs) {
|
||||||
|
if (a.role == Role::Pos) { if (a.format == VK_FORMAT_R32G32B32_SFLOAT || a.format == VK_FORMAT_R32G32B32A32_SFLOAT) posOff = static_cast<int>(a.offset); else movable = false; }
|
||||||
|
if (a.role == Role::Normal) { if (a.format == VK_FORMAT_R32G32B32_SFLOAT || a.format == VK_FORMAT_R32G32B32A32_SFLOAT) nrmOff = static_cast<int>(a.offset); else movable = false; }
|
||||||
|
}
|
||||||
|
if (!movable || posOff < 0) continue;
|
||||||
|
for (size_t start = 0; start < parts.size();) {
|
||||||
|
// as many parts as fit in one batch
|
||||||
|
all.clear();
|
||||||
|
indices.clear();
|
||||||
|
std::vector<size_t> members;
|
||||||
|
std::vector<std::pair<u32, u32>> ranges;
|
||||||
|
u32 verts = 0;
|
||||||
|
for (; start < parts.size(); ++start) {
|
||||||
|
const GpuMesh& gm = meshes_[parts[start]];
|
||||||
|
const MeshData& md = scene_->meshes[gm.sceneIndex];
|
||||||
|
const u32 nv = meshVerts(md);
|
||||||
|
if (verts + nv > kMaxBatchVerts && !members.empty()) break;
|
||||||
|
if (std::any_of(md.indices.begin(), md.indices.end(), [nv](u32 i) { return i >= nv; })) continue; // (damaged: alone)
|
||||||
|
buildVertices(p, md, vbo, normals);
|
||||||
|
if (vbo.size() != static_cast<size_t>(nv) * p.stride) continue;
|
||||||
|
// world position; normal by the inverse transpose (the cofactors), as the shader would have
|
||||||
|
const float* w = gm.world;
|
||||||
|
const float cof[9] = {w[5] * w[10] - w[6] * w[9], w[6] * w[8] - w[4] * w[10], w[4] * w[9] - w[5] * w[8],
|
||||||
|
w[2] * w[9] - w[1] * w[10], w[0] * w[10] - w[2] * w[8], w[1] * w[8] - w[0] * w[9],
|
||||||
|
w[1] * w[6] - w[2] * w[5], w[2] * w[4] - w[0] * w[6], w[0] * w[5] - w[1] * w[4]};
|
||||||
|
for (u32 v = 0; v < nv; ++v) {
|
||||||
|
u8* base = vbo.data() + static_cast<size_t>(v) * p.stride;
|
||||||
|
float q[3];
|
||||||
|
std::memcpy(q, base + posOff, 12);
|
||||||
|
const float o[3] = {w[0] * q[0] + w[4] * q[1] + w[8] * q[2] + w[12], w[1] * q[0] + w[5] * q[1] + w[9] * q[2] + w[13],
|
||||||
|
w[2] * q[0] + w[6] * q[1] + w[10] * q[2] + w[14]};
|
||||||
|
std::memcpy(base + posOff, o, 12);
|
||||||
|
if (nrmOff >= 0) {
|
||||||
|
std::memcpy(q, base + nrmOff, 12);
|
||||||
|
float n[3] = {cof[0] * q[0] + cof[3] * q[1] + cof[6] * q[2], cof[1] * q[0] + cof[4] * q[1] + cof[7] * q[2],
|
||||||
|
cof[2] * q[0] + cof[5] * q[1] + cof[8] * q[2]};
|
||||||
|
const float l = std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
|
||||||
|
if (l > 1e-12f) { n[0] /= l; n[1] /= l; n[2] /= l; }
|
||||||
|
std::memcpy(base + nrmOff, n, 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all.insert(all.end(), vbo.begin(), vbo.end());
|
||||||
|
ranges.push_back({static_cast<u32>(indices.size()), static_cast<u32>(md.indices.size())});
|
||||||
|
for (u32 i : md.indices) indices.push_back(verts + i);
|
||||||
|
verts += nv;
|
||||||
|
members.push_back(parts[start]);
|
||||||
|
}
|
||||||
|
if (members.size() < 2) continue;
|
||||||
|
Batch b;
|
||||||
|
GpuMesh& bm = b.gm;
|
||||||
|
bm.program = first.program; bm.variant = first.variant; bm.set2 = first.set2;
|
||||||
|
std::memcpy(bm.color, first.color, 16);
|
||||||
|
std::memcpy(bm.uvXform, first.uvXform, sizeof bm.uvXform);
|
||||||
|
bm.blend = first.blend; bm.cull = first.cull; bm.alphaToCoverage = first.alphaToCoverage;
|
||||||
|
b.sorted = first.blend != 0;
|
||||||
|
const float id[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
|
||||||
|
std::memcpy(bm.baseWorld, id, 64);
|
||||||
|
for (int k = 0; k < 3; ++k) { bm.localMin[k] = 1e30f; bm.localMax[k] = -1e30f; }
|
||||||
|
for (size_t m : members)
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
bm.localMin[k] = std::min(bm.localMin[k], meshes_[m].worldMin[k]);
|
||||||
|
bm.localMax[k] = std::max(bm.localMax[k], meshes_[m].worldMax[k]);
|
||||||
|
}
|
||||||
|
for (int k = 0; k < 3; ++k) bm.localCentroid[k] = 0.5f * (bm.localMin[k] + bm.localMax[k]);
|
||||||
|
placeMesh(bm);
|
||||||
|
beginUploadBatch();
|
||||||
|
bm.vbo = createDeviceBuffer(all.data(), all.size(), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
||||||
|
if (b.sorted) { // (written each frame the order changes: sortBatch)
|
||||||
|
for (Buffer& sb : b.sortIbo) sb = createBuffer(indices.size() * 4, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, true);
|
||||||
|
if (b.sortIbo[0].mapped && b.sortIbo[1].mapped) bm.ibo = b.sortIbo[0];
|
||||||
|
b.indices = indices;
|
||||||
|
b.ranges = std::move(ranges);
|
||||||
|
} else {
|
||||||
|
bm.ibo = createDeviceBuffer(indices.data(), indices.size() * 4, VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
|
||||||
|
}
|
||||||
|
flushUploadBatch();
|
||||||
|
bm.indexCount = b.sorted ? 0 : static_cast<uint32_t>(indices.size());
|
||||||
|
if (!bm.vbo.buf || !bm.ibo.buf) { // (no memory: the parts draw alone)
|
||||||
|
destroy(bm.vbo);
|
||||||
|
if (b.sorted) { for (Buffer& sb : b.sortIbo) destroy(sb); } else destroy(bm.ibo);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (size_t m : members) meshes_[m].batch = static_cast<int>(batches_.size());
|
||||||
|
b.members = std::move(members);
|
||||||
|
batches_.push_back(std::move(b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A see-through batch this frame (render(), after culling, meshes' inView still their own): the parts in view, far to
|
||||||
|
// near by their centres as render() orders see-through meshes, their indices written in that order into this command
|
||||||
|
// buffer's index buffer when it holds anything else (it was last read two frames ago: that frame's fence was waited).
|
||||||
|
void HslRenderer::sortBatch(Batch& b, const float head[3]) {
|
||||||
|
std::vector<std::pair<float, u32>> order;
|
||||||
|
order.reserve(b.members.size());
|
||||||
|
for (u32 k = 0; k < b.members.size(); ++k) {
|
||||||
|
const GpuMesh& gm = meshes_[b.members[k]];
|
||||||
|
if (!gm.inView) continue;
|
||||||
|
float d = 0;
|
||||||
|
for (int c = 0; c < 3; ++c) d += (gm.centroid[c] - head[c]) * (gm.centroid[c] - head[c]);
|
||||||
|
order.push_back({d, k});
|
||||||
|
}
|
||||||
|
std::stable_sort(order.begin(), order.end(), [](const auto& x, const auto& y) { return x.first > y.first; });
|
||||||
|
const int s = cmdIndex_ & 1;
|
||||||
|
std::vector<u32>& was = b.written[s];
|
||||||
|
bool same = was.size() == order.size();
|
||||||
|
for (size_t k = 0; same && k < order.size(); ++k) same = was[k] == order[k].second;
|
||||||
|
if (!same) {
|
||||||
|
u32* dst = static_cast<u32*>(b.sortIbo[s].mapped);
|
||||||
|
u32 n = 0;
|
||||||
|
was.clear();
|
||||||
|
for (const auto& o : order) {
|
||||||
|
const auto& r = b.ranges[o.second];
|
||||||
|
std::memcpy(dst + n, b.indices.data() + r.first, static_cast<size_t>(r.second) * 4);
|
||||||
|
n += r.second;
|
||||||
|
was.push_back(o.second);
|
||||||
|
}
|
||||||
|
b.writtenCount[s] = n;
|
||||||
|
}
|
||||||
|
b.gm.ibo = b.sortIbo[s];
|
||||||
|
b.gm.indexCount = b.writtenCount[s];
|
||||||
|
}
|
||||||
|
|
||||||
void HslRenderer::resize(uint32_t eyeWidth, uint32_t eyeHeight) {
|
void HslRenderer::resize(uint32_t eyeWidth, uint32_t eyeHeight) {
|
||||||
if (!device_ || (eyeWidth == width_ && eyeHeight == height_) || !eyeWidth || !eyeHeight) return;
|
if (!device_ || (eyeWidth == width_ && eyeHeight == height_) || !eyeWidth || !eyeHeight) return;
|
||||||
vkDeviceWaitIdle(device_);
|
vkDeviceWaitIdle(device_);
|
||||||
@@ -3659,7 +3967,10 @@ void HslRenderer::render(const EyeView eyes[2], float timeSec) {
|
|||||||
ta = clk::now();
|
ta = clk::now();
|
||||||
updateAnimations(timeSec); // bone matrices for skinned meshes (the GPU is idle here)
|
updateAnimations(timeSec); // bone matrices for skinned meshes (the GPU is idle here)
|
||||||
tb = clk::now();
|
tb = clk::now();
|
||||||
updateGlobals(eyes, timeSec);
|
{
|
||||||
|
EyeView late[2];
|
||||||
|
updateGlobals(latePose && latePose(late) ? late : eyes, timeSec);
|
||||||
|
}
|
||||||
tg = clk::now();
|
tg = clk::now();
|
||||||
refreshDynamicMeshes(!early, true); // animated Clay / glTF meshes: this frame's vertices + tint
|
refreshDynamicMeshes(!early, true); // animated Clay / glTF meshes: this frame's vertices + tint
|
||||||
tc = clk::now();
|
tc = clk::now();
|
||||||
@@ -3831,12 +4142,28 @@ void HslRenderer::render(const EyeView eyes[2], float timeSec) {
|
|||||||
const int frameNo_ = ++cullFrame;
|
const int frameNo_ = ++cullFrame;
|
||||||
for (auto& gm : meshes_) {
|
for (auto& gm : meshes_) {
|
||||||
gm.inView = gm.skybox || (gm.visible && (inFrustum(gm, 0) || inFrustum(gm, 1))); // (animated: bounds kept current)
|
gm.inView = gm.skybox || (gm.visible && (inFrustum(gm, 0) || inFrustum(gm, 1))); // (animated: bounds kept current)
|
||||||
|
if (gm.dupOf >= 0 && gm.inView) { // an exact copy of a mesh that already draws these pixels (uploadMesh)
|
||||||
|
const GpuMesh& o = meshes_[gm.dupOf];
|
||||||
|
if (o.visible && !std::memcmp(o.world, gm.world, 64)) gm.inView = false;
|
||||||
|
}
|
||||||
if (gm.inView && gm.visible) { ++drawn_; requestTextures(gm); }
|
if (gm.inView && gm.visible) { ++drawn_; requestTextures(gm); }
|
||||||
if (gm.debug && frameNo_ == 120)
|
if (gm.debug && frameNo_ == 120)
|
||||||
LOG("[cull] %s eye %.2f %.2f %.2f | visible %d inView %d dynamic %d | world box %.2f %.2f %.2f .. %.2f %.2f %.2f",
|
LOG("[cull] %s eye %.2f %.2f %.2f | visible %d inView %d dynamic %d | world box %.2f %.2f %.2f .. %.2f %.2f %.2f",
|
||||||
gm.debugName.c_str(), eyes[0].position[0], eyes[0].position[1], eyes[0].position[2], gm.visible, gm.inView, gm.dynamic,
|
gm.debugName.c_str(), eyes[0].position[0], eyes[0].position[1], eyes[0].position[2], gm.visible, gm.inView, gm.dynamic,
|
||||||
gm.worldMin[0], gm.worldMin[1], gm.worldMin[2], gm.worldMax[0], gm.worldMax[1], gm.worldMax[2]);
|
gm.worldMin[0], gm.worldMin[1], gm.worldMin[2], gm.worldMax[0], gm.worldMax[1], gm.worldMax[2]);
|
||||||
}
|
}
|
||||||
|
// A batch draws when any of its parts is in view, in their place (the debug views show the parts themselves).
|
||||||
|
for (Batch& b : batches_) {
|
||||||
|
b.gm.inView = false;
|
||||||
|
if (!b.active || debugView_ != kViewNormal) continue;
|
||||||
|
for (size_t m : b.members) b.gm.inView |= meshes_[m].inView;
|
||||||
|
if (b.sorted && b.gm.inView) { // see-through: the parts in view, in their order
|
||||||
|
sortBatch(b, head);
|
||||||
|
b.gm.inView = b.gm.indexCount > 0;
|
||||||
|
}
|
||||||
|
for (size_t m : b.members) meshes_[m].inView = false;
|
||||||
|
}
|
||||||
|
auto meshAt = [&](size_t k) -> GpuMesh& { return k < meshes_.size() ? meshes_[k] : batches_[k - meshes_.size()].gm; };
|
||||||
|
|
||||||
// Redundant state is not re-sent: consecutive meshes of one shader share its pipeline and its set 0 / set 1
|
// Redundant state is not re-sent: consecutive meshes of one shader share its pipeline and its set 0 / set 1
|
||||||
// (the same layout keeps them bound), so only set 2 and the push constants change between them.
|
// (the same layout keeps them bound), so only set 2 and the push constants change between them.
|
||||||
@@ -3927,12 +4254,13 @@ void HslRenderer::render(const EyeView eyes[2], float timeSec) {
|
|||||||
static const int opaqueMode = std::getenv("FH_ORDER") ? std::atoi(std::getenv("FH_ORDER")) : 2;
|
static const int opaqueMode = std::getenv("FH_ORDER") ? std::atoi(std::getenv("FH_ORDER")) : 2;
|
||||||
if (opaqueMode == 0) {
|
if (opaqueMode == 0) {
|
||||||
for (auto& gm : meshes_) if (gm.blend == 0 && !gm.skybox) draw(gm);
|
for (auto& gm : meshes_) if (gm.blend == 0 && !gm.skybox) draw(gm);
|
||||||
|
for (auto& b : batches_) if (b.gm.blend == 0) draw(b.gm);
|
||||||
} else {
|
} else {
|
||||||
opaqueOrder_.clear();
|
opaqueOrder_.clear();
|
||||||
for (size_t i = 0; i < meshes_.size(); ++i) {
|
for (size_t i = 0; i < meshes_.size() + batches_.size(); ++i) { // (past the meshes: the batches, at their first part)
|
||||||
const GpuMesh& gm = meshes_[i];
|
const GpuMesh& gm = meshAt(i);
|
||||||
if (gm.blend != 0 || gm.skybox || !gm.visible || !gm.inView) continue;
|
if (gm.blend != 0 || gm.skybox || !gm.visible || !gm.inView) continue;
|
||||||
float d = static_cast<float>(i);
|
float d = static_cast<float>(i < meshes_.size() ? i : batches_[i - meshes_.size()].members.front());
|
||||||
if (opaqueMode == 1) { // distance to the box (0 inside it): a big floor around you is nearest, not its far-off centre
|
if (opaqueMode == 1) { // distance to the box (0 inside it): a big floor around you is nearest, not its far-off centre
|
||||||
d = 0;
|
d = 0;
|
||||||
for (int k = 0; k < 3; ++k) {
|
for (int k = 0; k < 3; ++k) {
|
||||||
@@ -3951,16 +4279,16 @@ void HslRenderer::render(const EyeView eyes[2], float timeSec) {
|
|||||||
size_t firstCut = opaqueOrder_.size();
|
size_t firstCut = opaqueOrder_.size();
|
||||||
if (twoPass)
|
if (twoPass)
|
||||||
for (size_t k = 0; k < opaqueOrder_.size(); ++k)
|
for (size_t k = 0; k < opaqueOrder_.size(); ++k)
|
||||||
if (meshes_[opaqueOrder_[k].second].alphaToCoverage) { firstCut = k; break; }
|
if (meshAt(opaqueOrder_[k].second).alphaToCoverage) { firstCut = k; break; }
|
||||||
for (size_t k = 0; k < firstCut; ++k) draw(meshes_[opaqueOrder_[k].second]);
|
for (size_t k = 0; k < firstCut; ++k) draw(meshAt(opaqueOrder_[k].second));
|
||||||
for (size_t k = firstCut; k < opaqueOrder_.size(); ++k) {
|
for (size_t k = firstCut; k < opaqueOrder_.size(); ++k) {
|
||||||
GpuMesh& gm = meshes_[opaqueOrder_[k].second];
|
GpuMesh& gm = meshAt(opaqueOrder_[k].second);
|
||||||
const Program& cp = programs_[gm.program];
|
const Program& cp = programs_[gm.program];
|
||||||
if (gm.variant >= 0 ? !cp.variants[gm.variant].fsAlpha : !cp.fsAlpha) gm.cutOnce = true; // no alpha-only shader: drawn the one way
|
if (gm.variant >= 0 ? !cp.variants[gm.variant].fsAlpha : !cp.fsAlpha) gm.cutOnce = true; // no alpha-only shader: drawn the one way
|
||||||
else draw(gm, kCutDepth);
|
else draw(gm, kCutDepth);
|
||||||
}
|
}
|
||||||
for (size_t k = firstCut; k < opaqueOrder_.size(); ++k) {
|
for (size_t k = firstCut; k < opaqueOrder_.size(); ++k) {
|
||||||
GpuMesh& gm = meshes_[opaqueOrder_[k].second];
|
GpuMesh& gm = meshAt(opaqueOrder_[k].second);
|
||||||
draw(gm, gm.cutOnce ? kCutNone : kCutShade);
|
draw(gm, gm.cutOnce ? kCutNone : kCutShade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3975,15 +4303,18 @@ void HslRenderer::render(const EyeView eyes[2], float timeSec) {
|
|||||||
}
|
}
|
||||||
if (set) vkCmdSetViewport(cmd_, 0, 1, &vp);
|
if (set) vkCmdSetViewport(cmd_, 0, 1, &vp);
|
||||||
}
|
}
|
||||||
|
drawSplats(eyes, head, planes); // Gaussian splats: over what's solid and the sky, under what's see-through
|
||||||
|
if (splatsDrawn_) boundPipe = VK_NULL_HANDLE, boundLayout = VK_NULL_HANDLE; // (they bound their own)
|
||||||
std::vector<std::pair<float, size_t>> order;
|
std::vector<std::pair<float, size_t>> order;
|
||||||
for (size_t i = 0; i < meshes_.size(); ++i) {
|
for (size_t i = 0; i < meshes_.size() + batches_.size(); ++i) { // (past the meshes: the see-through batches, at their box's centre)
|
||||||
if (meshes_[i].blend == 0 || meshes_[i].skybox) continue;
|
const GpuMesh& gm = meshAt(i);
|
||||||
|
if (gm.blend == 0 || gm.skybox || (i >= meshes_.size() && !gm.inView)) continue;
|
||||||
float d = 0;
|
float d = 0;
|
||||||
for (int k = 0; k < 3; ++k) d += (meshes_[i].centroid[k] - head[k]) * (meshes_[i].centroid[k] - head[k]);
|
for (int k = 0; k < 3; ++k) d += (gm.centroid[k] - head[k]) * (gm.centroid[k] - head[k]);
|
||||||
order.push_back({d, i});
|
order.push_back({d, i});
|
||||||
}
|
}
|
||||||
std::sort(order.begin(), order.end(), [](const auto& a, const auto& b) { return a.first > b.first; });
|
std::sort(order.begin(), order.end(), [](const auto& a, const auto& b) { return a.first > b.first; });
|
||||||
for (auto& o : order) draw(meshes_[o.second]);
|
for (auto& o : order) draw(meshAt(o.second));
|
||||||
|
|
||||||
if (debugView_ != kViewNormal && dbg_.draws.mapped) {
|
if (debugView_ != kViewNormal && dbg_.draws.mapped) {
|
||||||
// Each mesh's colour: its own (a hash of its index), its cost, or its kind.
|
// Each mesh's colour: its own (a hash of its index), its cost, or its kind.
|
||||||
@@ -4080,20 +4411,27 @@ void HslRenderer::render(const EyeView eyes[2], float timeSec) {
|
|||||||
if (queryPool_) vkCmdWriteTimestamp(cmd_, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queryPool_, kMaxQueries - 2);
|
if (queryPool_) vkCmdWriteTimestamp(cmd_, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queryPool_, kMaxQueries - 2);
|
||||||
if (!direct && exportSlot >= 0 && static_cast<size_t>(exportSlot) < exportImages_.size()) { // the frame for the compositor (system mode), copied
|
if (!direct && exportSlot >= 0 && static_cast<size_t>(exportSlot) < exportImages_.size()) { // the frame for the compositor (system mode), copied
|
||||||
VkImage dst = exportImages_[exportSlot];
|
VkImage dst = exportImages_[exportSlot];
|
||||||
// GENERAL between frames: the layout both processes agree on (the compositor samples it as GENERAL).
|
// GENERAL between frames: the layout both processes agree on (the compositor samples it as GENERAL). What the
|
||||||
imageBarrier(cmd_, dst, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, viewCount, exportInited_[exportSlot] ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED,
|
// slot held is discarded (UNDEFINED): the copy overwrites all of it, and the compositor never hands it back.
|
||||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
imageBarrier(cmd_, dst, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, viewCount, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0,
|
||||||
|
VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
||||||
imageBarrier(cmd_, resolve_.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, viewCount, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
imageBarrier(cmd_, resolve_.image, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, viewCount, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
||||||
VkImageCopy cp = {};
|
VkImageCopy cp = {};
|
||||||
cp.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, viewCount};
|
cp.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, viewCount};
|
||||||
cp.dstSubresource = cp.srcSubresource;
|
cp.dstSubresource = cp.srcSubresource;
|
||||||
cp.extent = {width_, height_, 1};
|
cp.extent = {width_, height_, 1};
|
||||||
vkCmdCopyImage(cmd_, resolve_.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &cp);
|
vkCmdCopyImage(cmd_, resolve_.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst, VK_IMAGE_LAYOUT_GENERAL, 1, &cp);
|
||||||
imageBarrier(cmd_, dst, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, viewCount, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL,
|
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
|
|
||||||
exportInited_[exportSlot] = true;
|
|
||||||
}
|
}
|
||||||
|
// The finished frame goes to the compositor: its image released from our queue family to the outside one (the
|
||||||
|
// compositor's layer acquires it before sampling, layer/frame_home_layer.cpp). GENERAL to GENERAL, so this is no
|
||||||
|
// layout transition and an older layer that doesn't acquire reads it exactly as before. The next frame into this
|
||||||
|
// slot discards it (UNDEFINED) rather than taking it back.
|
||||||
|
if (exportSlot >= 0 && static_cast<size_t>(exportSlot) < exportImages_.size())
|
||||||
|
ownershipBarrier(cmd_, exportImages_[exportSlot], viewCount, queueFamily_, VK_QUEUE_FAMILY_EXTERNAL,
|
||||||
|
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, 0,
|
||||||
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
|
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
|
||||||
if (queryPool_) { vkCmdWriteTimestamp(cmd_, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queryPool_, kMaxQueries - 1); profFrame_ = true; }
|
if (queryPool_) { vkCmdWriteTimestamp(cmd_, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queryPool_, kMaxQueries - 1); profFrame_ = true; }
|
||||||
timedSlot_ = exportSlot;
|
timedSlot_ = exportSlot;
|
||||||
if (perfThis) { vkCmdEndQuery(cmd_, perfPool_, 0); perfRecorded_ = true; }
|
if (perfThis) { vkCmdEndQuery(cmd_, perfPool_, 0); perfRecorded_ = true; }
|
||||||
@@ -4216,7 +4554,6 @@ bool HslRenderer::createExportImages(uint32_t n, std::vector<ExportImage>& out,
|
|||||||
}
|
}
|
||||||
exportImages_.clear();
|
exportImages_.clear();
|
||||||
for (const ExportImage& e : out) exportImages_.push_back(e.image);
|
for (const ExportImage& e : out) exportImages_.push_back(e.image);
|
||||||
exportInited_.assign(out.size(), false);
|
|
||||||
if (exportDirect) createExportFramebuffers();
|
if (exportDirect) createExportFramebuffers();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -4232,7 +4569,6 @@ bool HslRenderer::importWin32Images(uint32_t n, const uint64_t handles[], uint32
|
|||||||
for (VkDeviceMemory m : importMem_) vkFreeMemory(device_, m, nullptr);
|
for (VkDeviceMemory m : importMem_) vkFreeMemory(device_, m, nullptr);
|
||||||
exportImages_.clear();
|
exportImages_.clear();
|
||||||
importMem_.clear();
|
importMem_.clear();
|
||||||
exportInited_.clear();
|
|
||||||
exportSlot = -1;
|
exportSlot = -1;
|
||||||
exportDirect = false; // copied in (the imported textures aren't our render targets)
|
exportDirect = false; // copied in (the imported textures aren't our render targets)
|
||||||
for (uint32_t i = 0; i < n; ++i) {
|
for (uint32_t i = 0; i < n; ++i) {
|
||||||
@@ -4274,7 +4610,6 @@ bool HslRenderer::importWin32Images(uint32_t n, const uint64_t handles[], uint32
|
|||||||
exportImages_.push_back(image);
|
exportImages_.push_back(image);
|
||||||
importMem_.push_back(mem);
|
importMem_.push_back(mem);
|
||||||
}
|
}
|
||||||
exportInited_.assign(n, false);
|
|
||||||
LOG("system mode: imported %u compositor textures (%ux%u, %u layers)", n, w, h, viewCount);
|
LOG("system mode: imported %u compositor textures (%ux%u, %u layers)", n, w, h, viewCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -4342,7 +4677,6 @@ void HslRenderer::destroyExportImages(std::vector<ExportImage>& images) {
|
|||||||
destroyExportFramebuffers();
|
destroyExportFramebuffers();
|
||||||
images.clear();
|
images.clear();
|
||||||
exportImages_.clear();
|
exportImages_.clear();
|
||||||
exportInited_.clear();
|
|
||||||
exportSlot = -1;
|
exportSlot = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4355,9 +4689,15 @@ bool HslRenderer::readback(uint32_t layer, std::vector<u8>& rgba) {
|
|||||||
VkBufferImageCopy copy = {};
|
VkBufferImageCopy copy = {};
|
||||||
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1};
|
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1};
|
||||||
copy.imageExtent = {width_, height_, 1};
|
copy.imageExtent = {width_, height_, 1};
|
||||||
if (lastExportSlot_ >= 0 && static_cast<size_t>(lastExportSlot_) < exportImages_.size()) // exportDirect: the frame is there
|
if (lastExportSlot_ >= 0 && static_cast<size_t>(lastExportSlot_) < exportImages_.size()) { // exportDirect: the frame is there
|
||||||
vkCmdCopyImageToBuffer(c, exportImages_[lastExportSlot_], VK_IMAGE_LAYOUT_GENERAL, b.buf, 1, ©);
|
// (released to the compositor: taken back for the copy, then handed over again)
|
||||||
else vkCmdCopyImageToBuffer(c, resolve_.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, b.buf, 1, ©);
|
VkImage img = exportImages_[lastExportSlot_];
|
||||||
|
ownershipBarrier(c, img, viewCount, VK_QUEUE_FAMILY_EXTERNAL, queueFamily_, 0, VK_ACCESS_TRANSFER_READ_BIT,
|
||||||
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
||||||
|
vkCmdCopyImageToBuffer(c, img, VK_IMAGE_LAYOUT_GENERAL, b.buf, 1, ©);
|
||||||
|
ownershipBarrier(c, img, viewCount, queueFamily_, VK_QUEUE_FAMILY_EXTERNAL, 0, 0, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
|
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
|
||||||
|
} else vkCmdCopyImageToBuffer(c, resolve_.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, b.buf, 1, ©);
|
||||||
});
|
});
|
||||||
rgba.assign(static_cast<u8*>(b.mapped), static_cast<u8*>(b.mapped) + size);
|
rgba.assign(static_cast<u8*>(b.mapped), static_cast<u8*>(b.mapped) + size);
|
||||||
destroy(b);
|
destroy(b);
|
||||||
@@ -4369,6 +4709,7 @@ void HslRenderer::shutdown() {
|
|||||||
stopWorkers();
|
stopWorkers();
|
||||||
unload();
|
unload();
|
||||||
destroyGuide();
|
destroyGuide();
|
||||||
|
destroySplatPipeline();
|
||||||
destroyDebug();
|
destroyDebug();
|
||||||
for (VkImageView v : layerViews_) if (v) vkDestroyImageView(device_, v, nullptr);
|
for (VkImageView v : layerViews_) if (v) vkDestroyImageView(device_, v, nullptr);
|
||||||
for (Image& i : pictures_) destroy(i);
|
for (Image& i : pictures_) destroy(i);
|
||||||
@@ -4404,7 +4745,7 @@ void HslRenderer::shutdown() {
|
|||||||
if (!pipelineCachePath.empty() && vkGetPipelineCacheData(device_, pipelineCache_, &n, nullptr) == VK_SUCCESS && n) {
|
if (!pipelineCachePath.empty() && vkGetPipelineCacheData(device_, pipelineCache_, &n, nullptr) == VK_SUCCESS && n) {
|
||||||
std::vector<char> data(n);
|
std::vector<char> data(n);
|
||||||
if (vkGetPipelineCacheData(device_, pipelineCache_, &n, data.data()) == VK_SUCCESS)
|
if (vkGetPipelineCacheData(device_, pipelineCache_, &n, data.data()) == VK_SUCCESS)
|
||||||
if (FILE* f = std::fopen(pipelineCachePath.c_str(), "wb")) { std::fwrite(data.data(), 1, n, f); std::fclose(f); }
|
if (FILE* f = texcache::open(std::filesystem::u8path(pipelineCachePath), true)) { std::fwrite(data.data(), 1, n, f); std::fclose(f); }
|
||||||
}
|
}
|
||||||
vkDestroyPipelineCache(device_, pipelineCache_, nullptr);
|
vkDestroyPipelineCache(device_, pipelineCache_, nullptr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,15 @@
|
|||||||
// set2 per material: matParams (cooked constants bound by MurmurHash3 name), lightprobesParams,
|
// set2 per material: matParams (cooked constants bound by MurmurHash3 name), lightprobesParams,
|
||||||
// lightBakerParams, instance, skinning, samplers, and textures routed by name
|
// lightBakerParams, instance, skinning, samplers, and textures routed by name
|
||||||
// Both eyes render in ONE multiview pass into a 2-layer target, then go to vrcompositor as layers 0/1.
|
// Both eyes render in ONE multiview pass into a 2-layer target, then go to vrcompositor as layers 0/1.
|
||||||
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include "guide_geom.h"
|
#include "guide_geom.h"
|
||||||
#include "hsl_scene.h"
|
#include "hsl_scene.h"
|
||||||
#include "shader_bake.h"
|
#include "shader_bake.h"
|
||||||
|
#include "splat_sort.h"
|
||||||
#include "spv_reflect.h"
|
#include "spv_reflect.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#if defined(_WIN32) && !defined(VK_USE_PLATFORM_WIN32_KHR) // (Frame Studio's build defines it for every file)
|
||||||
#define VK_USE_PLATFORM_WIN32_KHR // the Win32 external-memory structs (system mode imports D3D11 textures)
|
#define VK_USE_PLATFORM_WIN32_KHR // the Win32 external-memory structs (system mode imports D3D11 textures)
|
||||||
#endif
|
#endif
|
||||||
#ifdef FH_USE_VOLK
|
#ifdef FH_USE_VOLK
|
||||||
@@ -25,6 +27,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -55,8 +58,17 @@ public:
|
|||||||
bool loadStep(double budgetMs);
|
bool loadStep(double budgetMs);
|
||||||
float loadProgress() const;
|
float loadProgress() const;
|
||||||
void unload();
|
void unload();
|
||||||
|
// Frame Studio's paste: a mesh appended to the loaded scene (scene.meshes[sceneIndex]) uploaded now, drawn alone
|
||||||
|
// (never batched). Its index here, or -1 when it can't be drawn. reloadSplats: the scene's splat objects uploaded
|
||||||
|
// again (one was added).
|
||||||
|
int addMesh(int sceneIndex);
|
||||||
|
void reloadSplats();
|
||||||
// Renders both eyes; the result is left in `colorImage()` layers 0 (left) / 1 (right), TRANSFER_SRC layout.
|
// Renders both eyes; the result is left in `colorImage()` layers 0 (left) / 1 (right), TRANSFER_SRC layout.
|
||||||
void render(const EyeView eyes[2], float timeSec);
|
void render(const EyeView eyes[2], float timeSec);
|
||||||
|
// Late pose (system mode): asked inside render() for the eyes to draw from, just before the frame's view goes to
|
||||||
|
// the GPU (after it's recorded, and after the GPU finished the frame before). True: those eyes are drawn instead
|
||||||
|
// of render()'s (which culled and sorted: the projection's widened edge covers the head's turn in between).
|
||||||
|
std::function<bool(EyeView out[2])> latePose;
|
||||||
// Copies one eye layer of the last frame to RGBA8 (sRGB-encoded) — headless snapshots / tests.
|
// Copies one eye layer of the last frame to RGBA8 (sRGB-encoded) — headless snapshots / tests.
|
||||||
bool readback(uint32_t layer, std::vector<u8>& rgba);
|
bool readback(uint32_t layer, std::vector<u8>& rgba);
|
||||||
void shutdown();
|
void shutdown();
|
||||||
@@ -92,7 +104,10 @@ public:
|
|||||||
bool meshDynamic(size_t i) const { return meshes_[i].dynamic; }
|
bool meshDynamic(size_t i) const { return meshes_[i].dynamic; }
|
||||||
// The home isn't on screen (faded out, nobody looking): textures stream in big steps.
|
// The home isn't on screen (faded out, nobody looking): textures stream in big steps.
|
||||||
void setUploadsUnseen(bool unseen) { uploadsUnseen_ = unseen; }
|
void setUploadsUnseen(bool unseen) { uploadsUnseen_ = unseen; }
|
||||||
void setMeshVisible(size_t i, bool visible) { meshes_[i].visible = visible; }
|
void setMeshVisible(size_t i, bool visible) {
|
||||||
|
if (meshes_[i].visible != visible && meshes_[i].batch >= 0) batches_[meshes_[i].batch].active = false; // (its parts drawn one by one again)
|
||||||
|
meshes_[i].visible = visible;
|
||||||
|
}
|
||||||
// Scene meshes whose vertices change every frame (HslScene::animatedMeshes). Set before beginLoad().
|
// Scene meshes whose vertices change every frame (HslScene::animatedMeshes). Set before beginLoad().
|
||||||
void setDynamicMeshes(const std::vector<int>& sceneIndices);
|
void setDynamicMeshes(const std::vector<int>& sceneIndices);
|
||||||
// Highlight one mesh (its faces tinted, every triangle edge drawn) on the GPU; -1 clears. Built once per call,
|
// Highlight one mesh (its faces tinted, every triangle edge drawn) on the GPU; -1 clears. Built once per call,
|
||||||
@@ -111,6 +126,12 @@ public:
|
|||||||
void setEarlyRecord(bool on) { earlyRecord_ = on; }
|
void setEarlyRecord(bool on) { earlyRecord_ = on; }
|
||||||
bool guideAvailable() const { return guide_.visible != VK_NULL_HANDLE; }
|
bool guideAvailable() const { return guide_.visible != VK_NULL_HANDLE; }
|
||||||
const std::string& meshProgram(size_t i) const { return programs_[meshes_[i].program].path; }
|
const std::string& meshProgram(size_t i) const { return programs_[meshes_[i].program].path; }
|
||||||
|
// Gaussian splats (hsl_renderer_splat.cpp): the scene's splat objects (HslScene::splats) go to the GPU once its meshes
|
||||||
|
// are in (loadStep), and draw after the solid meshes and the sky, before the see-through ones, each blended back to
|
||||||
|
// front in an order sorted on a thread of its own as you move. Where each is and whether it shows are read from the
|
||||||
|
// scene every frame (Frame Studio moves and hides them there).
|
||||||
|
size_t splatObjectCount() const { return splatObjs_.size(); }
|
||||||
|
size_t splatsDrawn() const { return splatsDrawn_; } // last frame, all objects
|
||||||
|
|
||||||
// Debug views (Frame Studio's View > Debug View, the Environments panel's Debug page): how a home is put together
|
// Debug views (Frame Studio's View > Debug View, the Environments panel's Debug page): how a home is put together
|
||||||
// and what it costs, drawn from each mesh's positions alone (skinned meshes in their rest pose).
|
// and what it costs, drawn from each mesh's positions alone (skinned meshes in their rest pose).
|
||||||
@@ -279,7 +300,31 @@ private:
|
|||||||
float uvXform[6] = {1, 0, 0, 0, 1, 0}; // MeshData::uvXform (pushed as uvTransformU / V)
|
float uvXform[6] = {1, 0, 0, 0, 1, 0}; // MeshData::uvXform (pushed as uvTransformU / V)
|
||||||
bool debug = false; // FH_DEBUG matched: cull state logged once
|
bool debug = false; // FH_DEBUG matched: cull state logged once
|
||||||
std::string debugName;
|
std::string debugName;
|
||||||
|
int dupOf = -1; // an exact copy of that (earlier) mesh: not drawn while it shows
|
||||||
|
uint64_t lookHash = 0; // its shader, material values and textures (0: never batched)
|
||||||
|
int batch = -1; // the batches_ entry drawing it with its neighbours (-1: drawn alone)
|
||||||
};
|
};
|
||||||
|
// Static batching (Clay homes): solid meshes that look alike (one shader, the same textures and tint) and sit near
|
||||||
|
// one another, drawn as one mesh with their vertices placed in the world. The hand tutorials' hundreds of plants
|
||||||
|
// were hundreds of draws, each a fixed cost on the GPU. A batch stops (its parts drawn one by one) as soon as an
|
||||||
|
// edit hides or moves one of them.
|
||||||
|
// See-through batches keep the parts' own back-to-front order: each frame the parts in view are sorted as they
|
||||||
|
// would be one by one, and when that order (or what's in view) changes, their indices are rewritten in it into
|
||||||
|
// the index buffer of the command buffer being recorded (host-visible, one per command buffer).
|
||||||
|
struct Batch {
|
||||||
|
GpuMesh gm; // its own vertices and indices; set2 borrowed from its first part
|
||||||
|
std::vector<size_t> members;
|
||||||
|
bool active = true;
|
||||||
|
bool sorted = false; // see-through: drawn through sortIbo[], the parts in view, in order
|
||||||
|
std::vector<u32> indices; // (sorted) every part's indices, in members order...
|
||||||
|
std::vector<std::pair<u32, u32>> ranges; // ...each part's first index and count in them
|
||||||
|
Buffer sortIbo[2];
|
||||||
|
std::vector<u32> written[2]; // the parts (members positions) each sortIbo holds, in order
|
||||||
|
u32 writtenCount[2] = {0, 0};
|
||||||
|
};
|
||||||
|
void sortBatch(Batch& b, const float head[3]);
|
||||||
|
std::vector<Batch> batches_;
|
||||||
|
void buildBatches();
|
||||||
|
|
||||||
// Vulkan plumbing
|
// Vulkan plumbing
|
||||||
uint32_t memoryType(uint32_t bits, VkMemoryPropertyFlags flags);
|
uint32_t memoryType(uint32_t bits, VkMemoryPropertyFlags flags);
|
||||||
@@ -395,7 +440,6 @@ private:
|
|||||||
void destroyExportFramebuffers();
|
void destroyExportFramebuffers();
|
||||||
void densityTexels(float cx, float cy, std::vector<float>& dens) const; // two per texel: across, down
|
void densityTexels(float cx, float cy, std::vector<float>& dens) const; // two per texel: across, down
|
||||||
void packDensity(const std::vector<float>& dens, uint8_t* dst) const; // in the map's format
|
void packDensity(const std::vector<float>& dens, uint8_t* dst) const; // in the map's format
|
||||||
std::vector<bool> exportInited_;
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
std::vector<VkDeviceMemory> importMem_; // importWin32Images: the imported memories, freed on shutdown
|
std::vector<VkDeviceMemory> importMem_; // importWin32Images: the imported memories, freed on shutdown
|
||||||
uint8_t deviceLUID_[8] = {};
|
uint8_t deviceLUID_[8] = {};
|
||||||
@@ -426,6 +470,34 @@ private:
|
|||||||
void createGuidePipelines();
|
void createGuidePipelines();
|
||||||
void destroyGuide();
|
void destroyGuide();
|
||||||
void drawGuide(const EyeView eyes[2]);
|
void drawGuide(const EyeView eyes[2]);
|
||||||
|
// Splat objects (hsl_renderer_splat.cpp, splat_spirv.h): per object its splats (32 bytes each: centre, covariance
|
||||||
|
// as halves, colour) in video memory, and per command buffer an order buffer and its matrices (host-visible: written
|
||||||
|
// while the other command buffer may still be read).
|
||||||
|
struct SplatGpu {
|
||||||
|
Buffer splats;
|
||||||
|
Buffer order[2], ubo[2];
|
||||||
|
uint64_t orderSerial[2] = {0, 0}; // the sort each order buffer holds (0: none)
|
||||||
|
VkDescriptorSet set[2] = {};
|
||||||
|
uint32_t count = 0;
|
||||||
|
float localMin[3] = {0, 0, 0}, localMax[3] = {0, 0, 0}; // the centres' box, in its own frame
|
||||||
|
std::unique_ptr<SplatSorter> sorter;
|
||||||
|
bool sorted = false; // a sort was asked for: sortEye / sortWorld say where from
|
||||||
|
float sortEye[3] = {0, 0, 0}, sortWorld[16] = {};
|
||||||
|
};
|
||||||
|
std::vector<SplatGpu> splatObjs_;
|
||||||
|
VkDescriptorSetLayout splatSetLayout_ = VK_NULL_HANDLE;
|
||||||
|
VkPipelineLayout splatLayout_ = VK_NULL_HANDLE;
|
||||||
|
VkPipeline splatPipe_ = VK_NULL_HANDLE;
|
||||||
|
VkDescriptorPool splatPool_ = VK_NULL_HANDLE;
|
||||||
|
bool splatsUploaded_ = false; // this load's splat objects are in (or it has none)
|
||||||
|
float splatHead_[3] = {0, 0, 0}; // the head last frame: a jump is a teleport (sorted at once)
|
||||||
|
bool splatHeadSet_ = false;
|
||||||
|
size_t splatsDrawn_ = 0;
|
||||||
|
void createSplatPipeline();
|
||||||
|
void uploadSplats();
|
||||||
|
void drawSplats(const EyeView eyes[2], const float head[3], const float planes[2][5][4]);
|
||||||
|
void destroySplats();
|
||||||
|
void destroySplatPipeline();
|
||||||
void destroySelection();
|
void destroySelection();
|
||||||
// Debug views (setDebugView): one pipeline per vertex layout and mode, and the draws' matrices and colours in
|
// Debug views (setDebugView): one pipeline per vertex layout and mode, and the draws' matrices and colours in
|
||||||
// one host-visible buffer (indexed by firstInstance), rewritten each frame after the fence.
|
// one host-visible buffer (indexed by firstInstance), rewritten each frame after the fence.
|
||||||
@@ -513,7 +585,8 @@ private:
|
|||||||
std::vector<GpuMesh> meshes_;
|
std::vector<GpuMesh> meshes_;
|
||||||
std::map<uint64_t, Image> textureCache_; // content hash -> shared texture (freed on unload)
|
std::map<uint64_t, Image> textureCache_; // content hash -> shared texture (freed on unload)
|
||||||
const HslScene* scene_ = nullptr;
|
const HslScene* scene_ = nullptr;
|
||||||
size_t loadNext_ = 0, loadSkipped_ = 0;
|
size_t loadNext_ = 0, loadSkipped_ = 0, loadDuplicates_ = 0;
|
||||||
|
std::unordered_map<uint64_t, size_t> dedupe_; // an opaque mesh's whole-look hash -> its meshes_ index
|
||||||
bool loadDone_ = true;
|
bool loadDone_ = true;
|
||||||
std::chrono::steady_clock::time_point loadStart_;
|
std::chrono::steady_clock::time_point loadStart_;
|
||||||
float ambient_[3] = {1, 1, 1};
|
float ambient_[3] = {1, 1, 1};
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
// Gaussian splats in the home renderer (hsl_renderer.h): each splat object's splats in one storage buffer, drawn as one
|
||||||
|
// instanced quad per splat (tools/splat: the shaders) in the frame's own pass, both eyes at once, after the solid meshes
|
||||||
|
// and the sky. Back to front by an order sorted on the CPU (splat_sort.h), copied into the order buffer of the command
|
||||||
|
// buffer being recorded whenever a newer one is ready.
|
||||||
|
#include "hsl_renderer.h"
|
||||||
|
#include "splat_spirv.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#define LOG(...) do { std::fprintf(stderr, "[render] " __VA_ARGS__); std::fputc('\n', stderr); } while (0)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// As the shader reads it (std430): centre, the covariance's six entries as halves, colour, padding.
|
||||||
|
struct GpuSplat {
|
||||||
|
float pos[3];
|
||||||
|
uint32_t cov01, cov23, cov45;
|
||||||
|
uint32_t rgba;
|
||||||
|
uint32_t pad;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(GpuSplat) == 32, "the shader's Splat is 32 bytes");
|
||||||
|
|
||||||
|
// Round to nearest, with the small values a covariance has (a 1 mm splat is 1e-6 m^2) kept as subnormals, not zero.
|
||||||
|
uint16_t half(float f) {
|
||||||
|
uint32_t x;
|
||||||
|
std::memcpy(&x, &f, 4);
|
||||||
|
const uint32_t sign = (x >> 16) & 0x8000u;
|
||||||
|
x &= 0x7fffffffu;
|
||||||
|
if (x >= 0x7f800000u) return static_cast<uint16_t>(sign | 0x7c00u | (x > 0x7f800000u ? 0x200u : 0)); // inf / nan
|
||||||
|
if (x >= 0x477ff000u) return static_cast<uint16_t>(sign | 0x7bffu); // past the largest: the largest
|
||||||
|
if (x < 0x38800000u) { // subnormal (or zero)
|
||||||
|
if (x < 0x33000000u) return static_cast<uint16_t>(sign);
|
||||||
|
const uint32_t e = x >> 23, m = (x & 0x7fffffu) | 0x800000u, shift = 126 - e; // 14 + (127 - 15) - e
|
||||||
|
const uint32_t v = m >> shift, rest = m & ((1u << shift) - 1), halfway = 1u << (shift - 1);
|
||||||
|
return static_cast<uint16_t>(sign | (v + (rest > halfway || (rest == halfway && (v & 1)))));
|
||||||
|
}
|
||||||
|
const uint32_t v = ((x >> 13) - (112u << 10)), rest = x & 0x1fffu;
|
||||||
|
return static_cast<uint16_t>(sign | (v + (rest > 0x1000u || (rest == 0x1000u && (v & 1)))));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void HslRenderer::createSplatPipeline() {
|
||||||
|
VkDescriptorSetLayoutBinding b[3] = {};
|
||||||
|
for (uint32_t i = 0; i < 3; ++i) {
|
||||||
|
b[i].binding = i;
|
||||||
|
b[i].descriptorType = i < 2 ? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER : VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
||||||
|
b[i].descriptorCount = 1;
|
||||||
|
b[i].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
|
||||||
|
}
|
||||||
|
VkDescriptorSetLayoutCreateInfo lci = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
||||||
|
lci.bindingCount = 3; lci.pBindings = b;
|
||||||
|
if (vkCreateDescriptorSetLayout(device_, &lci, nullptr, &splatSetLayout_) != VK_SUCCESS) { splatSetLayout_ = VK_NULL_HANDLE; return; }
|
||||||
|
VkPipelineLayoutCreateInfo pli = {VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
||||||
|
pli.setLayoutCount = 1; pli.pSetLayouts = &splatSetLayout_;
|
||||||
|
if (vkCreatePipelineLayout(device_, &pli, nullptr, &splatLayout_) != VK_SUCCESS) { splatLayout_ = VK_NULL_HANDLE; destroySplatPipeline(); return; }
|
||||||
|
auto module = [&](const uint32_t* code, size_t size) {
|
||||||
|
VkShaderModuleCreateInfo ci = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||||
|
ci.codeSize = size; ci.pCode = code;
|
||||||
|
VkShaderModule m = VK_NULL_HANDLE;
|
||||||
|
if (vkCreateShaderModule(device_, &ci, nullptr, &m) != VK_SUCCESS) m = VK_NULL_HANDLE;
|
||||||
|
return m;
|
||||||
|
};
|
||||||
|
VkShaderModule vs = module(kSplatVertSpirv, kSplatVertSize), fs = module(kSplatFragSpirv, kSplatFragSize);
|
||||||
|
if (vs && fs) {
|
||||||
|
VkPipelineShaderStageCreateInfo st[2] = {{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}};
|
||||||
|
st[0].stage = VK_SHADER_STAGE_VERTEX_BIT; st[0].module = vs; st[0].pName = "main";
|
||||||
|
st[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; st[1].module = fs; st[1].pName = "main";
|
||||||
|
VkPipelineVertexInputStateCreateInfo vi = {VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; // (all from the buffers)
|
||||||
|
VkPipelineInputAssemblyStateCreateInfo ia = {VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
||||||
|
ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
|
||||||
|
VkPipelineViewportStateCreateInfo vps = {VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||||
|
vps.viewportCount = 1; vps.scissorCount = 1;
|
||||||
|
VkPipelineRasterizationStateCreateInfo rs = {VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||||
|
rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f;
|
||||||
|
VkPipelineMultisampleStateCreateInfo ms = {VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||||
|
ms.rasterizationSamples = samples_;
|
||||||
|
VkPipelineDepthStencilStateCreateInfo ds = {VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
||||||
|
ds.depthTestEnable = VK_TRUE; ds.depthWriteEnable = VK_FALSE; // hidden by what's solid in front, hiding nothing themselves
|
||||||
|
ds.depthCompareOp = VK_COMPARE_OP_GREATER_OR_EQUAL; // reversed-Z
|
||||||
|
VkPipelineColorBlendAttachmentState cb = {};
|
||||||
|
cb.colorWriteMask = 0xF;
|
||||||
|
cb.blendEnable = VK_TRUE; // premultiplied: the shader multiplied its alpha in
|
||||||
|
cb.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; cb.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; cb.colorBlendOp = VK_BLEND_OP_ADD;
|
||||||
|
cb.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; cb.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; cb.alphaBlendOp = VK_BLEND_OP_ADD; // the frame's alpha stays the home's
|
||||||
|
VkPipelineColorBlendStateCreateInfo cbs = {VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
||||||
|
cbs.attachmentCount = 1; cbs.pAttachments = &cb;
|
||||||
|
VkDynamicState dyn[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
||||||
|
VkPipelineDynamicStateCreateInfo dsi = {VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
|
||||||
|
dsi.dynamicStateCount = 2; dsi.pDynamicStates = dyn;
|
||||||
|
VkGraphicsPipelineCreateInfo pci = {VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||||
|
pci.stageCount = 2; pci.pStages = st; pci.pVertexInputState = &vi; pci.pInputAssemblyState = &ia; pci.pViewportState = &vps;
|
||||||
|
pci.pRasterizationState = &rs; pci.pMultisampleState = &ms; pci.pDepthStencilState = &ds; pci.pColorBlendState = &cbs;
|
||||||
|
pci.pDynamicState = &dsi; pci.layout = splatLayout_; pci.renderPass = renderPass_;
|
||||||
|
if (vkCreateGraphicsPipelines(device_, pipelineCache_, 1, &pci, nullptr, &splatPipe_) != VK_SUCCESS) splatPipe_ = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
if (vs) vkDestroyShaderModule(device_, vs, nullptr);
|
||||||
|
if (fs) vkDestroyShaderModule(device_, fs, nullptr);
|
||||||
|
if (!splatPipe_) { LOG("splat shaders failed: splats won't draw"); destroySplatPipeline(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void HslRenderer::destroySplatPipeline() {
|
||||||
|
if (splatPipe_) vkDestroyPipeline(device_, splatPipe_, nullptr);
|
||||||
|
if (splatLayout_) vkDestroyPipelineLayout(device_, splatLayout_, nullptr);
|
||||||
|
if (splatSetLayout_) vkDestroyDescriptorSetLayout(device_, splatSetLayout_, nullptr);
|
||||||
|
splatPipe_ = VK_NULL_HANDLE;
|
||||||
|
splatLayout_ = VK_NULL_HANDLE;
|
||||||
|
splatSetLayout_ = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once the meshes are in (loadStep): every splat object of the scene, its covariances worked out here once (in its own
|
||||||
|
// frame: the shader turns them by its placement), and its sorter started.
|
||||||
|
void HslRenderer::uploadSplats() {
|
||||||
|
splatsUploaded_ = true;
|
||||||
|
if (!scene_ || scene_->splats.empty() || !splatPipe_) return;
|
||||||
|
const auto t0 = std::chrono::steady_clock::now();
|
||||||
|
VkPhysicalDeviceProperties props;
|
||||||
|
vkGetPhysicalDeviceProperties(physical_, &props);
|
||||||
|
const size_t most = props.limits.maxStorageBufferRange / sizeof(GpuSplat); // what one storage buffer may show the shader
|
||||||
|
const uint32_t n = static_cast<uint32_t>(scene_->splats.size());
|
||||||
|
VkDescriptorPoolSize sizes[2] = {{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4 * n}, {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2 * n}};
|
||||||
|
VkDescriptorPoolCreateInfo pci = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||||
|
pci.maxSets = 2 * n; pci.poolSizeCount = 2; pci.pPoolSizes = sizes;
|
||||||
|
if (vkCreateDescriptorPool(device_, &pci, nullptr, &splatPool_) != VK_SUCCESS) { splatPool_ = VK_NULL_HANDLE; LOG("splats: no descriptor pool"); return; }
|
||||||
|
size_t total = 0;
|
||||||
|
for (const HslSplatObject& so : scene_->splats) {
|
||||||
|
splatObjs_.emplace_back();
|
||||||
|
SplatGpu& g = splatObjs_.back();
|
||||||
|
if (!so.splats || so.splats->empty()) continue;
|
||||||
|
const std::vector<Splat>& src = *so.splats;
|
||||||
|
const size_t count = std::min(src.size(), most);
|
||||||
|
if (count < src.size()) LOG("splats %s: %zu of %zu (the most one buffer holds on this GPU)", so.name.c_str(), count, src.size());
|
||||||
|
std::vector<GpuSplat> gpu(count);
|
||||||
|
for (int k = 0; k < 3; ++k) { g.localMin[k] = 1e30f; g.localMax[k] = -1e30f; }
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
const Splat& s = src[i];
|
||||||
|
float cov[6];
|
||||||
|
splatCovariance(s, cov);
|
||||||
|
GpuSplat& o = gpu[i];
|
||||||
|
std::memcpy(o.pos, s.pos, 12);
|
||||||
|
o.cov01 = half(cov[0]) | static_cast<uint32_t>(half(cov[1])) << 16;
|
||||||
|
o.cov23 = half(cov[2]) | static_cast<uint32_t>(half(cov[3])) << 16;
|
||||||
|
o.cov45 = half(cov[4]) | static_cast<uint32_t>(half(cov[5])) << 16;
|
||||||
|
o.rgba = s.rgba[0] | s.rgba[1] << 8 | s.rgba[2] << 16 | static_cast<uint32_t>(s.rgba[3]) << 24;
|
||||||
|
o.pad = 0;
|
||||||
|
for (int k = 0; k < 3; ++k) { g.localMin[k] = std::min(g.localMin[k], s.pos[k]); g.localMax[k] = std::max(g.localMax[k], s.pos[k]); }
|
||||||
|
}
|
||||||
|
g.splats = createDeviceBuffer(gpu.data(), gpu.size() * sizeof(GpuSplat), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
||||||
|
if (!g.splats.buf) { LOG("splats %s: no memory for them", so.name.c_str()); continue; }
|
||||||
|
bool ok = true;
|
||||||
|
for (int c = 0; c < 2 && ok; ++c) {
|
||||||
|
g.order[c] = createBuffer(count * sizeof(uint32_t), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true);
|
||||||
|
g.ubo[c] = createBuffer(208, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, true); // clipFromWorld[2], worldFromModel, viewport
|
||||||
|
VkDescriptorSetAllocateInfo ai = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||||
|
ai.descriptorPool = splatPool_; ai.descriptorSetCount = 1; ai.pSetLayouts = &splatSetLayout_;
|
||||||
|
ok = g.order[c].mapped && g.ubo[c].mapped && vkAllocateDescriptorSets(device_, &ai, &g.set[c]) == VK_SUCCESS;
|
||||||
|
if (!ok) break;
|
||||||
|
VkDescriptorBufferInfo bi[3] = {{g.splats.buf, 0, VK_WHOLE_SIZE}, {g.order[c].buf, 0, VK_WHOLE_SIZE}, {g.ubo[c].buf, 0, VK_WHOLE_SIZE}};
|
||||||
|
VkWriteDescriptorSet w[3] = {};
|
||||||
|
for (uint32_t k = 0; k < 3; ++k) {
|
||||||
|
w[k].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||||
|
w[k].dstSet = g.set[c]; w[k].dstBinding = k; w[k].descriptorCount = 1;
|
||||||
|
w[k].descriptorType = k < 2 ? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER : VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
||||||
|
w[k].pBufferInfo = &bi[k];
|
||||||
|
}
|
||||||
|
vkUpdateDescriptorSets(device_, 3, w, 0, nullptr);
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
LOG("splats %s: no memory for their order", so.name.c_str());
|
||||||
|
destroy(g.splats);
|
||||||
|
for (int c = 0; c < 2; ++c) { destroy(g.order[c]); destroy(g.ubo[c]); }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
g.count = static_cast<uint32_t>(count);
|
||||||
|
g.sorter = std::make_unique<SplatSorter>(src, count);
|
||||||
|
total += count;
|
||||||
|
}
|
||||||
|
LOG("uploaded %zu splats in %zu objects (%.0f ms)", total, splatObjs_.size(),
|
||||||
|
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count());
|
||||||
|
}
|
||||||
|
|
||||||
|
void HslRenderer::destroySplats() {
|
||||||
|
for (SplatGpu& g : splatObjs_) {
|
||||||
|
g.sorter.reset(); // (its thread stops first)
|
||||||
|
destroy(g.splats);
|
||||||
|
for (int c = 0; c < 2; ++c) { destroy(g.order[c]); destroy(g.ubo[c]); }
|
||||||
|
}
|
||||||
|
splatObjs_.clear();
|
||||||
|
if (splatPool_) vkDestroyDescriptorPool(device_, splatPool_, nullptr); // (its sets with it)
|
||||||
|
splatPool_ = VK_NULL_HANDLE;
|
||||||
|
splatsUploaded_ = false;
|
||||||
|
splatHeadSet_ = false;
|
||||||
|
splatsDrawn_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inside the frame's render pass, after the solid meshes and the sky. Each object in view gets a new order asked for
|
||||||
|
// when the head has moved a centimetre or the object has moved since its last one (at once: the first time it's
|
||||||
|
// drawn, and after a jump of over a metre, a teleport), and draws with the newest order there is.
|
||||||
|
void HslRenderer::drawSplats(const EyeView eyes[2], const float head[3], const float planes[2][5][4]) {
|
||||||
|
splatsDrawn_ = 0;
|
||||||
|
if (!splatPipe_ || splatObjs_.empty() || !scene_ || scene_->splats.size() != splatObjs_.size()) return;
|
||||||
|
// Debug views: drawn in the ones over the home as itself, not where every mesh is replaced; "see-through" hides them.
|
||||||
|
if (debugView_ == kViewMeshes || debugView_ == kViewOverdraw || (debugHide & kHideSeeThrough)) return;
|
||||||
|
const bool jumped = splatHeadSet_ && (head[0] - splatHead_[0]) * (head[0] - splatHead_[0]) + (head[1] - splatHead_[1]) * (head[1] - splatHead_[1]) +
|
||||||
|
(head[2] - splatHead_[2]) * (head[2] - splatHead_[2]) > 1.0f;
|
||||||
|
std::memcpy(splatHead_, head, 12);
|
||||||
|
splatHeadSet_ = true;
|
||||||
|
float clip[32];
|
||||||
|
for (int e = 0; e < 2; ++e) mat4mul4(eyes[e].proj.m, eyes[e].view.m, clip + 16 * e);
|
||||||
|
const float viewport[4] = {static_cast<float>(width_), static_cast<float>(height_), 1.0f / std::max(1u, width_), 1.0f / std::max(1u, height_)};
|
||||||
|
bool bound = false;
|
||||||
|
for (size_t i = 0; i < splatObjs_.size(); ++i) {
|
||||||
|
SplatGpu& g = splatObjs_[i];
|
||||||
|
const HslSplatObject& so = scene_->splats[i];
|
||||||
|
if (!g.count || !g.sorter || so.hidden) continue;
|
||||||
|
const float* w = so.worldFromModel;
|
||||||
|
// its box in the world: off both eyes' views, nothing to draw (and nothing to sort)
|
||||||
|
float mn[3] = {1e30f, 1e30f, 1e30f}, mx[3] = {-1e30f, -1e30f, -1e30f};
|
||||||
|
for (int c = 0; c < 8; ++c) {
|
||||||
|
const float p[3] = {c & 1 ? g.localMax[0] : g.localMin[0], c & 2 ? g.localMax[1] : g.localMin[1], c & 4 ? g.localMax[2] : g.localMin[2]};
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
const float v = w[k] * p[0] + w[4 + k] * p[1] + w[8 + k] * p[2] + w[12 + k];
|
||||||
|
mn[k] = std::min(mn[k], v);
|
||||||
|
mx[k] = std::max(mx[k], v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool seen = false;
|
||||||
|
for (int e = 0; e < 2 && !seen; ++e) {
|
||||||
|
bool in = true;
|
||||||
|
for (int k = 0; k < 5 && in; ++k) {
|
||||||
|
const float* pl = planes[e][k];
|
||||||
|
in = pl[0] * (pl[0] >= 0 ? mx[0] : mn[0]) + pl[1] * (pl[1] >= 0 ? mx[1] : mn[1]) + pl[2] * (pl[2] >= 0 ? mx[2] : mn[2]) + pl[3] >= 0;
|
||||||
|
}
|
||||||
|
seen = in;
|
||||||
|
}
|
||||||
|
if (!seen) continue;
|
||||||
|
// a new order: from where the head is now, for where the object is now
|
||||||
|
const float dx = head[0] - g.sortEye[0], dy = head[1] - g.sortEye[1], dz = head[2] - g.sortEye[2];
|
||||||
|
const bool moved = std::memcmp(g.sortWorld, w, 64) != 0;
|
||||||
|
if (!g.sorted || jumped || moved || dx * dx + dy * dy + dz * dz > 1e-4f) {
|
||||||
|
g.sorter->request(head, w, !g.sorted || jumped);
|
||||||
|
std::memcpy(g.sortEye, head, 12);
|
||||||
|
std::memcpy(g.sortWorld, w, 64);
|
||||||
|
g.sorted = true;
|
||||||
|
}
|
||||||
|
uint64_t serial = 0;
|
||||||
|
std::shared_ptr<const SplatSorter::Order> order = g.sorter->latest(serial);
|
||||||
|
if (!order || order->size() != g.count) continue;
|
||||||
|
if (g.orderSerial[cmdIndex_] != serial) { // this command buffer's copy (the other one's may still be read)
|
||||||
|
std::memcpy(g.order[cmdIndex_].mapped, order->data(), order->size() * sizeof(uint32_t));
|
||||||
|
g.orderSerial[cmdIndex_] = serial;
|
||||||
|
}
|
||||||
|
order.reset(); // (its buffer goes back to the sorter)
|
||||||
|
u8* u = static_cast<u8*>(g.ubo[cmdIndex_].mapped);
|
||||||
|
std::memcpy(u, clip, 128);
|
||||||
|
std::memcpy(u + 128, w, 64);
|
||||||
|
std::memcpy(u + 192, viewport, 16);
|
||||||
|
if (!bound) { vkCmdBindPipeline(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, splatPipe_); bound = true; }
|
||||||
|
vkCmdBindDescriptorSets(cmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, splatLayout_, 0, 1, &g.set[cmdIndex_], 0, nullptr);
|
||||||
|
vkCmdDraw(cmd_, 4, g.count, 0, 0);
|
||||||
|
splatsDrawn_ += g.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,11 +30,13 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Reads a whole zip entry; empty on failure.
|
// Reads a whole zip entry; empty on failure. Its declared size is the file's word (only the outer APK is validated):
|
||||||
|
// past 1 GB, or more than this memory holds, it's taken as damaged rather than allocated.
|
||||||
std::vector<u8> extract(mz_zip_archive& z, int index) {
|
std::vector<u8> extract(mz_zip_archive& z, int index) {
|
||||||
mz_zip_archive_file_stat st;
|
mz_zip_archive_file_stat st;
|
||||||
if (!mz_zip_reader_file_stat(&z, static_cast<mz_uint>(index), &st)) return {};
|
if (!mz_zip_reader_file_stat(&z, static_cast<mz_uint>(index), &st) || st.m_uncomp_size > (uint64_t(1) << 30)) return {};
|
||||||
std::vector<u8> out(static_cast<size_t>(st.m_uncomp_size)); // unpacked straight into place
|
std::vector<u8> out;
|
||||||
|
try { out.resize(static_cast<size_t>(st.m_uncomp_size)); } catch (const std::exception&) { return {}; } // unpacked straight into place
|
||||||
if (!mz_zip_reader_extract_to_mem(&z, static_cast<mz_uint>(index), out.data(), out.size(), 0)) return {};
|
if (!mz_zip_reader_extract_to_mem(&z, static_cast<mz_uint>(index), out.data(), out.size(), 0)) return {};
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -48,7 +50,8 @@ std::string surfaceName(const std::string& path) {
|
|||||||
|
|
||||||
// Scans assets/scene.zip: every RENDSHAD forward pass (shader programs) and the merged lightprobe
|
// Scans assets/scene.zip: every RENDSHAD forward pass (shader programs) and the merged lightprobe
|
||||||
// ambient (lightprobes_merged.lprb, field2 = global SH; L00 / 3.5449 = ambient radiance).
|
// ambient (lightprobes_merged.lprb, field2 = global SH; L00 / 3.5449 = ambient radiance).
|
||||||
void scanSceneZip(const std::string& apkPath, HslScene& scene) {
|
// only: just these programs (asset paths without "content/"), nothing else (borrowing from another APK).
|
||||||
|
void scanSceneZip(const std::string& apkPath, HslScene& scene, const std::set<std::string>* only = nullptr) {
|
||||||
const std::shared_ptr<const std::vector<u8>> sceneZip = scenezip::get(apkPath); // the loader's copy
|
const std::shared_ptr<const std::vector<u8>> sceneZip = scenezip::get(apkPath); // the loader's copy
|
||||||
if (!sceneZip || sceneZip->empty()) return;
|
if (!sceneZip || sceneZip->empty()) return;
|
||||||
|
|
||||||
@@ -61,6 +64,7 @@ void scanSceneZip(const std::string& apkPath, HslScene& scene) {
|
|||||||
std::string base = name.substr(name.find_last_of('/') + 1);
|
std::string base = name.substr(name.find_last_of('/') + 1);
|
||||||
|
|
||||||
// Any "<name>.surface/<hash>/shader*" entry: older homes keep theirs under meta/clay_shaders/0.0.1/, not /shaders/.
|
// Any "<name>.surface/<hash>/shader*" entry: older homes keep theirs under meta/clay_shaders/0.0.1/, not /shaders/.
|
||||||
|
if (only && !only->count(name.rfind("content/", 0) == 0 ? name.substr(8) : name)) continue;
|
||||||
if (name.find(".surface/") != std::string::npos && base.rfind("shader", 0) == 0) {
|
if (name.find(".surface/") != std::string::npos && base.rfind("shader", 0) == 0) {
|
||||||
std::vector<u8> data = extract(z, static_cast<int>(i));
|
std::vector<u8> data = extract(z, static_cast<int>(i));
|
||||||
std::vector<SpirvBlob> blobs;
|
std::vector<SpirvBlob> blobs;
|
||||||
@@ -93,6 +97,45 @@ void scanSceneZip(const std::string& apkPath, HslScene& scene) {
|
|||||||
mz_zip_reader_end(&z);
|
mz_zip_reader_end(&z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A vista is cooked against the footprint it ships with: horizon_shared_shaders programs the footprint carries are
|
||||||
|
// left out of the vista (Horror's garden ground / fence / rocks: unlittiledlightmaprgmasked, unpacked*, ...), and the
|
||||||
|
// headset has them because both load together. Opened alone (or with another room), those meshes had no program: take
|
||||||
|
// the exact same asset (same path = same cooked program) from a footprint APK beside it (Downloads; envs/ on the Quest).
|
||||||
|
void borrowMissingPrograms(HslScene& scene) {
|
||||||
|
std::set<std::string> missing;
|
||||||
|
for (const MeshData& md : scene.meshes)
|
||||||
|
if (!md.shaderPath.empty() && !md.overlayKind && !findProgram(scene, md.shaderPath)) missing.insert(md.shaderPath);
|
||||||
|
if (missing.empty()) return;
|
||||||
|
std::set<std::string> dirs;
|
||||||
|
for (const std::string& p : scene.apkPaths) {
|
||||||
|
const size_t s = p.find_last_of("/\\");
|
||||||
|
dirs.insert(s == std::string::npos ? std::string(".") : p.substr(0, s));
|
||||||
|
}
|
||||||
|
std::vector<std::string> candidates;
|
||||||
|
for (const std::string& d : dirs) {
|
||||||
|
std::error_code ec;
|
||||||
|
for (const auto& e : std::filesystem::directory_iterator(std::filesystem::u8path(d), ec)) {
|
||||||
|
std::string f = e.path().filename().u8string();
|
||||||
|
for (char& c : f) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
|
if (f.size() < 4 || f.compare(f.size() - 4, 4, ".apk") != 0 || f.find("footprint") == std::string::npos) continue;
|
||||||
|
const std::string full = e.path().u8string();
|
||||||
|
if (std::find(scene.apkPaths.begin(), scene.apkPaths.end(), full) == scene.apkPaths.end()) candidates.push_back(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::sort(candidates.begin(), candidates.end());
|
||||||
|
for (const std::string& apk : candidates) {
|
||||||
|
const size_t before = scene.programs.size();
|
||||||
|
scanSceneZip(apk, scene, &missing);
|
||||||
|
if (scene.programs.size() == before) continue;
|
||||||
|
for (auto it = missing.begin(); it != missing.end();) it = scene.programs.count(*it) ? missing.erase(it) : std::next(it);
|
||||||
|
std::fprintf(stderr, "[scene] %zu shader program(s) the vista leaves to its footprint taken from %s\n",
|
||||||
|
scene.programs.size() - before, apk.c_str());
|
||||||
|
if (missing.empty()) break;
|
||||||
|
}
|
||||||
|
for (const std::string& m : missing)
|
||||||
|
std::fprintf(stderr, "[scene] shader program %s: not in this home or a footprint beside it\n", m.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
// AndroidManifest.xml (binary XML): the <manifest> element's package and versionName attributes.
|
// AndroidManifest.xml (binary XML): the <manifest> element's package and versionName attributes.
|
||||||
static void readManifest(const std::string& apkPath, std::string& package, std::string& version) {
|
static void readManifest(const std::string& apkPath, std::string& package, std::string& version) {
|
||||||
mz_zip_archive apk = {};
|
mz_zip_archive apk = {};
|
||||||
@@ -154,6 +197,9 @@ constexpr const char* kClayProgram = "builtin/clay.surface/0/shader";
|
|||||||
constexpr const char* kClayCutoutProgram = "builtin/clay_cutout.surface/0/shader";
|
constexpr const char* kClayCutoutProgram = "builtin/clay_cutout.surface/0/shader";
|
||||||
constexpr const char* kClaySkinnedProgram = "builtin/clay_skinned.surface/0/shader"; // skinned on the GPU
|
constexpr const char* kClaySkinnedProgram = "builtin/clay_skinned.surface/0/shader"; // skinned on the GPU
|
||||||
constexpr const char* kClaySkinnedCutoutProgram = "builtin/clay_skinned_cutout.surface/0/shader";
|
constexpr const char* kClaySkinnedCutoutProgram = "builtin/clay_skinned_cutout.surface/0/shader";
|
||||||
|
// Imported models with their own vertex colours (PLY scans, glTF COLOR_0): the same programs, times those colours.
|
||||||
|
constexpr const char* kClayVColorProgram = "builtin/clay_vcolor.surface/0/shader";
|
||||||
|
constexpr const char* kClayVColorCutoutProgram = "builtin/clay_vcolor_cutout.surface/0/shader";
|
||||||
|
|
||||||
// The home's looping ambience: an Ogg Vorbis entry of scene.zip (Clay homes ship _BACKGROUND_LOOP.ogg).
|
// The home's looping ambience: an Ogg Vorbis entry of scene.zip (Clay homes ship _BACKGROUND_LOOP.ogg).
|
||||||
static std::vector<u8> findAmbientOgg(const std::string& apkPath, std::string& source, std::string& name) {
|
static std::vector<u8> findAmbientOgg(const std::string& apkPath, std::string& source, std::string& name) {
|
||||||
@@ -258,6 +304,25 @@ static void addClayPrograms(HslScene& scene) {
|
|||||||
p.frag.assign(std::begin(kClayFragSpirv), std::end(kClayFragSpirv));
|
p.frag.assign(std::begin(kClayFragSpirv), std::end(kClayFragSpirv));
|
||||||
scene.programs[p.path] = p;
|
scene.programs[p.path] = p;
|
||||||
}
|
}
|
||||||
|
// (only when a model uses them: a scene without vertex colours bakes exactly as before)
|
||||||
|
static void addVColorPrograms(HslScene& scene) {
|
||||||
|
ShaderProgramSrc p;
|
||||||
|
p.path = kClayVColorProgram;
|
||||||
|
p.vert.assign(std::begin(kClayVColorVertSpirv), std::end(kClayVColorVertSpirv));
|
||||||
|
p.frag.assign(std::begin(kClayFragSpirv), std::end(kClayFragSpirv));
|
||||||
|
scene.programs[p.path] = p;
|
||||||
|
p.path = kClayVColorCutoutProgram;
|
||||||
|
p.frag.assign(std::begin(kClayFragCutoutSpirv), std::end(kClayFragCutoutSpirv));
|
||||||
|
scene.programs[p.path] = p;
|
||||||
|
}
|
||||||
|
// Vertex colours a model's file means to be seen: PLY's (scans) and glTF's COLOR_0 (the spec multiplies it in).
|
||||||
|
// FBX / OBJ colour layers are often painting masks or bake data, so those stay as before.
|
||||||
|
static bool showsVertexColours(const std::string& path, const MeshData& md) {
|
||||||
|
if (md.colors.size() < static_cast<size_t>(md.nVerts) * 4 || md.colors.empty()) return false;
|
||||||
|
std::string ext = path.substr(path.find_last_of('.') == std::string::npos ? path.size() : path.find_last_of('.'));
|
||||||
|
for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
|
return ext == ".ply" || ext == ".gltf" || ext == ".glb";
|
||||||
|
}
|
||||||
|
|
||||||
// A .fenv: its baked scene when it has one (instant), else its homes and models through the normal loaders.
|
// A .fenv: its baked scene when it has one (instant), else its homes and models through the normal loaders.
|
||||||
static bool loadFenv(const std::string& path, HslScene& scene, std::string& error) {
|
static bool loadFenv(const std::string& path, HslScene& scene, std::string& error) {
|
||||||
@@ -275,8 +340,19 @@ static bool loadFenv(const std::string& path, HslScene& scene, std::string& erro
|
|||||||
} else {
|
} else {
|
||||||
std::vector<std::string> all = sources;
|
std::vector<std::string> all = sources;
|
||||||
all.insert(all.end(), models.begin(), models.end());
|
all.insert(all.end(), models.begin(), models.end());
|
||||||
if (all.empty()) { error = "empty environment: " + path; return false; }
|
if (all.empty() && m.splats.empty()) { error = "empty environment: " + path; return false; }
|
||||||
if (!loadHslScene(all, scene, error)) return false;
|
if (!all.empty() && !loadHslScene(all, scene, error)) return false;
|
||||||
|
}
|
||||||
|
// Its splat objects (each its own .spz member), placed as saved. One that can't be read is left out (and said so).
|
||||||
|
for (const FenvSplats& sp : m.splats) {
|
||||||
|
SplatCloud c;
|
||||||
|
std::string e;
|
||||||
|
if (!loadSplats(sp.file, c, e)) { std::fprintf(stderr, "[scene] splats %s left out: %s\n", sp.name.c_str(), e.c_str()); continue; }
|
||||||
|
HslSplatObject o;
|
||||||
|
o.name = sp.name;
|
||||||
|
std::memcpy(o.worldFromModel, sp.xform, sizeof o.worldFromModel);
|
||||||
|
o.splats = std::make_shared<const std::vector<Splat>>(std::move(c.splats));
|
||||||
|
scene.splats.push_back(std::move(o));
|
||||||
}
|
}
|
||||||
scene.fenvPath = path;
|
scene.fenvPath = path;
|
||||||
scene.fenvEdits = edits;
|
scene.fenvEdits = edits;
|
||||||
@@ -286,7 +362,8 @@ static bool loadFenv(const std::string& path, HslScene& scene, std::string& erro
|
|||||||
HomeWorld w;
|
HomeWorld w;
|
||||||
if (worldFromJson(wj, w)) scene.world = std::move(w);
|
if (worldFromJson(wj, w)) scene.world = std::move(w);
|
||||||
}
|
}
|
||||||
std::fprintf(stderr, "[scene] environment %s (%s): %zu meshes\n", m.name.c_str(), m.scene.empty() ? "from its homes" : "baked", scene.meshes.size());
|
std::fprintf(stderr, "[scene] environment %s (%s): %zu meshes, %zu splat objects\n", m.name.c_str(), m.scene.empty() ? "from its homes" : "baked",
|
||||||
|
scene.meshes.size(), scene.splats.size());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,13 +388,22 @@ bool loadHslScene(const std::vector<std::string>& apkPaths, HslScene& scene, std
|
|||||||
std::vector<std::unique_ptr<GltfLoader>> gltfs(apkPaths.size()); // the oldest custom homes (_world_model.gltf.ovrscene)
|
std::vector<std::unique_ptr<GltfLoader>> gltfs(apkPaths.size()); // the oldest custom homes (_world_model.gltf.ovrscene)
|
||||||
std::vector<char> ok(apkPaths.size(), 0);
|
std::vector<char> ok(apkPaths.size(), 0);
|
||||||
std::vector<std::vector<MeshData>> models(apkPaths.size()); // imported Blender / Unity models
|
std::vector<std::vector<MeshData>> models(apkPaths.size()); // imported Blender / Unity models
|
||||||
|
std::vector<SplatCloud> clouds(apkPaths.size()); // Gaussian splat captures
|
||||||
std::vector<std::string> modelErrors(apkPaths.size());
|
std::vector<std::string> modelErrors(apkPaths.size());
|
||||||
scene.sourceFiles.resize(apkPaths.size());
|
scene.sourceFiles.resize(apkPaths.size());
|
||||||
std::vector<std::thread> threads;
|
std::vector<std::thread> threads;
|
||||||
for (size_t i = 0; i < apkPaths.size(); ++i) {
|
for (size_t i = 0; i < apkPaths.size(); ++i) {
|
||||||
loaders[i] = std::make_unique<SceneLoader>();
|
loaders[i] = std::make_unique<SceneLoader>();
|
||||||
loaders[i]->verbose = std::getenv("FH_VERBOSE") != nullptr;
|
loaders[i]->verbose = std::getenv("FH_VERBOSE") != nullptr;
|
||||||
|
// (an exception leaving a thread ends the whole process: a damaged file's bad_alloc or length_error, or a
|
||||||
|
// parser's throw, fails this load instead)
|
||||||
threads.emplace_back([&, i] {
|
threads.emplace_back([&, i] {
|
||||||
|
try {
|
||||||
|
[&] {
|
||||||
|
if (isSplatPath(apkPaths[i])) { // (before models: a 3DGS capture is a .ply too)
|
||||||
|
if (loadSplats(apkPaths[i], clouds[i], modelErrors[i])) ok[i] = 5;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isModelPath(apkPaths[i])) {
|
if (isModelPath(apkPaths[i])) {
|
||||||
if (importModel(apkPaths[i], models[i], scene.sourceFiles[i], modelErrors[i])) ok[i] = 4;
|
if (importModel(apkPaths[i], models[i], scene.sourceFiles[i], modelErrors[i])) ok[i] = 4;
|
||||||
return;
|
return;
|
||||||
@@ -329,6 +415,14 @@ bool loadHslScene(const std::vector<std::string>& apkPaths, HslScene& scene, std
|
|||||||
gltfs[i] = std::make_unique<GltfLoader>(); // not .opa: a Clay .ovrscene home (glTF in scene.zip: SideQuest custom homes)?
|
gltfs[i] = std::make_unique<GltfLoader>(); // not .opa: a Clay .ovrscene home (glTF in scene.zip: SideQuest custom homes)?
|
||||||
if (gltfs[i]->load(apkPaths[i])) ok[i] = 3;
|
if (gltfs[i]->load(apkPaths[i])) ok[i] = 3;
|
||||||
else gltfs[i].reset();
|
else gltfs[i].reset();
|
||||||
|
}();
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
ok[i] = 0;
|
||||||
|
modelErrors[i] = "couldn't load " + apkPaths[i] + " (damaged? " + e.what() + ")";
|
||||||
|
} catch (...) {
|
||||||
|
ok[i] = 0;
|
||||||
|
modelErrors[i] = "couldn't load " + apkPaths[i] + " (damaged?)";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (auto& t : threads) t.join();
|
for (auto& t : threads) t.join();
|
||||||
@@ -340,20 +434,20 @@ bool loadHslScene(const std::vector<std::string>& apkPaths, HslScene& scene, std
|
|||||||
for (size_t i = 0; i < apkPaths.size(); ++i) {
|
for (size_t i = 0; i < apkPaths.size(); ++i) {
|
||||||
HslSourceInfo info;
|
HslSourceInfo info;
|
||||||
readManifest(apkPaths[i], info.package, info.version);
|
readManifest(apkPaths[i], info.package, info.version);
|
||||||
info.format = ok[i] == 1 ? "HSL" : ok[i] == 2 ? "Clay (.opa)" : ok[i] == 3 ? "Clay (.ovrscene)" : "Model";
|
info.format = ok[i] == 1 ? "HSL" : ok[i] == 2 ? "Clay (.opa)" : ok[i] == 3 ? "Clay (.ovrscene)" : ok[i] == 5 ? "Splats (" + clouds[i].format + ")" : "Model";
|
||||||
if (ok[i] == 4) {
|
if (ok[i] == 4 || ok[i] == 5) {
|
||||||
const std::string file = apkPaths[i].substr(apkPaths[i].find_last_of("/\\") + 1);
|
const std::string file = apkPaths[i].substr(apkPaths[i].find_last_of("/\\") + 1);
|
||||||
info.package = file;
|
info.package = file;
|
||||||
std::string ext = file.substr(file.find_last_of('.') + 1);
|
std::string ext = file.substr(file.find_last_of('.') + 1);
|
||||||
for (char& c : ext) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
for (char& c : ext) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||||
info.format = "Model (" + ext + ")";
|
if (ok[i] == 4) info.format = isSkyPanoramaPath(apkPaths[i]) ? "Panorama sky (" + ext + ")" : "Model (" + ext + ")";
|
||||||
}
|
}
|
||||||
// Split homes (Haven-era) ship com.meta.shell.env.footprint.<room> + ...vista.<world>; "prod.vista" is
|
// Split homes (Haven-era) ship com.meta.shell.env.footprint.<room> + ...vista.<world>; "prod.vista" is
|
||||||
// Cascadia, a whole home. Without a package, the file name decides.
|
// Cascadia, a whole home. Without a package, the file name decides.
|
||||||
std::string n = info.package.empty() ? apkPaths[i].substr(apkPaths[i].find_last_of("/\\") + 1) : info.package;
|
std::string n = info.package.empty() ? apkPaths[i].substr(apkPaths[i].find_last_of("/\\") + 1) : info.package;
|
||||||
for (char& c : n) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
for (char& c : n) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
const bool named = !info.package.empty();
|
const bool named = !info.package.empty();
|
||||||
info.role = ok[i] == 4 ? "Model" : ok[i] != 1 ? "Home"
|
info.role = ok[i] == 4 ? "Model" : ok[i] == 5 ? "Splats" : ok[i] != 1 ? "Home"
|
||||||
: n.find(named ? ".footprint." : "footprint") != std::string::npos ? "Footprint"
|
: n.find(named ? ".footprint." : "footprint") != std::string::npos ? "Footprint"
|
||||||
: n.find(named ? ".vista." : "vista") != std::string::npos ? "Vista" : "Home";
|
: n.find(named ? ".vista." : "vista") != std::string::npos ? "Vista" : "Home";
|
||||||
scene.sources.push_back(info);
|
scene.sources.push_back(info);
|
||||||
@@ -367,11 +461,28 @@ bool loadHslScene(const std::vector<std::string>& apkPaths, HslScene& scene, std
|
|||||||
if (j != i) loaders[i]->applyLightmapOverrides(loaders[i]->meshes, loaders[j].get());
|
if (j != i) loaders[i]->applyLightmapOverrides(loaders[i]->meshes, loaders[j].get());
|
||||||
}
|
}
|
||||||
size_t lightmapped = 0;
|
size_t lightmapped = 0;
|
||||||
|
// Models and splats beside a Clay / glTF home: it still animates (its meshes come first; splats have none).
|
||||||
|
const long extras = static_cast<long>(std::count(ok.begin(), ok.end(), 4) + std::count(ok.begin(), ok.end(), 5));
|
||||||
for (size_t i = 0; i < loaders.size(); ++i) {
|
for (size_t i = 0; i < loaders.size(); ++i) {
|
||||||
|
if (ok[i] == 5) { // splats: an object of their own, as the file has them
|
||||||
|
HslSplatObject o;
|
||||||
|
const std::string file = apkPaths[i].substr(apkPaths[i].find_last_of("/\\") + 1);
|
||||||
|
o.name = file.substr(0, file.find_last_of('.'));
|
||||||
|
o.source = static_cast<int>(i);
|
||||||
|
o.splats = std::make_shared<const std::vector<Splat>>(std::move(clouds[i].splats));
|
||||||
|
std::fprintf(stderr, "[scene] splats %s: %zu (%s)\n", apkPaths[i].c_str(), o.splats->size(), clouds[i].format.c_str());
|
||||||
|
scene.splats.push_back(std::move(o));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (ok[i] == 4) { // imported model: the built-in unlit programs
|
if (ok[i] == 4) { // imported model: the built-in unlit programs
|
||||||
addClayPrograms(scene);
|
addClayPrograms(scene);
|
||||||
for (MeshData& md : models[i]) {
|
for (MeshData& md : models[i]) {
|
||||||
md.shaderPath = md.alphaTest ? kClayCutoutProgram : kClayProgram;
|
if (showsVertexColours(apkPaths[i], md)) {
|
||||||
|
addVColorPrograms(scene);
|
||||||
|
md.shaderPath = md.alphaTest ? kClayVColorCutoutProgram : kClayVColorProgram;
|
||||||
|
} else {
|
||||||
|
md.shaderPath = md.alphaTest ? kClayCutoutProgram : kClayProgram;
|
||||||
|
}
|
||||||
scene.meshes.push_back(std::move(md));
|
scene.meshes.push_back(std::move(md));
|
||||||
scene.meshSource.push_back(static_cast<int>(i));
|
scene.meshSource.push_back(static_cast<int>(i));
|
||||||
}
|
}
|
||||||
@@ -381,7 +492,7 @@ bool loadHslScene(const std::vector<std::string>& apkPaths, HslScene& scene, std
|
|||||||
if (clays[i] || gltfs[i]) { // Clay / glTF home: built-in programs
|
if (clays[i] || gltfs[i]) { // Clay / glTF home: built-in programs
|
||||||
addClayPrograms(scene);
|
addClayPrograms(scene);
|
||||||
// The home animates here (below: the first home, the rest only models): its skinned meshes skin on the GPU.
|
// The home animates here (below: the first home, the rest only models): its skinned meshes skin on the GPU.
|
||||||
if (clays[i] && clays[i]->hasAnimation() && i == 0 && std::count(ok.begin(), ok.end(), 4) + 1 == static_cast<long>(apkPaths.size()))
|
if (clays[i] && clays[i]->hasAnimation() && i == 0 && extras + 1 == static_cast<long>(apkPaths.size()))
|
||||||
clays[i]->prepareGpuSkin();
|
clays[i]->prepareGpuSkin();
|
||||||
// Base textures: one shared, still-compressed source per owner. The loader lets meshes cut from the same
|
// Base textures: one shared, still-compressed source per owner. The loader lets meshes cut from the same
|
||||||
// part BORROW the owner's texture (texShareSrc) and empties their own copy — unresolved, those uploaded
|
// part BORROW the owner's texture (texShareSrc) and empties their own copy — unresolved, those uploaded
|
||||||
@@ -454,7 +565,7 @@ bool loadHslScene(const std::vector<std::string>& apkPaths, HslScene& scene, std
|
|||||||
// The loader keeps evaluating the animation (anim_bake.h: as a .fenv's does); it works on the scene's copy of its
|
// The loader keeps evaluating the animation (anim_bake.h: as a .fenv's does); it works on the scene's copy of its
|
||||||
// meshes (a single Clay / glTF home: the scene's meshes ARE that loader's meshes, same order).
|
// meshes (a single Clay / glTF home: the scene's meshes ARE that loader's meshes, same order).
|
||||||
// Models may follow the home: they sit after its meshes, which the animator indexes from 0.
|
// Models may follow the home: they sit after its meshes, which the animator indexes from 0.
|
||||||
if (i == 0 && std::count(ok.begin(), ok.end(), 4) + 1 == static_cast<long>(apkPaths.size())) {
|
if (i == 0 && extras + 1 == static_cast<long>(apkPaths.size())) {
|
||||||
if (clays[i] && clays[i]->hasAnimation()) {
|
if (clays[i] && clays[i]->hasAnimation()) {
|
||||||
std::shared_ptr<OpaLoader> L(std::move(clays[i]));
|
std::shared_ptr<OpaLoader> L(std::move(clays[i]));
|
||||||
scene.animDuration = L->animDuration();
|
scene.animDuration = L->animDuration();
|
||||||
@@ -496,6 +607,15 @@ bool loadHslScene(const std::vector<std::string>& apkPaths, HslScene& scene, std
|
|||||||
}
|
}
|
||||||
scanSceneZip(apkPaths[i], scene);
|
scanSceneZip(apkPaths[i], scene);
|
||||||
}
|
}
|
||||||
|
{ // A panorama sky chosen in Studio (model_import.h) takes the place of the home's own sky: that one draws nothing.
|
||||||
|
// (emptied, not removed: every mesh keeps its index, which Studio's edits go by)
|
||||||
|
auto fromModel = [&](size_t m) { return m < scene.meshSource.size() && scene.meshSource[m] >= 0 && ok[scene.meshSource[m]] == 4; };
|
||||||
|
bool chosenSky = false;
|
||||||
|
for (size_t m = 0; m < scene.meshes.size(); ++m) chosenSky |= scene.meshes[m].isSkybox && fromModel(m);
|
||||||
|
for (size_t m = 0; chosenSky && m < scene.meshes.size(); ++m)
|
||||||
|
if (scene.meshes[m].isSkybox && !fromModel(m)) { scene.meshes[m].indices.clear(); scene.meshes[m].nIdx = 0; }
|
||||||
|
}
|
||||||
|
borrowMissingPrograms(scene);
|
||||||
// The ambience is the vista's (the world outside you hear: Calming's, Central's), the footprint's only without one
|
// The ambience is the vista's (the world outside you hear: Calming's, Central's), the footprint's only without one
|
||||||
// (Haven's footprint carries a stand-in bed of its own, Central's).
|
// (Haven's footprint carries a stand-in bed of its own, Central's).
|
||||||
for (auto p = apkPaths.rbegin(); p != apkPaths.rend(); ++p)
|
for (auto p = apkPaths.rbegin(); p != apkPaths.rend(); ++p)
|
||||||
@@ -510,11 +630,36 @@ bool loadHslScene(const std::vector<std::string>& apkPaths, HslScene& scene, std
|
|||||||
apkPaths.size(), scene.meshes.size(), lightmapped, scene.programs.size(),
|
apkPaths.size(), scene.meshes.size(), lightmapped, scene.programs.size(),
|
||||||
scene.hasEnvAmbient ? "from env" : "synthesized", scene.ambientRGB[0], scene.ambientRGB[1], scene.ambientRGB[2],
|
scene.hasEnvAmbient ? "from env" : "synthesized", scene.ambientRGB[0], scene.ambientRGB[1], scene.ambientRGB[2],
|
||||||
std::chrono::duration<double>(t1 - t0).count(), std::chrono::duration<double>(t2 - t1).count(), scene.anims.size());
|
std::chrono::duration<double>(t1 - t0).count(), std::chrono::duration<double>(t2 - t1).count(), scene.anims.size());
|
||||||
if (scene.meshes.empty()) { error = "home has no meshes"; return false; }
|
if (scene.meshes.empty() && scene.splats.empty()) { error = "home has no meshes"; return false; }
|
||||||
simplifyWorldMeshes(scene.world);
|
simplifyWorldMeshes(scene.world);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addSplatMembers(const std::vector<HslSplatObject>& objects, FenvManifest& m, std::vector<FenvMember>& members) {
|
||||||
|
std::set<std::string> taken;
|
||||||
|
for (const HslSplatObject& o : objects) {
|
||||||
|
if (!o.splats || o.splats->empty()) continue;
|
||||||
|
// its member from its name: what any file system and zip tool takes, and a name of its own
|
||||||
|
std::string base;
|
||||||
|
for (char c : o.name) base += std::isalnum(static_cast<unsigned char>(c)) || c == '-' || c == '_' ? c : '_';
|
||||||
|
if (base.empty()) base = "splats";
|
||||||
|
std::string file = "splats/" + base + ".spz";
|
||||||
|
for (int k = 2; !taken.insert(file).second; ++k) file = "splats/" + base + "-" + std::to_string(k) + ".spz";
|
||||||
|
FenvSplats e;
|
||||||
|
e.file = file;
|
||||||
|
e.name = o.name;
|
||||||
|
e.count = o.splats->size();
|
||||||
|
std::memcpy(e.xform, o.worldFromModel, sizeof e.xform);
|
||||||
|
m.splats.push_back(e);
|
||||||
|
FenvMember f;
|
||||||
|
f.name = file;
|
||||||
|
f.bytes = writeSpz(*o.splats);
|
||||||
|
f.store = true; // gzip already
|
||||||
|
members.push_back(std::move(f));
|
||||||
|
}
|
||||||
|
if (!m.splats.empty() && std::find(m.needs.begin(), m.needs.end(), "splats") == m.needs.end()) m.needs.push_back("splats");
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<WorldCollider> geometryColliders(const HslScene& scene) {
|
std::vector<WorldCollider> geometryColliders(const HslScene& scene) {
|
||||||
std::vector<WorldCollider> out;
|
std::vector<WorldCollider> out;
|
||||||
for (const MeshData& md : scene.meshes) {
|
for (const MeshData& md : scene.meshes) {
|
||||||
@@ -763,8 +908,8 @@ std::shared_ptr<const WalkGrid> bakeWalkGrid(const HslScene& scene, const HomeWo
|
|||||||
// the home was read from (path, size, time) and its world layer, so any change bakes afresh. Under
|
// the home was read from (path, size, time) and its world layer, so any change bakes afresh. Under
|
||||||
// %LOCALAPPDATA%\FrameStudio\cache\walk or ~/.cache/frame-home/walk; the 300 most recent are kept.
|
// %LOCALAPPDATA%\FrameStudio\cache\walk or ~/.cache/frame-home/walk; the 300 most recent are kept.
|
||||||
static std::filesystem::path walkCacheDir() {
|
static std::filesystem::path walkCacheDir() {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32 // (wide: the narrow getenv is the ANSI code page, which u8path throws on)
|
||||||
if (const char* l = std::getenv("LOCALAPPDATA")) return std::filesystem::u8path(std::string(l) + "\\FrameStudio\\cache\\walk");
|
if (const wchar_t* l = _wgetenv(L"LOCALAPPDATA")) return std::filesystem::path(l) / L"FrameStudio" / L"cache" / L"walk";
|
||||||
#endif
|
#endif
|
||||||
if (const char* x = std::getenv("XDG_CACHE_HOME")) return std::filesystem::u8path(std::string(x) + "/frame-home/walk");
|
if (const char* x = std::getenv("XDG_CACHE_HOME")) return std::filesystem::u8path(std::string(x) + "/frame-home/walk");
|
||||||
const char* h = std::getenv("HOME");
|
const char* h = std::getenv("HOME");
|
||||||
|
|||||||
@@ -8,9 +8,11 @@
|
|||||||
#include "core/world.h"
|
#include "core/world.h"
|
||||||
#include "walkgrid.h"
|
#include "walkgrid.h"
|
||||||
#include "loaders/rendskel_parser.h"
|
#include "loaders/rendskel_parser.h"
|
||||||
|
#include "splat_import.h"
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -35,6 +37,16 @@ struct HslAnimGroup {
|
|||||||
std::vector<int> meshes; // HslScene::meshes indices
|
std::vector<int> meshes; // HslScene::meshes indices
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A Gaussian splat capture (splat_import.h) placed in the home: drawn after the solid meshes, blended back to front.
|
||||||
|
// Not walkable (the walkable area and the colliders never see it). A .fenv carries each as splats/<name>.spz.
|
||||||
|
struct HslSplatObject {
|
||||||
|
std::string name;
|
||||||
|
std::shared_ptr<const std::vector<Splat>> splats; // (shared: a copy of the scene, a save's, doesn't copy them)
|
||||||
|
float worldFromModel[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; // column-major
|
||||||
|
bool hidden = false; // not drawn (Frame Studio: hidden, deleted, or something else shown alone)
|
||||||
|
int source = -1; // apkPaths index of the file it was opened from (-1: a .fenv's)
|
||||||
|
};
|
||||||
|
|
||||||
// What one APK of the home is (the editor's info panel).
|
// What one APK of the home is (the editor's info panel).
|
||||||
struct HslSourceInfo {
|
struct HslSourceInfo {
|
||||||
std::string package, version; // from AndroidManifest.xml: com.meta.shell.env.footprint.haven2025, 206.0.0.0.70
|
std::string package, version; // from AndroidManifest.xml: com.meta.shell.env.footprint.haven2025, 206.0.0.0.70
|
||||||
@@ -46,6 +58,7 @@ struct HslScene {
|
|||||||
std::vector<std::string> apkPaths; // footprint first, then the vista (if any)
|
std::vector<std::string> apkPaths; // footprint first, then the vista (if any)
|
||||||
std::vector<MeshData> meshes; // all APKs' meshes, in apkPaths order
|
std::vector<MeshData> meshes; // all APKs' meshes, in apkPaths order
|
||||||
std::vector<int> meshSource; // per mesh: index into apkPaths
|
std::vector<int> meshSource; // per mesh: index into apkPaths
|
||||||
|
std::vector<HslSplatObject> splats; // Gaussian splat objects (a splat file opened, or a .fenv's)
|
||||||
std::vector<HslAnimGroup> anims;
|
std::vector<HslAnimGroup> anims;
|
||||||
std::map<std::string, ShaderProgramSrc> programs; // by asset path
|
std::map<std::string, ShaderProgramSrc> programs; // by asset path
|
||||||
float ambientRGB[3] = {0.95f, 0.90f, 0.83f}; // radiance; replaced by the env's .lprb L00 when present
|
float ambientRGB[3] = {0.95f, 0.90f, 0.83f}; // radiance; replaced by the env's .lprb L00 when present
|
||||||
@@ -98,5 +111,11 @@ std::vector<WalkSource> drawnWalkSources(const HslScene& scene, const HomeWorld&
|
|||||||
// Is this mesh in the walkable-area bake: 0 no, 1 solid, 2 blocks but isn't floor (see-through).
|
// Is this mesh in the walkable-area bake: 0 no, 1 solid, 2 blocks but isn't floor (see-through).
|
||||||
int walkMeshUse(const MeshData& md);
|
int walkMeshUse(const MeshData& md);
|
||||||
|
|
||||||
|
// Splat objects as .fenv members (splats/<name>.spz, stored: .spz is gzip already) and their manifest entries; the
|
||||||
|
// manifest then says it needs "splats" (none: nothing added).
|
||||||
|
struct FenvManifest;
|
||||||
|
struct FenvMember;
|
||||||
|
void addSplatMembers(const std::vector<HslSplatObject>& objects, FenvManifest& m, std::vector<FenvMember>& members);
|
||||||
|
|
||||||
// Finds the program a mesh's material references (exact asset path, else same ".surface" name).
|
// Finds the program a mesh's material references (exact asset path, else same ".surface" name).
|
||||||
const ShaderProgramSrc* findProgram(const HslScene& scene, const std::string& shaderPath);
|
const ShaderProgramSrc* findProgram(const HslScene& scene, const std::string& shaderPath);
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
// right stick forward aims a teleport arc from the right hand, drawn by Frame Home itself (guide(), guide_geom.h)
|
// right stick forward aims a teleport arc from the right hand, drawn by Frame Home itself (guide(), guide_geom.h)
|
||||||
// with a marker where you'll land. The arc stops at the first thing it meets: a floor you can stand on (or a
|
// with a marker where you'll land. The arc stops at the first thing it meets: a floor you can stand on (or a
|
||||||
// seat) is the landing; a wall, a ceiling or the side of furniture means nowhere — unless it's low, then you land
|
// seat) is the landing; a wall, a ceiling or the side of furniture means nowhere — unless it's low, then you land
|
||||||
// at its foot. Let go to go there: instantly behind a quick fade to black (a blink), or as a 0.25 s
|
// at its foot. Let go to go there: instantly behind a quick fade to black (a blink: SteamVR's fade, or in system
|
||||||
|
// mode Frame Home's own through the layer, setOwnFade / blinkLevel), or as a 0.25 s
|
||||||
// eased dash along the ground (slide). Landing near a teleport hotspot snaps onto it. Teleport off: the stick
|
// eased dash along the ground (slide). Landing near a teleport hotspot snaps onto it. Teleport off: the stick
|
||||||
// forward does nothing. In gravity homes a slide is an instant blink (the walker re-orients you on landing).
|
// forward does nothing. In gravity homes a slide is an instant blink (the walker re-orients you on landing).
|
||||||
// seats show a faint marker near you; pointing a hand at one lights it up (with a short pointer line), and pulling
|
// seats show a faint marker near you; pointing a hand at one lights it up (with a short pointer line), and pulling
|
||||||
@@ -212,6 +213,30 @@ public:
|
|||||||
// Frame Home draws the guide itself: the SteamVR ring overlay only stands in when it can't (fallback = true).
|
// Frame Home draws the guide itself: the SteamVR ring overlay only stands in when it can't (fallback = true).
|
||||||
void setRingFallback(bool on) { ringOn_ = on; if (!on && ring_ && vr::VROverlay()) vr::VROverlay()->HideOverlay(ring_); }
|
void setRingFallback(bool on) { ringOn_ = on; if (!on && ring_ && vr::VROverlay()) vr::VROverlay()->HideOverlay(ring_); }
|
||||||
bool sliding() const { return slideT_ >= 0; }
|
bool sliding() const { return slideT_ >= 0; }
|
||||||
|
// This frame's smooth turn (rad/s, + = left; 0 for a snap, which is a jump, not motion to carry on). The layer turns
|
||||||
|
// a frame it shows again by it (env_share.h Motion).
|
||||||
|
float turnRate() const { return turnRate_; }
|
||||||
|
// System mode: SteamVR's fade is the scene app's, so a blink fades Frame Home's own picture instead (main.cpp
|
||||||
|
// multiplies blinkLevel() into the brightness the layer draws with).
|
||||||
|
void setOwnFade(bool on) { ownFade_ = on; }
|
||||||
|
// A blink's brightness now: 1 open, down to 0 over kBlinkOut, black as you land, back up over kBlinkIn.
|
||||||
|
float blinkLevel() const {
|
||||||
|
if (blinkT_ >= 0) return std::clamp(blinkT_ / kBlinkOut, 0.0f, 1.0f);
|
||||||
|
return blinkInT_ > 0 ? 1 - blinkInT_ / kBlinkIn : 1.0f;
|
||||||
|
}
|
||||||
|
// How far ahead of the eyes the view meets something (m, 1..8; 8 when nothing does, 4 in a home with nothing to
|
||||||
|
// walk on): the layer's guess at depth when it carries a frame on for walking (env_share.h Motion).
|
||||||
|
float viewDepth(const float headT[16]) const {
|
||||||
|
if (!runtime_.hasFloor()) return 4.0f;
|
||||||
|
float R[16], o[3], f[3];
|
||||||
|
rig(R);
|
||||||
|
apply(R, &headT[12], o);
|
||||||
|
const float hz[3] = {-headT[8] * 8, -headT[9] * 8, -headT[10] * 8}; // the head's -Z, 8 m of it
|
||||||
|
dir(R, hz, f);
|
||||||
|
float t, n[3];
|
||||||
|
if (runtime_.trace(o, f, t, n) == WorldRuntime::None) return 8.0f;
|
||||||
|
return std::clamp(t * 8, 1.0f, 8.0f);
|
||||||
|
}
|
||||||
|
|
||||||
// worldFromTracking (column-major).
|
// worldFromTracking (column-major).
|
||||||
void rig(float m[16]) const {
|
void rig(float m[16]) const {
|
||||||
@@ -228,6 +253,8 @@ public:
|
|||||||
// Once per frame, with the head's tracking-space pose (column-major, as from OpenVR).
|
// Once per frame, with the head's tracking-space pose (column-major, as from OpenVR).
|
||||||
void update(float dt, const float headT[16]) {
|
void update(float dt, const float headT[16]) {
|
||||||
dt = std::min(dt, 0.05f);
|
dt = std::min(dt, 0.05f);
|
||||||
|
turnRate_ = 0;
|
||||||
|
if (blinkInT_ > 0) blinkInT_ = std::max(0.0f, blinkInT_ - dt);
|
||||||
checkEyeHeight(dt, headT[13]);
|
checkEyeHeight(dt, headT[13]);
|
||||||
guide_.seats.clear();
|
guide_.seats.clear();
|
||||||
guide_.pointer = false;
|
guide_.pointer = false;
|
||||||
@@ -277,13 +304,17 @@ public:
|
|||||||
float lx = 0, ly = 0;
|
float lx = 0, ly = 0;
|
||||||
if (walk) grav::shapeStick(l.x, l.y, lx, ly);
|
if (walk) grav::shapeStick(l.x, l.y, lx, ly);
|
||||||
if (gravityMode_) { updateGravity(dt, headT, lx, ly, r); return; }
|
if (gravityMode_) { updateGravity(dt, headT, lx, ly, r); return; }
|
||||||
// Walking and teleport both off: the stick still stands you up from a seat (else only stepping away would)
|
// Seated, the stick stands you up (walking and teleport both off too: else only stepping away would). Only
|
||||||
if (seated_ >= 0 && walk_ == 0 && teleport_ == 0 && l.bActive) {
|
// once it has come back to centre since you sat, and pushed well past the deadzone: a stick still held
|
||||||
float sx = 0, sy = 0;
|
// from walking there, or a worn one drifting, would jump you straight back out of the seat.
|
||||||
grav::shapeStick(l.x, l.y, sx, sy);
|
if (seated_ >= 0) {
|
||||||
if ((sx != 0 || sy != 0) && slideT_ < 0 && blinkT_ < 0) { leaveSeat(headT, head, R); rig(R); apply(R, &headT[12], head); }
|
const float m = l.bActive ? std::hypot(l.x, l.y) : 0.0f;
|
||||||
|
if (m < 0.2f) standArmed_ = true;
|
||||||
|
if (standArmed_ && m > 0.6f && (walk || (walk_ == 0 && teleport_ == 0 && l.bActive)) && slideT_ < 0 && blinkT_ < 0) {
|
||||||
|
leaveSeat(headT, head, R); rig(R); apply(R, &headT[12], head);
|
||||||
|
}
|
||||||
|
lx = ly = 0; // (the push that stood you up doesn't also walk you this frame)
|
||||||
}
|
}
|
||||||
if ((lx != 0 || ly != 0) && seated_ >= 0) { leaveSeat(headT, head, R); rig(R); apply(R, &headT[12], head); }
|
|
||||||
// The velocity eases toward the stick's (grav::approach); a jump or a seat stops it dead.
|
// The velocity eases toward the stick's (grav::approach); a jump or a seat stops it dead.
|
||||||
grav::V want;
|
grav::V want;
|
||||||
if (lx != 0 || ly != 0) {
|
if (lx != 0 || ly != 0) {
|
||||||
@@ -307,7 +338,7 @@ public:
|
|||||||
float f;
|
float f;
|
||||||
if (!runtime_.hasFloor() || runtime_.floorAt(x, z, floorY_, 0.45f, 1.2f, f)) { // never walk off an edge
|
if (!runtime_.hasFloor() || runtime_.floorAt(x, z, floorY_, 0.45f, 1.2f, f)) { // never walk off an edge
|
||||||
rigX_ += x - head[0]; rigZ_ += z - head[2];
|
rigX_ += x - head[0]; rigZ_ += z - head[2];
|
||||||
vel_[0] = (x - head[0]) / dt; vel_[1] = (z - head[2]) / dt; // what a wall left of it: no pent-up push
|
if (dt > 0) { vel_[0] = (x - head[0]) / dt; vel_[1] = (z - head[2]) / dt; } // what a wall left of it: no pent-up push (a 0 s frame: NaN)
|
||||||
head[0] = x; head[2] = z;
|
head[0] = x; head[2] = z;
|
||||||
} else {
|
} else {
|
||||||
vel_[0] = vel_[1] = 0;
|
vel_[0] = vel_[1] = 0;
|
||||||
@@ -485,8 +516,10 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sits in seat i: the head over the seat, still facing the way you were, eyes kSeatedEye over its surface (whatever
|
// Sits in seat i: the head over the seat, turned to face the seat's way (its yaw, as Frame Studio's arrow shows it:
|
||||||
// your real height: you may be standing in the room).
|
// 0 faces -Z), eyes kSeatedEye over its surface (whatever your real height: you may be standing in the room).
|
||||||
|
// The turn is the rig's, about the head (as a snap turn), so the way your head points in the room ends up along the
|
||||||
|
// seat's front and the head stays over the seat.
|
||||||
static constexpr float kSeatedEye = 0.78f;
|
static constexpr float kSeatedEye = 0.78f;
|
||||||
static constexpr float kArcSpeed = 10.0f; // the teleport arc's launch speed (m/s)
|
static constexpr float kArcSpeed = 10.0f; // the teleport arc's launch speed (m/s)
|
||||||
static constexpr float kHeadClear = 0.15f; // the eyes never come closer than this to anything solid (m)
|
static constexpr float kHeadClear = 0.15f; // the eyes never come closer than this to anything solid (m)
|
||||||
@@ -495,9 +528,30 @@ private:
|
|||||||
rigX_ += st.pos[0] - head[0];
|
rigX_ += st.pos[0] - head[0];
|
||||||
rigZ_ += st.pos[2] - head[2];
|
rigZ_ += st.pos[2] - head[2];
|
||||||
head[0] = st.pos[0]; head[2] = st.pos[2];
|
head[0] = st.pos[0]; head[2] = st.pos[2];
|
||||||
(void)R;
|
{ // the head's forward on the floor (looking straight down or up: the top of the head points the way you face)
|
||||||
|
float f[3];
|
||||||
|
const float hz[3] = {-headT[8], -headT[9], -headT[10]};
|
||||||
|
dir(R, hz, f);
|
||||||
|
if (std::hypot(f[0], f[2]) < 0.2f) {
|
||||||
|
float u[3];
|
||||||
|
const float hy[3] = {headT[4], headT[5], headT[6]};
|
||||||
|
dir(R, hy, u);
|
||||||
|
const float sg = f[1] < 0 ? 1.0f : -1.0f;
|
||||||
|
f[0] = u[0] * sg; f[2] = u[2] * sg;
|
||||||
|
}
|
||||||
|
if (std::hypot(f[0], f[2]) > 1e-4f) {
|
||||||
|
float a = st.yaw - std::atan2(-f[0], -f[2]);
|
||||||
|
a = std::remainder(a, 6.2831853f);
|
||||||
|
const float c = std::cos(a), s = std::sin(a);
|
||||||
|
const float ox = rigX_ - head[0], oz = rigZ_ - head[2];
|
||||||
|
rigX_ = head[0] + c * ox + s * oz;
|
||||||
|
rigZ_ = head[2] - s * ox + c * oz;
|
||||||
|
rigYaw_ = std::remainder(rigYaw_ + a, 6.2831853f);
|
||||||
|
}
|
||||||
|
}
|
||||||
seatRigY_ = st.pos[1] + kSeatedEye - headT[13];
|
seatRigY_ = st.pos[1] + kSeatedEye - headT[13];
|
||||||
seated_ = i;
|
seated_ = i;
|
||||||
|
standArmed_ = false; // the stick must come back to centre before it stands you up
|
||||||
std::fprintf(stderr, "frame-home: sitting in %s\n", st.name.c_str());
|
std::fprintf(stderr, "frame-home: sitting in %s\n", st.name.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,17 +651,26 @@ private:
|
|||||||
// held slightly off-centre shouldn't spin you).
|
// held slightly off-centre shouldn't spin you).
|
||||||
static constexpr float kTurnRate = 2.0944f; // 120 degrees/s
|
static constexpr float kTurnRate = 2.0944f; // 120 degrees/s
|
||||||
float turnBy(float dt, float rx, float ry) {
|
float turnBy(float dt, float rx, float ry) {
|
||||||
if (turn_ == 1) return aiming_ || std::fabs(ry) > 0.7f ? 0.0f : -dead(rx) * kTurnRate * dt;
|
if (turn_ == 1) { turnRate_ = aiming_ || std::fabs(ry) > 0.7f ? 0.0f : -dead(rx) * kTurnRate; return turnRate_ * dt; }
|
||||||
if (!turnArmed_ && std::fabs(rx) < 0.3f) turnArmed_ = true;
|
if (!turnArmed_ && std::fabs(rx) < 0.3f) turnArmed_ = true;
|
||||||
if (turnArmed_ && std::fabs(rx) > 0.75f && std::fabs(ry) < 0.6f) { turnArmed_ = false; return rx > 0 ? -snap_ : snap_; }
|
if (turnArmed_ && std::fabs(rx) > 0.75f && std::fabs(ry) < 0.6f) { turnArmed_ = false; return rx > 0 ? -snap_ : snap_; }
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instant teleports blink: fade to black, jump, fade back. In system mode Frame Home isn't the scene app and
|
// Instant teleports blink: fade to black, jump, fade back. In system mode Frame Home isn't the scene app and
|
||||||
// the compositor may ignore the fade; then the blink is just a 80 ms wait.
|
// the compositor ignores its fade (the blink was an unfaded 80 ms freeze, then the jump): there the fade is
|
||||||
static constexpr float kBlinkOut = 0.08f, kSlide = 0.25f;
|
// Frame Home's own (setOwnFade), blinkLevel() darkening the picture the layer draws.
|
||||||
bool blinkOut() const { if (!ok_ || !vr::VRCompositor()) return false; vr::VRCompositor()->FadeToColor(kBlinkOut, 0, 0, 0, 1); return true; }
|
static constexpr float kBlinkOut = 0.08f, kBlinkIn = 0.12f, kSlide = 0.25f;
|
||||||
void blinkIn() const { if (ok_ && vr::VRCompositor()) vr::VRCompositor()->FadeToColor(0.12f, 0, 0, 0, 0); }
|
bool blinkOut() const {
|
||||||
|
if (ownFade_) return true;
|
||||||
|
if (!ok_ || !vr::VRCompositor()) return false;
|
||||||
|
vr::VRCompositor()->FadeToColor(kBlinkOut, 0, 0, 0, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void blinkIn() {
|
||||||
|
if (ownFade_) { blinkInT_ = kBlinkIn; return; }
|
||||||
|
if (ok_ && vr::VRCompositor()) vr::VRCompositor()->FadeToColor(kBlinkIn, 0, 0, 0, 0);
|
||||||
|
}
|
||||||
static float ease(float t) { return t * t * (3 - 2 * t); }
|
static float ease(float t) { return t * t * (3 - 2 * t); }
|
||||||
|
|
||||||
// Off to jump_ (and into seat jumpSeat_ when >= 0): a 0.25 s dash (slide), or a blink — instant behind a fade.
|
// Off to jump_ (and into seat jumpSeat_ when >= 0): a 0.25 s dash (slide), or a blink — instant behind a fade.
|
||||||
@@ -662,6 +725,7 @@ private:
|
|||||||
float fallV_ = 0, fallen_ = 0, fallLimit_ = 30;
|
float fallV_ = 0, fallen_ = 0, fallLimit_ = 30;
|
||||||
int respawns_ = 0;
|
int respawns_ = 0;
|
||||||
int seated_ = -1;
|
int seated_ = -1;
|
||||||
|
bool standArmed_ = false; // seated: the left stick has been back at centre
|
||||||
float seatRigY_ = 0;
|
float seatRigY_ = 0;
|
||||||
bool ownInput_ = false;
|
bool ownInput_ = false;
|
||||||
bool menuAim_ = false;
|
bool menuAim_ = false;
|
||||||
@@ -697,6 +761,9 @@ private:
|
|||||||
float jump_[3] = {0, 0, 0}, jumpN_[3] = {0, 1, 0};
|
float jump_[3] = {0, 0, 0}, jumpN_[3] = {0, 1, 0};
|
||||||
int jumpSeat_ = -1;
|
int jumpSeat_ = -1;
|
||||||
float blinkT_ = -1, slideT_ = -1, slideDX_ = 0, slideDZ_ = 0, slideY0_ = 0;
|
float blinkT_ = -1, slideT_ = -1, slideDX_ = 0, slideDZ_ = 0, slideY0_ = 0;
|
||||||
|
float blinkInT_ = 0; // own fade: a blink's fade back in still to go (s)
|
||||||
|
bool ownFade_ = false;
|
||||||
|
float turnRate_ = 0; // this frame's smooth turn (rad/s)
|
||||||
bool test_ = false, testAimOk_ = false, testLeftOk_ = false, testTrig_[2] = {false, false};
|
bool test_ = false, testAimOk_ = false, testLeftOk_ = false, testTrig_[2] = {false, false};
|
||||||
float testIn_[4] = {}, testAim_[12] = {}, testLeft_[12] = {};
|
float testIn_[4] = {}, testAim_[12] = {}, testLeft_[12] = {};
|
||||||
|
|
||||||
@@ -820,7 +887,10 @@ private:
|
|||||||
const WorldCollider* shape = nullptr;
|
const WorldCollider* shape = nullptr;
|
||||||
const WorldRuntime::Hit h = runtime_.trace(p, seg, t, n, &shape);
|
const WorldRuntime::Hit h = runtime_.trace(p, seg, t, n, &shape);
|
||||||
const int si = seatOnSegment(p, seg, ts);
|
const int si = seatOnSegment(p, seg, ts);
|
||||||
if (si >= 0 && (h == WorldRuntime::None || ts <= t)) {
|
// only an arc coming down onto the top of a seat takes it: one passing a raised stool on its way up, or
|
||||||
|
// through its side on the way to the floor beside it, flies on
|
||||||
|
if (si >= 0 && seg[1] < 0 && p[1] + seg[1] * ts > world_.seats[si].pos[1] + kSeatLift &&
|
||||||
|
(h == WorldRuntime::None || ts <= t)) {
|
||||||
const float q[3] = {p[0] + seg[0] * ts, p[1] + seg[1] * ts, p[2] + seg[2] * ts};
|
const float q[3] = {p[0] + seg[0] * ts, p[1] + seg[1] * ts, p[2] + seg[2] * ts};
|
||||||
keep(q);
|
keep(q);
|
||||||
seat_ = si;
|
seat_ = si;
|
||||||
@@ -831,7 +901,9 @@ private:
|
|||||||
if (h != WorldRuntime::None) {
|
if (h != WorldRuntime::None) {
|
||||||
const float q[3] = {p[0] + seg[0] * t, p[1] + seg[1] * t, p[2] + seg[2] * t};
|
const float q[3] = {p[0] + seg[0] * t, p[1] + seg[1] * t, p[2] + seg[2] * t};
|
||||||
keep(q);
|
keep(q);
|
||||||
if (h == WorldRuntime::Block && (seat_ = seatOf(q, shape)) >= 0) { // the chair's back or arm: the chair
|
// the chair's own back or arm (its collider centred on the seat): the chair. Not a desk or table beside it:
|
||||||
|
// aiming at one of those lands by it.
|
||||||
|
if (h == WorldRuntime::Block && shape && (seat_ = seatNear(&shape->world[12])) >= 0) {
|
||||||
seatTarget(seat_, target_);
|
seatTarget(seat_, target_);
|
||||||
valid_ = true;
|
valid_ = true;
|
||||||
} else if (runtime_.landingFor(h, q, n, target_)) {
|
} else if (runtime_.landingFor(h, q, n, target_)) {
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ struct SystemPacer {
|
|||||||
uint64_t vsyncNs = 0; // a recent vsync
|
uint64_t vsyncNs = 0; // a recent vsync
|
||||||
static constexpr size_t kLeads = 240; // two seconds at 120 Hz
|
static constexpr size_t kLeads = 240; // two seconds at 120 Hz
|
||||||
double pct = 0.98, marginMs = 1.0;
|
double pct = 0.98, marginMs = 1.0;
|
||||||
|
int stride = 1; // compositor samples per frame: 2 at half rate (every other one)
|
||||||
// The lead to start with: a high percentile of recent make times + margin. More than a refresh (a frame that
|
// The lead to start with: a high percentile of recent make times + margin. More than a refresh (a frame that
|
||||||
// can't be made in one) aims two samples ahead: frames overlap (render() waits for the one before) and every
|
// can't be made in one) aims two samples ahead: frames overlap (render() waits for the one before) and every
|
||||||
// compositor frame still gets a new one, a refresh later.
|
// compositor frame still gets a new one, a refresh later.
|
||||||
@@ -356,16 +357,19 @@ struct SystemPacer {
|
|||||||
void plan(uint64_t now, float vsyncToPhotons, uint64_t& start, uint64_t& target, float& predict) {
|
void plan(uint64_t now, float vsyncToPhotons, uint64_t& start, uint64_t& target, float& predict) {
|
||||||
const double p = period * 1e9, lead = leadMs() * 1e6;
|
const double p = period * 1e9, lead = leadMs() * 1e6;
|
||||||
if (phaseNs < 0 || !vsyncNs) { // nothing seen yet: once a refresh, aimed two refreshes ahead
|
if (phaseNs < 0 || !vsyncNs) { // nothing seen yet: once a refresh, aimed two refreshes ahead
|
||||||
start = std::max(now, lastTarget + static_cast<uint64_t>(p));
|
start = std::max(now, lastTarget + static_cast<uint64_t>(p * stride));
|
||||||
target = start + static_cast<uint64_t>(p);
|
target = start + static_cast<uint64_t>(p);
|
||||||
lastTarget = start;
|
lastTarget = start;
|
||||||
predict = static_cast<float>(2 * period) + vsyncToPhotons;
|
predict = static_cast<float>(2 * period) + vsyncToPhotons;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The first sample far enough ahead to make a frame for, and never the same one twice.
|
// The first sample far enough ahead to make a frame for, and never the same one twice (at half rate, never the
|
||||||
|
// one straight after the last either: every other sample, locked to the display like full rate).
|
||||||
double t = static_cast<double>(vsyncNs) + phaseNs;
|
double t = static_cast<double>(vsyncNs) + phaseNs;
|
||||||
while (t < static_cast<double>(now) + lead) t += p;
|
// (whole periods at once: a vsync seen long ago, after the display slept, is many thousands of them back)
|
||||||
while (lastTarget && t < static_cast<double>(lastTarget) + p * 0.5) t += p;
|
if (t < static_cast<double>(now) + lead) t += std::ceil((static_cast<double>(now) + lead - t) / p) * p;
|
||||||
|
if (t < static_cast<double>(now) + lead) t += p;
|
||||||
|
while (lastTarget && t < static_cast<double>(lastTarget) + p * (stride - 0.5)) t += p;
|
||||||
target = static_cast<uint64_t>(t);
|
target = static_cast<uint64_t>(t);
|
||||||
start = target - static_cast<uint64_t>(lead);
|
start = target - static_cast<uint64_t>(lead);
|
||||||
lastTarget = target;
|
lastTarget = target;
|
||||||
@@ -377,7 +381,7 @@ struct SystemPacer {
|
|||||||
// System mode: publishes each frame to the compositor layer the moment the GPU has finished it, from a thread of its
|
// System mode: publishes each frame to the compositor layer the moment the GPU has finished it, from a thread of its
|
||||||
// own, so the next frame's CPU work overlaps this one's GPU work and no frame waits a refresh to go out.
|
// own, so the next frame's CPU work overlaps this one's GPU work and no frame waits a refresh to go out.
|
||||||
struct Publisher {
|
struct Publisher {
|
||||||
struct Job { uint64_t serial; int slot; float vp[2][16]; float eye[2][3]; uint64_t startNs, target; };
|
struct Job { uint64_t serial; int slot; float vp[2][16]; float eye[2][3]; uint64_t startNs, target; envshare::Motion motion; };
|
||||||
struct Done { uint64_t ns; double leadMs; bool late; };
|
struct Done { uint64_t ns; double leadMs; bool late; };
|
||||||
HslRenderer* r = nullptr;
|
HslRenderer* r = nullptr;
|
||||||
EnvExport* env = nullptr;
|
EnvExport* env = nullptr;
|
||||||
@@ -398,8 +402,16 @@ struct Publisher {
|
|||||||
const Job j = jobs.front();
|
const Job j = jobs.front();
|
||||||
working = true;
|
working = true;
|
||||||
l.unlock();
|
l.unlock();
|
||||||
while (!r->waitFrameSerial(j.serial, 100000000ull)) { std::lock_guard<std::mutex> g(m); if (stop) break; }
|
// A GPU that never finishes it (the device lost): after 3 s the frame is let go, so drain() and
|
||||||
env->publish(j.slot, j.vp, j.eye);
|
// the main loop (which notices the lost device and restarts) aren't held up forever.
|
||||||
|
bool finished = false;
|
||||||
|
for (int tries = 0; !finished && tries < 30; ++tries) {
|
||||||
|
finished = r->waitFrameSerial(j.serial, 100000000ull);
|
||||||
|
std::lock_guard<std::mutex> g(m);
|
||||||
|
if (stop) break;
|
||||||
|
}
|
||||||
|
if (finished) env->publish(j.slot, j.vp, j.eye, j.motion);
|
||||||
|
else LOG("publisher: frame %llu never finished on the GPU: not shown", static_cast<unsigned long long>(j.serial));
|
||||||
const uint64_t now = envshare::nowNs();
|
const uint64_t now = envshare::nowNs();
|
||||||
l.lock();
|
l.lock();
|
||||||
jobs.pop_front();
|
jobs.pop_front();
|
||||||
@@ -581,8 +593,23 @@ int runSnapshot(const std::string& out, const std::vector<std::string>& apks, co
|
|||||||
// FH_BENCH_MOVES=1 (with FH_BENCH_WALK): every 8 s, after 4 s of walking, a snap turn, a teleport ahead (aimed with the
|
// FH_BENCH_MOVES=1 (with FH_BENCH_WALK): every 8 s, after 4 s of walking, a snap turn, a teleport ahead (aimed with the
|
||||||
// real arc, landing at once) and a fast head turn (180 degrees in half a second): what a visitor does. The JSON's
|
// real arc, landing at once) and a fast head turn (180 degrees in half a second): what a visitor does. The JSON's
|
||||||
// "moves" says, for each, how many frames ran late and the slowest; "spikes" the slowest frames (s, ms, move).
|
// "moves" says, for each, how many frames ran late and the slowest; "spikes" the slowest frames (s, ms, move).
|
||||||
|
// Timing starts after a quarter second of untimed frames at the spawn (FH_BENCH_WARMUP=s), once the GPU's clock is up.
|
||||||
|
// FH_BENCH_TRACE=out.csv: every frame's times (CPU phases, GPU, uploads) and the GPU clock, one line each.
|
||||||
static long peakRamMB() { return homeos::peakRamMB(); }
|
static long peakRamMB() { return homeos::peakRamMB(); }
|
||||||
|
|
||||||
|
// The CPU time this thread has used, ns: a bench frame's own work (what cpuAvg can't tell apart: it includes the
|
||||||
|
// wait for the GPU's last frame). 0 where there's no per-thread clock.
|
||||||
|
static uint64_t threadCpuNs() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
FILETIME c, e, k, u;
|
||||||
|
if (!GetThreadTimes(GetCurrentThread(), &c, &e, &k, &u)) return 0;
|
||||||
|
return ((static_cast<uint64_t>(k.dwHighDateTime) << 32 | k.dwLowDateTime) + (static_cast<uint64_t>(u.dwHighDateTime) << 32 | u.dwLowDateTime)) * 100;
|
||||||
|
#else
|
||||||
|
timespec ts;
|
||||||
|
return clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts) == 0 ? static_cast<uint64_t>(ts.tv_sec) * 1000000000ull + ts.tv_nsec : 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
int runBench(const std::vector<std::string>& apks, double seconds, uint32_t w, uint32_t h, float hz, bool unpaced) {
|
int runBench(const std::vector<std::string>& apks, double seconds, uint32_t w, uint32_t h, float hz, bool unpaced) {
|
||||||
const fs::path settingsPath = rootDir() / "settings.json";
|
const fs::path settingsPath = rootDir() / "settings.json";
|
||||||
PerfSettings perf;
|
PerfSettings perf;
|
||||||
@@ -703,19 +730,77 @@ int runBench(const std::vector<std::string>& apks, double seconds, uint32_t w, u
|
|||||||
std::vector<View> views; // every frame's view and the GPU time that came back with it
|
std::vector<View> views; // every frame's view and the GPU time that came back with it
|
||||||
FrameProfile prof;
|
FrameProfile prof;
|
||||||
prof.on = true;
|
prof.on = true;
|
||||||
Window make, cpu, gpu, gap;
|
Window make, cpu, gpu, gap, work;
|
||||||
const double period = 1.0 / hz;
|
const double period = 1.0 / hz;
|
||||||
uint32_t over = 0, hitches = 0, frames = 0, earlyFrames = 0;
|
uint32_t over = 0, hitches = 0, frames = 0, earlyFrames = 0;
|
||||||
float animTime = 0;
|
float animTime = 0;
|
||||||
const uint64_t t0 = envshare::nowNs();
|
|
||||||
uint64_t next = t0, lastDone = 0, gpuSeen = r.gpuResults();
|
|
||||||
uint32_t slot = 0;
|
uint32_t slot = 0;
|
||||||
// FH_BENCH_FRAMES=n: exactly n frames, each 1/120 s after the one before (the same pictures every run: with
|
// FH_BENCH_FRAMES=n: exactly n frames, each 1/120 s after the one before (the same pictures every run: with
|
||||||
// FH_BENCH_SHOT=out.ppm, the last one is saved, so ways of making frames can be compared pixel for pixel).
|
// FH_BENCH_SHOT=out.ppm, the last one is saved, so ways of making frames can be compared pixel for pixel).
|
||||||
static const int fixedFrames = std::getenv("FH_BENCH_FRAMES") ? std::atoi(std::getenv("FH_BENCH_FRAMES")) : 0;
|
static const int fixedFrames = std::getenv("FH_BENCH_FRAMES") ? std::atoi(std::getenv("FH_BENCH_FRAMES")) : 0;
|
||||||
|
// Warm-up, untimed: the spawn view drawn at the display rate for a moment first. An idle GPU sits at its lowest
|
||||||
|
// clock (231-310 MHz on the Frame, of 903) until the governor's next look (every 50 ms) raises it, so a run's
|
||||||
|
// first frames took 3-4x as long and showed as 10-20 ms "spikes" at t=0 in every build. A shown home never draws
|
||||||
|
// those (it fades in). FH_BENCH_WARMUP=s (default 0.25; 0: none, the old way; never with FH_BENCH_FRAMES).
|
||||||
|
const double warmup = fixedFrames > 0 ? 0.0 : std::getenv("FH_BENCH_WARMUP") ? std::atof(std::getenv("FH_BENCH_WARMUP")) : 0.25;
|
||||||
|
if (warmup > 0) {
|
||||||
|
Mat4 pose = {};
|
||||||
|
pose.m[0] = std::cos(yaw0); pose.m[2] = -std::sin(yaw0);
|
||||||
|
pose.m[5] = 1;
|
||||||
|
pose.m[8] = std::sin(yaw0); pose.m[10] = std::cos(yaw0);
|
||||||
|
pose.m[15] = 1;
|
||||||
|
EyeView eyes[2];
|
||||||
|
for (int e = 0; e < 2; ++e) {
|
||||||
|
Mat4 p = pose;
|
||||||
|
for (int k = 0; k < 3; ++k) p.m[12 + k] = base[k] + pose.m[k] * (e ? 0.032f : -0.032f);
|
||||||
|
eyes[e] = eyeFromPose(p, proj);
|
||||||
|
}
|
||||||
|
const uint64_t w0 = envshare::nowNs();
|
||||||
|
uint64_t wn = w0;
|
||||||
|
while (envshare::nowNs() - w0 < static_cast<uint64_t>(warmup * 1e9) && !r.deviceLost()) {
|
||||||
|
if (!unpaced) { sleepUntilNs(wn); wn += static_cast<uint64_t>(period * 1e9); }
|
||||||
|
r.exportSlot = exported ? static_cast<int>(slot++ % static_cast<uint32_t>(envshare::kSlots)) : -1;
|
||||||
|
// (the CPU's animation too: its first frames took 5-25 ms while the CPU clock came up)
|
||||||
|
if (scene.animate && !scene.animatedMeshes.empty() && !std::getenv("FH_BENCH_NOANIM")) scene.animate(0, scene.meshes);
|
||||||
|
r.render(eyes, 0);
|
||||||
|
}
|
||||||
|
r.waitFrame();
|
||||||
|
}
|
||||||
|
const uint64_t t0 = envshare::nowNs();
|
||||||
|
uint64_t next = t0, lastDone = 0, gpuSeen = r.gpuResults();
|
||||||
|
// Latency, as system mode has it: from the pose (the frame's start) to the frame finished on the GPU (when the
|
||||||
|
// publisher hands it to the compositor), each frame's waited for on a thread of its own like Publisher does;
|
||||||
|
// and from the submit to finished (the GPU's queue and its work). Printed as "bench latency:" on stderr.
|
||||||
|
// (system mode reads the pose again inside render(): HslRenderer::latePose; here only the moment it would)
|
||||||
|
struct LatJob { uint64_t serial, startNs, lateNs, submitNs; };
|
||||||
|
std::mutex latM;
|
||||||
|
std::condition_variable latCv;
|
||||||
|
std::deque<LatJob> latJobs;
|
||||||
|
bool latStop = false;
|
||||||
|
Window latTotal, latLate, latGpu;
|
||||||
|
uint64_t lateNs = 0;
|
||||||
|
r.latePose = [&](EyeView*) { lateNs = envshare::nowNs(); return false; };
|
||||||
|
std::thread latThread([&] {
|
||||||
|
std::unique_lock<std::mutex> l(latM);
|
||||||
|
for (;;) {
|
||||||
|
latCv.wait(l, [&] { return latStop || !latJobs.empty(); });
|
||||||
|
if (latJobs.empty()) return;
|
||||||
|
const LatJob j = latJobs.front();
|
||||||
|
latJobs.pop_front();
|
||||||
|
l.unlock();
|
||||||
|
const bool ok = r.waitFrameSerial(j.serial, 1000000000ull);
|
||||||
|
const uint64_t doneNs = envshare::nowNs();
|
||||||
|
l.lock();
|
||||||
|
if (ok) {
|
||||||
|
latTotal.add((doneNs - j.startNs) / 1e6);
|
||||||
|
latLate.add((doneNs - (j.lateNs ? j.lateNs : j.startNs)) / 1e6);
|
||||||
|
latGpu.add((doneNs - j.submitNs) / 1e6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
while (fixedFrames > 0 ? frames < static_cast<uint32_t>(fixedFrames) : envshare::nowNs() - t0 < static_cast<uint64_t>(seconds * 1e9)) {
|
while (fixedFrames > 0 ? frames < static_cast<uint32_t>(fixedFrames) : envshare::nowNs() - t0 < static_cast<uint64_t>(seconds * 1e9)) {
|
||||||
if (!unpaced) { sleepUntilNs(next); next += static_cast<uint64_t>(period * 1e9); if (next < envshare::nowNs()) next = envshare::nowNs(); }
|
if (!unpaced) { sleepUntilNs(next); next += static_cast<uint64_t>(period * 1e9); if (next < envshare::nowNs()) next = envshare::nowNs(); }
|
||||||
const uint64_t fs0 = envshare::nowNs();
|
const uint64_t fs0 = envshare::nowNs(), cpu0 = threadCpuNs();
|
||||||
const float t = fixedFrames > 0 ? frames / 120.0f : (fs0 - t0) / 1e9f;
|
const float t = fixedFrames > 0 ? frames / 120.0f : (fs0 - t0) / 1e9f;
|
||||||
// Looking around at the spawn: a full turn every 16 s, nodding, swaying a little.
|
// Looking around at the spawn: a full turn every 16 s, nodding, swaying a little.
|
||||||
const float yaw = yaw0 + t * 6.2831853f / 16.0f, pitch = 0.25f * std::sin(t * 0.9f);
|
const float yaw = yaw0 + t * 6.2831853f / 16.0f, pitch = 0.25f * std::sin(t * 0.9f);
|
||||||
@@ -812,12 +897,15 @@ int runBench(const std::vector<std::string>& apks, double seconds, uint32_t w, u
|
|||||||
static const bool early = [&] { const bool e = std::getenv("FH_BENCH_EARLY") != nullptr; r.setEarlyRecord(e); return e; }();
|
static const bool early = [&] { const bool e = std::getenv("FH_BENCH_EARLY") != nullptr; r.setEarlyRecord(e); return e; }();
|
||||||
(void)early;
|
(void)early;
|
||||||
r.render(eyes, rt);
|
r.render(eyes, rt);
|
||||||
|
if (frames > 2) { std::lock_guard<std::mutex> l(latM); latJobs.push_back({r.frameSerial(), fs0, lateNs, envshare::nowNs()}); latCv.notify_one(); }
|
||||||
|
lateNs = 0;
|
||||||
if (!overlap) r.waitFrame();
|
if (!overlap) r.waitFrame();
|
||||||
if (r.deviceLost()) { LOG("bench: device lost (a GPU reset) after %u frames", frames); break; }
|
if (r.deviceLost()) { LOG("bench: device lost (a GPU reset) after %u frames", frames); break; }
|
||||||
const uint64_t done = envshare::nowNs();
|
const uint64_t done = envshare::nowNs();
|
||||||
const double makeMs = (done - fs0) / 1e6;
|
const double makeMs = (done - fs0) / 1e6;
|
||||||
make.add(makeMs);
|
make.add(makeMs);
|
||||||
cpu.add(r.lastStats().totalMs + animMs);
|
cpu.add(r.lastStats().totalMs + animMs);
|
||||||
|
work.add((threadCpuNs() - cpu0) / 1e6);
|
||||||
const bool gpuNew = r.gpuResults() != gpuSeen;
|
const bool gpuNew = r.gpuResults() != gpuSeen;
|
||||||
gpuSeen = r.gpuResults();
|
gpuSeen = r.gpuResults();
|
||||||
if (gpuNew && frames > 2) gpu.add(r.lastGpuMs());
|
if (gpuNew && frames > 2) gpu.add(r.lastGpuMs());
|
||||||
@@ -839,9 +927,33 @@ int runBench(const std::vector<std::string>& apks, double seconds, uint32_t w, u
|
|||||||
if (makeMs > period * 1500) spikes.push_back({t, makeMs, move});
|
if (makeMs > period * 1500) spikes.push_back({t, makeMs, move});
|
||||||
}
|
}
|
||||||
earlyFrames += r.lastStats().early ? 1 : 0;
|
earlyFrames += r.lastStats().early ? 1 : 0;
|
||||||
|
// FH_BENCH_TRACE=out.csv: one line a frame (where a slow one's time went, and the GPU clock then).
|
||||||
|
static FILE* trace = [] {
|
||||||
|
const char* p = std::getenv("FH_BENCH_TRACE");
|
||||||
|
FILE* f = p ? std::fopen(p, "w") : nullptr;
|
||||||
|
if (f) std::fprintf(f, "frame,t,move,makeMs,waitMs,prepMs,uploadMs,uploads,recordMs,submitMs,animMs,gpuNew,gpuMs,gpuUploadMs,gpuCopyMs,draws,early,mhz\n");
|
||||||
|
return f;
|
||||||
|
}();
|
||||||
|
if (trace) {
|
||||||
|
int mhz = 0;
|
||||||
|
#ifndef _WIN32
|
||||||
|
if (FILE* g = std::fopen("/sys/class/devfreq/3d00000.gpu/cur_freq", "r")) { long hzv = 0; if (std::fscanf(g, "%ld", &hzv) == 1) mhz = static_cast<int>(hzv / 1000000); std::fclose(g); }
|
||||||
|
#endif
|
||||||
|
const HslRenderer::FrameStats& s = r.lastStats();
|
||||||
|
std::fprintf(trace, "%u,%.4f,%d,%.3f,%.3f,%.3f,%.3f,%u,%.3f,%.3f,%.3f,%d,%.3f,%.3f,%.3f,%u,%d,%d\n", frames, t, move, makeMs, s.waitMs,
|
||||||
|
s.readbackMs + s.animMs + s.globalsMs + s.dynamicMs, s.uploadMs, s.uploads, s.recordMs, s.submitMs, animMs, gpuNew ? 1 : 0,
|
||||||
|
r.lastGpuMs(), r.lastGpuUploadMs(), r.lastGpuCopyMs(), s.draws, s.early ? 1 : 0, mhz);
|
||||||
|
}
|
||||||
++frames;
|
++frames;
|
||||||
}
|
}
|
||||||
const double secs = (envshare::nowNs() - t0) / 1e9;
|
const double secs = (envshare::nowNs() - t0) / 1e9;
|
||||||
|
{ std::lock_guard<std::mutex> l(latM); latStop = true; }
|
||||||
|
latCv.notify_all();
|
||||||
|
latThread.join();
|
||||||
|
r.latePose = nullptr;
|
||||||
|
std::fprintf(stderr, "bench latency: first pose to GPU done %.2f/%.2f/%.2f, late pose to GPU done %.2f/%.2f/%.2f, submit to GPU done %.2f/%.2f/%.2f "
|
||||||
|
"(avg/p95/max ms, %zu frames)\n", latTotal.avg(), latTotal.pct(0.95), latTotal.max(), latLate.avg(), latLate.pct(0.95), latLate.max(),
|
||||||
|
latGpu.avg(), latGpu.pct(0.95), latGpu.max(), latTotal.v.size());
|
||||||
std::sort(views.begin(), views.end(), [](const View& a, const View& b) { return a.gpuMs > b.gpuMs; });
|
std::sort(views.begin(), views.end(), [](const View& a, const View& b) { return a.gpuMs > b.gpuMs; });
|
||||||
std::string worst;
|
std::string worst;
|
||||||
std::vector<const View*> picked;
|
std::vector<const View*> picked;
|
||||||
@@ -887,10 +999,11 @@ int runBench(const std::vector<std::string>& apks, double seconds, uint32_t w, u
|
|||||||
}
|
}
|
||||||
std::printf("{\"home\":\"%s\",\"width\":%u,\"height\":%u,\"msaa\":%d,\"foveation\":%s,\"fovStrength\":%.2f,\"frames\":%u,\"fps\":%.1f,"
|
std::printf("{\"home\":\"%s\",\"width\":%u,\"height\":%u,\"msaa\":%d,\"foveation\":%s,\"fovStrength\":%.2f,\"frames\":%u,\"fps\":%.1f,"
|
||||||
"\"makeAvg\":%.2f,\"makeP95\":%.2f,\"makeP99\":%.2f,\"makeMax\":%.2f,\"cpuAvg\":%.2f,\"cpuP95\":%.2f,\"cpuMax\":%.2f,"
|
"\"makeAvg\":%.2f,\"makeP95\":%.2f,\"makeP99\":%.2f,\"makeMax\":%.2f,\"cpuAvg\":%.2f,\"cpuP95\":%.2f,\"cpuMax\":%.2f,"
|
||||||
|
"\"cpuWorkAvg\":%.2f,\"cpuWorkP95\":%.2f,"
|
||||||
"\"gpuP10\":%.2f,\"gpuAvg\":%.2f,\"gpuP95\":%.2f,\"gpuMax\":%.2f,\"gapSd\":%.2f,\"gapMax\":%.2f,\"overBudget\":%u,\"hitches\":%u,"
|
"\"gpuP10\":%.2f,\"gpuAvg\":%.2f,\"gpuP95\":%.2f,\"gpuMax\":%.2f,\"gapSd\":%.2f,\"gapMax\":%.2f,\"overBudget\":%u,\"hitches\":%u,"
|
||||||
"\"parseS\":%.2f,\"uploadS\":%.2f,\"draws\":%u,\"early\":%u,\"walkedM\":%.1f,\"peakRamMB\":%ld,\"worst\":[%s],\"moves\":{%s},\"spikes\":[%s]}\n",
|
"\"parseS\":%.2f,\"uploadS\":%.2f,\"draws\":%u,\"early\":%u,\"walkedM\":%.1f,\"peakRamMB\":%ld,\"worst\":[%s],\"moves\":{%s},\"spikes\":[%s]}\n",
|
||||||
fs::path(apks.front()).filename().c_str(), r.width(), r.height(), perf.msaa, r.foveationActive() ? "true" : "false", r.foveationStrength(), frames,
|
fs::path(apks.front()).filename().c_str(), r.width(), r.height(), perf.msaa, r.foveationActive() ? "true" : "false", r.foveationStrength(), frames,
|
||||||
frames / secs, make.avg(), make.pct(0.95), make.pct(0.99), make.max(), cpu.avg(), cpu.pct(0.95), cpu.max(), gpu.pct(0.10), gpu.avg(),
|
frames / secs, make.avg(), make.pct(0.95), make.pct(0.99), make.max(), cpu.avg(), cpu.pct(0.95), cpu.max(), work.avg(), work.pct(0.95), gpu.pct(0.10), gpu.avg(),
|
||||||
gpu.pct(0.95), gpu.max(), gap.stddev(), gap.max(), over, hitches, std::chrono::duration<double>(tl1 - tl0).count(),
|
gpu.pct(0.95), gpu.max(), gap.stddev(), gap.max(), over, hitches, std::chrono::duration<double>(tl1 - tl0).count(),
|
||||||
std::chrono::duration<double>(tl3 - tl2).count(), r.lastStats().draws, earlyFrames, walked, peakRamMB(), worst.c_str(), movesJson.c_str(), spikesJson.c_str());
|
std::chrono::duration<double>(tl3 - tl2).count(), r.lastStats().draws, earlyFrames, walked, peakRamMB(), worst.c_str(), movesJson.c_str(), spikesJson.c_str());
|
||||||
for (const auto& pc : r.perfCounterAverages()) std::fprintf(stderr, "perf: %14.0f %s\n", pc.second, pc.first.c_str());
|
for (const auto& pc : r.perfCounterAverages()) std::fprintf(stderr, "perf: %14.0f %s\n", pc.second, pc.first.c_str());
|
||||||
@@ -1056,7 +1169,7 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
}
|
}
|
||||||
std::string debugStamp = stamp(debugPath.string());
|
std::string debugStamp = stamp(debugPath.string());
|
||||||
int shotSeen = dbg.shot;
|
int shotSeen = dbg.shot;
|
||||||
const long startedAt = static_cast<long>(std::time(nullptr)); // picture requests from before this are old
|
long startedAt = static_cast<long>(std::time(nullptr)); // picture requests from before this are old
|
||||||
bool overlayDirty = true; // the world overlay is (re)built for the home shown
|
bool overlayDirty = true; // the world overlay is (re)built for the home shown
|
||||||
r.msaa = perf.msaa;
|
r.msaa = perf.msaa;
|
||||||
r.anisotropy = static_cast<float>(perf.anisotropy);
|
r.anisotropy = static_cast<float>(perf.anisotropy);
|
||||||
@@ -1077,7 +1190,7 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
r.setDebugView(dbg.view);
|
r.setDebugView(dbg.view);
|
||||||
r.debugHide = dbg.hideBits();
|
r.debugHide = dbg.hideBits();
|
||||||
float hz = vr::VRSystem()->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_DisplayFrequency_Float);
|
float hz = vr::VRSystem()->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_DisplayFrequency_Float);
|
||||||
if (hz < 30 || hz > 500) hz = 90;
|
if (!(hz >= 30 && hz <= 500)) hz = 90; // (NaN too: it would pace system mode's frames to never)
|
||||||
LOG("settings: resolution %s (%ux%u per eye), %dx MSAA, %dx filtering, foveation %s, %.0f Hz display", perf.autoResolution ? "auto" : "fixed",
|
LOG("settings: resolution %s (%ux%u per eye), %dx MSAA, %dx filtering, foveation %s, %.0f Hz display", perf.autoResolution ? "auto" : "fixed",
|
||||||
r.width(), r.height(), perf.msaa, perf.anisotropy, !r.foveationActive() ? "off" : perf.foveation == 2 ? "eye tracked" : "fixed", hz);
|
r.width(), r.height(), perf.msaa, perf.anisotropy, !r.foveationActive() ? "off" : perf.foveation == 2 ? "eye tracked" : "fixed", hz);
|
||||||
// Walking, turning and teleporting through the home's world layer (spawns, floors, walls, seats).
|
// Walking, turning and teleporting through the home's world layer (spawns, floors, walls, seats).
|
||||||
@@ -1085,6 +1198,7 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
if (!loco.init(rootDir() / "input")) LOG("controller input unavailable: walking and teleport are off");
|
if (!loco.init(rootDir() / "input")) LOG("controller input unavailable: walking and teleport are off");
|
||||||
loco.setOptions(perf.walk, perf.teleport, perf.turn, perf.snapAngle, perf.moveSpeed);
|
loco.setOptions(perf.walk, perf.teleport, perf.turn, perf.snapAngle, perf.moveSpeed);
|
||||||
loco.setRingFallback(!r.guideAvailable()); // Frame Home draws the arc itself; the overlay ring only if it can't
|
loco.setRingFallback(!r.guideAvailable()); // Frame Home draws the arc itself; the overlay ring only if it can't
|
||||||
|
loco.setOwnFade(system); // (SteamVR's fade is the scene app's: a blink fades through the layer)
|
||||||
std::vector<GuideVertex> guideVerts;
|
std::vector<GuideVertex> guideVerts;
|
||||||
// Homes are parsed (and their textures decoded) on a worker thread and uploaded between frames,
|
// Homes are parsed (and their textures decoded) on a worker thread and uploaded between frames,
|
||||||
// so the headset keeps getting frames while a new home loads.
|
// so the headset keeps getting frames while a new home loads.
|
||||||
@@ -1125,7 +1239,15 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
const auto began = std::chrono::steady_clock::now();
|
const auto began = std::chrono::steady_clock::now();
|
||||||
auto s = std::make_unique<HslScene>();
|
auto s = std::make_unique<HslScene>();
|
||||||
std::string e;
|
std::string e;
|
||||||
if (loadHslScene(home, *s, e)) {
|
bool loaded = false;
|
||||||
|
try { // (thrown out of this thread it would end Frame Home: a damaged home fails to load instead)
|
||||||
|
loaded = loadHslScene(home, *s, e);
|
||||||
|
} catch (const std::exception& x) {
|
||||||
|
e = std::string("couldn't load ") + home.front() + ": " + x.what();
|
||||||
|
} catch (...) {
|
||||||
|
e = "couldn't load " + home.front();
|
||||||
|
}
|
||||||
|
if (loaded) {
|
||||||
// The world layer sent from Frame Studio next to the home replaces the converted one; then the
|
// The world layer sent from Frame Studio next to the home replaces the converted one; then the
|
||||||
// walkable area (read from it, or baked) — here, not on the frame loop: a bake can take a while.
|
// walkable area (read from it, or baked) — here, not on the frame loop: a bake can take a while.
|
||||||
std::ifstream wf(worldPathFor(home.front()));
|
std::ifstream wf(worldPathFor(home.front()));
|
||||||
@@ -1244,7 +1366,9 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
{
|
{
|
||||||
std::ifstream in(rootDir() / "autoscale.txt");
|
std::ifstream in(rootDir() / "autoscale.txt");
|
||||||
float v;
|
float v;
|
||||||
for (std::string key; in >> v && std::getline(in >> std::ws, key);) if (v > 0) autoScales[key] = v;
|
// (only what auto resolution itself would pick: a damaged line mustn't make enormous render targets)
|
||||||
|
for (std::string key; in >> v && std::getline(in >> std::ws, key);)
|
||||||
|
if (v >= PerfSettings::kAutoMin && v <= PerfSettings::kAutoMax) autoScales[key] = v;
|
||||||
}
|
}
|
||||||
auto homeKey = [&] { std::string k; for (const auto& f : shown) k += fs::path(f).filename().string() + "+"; return k; };
|
auto homeKey = [&] { std::string k; for (const auto& f : shown) k += fs::path(f).filename().string() + "+"; return k; };
|
||||||
auto rememberScale = [&] {
|
auto rememberScale = [&] {
|
||||||
@@ -1286,6 +1410,12 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
prof.published(ns, leadMs, late);
|
prof.published(ns, leadMs, late);
|
||||||
};
|
};
|
||||||
uint64_t frameStartNs = 0, frameTarget = 0;
|
uint64_t frameStartNs = 0, frameTarget = 0;
|
||||||
|
uint64_t posePhotonNs = 0; // system mode: when the frame's pose was predicted for (its light)
|
||||||
|
uint64_t lastLocoTarget = 0; // system mode: the last frame's target (walking steps by targets)
|
||||||
|
int frameStride = 1; // system mode: compositor samples per frame (2: half rate)
|
||||||
|
auto movingUntil = std::chrono::steady_clock::time_point{}; // half rate: drawn at full rate until then (moving)
|
||||||
|
bool moveFull = false; // ...and frames fit in a refresh (decided as the moving starts)
|
||||||
|
envshare::Motion motion; // this frame's, for the layer (env_share.h)
|
||||||
const float vsyncToPhotons = vr::VRSystem()->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_SecondsFromVsyncToPhotons_Float);
|
const float vsyncToPhotons = vr::VRSystem()->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_SecondsFromVsyncToPhotons_Float);
|
||||||
if (system) LOG("display: %.0f Hz, %.1f ms from vsync to photons", hz, vsyncToPhotons * 1000);
|
if (system) LOG("display: %.0f Hz, %.1f ms from vsync to photons", hz, vsyncToPhotons * 1000);
|
||||||
auto loopStart = std::chrono::steady_clock::now();
|
auto loopStart = std::chrono::steady_clock::now();
|
||||||
@@ -1302,8 +1432,9 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
bool haveLastRig = false;
|
bool haveLastRig = false;
|
||||||
float frozenT = -1; // Settings > Animations off: the clock the home holds at
|
float frozenT = -1; // Settings > Animations off: the clock the home holds at
|
||||||
bool dashboard = false; // system mode: the Steam menu is open
|
bool dashboard = false; // system mode: the Steam menu is open
|
||||||
uint64_t dashFrames = 0;
|
|
||||||
auto dashChecked = std::chrono::steady_clock::now();
|
auto dashChecked = std::chrono::steady_clock::now();
|
||||||
|
vr::EVRSceneApplicationState sceneState = vr::EVRSceneApplicationState_None; // system mode: a game's (every 250 ms)
|
||||||
|
bool inGame = false; // system mode: a game is on screen and the home is stopped
|
||||||
while (running) {
|
while (running) {
|
||||||
vr::VREvent_t ev;
|
vr::VREvent_t ev;
|
||||||
while (vr::VRSystem()->PollNextEvent(&ev, sizeof(ev)))
|
while (vr::VRSystem()->PollNextEvent(&ev, sizeof(ev)))
|
||||||
@@ -1336,7 +1467,7 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
// Its textures in isn't yet smooth: the first frames with them still stall (Windows makes new GPU memory
|
// Its textures in isn't yet smooth: the first frames with them still stall (Windows makes new GPU memory
|
||||||
// resident on first use, drivers finish shaders on first draw: 100 ms frames for a second or two). Black
|
// resident on first use, drivers finish shaders on first draw: 100 ms frames for a second or two). Black
|
||||||
// until 45 frames in a row are on time as well (8 s at most).
|
// until 45 frames in a row are on time as well (8 s at most).
|
||||||
smoothFrames = r.texturesPending() == 0 && lastLoopMs < 1.2 * 1000.0 / hz ? smoothFrames + 1 : 0;
|
smoothFrames = r.texturesPending() == 0 && lastLoopMs < 1.2 * 1000.0 / hz * frameStride ? smoothFrames + 1 : 0;
|
||||||
if (texturesUntil != std::chrono::steady_clock::time_point{} && loadState == 0) {
|
if (texturesUntil != std::chrono::steady_clock::time_point{} && loadState == 0) {
|
||||||
const bool late = std::chrono::steady_clock::now() > texturesUntil;
|
const bool late = std::chrono::steady_clock::now() > texturesUntil;
|
||||||
if ((r.texturesPending() == 0 && smoothFrames >= 45) || late) {
|
if ((r.texturesPending() == 0 && smoothFrames >= 45) || late) {
|
||||||
@@ -1348,7 +1479,7 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (uploadDone || loadState == 3) {
|
if (uploadDone || loadState == 3) {
|
||||||
const bool ok = uploadDone && r.meshCount() > 0;
|
const bool ok = uploadDone && (r.meshCount() > 0 || r.splatObjectCount() > 0); // (a splats-only environment has no meshes)
|
||||||
uploadDone = false;
|
uploadDone = false;
|
||||||
const std::string edits = stamp(editsPathFor(pendingHome.front())), file = fileSig(pendingHome) + stamp(worldPathFor(pendingHome.front()));
|
const std::string edits = stamp(editsPathFor(pendingHome.front())), file = fileSig(pendingHome) + stamp(worldPathFor(pendingHome.front()));
|
||||||
const double uploadSec = std::chrono::duration<double>(std::chrono::steady_clock::now() - uploadStart).count();
|
const double uploadSec = std::chrono::duration<double>(std::chrono::steady_clock::now() - uploadStart).count();
|
||||||
@@ -1456,7 +1587,7 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
// Debug tools changed? All of them apply now.
|
// Debug tools changed? All of them apply now.
|
||||||
const std::string ds = stamp(debugPath.string());
|
const std::string ds = stamp(debugPath.string());
|
||||||
DebugSettings nd;
|
DebugSettings nd;
|
||||||
if (ds != debugStamp && (ds.empty() || nd.fromJson(readText(debugPath)))) {
|
if (ds != debugStamp && (ds == "-" || nd.fromJson(readText(debugPath)))) { // ("-": deleted, every view off)
|
||||||
debugStamp = ds;
|
debugStamp = ds;
|
||||||
if (!(nd == dbg)) {
|
if (!(nd == dbg)) {
|
||||||
LOG("debug: %s", nd.toJson().c_str());
|
LOG("debug: %s", nd.toJson().c_str());
|
||||||
@@ -1629,27 +1760,57 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
env.state()->show = scene && !exportLost ? 1 : 0;
|
env.state()->show = scene && !exportLost ? 1 : 0;
|
||||||
const float dtFade = 1.0f / hz / 0.3f; // 0.3 s fades
|
const float dtFade = 1.0f / hz / 0.3f; // 0.3 s fades
|
||||||
fade = fadeTarget > fade ? std::min(fadeTarget, fade + dtFade) : std::max(fadeTarget, fade - dtFade);
|
fade = fadeTarget > fade ? std::min(fadeTarget, fade + dtFade) : std::max(fadeTarget, fade - dtFade);
|
||||||
env.state()->fade = static_cast<uint32_t>(fade * 1000);
|
env.state()->fade = static_cast<uint32_t>(fade * loco.blinkLevel() * 1000); // (a teleport blink too)
|
||||||
// Nobody looking (a game is running, the compositor is idle, or no layer yet): a frame a second keeps one
|
|
||||||
// fresh for the layer (it goes back to Aurora only after 5 s without one) and leaves the GPU to the game;
|
|
||||||
// waking every 50 ms catches Aurora's slot being drawn again, and then it's full rate. No layer yet:
|
|
||||||
// every 100 ms (it connects once a second).
|
|
||||||
const bool idle = !layer || env.sinceSampled() > 1.0;
|
|
||||||
// The Steam menu is open: the home is only the dimmed backdrop behind it, so it's drawn every other
|
// The Steam menu is open: the home is only the dimmed backdrop behind it, so it's drawn every other
|
||||||
// compositor frame (the layer re-aims the last one) and the GPU goes to the menu and the apps in it.
|
// compositor frame (the layer re-aims the last one) and the GPU goes to the menu and the apps in it.
|
||||||
// Auto resolution waits it out: the GPU is shared then, and its times would only lower the home's size.
|
// Auto resolution waits it out: the GPU is shared then, and its times would only lower the home's size.
|
||||||
if (now - dashChecked > std::chrono::milliseconds(250)) {
|
if (now - dashChecked > std::chrono::milliseconds(250)) {
|
||||||
dashChecked = now;
|
dashChecked = now;
|
||||||
dashboard = vr::VROverlay() && vr::VROverlay()->IsDashboardVisible();
|
dashboard = vr::VROverlay() && vr::VROverlay()->IsDashboardVisible();
|
||||||
|
sceneState = vr::VRApplications() ? vr::VRApplications()->GetSceneApplicationState() : vr::EVRSceneApplicationState_None;
|
||||||
}
|
}
|
||||||
if (dashboard && perf.menuRate == 0) settleUntil = now + std::chrono::seconds(2);
|
// A game loading (launched, or connected and waiting to be shown): the home is what's on screen meanwhile,
|
||||||
// Settings > Frame rate: Half draws every other frame all the time (as Meta's home does).
|
// so it keeps going at full rate, never idle.
|
||||||
if (!idle && ((dashboard && perf.menuRate == 0) || perf.frameRate == 1) && (++dashFrames & 1)) {
|
const bool gameLoading = sceneState == vr::EVRSceneApplicationState_Starting || sceneState == vr::EVRSceneApplicationState_Waiting;
|
||||||
std::this_thread::sleep_for(std::chrono::duration<double>(1.0 / hz));
|
// Nobody looking (the compositor is idle, passthrough, or no layer yet): a frame a second keeps one fresh
|
||||||
continue;
|
// for the layer (it goes back to Aurora only after 5 s without one); waking every 50 ms catches Aurora's
|
||||||
}
|
// slot being drawn again, and then it's full rate. No layer yet: every 100 ms (it connects once a second).
|
||||||
|
const bool idle = !layer || (env.sinceSampled() > 1.0 && !gameLoading);
|
||||||
// (an idle home still loads at full speed: uploading, or its textures' 8 s window)
|
// (an idle home still loads at full speed: uploading, or its textures' 8 s window)
|
||||||
const bool loadingIn = uploading || texturesUntil != std::chrono::steady_clock::time_point{};
|
const bool loadingIn = uploading || texturesUntil != std::chrono::steady_clock::time_point{};
|
||||||
|
// A game on screen (running, and the home not drawn for a second): nothing of the home runs. No frames,
|
||||||
|
// no sound, no clock, no controllers: the GPU and CPU are the game's. The last frame stays in the layer,
|
||||||
|
// marked fresh once a second instead of drawn again (so Aurora doesn't take over), for the moment the
|
||||||
|
// game quits or the Steam menu shows the home; checking every 50 ms, the home is back at full rate then.
|
||||||
|
if (layer && idle && !loadingIn && sceneState == vr::EVRSceneApplicationState_Running) {
|
||||||
|
if (!inGame) { inGame = true; LOG("a game is on screen: the home stops (no frames, no sound) until it shows again"); }
|
||||||
|
g_audioMuted = true;
|
||||||
|
ambience.pause(true);
|
||||||
|
addedSound.pause(true);
|
||||||
|
if (env.state()->published.load() && envshare::nowNs() - env.state()->publishNs.load() > 1000000000ull)
|
||||||
|
env.state()->publishNs.store(envshare::nowNs());
|
||||||
|
env.still(); // (a turn under way isn't carried on while the frame waits)
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
lastFrame = animTick = std::chrono::steady_clock::now(); // (back: no long step in walking or the clock)
|
||||||
|
lastLocoTarget = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inGame) {
|
||||||
|
inGame = false;
|
||||||
|
ambience.pause(false);
|
||||||
|
addedSound.pause(false);
|
||||||
|
LOG("the home shows again: drawing");
|
||||||
|
}
|
||||||
|
if (dashboard && perf.menuRate == 0) settleUntil = now + std::chrono::seconds(2);
|
||||||
|
// Settings > Frame rate: Half draws every other compositor frame (as Meta's home does), timed to the
|
||||||
|
// display like full rate (SystemPacer::stride). The layer shows each frame twice, carrying the second on
|
||||||
|
// by your head's turn and the rig's (Motion). Walking it can only guess at (no depth), so while you move
|
||||||
|
// (walk, turn, dash, blink, fall) every frame is drawn, if frames are made within a refresh (a home too
|
||||||
|
// heavy for that stays at half: frames would come late, which is worse).
|
||||||
|
const bool halfAsked = !idle && ((dashboard && perf.menuRate == 0) || perf.frameRate == 1);
|
||||||
|
if (now >= movingUntil) moveFull = false;
|
||||||
|
frameStride = halfAsked && !moveFull ? 2 : 1;
|
||||||
|
pacer.stride = frameStride;
|
||||||
if (idle && !layer && !loadingIn) std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
if (idle && !layer && !loadingIn) std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
else if (idle && !loadingIn)
|
else if (idle && !loadingIn)
|
||||||
for (int i = 0; i < 20 && env.sinceSampled() > 1.0 && running; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
for (int i = 0; i < 20 && env.sinceSampled() > 1.0 && running; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
@@ -1672,22 +1833,61 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
}
|
}
|
||||||
frameStartNs = envshare::nowNs();
|
frameStartNs = envshare::nowNs();
|
||||||
if (frameStartNs > startNs) predict = std::max(0.0f, predict - (frameStartNs - startNs) / 1e9f); // started late
|
if (frameStartNs > startNs) predict = std::max(0.0f, predict - (frameStartNs - startNs) / 1e9f); // started late
|
||||||
|
posePhotonNs = frameStartNs + static_cast<uint64_t>(predict * 1e9);
|
||||||
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, predict, poses, vr::k_unMaxTrackedDeviceCount);
|
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, predict, poses, vr::k_unMaxTrackedDeviceCount);
|
||||||
r.exportSlot = env.beginFrame();
|
r.exportSlot = env.beginFrame();
|
||||||
} else {
|
} else {
|
||||||
vr::VRCompositor()->WaitGetPoses(poses, vr::k_unMaxTrackedDeviceCount, nullptr, 0);
|
vr::VRCompositor()->WaitGetPoses(poses, vr::k_unMaxTrackedDeviceCount, nullptr, 0);
|
||||||
frameStartNs = envshare::nowNs();
|
frameStartNs = envshare::nowNs();
|
||||||
}
|
}
|
||||||
loco.setPoseValid(poses[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid);
|
{ // A pose with a NaN or infinity in it (tracking glitch) is taken as no pose: one would poison the rig for good.
|
||||||
if (poses[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid)
|
const vr::TrackedDevicePose_t& hp = poses[vr::k_unTrackedDeviceIndex_Hmd];
|
||||||
head = fromHmd34(poses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking);
|
bool valid = hp.bPoseIsValid;
|
||||||
|
for (int i = 0; i < 3 && valid; ++i)
|
||||||
|
for (int k = 0; k < 4 && valid; ++k) valid = std::isfinite(hp.mDeviceToAbsoluteTracking.m[i][k]);
|
||||||
|
loco.setPoseValid(valid);
|
||||||
|
if (valid) head = fromHmd34(hp.mDeviceToAbsoluteTracking);
|
||||||
|
}
|
||||||
const auto frameNow = std::chrono::steady_clock::now();
|
const auto frameNow = std::chrono::steady_clock::now();
|
||||||
const float frameDt = std::chrono::duration<float>(frameNow - lastFrame).count();
|
float frameDt = std::chrono::duration<float>(frameNow - lastFrame).count();
|
||||||
|
// System mode: each frame is shown at its target, so walking and turning step by the time between targets
|
||||||
|
// (whole display periods: speed / 120 a frame at 120 Hz, / 60 at half rate), not by when the loop woke up
|
||||||
|
// (sleep jitter, a changing lead), which made the speed per shown frame wobble.
|
||||||
|
if (system && lastLocoTarget && frameTarget > lastLocoTarget && frameTarget - lastLocoTarget < 250000000ull)
|
||||||
|
frameDt = static_cast<float>((frameTarget - lastLocoTarget) / 1e9);
|
||||||
|
lastLocoTarget = system ? frameTarget : 0;
|
||||||
|
Mat4 rigBefore;
|
||||||
|
loco.rig(rigBefore.m);
|
||||||
loco.update(frameDt, head.m);
|
loco.update(frameDt, head.m);
|
||||||
lastFrame = frameNow;
|
lastFrame = frameNow;
|
||||||
Mat4 rig;
|
Mat4 rig;
|
||||||
loco.rig(rig.m);
|
loco.rig(rig.m);
|
||||||
const Mat4 headWorld = mul(rig, head);
|
const Mat4 headWorld = mul(rig, head);
|
||||||
|
if (system) {
|
||||||
|
env.state()->fade = static_cast<uint32_t>(fade * loco.blinkLevel() * 1000);
|
||||||
|
// How the rig moves this frame (not your body): its smooth turn, and where the head's spot in the room
|
||||||
|
// went (walking, a dash, a step; turning pivots about the head, so it isn't in this), in this frame's
|
||||||
|
// tracking space. A jump (a teleport, a seat, a snap turn) isn't motion: nothing to carry on.
|
||||||
|
motion = envshare::Motion();
|
||||||
|
motion.targetNs = frameTarget;
|
||||||
|
motion.yawRate = loco.turnRate();
|
||||||
|
const float dtm = std::max(frameDt, 1e-3f);
|
||||||
|
float dw[3];
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
dw[k] = rig.m[k] * head.m[12] + rig.m[4 + k] * head.m[13] + rig.m[8 + k] * head.m[14] + rig.m[12 + k] -
|
||||||
|
(rigBefore.m[k] * head.m[12] + rigBefore.m[4 + k] * head.m[13] + rigBefore.m[8 + k] * head.m[14] + rigBefore.m[12 + k]);
|
||||||
|
const float moved = std::sqrt(dw[0] * dw[0] + dw[1] * dw[1] + dw[2] * dw[2]);
|
||||||
|
if (moved < 0.3f && moved > 1e-5f) {
|
||||||
|
for (int k = 0; k < 3; ++k) // world to tracking: the rig's rotation, transposed
|
||||||
|
motion.velocity[k] = (rig.m[k * 4] * dw[0] + rig.m[k * 4 + 1] * dw[1] + rig.m[k * 4 + 2] * dw[2]) / dtm;
|
||||||
|
motion.depth = loco.viewDepth(head.m);
|
||||||
|
}
|
||||||
|
// Moving (half rate: every frame drawn meanwhile, and 0.3 s after, so stopping isn't a step back down)
|
||||||
|
if ((moved < 0.3f && moved / dtm > 0.02f) || motion.yawRate != 0 || loco.sliding() || loco.blinkLevel() < 1) {
|
||||||
|
if (now >= movingUntil) moveFull = pacer.leadMs() < 1000.0 / hz * 0.9; // frames fit in a refresh
|
||||||
|
movingUntil = now + std::chrono::milliseconds(300);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (system && widenFixed <= 0) {
|
if (system && widenFixed <= 0) {
|
||||||
// head turn rate (rad/s) from the rotation since the last frame: angle of R_prev^T * R
|
// head turn rate (rad/s) from the rotation since the last frame: angle of R_prev^T * R
|
||||||
float tr = 0;
|
float tr = 0;
|
||||||
@@ -1698,7 +1898,7 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
const float rate = frameDt > 1e-4f ? ang / std::max(frameDt, 1.0f / hz) : 0.0f;
|
const float rate = frameDt > 1e-4f ? ang / std::max(frameDt, 1.0f / hz) : 0.0f;
|
||||||
// The frame is shown up to ~2 refreshes after its pose was predicted (more when only every other one is
|
// The frame is shown up to ~2 refreshes after its pose was predicted (more when only every other one is
|
||||||
// drawn): the margin covers that much turning at the lens edge (tan grows ~2.5x per radian out there).
|
// drawn): the margin covers that much turning at the lens edge (tan grows ~2.5x per radian out there).
|
||||||
const bool half = (dashboard && perf.menuRate == 0) || perf.frameRate == 1;
|
const bool half = frameStride == 2;
|
||||||
const float hold = (half ? 3.0f : 2.0f) / hz;
|
const float hold = (half ? 3.0f : 2.0f) / hz;
|
||||||
const float want = std::clamp(1.03f + rate * hold * 2.5f, half ? 1.05f : 1.03f, 1.18f);
|
const float want = std::clamp(1.03f + rate * hold * 2.5f, half ? 1.05f : 1.03f, 1.18f);
|
||||||
const float next = want > widen ? want : std::max(want, widen - frameDt * 0.25f); // up at once, down over ~0.4 s
|
const float next = want > widen ? want : std::max(want, widen - frameDt * 0.25f); // up at once, down over ~0.4 s
|
||||||
@@ -1789,7 +1989,33 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
r.setGuide(guideVerts);
|
r.setGuide(guideVerts);
|
||||||
}
|
}
|
||||||
if (prof.on) prof.pre.add((envshare::nowNs() - frameStartNs) / 1e6); // this frame's main-thread work before render()
|
if (prof.on) prof.pre.add((envshare::nowNs() - frameStartNs) / 1e6); // this frame's main-thread work before render()
|
||||||
|
// System mode: the head pose read again inside render(), once the frame is recorded and the GPU is free for it,
|
||||||
|
// for the same moment of light: the frame's view is that much newer (the CPU's work for the frame, and any wait
|
||||||
|
// for the frame before, no longer age it). The rig, culling and the rest stay with the first read; the frame
|
||||||
|
// is published with the pose it was drawn from. FH_NO_LATE_POSE=1: the first read only (A/B).
|
||||||
|
static const bool latePoseOff = std::getenv("FH_NO_LATE_POSE") != nullptr;
|
||||||
|
if (system && posePhotonNs && !latePoseOff)
|
||||||
|
r.latePose = [&](EyeView out[2]) {
|
||||||
|
const uint64_t nowL = envshare::nowNs();
|
||||||
|
const float ahead = posePhotonNs > nowL ? (posePhotonNs - nowL) / 1e9f : 0.0f;
|
||||||
|
vr::TrackedDevicePose_t lp[vr::k_unMaxTrackedDeviceCount];
|
||||||
|
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, ahead, lp, vr::k_unMaxTrackedDeviceCount);
|
||||||
|
const vr::TrackedDevicePose_t& hp = lp[vr::k_unTrackedDeviceIndex_Hmd];
|
||||||
|
bool valid = hp.bPoseIsValid;
|
||||||
|
for (int i = 0; i < 3 && valid; ++i)
|
||||||
|
for (int k = 0; k < 4 && valid; ++k) valid = std::isfinite(hp.mDeviceToAbsoluteTracking.m[i][k]);
|
||||||
|
if (!valid) return false;
|
||||||
|
const Mat4 lh = fromHmd34(hp.mDeviceToAbsoluteTracking);
|
||||||
|
// (a jump since the first read, e.g. tracking recentred: that frame keeps the first)
|
||||||
|
const float dx = lh.m[12] - head.m[12], dy = lh.m[13] - head.m[13], dz = lh.m[14] - head.m[14];
|
||||||
|
if (dx * dx + dy * dy + dz * dz > 0.05f * 0.05f) return false;
|
||||||
|
head = lh;
|
||||||
|
const Mat4 hw = mul(rig, head);
|
||||||
|
for (int e = 0; e < 2; ++e) out[e] = eyeFromPose(mul(hw, eyeToHead[e]), proj[e]);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
r.render(eyes, t);
|
r.render(eyes, t);
|
||||||
|
r.latePose = nullptr;
|
||||||
if (r.deviceLost()) { // a GPU reset (a hang anywhere on the GPU) took the device: start again with a new one
|
if (r.deviceLost()) { // a GPU reset (a hang anywhere on the GPU) took the device: start again with a new one
|
||||||
LOG("the GPU was reset and this process lost its device: restarting");
|
LOG("the GPU was reset and this process lost its device: restarting");
|
||||||
restart = lostDevice = true;
|
restart = lostDevice = true;
|
||||||
@@ -1800,6 +2026,9 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
++statFrames;
|
++statFrames;
|
||||||
if (dbg.shot != shotSeen) { // Debug > Save a Picture: the left eye as it was just drawn, to logs/
|
if (dbg.shot != shotSeen) { // Debug > Save a Picture: the left eye as it was just drawn, to logs/
|
||||||
shotSeen = dbg.shot;
|
shotSeen = dbg.shot;
|
||||||
|
// The clock set back since the start (a fast clock corrected after boot): the start moves back with it,
|
||||||
|
// or every request would look older than it.
|
||||||
|
if (const long t = static_cast<long>(std::time(nullptr)); t < startedAt) startedAt = t;
|
||||||
std::vector<u8> px;
|
std::vector<u8> px;
|
||||||
if (dbg.shot >= startedAt && r.readback(0, px) && px.size() >= static_cast<size_t>(r.width()) * r.height() * 4) {
|
if (dbg.shot >= startedAt && r.readback(0, px) && px.size() >= static_cast<size_t>(r.width()) * r.height() * 4) {
|
||||||
char name[64];
|
char name[64];
|
||||||
@@ -1833,10 +2062,11 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
std::memcpy(j.eye, eyeOut, sizeof j.eye);
|
std::memcpy(j.eye, eyeOut, sizeof j.eye);
|
||||||
j.startNs = frameStartNs;
|
j.startNs = frameStartNs;
|
||||||
j.target = frameTarget;
|
j.target = frameTarget;
|
||||||
|
j.motion = motion;
|
||||||
publisher.push(j);
|
publisher.push(j);
|
||||||
} else { // no timeline semaphores: wait here
|
} else { // no timeline semaphores: wait here
|
||||||
r.waitFrame();
|
r.waitFrame();
|
||||||
env.publish(r.exportSlot, vpOut, eyeOut);
|
env.publish(r.exportSlot, vpOut, eyeOut, motion);
|
||||||
const uint64_t pubNs = envshare::nowNs();
|
const uint64_t pubNs = envshare::nowNs();
|
||||||
published(pubNs, (pubNs - frameStartNs) / 1e6, frameTarget && pubNs > frameTarget);
|
published(pubNs, (pubNs - frameStartNs) / 1e6, frameTarget && pubNs > frameTarget);
|
||||||
}
|
}
|
||||||
@@ -1866,7 +2096,7 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
const auto le = std::chrono::steady_clock::now();
|
const auto le = std::chrono::steady_clock::now();
|
||||||
const double loopMs = std::chrono::duration<double, std::milli>(le - loopStart).count();
|
const double loopMs = std::chrono::duration<double, std::milli>(le - loopStart).count();
|
||||||
lastLoopMs = loopMs;
|
lastLoopMs = loopMs;
|
||||||
if (fade >= 0.99f && loopMs > 1.5 * 1000.0 / hz) ++statHitchShown; // a frame you could see, late (stuttered)
|
if (fade >= 0.99f && loopMs > 1.5 * 1000.0 / hz * frameStride) ++statHitchShown; // a frame you could see, late (stuttered)
|
||||||
prof.frame(r, loopMs, animMs, r.lastGpuMs(), gpuNew);
|
prof.frame(r, loopMs, animMs, r.lastGpuMs(), gpuNew);
|
||||||
loopStart = le;
|
loopStart = le;
|
||||||
prof.report(system ? (uploading ? "system(loading)" : "system") : "scene");
|
prof.report(system ? (uploading ? "system(loading)" : "system") : "scene");
|
||||||
@@ -1896,6 +2126,12 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
|||||||
LOG("restarting %s", lostDevice ? "with a new GPU device" : "with the new settings");
|
LOG("restarting %s", lostDevice ? "with a new GPU device" : "with the new settings");
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
homeos::setEnv("FH_RESTARTED", "1");
|
homeos::setEnv("FH_RESTARTED", "1");
|
||||||
|
#ifdef _WIN32
|
||||||
|
// (restartSelf ends with exit(), which skips the guard's destructor: let go of the mutex and the pid file
|
||||||
|
// here, or the new copy finds the mutex still held and steps aside, leaving no renderer)
|
||||||
|
if (systemGuard.held) { std::error_code ec; fs::remove(systemGuard.pidFile, ec); systemGuard.held = false; }
|
||||||
|
if (systemGuard.mutex) { ReleaseMutex(systemGuard.mutex); CloseHandle(systemGuard.mutex); systemGuard.mutex = nullptr; }
|
||||||
|
#endif
|
||||||
homeos::restartSelf();
|
homeos::restartSelf();
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -6,13 +6,28 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
extern "C" unsigned char* stbi_load_from_memory(const unsigned char* buffer, int len, int* x, int* y, int* comp, int req_comp);
|
extern "C" unsigned char* stbi_load_from_memory(const unsigned char* buffer, int len, int* x, int* y, int* comp, int req_comp);
|
||||||
|
extern "C" float* stbi_loadf_from_memory(const unsigned char* buffer, int len, int* x, int* y, int* comp, int req_comp);
|
||||||
extern "C" void stbi_image_free(void* retval_from_stbi_load);
|
extern "C" void stbi_image_free(void* retval_from_stbi_load);
|
||||||
|
extern "C" int stbi_info_from_memory(const unsigned char* buffer, int len, int* x, int* y, int* comp);
|
||||||
|
|
||||||
|
// At most this many pixels decoded (16384 x 8192, a 16K panorama): a small damaged file can claim a picture of
|
||||||
|
// gigabytes. (stb itself also refuses a side over 16384: stb_image_impl.c.)
|
||||||
|
constexpr unsigned long long kMaxPixels = 16384ull * 8192ull;
|
||||||
|
static bool sizeOk(const unsigned char* data, size_t n) {
|
||||||
|
int w = 0, h = 0, c = 0;
|
||||||
|
return data && n > 0 && n <= 0x7fffffff && stbi_info_from_memory(data, static_cast<int>(n), &w, &h, &c) && w > 0 && h > 0 &&
|
||||||
|
static_cast<unsigned long long>(w) * static_cast<unsigned long long>(h) <= kMaxPixels;
|
||||||
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -28,10 +43,41 @@ std::vector<u8> readFile(const std::string& path) {
|
|||||||
return std::vector<u8>((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
return std::vector<u8>((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cgltf's file reads, by the UTF-8 path as a wide name on Windows (its own fopen takes the ANSI code page, so a
|
||||||
|
// model under C:\Users\Zoë\ wasn't found).
|
||||||
|
cgltf_result gltfRead(const cgltf_memory_options*, const cgltf_file_options*, const char* path, cgltf_size* size, void** data) {
|
||||||
|
std::ifstream f(fs::u8path(path), std::ios::binary);
|
||||||
|
if (!f) return cgltf_result_file_not_found;
|
||||||
|
std::vector<char> bytes((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||||
|
if (size && *size) { // a buffer's declared length: as cgltf's own, short is an error
|
||||||
|
if (bytes.size() < *size) return cgltf_result_io_error;
|
||||||
|
bytes.resize(*size);
|
||||||
|
}
|
||||||
|
void* out = std::malloc(std::max<size_t>(bytes.size(), 1));
|
||||||
|
if (!out) return cgltf_result_out_of_memory;
|
||||||
|
if (!bytes.empty()) std::memcpy(out, bytes.data(), bytes.size());
|
||||||
|
if (size) *size = bytes.size();
|
||||||
|
*data = out;
|
||||||
|
return cgltf_result_success;
|
||||||
|
}
|
||||||
|
void gltfRelease(const cgltf_memory_options*, const cgltf_file_options*, void* data) { std::free(data); }
|
||||||
|
|
||||||
|
// An accessor's floats, `n` a vertex, sparse substitutions applied (cgltf_accessor_read_float leaves sparse
|
||||||
|
// accessors as zeros). `comps` floats a vertex; vertices past the accessor's own count stay `fill`.
|
||||||
|
std::vector<float> unpackFloats(const cgltf_accessor* a, size_t vertices, size_t comps, float fill) {
|
||||||
|
std::vector<float> out(vertices * comps, fill);
|
||||||
|
if (!a || cgltf_num_components(a->type) != comps) return out;
|
||||||
|
std::vector<float> all(a->count * comps);
|
||||||
|
if (cgltf_accessor_unpack_floats(a, all.data(), all.size()) != all.size()) return out;
|
||||||
|
std::copy_n(all.begin(), std::min(all.size(), out.size()), out.begin());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
struct Image { std::vector<u8> rgba; u32 w = 0, h = 0; bool alpha = false; };
|
struct Image { std::vector<u8> rgba; u32 w = 0, h = 0; bool alpha = false; };
|
||||||
|
|
||||||
bool decode(const u8* data, size_t n, Image& img) {
|
bool decode(const u8* data, size_t n, Image& img) {
|
||||||
int w = 0, h = 0, comp = 0;
|
int w = 0, h = 0, comp = 0;
|
||||||
|
if (!sizeOk(data, n)) return false;
|
||||||
unsigned char* px = stbi_load_from_memory(data, static_cast<int>(n), &w, &h, &comp, 4);
|
unsigned char* px = stbi_load_from_memory(data, static_cast<int>(n), &w, &h, &comp, 4);
|
||||||
if (!px) return false;
|
if (!px) return false;
|
||||||
img.w = static_cast<u32>(w);
|
img.w = static_cast<u32>(w);
|
||||||
@@ -64,6 +110,8 @@ void finish(MeshData& md, const Image* img, const float color[4], bool blend, bo
|
|||||||
|
|
||||||
bool importGltf(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
bool importGltf(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
||||||
cgltf_options opt = {};
|
cgltf_options opt = {};
|
||||||
|
opt.file.read = gltfRead;
|
||||||
|
opt.file.release = gltfRelease;
|
||||||
cgltf_data* data = nullptr;
|
cgltf_data* data = nullptr;
|
||||||
if (cgltf_parse_file(&opt, path.c_str(), &data) != cgltf_result_success) { error = "not a readable glTF file: " + path; return false; }
|
if (cgltf_parse_file(&opt, path.c_str(), &data) != cgltf_result_success) { error = "not a readable glTF file: " + path; return false; }
|
||||||
if (cgltf_load_buffers(&opt, data, path.c_str()) != cgltf_result_success) { cgltf_free(data); error = "glTF buffers missing next to " + path; return false; }
|
if (cgltf_load_buffers(&opt, data, path.c_str()) != cgltf_result_success) { cgltf_free(data); error = "glTF buffers missing next to " + path; return false; }
|
||||||
@@ -72,7 +120,12 @@ bool importGltf(const std::string& path, std::vector<MeshData>& meshes, std::vec
|
|||||||
files.push_back(path);
|
files.push_back(path);
|
||||||
const fs::path dir = fs::u8path(path).parent_path();
|
const fs::path dir = fs::u8path(path).parent_path();
|
||||||
for (cgltf_size b = 0; b < data->buffers_count; ++b)
|
for (cgltf_size b = 0; b < data->buffers_count; ++b)
|
||||||
if (data->buffers[b].uri && std::strncmp(data->buffers[b].uri, "data:", 5) != 0) files.push_back((dir / fs::u8path(data->buffers[b].uri)).u8string());
|
if (data->buffers[b].uri && std::strncmp(data->buffers[b].uri, "data:", 5) != 0) {
|
||||||
|
std::string rel = data->buffers[b].uri; // (as written in the file: "my%20model.bin" is "my model.bin")
|
||||||
|
cgltf_decode_uri(rel.data());
|
||||||
|
rel.resize(std::strlen(rel.c_str()));
|
||||||
|
files.push_back((dir / fs::u8path(rel)).u8string());
|
||||||
|
}
|
||||||
std::map<const cgltf_image*, Image> images;
|
std::map<const cgltf_image*, Image> images;
|
||||||
auto image = [&](const cgltf_texture_view& view) -> const Image* {
|
auto image = [&](const cgltf_texture_view& view) -> const Image* {
|
||||||
if (!view.texture || !view.texture->image) return nullptr;
|
if (!view.texture || !view.texture->image) return nullptr;
|
||||||
@@ -88,7 +141,11 @@ bool importGltf(const std::string& path, std::vector<MeshData>& meshes, std::vec
|
|||||||
} else if (im->uri && std::strncmp(im->uri, "data:", 5) == 0) {
|
} else if (im->uri && std::strncmp(im->uri, "data:", 5) == 0) {
|
||||||
const char* comma = std::strchr(im->uri, ',');
|
const char* comma = std::strchr(im->uri, ',');
|
||||||
if (comma) {
|
if (comma) {
|
||||||
const size_t len = std::strlen(comma + 1), size = len / 4 * 3;
|
// (the '=' padding isn't data: cgltf's decoder fails on it, so the length leaves it out)
|
||||||
|
const size_t len = std::strlen(comma + 1);
|
||||||
|
size_t pad = 0;
|
||||||
|
while (pad < 2 && pad < len && comma[len - pad] == '=') ++pad;
|
||||||
|
const size_t size = len / 4 * 3 - std::min(pad, len / 4 * 3);
|
||||||
void* bytes = nullptr;
|
void* bytes = nullptr;
|
||||||
if (cgltf_load_buffer_base64(&opt, size, comma + 1, &bytes) == cgltf_result_success) {
|
if (cgltf_load_buffer_base64(&opt, size, comma + 1, &bytes) == cgltf_result_success) {
|
||||||
decode(static_cast<const u8*>(bytes), size, out);
|
decode(static_cast<const u8*>(bytes), size, out);
|
||||||
@@ -125,19 +182,15 @@ bool importGltf(const std::string& path, std::vector<MeshData>& meshes, std::vec
|
|||||||
md.name = std::string(node.name ? node.name : node.mesh->name ? node.mesh->name : "mesh") + (node.mesh->primitives_count > 1 ? "_" + std::to_string(p) : "");
|
md.name = std::string(node.name ? node.name : node.mesh->name ? node.mesh->name : "mesh") + (node.mesh->primitives_count > 1 ? "_" + std::to_string(p) : "");
|
||||||
for (const cgltf_node* a = node.parent; a; a = a->parent) // the nodes above it in the file
|
for (const cgltf_node* a = node.parent; a; a = a->parent) // the nodes above it in the file
|
||||||
if (a->name) md.path = md.path.empty() ? std::string(a->name) : std::string(a->name) + "/" + md.path;
|
if (a->name) md.path = md.path.empty() ? std::string(a->name) : std::string(a->name) + "/" + md.path;
|
||||||
md.positions.resize(pos->count * 3);
|
md.positions = unpackFloats(pos, pos->count, 3, 0.0f);
|
||||||
md.uvs.assign(pos->count * 2, 0.0f);
|
md.uvs = unpackFloats(uv, pos->count, 2, 0.0f);
|
||||||
for (cgltf_size v = 0; v < pos->count; ++v) {
|
|
||||||
cgltf_accessor_read_float(pos, v, &md.positions[v * 3], 3);
|
|
||||||
if (uv) cgltf_accessor_read_float(uv, v, &md.uvs[v * 2], 2);
|
|
||||||
}
|
|
||||||
if (col) {
|
if (col) {
|
||||||
|
const size_t nc = cgltf_num_components(col->type);
|
||||||
|
const std::vector<float> c = unpackFloats(col, pos->count, nc, 1.0f);
|
||||||
md.colors.resize(pos->count * 4);
|
md.colors.resize(pos->count * 4);
|
||||||
for (cgltf_size v = 0; v < pos->count; ++v) {
|
for (cgltf_size v = 0; v < pos->count; ++v)
|
||||||
float c[4] = {1, 1, 1, 1};
|
for (size_t k = 0; k < 4; ++k)
|
||||||
cgltf_accessor_read_float(col, v, c, cgltf_num_components(col->type));
|
md.colors[v * 4 + k] = static_cast<u8>(std::clamp(k < nc ? c[v * nc + k] : 1.0f, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||||
for (int k = 0; k < 4; ++k) md.colors[v * 4 + k] = static_cast<u8>(std::clamp(c[k], 0.0f, 1.0f) * 255.0f + 0.5f);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (prim.indices) {
|
if (prim.indices) {
|
||||||
md.indices.resize(prim.indices->count);
|
md.indices.resize(prim.indices->count);
|
||||||
@@ -148,6 +201,8 @@ bool importGltf(const std::string& path, std::vector<MeshData>& meshes, std::vec
|
|||||||
md.indices.resize(pos->count);
|
md.indices.resize(pos->count);
|
||||||
for (cgltf_size i = 0; i < pos->count; ++i) md.indices[i] = static_cast<u32>(i);
|
for (cgltf_size i = 0; i < pos->count; ++i) md.indices[i] = static_cast<u32>(i);
|
||||||
}
|
}
|
||||||
|
md.indices.resize(md.indices.size() / 3 * 3); // only whole triangles (a damaged file's count may not be)
|
||||||
|
if (md.indices.empty()) continue;
|
||||||
std::memcpy(md.worldMatrix, world, sizeof world);
|
std::memcpy(md.worldMatrix, world, sizeof world);
|
||||||
md.hasWorldMatrix = true;
|
md.hasWorldMatrix = true;
|
||||||
float color[4] = {1, 1, 1, 1};
|
float color[4] = {1, 1, 1, 1};
|
||||||
@@ -183,6 +238,7 @@ bool importUfbx(const std::string& path, std::vector<MeshData>& meshes, std::vec
|
|||||||
opts.target_unit_meters = 1.0f;
|
opts.target_unit_meters = 1.0f;
|
||||||
opts.space_conversion = UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY;
|
opts.space_conversion = UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY;
|
||||||
opts.load_external_files = true;
|
opts.load_external_files = true;
|
||||||
|
opts.ignore_missing_external_files = true; // (an OBJ whose .mtl is missing opens, untextured, instead of not at all)
|
||||||
ufbx_error err;
|
ufbx_error err;
|
||||||
ufbx_scene* scene = ufbx_load_file(path.c_str(), &opts, &err);
|
ufbx_scene* scene = ufbx_load_file(path.c_str(), &opts, &err);
|
||||||
if (!scene) {
|
if (!scene) {
|
||||||
@@ -279,17 +335,315 @@ bool importUfbx(const std::string& path, std::vector<MeshData>& meshes, std::vec
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One mesh of triangles (3 positions each, already Y-up) drawn flat white, or in its vertex colours when it has them.
|
||||||
|
void finishPlain(MeshData& md, const std::string& path) {
|
||||||
|
md.name = fs::u8path(path).stem().u8string();
|
||||||
|
md.uvs.resize(md.positions.size() / 3 * 2, 0.0f);
|
||||||
|
md.indices.resize(md.positions.size() / 3);
|
||||||
|
for (size_t i = 0; i < md.indices.size(); ++i) md.indices[i] = static_cast<u32>(i);
|
||||||
|
const float white[4] = {1, 1, 1, 1};
|
||||||
|
finish(md, nullptr, white, false, false, 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// STL (3D printing, CAD): binary (80-byte header, a count, 50 bytes a triangle) or ASCII ("solid ... facet ...
|
||||||
|
// vertex x y z"). Its axes are Z-up, as CAD's are: turned to Y-up. No units are recorded; taken as metres (the
|
||||||
|
// Scale field fixes a millimetre file: 0.001).
|
||||||
|
bool importStl(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
||||||
|
const std::vector<u8> b = readFile(path);
|
||||||
|
if (b.size() < 15) { error = "not an STL file: " + path; return false; }
|
||||||
|
MeshData md;
|
||||||
|
auto vertex = [&](float x, float y, float z) { md.positions.insert(md.positions.end(), {x, z, -y}); };
|
||||||
|
uint32_t count = 0;
|
||||||
|
if (b.size() >= 84) std::memcpy(&count, &b[80], 4);
|
||||||
|
if (b.size() >= 84 && static_cast<uint64_t>(count) * 50 + 84 == b.size()) { // binary: the size says so exactly
|
||||||
|
md.positions.reserve(static_cast<size_t>(count) * 9);
|
||||||
|
for (uint32_t t = 0; t < count; ++t) {
|
||||||
|
float v[9];
|
||||||
|
std::memcpy(v, &b[84 + static_cast<size_t>(t) * 50 + 12], sizeof v); // (after the facet normal)
|
||||||
|
for (int k = 0; k < 3; ++k) vertex(v[k * 3], v[k * 3 + 1], v[k * 3 + 2]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::istringstream in(std::string(b.begin(), b.end()));
|
||||||
|
std::string word;
|
||||||
|
while (in >> word) {
|
||||||
|
if (word != "vertex") continue;
|
||||||
|
float x = 0, y = 0, z = 0;
|
||||||
|
if (!(in >> x >> y >> z)) { error = "damaged STL file: " + path; return false; }
|
||||||
|
vertex(x, y, z);
|
||||||
|
}
|
||||||
|
md.positions.resize(md.positions.size() / 9 * 9); // only whole triangles
|
||||||
|
}
|
||||||
|
for (float v : md.positions) if (!std::isfinite(v)) { error = "damaged STL file (bad numbers): " + path; return false; }
|
||||||
|
if (md.positions.empty()) { error = "no triangles in " + path; return false; }
|
||||||
|
finishPlain(md, path);
|
||||||
|
files.push_back(path);
|
||||||
|
meshes.push_back(std::move(md));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PLY (scanners, photogrammetry, MeshLab): ASCII or binary, any element layout. The vertices' x y z, colours
|
||||||
|
// (red green blue [alpha], 0..255 or 0..1) and texture coordinates (s t / u v / texture_u texture_v) when present, and
|
||||||
|
// the faces' index lists, fanned into triangles. Axes as stored (scanners mostly write Y-up). A PLY with Gaussian
|
||||||
|
// splat properties, or only points, isn't a mesh: said so.
|
||||||
|
bool importPly(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
||||||
|
const std::vector<u8> b = readFile(path);
|
||||||
|
const std::string bad = "damaged or unsupported PLY file: " + path;
|
||||||
|
struct Prop { std::string name, type, countType; bool list = false; };
|
||||||
|
struct Element { std::string name; uint64_t count = 0; std::vector<Prop> props; };
|
||||||
|
std::vector<Element> els;
|
||||||
|
int format = -1; // 0 ascii, 1 binary little-endian, 2 binary big-endian
|
||||||
|
size_t pos = 0;
|
||||||
|
{ // the header: text lines up to "end_header"
|
||||||
|
const std::string head(b.begin(), b.begin() + std::min<size_t>(b.size(), 1 << 16));
|
||||||
|
const size_t end = head.find("end_header");
|
||||||
|
if (head.rfind("ply", 0) != 0 || end == std::string::npos) { error = "not a PLY file: " + path; return false; }
|
||||||
|
pos = head.find('\n', end);
|
||||||
|
if (pos == std::string::npos) { error = bad; return false; }
|
||||||
|
++pos;
|
||||||
|
std::istringstream in(head.substr(0, end));
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(in, line)) {
|
||||||
|
std::istringstream ls(line);
|
||||||
|
std::string w;
|
||||||
|
ls >> w;
|
||||||
|
if (w == "format") {
|
||||||
|
std::string f;
|
||||||
|
ls >> f;
|
||||||
|
format = f == "ascii" ? 0 : f == "binary_little_endian" ? 1 : f == "binary_big_endian" ? 2 : -1;
|
||||||
|
} else if (w == "element") {
|
||||||
|
Element e;
|
||||||
|
ls >> e.name >> e.count;
|
||||||
|
els.push_back(e);
|
||||||
|
} else if (w == "property" && !els.empty()) {
|
||||||
|
Prop p;
|
||||||
|
ls >> p.type;
|
||||||
|
if (p.type == "list") { p.list = true; ls >> p.countType >> p.type; }
|
||||||
|
ls >> p.name;
|
||||||
|
els.back().props.push_back(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (format < 0) { error = bad; return false; }
|
||||||
|
for (const Element& e : els)
|
||||||
|
for (const Prop& p : e.props)
|
||||||
|
if (e.name == "vertex" && (p.name == "f_dc_0" || p.name == "scale_0")) {
|
||||||
|
error = "this PLY holds Gaussian splats, which Frame Studio can't open yet: " + path;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto size = [](const std::string& t) -> int {
|
||||||
|
if (t == "char" || t == "uchar" || t == "int8" || t == "uint8") return 1;
|
||||||
|
if (t == "short" || t == "ushort" || t == "int16" || t == "uint16") return 2;
|
||||||
|
if (t == "int" || t == "uint" || t == "int32" || t == "uint32" || t == "float" || t == "float32") return 4;
|
||||||
|
if (t == "double" || t == "float64") return 8;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
std::istringstream ascii(format == 0 ? std::string(b.begin() + pos, b.end()) : std::string());
|
||||||
|
bool ok = true;
|
||||||
|
auto read = [&](const std::string& t) -> double { // one value of type t, at pos (binary) or the next word (ASCII)
|
||||||
|
if (format == 0) { double v = 0; if (!(ascii >> v)) ok = false; return v; }
|
||||||
|
const int n = size(t);
|
||||||
|
if (n == 0 || pos + n > b.size()) { ok = false; return 0; }
|
||||||
|
u8 raw[8];
|
||||||
|
std::memcpy(raw, &b[pos], n);
|
||||||
|
pos += n;
|
||||||
|
if (format == 2) std::reverse(raw, raw + n);
|
||||||
|
if (t == "char" || t == "int8") return static_cast<int8_t>(raw[0]);
|
||||||
|
if (t == "uchar" || t == "uint8") return raw[0];
|
||||||
|
if (t == "short" || t == "int16") { int16_t v; std::memcpy(&v, raw, 2); return v; }
|
||||||
|
if (t == "ushort" || t == "uint16") { uint16_t v; std::memcpy(&v, raw, 2); return v; }
|
||||||
|
if (t == "int" || t == "int32") { int32_t v; std::memcpy(&v, raw, 4); return v; }
|
||||||
|
if (t == "uint" || t == "uint32") { uint32_t v; std::memcpy(&v, raw, 4); return v; }
|
||||||
|
if (t == "float" || t == "float32") { float v; std::memcpy(&v, raw, 4); return v; }
|
||||||
|
double v; std::memcpy(&v, raw, 8); return v;
|
||||||
|
};
|
||||||
|
std::vector<float> xyz, uv;
|
||||||
|
std::vector<u8> rgba;
|
||||||
|
std::vector<u32> tris;
|
||||||
|
bool colours = false, coords = false;
|
||||||
|
for (const Element& e : els) {
|
||||||
|
// a hostile count: never more items than bytes left (each takes at least one)
|
||||||
|
if (e.count > b.size()) { error = bad; return false; }
|
||||||
|
const bool vert = e.name == "vertex", face = e.name == "face";
|
||||||
|
if (vert) {
|
||||||
|
xyz.reserve(e.count * 3);
|
||||||
|
for (const Prop& p : e.props) {
|
||||||
|
colours |= p.name == "red";
|
||||||
|
coords |= p.name == "s" || p.name == "u" || p.name == "texture_u";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (uint64_t i = 0; i < e.count && ok; ++i) {
|
||||||
|
float v[3] = {0, 0, 0}, t[2] = {0, 0}, c[4] = {1, 1, 1, 1};
|
||||||
|
for (const Prop& p : e.props) {
|
||||||
|
if (p.list) {
|
||||||
|
const double n = read(p.countType);
|
||||||
|
if (!ok || n < 0 || n > 1024) { ok = false; break; }
|
||||||
|
std::vector<u32> poly(static_cast<size_t>(n));
|
||||||
|
for (u32& k : poly) { // (a negative, huge or NaN index is no vertex: refused below, never an out-of-range cast)
|
||||||
|
const double d = read(p.type);
|
||||||
|
k = d >= 0 && d < 4294967295.0 ? static_cast<u32>(d) : UINT32_MAX;
|
||||||
|
}
|
||||||
|
if (face && (p.name == "vertex_indices" || p.name == "vertex_index"))
|
||||||
|
for (size_t k = 2; k < poly.size(); ++k) tris.insert(tris.end(), {poly[0], poly[k - 1], poly[k]});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const double x = read(p.type);
|
||||||
|
if (!vert) continue;
|
||||||
|
const bool byteColour = size(p.type) == 1;
|
||||||
|
if (p.name == "x") v[0] = static_cast<float>(x);
|
||||||
|
else if (p.name == "y") v[1] = static_cast<float>(x);
|
||||||
|
else if (p.name == "z") v[2] = static_cast<float>(x);
|
||||||
|
else if (p.name == "s" || p.name == "u" || p.name == "texture_u") t[0] = static_cast<float>(x);
|
||||||
|
else if (p.name == "t" || p.name == "v" || p.name == "texture_v") t[1] = static_cast<float>(x);
|
||||||
|
else if (p.name == "red" || p.name == "green" || p.name == "blue" || p.name == "alpha") {
|
||||||
|
// 0..255 (uchar), 0..65535 (ushort) or 0..1 (float)
|
||||||
|
const double unit = byteColour ? x / 255 : size(p.type) == 2 && p.type.find('u') == 0 ? x / 65535 : x;
|
||||||
|
c[p.name == "red" ? 0 : p.name == "green" ? 1 : p.name == "blue" ? 2 : 3] = static_cast<float>(unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (vert) {
|
||||||
|
xyz.insert(xyz.end(), v, v + 3);
|
||||||
|
uv.insert(uv.end(), {t[0], 1.0f - t[1]});
|
||||||
|
// Scanners and MeshLab store display (sRGB) colours; the shader works in linear light, as glTF's COLOR_0 is.
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
const float s = std::clamp(c[k], 0.0f, 1.0f);
|
||||||
|
c[k] = s <= 0.04045f ? s / 12.92f : std::pow((s + 0.055f) / 1.055f, 2.4f);
|
||||||
|
}
|
||||||
|
for (float k : c) rgba.push_back(static_cast<u8>(std::clamp(k, 0.0f, 1.0f) * 255.0f + 0.5f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ok) { error = bad; return false; }
|
||||||
|
}
|
||||||
|
for (float v : xyz) if (!std::isfinite(v)) { error = "damaged PLY file (bad numbers): " + path; return false; }
|
||||||
|
if (tris.empty()) { error = "this PLY has only points (a point cloud), not faces: Frame Studio can't open those yet: " + path; return false; }
|
||||||
|
MeshData md;
|
||||||
|
for (u32 k : tris) {
|
||||||
|
if (k >= xyz.size() / 3) { error = bad; return false; }
|
||||||
|
md.positions.insert(md.positions.end(), &xyz[k * 3], &xyz[k * 3] + 3);
|
||||||
|
if (colours) md.colors.insert(md.colors.end(), &rgba[k * 4], &rgba[k * 4] + 4);
|
||||||
|
}
|
||||||
|
finishPlain(md, path);
|
||||||
|
for (size_t i = 3; i < md.colors.size() && !md.useBlend; i += 4) md.useBlend = md.colors[i] < 250; // see-through vertices: blended
|
||||||
|
if (coords) for (size_t i = 0; i < tris.size(); ++i) { md.uvs[i * 2] = uv[tris[i] * 2]; md.uvs[i * 2 + 1] = uv[tris[i] * 2 + 1]; }
|
||||||
|
files.push_back(path);
|
||||||
|
meshes.push_back(std::move(md));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A panorama sky (equirectangular: 360 x 180 degrees, width = 2 x height) made into the six faces of a sky cube, each
|
||||||
|
// a quad with its own picture, drawn as a home's own skybox is (the unit cube follows the head, behind everything).
|
||||||
|
// The middle of the picture faces -Z (ahead). Radiance .hdr is shown at its own exposure (clipped at white).
|
||||||
|
bool importSkyPanorama(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
||||||
|
const std::vector<u8> b = readFile(path);
|
||||||
|
if (b.empty()) { error = "can't read the panorama " + path; return false; }
|
||||||
|
if (!sizeOk(b.data(), b.size())) { error = "not a picture Frame Studio can read (.hdr, .jpg, .png), or larger than 16384 x 8192: " + path; return false; }
|
||||||
|
const bool hdr = b.size() > 10 && (std::memcmp(b.data(), "#?RADIANCE", 10) == 0 || std::memcmp(b.data(), "#?RGBE", 6) == 0);
|
||||||
|
int w = 0, h = 0, comp = 0;
|
||||||
|
std::vector<float> px; // RGB: linear (HDR) or 0..1 as stored (sRGB pictures)
|
||||||
|
if (hdr) {
|
||||||
|
float* f = stbi_loadf_from_memory(b.data(), static_cast<int>(b.size()), &w, &h, &comp, 3);
|
||||||
|
if (f) { px.assign(f, f + static_cast<size_t>(w) * h * 3); stbi_image_free(f); }
|
||||||
|
} else {
|
||||||
|
unsigned char* c = stbi_load_from_memory(b.data(), static_cast<int>(b.size()), &w, &h, &comp, 3);
|
||||||
|
if (c) {
|
||||||
|
px.resize(static_cast<size_t>(w) * h * 3);
|
||||||
|
for (size_t i = 0; i < px.size(); ++i) px[i] = c[i] / 255.0f;
|
||||||
|
stbi_image_free(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (px.empty() || w < 8 || h < 4) { error = "not a picture Frame Studio can read (.hdr, .jpg, .png): " + path; return false; }
|
||||||
|
// A face spans 90 degrees = a quarter of the width; at most 2048 (8K panoramas), at least 64.
|
||||||
|
const u32 n = static_cast<u32>(std::clamp(w / 4, 64, 2048));
|
||||||
|
auto sample = [&](const float d[3], float out[3]) { // bilinear, wrapping round, clamped at the poles
|
||||||
|
const float len = std::sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
|
||||||
|
const float lon = std::atan2(d[0], -d[2]), lat = std::asin(std::clamp(d[1] / len, -1.0f, 1.0f));
|
||||||
|
const float x = (0.5f + lon / 6.2831853f) * w - 0.5f, y = (0.5f - lat / 3.14159265f) * h - 0.5f;
|
||||||
|
const int x0 = static_cast<int>(std::floor(x)), y0 = static_cast<int>(std::floor(y));
|
||||||
|
const float fx = x - x0, fy = y - y0;
|
||||||
|
auto at = [&](int xx, int yy, int c) {
|
||||||
|
xx = ((xx % w) + w) % w;
|
||||||
|
yy = std::clamp(yy, 0, h - 1);
|
||||||
|
return px[(static_cast<size_t>(yy) * w + xx) * 3 + c];
|
||||||
|
};
|
||||||
|
for (int c = 0; c < 3; ++c)
|
||||||
|
out[c] = (at(x0, y0, c) * (1 - fx) + at(x0 + 1, y0, c) * fx) * (1 - fy) + (at(x0, y0 + 1, c) * (1 - fx) + at(x0 + 1, y0 + 1, c) * fx) * fy;
|
||||||
|
};
|
||||||
|
struct Face { const char* name; float f[3], r[3], u[3]; };
|
||||||
|
static const Face faces[6] = {
|
||||||
|
{"ahead", {0, 0, -1}, {1, 0, 0}, {0, 1, 0}}, {"behind", {0, 0, 1}, {-1, 0, 0}, {0, 1, 0}},
|
||||||
|
{"right", {1, 0, 0}, {0, 0, 1}, {0, 1, 0}}, {"left", {-1, 0, 0}, {0, 0, -1}, {0, 1, 0}},
|
||||||
|
{"up", {0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, {"down", {0, -1, 0}, {1, 0, 0}, {0, 0, -1}},
|
||||||
|
};
|
||||||
|
const std::string stem = fs::u8path(path).stem().stem().u8string(); // ("dusk.sky.hdr": "dusk")
|
||||||
|
const float white[4] = {1, 1, 1, 1};
|
||||||
|
for (const Face& fc : faces) {
|
||||||
|
Image img;
|
||||||
|
img.w = img.h = n;
|
||||||
|
img.rgba.resize(static_cast<size_t>(n) * n * 4);
|
||||||
|
// Texel centres run edge to edge (the outer ones ON the cube's edges) and the quad's UVs stop half a texel in,
|
||||||
|
// so neighbouring faces meet on the same colours: no seam, and the wrapping sampler never reaches across.
|
||||||
|
for (u32 j = 0; j < n; ++j)
|
||||||
|
for (u32 i = 0; i < n; ++i) {
|
||||||
|
const float a = -1.0f + 2.0f * i / (n - 1), bb = 1.0f - 2.0f * j / (n - 1);
|
||||||
|
const float d[3] = {fc.f[0] + a * fc.r[0] + bb * fc.u[0], fc.f[1] + a * fc.r[1] + bb * fc.u[1], fc.f[2] + a * fc.r[2] + bb * fc.u[2]};
|
||||||
|
float c[3];
|
||||||
|
sample(d, c);
|
||||||
|
u8* o = &img.rgba[(static_cast<size_t>(j) * n + i) * 4];
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
float s = std::clamp(c[k], 0.0f, 1.0f);
|
||||||
|
if (hdr) s = s <= 0.0031308f ? s * 12.92f : 1.055f * std::pow(s, 1.0f / 2.4f) - 0.055f; // linear -> sRGB
|
||||||
|
o[k] = static_cast<u8>(s * 255.0f + 0.5f);
|
||||||
|
}
|
||||||
|
o[3] = 255;
|
||||||
|
}
|
||||||
|
MeshData md;
|
||||||
|
md.name = "Sky " + std::string(fc.name);
|
||||||
|
md.path = stem + " (panorama sky)";
|
||||||
|
const float hf = 0.5f / n;
|
||||||
|
const float corner[4][4] = {{-1, 1, hf, hf}, {1, 1, 1 - hf, hf}, {1, -1, 1 - hf, 1 - hf}, {-1, -1, hf, 1 - hf}};
|
||||||
|
for (const auto& q : corner) {
|
||||||
|
for (int k = 0; k < 3; ++k) md.positions.push_back(fc.f[k] + q[0] * fc.r[k] + q[1] * fc.u[k]);
|
||||||
|
md.uvs.insert(md.uvs.end(), {q[2], q[3]});
|
||||||
|
}
|
||||||
|
md.indices = {0, 2, 1, 0, 3, 2};
|
||||||
|
finish(md, &img, white, false, false, 0.5f);
|
||||||
|
md.isSkybox = true;
|
||||||
|
meshes.push_back(std::move(md));
|
||||||
|
}
|
||||||
|
files.push_back(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
bool isSkyPanoramaPath(const std::string& path) {
|
||||||
|
const std::string name = lower(fs::u8path(path).filename().u8string());
|
||||||
|
for (const char* e : {".sky.hdr", ".sky.jpg", ".sky.jpeg", ".sky.png"})
|
||||||
|
if (name.size() > std::strlen(e) && name.compare(name.size() - std::strlen(e), std::string::npos, e) == 0) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool isModelPath(const std::string& path) {
|
bool isModelPath(const std::string& path) {
|
||||||
const std::string ext = lower(fs::u8path(path).extension().u8string());
|
const std::string ext = lower(fs::u8path(path).extension().u8string());
|
||||||
return ext == ".gltf" || ext == ".glb" || ext == ".fbx" || ext == ".obj";
|
return ext == ".gltf" || ext == ".glb" || ext == ".fbx" || ext == ".obj" || ext == ".stl" || ext == ".ply" || isSkyPanoramaPath(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool importModel(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
bool importModel(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
||||||
const std::string ext = lower(fs::u8path(path).extension().u8string());
|
// Each runs on a loader thread of its own: a model too big for memory (bad_alloc, length_error) must come back as
|
||||||
if (ext == ".gltf" || ext == ".glb") return importGltf(path, meshes, files, error);
|
// an error, not escape the thread and end the program (with the open home's unsaved changes).
|
||||||
if (ext == ".fbx" || ext == ".obj") return importUfbx(path, meshes, files, error);
|
const size_t meshesBefore = meshes.size(), filesBefore = files.size();
|
||||||
error = "unsupported model type: " + path;
|
try {
|
||||||
return false;
|
if (isSkyPanoramaPath(path)) return importSkyPanorama(path, meshes, files, error);
|
||||||
|
const std::string ext = lower(fs::u8path(path).extension().u8string());
|
||||||
|
if (ext == ".gltf" || ext == ".glb") return importGltf(path, meshes, files, error);
|
||||||
|
if (ext == ".fbx" || ext == ".obj") return importUfbx(path, meshes, files, error);
|
||||||
|
if (ext == ".stl") return importStl(path, meshes, files, error);
|
||||||
|
if (ext == ".ply") return importPly(path, meshes, files, error);
|
||||||
|
error = "unsupported model type: " + path;
|
||||||
|
return false;
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
meshes.resize(meshesBefore);
|
||||||
|
files.resize(filesBefore);
|
||||||
|
error = "not enough memory to open " + path;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// Models from Blender or Unity placed into a home: glTF 2.0 (.gltf / .glb — Blender's native export, Unity's
|
// Models from Blender or Unity placed into a home: glTF 2.0 (.gltf / .glb — Blender's native export, Unity's
|
||||||
// glTFast / UnityGLTF), FBX and OBJ (both tools' classic exports). Each mesh part becomes one MeshData drawn
|
// glTFast / UnityGLTF), FBX and OBJ (both tools' classic exports), STL (3D printing, CAD) and PLY meshes (scans,
|
||||||
|
// photogrammetry; vertex colours kept). Each mesh part becomes one MeshData drawn
|
||||||
// with the built-in unlit textured program (the same one Clay homes use): base-colour texture x colour factor,
|
// with the built-in unlit textured program (the same one Clay homes use): base-colour texture x colour factor,
|
||||||
// alpha blending or cut-out from the material. Units become metres and up becomes +Y.
|
// alpha blending or cut-out from the material. Units become metres and up becomes +Y.
|
||||||
#include "core/types.h"
|
#include "core/types.h"
|
||||||
@@ -10,5 +11,9 @@
|
|||||||
|
|
||||||
bool isModelPath(const std::string& path);
|
bool isModelPath(const std::string& path);
|
||||||
|
|
||||||
|
// A panorama sky chosen in Studio: an equirectangular picture named "<name>.sky.hdr|.jpg|.jpeg|.png" (Studio writes
|
||||||
|
// that copy). It imports as a model does, as the six faces of a sky cube (isSkybox), and replaces the home's own sky.
|
||||||
|
bool isSkyPanoramaPath(const std::string& path);
|
||||||
|
|
||||||
// `files` receives every file the model read (itself, external buffers, textures), so a .fenv can carry them.
|
// `files` receives every file the model read (itself, external buffers, textures), so a .fenv can carry them.
|
||||||
bool importModel(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error);
|
bool importModel(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error);
|
||||||
@@ -284,7 +284,7 @@ bool bakeScene(const HslScene& scene, std::vector<u8>& out, std::string& error,
|
|||||||
for (size_t i = 0; i < s.meshes.size(); ++i) writeMesh(mw, *meshes[i], tex, half, vis && vis->changed[i] ? &vis->indices[i] : nullptr);
|
for (size_t i = 0; i < s.meshes.size(); ++i) writeMesh(mw, *meshes[i], tex, half, vis && vis->changed[i] ? &vis->indices[i] : nullptr);
|
||||||
|
|
||||||
Writer w{out};
|
Writer w{out};
|
||||||
w.u(kMagic); w.u(kSceneBakeVersion);
|
w.u(kMagic); w.u(sceneBakeVersionFor(s));
|
||||||
w.u(static_cast<u32>(s.apkPaths.size()));
|
w.u(static_cast<u32>(s.apkPaths.size()));
|
||||||
for (size_t i = 0; i < s.apkPaths.size(); ++i) {
|
for (size_t i = 0; i < s.apkPaths.size(); ++i) {
|
||||||
w.str(s.apkPaths[i]);
|
w.str(s.apkPaths[i]);
|
||||||
@@ -465,6 +465,9 @@ bool unbakeScene(const u8* data, size_t size, HslScene& s, std::string& error) {
|
|||||||
u32 chain = 1; // a mip count past the image's own chain: the levels it can have
|
u32 chain = 1; // a mip count past the image's own chain: the levels it can have
|
||||||
while ((std::max(ts->w, ts->h) >> chain) != 0) ++chain;
|
while ((std::max(ts->w, ts->h) >> chain) != 0) ++chain;
|
||||||
ts->mips = std::min(std::max(ts->mips, 1u), chain);
|
ts->mips = std::min(std::max(ts->mips, 1u), chain);
|
||||||
|
// ...and only the levels its bytes hold (a damaged file): the GPU image is made with that many levels, and a
|
||||||
|
// level with nothing uploaded into it would be sampled as whatever the memory held.
|
||||||
|
while (ts->mips > 1 && static_cast<u64>(astcMipBytesForCount(ts->w, ts->h, ts->bw, ts->bh, ts->mips)) * ts->faces > ts->astc.size()) --ts->mips;
|
||||||
t = ts;
|
t = ts;
|
||||||
}
|
}
|
||||||
std::vector<std::shared_ptr<const std::vector<u16>>> half(r.count(4));
|
std::vector<std::shared_ptr<const std::vector<u16>>> half(r.count(4));
|
||||||
|
|||||||
@@ -11,7 +11,11 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
constexpr u32 kSceneBakeVersion = 14; // 2: world; 3: HDR maps as ASTC, binary world; 4: roles; 5: mesh refs; 6: gravity; 7: mesh paths; 8: ambience name; 9: compressed 8-bit lightmaps; 10: reflection cube as ASTC; 11: walkable area (and simplified colliders); 12: prepared and specialized shaders; 13: Clay / glTF CPU animation; 14: wall spots removed
|
constexpr u32 kSceneBakeVersion = 15; // 2: world; 3: HDR maps as ASTC, binary world; 4: roles; 5: mesh refs; 6: gravity; 7: mesh paths; 8: ambience name; 9: compressed 8-bit lightmaps; 10: reflection cube as ASTC; 11: walkable area (and simplified colliders); 12: prepared and specialized shaders; 13: Clay / glTF CPU animation; 14: wall spots removed; 15: the environment has splats (nothing new in here: they're .fenv members, and a reader older than splats must refuse it rather than show it without them)
|
||||||
|
// What a scene is written as: the oldest version that holds it, so one without splats stays 14 (older Frame Homes still
|
||||||
|
// open it). A reader of a file needs at least this.
|
||||||
|
constexpr u32 kSceneBakeVersionNoSplats = 14;
|
||||||
|
inline u32 sceneBakeVersionFor(const HslScene& scene) { return scene.splats.empty() ? kSceneBakeVersionNoSplats : kSceneBakeVersion; }
|
||||||
|
|
||||||
struct VisibilityResult;
|
struct VisibilityResult;
|
||||||
struct ObjectEdit;
|
struct ObjectEdit;
|
||||||
|
|||||||
@@ -7,12 +7,16 @@
|
|||||||
// compute every position bit for bit alike)
|
// compute every position bit for bit alike)
|
||||||
// - alphaOnly: the fragment shader writing only its alpha (the cut-out depth pass)
|
// - alphaOnly: the fragment shader writing only its alpha (the cut-out depth pass)
|
||||||
// - specialize: per material, the uniform values that never change baked in as constants (spv_specialize.h)
|
// - specialize: per material, the uniform values that never change baked in as constants (spv_specialize.h)
|
||||||
|
// - optimized: each of those through spv_opt.h when it's switched on, so a .fenv's home loads without making them;
|
||||||
|
// the shipped vertex and fragment shaders stay as they are (the renderer lays a program out from them, and a
|
||||||
|
// variant the bake didn't make is specialized from them)
|
||||||
// The set-2 (material) uniform buffers are filled here too, by the same code for both, so the bytes a baked shader
|
// The set-2 (material) uniform buffers are filled here too, by the same code for both, so the bytes a baked shader
|
||||||
// assumes are the bytes the renderer uploads.
|
// assumes are the bytes the renderer uploads.
|
||||||
#include "core/types.h"
|
#include "core/types.h"
|
||||||
#include "hsl_scene.h"
|
#include "hsl_scene.h"
|
||||||
#include "spv_reflect.h"
|
#include "spv_reflect.h"
|
||||||
#include "spv_specialize.h"
|
#include "spv_specialize.h"
|
||||||
|
#include "spv_opt.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
@@ -327,6 +331,14 @@ inline spvspec::Fetch fetchFor(const SceneConstants& sc, const std::vector<spv::
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether a baked shader can be used as it is: made with the optimizer flags this run has (spv_opt.h), or by a build
|
||||||
|
// from before it (the renderer makes that one as it loads). One made otherwise (an A/B run with FH_SPV_OPT or
|
||||||
|
// FH_SPV_HALF against a .fenv baked without them, or the other way round) is made again from the shipped shader.
|
||||||
|
inline bool bakedUsable(const std::vector<u32>& code) {
|
||||||
|
const int m = spvopt::markOf(code);
|
||||||
|
return m < 0 || m == spvopt::flags();
|
||||||
|
}
|
||||||
|
|
||||||
// Which float values specialize bakes in: 0 none, 1 zeros, 2 zeros and +-1 (the default; FH_SPEC_FLOATS).
|
// Which float values specialize bakes in: 0 none, 1 zeros, 2 zeros and +-1 (the default; FH_SPEC_FLOATS).
|
||||||
inline int floatsMode() {
|
inline int floatsMode() {
|
||||||
static const int m = std::getenv("FH_SPEC_FLOATS") ? std::atoi(std::getenv("FH_SPEC_FLOATS")) : 2;
|
static const int m = std::getenv("FH_SPEC_FLOATS") ? std::atoi(std::getenv("FH_SPEC_FLOATS")) : 2;
|
||||||
@@ -364,8 +376,9 @@ inline void bakePrograms(const std::vector<MeshData>& meshes, const std::functio
|
|||||||
std::map<std::string, Info> info;
|
std::map<std::string, Info> info;
|
||||||
for (auto& kv : programs) {
|
for (auto& kv : programs) {
|
||||||
ShaderProgramSrc& p = kv.second;
|
ShaderProgramSrc& p = kv.second;
|
||||||
p.vertInvariant = invariantPosition(p.vert);
|
p.vertInvariant = spvopt::run(invariantPosition(p.vert));
|
||||||
p.fragAlpha = alphaOnly(p.frag);
|
p.fragAlpha = alphaOnly(p.frag);
|
||||||
|
if (!p.fragAlpha.empty()) p.fragAlpha = spvopt::run(p.fragAlpha);
|
||||||
p.variants.clear();
|
p.variants.clear();
|
||||||
p.prepared = true;
|
p.prepared = true;
|
||||||
Info& in = info[kv.first];
|
Info& in = info[kv.first];
|
||||||
@@ -400,7 +413,9 @@ inline void bakePrograms(const std::vector<MeshData>& meshes, const std::functio
|
|||||||
v.key = key;
|
v.key = key;
|
||||||
v.frag = spvspec::specialize(p.frag, fetch, &folded, -1, floats);
|
v.frag = spvspec::specialize(p.frag, fetch, &folded, -1, floats);
|
||||||
if (!folded) v.frag.clear(); // (nothing to bake in: the shader as shipped)
|
if (!folded) v.frag.clear(); // (nothing to bake in: the shader as shipped)
|
||||||
else v.fragAlpha = alphaOnly(v.frag);
|
else v.fragAlpha = alphaOnly(v.frag); // (from the specialized shader as it is: its whole-colour stores)
|
||||||
|
if (!v.frag.empty()) v.frag = spvopt::run(v.frag);
|
||||||
|
if (!v.fragAlpha.empty()) v.fragAlpha = spvopt::run(v.fragAlpha);
|
||||||
p.variants.push_back(std::move(v));
|
p.variants.push_back(std::move(v));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,428 @@
|
|||||||
|
#include "splat_import.h"
|
||||||
|
|
||||||
|
#include "miniz.h"
|
||||||
|
#include "zstd.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <functional>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
constexpr float kShC0 = 0.28209479177387814f; // degree-0 spherical harmonic: base colour = 0.5 + C0 * dc
|
||||||
|
constexpr uint64_t kMaxSplats = 20'000'000; // far beyond any capture; a hostile count can't run memory out
|
||||||
|
|
||||||
|
std::string lower(std::string s) {
|
||||||
|
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> readFile(const std::string& path) {
|
||||||
|
std::ifstream in(fs::u8path(path), std::ios::binary);
|
||||||
|
return std::vector<uint8_t>((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// (NaN, from a damaged file, is 0: clamp lets it through, and NaN to a byte is undefined)
|
||||||
|
uint8_t unit(float v) { return v > 0 ? static_cast<uint8_t>(std::min(v, 1.0f) * 255.0f + 0.5f) : 0; }
|
||||||
|
float sigmoid(float x) { return 1.0f / (1.0f + std::exp(-x)); }
|
||||||
|
|
||||||
|
// A finished splat: the rotation normalised (files store it loosely), nothing non-finite (a damaged file).
|
||||||
|
bool keep(Splat& s) {
|
||||||
|
float n = 0;
|
||||||
|
for (float q : s.rot) n += q * q;
|
||||||
|
if (!(n > 1e-12f) || !std::isfinite(n)) return false;
|
||||||
|
n = 1.0f / std::sqrt(n);
|
||||||
|
for (float& q : s.rot) q *= n;
|
||||||
|
for (int k = 0; k < 3; ++k) if (!std::isfinite(s.pos[k]) || !std::isfinite(s.scale[k])) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 3DGS .ply and .splat use the training cameras' axes (x right, y down, z forward): turned 180 degrees about x
|
||||||
|
// into Frame's (y up, z toward the viewer). Quaternion: q' = (1,0,0,0 as xyzw) * q.
|
||||||
|
void fromRdf(Splat& s) {
|
||||||
|
s.pos[1] = -s.pos[1];
|
||||||
|
s.pos[2] = -s.pos[2];
|
||||||
|
const float x = s.rot[0], y = s.rot[1], z = s.rot[2], w = s.rot[3];
|
||||||
|
s.rot[0] = w; s.rot[1] = -z; s.rot[2] = y; s.rot[3] = -x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3DGS .ply: binary (little- or big-endian) or ASCII vertices; x y z, f_dc_0..2 (colour SH), opacity (logit),
|
||||||
|
// scale_0..2 (log), rot_0..3 (w x y z). Other properties (normals, f_rest_*) are skipped by their size. ──
|
||||||
|
bool loadPly(const std::vector<uint8_t>& b, SplatCloud& out, std::string& error) {
|
||||||
|
const std::string head(b.begin(), b.begin() + std::min<size_t>(b.size(), 1 << 16));
|
||||||
|
const size_t end = head.find("end_header");
|
||||||
|
if (head.rfind("ply", 0) != 0 || end == std::string::npos || head.find('\n', end) == std::string::npos) { error = "not a PLY file"; return false; }
|
||||||
|
size_t pos = head.find('\n', end) + 1;
|
||||||
|
struct Prop { std::string name, type; bool list = false; };
|
||||||
|
std::vector<std::pair<std::string, uint64_t>> elements;
|
||||||
|
std::vector<std::vector<Prop>> props;
|
||||||
|
int format = -1;
|
||||||
|
std::istringstream in(head.substr(0, end));
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(in, line)) {
|
||||||
|
std::istringstream ls(line);
|
||||||
|
std::string w;
|
||||||
|
ls >> w;
|
||||||
|
if (w == "format") { std::string f; ls >> f; format = f == "ascii" ? 0 : f == "binary_little_endian" ? 1 : f == "binary_big_endian" ? 2 : -1; }
|
||||||
|
else if (w == "element") { std::string n; uint64_t c = 0; ls >> n >> c; elements.push_back({n, c}); props.emplace_back(); }
|
||||||
|
else if (w == "property" && !props.empty()) { Prop p; ls >> p.type; if (p.type == "list") { p.list = true; std::string a; ls >> a >> p.type; } ls >> p.name; props.back().push_back(p); }
|
||||||
|
}
|
||||||
|
auto size = [](const std::string& t) -> int {
|
||||||
|
if (t == "char" || t == "uchar" || t == "int8" || t == "uint8") return 1;
|
||||||
|
if (t == "short" || t == "ushort" || t == "int16" || t == "uint16") return 2;
|
||||||
|
if (t == "int" || t == "uint" || t == "int32" || t == "uint32" || t == "float" || t == "float32") return 4;
|
||||||
|
if (t == "double" || t == "float64") return 8;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
if (format < 0 || elements.empty() || elements[0].first != "vertex") { error = "unsupported splat PLY (it must start with its vertices)"; return false; }
|
||||||
|
const uint64_t count = elements[0].second;
|
||||||
|
const std::vector<Prop>& vp = props[0];
|
||||||
|
int stride = 0;
|
||||||
|
for (const Prop& p : vp) { if (p.list || size(p.type) == 0) { error = "unsupported splat PLY property " + p.name; return false; } stride += size(p.type); }
|
||||||
|
if (count > kMaxSplats || (format != 0 && pos + count * static_cast<uint64_t>(stride) > b.size()) ||
|
||||||
|
(format == 0 && count * vp.size() > b.size() - pos)) { // (ASCII: each value at least a character)
|
||||||
|
error = "damaged splat PLY (fewer bytes than splats)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto index = [&](const char* n) { for (size_t i = 0; i < vp.size(); ++i) if (vp[i].name == n) return static_cast<int>(i); return -1; };
|
||||||
|
// each looked up by name (tools write them in different orders)
|
||||||
|
const int ix = index("x"), iy = index("y"), iz = index("z"), iop = index("opacity");
|
||||||
|
const int idc[3] = {index("f_dc_0"), index("f_dc_1"), index("f_dc_2")}, isc[3] = {index("scale_0"), index("scale_1"), index("scale_2")};
|
||||||
|
const int iro[4] = {index("rot_0"), index("rot_1"), index("rot_2"), index("rot_3")};
|
||||||
|
bool all = ix >= 0 && iy >= 0 && iz >= 0 && iop >= 0;
|
||||||
|
for (int k = 0; k < 3; ++k) all = all && idc[k] >= 0 && isc[k] >= 0;
|
||||||
|
for (int k = 0; k < 4; ++k) all = all && iro[k] >= 0;
|
||||||
|
if (!all) { error = "not a Gaussian splat PLY (no f_dc / opacity / scale / rot)"; return false; }
|
||||||
|
std::istringstream ascii(format == 0 ? std::string(b.begin() + pos, b.end()) : std::string());
|
||||||
|
std::vector<float> v(vp.size());
|
||||||
|
out.splats.reserve(count);
|
||||||
|
for (uint64_t i = 0; i < count; ++i) {
|
||||||
|
for (size_t k = 0; k < vp.size(); ++k) {
|
||||||
|
if (format == 0) { double d = 0; if (!(ascii >> d)) { error = "damaged splat PLY"; return false; } v[k] = static_cast<float>(d); continue; }
|
||||||
|
const int n = size(vp[k].type);
|
||||||
|
uint8_t raw[8];
|
||||||
|
std::memcpy(raw, &b[pos], n);
|
||||||
|
pos += n;
|
||||||
|
if (format == 2) std::reverse(raw, raw + n);
|
||||||
|
const std::string& t = vp[k].type;
|
||||||
|
if (t == "float" || t == "float32") { float f; std::memcpy(&f, raw, 4); v[k] = f; }
|
||||||
|
else if (n == 8) { double d; std::memcpy(&d, raw, 8); v[k] = static_cast<float>(d); }
|
||||||
|
else if (t == "uchar" || t == "uint8") v[k] = raw[0];
|
||||||
|
else if (t == "char" || t == "int8") v[k] = static_cast<int8_t>(raw[0]);
|
||||||
|
else if (n == 2) { int16_t s; uint16_t u; std::memcpy(&s, raw, 2); std::memcpy(&u, raw, 2); v[k] = t[0] == 'u' ? u : s; }
|
||||||
|
else { int32_t s; uint32_t u; std::memcpy(&s, raw, 4); std::memcpy(&u, raw, 4); v[k] = t[0] == 'u' ? static_cast<float>(u) : static_cast<float>(s); }
|
||||||
|
}
|
||||||
|
Splat s;
|
||||||
|
s.pos[0] = v[ix]; s.pos[1] = v[iy]; s.pos[2] = v[iz];
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
s.scale[k] = std::exp(v[isc[k]]);
|
||||||
|
s.rgba[k] = unit(0.5f + kShC0 * v[idc[k]]);
|
||||||
|
}
|
||||||
|
s.rgba[3] = unit(sigmoid(v[iop]));
|
||||||
|
s.rot[0] = v[iro[1]]; s.rot[1] = v[iro[2]]; s.rot[2] = v[iro[3]]; s.rot[3] = v[iro[0]]; // file: w x y z
|
||||||
|
fromRdf(s);
|
||||||
|
if (keep(s)) out.splats.push_back(s);
|
||||||
|
}
|
||||||
|
out.format = "3DGS .ply";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── .splat (antimatter15's web viewer): 32 bytes a splat: position 3 x f32, scale 3 x f32 (linear), colour RGBA u8
|
||||||
|
// (opacity in A), rotation 4 x u8 (w x y z, (v - 128) / 128). The 3DGS axes, as the .ply it was made from. ──
|
||||||
|
bool loadDotSplat(const std::vector<uint8_t>& b, SplatCloud& out, std::string& error) {
|
||||||
|
if (b.empty() || b.size() % 32 != 0) { error = "damaged .splat file (not whole 32-byte splats)"; return false; }
|
||||||
|
const size_t n = b.size() / 32;
|
||||||
|
if (n > kMaxSplats) { error = "too many splats"; return false; }
|
||||||
|
out.splats.reserve(n);
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
const uint8_t* p = &b[i * 32];
|
||||||
|
Splat s;
|
||||||
|
std::memcpy(s.pos, p, 12);
|
||||||
|
std::memcpy(s.scale, p + 12, 12);
|
||||||
|
std::memcpy(s.rgba, p + 24, 4);
|
||||||
|
s.rot[3] = (p[28] - 128) / 128.0f;
|
||||||
|
s.rot[0] = (p[29] - 128) / 128.0f;
|
||||||
|
s.rot[1] = (p[30] - 128) / 128.0f;
|
||||||
|
s.rot[2] = (p[31] - 128) / 128.0f;
|
||||||
|
fromRdf(s);
|
||||||
|
if (keep(s)) out.splats.push_back(s);
|
||||||
|
}
|
||||||
|
out.format = ".splat";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── .spz (Niantic, MIT). Version 2 and 3: gzip of a 16-byte header (magic "NGSP", version, count, SH degree,
|
||||||
|
// fractional bits, flags) then, splat by splat in columns: positions (24-bit fixed point), opacities, colours,
|
||||||
|
// scales, rotations, spherical harmonics. Version 4: the same columns, each its own zstd stream, after a plain
|
||||||
|
// 32-byte header and a table of the streams' sizes. Stored in Frame's own axes (RUB, as glTF). ──
|
||||||
|
// The raw deflate after the gzip header, unpacked in pieces. `limit(sofar)` says how much is wanted at most (it may
|
||||||
|
// tighten once the start says what the file holds): the unpacking stops there, so a small file that unpacks to
|
||||||
|
// gigabytes (a decompression bomb) can't run memory out. What's past the limit isn't unpacked (and isn't needed).
|
||||||
|
bool gunzip(const std::vector<uint8_t>& b, std::vector<uint8_t>& out, const std::function<size_t(const std::vector<uint8_t>&)>& limit) {
|
||||||
|
if (b.size() < 18 || b[0] != 0x1f || b[1] != 0x8b || b[2] != 8) return false;
|
||||||
|
const uint8_t flags = b[3];
|
||||||
|
size_t p = 10;
|
||||||
|
if (flags & 4) { if (p + 2 > b.size()) return false; p += 2 + (b[p] | (b[p + 1] << 8)); } // extra field
|
||||||
|
if (flags & 8) { while (p < b.size() && b[p]) ++p; ++p; } // file name
|
||||||
|
if (flags & 16) { while (p < b.size() && b[p]) ++p; ++p; } // comment
|
||||||
|
if (flags & 2) p += 2; // header CRC
|
||||||
|
if (p >= b.size() || b.size() - p > 0xffffffffu) return false;
|
||||||
|
mz_stream s = {};
|
||||||
|
if (mz_inflateInit2(&s, -MZ_DEFAULT_WINDOW_BITS) != MZ_OK) return false; // raw deflate
|
||||||
|
s.next_in = &b[p];
|
||||||
|
s.avail_in = static_cast<unsigned int>(b.size() - p);
|
||||||
|
out.clear();
|
||||||
|
bool ok = false;
|
||||||
|
for (;;) {
|
||||||
|
const size_t want = limit(out);
|
||||||
|
if (out.size() >= want) { out.resize(want); ok = true; break; }
|
||||||
|
const size_t at = out.size();
|
||||||
|
out.resize(std::min(want, at + std::max<size_t>(at, size_t(1) << 16)));
|
||||||
|
s.next_out = out.data() + at;
|
||||||
|
s.avail_out = static_cast<unsigned int>(out.size() - at);
|
||||||
|
const int rc = mz_inflate(&s, MZ_NO_FLUSH);
|
||||||
|
out.resize(out.size() - s.avail_out);
|
||||||
|
if (rc == MZ_STREAM_END) { ok = true; break; }
|
||||||
|
if (rc != MZ_OK) break; // damaged, or cut short
|
||||||
|
}
|
||||||
|
mz_inflateEnd(&s);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint32_t kSpzMagic = 0x5053474e; // "NGSP"
|
||||||
|
constexpr size_t kShDim[5] = {0, 3, 8, 15, 24}; // SH coefficients a colour channel, by degree
|
||||||
|
constexpr size_t kShDimMax = 24;
|
||||||
|
|
||||||
|
// The columns of a .spz, `count` splats each, whichever version they came from.
|
||||||
|
struct SpzColumns {
|
||||||
|
const uint8_t *pos, *alpha, *color, *scale, *rot;
|
||||||
|
uint32_t count;
|
||||||
|
int fracBits;
|
||||||
|
bool smallestThree; // version 3 on: 4-byte rotations
|
||||||
|
};
|
||||||
|
|
||||||
|
void decodeSpz(const SpzColumns& c, SplatCloud& out) {
|
||||||
|
const float fixed = 1.0f / static_cast<float>(1 << std::min(c.fracBits, 24));
|
||||||
|
constexpr float kColorScale = 0.15f; // how .spz packs the SH base colour into a byte
|
||||||
|
out.splats.reserve(c.count);
|
||||||
|
for (size_t i = 0; i < c.count; ++i) {
|
||||||
|
Splat s;
|
||||||
|
for (size_t k = 0; k < 3; ++k) {
|
||||||
|
const uint8_t* q = c.pos + (i * 3 + k) * 3;
|
||||||
|
int32_t f = q[0] | (q[1] << 8) | (q[2] << 16);
|
||||||
|
if (f & 0x800000) f |= ~0xffffff; // sign-extend 24 bits
|
||||||
|
s.pos[k] = static_cast<float>(f) * fixed;
|
||||||
|
s.scale[k] = std::exp(c.scale[i * 3 + k] / 16.0f - 10.0f);
|
||||||
|
const float dc = (c.color[i * 3 + k] / 255.0f - 0.5f) / kColorScale;
|
||||||
|
s.rgba[k] = unit(0.5f + kShC0 * dc);
|
||||||
|
}
|
||||||
|
s.rgba[3] = c.alpha[i]; // already the opacity (0..255)
|
||||||
|
if (c.smallestThree) { // "smallest three": 2 bits the largest component, then three 10-bit (9 + sign) others
|
||||||
|
const uint8_t* r = c.rot + i * 4;
|
||||||
|
uint32_t v = r[0] | (r[1] << 8) | (r[2] << 16) | (static_cast<uint32_t>(r[3]) << 24);
|
||||||
|
const int largest = static_cast<int>(v >> 30);
|
||||||
|
constexpr uint32_t mask = (1u << 9) - 1;
|
||||||
|
float sum = 0;
|
||||||
|
for (int k = 3; k >= 0; --k) {
|
||||||
|
if (k == largest) continue;
|
||||||
|
const float m = static_cast<float>(v & mask) / mask * 0.70710678f;
|
||||||
|
const bool neg = (v >> 9) & 1;
|
||||||
|
v >>= 10;
|
||||||
|
s.rot[k] = neg ? -m : m;
|
||||||
|
sum += m * m;
|
||||||
|
}
|
||||||
|
s.rot[largest] = std::sqrt(std::max(0.0f, 1.0f - sum));
|
||||||
|
} else {
|
||||||
|
float sum = 0;
|
||||||
|
for (size_t k = 0; k < 3; ++k) { s.rot[k] = c.rot[i * 3 + k] / 127.5f - 1.0f; sum += s.rot[k] * s.rot[k]; }
|
||||||
|
s.rot[3] = std::sqrt(std::max(0.0f, 1.0f - sum));
|
||||||
|
}
|
||||||
|
if (keep(s)) out.splats.push_back(s);
|
||||||
|
}
|
||||||
|
out.format = ".spz";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version 2 and 3: one gzip of the header and every column.
|
||||||
|
bool loadSpzGzip(const std::vector<uint8_t>& gz, SplatCloud& out, std::string& error) {
|
||||||
|
std::vector<uint8_t> b;
|
||||||
|
// At most what the header says the columns take (once the 16 header bytes are in), never past the largest a
|
||||||
|
// file may hold.
|
||||||
|
constexpr size_t kMostBytes = 16 + static_cast<size_t>(kMaxSplats) * (9 + 1 + 3 + 3 + 4 + kShDimMax * 3);
|
||||||
|
const auto limit = [&](const std::vector<uint8_t>& sofar) -> size_t {
|
||||||
|
if (sofar.size() < 16) return kMostBytes;
|
||||||
|
uint32_t v, n;
|
||||||
|
std::memcpy(&v, &sofar[4], 4); std::memcpy(&n, &sofar[8], 4);
|
||||||
|
if (n > kMaxSplats || sofar[12] > 4) return 16; // refused below
|
||||||
|
return 16 + static_cast<size_t>(n) * (9 + 1 + 3 + 3 + (v >= 3 ? 4 : 3) + kShDim[sofar[12]] * 3);
|
||||||
|
};
|
||||||
|
if (!gunzip(gz, b, limit) || b.size() < 16) { error = "damaged .spz file (not gzip)"; return false; }
|
||||||
|
uint32_t magic, version, count;
|
||||||
|
std::memcpy(&magic, &b[0], 4); std::memcpy(&version, &b[4], 4); std::memcpy(&count, &b[8], 4);
|
||||||
|
const int shDegree = b[12], fracBits = b[13];
|
||||||
|
if (magic != kSpzMagic) { error = "not a .spz file"; return false; }
|
||||||
|
if (version < 2 || version > 3) { error = "unsupported .spz version " + std::to_string(version); return false; }
|
||||||
|
if (count > kMaxSplats || shDegree > 4) { error = "damaged .spz file"; return false; }
|
||||||
|
const size_t n = count, rotBytes = version >= 3 ? 4 : 3;
|
||||||
|
const size_t need = 16 + n * (9 + 1 + 3 + 3 + rotBytes + kShDim[shDegree] * 3);
|
||||||
|
if (b.size() < need) { error = "damaged .spz file (cut short)"; return false; }
|
||||||
|
SpzColumns c;
|
||||||
|
c.pos = &b[16];
|
||||||
|
c.alpha = c.pos + n * 9;
|
||||||
|
c.color = c.alpha + n;
|
||||||
|
c.scale = c.color + n * 3;
|
||||||
|
c.rot = c.scale + n * 3;
|
||||||
|
c.count = count;
|
||||||
|
c.fracBits = fracBits;
|
||||||
|
c.smallestThree = version >= 3;
|
||||||
|
decodeSpz(c, out);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version 4: a plain 32-byte header (magic, version, count, SH degree, fractional bits, flags, stream count, where
|
||||||
|
// the table of streams starts; any extensions sit between), the table (each stream: u64 packed size, u64 unpacked
|
||||||
|
// size), then the zstd streams back to back: positions, opacities, colours, scales, rotations, SH (none at degree 0).
|
||||||
|
bool loadSpzZstd(const std::vector<uint8_t>& b, SplatCloud& out, std::string& error) {
|
||||||
|
if (b.size() < 32) { error = "damaged .spz file (cut short)"; return false; }
|
||||||
|
uint32_t version, count, toc;
|
||||||
|
std::memcpy(&version, &b[4], 4); std::memcpy(&count, &b[8], 4); std::memcpy(&toc, &b[16], 4);
|
||||||
|
const int shDegree = b[12], fracBits = b[13], numStreams = b[15];
|
||||||
|
if (version != 4) { error = "unsupported .spz version " + std::to_string(version); return false; }
|
||||||
|
// a hostile count can't run memory out: at most 1024:1 on the 9 position bytes a splat (Niantic's own check)
|
||||||
|
if (count == 0 || count > kMaxSplats || shDegree > 4 || static_cast<uint64_t>(count) * 9 > static_cast<uint64_t>(b.size()) * 1024) {
|
||||||
|
error = "damaged .spz file";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const size_t n = count;
|
||||||
|
const size_t expect[6] = {n * 9, n, n * 3, n * 3, n * 4, n * kShDim[shDegree] * 3};
|
||||||
|
const int streams = expect[5] ? 6 : 5;
|
||||||
|
if (numStreams != streams || toc < 32 || static_cast<uint64_t>(toc) + 16ull * numStreams > b.size()) {
|
||||||
|
error = "damaged .spz file (stream table)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::vector<uint8_t> col[5];
|
||||||
|
size_t at = static_cast<size_t>(toc) + 16u * numStreams;
|
||||||
|
for (int i = 0; i < numStreams; ++i) {
|
||||||
|
uint64_t packed, unpacked;
|
||||||
|
std::memcpy(&packed, &b[toc + 16u * i], 8);
|
||||||
|
std::memcpy(&unpacked, &b[toc + 16u * i + 8], 8);
|
||||||
|
if (unpacked != expect[i] || packed > b.size() - at) { error = "damaged .spz file (stream table)"; return false; }
|
||||||
|
if (i < 5) { // the SH past the base colour aren't kept, so aren't unpacked
|
||||||
|
col[i].resize(expect[i]);
|
||||||
|
const size_t got = ZSTD_decompress(col[i].data(), col[i].size(), &b[at], static_cast<size_t>(packed));
|
||||||
|
if (ZSTD_isError(got) || got != expect[i]) { error = "damaged .spz file (zstd)"; return false; }
|
||||||
|
}
|
||||||
|
at += static_cast<size_t>(packed);
|
||||||
|
}
|
||||||
|
SpzColumns c;
|
||||||
|
c.pos = col[0].data();
|
||||||
|
c.alpha = col[1].data();
|
||||||
|
c.color = col[2].data();
|
||||||
|
c.scale = col[3].data();
|
||||||
|
c.rot = col[4].data();
|
||||||
|
c.count = count;
|
||||||
|
c.fracBits = fracBits;
|
||||||
|
c.smallestThree = true;
|
||||||
|
decodeSpz(c, out);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool loadSpz(const std::vector<uint8_t>& b, SplatCloud& out, std::string& error) {
|
||||||
|
uint32_t magic = 0;
|
||||||
|
if (b.size() >= 4) std::memcpy(&magic, b.data(), 4);
|
||||||
|
if (magic == kSpzMagic) return loadSpzZstd(b, out, error); // version 4 on: not gzipped
|
||||||
|
return loadSpzGzip(b, out, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool isSplatPath(const std::string& path) {
|
||||||
|
const std::string ext = lower(fs::u8path(path).extension().u8string());
|
||||||
|
if (ext == ".splat" || ext == ".spz") return true;
|
||||||
|
if (ext != ".ply") return false;
|
||||||
|
std::ifstream in(fs::u8path(path), std::ios::binary);
|
||||||
|
std::string head(4096, '\0');
|
||||||
|
in.read(&head[0], static_cast<std::streamsize>(head.size()));
|
||||||
|
head.resize(static_cast<size_t>(in.gcount()));
|
||||||
|
const size_t end = head.find("end_header");
|
||||||
|
return end != std::string::npos && head.find("f_dc_0") < end;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool loadSplats(const std::string& path, SplatCloud& out, std::string& error) {
|
||||||
|
const std::vector<uint8_t> b = readFile(path);
|
||||||
|
if (b.empty()) { out = SplatCloud{}; error = "couldn't read " + path; return false; }
|
||||||
|
const bool ok = loadSplatBytes(b, lower(fs::u8path(path).extension().u8string()), out, error);
|
||||||
|
if (!ok) error += ": " + path;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> writeSpz(const std::vector<Splat>& splats) {
|
||||||
|
const uint32_t n = static_cast<uint32_t>(splats.size());
|
||||||
|
std::vector<uint8_t> raw(16 + static_cast<size_t>(n) * 19); // position 9, opacity 1, colour 3, scale 3, rotation 3
|
||||||
|
const uint32_t magic = 0x5053474e, version = 2;
|
||||||
|
std::memcpy(&raw[0], &magic, 4); std::memcpy(&raw[4], &version, 4); std::memcpy(&raw[8], &n, 4);
|
||||||
|
raw[12] = 0; // SH degree: base colour only
|
||||||
|
raw[13] = 12; // fractional bits: 1/4096 m
|
||||||
|
uint8_t* pp = &raw[16];
|
||||||
|
uint8_t* pa = pp + n * 9;
|
||||||
|
uint8_t* pc = pa + n;
|
||||||
|
uint8_t* ps = pc + n * 3;
|
||||||
|
uint8_t* pr = ps + n * 3;
|
||||||
|
auto byte = [](float v) { return static_cast<uint8_t>(std::clamp(std::lround(v), 0L, 255L)); };
|
||||||
|
for (uint32_t i = 0; i < n; ++i) {
|
||||||
|
const Splat& s = splats[i];
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
const long f = std::clamp(std::lround(s.pos[k] * 4096.0f), -0x7fffffL, 0x7fffffL);
|
||||||
|
pp[(i * 3 + k) * 3] = static_cast<uint8_t>(f & 0xff);
|
||||||
|
pp[(i * 3 + k) * 3 + 1] = static_cast<uint8_t>((f >> 8) & 0xff);
|
||||||
|
pp[(i * 3 + k) * 3 + 2] = static_cast<uint8_t>((f >> 16) & 0xff);
|
||||||
|
const float dc = (s.rgba[k] / 255.0f - 0.5f) / kShC0;
|
||||||
|
pc[i * 3 + k] = byte((dc * 0.15f + 0.5f) * 255.0f);
|
||||||
|
ps[i * 3 + k] = byte((std::log(std::max(s.scale[k], 1e-6f)) + 10.0f) * 16.0f);
|
||||||
|
}
|
||||||
|
pa[i] = s.rgba[3];
|
||||||
|
const float sign = s.rot[3] < 0 ? -1.0f : 1.0f; // w is rebuilt as the positive root: store q or -q to match
|
||||||
|
for (int k = 0; k < 3; ++k) pr[i * 3 + k] = byte((sign * s.rot[k] + 1.0f) * 127.5f);
|
||||||
|
}
|
||||||
|
// gzip: header, raw deflate, CRC-32 and size of the unpacked bytes
|
||||||
|
std::vector<uint8_t> out = {0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 0xff};
|
||||||
|
size_t z = 0;
|
||||||
|
void* d = tdefl_compress_mem_to_heap(raw.data(), raw.size(), &z, 128 | TDEFL_GREEDY_PARSING_FLAG);
|
||||||
|
if (!d) return {};
|
||||||
|
out.insert(out.end(), static_cast<uint8_t*>(d), static_cast<uint8_t*>(d) + z);
|
||||||
|
mz_free(d);
|
||||||
|
const uint32_t crc = static_cast<uint32_t>(mz_crc32(MZ_CRC32_INIT, raw.data(), raw.size())), size = static_cast<uint32_t>(raw.size());
|
||||||
|
for (uint32_t v : {crc, size}) for (int k = 0; k < 4; ++k) out.push_back(static_cast<uint8_t>(v >> (8 * k)));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void splatCovariance(const Splat& s, float cov[6]) {
|
||||||
|
const float x = s.rot[0], y = s.rot[1], z = s.rot[2], w = s.rot[3];
|
||||||
|
const float r[3][3] = {{1 - 2 * (y * y + z * z), 2 * (x * y - w * z), 2 * (x * z + w * y)},
|
||||||
|
{2 * (x * y + w * z), 1 - 2 * (x * x + z * z), 2 * (y * z - w * x)},
|
||||||
|
{2 * (x * z - w * y), 2 * (y * z + w * x), 1 - 2 * (x * x + y * y)}};
|
||||||
|
float m[3][3]; // R S
|
||||||
|
for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) m[i][j] = r[i][j] * s.scale[j];
|
||||||
|
auto dot = [&](int a, int b) { return m[a][0] * m[b][0] + m[a][1] * m[b][1] + m[a][2] * m[b][2]; }; // (R S)(R S)^T
|
||||||
|
cov[0] = dot(0, 0); cov[1] = dot(0, 1); cov[2] = dot(0, 2); cov[3] = dot(1, 1); cov[4] = dot(1, 2); cov[5] = dot(2, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool loadSplatBytes(const std::vector<uint8_t>& b, const std::string& ext, SplatCloud& out, std::string& error) {
|
||||||
|
out = SplatCloud{};
|
||||||
|
bool ok = false;
|
||||||
|
try {
|
||||||
|
ok = ext == ".spz" ? loadSpz(b, out, error) : ext == ".splat" ? loadDotSplat(b, out, error) : loadPly(b, out, error);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
error = "not enough memory for the splats";
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
if (ok && out.splats.empty()) { error = "no splats in the file"; ok = false; }
|
||||||
|
if (!ok) out.splats.clear();
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
// Gaussian splats (3D Gaussian Splatting captures: Polycam, Luma, Scaniverse, SuperSplat, Postshot, Niantic) read
|
||||||
|
// into one list, whatever the file: the 3DGS training .ply, antimatter15's .splat and Niantic's .spz. Each splat is a
|
||||||
|
// soft 3D blob: a centre, its size along three axes, a rotation, a colour and how opaque it is. Only the base colour
|
||||||
|
// is kept (the view-dependent spherical harmonics beyond degree 0 are dropped: most of the bytes, little to see on
|
||||||
|
// the headset). Axes become Frame's: metres, +Y up, right-handed (glTF's), whatever the file used.
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct Splat {
|
||||||
|
float pos[3]; // centre (metres)
|
||||||
|
float scale[3]; // the blob's radius (one standard deviation) along its own x, y, z (metres)
|
||||||
|
float rot[4]; // its rotation, a unit quaternion x y z w
|
||||||
|
uint8_t rgba[4]; // base colour (sRGB) and opacity
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SplatCloud {
|
||||||
|
std::vector<Splat> splats;
|
||||||
|
std::string format; // "3DGS .ply", ".splat", ".spz"
|
||||||
|
};
|
||||||
|
|
||||||
|
// .splat, .spz, and a .ply whose vertices carry splat properties (f_dc_0, scale_0, ...): the file is read to tell.
|
||||||
|
bool isSplatPath(const std::string& path);
|
||||||
|
bool loadSplats(const std::string& path, SplatCloud& out, std::string& error);
|
||||||
|
// The same from bytes already in memory (a .fenv member); `ext` says which format (".spz", ".splat", ".ply").
|
||||||
|
bool loadSplatBytes(const std::vector<uint8_t>& bytes, const std::string& ext, SplatCloud& out, std::string& error);
|
||||||
|
|
||||||
|
// How a .fenv carries splats: .spz (version 2, Frame's axes), 19 bytes a splat before its gzip (about 12 after). Positions to 1/4096 m within
|
||||||
|
// 2 km of the object's origin; sizes, colours and rotations to 8 bits.
|
||||||
|
std::vector<uint8_t> writeSpz(const std::vector<Splat>& splats);
|
||||||
|
|
||||||
|
// A splat's 3D covariance (its shape: R S S^T R^T) as the six distinct entries xx xy xz yy yz zz.
|
||||||
|
void splatCovariance(const Splat& s, float cov[6]);
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
#pragma once
|
||||||
|
// The draw order of one splat object: splats blend back to front, so the order follows the viewer. Sorted on the CPU by
|
||||||
|
// each splat's distance from one point (the midpoint between the eyes: one order serves both eyes), as 16-bit keys and a
|
||||||
|
// counting sort (two passes over the splats: a few ms for a million on the Frame). A worker thread of its own sorts
|
||||||
|
// while frames go on drawing the last order; the newest request wins (one asked for while it sorts waits, the ones
|
||||||
|
// before it are dropped). Orders are recycled from a few buffers, never allocated per sort.
|
||||||
|
#include "splat_import.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cmath>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class SplatSorter {
|
||||||
|
public:
|
||||||
|
using Order = std::vector<uint32_t>;
|
||||||
|
|
||||||
|
// The first `count` splats of `splats` (their positions are copied: the splats may go once this is made).
|
||||||
|
SplatSorter(const std::vector<Splat>& splats, size_t count) {
|
||||||
|
pos_.resize(count * 3);
|
||||||
|
for (size_t i = 0; i < count; ++i) std::memcpy(&pos_[i * 3], splats[i].pos, 12);
|
||||||
|
worker_ = std::thread([this] { run(); });
|
||||||
|
}
|
||||||
|
~SplatSorter() {
|
||||||
|
{ std::lock_guard<std::mutex> l(m_); stop_ = true; }
|
||||||
|
cv_.notify_all();
|
||||||
|
if (worker_.joinable()) worker_.join();
|
||||||
|
}
|
||||||
|
SplatSorter(const SplatSorter&) = delete;
|
||||||
|
SplatSorter& operator=(const SplatSorter&) = delete;
|
||||||
|
|
||||||
|
// An order for a viewer at `eye` (world) with the object placed at `world` (column-major): on the worker, or (now)
|
||||||
|
// here before returning: the first frame that draws it, a teleport.
|
||||||
|
void request(const float eye[3], const float world[16], bool now) {
|
||||||
|
const uint64_t id = ++requests_;
|
||||||
|
if (now) {
|
||||||
|
std::shared_ptr<Order> out = take();
|
||||||
|
sort(pos_, eye, world, here_, *out);
|
||||||
|
publish(out, id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
{ std::lock_guard<std::mutex> l(m_); std::memcpy(want_.eye, eye, 12); std::memcpy(want_.world, world, 64); want_.id = id; }
|
||||||
|
cv_.notify_one();
|
||||||
|
}
|
||||||
|
// The newest order finished (empty: none yet) and its serial (a new one each time it changes).
|
||||||
|
std::shared_ptr<const Order> latest(uint64_t& serial) const {
|
||||||
|
std::lock_guard<std::mutex> l(m_);
|
||||||
|
serial = doneId_;
|
||||||
|
return done_;
|
||||||
|
}
|
||||||
|
size_t count() const { return pos_.size() / 3; }
|
||||||
|
|
||||||
|
// Farthest first from `eye`, the positions placed by `world`: 16-bit keys over the range of distances there are.
|
||||||
|
struct Scratch { std::vector<float> dist; std::vector<uint16_t> keys; std::vector<uint32_t> counts; };
|
||||||
|
static void sort(const std::vector<float>& pos, const float eye[3], const float* w, Scratch& s, Order& out) {
|
||||||
|
const size_t n = pos.size() / 3;
|
||||||
|
out.resize(n);
|
||||||
|
s.keys.resize(n);
|
||||||
|
s.dist.resize(n);
|
||||||
|
if (!n) return;
|
||||||
|
std::vector<float>& dist = s.dist;
|
||||||
|
std::vector<uint16_t>& keys = s.keys;
|
||||||
|
std::vector<uint32_t>& counts = s.counts;
|
||||||
|
float lo = 1e30f, hi = 0;
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
const float* p = &pos[i * 3];
|
||||||
|
const float x = w[0] * p[0] + w[4] * p[1] + w[8] * p[2] + w[12] - eye[0];
|
||||||
|
const float y = w[1] * p[0] + w[5] * p[1] + w[9] * p[2] + w[13] - eye[1];
|
||||||
|
const float z = w[2] * p[0] + w[6] * p[1] + w[10] * p[2] + w[14] - eye[2];
|
||||||
|
const float d = std::sqrt(x * x + y * y + z * z);
|
||||||
|
dist[i] = d;
|
||||||
|
lo = std::min(lo, d);
|
||||||
|
hi = std::max(hi, d);
|
||||||
|
}
|
||||||
|
const float scale = hi > lo ? 65535.0f / (hi - lo) : 0.0f;
|
||||||
|
counts.assign(65536, 0);
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
const uint16_t k = static_cast<uint16_t>(65535 - static_cast<uint32_t>((dist[i] - lo) * scale)); // far = 0: first
|
||||||
|
keys[i] = k;
|
||||||
|
++counts[k];
|
||||||
|
}
|
||||||
|
uint32_t at = 0;
|
||||||
|
for (uint32_t& c : counts) { const uint32_t k = c; c = at; at += k; }
|
||||||
|
for (size_t i = 0; i < n; ++i) out[counts[keys[i]]++] = static_cast<uint32_t>(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Want { float eye[3] = {0, 0, 0}; float world[16] = {}; uint64_t id = 0; };
|
||||||
|
|
||||||
|
void run() {
|
||||||
|
Scratch scratch;
|
||||||
|
uint64_t doneWant = 0;
|
||||||
|
for (;;) {
|
||||||
|
Want w;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> l(m_);
|
||||||
|
cv_.wait(l, [&] { return stop_ || want_.id != doneWant; });
|
||||||
|
if (stop_) return;
|
||||||
|
w = want_;
|
||||||
|
doneWant = w.id;
|
||||||
|
}
|
||||||
|
std::shared_ptr<Order> out = take();
|
||||||
|
sort(pos_, w.eye, w.world, scratch, *out);
|
||||||
|
publish(out, w.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A buffer nobody holds (the renderer drops an order once it's copied), else a new one: three in use at most.
|
||||||
|
std::shared_ptr<Order> take() {
|
||||||
|
std::lock_guard<std::mutex> l(m_);
|
||||||
|
for (auto& b : pool_) if (b.use_count() == 1) return b;
|
||||||
|
pool_.push_back(std::make_shared<Order>());
|
||||||
|
return pool_.back();
|
||||||
|
}
|
||||||
|
// The newest request's order wins: one that finishes after a newer one (a slow sort, then one made here) is dropped.
|
||||||
|
void publish(const std::shared_ptr<Order>& out, uint64_t id) {
|
||||||
|
std::lock_guard<std::mutex> l(m_);
|
||||||
|
if (id <= doneId_) return;
|
||||||
|
done_ = out;
|
||||||
|
doneId_ = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<float> pos_;
|
||||||
|
Scratch here_; // request(now)'s (the renderer's thread)
|
||||||
|
std::thread worker_;
|
||||||
|
mutable std::mutex m_;
|
||||||
|
std::condition_variable cv_;
|
||||||
|
bool stop_ = false;
|
||||||
|
Want want_;
|
||||||
|
std::atomic<uint64_t> requests_{0};
|
||||||
|
std::vector<std::shared_ptr<Order>> pool_;
|
||||||
|
std::shared_ptr<const Order> done_;
|
||||||
|
uint64_t doneId_ = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
#pragma once
|
||||||
|
// GENERATED by tools/splat/gen_splat_spirv.sh from tools/splat/splat.{vert,frag}. Do not edit by hand.
|
||||||
|
// vert: set0 b0 Splats[] (32 bytes each), b1 order[], b2 Frame { mat4 clipFromWorld[2]; mat4 worldFromModel; vec4 viewport; }
|
||||||
|
// 4 vertices a splat (triangle strip), one instance a splat; multiview
|
||||||
|
// frag: premultiplied colour (blend ONE, ONE_MINUS_SRC_ALPHA)
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
static const uint32_t kSplatVertSpirv[] = {
|
||||||
|
0x07230203,0x00010300,0x0008000b,0x000001df,0x00000000,0x00020011,0x00000001,0x00020011,
|
||||||
|
0x00001157,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,
|
||||||
|
0x00000000,0x00000001,0x000b000f,0x00000000,0x00000004,0x6e69616d,0x00000000,0x00000032,
|
||||||
|
0x0000009e,0x000000c0,0x0000016f,0x00000192,0x00000199,0x00030003,0x00000002,0x000001c2,
|
||||||
|
0x00060004,0x455f4c47,0x6d5f5458,0x69746c75,0x77656976,0x00000000,0x00040005,0x00000004,
|
||||||
|
0x6e69616d,0x00000000,0x00040005,0x00000026,0x616c7053,0x00000074,0x00040006,0x00000026,
|
||||||
|
0x00000000,0x00736f70,0x00050006,0x00000026,0x00000001,0x30766f63,0x00000031,0x00050006,
|
||||||
|
0x00000026,0x00000002,0x32766f63,0x00000033,0x00050006,0x00000026,0x00000003,0x34766f63,
|
||||||
|
0x00000035,0x00050006,0x00000026,0x00000004,0x61626772,0x00000000,0x00040006,0x00000026,
|
||||||
|
0x00000005,0x00646170,0x00040005,0x00000028,0x616c7053,0x00007374,0x00050006,0x00000028,
|
||||||
|
0x00000000,0x616c7073,0x00007374,0x00030005,0x0000002a,0x00000000,0x00040005,0x0000002e,
|
||||||
|
0x6564724f,0x00000072,0x00050006,0x0000002e,0x00000000,0x6564726f,0x00000072,0x00030005,
|
||||||
|
0x00000030,0x00000000,0x00070005,0x00000032,0x495f6c67,0x6174736e,0x4965636e,0x7865646e,
|
||||||
|
0x00000000,0x00040005,0x0000007d,0x6d617246,0x00000065,0x00070006,0x0000007d,0x00000000,
|
||||||
|
0x70696c63,0x6d6f7246,0x6c726f57,0x00000064,0x00070006,0x0000007d,0x00000001,0x6c726f77,
|
||||||
|
0x6f724664,0x646f4d6d,0x00006c65,0x00060006,0x0000007d,0x00000002,0x77656976,0x74726f70,
|
||||||
|
0x00000000,0x00030005,0x0000007f,0x00000075,0x00060005,0x0000009e,0x565f6c67,0x49776569,
|
||||||
|
0x7865646e,0x00000000,0x00060005,0x000000be,0x505f6c67,0x65567265,0x78657472,0x00000000,
|
||||||
|
0x00060006,0x000000be,0x00000000,0x505f6c67,0x7469736f,0x006e6f69,0x00070006,0x000000be,
|
||||||
|
0x00000001,0x505f6c67,0x746e696f,0x657a6953,0x00000000,0x00070006,0x000000be,0x00000002,
|
||||||
|
0x435f6c67,0x4470696c,0x61747369,0x0065636e,0x00070006,0x000000be,0x00000003,0x435f6c67,
|
||||||
|
0x446c6c75,0x61747369,0x0065636e,0x00030005,0x000000c0,0x00000000,0x00060005,0x0000016f,
|
||||||
|
0x565f6c67,0x65747265,0x646e4978,0x00007865,0x00050005,0x00000172,0x65646e69,0x6c626178,
|
||||||
|
0x00000065,0x00040005,0x00000192,0x67695376,0x0000616d,0x00040005,0x00000199,0x6c6f4376,
|
||||||
|
0x0000726f,0x00050048,0x00000026,0x00000000,0x00000023,0x00000000,0x00050048,0x00000026,
|
||||||
|
0x00000001,0x00000023,0x0000000c,0x00050048,0x00000026,0x00000002,0x00000023,0x00000010,
|
||||||
|
0x00050048,0x00000026,0x00000003,0x00000023,0x00000014,0x00050048,0x00000026,0x00000004,
|
||||||
|
0x00000023,0x00000018,0x00050048,0x00000026,0x00000005,0x00000023,0x0000001c,0x00040047,
|
||||||
|
0x00000027,0x00000006,0x00000020,0x00030047,0x00000028,0x00000002,0x00040048,0x00000028,
|
||||||
|
0x00000000,0x00000018,0x00050048,0x00000028,0x00000000,0x00000023,0x00000000,0x00030047,
|
||||||
|
0x0000002a,0x00000018,0x00040047,0x0000002a,0x00000021,0x00000000,0x00040047,0x0000002a,
|
||||||
|
0x00000022,0x00000000,0x00040047,0x0000002d,0x00000006,0x00000004,0x00030047,0x0000002e,
|
||||||
|
0x00000002,0x00040048,0x0000002e,0x00000000,0x00000018,0x00050048,0x0000002e,0x00000000,
|
||||||
|
0x00000023,0x00000000,0x00030047,0x00000030,0x00000018,0x00040047,0x00000030,0x00000021,
|
||||||
|
0x00000001,0x00040047,0x00000030,0x00000022,0x00000000,0x00040047,0x00000032,0x0000000b,
|
||||||
|
0x0000002b,0x00040047,0x0000007c,0x00000006,0x00000040,0x00030047,0x0000007d,0x00000002,
|
||||||
|
0x00040048,0x0000007d,0x00000000,0x00000005,0x00050048,0x0000007d,0x00000000,0x00000007,
|
||||||
|
0x00000010,0x00050048,0x0000007d,0x00000000,0x00000023,0x00000000,0x00040048,0x0000007d,
|
||||||
|
0x00000001,0x00000005,0x00050048,0x0000007d,0x00000001,0x00000007,0x00000010,0x00050048,
|
||||||
|
0x0000007d,0x00000001,0x00000023,0x00000080,0x00050048,0x0000007d,0x00000002,0x00000023,
|
||||||
|
0x000000c0,0x00040047,0x0000007f,0x00000021,0x00000002,0x00040047,0x0000007f,0x00000022,
|
||||||
|
0x00000000,0x00040047,0x0000009e,0x0000000b,0x00001158,0x00030047,0x000000be,0x00000002,
|
||||||
|
0x00050048,0x000000be,0x00000000,0x0000000b,0x00000000,0x00050048,0x000000be,0x00000001,
|
||||||
|
0x0000000b,0x00000001,0x00050048,0x000000be,0x00000002,0x0000000b,0x00000003,0x00050048,
|
||||||
|
0x000000be,0x00000003,0x0000000b,0x00000004,0x00040047,0x0000016f,0x0000000b,0x0000002a,
|
||||||
|
0x00040047,0x00000192,0x0000001e,0x00000000,0x00040047,0x00000199,0x0000001e,0x00000001,
|
||||||
|
0x00020013,0x00000002,0x00030021,0x00000003,0x00000002,0x00030016,0x00000006,0x00000020,
|
||||||
|
0x00040017,0x00000007,0x00000006,0x00000003,0x0004002b,0x00000006,0x00000012,0x3d6147ae,
|
||||||
|
0x0004002b,0x00000006,0x00000018,0x4019999a,0x0006002c,0x00000007,0x00000019,0x00000018,
|
||||||
|
0x00000018,0x00000018,0x0004002b,0x00000006,0x0000001b,0x3d25aee6,0x00040015,0x00000022,
|
||||||
|
0x00000020,0x00000000,0x0008001e,0x00000026,0x00000007,0x00000022,0x00000022,0x00000022,
|
||||||
|
0x00000022,0x00000022,0x0003001d,0x00000027,0x00000026,0x0003001e,0x00000028,0x00000027,
|
||||||
|
0x00040020,0x00000029,0x0000000c,0x00000028,0x0004003b,0x00000029,0x0000002a,0x0000000c,
|
||||||
|
0x00040015,0x0000002b,0x00000020,0x00000001,0x0004002b,0x0000002b,0x0000002c,0x00000000,
|
||||||
|
0x0003001d,0x0000002d,0x00000022,0x0003001e,0x0000002e,0x0000002d,0x00040020,0x0000002f,
|
||||||
|
0x0000000c,0x0000002e,0x0004003b,0x0000002f,0x00000030,0x0000000c,0x00040020,0x00000031,
|
||||||
|
0x00000001,0x0000002b,0x0004003b,0x00000031,0x00000032,0x00000001,0x00040020,0x00000034,
|
||||||
|
0x0000000c,0x00000022,0x00040020,0x00000037,0x0000000c,0x00000026,0x0004002b,0x0000002b,
|
||||||
|
0x0000003d,0x00000001,0x0004002b,0x0000002b,0x00000041,0x00000002,0x00040017,0x0000004c,
|
||||||
|
0x00000006,0x00000002,0x00040020,0x0000004d,0x00000007,0x0000004c,0x00040018,0x0000005a,
|
||||||
|
0x00000007,0x00000003,0x0004002b,0x00000022,0x0000005d,0x00000000,0x0004002b,0x00000022,
|
||||||
|
0x00000061,0x00000001,0x0004002b,0x00000006,0x00000072,0x3f800000,0x0004002b,0x00000006,
|
||||||
|
0x00000073,0x00000000,0x00040017,0x00000079,0x00000006,0x00000004,0x00040018,0x0000007a,
|
||||||
|
0x00000079,0x00000004,0x0004002b,0x00000022,0x0000007b,0x00000002,0x0004001c,0x0000007c,
|
||||||
|
0x0000007a,0x0000007b,0x0005001e,0x0000007d,0x0000007c,0x0000007a,0x00000079,0x00040020,
|
||||||
|
0x0000007e,0x00000002,0x0000007d,0x0004003b,0x0000007e,0x0000007f,0x00000002,0x00040020,
|
||||||
|
0x00000080,0x00000002,0x0000007a,0x0004003b,0x00000031,0x0000009e,0x00000001,0x00020014,
|
||||||
|
0x000000a6,0x0004002b,0x00000022,0x000000a7,0x00000003,0x0004002b,0x00000006,0x000000aa,
|
||||||
|
0x3c23d70a,0x0004002b,0x00000006,0x000000b2,0x3fa66666,0x00040017,0x000000b7,0x000000a6,
|
||||||
|
0x00000002,0x0004001c,0x000000bd,0x00000006,0x00000061,0x0006001e,0x000000be,0x00000079,
|
||||||
|
0x00000006,0x000000bd,0x000000bd,0x00040020,0x000000bf,0x00000003,0x000000be,0x0004003b,
|
||||||
|
0x000000bf,0x000000c0,0x00000003,0x0004002b,0x00000006,0x000000c1,0xc0000000,0x0007002c,
|
||||||
|
0x00000079,0x000000c2,0x00000073,0x00000073,0x000000c1,0x00000072,0x00040020,0x000000c3,
|
||||||
|
0x00000003,0x00000079,0x0004002b,0x00000006,0x000000ed,0x3f000000,0x00040020,0x000000ee,
|
||||||
|
0x00000002,0x00000006,0x0004002b,0x00000006,0x00000108,0x3e99999a,0x0004002b,0x00000006,
|
||||||
|
0x0000011d,0x3dcccccd,0x0004002b,0x00000006,0x00000137,0x358637bd,0x0005002c,0x0000004c,
|
||||||
|
0x00000146,0x00000072,0x00000073,0x0005002c,0x0000004c,0x00000147,0x00000073,0x00000072,
|
||||||
|
0x0004002b,0x00000006,0x0000015f,0x40400000,0x0004002b,0x00000022,0x00000167,0x00000004,
|
||||||
|
0x0004001c,0x00000168,0x0000004c,0x00000167,0x0004002b,0x00000006,0x00000169,0xbf800000,
|
||||||
|
0x0005002c,0x0000004c,0x0000016a,0x00000169,0x00000169,0x0005002c,0x0000004c,0x0000016b,
|
||||||
|
0x00000072,0x00000169,0x0005002c,0x0000004c,0x0000016c,0x00000169,0x00000072,0x0005002c,
|
||||||
|
0x0000004c,0x0000016d,0x00000072,0x00000072,0x0007002c,0x00000168,0x0000016e,0x0000016a,
|
||||||
|
0x0000016b,0x0000016c,0x0000016d,0x0004003b,0x00000031,0x0000016f,0x00000001,0x00040020,
|
||||||
|
0x00000171,0x00000007,0x00000168,0x0004002b,0x00000006,0x00000182,0x40000000,0x00040020,
|
||||||
|
0x00000184,0x00000002,0x00000079,0x00040020,0x00000191,0x00000003,0x0000004c,0x0004003b,
|
||||||
|
0x00000191,0x00000192,0x00000003,0x0004003b,0x000000c3,0x00000199,0x00000003,0x0006002c,
|
||||||
|
0x00000007,0x000001c6,0x00000012,0x00000012,0x00000012,0x0006002c,0x00000007,0x000001c8,
|
||||||
|
0x0000001b,0x0000001b,0x0000001b,0x00040020,0x000001c9,0x0000000c,0x00000007,0x0004002b,
|
||||||
|
0x00000006,0x000001d7,0x3f19999a,0x0004002b,0x00000006,0x000001d9,0x3e800000,0x0004002b,
|
||||||
|
0x00000006,0x000001db,0x3d9e8391,0x0006002c,0x00000007,0x000001dc,0x000001db,0x000001db,
|
||||||
|
0x000001db,0x0004002b,0x00000006,0x000001dd,0x3f72a76f,0x0006002c,0x00000007,0x000001de,
|
||||||
|
0x000001dd,0x000001dd,0x000001dd,0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,
|
||||||
|
0x000200f8,0x00000005,0x0004003b,0x00000171,0x00000172,0x00000007,0x000300f7,0x000001a4,
|
||||||
|
0x00000000,0x000300fb,0x0000005d,0x000001a5,0x000200f8,0x000001a5,0x0004003d,0x0000002b,
|
||||||
|
0x00000033,0x00000032,0x00060041,0x00000034,0x00000035,0x00000030,0x0000002c,0x00000033,
|
||||||
|
0x0004003d,0x00000022,0x00000036,0x00000035,0x00060041,0x00000037,0x00000038,0x0000002a,
|
||||||
|
0x0000002c,0x00000036,0x00050041,0x000001c9,0x000001ca,0x00000038,0x0000005d,0x0004003d,
|
||||||
|
0x00000007,0x000001cb,0x000001ca,0x00050041,0x00000034,0x000001cc,0x00000038,0x00000061,
|
||||||
|
0x0004003d,0x00000022,0x000001cd,0x000001cc,0x00050041,0x00000034,0x000001ce,0x00000038,
|
||||||
|
0x0000007b,0x0004003d,0x00000022,0x000001cf,0x000001ce,0x00050041,0x00000034,0x000001d0,
|
||||||
|
0x00000038,0x000000a7,0x0004003d,0x00000022,0x000001d1,0x000001d0,0x00050041,0x00000034,
|
||||||
|
0x000001d2,0x00000038,0x00000167,0x0004003d,0x00000022,0x000001d3,0x000001d2,0x0006000c,
|
||||||
|
0x0000004c,0x00000051,0x00000001,0x0000003e,0x000001cd,0x0006000c,0x0000004c,0x00000055,
|
||||||
|
0x00000001,0x0000003e,0x000001cf,0x0006000c,0x0000004c,0x00000059,0x00000001,0x0000003e,
|
||||||
|
0x000001d1,0x00050051,0x00000006,0x00000060,0x00000051,0x00000000,0x00050051,0x00000006,
|
||||||
|
0x00000063,0x00000051,0x00000001,0x00050051,0x00000006,0x00000065,0x00000055,0x00000000,
|
||||||
|
0x00050051,0x00000006,0x00000069,0x00000055,0x00000001,0x00050051,0x00000006,0x0000006b,
|
||||||
|
0x00000059,0x00000000,0x00050051,0x00000006,0x00000071,0x00000059,0x00000001,0x00060050,
|
||||||
|
0x00000007,0x00000074,0x00000060,0x00000063,0x00000065,0x00060050,0x00000007,0x00000075,
|
||||||
|
0x00000063,0x00000069,0x0000006b,0x00060050,0x00000007,0x00000076,0x00000065,0x0000006b,
|
||||||
|
0x00000071,0x00060050,0x0000005a,0x00000077,0x00000074,0x00000075,0x00000076,0x00050041,
|
||||||
|
0x00000080,0x00000081,0x0000007f,0x0000003d,0x0004003d,0x0000007a,0x00000082,0x00000081,
|
||||||
|
0x00050051,0x00000079,0x00000083,0x00000082,0x00000000,0x0008004f,0x00000007,0x00000084,
|
||||||
|
0x00000083,0x00000083,0x00000000,0x00000001,0x00000002,0x00050051,0x00000079,0x00000085,
|
||||||
|
0x00000082,0x00000001,0x0008004f,0x00000007,0x00000086,0x00000085,0x00000085,0x00000000,
|
||||||
|
0x00000001,0x00000002,0x00050051,0x00000079,0x00000087,0x00000082,0x00000002,0x0008004f,
|
||||||
|
0x00000007,0x00000088,0x00000087,0x00000087,0x00000000,0x00000001,0x00000002,0x00060050,
|
||||||
|
0x0000005a,0x00000089,0x00000084,0x00000086,0x00000088,0x00050092,0x0000005a,0x0000008d,
|
||||||
|
0x00000089,0x00000077,0x00040054,0x0000005a,0x0000008f,0x00000089,0x00050092,0x0000005a,
|
||||||
|
0x00000090,0x0000008d,0x0000008f,0x00050051,0x00000006,0x00000097,0x000001cb,0x00000000,
|
||||||
|
0x00050051,0x00000006,0x00000098,0x000001cb,0x00000001,0x00050051,0x00000006,0x00000099,
|
||||||
|
0x000001cb,0x00000002,0x00070050,0x00000079,0x0000009a,0x00000097,0x00000098,0x00000099,
|
||||||
|
0x00000072,0x00050091,0x00000079,0x0000009b,0x00000082,0x0000009a,0x0004003d,0x0000002b,
|
||||||
|
0x0000009f,0x0000009e,0x00060041,0x00000080,0x000000a0,0x0000007f,0x0000002c,0x0000009f,
|
||||||
|
0x0004003d,0x0000007a,0x000000a1,0x000000a0,0x00050091,0x00000079,0x000000a5,0x000000a1,
|
||||||
|
0x0000009b,0x00050051,0x00000006,0x000000a9,0x000000a5,0x00000003,0x000500bc,0x000000a6,
|
||||||
|
0x000000ab,0x000000a9,0x000000aa,0x000400a8,0x000000a6,0x000000ac,0x000000ab,0x000300f7,
|
||||||
|
0x000000ae,0x00000000,0x000400fa,0x000000ac,0x000000ad,0x000000ae,0x000200f8,0x000000ad,
|
||||||
|
0x0007004f,0x0000004c,0x000000b0,0x000000a5,0x000000a5,0x00000000,0x00000001,0x0006000c,
|
||||||
|
0x0000004c,0x000000b1,0x00000001,0x00000004,0x000000b0,0x00050085,0x00000006,0x000000b5,
|
||||||
|
0x000000b2,0x000000a9,0x00050050,0x0000004c,0x000000b6,0x000000b5,0x000000b5,0x000500ba,
|
||||||
|
0x000000b7,0x000000b8,0x000000b1,0x000000b6,0x0004009a,0x000000a6,0x000000b9,0x000000b8,
|
||||||
|
0x000200f9,0x000000ae,0x000200f8,0x000000ae,0x000700f5,0x000000a6,0x000000ba,0x000000ab,
|
||||||
|
0x000001a5,0x000000b9,0x000000ad,0x000300f7,0x000000bc,0x00000000,0x000400fa,0x000000ba,
|
||||||
|
0x000000bb,0x000000bc,0x000200f8,0x000000bb,0x00050041,0x000000c3,0x000000c4,0x000000c0,
|
||||||
|
0x0000002c,0x0003003e,0x000000c4,0x000000c2,0x000200f9,0x000001a4,0x000200f8,0x000000bc,
|
||||||
|
0x00060051,0x00000006,0x000000c8,0x000000a1,0x00000000,0x00000000,0x00060051,0x00000006,
|
||||||
|
0x000000ca,0x000000a1,0x00000001,0x00000000,0x00060051,0x00000006,0x000000cc,0x000000a1,
|
||||||
|
0x00000002,0x00000000,0x00060050,0x00000007,0x000000cd,0x000000c8,0x000000ca,0x000000cc,
|
||||||
|
0x00060051,0x00000006,0x000000d0,0x000000a1,0x00000000,0x00000001,0x00060051,0x00000006,
|
||||||
|
0x000000d2,0x000000a1,0x00000001,0x00000001,0x00060051,0x00000006,0x000000d4,0x000000a1,
|
||||||
|
0x00000002,0x00000001,0x00060050,0x00000007,0x000000d5,0x000000d0,0x000000d2,0x000000d4,
|
||||||
|
0x00060051,0x00000006,0x000000d8,0x000000a1,0x00000000,0x00000003,0x00060051,0x00000006,
|
||||||
|
0x000000da,0x000000a1,0x00000001,0x00000003,0x00060051,0x00000006,0x000000dc,0x000000a1,
|
||||||
|
0x00000002,0x00000003,0x00060050,0x00000007,0x000000dd,0x000000d8,0x000000da,0x000000dc,
|
||||||
|
0x00050088,0x00000006,0x000000e1,0x00000072,0x000000a9,0x00050051,0x00000006,0x000000e5,
|
||||||
|
0x000000a5,0x00000000,0x00050085,0x00000006,0x000000e7,0x000000e5,0x000000e1,0x0005008e,
|
||||||
|
0x00000007,0x000000e9,0x000000dd,0x000000e7,0x00050083,0x00000007,0x000000ea,0x000000cd,
|
||||||
|
0x000000e9,0x0005008e,0x00000007,0x000000ec,0x000000ea,0x000000e1,0x00060041,0x000000ee,
|
||||||
|
0x000000ef,0x0000007f,0x00000041,0x0000005d,0x0004003d,0x00000006,0x000000f0,0x000000ef,
|
||||||
|
0x00050085,0x00000006,0x000000f1,0x000000ed,0x000000f0,0x0005008e,0x00000007,0x000000f2,
|
||||||
|
0x000000ec,0x000000f1,0x00050051,0x00000006,0x000000f6,0x000000a5,0x00000001,0x00050085,
|
||||||
|
0x00000006,0x000000f8,0x000000f6,0x000000e1,0x0005008e,0x00000007,0x000000fa,0x000000dd,
|
||||||
|
0x000000f8,0x00050083,0x00000007,0x000000fb,0x000000d5,0x000000fa,0x0005008e,0x00000007,
|
||||||
|
0x000000fd,0x000000fb,0x000000e1,0x00060041,0x000000ee,0x000000fe,0x0000007f,0x00000041,
|
||||||
|
0x00000061,0x0004003d,0x00000006,0x000000ff,0x000000fe,0x00050085,0x00000006,0x00000100,
|
||||||
|
0x000000ed,0x000000ff,0x0005008e,0x00000007,0x00000101,0x000000fd,0x00000100,0x00050091,
|
||||||
|
0x00000007,0x00000106,0x00000090,0x000000f2,0x00050094,0x00000006,0x00000107,0x000000f2,
|
||||||
|
0x00000106,0x00050081,0x00000006,0x00000109,0x00000107,0x00000108,0x00050091,0x00000007,
|
||||||
|
0x0000010e,0x00000090,0x00000101,0x00050094,0x00000006,0x0000010f,0x000000f2,0x0000010e,
|
||||||
|
0x00050094,0x00000006,0x00000115,0x00000101,0x0000010e,0x00050081,0x00000006,0x00000116,
|
||||||
|
0x00000115,0x00000108,0x00050081,0x00000006,0x000001d8,0x00000107,0x00000115,0x00050081,
|
||||||
|
0x00000006,0x0000011a,0x000001d7,0x000001d8,0x00050085,0x00000006,0x0000011b,0x000000ed,
|
||||||
|
0x0000011a,0x00050085,0x00000006,0x000001da,0x0000011a,0x0000011a,0x00050085,0x00000006,
|
||||||
|
0x00000120,0x000001d9,0x000001da,0x00050085,0x00000006,0x00000123,0x00000109,0x00000116,
|
||||||
|
0x00050085,0x00000006,0x00000126,0x0000010f,0x0000010f,0x00050083,0x00000006,0x00000127,
|
||||||
|
0x00000123,0x00000126,0x00050083,0x00000006,0x00000128,0x00000120,0x00000127,0x0007000c,
|
||||||
|
0x00000006,0x00000129,0x00000001,0x00000028,0x0000011d,0x00000128,0x0006000c,0x00000006,
|
||||||
|
0x0000012a,0x00000001,0x0000001f,0x00000129,0x00050081,0x00000006,0x0000012e,0x0000011b,
|
||||||
|
0x0000012a,0x00050083,0x00000006,0x00000132,0x0000011b,0x0000012a,0x0007000c,0x00000006,
|
||||||
|
0x00000133,0x00000001,0x00000028,0x0000011d,0x00000132,0x0006000c,0x00000006,0x00000136,
|
||||||
|
0x00000001,0x00000004,0x0000010f,0x000500ba,0x000000a6,0x00000138,0x00000136,0x00000137,
|
||||||
|
0x000300f7,0x0000013b,0x00000000,0x000400fa,0x00000138,0x0000013a,0x00000142,0x000200f8,
|
||||||
|
0x0000013a,0x00050083,0x00000006,0x0000013f,0x0000012e,0x00000109,0x00050050,0x0000004c,
|
||||||
|
0x00000140,0x0000010f,0x0000013f,0x0006000c,0x0000004c,0x00000141,0x00000001,0x00000045,
|
||||||
|
0x00000140,0x000200f9,0x0000013b,0x000200f8,0x00000142,0x000500be,0x000000a6,0x00000145,
|
||||||
|
0x00000109,0x00000116,0x00050050,0x000000b7,0x00000148,0x00000145,0x00000145,0x000600a9,
|
||||||
|
0x0000004c,0x00000149,0x00000148,0x00000146,0x00000147,0x000200f9,0x0000013b,0x000200f8,
|
||||||
|
0x0000013b,0x000700f5,0x0000004c,0x000001c0,0x00000141,0x0000013a,0x00000149,0x00000142,
|
||||||
|
0x0006000c,0x00000006,0x0000014e,0x00000001,0x0000001f,0x0000012e,0x0005008e,0x0000004c,
|
||||||
|
0x0000014f,0x000001c0,0x0000014e,0x00050051,0x00000006,0x00000152,0x000001c0,0x00000001,
|
||||||
|
0x0004007f,0x00000006,0x00000153,0x00000152,0x00050051,0x00000006,0x00000155,0x000001c0,
|
||||||
|
0x00000000,0x00050050,0x0000004c,0x00000156,0x00000153,0x00000155,0x0006000c,0x00000006,
|
||||||
|
0x00000158,0x00000001,0x0000001f,0x00000133,0x0005008e,0x0000004c,0x00000159,0x00000156,
|
||||||
|
0x00000158,0x0006000c,0x00000006,0x0000015b,0x00000001,0x00000042,0x0000014f,0x0006000c,
|
||||||
|
0x00000006,0x0000015d,0x00000001,0x00000042,0x00000159,0x0007000c,0x00000006,0x0000015e,
|
||||||
|
0x00000001,0x00000028,0x0000015b,0x0000015d,0x00050085,0x00000006,0x00000160,0x0000015e,
|
||||||
|
0x0000015f,0x000500b8,0x000000a6,0x00000161,0x00000160,0x000000ed,0x000300f7,0x00000163,
|
||||||
|
0x00000000,0x000400fa,0x00000161,0x00000162,0x00000163,0x000200f8,0x00000162,0x00050041,
|
||||||
|
0x000000c3,0x00000164,0x000000c0,0x0000002c,0x0003003e,0x00000164,0x000000c2,0x000200f9,
|
||||||
|
0x000001a4,0x000200f8,0x00000163,0x0004003d,0x0000002b,0x00000170,0x0000016f,0x0003003e,
|
||||||
|
0x00000172,0x0000016e,0x00050041,0x0000004d,0x00000173,0x00000172,0x00000170,0x0004003d,
|
||||||
|
0x0000004c,0x00000174,0x00000173,0x00050051,0x00000006,0x00000177,0x00000174,0x00000000,
|
||||||
|
0x0005008e,0x0000004c,0x00000179,0x0000014f,0x00000177,0x00050051,0x00000006,0x0000017b,
|
||||||
|
0x00000174,0x00000001,0x0005008e,0x0000004c,0x0000017d,0x00000159,0x0000017b,0x00050081,
|
||||||
|
0x0000004c,0x0000017e,0x00000179,0x0000017d,0x0005008e,0x0000004c,0x0000017f,0x0000017e,
|
||||||
|
0x0000015f,0x0005008e,0x0000004c,0x00000183,0x0000017f,0x00000182,0x00050041,0x00000184,
|
||||||
|
0x00000185,0x0000007f,0x00000041,0x0004003d,0x00000079,0x00000186,0x00000185,0x0007004f,
|
||||||
|
0x0000004c,0x00000187,0x00000186,0x00000186,0x00000002,0x00000003,0x00050085,0x0000004c,
|
||||||
|
0x00000188,0x00000183,0x00000187,0x0005008e,0x0000004c,0x0000018b,0x00000188,0x000000a9,
|
||||||
|
0x00050051,0x00000006,0x0000018c,0x0000018b,0x00000000,0x00050051,0x00000006,0x0000018d,
|
||||||
|
0x0000018b,0x00000001,0x00070050,0x00000079,0x0000018e,0x0000018c,0x0000018d,0x00000073,
|
||||||
|
0x00000073,0x00050081,0x00000079,0x0000018f,0x000000a5,0x0000018e,0x00050041,0x000000c3,
|
||||||
|
0x00000190,0x000000c0,0x0000002c,0x0003003e,0x00000190,0x0000018f,0x0005008e,0x0000004c,
|
||||||
|
0x00000194,0x00000174,0x0000015f,0x0003003e,0x00000192,0x00000194,0x0006000c,0x00000079,
|
||||||
|
0x00000198,0x00000001,0x00000040,0x000001d3,0x0008004f,0x00000007,0x0000019c,0x00000198,
|
||||||
|
0x00000198,0x00000000,0x00000001,0x00000002,0x00050085,0x00000007,0x000001ae,0x0000019c,
|
||||||
|
0x000001dc,0x00050081,0x00000007,0x000001b1,0x0000019c,0x000001c6,0x00050085,0x00000007,
|
||||||
|
0x000001b3,0x000001b1,0x000001de,0x0007000c,0x00000007,0x000001b4,0x00000001,0x0000001a,
|
||||||
|
0x000001b3,0x00000019,0x0007000c,0x00000007,0x000001b7,0x00000001,0x00000030,0x000001c8,
|
||||||
|
0x0000019c,0x0008000c,0x00000007,0x000001b8,0x00000001,0x0000002e,0x000001ae,0x000001b4,
|
||||||
|
0x000001b7,0x00050051,0x00000006,0x0000019f,0x00000198,0x00000003,0x00050051,0x00000006,
|
||||||
|
0x000001a0,0x000001b8,0x00000000,0x00050051,0x00000006,0x000001a1,0x000001b8,0x00000001,
|
||||||
|
0x00050051,0x00000006,0x000001a2,0x000001b8,0x00000002,0x00070050,0x00000079,0x000001a3,
|
||||||
|
0x000001a0,0x000001a1,0x000001a2,0x0000019f,0x0003003e,0x00000199,0x000001a3,0x000200f9,
|
||||||
|
0x000001a4,0x000200f8,0x000001a4,0x000100fd,0x00010038,
|
||||||
|
};
|
||||||
|
static const size_t kSplatVertSize = sizeof(kSplatVertSpirv);
|
||||||
|
|
||||||
|
static const uint32_t kSplatFragSpirv[] = {
|
||||||
|
0x07230203,0x00010300,0x0008000b,0x00000038,0x00000000,0x00020011,0x00000001,0x0006000b,
|
||||||
|
0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,0x00000000,0x00000001,
|
||||||
|
0x0008000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x0000000b,0x0000001a,0x0000002d,
|
||||||
|
0x00030010,0x00000004,0x00000007,0x00030003,0x00000002,0x000001c2,0x00040005,0x00000004,
|
||||||
|
0x6e69616d,0x00000000,0x00040005,0x0000000b,0x67695376,0x0000616d,0x00040005,0x0000001a,
|
||||||
|
0x6c6f4376,0x0000726f,0x00050005,0x0000002d,0x4374756f,0x726f6c6f,0x00000000,0x00040047,
|
||||||
|
0x0000000b,0x0000001e,0x00000000,0x00040047,0x0000001a,0x0000001e,0x00000001,0x00040047,
|
||||||
|
0x0000002d,0x0000001e,0x00000000,0x00020013,0x00000002,0x00030021,0x00000003,0x00000002,
|
||||||
|
0x00030016,0x00000006,0x00000020,0x00040017,0x00000009,0x00000006,0x00000002,0x00040020,
|
||||||
|
0x0000000a,0x00000001,0x00000009,0x0004003b,0x0000000a,0x0000000b,0x00000001,0x0004002b,
|
||||||
|
0x00000006,0x00000010,0x41100000,0x00020014,0x00000011,0x0004002b,0x00000006,0x00000017,
|
||||||
|
0x3f7d70a4,0x00040017,0x00000018,0x00000006,0x00000004,0x00040020,0x00000019,0x00000001,
|
||||||
|
0x00000018,0x0004003b,0x00000019,0x0000001a,0x00000001,0x00040015,0x0000001b,0x00000020,
|
||||||
|
0x00000000,0x0004002b,0x0000001b,0x0000001c,0x00000003,0x00040020,0x0000001d,0x00000001,
|
||||||
|
0x00000006,0x0004002b,0x00000006,0x00000020,0xbf000000,0x0004002b,0x00000006,0x00000027,
|
||||||
|
0x3b808081,0x00040020,0x0000002c,0x00000003,0x00000018,0x0004003b,0x0000002c,0x0000002d,
|
||||||
|
0x00000003,0x00040017,0x0000002e,0x00000006,0x00000003,0x00050036,0x00000002,0x00000004,
|
||||||
|
0x00000000,0x00000003,0x000200f8,0x00000005,0x0004003d,0x00000009,0x0000000c,0x0000000b,
|
||||||
|
0x00050094,0x00000006,0x0000000e,0x0000000c,0x0000000c,0x000500ba,0x00000011,0x00000012,
|
||||||
|
0x0000000e,0x00000010,0x000300f7,0x00000014,0x00000000,0x000400fa,0x00000012,0x00000013,
|
||||||
|
0x00000014,0x000200f8,0x00000013,0x000100fc,0x000200f8,0x00000014,0x00050041,0x0000001d,
|
||||||
|
0x0000001e,0x0000001a,0x0000001c,0x0004003d,0x00000006,0x0000001f,0x0000001e,0x00050085,
|
||||||
|
0x00000006,0x00000022,0x00000020,0x0000000e,0x0006000c,0x00000006,0x00000023,0x00000001,
|
||||||
|
0x0000001b,0x00000022,0x00050085,0x00000006,0x00000024,0x0000001f,0x00000023,0x0007000c,
|
||||||
|
0x00000006,0x00000025,0x00000001,0x00000025,0x00000017,0x00000024,0x000500b8,0x00000011,
|
||||||
|
0x00000028,0x00000025,0x00000027,0x000300f7,0x0000002a,0x00000000,0x000400fa,0x00000028,
|
||||||
|
0x00000029,0x0000002a,0x000200f8,0x00000029,0x000100fc,0x000200f8,0x0000002a,0x0004003d,
|
||||||
|
0x00000018,0x0000002f,0x0000001a,0x0008004f,0x0000002e,0x00000030,0x0000002f,0x0000002f,
|
||||||
|
0x00000000,0x00000001,0x00000002,0x0005008e,0x0000002e,0x00000032,0x00000030,0x00000025,
|
||||||
|
0x00050051,0x00000006,0x00000034,0x00000032,0x00000000,0x00050051,0x00000006,0x00000035,
|
||||||
|
0x00000032,0x00000001,0x00050051,0x00000006,0x00000036,0x00000032,0x00000002,0x00070050,
|
||||||
|
0x00000018,0x00000037,0x00000034,0x00000035,0x00000036,0x00000025,0x0003003e,0x0000002d,
|
||||||
|
0x00000037,0x000100fd,0x00010038,
|
||||||
|
};
|
||||||
|
static const size_t kSplatFragSize = sizeof(kSplatFragSpirv);
|
||||||
|
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
#pragma once
|
||||||
|
// A structural check of SPIR-V before it reaches the driver (vkCreateShaderModule): a .fenv carries the homes'
|
||||||
|
// shaders as it was given them, and a damaged or hostile one is undefined behaviour in the driver's compiler (it may
|
||||||
|
// crash the process or hang the GPU) rather than a refusal. Not a full validator (spirv-val is SPIRV-Tools, megabytes,
|
||||||
|
// and it refuses some shaders drivers take): what's checked is what keeps a parser in bounds and its id tables sane:
|
||||||
|
// - the header: magic (this byte order), SPIR-V 1.0 .. 1.6, an id bound under 4M, schema 0
|
||||||
|
// - every instruction's word count: at least 1, and within the module
|
||||||
|
// - every result id (the opcodes that have one, from the SPIR-V grammar): non-zero, under the bound, defined once,
|
||||||
|
// and every result type an id the module defines
|
||||||
|
// - the literal strings of the debug and mode-setting instructions nul-terminated inside their instruction, and
|
||||||
|
// the ids those instructions name under the bound
|
||||||
|
// - an OpCapability first, one OpMemoryModel, at least one OpEntryPoint, functions opened and closed in order (none
|
||||||
|
// inside another), blocks and parameters only inside functions
|
||||||
|
// Linear in the module: well under a millisecond for a home's biggest shader (tests_loco/spv_check_test.cpp).
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace spvcheck {
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
// (opcode << 2) | (1: has a result id) | (2: has a result type), sorted, for every opcode the SPIR-V headers know
|
||||||
|
// (third_party/spirv-headers; regenerate with tools/gen_spv_ops.py). An opcode not in it is passed over unchecked.
|
||||||
|
constexpr uint16_t kOps[] = {
|
||||||
|
0, 7, 8, 12, 16, 20, 24, 29, 32, 40, 45, 51, 56, 60, 64, 68, 77, 81, 85, 89, 93, 97, 101, 105, 109, 113, 117, 121,
|
||||||
|
125, 129, 133, 137, 141, 145, 149, 153, 156, 167, 171, 175, 179, 183, 187, 195, 199, 203, 207, 211, 219, 223, 224,
|
||||||
|
231, 239, 243, 247, 248, 252, 256, 263, 267, 271, 275, 279, 283, 284, 288, 293, 296, 300, 311, 315, 319, 323, 327,
|
||||||
|
331, 335, 339, 347, 351, 355, 359, 363, 367, 371, 375, 379, 383, 387, 391, 395, 396, 403, 407, 411, 415, 419, 423,
|
||||||
|
427, 431, 439, 443, 447, 451, 455, 459, 463, 467, 471, 475, 479, 483, 487, 491, 495, 499, 507, 511, 515, 519, 523,
|
||||||
|
527, 531, 535, 539, 543, 547, 551, 555, 559, 563, 567, 571, 575, 579, 583, 587, 591, 595, 599, 603, 607, 611, 619,
|
||||||
|
623, 627, 631, 635, 639, 643, 647, 651, 655, 659, 663, 667, 671, 675, 679, 683, 687, 691, 695, 699, 703, 707, 711,
|
||||||
|
715, 719, 723, 727, 731, 735, 739, 743, 747, 751, 755, 759, 763, 767, 779, 783, 787, 791, 795, 799, 803, 807, 811,
|
||||||
|
815, 819, 823, 831, 835, 839, 843, 847, 851, 855, 859, 863, 872, 876, 880, 884, 896, 900, 911, 912, 919, 923, 927,
|
||||||
|
931, 935, 939, 943, 947, 951, 955, 959, 963, 967, 971, 983, 984, 988, 993, 996, 1000, 1004, 1008, 1012, 1016,
|
||||||
|
1020, 1024, 1028, 1039, 1040, 1047, 1051, 1055, 1059, 1063, 1067, 1071, 1075, 1079, 1083, 1087, 1099, 1103, 1107,
|
||||||
|
1111, 1115, 1119, 1120, 1124, 1131, 1135, 1139, 1143, 1147, 1148, 1152, 1167, 1171, 1175, 1179, 1183, 1187, 1188,
|
||||||
|
1192, 1199, 1203, 1204, 1208, 1215, 1219, 1223, 1227, 1231, 1235, 1239, 1243, 1247, 1251, 1255, 1259, 1263, 1267,
|
||||||
|
1268, 1275, 1276, 1283, 1287, 1289, 1295, 1299, 1303, 1307, 1309, 1315, 1316, 1320, 1324, 1328, 1335, 1339, 1343,
|
||||||
|
1347, 1351, 1355, 1359, 1363, 1367, 1371, 1375, 1379, 1383, 1387, 1391, 1395, 1399, 1403, 1407, 1411, 1415, 1419,
|
||||||
|
1423, 1427, 1431, 1435, 1439, 1443, 1447, 1451, 1455, 1459, 1463, 1467, 1603, 1607, 1611, 1615, 16643, 16647,
|
||||||
|
16651, 16653, 16659, 16660, 16667, 16727, 16728, 16735, 16739, 16740, 16744, 16761, 16783, 17664, 17669, 17675,
|
||||||
|
17679, 17683, 17687, 17691, 17695, 17699, 17703, 17704, 17711, 17715, 17719, 17723, 17727, 17731, 17735, 17739,
|
||||||
|
17780, 17784, 17791, 17792, 17796, 17803, 17807, 17811, 17815, 17819, 17823, 17825, 17831, 17832, 17839, 17843,
|
||||||
|
17847, 17851, 17855, 17889, 17892, 17896, 17900, 17904, 17911, 17919, 17923, 17927, 17931, 17935, 17991, 18003,
|
||||||
|
18007, 18011, 18015, 18023, 18163, 18167, 18171, 18183, 20003, 20007, 20011, 20015, 20019, 20023, 20027, 20031,
|
||||||
|
20047, 20051, 20227, 20299, 20300, 20305, 20315, 20363, 20407, 20413, 20417, 20443, 20447, 20461, 20479, 20484,
|
||||||
|
20507, 20508, 20519, 20591, 20595, 20635, 20639, 20996, 21000, 21004, 21011, 21015, 21019, 21023, 21024, 21031,
|
||||||
|
21035, 21036, 21040, 21044, 21048, 21052, 21056, 21063, 21064, 21071, 21075, 21079, 21083, 21087, 21091, 21095,
|
||||||
|
21099, 21103, 21107, 21111, 21115, 21116, 21120, 21125, 21135, 21153, 21159, 21160, 21164, 21171, 21175, 21176,
|
||||||
|
21180, 21187, 21196, 21203, 21207, 21211, 21212, 21216, 21220, 21224, 21231, 21235, 21236, 21240, 21244, 21248,
|
||||||
|
21253, 21256, 21260, 21264, 21268, 21272, 21276, 21283, 21284, 21291, 21295, 21299, 21303, 21307, 21311, 21315,
|
||||||
|
21319, 21323, 21327, 21331, 21335, 21339, 21340, 21344, 21348, 21352, 21356, 21363, 21365, 21376, 21383, 21387,
|
||||||
|
21391, 21395, 21399, 21403, 21407, 21411, 21433, 21439, 21440, 21447, 21451, 21455, 21456, 21460, 21467, 21471,
|
||||||
|
21472, 21479, 21481, 21485, 21491, 21495, 21499, 21503, 21507, 21511, 21515, 21519, 21520, 21527, 21531, 21539,
|
||||||
|
21563, 21567, 21571, 21575, 21579, 21583, 21587, 21588, 21595, 21711, 21715, 21719, 21723, 21727, 21731, 21735,
|
||||||
|
21739, 21743, 21747, 21751, 21755, 21759, 22287, 22291, 22295, 22299, 22303, 22304, 22311, 22312, 22323, 22324,
|
||||||
|
22343, 22347, 22351, 22355, 22359, 22363, 22367, 22371, 22375, 22379, 22383, 22387, 22391, 22395, 22403, 22407,
|
||||||
|
22437, 22443, 22447, 22459, 22463, 22520, 22527, 22528, 22532, 22799, 22801, 22805, 22809, 22813, 22817, 22821,
|
||||||
|
22825, 22829, 22833, 22837, 22841, 22845, 22849, 22855, 22859, 22863, 22867, 22871, 22875, 22879, 22883, 22887,
|
||||||
|
22891, 22895, 22899, 22903, 22907, 22911, 22915, 22919, 22923, 22927, 22931, 22935, 22939, 22943, 22947, 22951,
|
||||||
|
22955, 22959, 22963, 22967, 22971, 22975, 22979, 22983, 22987, 22991, 22995, 22999, 23003, 23007, 23011, 23015,
|
||||||
|
23019, 23023, 23027, 23031, 23035, 23039, 23043, 23047, 23051, 23055, 23059, 23063, 23067, 23071, 23075, 23079,
|
||||||
|
23083, 23087, 23091, 23095, 23099, 23103, 23107, 23111, 23115, 23119, 23123, 23127, 23131, 23135, 23139, 23143,
|
||||||
|
23147, 23151, 23155, 23159, 23163, 23167, 23171, 23175, 23179, 23183, 23187, 23191, 23195, 23199, 23203, 23207,
|
||||||
|
23211, 23215, 23219, 23223, 23227, 23231, 23235, 23239, 23243, 23247, 23251, 23255, 23259, 23263, 23267, 23275,
|
||||||
|
23279, 23280, 23363, 23367, 23371, 23375, 23387, 23391, 23395, 23399, 23403, 23407, 23411, 23415, 23419, 23423,
|
||||||
|
23427, 23431, 23435, 23439, 23443, 23447, 23451, 23455, 23459, 23463, 23467, 23471, 23475, 23479, 23483, 23487,
|
||||||
|
23491, 23495, 23499, 23503, 23507, 23511, 23515, 23519, 23523, 23527, 23531, 23548, 23645, 23649, 23653, 23695,
|
||||||
|
23699, 23703, 23707, 23711, 23715, 23719, 23723, 23727, 23731, 23735, 23739, 23755, 23784, 23788, 23799, 24067,
|
||||||
|
24071, 24075, 24079, 24083, 24087, 24091, 24095, 24099, 24103, 24107, 24111, 24115, 24119, 24123, 24127, 24131,
|
||||||
|
24143, 24345, 24360, 24364, 24368, 24384, 24467, 24471, 24568, 24572, 24583, 24655, 24656, 24663, 24664, 24797,
|
||||||
|
24884, 24924, 24928, 24932, 24936, 24940, 24951, 24971, 24979, 24992, 24996, 25000, 25007, 25011, 25015, 25019,
|
||||||
|
25035, 25036, 25607, 25611, 25615, 25619, 25623, 25627, 25631, 25635, 25707, 25715, 25716, 26119, 26123, 26127,
|
||||||
|
27667, 27671, 27675,
|
||||||
|
};
|
||||||
|
// -1: unknown opcode, else its flags.
|
||||||
|
inline int opFlags(uint32_t op) {
|
||||||
|
if (op > 0x3FFF) return -1;
|
||||||
|
size_t lo = 0, hi = sizeof kOps / sizeof kOps[0];
|
||||||
|
while (lo < hi) {
|
||||||
|
const size_t mid = (lo + hi) / 2;
|
||||||
|
const uint32_t o = kOps[mid] >> 2u;
|
||||||
|
if (o == op) return kOps[mid] & 3;
|
||||||
|
if (o < op) lo = mid + 1;
|
||||||
|
else hi = mid;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// The literal string starting at word `at` of an instruction of `n` words ends (a nul byte) inside it; `after` is the
|
||||||
|
// word after it.
|
||||||
|
inline bool stringEnds(const uint32_t* w, uint32_t n, uint32_t at, uint32_t* after = nullptr) {
|
||||||
|
for (uint32_t i = at; i < n; ++i) {
|
||||||
|
const uint32_t v = w[i];
|
||||||
|
if ((v & 0xFFu) == 0 || (v & 0xFF00u) == 0 || (v & 0xFF0000u) == 0 || (v & 0xFF000000u) == 0) {
|
||||||
|
if (after) *after = i + 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
enum : uint32_t {
|
||||||
|
OpSourceExtension = 4, OpName = 5, OpMemberName = 6, OpString = 7, OpExtension = 10, OpExtInstImport = 11,
|
||||||
|
OpMemoryModel = 14, OpEntryPoint = 15, OpExecutionMode = 16, OpCapability = 17, OpTypeForwardPointer = 39,
|
||||||
|
OpFunction = 54, OpFunctionParameter = 55, OpFunctionEnd = 56, OpDecorate = 71, OpMemberDecorate = 72,
|
||||||
|
OpLabel = 248, OpModuleProcessed = 330,
|
||||||
|
};
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
// True when `code` (`words` 32-bit words) passes; else false, and `why` says what's wrong.
|
||||||
|
inline bool check(const uint32_t* code, size_t words, std::string* why = nullptr) {
|
||||||
|
using namespace detail;
|
||||||
|
auto fail = [&](const char* what, size_t at) {
|
||||||
|
if (why) *why = std::string(what) + " (word " + std::to_string(at) + ")";
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (!code || words < 5) return fail("shorter than a SPIR-V header", 0);
|
||||||
|
if (code[0] != 0x07230203u) return fail("not SPIR-V (bad magic number)", 0);
|
||||||
|
const uint32_t version = code[1];
|
||||||
|
if ((version & 0xFF0000FFu) != 0 || (version >> 16) != 1 || ((version >> 8) & 0xFFu) > 6) return fail("unknown SPIR-V version", 1);
|
||||||
|
const uint32_t bound = code[3];
|
||||||
|
if (bound == 0 || bound > (1u << 22)) return fail("id bound out of range", 3);
|
||||||
|
if (code[4] != 0) return fail("non-zero schema", 4);
|
||||||
|
std::vector<uint8_t> defined(bound, 0);
|
||||||
|
std::vector<uint32_t> typeUses; // result types named: checked once every id is known
|
||||||
|
uint32_t memoryModels = 0, entryPoints = 0;
|
||||||
|
bool inFunction = false, first = true;
|
||||||
|
for (size_t i = 5; i < words;) {
|
||||||
|
const uint32_t n = code[i] >> 16, op = code[i] & 0xFFFFu;
|
||||||
|
if (n == 0) return fail("instruction of 0 words", i);
|
||||||
|
if (n > words - i) return fail("instruction runs past the end", i);
|
||||||
|
const uint32_t* w = code + i;
|
||||||
|
if (first && op != OpCapability) return fail("doesn't start with OpCapability", i);
|
||||||
|
first = false;
|
||||||
|
const int flags = opFlags(op);
|
||||||
|
if (flags > 0) {
|
||||||
|
const uint32_t at = (flags & 2) ? 2u : 1u;
|
||||||
|
if (n <= at) return fail("instruction too short for its result", i);
|
||||||
|
if (flags & 2) {
|
||||||
|
if (w[1] == 0 || w[1] >= bound) return fail("result type out of range", i);
|
||||||
|
typeUses.push_back(w[1]);
|
||||||
|
}
|
||||||
|
const uint32_t id = w[at];
|
||||||
|
if (id == 0 || id >= bound) return fail("result id out of range", i);
|
||||||
|
if (defined[id]) return fail("result id defined twice", i);
|
||||||
|
defined[id] = 1;
|
||||||
|
}
|
||||||
|
switch (op) {
|
||||||
|
case OpSourceExtension: case OpExtension: case OpModuleProcessed:
|
||||||
|
if (!stringEnds(w, n, 1)) return fail("unterminated string", i);
|
||||||
|
break;
|
||||||
|
case OpName:
|
||||||
|
if (n < 3 || w[1] == 0 || w[1] >= bound || !stringEnds(w, n, 2)) return fail("bad OpName", i);
|
||||||
|
break;
|
||||||
|
case OpMemberName:
|
||||||
|
if (n < 4 || w[1] == 0 || w[1] >= bound || !stringEnds(w, n, 3)) return fail("bad OpMemberName", i);
|
||||||
|
break;
|
||||||
|
case OpString: case OpExtInstImport:
|
||||||
|
if (!stringEnds(w, n, 2)) return fail("unterminated string", i);
|
||||||
|
break;
|
||||||
|
case OpMemoryModel:
|
||||||
|
if (n < 3) return fail("bad OpMemoryModel", i);
|
||||||
|
++memoryModels;
|
||||||
|
break;
|
||||||
|
case OpEntryPoint: {
|
||||||
|
uint32_t after = 0;
|
||||||
|
if (n < 4 || w[2] == 0 || w[2] >= bound || !stringEnds(w, n, 3, &after)) return fail("bad OpEntryPoint", i);
|
||||||
|
for (uint32_t k = after; k < n; ++k)
|
||||||
|
if (w[k] == 0 || w[k] >= bound) return fail("OpEntryPoint interface id out of range", i);
|
||||||
|
++entryPoints;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OpExecutionMode:
|
||||||
|
if (n < 3 || w[1] == 0 || w[1] >= bound) return fail("bad OpExecutionMode", i);
|
||||||
|
break;
|
||||||
|
case OpDecorate: case OpMemberDecorate:
|
||||||
|
if (n < (op == OpDecorate ? 3u : 4u) || w[1] == 0 || w[1] >= bound) return fail("bad decoration", i);
|
||||||
|
break;
|
||||||
|
case OpTypeForwardPointer:
|
||||||
|
if (n < 3 || w[1] == 0 || w[1] >= bound) return fail("bad OpTypeForwardPointer", i);
|
||||||
|
break;
|
||||||
|
case OpFunction:
|
||||||
|
if (inFunction) return fail("function inside a function", i);
|
||||||
|
inFunction = true;
|
||||||
|
break;
|
||||||
|
case OpFunctionEnd:
|
||||||
|
if (!inFunction) return fail("OpFunctionEnd outside a function", i);
|
||||||
|
inFunction = false;
|
||||||
|
break;
|
||||||
|
case OpFunctionParameter: case OpLabel:
|
||||||
|
if (!inFunction) return fail("block or parameter outside a function", i);
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
i += n;
|
||||||
|
}
|
||||||
|
if (inFunction) return fail("function not ended", words);
|
||||||
|
if (memoryModels != 1) return fail("not exactly one OpMemoryModel", words);
|
||||||
|
if (entryPoints == 0) return fail("no OpEntryPoint", words);
|
||||||
|
for (uint32_t t : typeUses)
|
||||||
|
if (!defined[t]) return fail("a result type that's never defined", words);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
inline bool check(const std::vector<uint32_t>& code, std::string* why = nullptr) { return check(code.data(), code.size(), why); }
|
||||||
|
|
||||||
|
} // namespace spvcheck
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
// spv_opt.h: Meta's shaders through the SPIRV-Tools optimizer, or made relaxed precision.
|
||||||
|
#include "spv_opt.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <set>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#ifdef FH_HAVE_SPV_OPT
|
||||||
|
#include "spirv-tools/optimizer.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace spvopt {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr const char* kMark = "frame-spvopt ";
|
||||||
|
|
||||||
|
enum : uint32_t {
|
||||||
|
OpSourceContinued = 2, OpSource = 3, OpSourceExtension = 4, OpName = 5, OpMemberName = 6, OpString = 7,
|
||||||
|
OpExtension = 10, OpExtInstImport = 11, OpExtInst = 12, OpMemoryModel = 14, OpEntryPoint = 15, OpExecutionMode = 16,
|
||||||
|
OpCapability = 17, OpTypeFloat = 22, OpTypeVector = 23, OpTypeMatrix = 24, OpTypePointer = 32, OpFunction = 54,
|
||||||
|
OpVariable = 59, OpLoad = 61, OpStore = 62, OpAccessChain = 65, OpInBoundsAccessChain = 66, OpPtrAccessChain = 67,
|
||||||
|
OpDecorate = 71, OpMemberDecorate = 72, OpDecorationGroup = 73, OpGroupDecorate = 74, OpGroupMemberDecorate = 75,
|
||||||
|
OpVectorShuffle = 79, OpCompositeConstruct = 80, OpCompositeExtract = 81, OpCompositeInsert = 82, OpCopyObject = 83,
|
||||||
|
OpImageSampleImplicitLod = 87, OpImageGather = 96, OpImageDrefGather = 97, OpImageRead = 98, OpImageQueryLod = 105,
|
||||||
|
OpConvertSToF = 111, OpConvertUToF = 112, OpFNegate = 127, OpFAdd = 129, OpFSub = 131, OpFMul = 133, OpFDiv = 136,
|
||||||
|
OpFRem = 140, OpFMod = 141, OpVectorTimesScalar = 142, OpDot = 148, OpSelect = 169, OpDPdx = 207, OpFwidthCoarse = 215,
|
||||||
|
OpPhi = 245, OpImageSparseSampleImplicitLod = 305, OpImageSparseRead = 320, OpModuleProcessed = 330,
|
||||||
|
OpExecutionModeId = 331, OpDecorateId = 332, OpDecorateString = 5632, OpMemberDecorateString = 5633,
|
||||||
|
DecRelaxedPrecision = 0, DecBuiltIn = 11, BuiltInFragDepth = 22, ModelFragment = 4, StorageFunction = 7,
|
||||||
|
OpUndef = 1, OpConstantTrue = 41, OpConstantNull = 46,
|
||||||
|
GlslFAbs = 4, GlslFloor = 8, GlslCeil = 9, GlslFract = 10, GlslPow = 26, GlslExp2 = 29, GlslSqrt = 31, GlslFMin = 37,
|
||||||
|
GlslFMax = 40, GlslFClamp = 43, GlslFMix = 46, GlslStep = 48, GlslSmoothStep = 49, GlslLength = 66, GlslNormalize = 69,
|
||||||
|
};
|
||||||
|
|
||||||
|
bool preamble(uint32_t op) { // capabilities .. debug names: where the mark is looked for
|
||||||
|
return op == OpSourceContinued || op == OpSource || op == OpSourceExtension || op == OpName || op == OpMemberName ||
|
||||||
|
op == OpString || op == OpExtension || op == OpExtInstImport || op == OpMemoryModel || op == OpEntryPoint ||
|
||||||
|
op == OpExecutionMode || op == OpCapability || op == OpModuleProcessed || op == OpExecutionModeId;
|
||||||
|
}
|
||||||
|
bool annotation(uint32_t op) {
|
||||||
|
return (op >= OpDecorate && op <= OpGroupMemberDecorate) || op == OpDecorateId || op == OpDecorateString ||
|
||||||
|
op == OpMemberDecorateString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The words of a literal string, nul-terminated and padded.
|
||||||
|
std::vector<uint32_t> literal(const std::string& s) {
|
||||||
|
std::vector<uint32_t> w(s.size() / 4 + 1, 0);
|
||||||
|
std::memcpy(w.data(), s.data(), s.size());
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `code` with its mark: an OpSourceExtension, after the module's own OpString/OpSource* and before its debug names
|
||||||
|
// (where the spec puts it). Not OpModuleProcessed, the instruction meant for it: that needs SPIR-V 1.1, and Meta's
|
||||||
|
// shaders are 1.0 (spirv-val rejects them with one).
|
||||||
|
std::vector<uint32_t> withMark(const std::vector<uint32_t>& code, int flags) {
|
||||||
|
size_t at = 5;
|
||||||
|
while (at < code.size() && (code[at] >> 16) && preamble(code[at] & 0xFFFF) && (code[at] & 0xFFFF) != OpName &&
|
||||||
|
(code[at] & 0xFFFF) != OpMemberName && (code[at] & 0xFFFF) != OpModuleProcessed)
|
||||||
|
at += code[at] >> 16;
|
||||||
|
const std::vector<uint32_t> s = literal(kMark + std::to_string(flags));
|
||||||
|
std::vector<uint32_t> out(code.begin(), code.begin() + static_cast<std::ptrdiff_t>(at));
|
||||||
|
out.push_back(((1u + static_cast<uint32_t>(s.size())) << 16) | OpSourceExtension);
|
||||||
|
out.insert(out.end(), s.begin(), s.end());
|
||||||
|
out.insert(out.end(), code.begin() + static_cast<std::ptrdiff_t>(at), code.end());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t hashOf(const std::vector<uint32_t>& code, int flags) {
|
||||||
|
uint64_t h = 1469598103934665603ull ^ static_cast<uint64_t>(flags);
|
||||||
|
for (uint32_t w : code) h = (h ^ w) * 0x100000001B3ull;
|
||||||
|
return h ^ code.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// kRelax: a fragment shader's colour math decorated RelaxedPrecision (16-bit is enough; see "Relaxed" below), but
|
||||||
|
// never a value that reaches a texture lookup's coordinates (or lod, gradients, offsets, depth reference), a
|
||||||
|
// derivative, or the depth output: half precision on a UV is a visibly blocky texture, and on a derivative a wrong
|
||||||
|
// mip. Everything stored to a function variable that a kept value is loaded from is kept too. The input when it
|
||||||
|
// isn't a fragment shader; empty when the module can't be read.
|
||||||
|
std::vector<uint32_t> relax(const std::vector<uint32_t>& code) {
|
||||||
|
if (code.size() < 5 || code[0] != 0x07230203u) return {};
|
||||||
|
struct Ins { size_t at; uint32_t op, words; };
|
||||||
|
std::vector<Ins> ins;
|
||||||
|
for (size_t i = 5; i < code.size();) {
|
||||||
|
const uint32_t words = code[i] >> 16;
|
||||||
|
if (!words || i + words > code.size()) return {};
|
||||||
|
ins.push_back({i, code[i] & 0xFFFF, words});
|
||||||
|
i += words;
|
||||||
|
}
|
||||||
|
bool fragment = false;
|
||||||
|
uint32_t glsl = 0, entryPoints = 0;
|
||||||
|
std::set<uint32_t> float32, relaxedAlready, fragDepth;
|
||||||
|
std::map<uint32_t, uint32_t> varStorage; // variable -> storage class
|
||||||
|
std::map<uint32_t, size_t> def; // result id -> instruction index (function bodies)
|
||||||
|
std::map<uint32_t, uint32_t> root; // pointer -> the variable it points into
|
||||||
|
std::map<uint32_t, std::vector<uint32_t>> storedTo; // variable -> the values stored to it
|
||||||
|
bool inFunction = false;
|
||||||
|
for (size_t k = 0; k < ins.size(); ++k) {
|
||||||
|
const uint32_t* w = &code[ins[k].at];
|
||||||
|
const uint32_t op = ins[k].op, n = ins[k].words;
|
||||||
|
if (op == OpEntryPoint && n >= 2) { ++entryPoints; fragment = w[1] == ModelFragment; }
|
||||||
|
else if (op == OpExtInstImport && n >= 3 && !std::strncmp(reinterpret_cast<const char*>(w + 2), "GLSL.std.450", 12)) glsl = w[1];
|
||||||
|
else if (op == OpTypeFloat && n >= 3 && w[2] == 32) float32.insert(w[1]);
|
||||||
|
else if ((op == OpTypeVector || op == OpTypeMatrix) && n >= 4 && float32.count(w[2])) float32.insert(w[1]);
|
||||||
|
else if (op == OpDecorate && n >= 3 && w[2] == DecRelaxedPrecision) relaxedAlready.insert(w[1]);
|
||||||
|
else if (op == OpDecorate && n >= 4 && w[2] == DecBuiltIn && w[3] == BuiltInFragDepth) fragDepth.insert(w[1]);
|
||||||
|
else if (op == OpVariable && n >= 4) { varStorage[w[2]] = w[3]; root[w[2]] = w[2]; }
|
||||||
|
if (op == OpFunction) inFunction = true;
|
||||||
|
if (!inFunction) continue;
|
||||||
|
if ((op == OpAccessChain || op == OpInBoundsAccessChain || op == OpPtrAccessChain) && n >= 4 && root.count(w[3])) root[w[2]] = root[w[3]];
|
||||||
|
if (op == OpStore && n >= 3 && root.count(w[1])) storedTo[root[w[1]]].push_back(w[2]);
|
||||||
|
const bool noResult = op == OpStore || op == 63 /*CopyMemory*/ || op == 99 /*ImageWrite*/ || (op >= 246 && op <= 255) || // (merges, branches, returns)
|
||||||
|
op == 8 /*Line*/ || op == 224 || op == 225 /*barriers*/;
|
||||||
|
if (n >= 3 && !noResult) def[w[2]] = k; // (a result id, for every op that has one)
|
||||||
|
}
|
||||||
|
if (!fragment || entryPoints != 1) return code;
|
||||||
|
// The values to keep at full precision, and everything they are made from.
|
||||||
|
std::set<uint32_t> keep;
|
||||||
|
std::vector<uint32_t> work;
|
||||||
|
auto seed = [&](uint32_t id) { if (keep.insert(id).second) work.push_back(id); };
|
||||||
|
for (const Ins& in : ins) {
|
||||||
|
const uint32_t* w = &code[in.at];
|
||||||
|
const bool image = (in.op >= OpImageSampleImplicitLod && in.op <= OpImageRead) || in.op == OpImageQueryLod ||
|
||||||
|
(in.op >= OpImageSparseSampleImplicitLod && in.op <= OpImageSparseRead);
|
||||||
|
if (image) for (uint32_t j = 4; j < in.words; ++j) seed(w[j]);
|
||||||
|
if (in.op >= OpDPdx && in.op <= OpFwidthCoarse && in.words >= 4) seed(w[3]);
|
||||||
|
if (in.op == OpStore && in.words >= 3 && root.count(w[1]) && fragDepth.count(root[w[1]])) seed(w[2]);
|
||||||
|
}
|
||||||
|
while (!work.empty()) {
|
||||||
|
const uint32_t id = work.back();
|
||||||
|
work.pop_back();
|
||||||
|
const auto d = def.find(id);
|
||||||
|
if (d == def.end()) continue;
|
||||||
|
const Ins& in = ins[d->second];
|
||||||
|
const uint32_t* w = &code[in.at];
|
||||||
|
for (uint32_t j = 3; j < in.words; ++j) if (def.count(w[j])) seed(w[j]); // (literals that look like ids: kept too)
|
||||||
|
if (in.op == OpLoad && in.words >= 4 && root.count(w[3]) && varStorage[root[w[3]]] == StorageFunction)
|
||||||
|
for (uint32_t v : storedTo[root[w[3]]]) seed(v);
|
||||||
|
}
|
||||||
|
// Relaxed: colour math only. A value is relaxed when it is made (by an operation that can't overflow 16 bits from
|
||||||
|
// inputs that fit them) from texture lookups, constants, values Meta's own shader already marks relaxed, and
|
||||||
|
// other relaxed values, and is not kept above. Anything made from an interpolated input or a uniform (a
|
||||||
|
// position, a view vector, a light's distance) stays 32-bit: relaxing every float op instead turned Haven's
|
||||||
|
// lit walls black (2026-10-24), the view and light math overflowing or losing its precision in 16 bits.
|
||||||
|
std::set<uint32_t> relaxed, constants;
|
||||||
|
for (const Ins& in : ins)
|
||||||
|
if (in.words >= 3 && (in.op == OpUndef || (in.op >= OpConstantTrue && in.op <= OpConstantNull))) constants.insert(code[in.at + 2]);
|
||||||
|
auto source = [&](uint32_t id) { // an operand a relaxed value may be made from
|
||||||
|
return relaxed.count(id) || constants.count(id) || relaxedAlready.count(id);
|
||||||
|
};
|
||||||
|
auto colourOp = [&](const Ins& in, const uint32_t* w) {
|
||||||
|
const uint32_t op = in.op;
|
||||||
|
if (op == OpFNegate || op == OpFAdd || op == OpFSub || op == OpFMul || (op >= OpVectorTimesScalar && op <= OpDot) ||
|
||||||
|
op == OpCopyObject || op == OpCompositeConstruct)
|
||||||
|
return true;
|
||||||
|
if (op == OpExtInst && in.words >= 5 && w[3] == glsl)
|
||||||
|
switch (w[4]) {
|
||||||
|
case GlslFAbs: case GlslFloor: case GlslCeil: case GlslFract: case GlslSqrt: case GlslPow: case GlslExp2:
|
||||||
|
case GlslFMin: case GlslFMax: case GlslFClamp: case GlslFMix: case GlslStep: case GlslSmoothStep:
|
||||||
|
case GlslLength: case GlslNormalize: return true;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
return op == OpVectorShuffle || op == OpCompositeExtract || op == OpCompositeInsert || op == OpSelect || op == OpPhi ||
|
||||||
|
(op >= OpImageSampleImplicitLod && op <= OpImageGather) || op == OpLoad;
|
||||||
|
};
|
||||||
|
// The value operands of one instruction (not its literals, a select's condition or a phi's blocks).
|
||||||
|
auto operands = [&](const Ins& in, const uint32_t* w, std::vector<uint32_t>& ids) {
|
||||||
|
ids.clear();
|
||||||
|
const uint32_t op = in.op, n = in.words;
|
||||||
|
if (op == OpVectorShuffle || op == OpCompositeInsert) { if (n >= 5) ids = {w[3], w[4]}; }
|
||||||
|
else if (op == OpCompositeExtract) { if (n >= 4) ids = {w[3]}; }
|
||||||
|
else if (op == OpSelect) { if (n >= 6) ids = {w[4], w[5]}; }
|
||||||
|
else if (op == OpPhi) { for (uint32_t j = 3; j + 1 < n; j += 2) ids.push_back(w[j]); }
|
||||||
|
else if (op == OpExtInst) { for (uint32_t j = 5; j < n; ++j) ids.push_back(w[j]); }
|
||||||
|
else for (uint32_t j = 3; j < n; ++j) ids.push_back(w[j]);
|
||||||
|
};
|
||||||
|
std::vector<uint32_t> ids;
|
||||||
|
for (bool changed = true; changed;) { // (to a fixed point: phis and function variables loop back)
|
||||||
|
changed = false;
|
||||||
|
for (const Ins& in : ins) {
|
||||||
|
const uint32_t* w = &code[in.at];
|
||||||
|
if (in.words < 3 || !float32.count(w[1]) || keep.count(w[2]) || relaxed.count(w[2]) || !colourOp(in, w)) continue;
|
||||||
|
bool ok = true;
|
||||||
|
if (in.op >= OpImageSampleImplicitLod && in.op <= OpImageGather) ok = true; // (a texel: what colour math starts from)
|
||||||
|
else if (in.op == OpLoad) { // a function variable's value: relaxed when all stored to it is
|
||||||
|
ok = in.words >= 4 && root.count(w[3]) && varStorage[root[w[3]]] == StorageFunction;
|
||||||
|
if (ok) for (uint32_t v : storedTo[root[w[3]]]) ok = ok && source(v);
|
||||||
|
} else {
|
||||||
|
operands(in, w, ids);
|
||||||
|
for (uint32_t id : ids) ok = ok && source(id);
|
||||||
|
}
|
||||||
|
if (ok) { relaxed.insert(w[2]); changed = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::vector<uint32_t> decorations;
|
||||||
|
for (uint32_t id : relaxed)
|
||||||
|
if (!relaxedAlready.count(id) && def.count(id) && ins[def[id]].op != OpLoad)
|
||||||
|
decorations.insert(decorations.end(), {(3u << 16) | OpDecorate, id, DecRelaxedPrecision});
|
||||||
|
if (decorations.empty()) return code;
|
||||||
|
size_t at = 5; // after the module's own annotations
|
||||||
|
while (at < code.size() && (code[at] >> 16) && (preamble(code[at] & 0xFFFF) || annotation(code[at] & 0xFFFF))) at += code[at] >> 16;
|
||||||
|
std::vector<uint32_t> out(code.begin(), code.begin() + static_cast<std::ptrdiff_t>(at));
|
||||||
|
out.insert(out.end(), decorations.begin(), decorations.end());
|
||||||
|
out.insert(out.end(), code.begin() + static_cast<std::ptrdiff_t>(at), code.end());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::mutex g_mutex;
|
||||||
|
std::unordered_map<uint64_t, std::vector<uint32_t>> g_cache;
|
||||||
|
Stats g_stats;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int markOf(const std::vector<uint32_t>& code) {
|
||||||
|
if (code.size() < 5 || code[0] != 0x07230203u) return -1;
|
||||||
|
const size_t len = std::strlen(kMark);
|
||||||
|
for (size_t i = 5; i < code.size();) {
|
||||||
|
const uint32_t words = code[i] >> 16, op = code[i] & 0xFFFF;
|
||||||
|
if (!words || i + words > code.size() || !preamble(op)) break;
|
||||||
|
if ((op == OpSourceExtension || op == OpModuleProcessed) && (words - 1) * 4 > len) {
|
||||||
|
const char* s = reinterpret_cast<const char*>(&code[i + 1]);
|
||||||
|
if (!std::strncmp(s, kMark, len) && std::memchr(s, 0, (words - 1) * 4)) return std::atoi(s + len);
|
||||||
|
}
|
||||||
|
i += words;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flags() {
|
||||||
|
static const int f = [] {
|
||||||
|
auto on = [](const char* name) { const char* v = std::getenv(name); return v && *v && *v != '0'; };
|
||||||
|
int v = 0;
|
||||||
|
#ifdef FH_HAVE_SPV_OPT
|
||||||
|
if (on("FH_SPV_OPT")) v |= kOptimize;
|
||||||
|
#endif
|
||||||
|
if (const char* h = std::getenv("FH_SPV_HALF")) v |= std::atoi(h) >= 2 ? (kRelax | kHalf) : std::atoi(h) == 1 ? kRelax : 0;
|
||||||
|
#ifndef FH_HAVE_SPV_OPT
|
||||||
|
v &= ~kHalf; // (the 16-bit rewrite is SPIRV-Tools'; relaxing alone is ours)
|
||||||
|
(void)on;
|
||||||
|
#endif
|
||||||
|
return v;
|
||||||
|
}();
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flagsFor(const std::string& programPath) {
|
||||||
|
static const char* only = std::getenv("FH_SPV_HALF_ONLY");
|
||||||
|
const int f = flags();
|
||||||
|
return only && programPath.find(only) == std::string::npos ? f & ~(kRelax | kHalf) : f;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stats stats() {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
return g_stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint32_t> run(const std::vector<uint32_t>& code, int flags, std::string* err) {
|
||||||
|
if (!flags || code.size() < 5 || code[0] != 0x07230203u || markOf(code) == flags) return code;
|
||||||
|
const uint64_t key = hashOf(code, flags);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
if (const auto it = g_cache.find(key); it != g_cache.end()) {
|
||||||
|
++g_stats.cached;
|
||||||
|
return it->second.empty() ? code : it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto t0 = std::chrono::steady_clock::now();
|
||||||
|
std::string why;
|
||||||
|
std::vector<uint32_t> out = code;
|
||||||
|
bool ok = true;
|
||||||
|
#ifdef FH_HAVE_SPV_OPT
|
||||||
|
// The shaders' own SPIR-V version picks the Vulkan the validator holds them to (1.3 is Vulkan 1.1's, 1.5 1.2's).
|
||||||
|
const spv_target_env env = code[1] > 0x10500u ? SPV_ENV_VULKAN_1_3 : code[1] > 0x10300u ? SPV_ENV_VULKAN_1_2 : SPV_ENV_VULKAN_1_1;
|
||||||
|
auto consumer = [&why](spv_message_level_t level, const char*, const spv_position_t& pos, const char* msg) {
|
||||||
|
if (level <= SPV_MSG_ERROR && why.empty()) why = std::string(msg) + " (word " + std::to_string(pos.index) + ")";
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
// Relaxed first, on Meta's module as it ships: relax() reads its function variables and its own RelaxedPrecision
|
||||||
|
// marks, which the optimizer folds away. Relaxing after the optimizer turned Haven's lit walls black (2026-10-25).
|
||||||
|
bool relaxed = false;
|
||||||
|
if (flags & kRelax) {
|
||||||
|
std::vector<uint32_t> r = relax(out);
|
||||||
|
ok = !r.empty();
|
||||||
|
relaxed = ok && r.size() != out.size();
|
||||||
|
if (!ok) why = "relax: unreadable module";
|
||||||
|
else out.swap(r);
|
||||||
|
}
|
||||||
|
#ifdef FH_HAVE_SPV_OPT
|
||||||
|
spvtools::OptimizerOptions options;
|
||||||
|
options.set_run_validator(true); // (a module it can't read is left as it is, not optimized wrong)
|
||||||
|
if (ok && (flags & kOptimize)) {
|
||||||
|
spvtools::Optimizer opt(env);
|
||||||
|
opt.SetMessageConsumer(consumer);
|
||||||
|
// The interface (inputs, outputs, bindings) as it was: the renderer lays out vertex streams and descriptor
|
||||||
|
// sets from the shipped shaders.
|
||||||
|
opt.RegisterPerformancePasses(true);
|
||||||
|
std::vector<uint32_t> o;
|
||||||
|
ok = opt.Run(out.data(), out.size(), &o, options);
|
||||||
|
if (ok) out.swap(o);
|
||||||
|
}
|
||||||
|
if (ok && relaxed && (flags & kHalf)) {
|
||||||
|
spvtools::Optimizer opt(env);
|
||||||
|
opt.SetMessageConsumer(consumer);
|
||||||
|
opt.RegisterPass(spvtools::CreateConvertRelaxedToHalfPass())
|
||||||
|
.RegisterPass(spvtools::CreateSimplificationPass())
|
||||||
|
.RegisterPass(spvtools::CreateRedundancyEliminationPass())
|
||||||
|
.RegisterPass(spvtools::CreateAggressiveDCEPass());
|
||||||
|
std::vector<uint32_t> o;
|
||||||
|
ok = opt.Run(out.data(), out.size(), &o, options);
|
||||||
|
if (ok) out.swap(o);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
(void)relaxed;
|
||||||
|
#endif
|
||||||
|
if (ok) out = withMark(out, flags);
|
||||||
|
if (const char* dir = std::getenv("FH_SPV_OPT_DUMP")) { // diagnostics: every module before and after, to spirv-val
|
||||||
|
char base[64];
|
||||||
|
std::snprintf(base, sizeof base, "/%016llx", static_cast<unsigned long long>(key));
|
||||||
|
for (const auto& [ext, c] : {std::pair<const char*, const std::vector<uint32_t>*>{".in.spv", &code}, {ok ? ".out.spv" : ".failed.spv", &out}})
|
||||||
|
if (FILE* f = std::fopen((std::string(dir) + base + ext).c_str(), "wb")) { std::fwrite(c->data(), 4, c->size(), f); std::fclose(f); }
|
||||||
|
}
|
||||||
|
const double ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count();
|
||||||
|
if (!ok && err) *err = why.empty() ? std::string("optimizer failed") : why;
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
g_stats.ms += ms;
|
||||||
|
if (ok) ++g_stats.made; else ++g_stats.failed;
|
||||||
|
if (!ok) { g_cache[key].clear(); return code; }
|
||||||
|
return g_cache[key] = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace spvopt
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#pragma once
|
||||||
|
// Meta's shaders made cheaper for the GPU before the driver sees them, two ways, both off unless asked for:
|
||||||
|
// - FH_SPV_OPT=1: the SPIRV-Tools optimizer's performance recipe (third_party/spirv-tools, built with
|
||||||
|
// -DFRAME_SPV_OPT=ON): inlining, dead code removal, constant folding of what spv_specialize.h baked in, copy
|
||||||
|
// propagation, if-conversion ... The picture stays the same (at most 1/255 apart), but on the Frame it saved
|
||||||
|
// nothing: Turnip's own compiler already does all of it (the same fragment instructions, the same GPU cycles in
|
||||||
|
// every home measured, 2026-10-24), while making the modules cost ~22 ms each as a home loaded (4 s for Haven).
|
||||||
|
// - FH_SPV_HALF=1: fragment shaders' colour math (what is made from texels and constants alone) marked
|
||||||
|
// RelaxedPrecision (Turnip then runs it in 16 bits), never what feeds a texture lookup, a derivative or the depth;
|
||||||
|
// =2 also rewrites it as 16-bit floats (SPIRV-Tools). =1 keeps the picture the same (at most 3/255 apart, alone
|
||||||
|
// or with FH_SPV_OPT=1); =2 leaves a few specks (up to 3 pixels in a million off by as much as 104/255), so it
|
||||||
|
// stays experimental. On the Frame neither saved anything (GPU cycles within +-1% in 9 homes, 2026-10-25): Meta's shaders already mark
|
||||||
|
// their own colour math relaxed. Relaxing all float math did save 8% in Desert Terrace, but turned Haven's walls
|
||||||
|
// black.
|
||||||
|
// Every mode's output passes spirv-val (all 198 of Haven Central's shipped and specialized shaders).
|
||||||
|
// A module made this way carries a mark (an OpSourceExtension "frame-spvopt <flags>"), so one baked into a .fenv by
|
||||||
|
// Frame Studio with the same switches isn't made again when it loads, and one made with others is made again.
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace spvopt {
|
||||||
|
|
||||||
|
enum : int {
|
||||||
|
kOptimize = 1, // the performance recipe
|
||||||
|
kRelax = 2, // fragment shaders: colour math RelaxedPrecision (relax() in spv_opt.cpp)
|
||||||
|
kHalf = 4, // ... and the relaxed math rewritten as 16-bit floats (with kRelax; experimental)
|
||||||
|
};
|
||||||
|
|
||||||
|
// What's on: kOptimize with FH_SPV_OPT=1, kRelax with FH_SPV_HALF=1, kRelax|kHalf with =2 (kOptimize and kHalf only
|
||||||
|
// in a build with SPIRV-Tools).
|
||||||
|
int flags();
|
||||||
|
|
||||||
|
// The flags for one program's shaders (its path in the home): flags(), less kRelax and kHalf when FH_SPV_HALF_ONLY is
|
||||||
|
// set and the path doesn't contain it (diagnostics: one shader family at a time).
|
||||||
|
int flagsFor(const std::string& programPath);
|
||||||
|
|
||||||
|
// `code` made with `flags` (flags() by default); `code` itself when it already carries the same mark, when there is
|
||||||
|
// nothing to do, or when the optimizer fails (`err` says why). Thread-safe; results are cached by content.
|
||||||
|
std::vector<uint32_t> run(const std::vector<uint32_t>& code, int flags, std::string* err = nullptr);
|
||||||
|
inline std::vector<uint32_t> run(const std::vector<uint32_t>& code) { return run(code, flags()); }
|
||||||
|
inline std::vector<uint32_t> run(const std::vector<uint32_t>& code, const std::string& programPath) { return run(code, flagsFor(programPath)); }
|
||||||
|
|
||||||
|
// The flags a module was made with (its mark), -1 when it carries none.
|
||||||
|
int markOf(const std::vector<uint32_t>& code);
|
||||||
|
|
||||||
|
// Totals since start: modules made, cache hits, failures, and the milliseconds spent making them (all threads).
|
||||||
|
struct Stats { uint32_t made = 0, cached = 0, failed = 0; double ms = 0; };
|
||||||
|
Stats stats();
|
||||||
|
|
||||||
|
} // namespace spvopt
|
||||||
@@ -84,18 +84,28 @@ inline uint32_t scalarBytes(const Module& m, uint32_t t) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An offset moved by `by` bytes, false past kMaxOffset: the strides and indices are the shader's word (an APK's or a
|
||||||
|
// .fenv's), and a wrapped 32-bit offset would pass the block-size checks and read far outside the block.
|
||||||
|
constexpr uint64_t kMaxOffset = uint64_t(1) << 30;
|
||||||
|
inline bool advance(uint32_t& offset, uint64_t by) {
|
||||||
|
if (offset + by > kMaxOffset) return false;
|
||||||
|
offset = static_cast<uint32_t>(offset + by);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// One step down an access chain: (type, offset) of member / element `index`.
|
// One step down an access chain: (type, offset) of member / element `index`.
|
||||||
inline bool step(const Module& m, Ptr& p, uint32_t index) {
|
inline bool step(const Module& m, Ptr& p, uint32_t index) {
|
||||||
const auto it = m.types.find(p.type);
|
const auto it = m.types.find(p.type);
|
||||||
if (it == m.types.end()) return false;
|
if (it == m.types.end()) return false;
|
||||||
const Type& t = it->second;
|
const Type& t = it->second;
|
||||||
|
if (t.a.empty()) return false; // (a type with no operands: a damaged module)
|
||||||
if (t.op == OpTypeStruct) {
|
if (t.op == OpTypeStruct) {
|
||||||
if (index >= t.a.size()) return false;
|
if (index >= t.a.size()) return false;
|
||||||
const auto ms = m.members.find(p.type);
|
const auto ms = m.members.find(p.type);
|
||||||
if (ms == m.members.end()) return false;
|
if (ms == m.members.end()) return false;
|
||||||
const auto mm = ms->second.find(index);
|
const auto mm = ms->second.find(index);
|
||||||
if (mm == ms->second.end() || mm->second.offset == 0xFFFFFFFFu) return false;
|
if (mm == ms->second.end() || mm->second.offset == 0xFFFFFFFFu) return false;
|
||||||
p.offset += mm->second.offset;
|
if (!advance(p.offset, mm->second.offset)) return false;
|
||||||
p.matrixStride = mm->second.matrixStride;
|
p.matrixStride = mm->second.matrixStride;
|
||||||
p.rowMajor = mm->second.rowMajor;
|
p.rowMajor = mm->second.rowMajor;
|
||||||
p.type = t.a[index];
|
p.type = t.a[index];
|
||||||
@@ -104,20 +114,20 @@ inline bool step(const Module& m, Ptr& p, uint32_t index) {
|
|||||||
if (t.op == OpTypeArray) {
|
if (t.op == OpTypeArray) {
|
||||||
const auto st = m.arrayStride.find(p.type);
|
const auto st = m.arrayStride.find(p.type);
|
||||||
if (st == m.arrayStride.end()) return false;
|
if (st == m.arrayStride.end()) return false;
|
||||||
p.offset += index * st->second;
|
if (!advance(p.offset, uint64_t(index) * st->second)) return false;
|
||||||
p.type = t.a[0];
|
p.type = t.a[0];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (t.op == OpTypeMatrix) {
|
if (t.op == OpTypeMatrix) {
|
||||||
if (!p.matrixStride || p.rowMajor) return false;
|
if (!p.matrixStride || p.rowMajor) return false;
|
||||||
p.offset += index * p.matrixStride;
|
if (!advance(p.offset, uint64_t(index) * p.matrixStride)) return false;
|
||||||
p.type = t.a[0];
|
p.type = t.a[0];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (t.op == OpTypeVector) {
|
if (t.op == OpTypeVector) {
|
||||||
const uint32_t b = scalarBytes(m, t.a[0]);
|
const uint32_t b = scalarBytes(m, t.a[0]);
|
||||||
if (!b) return false;
|
if (!b) return false;
|
||||||
p.offset += index * b;
|
if (!advance(p.offset, uint64_t(index) * b)) return false;
|
||||||
p.type = t.a[0];
|
p.type = t.a[0];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -125,25 +135,33 @@ inline bool step(const Module& m, Ptr& p, uint32_t index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The bytes a value of type `type` at `p` spans (0 when it can't be built as a constant).
|
// The bytes a value of type `type` at `p` spans (0 when it can't be built as a constant).
|
||||||
inline uint32_t extent(const Module& m, const Ptr& p) {
|
// (depth: a damaged module's type may contain itself, which would recurse until the stack runs out; budget: arrays of
|
||||||
|
// arrays of ... each 1024 long would visit 1024^depth members. build() walks only what an extent here walked.)
|
||||||
|
constexpr int kMaxDepth = 32;
|
||||||
|
inline uint32_t extent(const Module& m, const Ptr& p, int depth = 0, uint32_t* budget = nullptr) {
|
||||||
|
uint32_t mine = 1u << 16;
|
||||||
|
if (!budget) budget = &mine;
|
||||||
|
if (depth > kMaxDepth || !*budget) return 0;
|
||||||
|
--*budget;
|
||||||
const auto it = m.types.find(p.type);
|
const auto it = m.types.find(p.type);
|
||||||
if (it == m.types.end()) return 0;
|
if (it == m.types.end()) return 0;
|
||||||
const Type& t = it->second;
|
const Type& t = it->second;
|
||||||
if (t.op == OpTypeFloat || t.op == OpTypeInt) return scalarBytes(m, p.type);
|
if (t.op == OpTypeFloat || t.op == OpTypeInt) return scalarBytes(m, p.type);
|
||||||
if (t.op == OpTypeVector) return scalarBytes(m, t.a[0]) * (t.a.size() >= 2 ? t.a[1] : 0);
|
if (t.op == OpTypeVector) return t.a.size() >= 2 && t.a[1] <= 4 ? scalarBytes(m, t.a[0]) * t.a[1] : 0;
|
||||||
uint32_t end = 0, n = 0;
|
uint32_t end = 0, n = 0;
|
||||||
if (t.op == OpTypeMatrix) n = t.a.size() >= 2 ? t.a[1] : 0;
|
if (t.op == OpTypeMatrix) n = t.a.size() >= 2 ? t.a[1] : 0;
|
||||||
else if (t.op == OpTypeArray) { const auto c = m.constU32.find(t.a.size() >= 2 ? t.a[1] : 0); n = c == m.constU32.end() ? 0 : c->second; }
|
else if (t.op == OpTypeArray) { const auto c = m.constU32.find(t.a.size() >= 2 ? t.a[1] : 0); n = c == m.constU32.end() ? 0 : c->second; }
|
||||||
else if (t.op == OpTypeStruct) n = static_cast<uint32_t>(t.a.size());
|
else if (t.op == OpTypeStruct) n = static_cast<uint32_t>(t.a.size());
|
||||||
else return 0;
|
else return 0;
|
||||||
|
if (n > 1024) return 0; // (as build: no constant that large is made, and a huge count hangs)
|
||||||
for (uint32_t k = 0; k < n; ++k) {
|
for (uint32_t k = 0; k < n; ++k) {
|
||||||
Ptr q = p;
|
Ptr q = p;
|
||||||
if (!step(m, q, k)) return 0;
|
if (!step(m, q, k)) return 0;
|
||||||
const uint32_t e = extent(m, q);
|
const uint32_t e = extent(m, q, depth + 1, budget);
|
||||||
if (!e) return 0;
|
if (!e) return 0;
|
||||||
end = std::max(end, q.offset - p.offset + e);
|
end = std::max(end, q.offset - p.offset + e);
|
||||||
}
|
}
|
||||||
return end;
|
return end <= kMaxOffset ? end : 0;
|
||||||
}
|
}
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
@@ -174,7 +192,8 @@ inline std::vector<uint32_t> specialize(const std::vector<uint32_t>& code, const
|
|||||||
uint32_t bound = code[3];
|
uint32_t bound = code[3];
|
||||||
std::vector<uint32_t> consts; // new constants, before the first function
|
std::vector<uint32_t> consts; // new constants, before the first function
|
||||||
std::map<std::pair<uint32_t, std::vector<uint32_t>>, uint32_t> made; // (type, words) -> id
|
std::map<std::pair<uint32_t, std::vector<uint32_t>>, uint32_t> made; // (type, words) -> id
|
||||||
std::function<uint32_t(const Ptr&, const BlockBytes&)> build = [&](const Ptr& p, const BlockBytes& bb) -> uint32_t {
|
std::function<uint32_t(const Ptr&, const BlockBytes&, int)> build = [&](const Ptr& p, const BlockBytes& bb, int depth) -> uint32_t {
|
||||||
|
if (depth > kMaxDepth) return 0;
|
||||||
const auto it = m.types.find(p.type);
|
const auto it = m.types.find(p.type);
|
||||||
if (it == m.types.end()) return 0;
|
if (it == m.types.end()) return 0;
|
||||||
const Type& t = it->second;
|
const Type& t = it->second;
|
||||||
@@ -200,7 +219,7 @@ inline std::vector<uint32_t> specialize(const std::vector<uint32_t>& code, const
|
|||||||
for (uint32_t k = 0; k < n; ++k) {
|
for (uint32_t k = 0; k < n; ++k) {
|
||||||
Ptr q = p;
|
Ptr q = p;
|
||||||
if (!step(m, q, k)) return 0;
|
if (!step(m, q, k)) return 0;
|
||||||
const uint32_t id = build(q, bb);
|
const uint32_t id = build(q, bb, depth + 1);
|
||||||
if (!id) return 0;
|
if (!id) return 0;
|
||||||
parts.push_back(id);
|
parts.push_back(id);
|
||||||
}
|
}
|
||||||
@@ -243,7 +262,7 @@ inline std::vector<uint32_t> specialize(const std::vector<uint32_t>& code, const
|
|||||||
const BlockBytes& bb = blocks[sb];
|
const BlockBytes& bb = blocks[sb];
|
||||||
const uint32_t ext = have[sb] ? extent(m, p) : 0;
|
const uint32_t ext = have[sb] ? extent(m, p) : 0;
|
||||||
if (ext && p.offset + ext <= bb.size && (!bb.foldable || bb.foldable(p.offset, ext))) {
|
if (ext && p.offset + ext <= bb.size && (!bb.foldable || bb.foldable(p.offset, ext))) {
|
||||||
if (const uint32_t id = build(p, bb)) {
|
if (const uint32_t id = build(p, bb, 0)) {
|
||||||
body.insert(body.end(), {(4u << 16) | OpCopyObject, w[1], w[2], id});
|
body.insert(body.end(), {(4u << 16) | OpCopyObject, w[1], w[2], id});
|
||||||
++*folded;
|
++*folded;
|
||||||
i += words;
|
i += words;
|
||||||
|
|||||||
@@ -154,10 +154,17 @@ inline bool inside(const std::vector<float>& tris, const float* p) {
|
|||||||
return n & 1;
|
return n & 1;
|
||||||
}
|
}
|
||||||
// The volume a soup encloses (0 for an open sheet: a flat plate has no inside to bury anything in).
|
// The volume a soup encloses (0 for an open sheet: a flat plate has no inside to bury anything in).
|
||||||
|
// In doubles, about its first corner: far from the world's origin (a home 100 m out) float products of the
|
||||||
|
// coordinates lose the volume to rounding, and a plate off the origin's plane would seem to enclose one.
|
||||||
inline float volume(const std::vector<float>& t) {
|
inline float volume(const std::vector<float>& t) {
|
||||||
|
if (t.size() < 9) return 0;
|
||||||
double v = 0;
|
double v = 0;
|
||||||
for (size_t i = 0; i + 8 < t.size(); i += 9)
|
const double o[3] = {t[0], t[1], t[2]};
|
||||||
v += t[i] * (t[i + 4] * t[i + 8] - t[i + 5] * t[i + 7]) - t[i + 1] * (t[i + 3] * t[i + 8] - t[i + 5] * t[i + 6]) + t[i + 2] * (t[i + 3] * t[i + 7] - t[i + 4] * t[i + 6]);
|
for (size_t i = 0; i + 8 < t.size(); i += 9) {
|
||||||
|
double q[9];
|
||||||
|
for (int k = 0; k < 9; ++k) q[k] = t[i + k] - o[k % 3];
|
||||||
|
v += q[0] * (q[4] * q[8] - q[5] * q[7]) - q[1] * (q[3] * q[8] - q[5] * q[6]) + q[2] * (q[3] * q[7] - q[4] * q[6]);
|
||||||
|
}
|
||||||
return static_cast<float>(std::fabs(v) / 6);
|
return static_cast<float>(std::fabs(v) / 6);
|
||||||
}
|
}
|
||||||
inline void bounds(const std::vector<float>& tris, float* lo, float* hi) {
|
inline void bounds(const std::vector<float>& tris, float* lo, float* hi) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include "overlays.h"
|
#include "overlays.h"
|
||||||
#include "perf_settings.h"
|
#include "perf_settings.h"
|
||||||
#include "home_platform.h"
|
#include "home_platform.h"
|
||||||
|
#include "dashboard_icon.h"
|
||||||
|
|
||||||
#include <openvr.h>
|
#include <openvr.h>
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <future>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <set>
|
#include <set>
|
||||||
@@ -177,7 +179,9 @@ std::vector<Env> scanEnvs() {
|
|||||||
std::vector<uint8_t> thumb;
|
std::vector<uint8_t> thumb;
|
||||||
if (!fenvReadManifest(p.string(), m, &thumb)) continue;
|
if (!fenvReadManifest(p.string(), m, &thumb)) continue;
|
||||||
e.name = m.name.empty() ? pretty(p.stem().string()) : m.name;
|
e.name = m.name.empty() ? pretty(p.stem().string()) : m.name;
|
||||||
e.kind = m.sources.size() > 1 ? "Frame environment, footprint + vista" : "Frame environment";
|
e.kind = std::string(m.sources.size() > 1 ? "Frame environment, footprint + vista" : "Frame environment") + " \u00b7 " +
|
||||||
|
(m.studioVersion.empty() ? "made before v0.2" : "made with Studio " + m.studioVersion);
|
||||||
|
if (!fenvMissingFeatures(m).empty()) e.kind = "Needs a newer Frame Home: update it from this panel"; // (it won't load)
|
||||||
e.thumb = decodeBytes(thumb);
|
e.thumb = decodeBytes(thumb);
|
||||||
} else {
|
} else {
|
||||||
e.name = pretty(p.stem().string());
|
e.name = pretty(p.stem().string());
|
||||||
@@ -205,7 +209,8 @@ bool setCurrent(const Env& e) {
|
|||||||
{
|
{
|
||||||
std::ofstream f(tmp);
|
std::ofstream f(tmp);
|
||||||
for (const auto& n : e.files) f << n << "\n";
|
for (const auto& n : e.files) f << n << "\n";
|
||||||
if (!f) return false;
|
f.close(); // (a full disk shows at the flush: never renamed in half written)
|
||||||
|
if (!f) { std::error_code ec; fs::remove(tmp, ec); return false; }
|
||||||
}
|
}
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
fs::rename(tmp, rootDir() / "current", ec); // atomic: Frame Home never reads half a file
|
fs::rename(tmp, rootDir() / "current", ec); // atomic: Frame Home never reads half a file
|
||||||
@@ -233,7 +238,8 @@ bool writeCurrent(const std::vector<std::string>& files) {
|
|||||||
{
|
{
|
||||||
std::ofstream f(tmp);
|
std::ofstream f(tmp);
|
||||||
for (const auto& n : files) f << n << "\n";
|
for (const auto& n : files) f << n << "\n";
|
||||||
if (!f) return false;
|
f.close(); // (as setCurrent: checked after the flush)
|
||||||
|
if (!f) { std::error_code ec; fs::remove(tmp, ec); return false; }
|
||||||
}
|
}
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
fs::rename(tmp, rootDir() / "current", ec);
|
fs::rename(tmp, rootDir() / "current", ec);
|
||||||
@@ -269,7 +275,8 @@ int removeCacheEntries(const CacheDoomed& c) {
|
|||||||
}
|
}
|
||||||
for (const fs::path& d : doomed) if (fs::remove_all(d, ec) > 0 && !ec) ++n;
|
for (const fs::path& d : doomed) if (fs::remove_all(d, ec) > 0 && !ec) ++n;
|
||||||
}
|
}
|
||||||
fs::remove(c.root / (c.key + ".lock"), ec);
|
// (the .lock file stays: removed, a waiter would hold a lock on the gone file while the next opener made a new
|
||||||
|
// one, and both would think they had it to themselves)
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,8 +321,12 @@ DeleteResult deleteEnv(const Env& e, const std::vector<Env>& all, bool cacheInBa
|
|||||||
// 2. The files (cache names first: they hash the path of the file as it exists).
|
// 2. The files (cache names first: they hash the path of the file as it exists).
|
||||||
std::vector<CacheDoomed> caches;
|
std::vector<CacheDoomed> caches;
|
||||||
for (const auto& f : e.files) if (fs::path(f).extension() == ".fenv") caches.push_back(cacheEntriesOf(envs / f));
|
for (const auto& f : e.files) if (fs::path(f).extension() == ".fenv") caches.push_back(cacheEntriesOf(envs / f));
|
||||||
|
// One that won't go doesn't stop the rest: half a pair left behind would still be named in pairs.txt.
|
||||||
for (const auto& f : e.files) {
|
for (const auto& f : e.files) {
|
||||||
if (!fs::remove(envs / f, ec) || ec) { r.error = "couldn't delete " + f + (ec ? ": " + ec.message() : ""); return r; }
|
if (!fs::remove(envs / f, ec) || ec) {
|
||||||
|
if (r.error.empty()) r.error = "couldn't delete " + f + (ec ? ": " + ec.message() : "");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
r.removed.push_back("envs/" + f);
|
r.removed.push_back("envs/" + f);
|
||||||
for (const char* comp : {".world.json", ".edits.json"})
|
for (const char* comp : {".world.json", ".edits.json"})
|
||||||
if (fs::remove(envs / (f + comp), ec)) r.removed.push_back("envs/" + f + comp);
|
if (fs::remove(envs / (f + comp), ec)) r.removed.push_back("envs/" + f + comp);
|
||||||
@@ -358,8 +369,8 @@ DeleteResult deleteEnv(const Env& e, const std::vector<Env>& all, bool cacheInBa
|
|||||||
if (cacheInBackground) std::thread([caches] { for (const auto& c : caches) removeCacheEntries(c); }).detach();
|
if (cacheInBackground) std::thread([caches] { for (const auto& c : caches) removeCacheEntries(c); }).detach();
|
||||||
else for (const auto& c : caches) for (int i = removeCacheEntries(c); i > 0; --i) r.removed.push_back("cache/" + c.key + "-*");
|
else for (const auto& c : caches) for (int i = removeCacheEntries(c); i > 0; --i) r.removed.push_back("cache/" + c.key + "-*");
|
||||||
}
|
}
|
||||||
LOG("deleted %s (%zu files)", e.name.c_str(), r.removed.size());
|
LOG("deleted %s (%zu files)%s", e.name.c_str(), r.removed.size(), r.error.empty() ? "" : (": " + r.error).c_str());
|
||||||
r.ok = true;
|
r.ok = r.error.empty();
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,6 +736,10 @@ std::string releaseUrl() {
|
|||||||
return u && *u ? std::string(u) : std::string(kReleaseUrl);
|
return u && *u ? std::string(u) : std::string(kReleaseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef FRAME_VERSION_BASE
|
||||||
|
#define FRAME_VERSION_BASE "?" // (CMakeLists.txt sets it from VERSION)
|
||||||
|
#endif
|
||||||
|
|
||||||
struct Updates {
|
struct Updates {
|
||||||
enum class State { Idle, Available, Updating, Failed };
|
enum class State { Idle, Available, Updating, Failed };
|
||||||
std::mutex m;
|
std::mutex m;
|
||||||
@@ -757,6 +772,10 @@ struct Updates {
|
|||||||
// What's installed, hashed as CI hashes frame/payload.
|
// What's installed, hashed as CI hashes frame/payload.
|
||||||
static std::string installedHash() {
|
static std::string installedHash() {
|
||||||
int rc = 0;
|
int rc = 0;
|
||||||
|
// (the pipeline's exit code is the last cut's: a file sha256sum couldn't read would still give a hash)
|
||||||
|
std::error_code ec;
|
||||||
|
for (const char* f : {"bin/frame-home", "bin/frame-switcher", "bin/fenv-tool", "layer/libframe_home_layer.so", "layer/env_ps.spv"})
|
||||||
|
if (!fs::is_regular_file(rootDir() / f, ec)) return {};
|
||||||
std::string h = sh("cd " + quoted(rootDir()) + " && sha256sum bin/frame-home bin/frame-switcher bin/fenv-tool "
|
std::string h = sh("cd " + quoted(rootDir()) + " && sha256sum bin/frame-home bin/frame-switcher bin/fenv-tool "
|
||||||
"layer/libframe_home_layer.so layer/env_ps.spv | cut -c1-64 | sha256sum | cut -c1-64", rc);
|
"layer/libframe_home_layer.so layer/env_ps.spv | cut -c1-64 | sha256sum | cut -c1-64", rc);
|
||||||
while (!h.empty() && (h.back() == '\n' || h.back() == '\r')) h.pop_back();
|
while (!h.empty() && (h.back() == '\n' || h.back() == '\r')) h.pop_back();
|
||||||
@@ -795,18 +814,41 @@ struct Updates {
|
|||||||
fs::remove_all(dir, ec);
|
fs::remove_all(dir, ec);
|
||||||
fs::create_directories(dir, ec);
|
fs::create_directories(dir, ec);
|
||||||
fs::create_directories(log.parent_path(), ec);
|
fs::create_directories(log.parent_path(), ec);
|
||||||
{ std::ofstream(dir / "started") << "1\n"; } // the new panel says it's updated
|
|
||||||
LOG("updating Frame Home from %s", releaseUrl().c_str());
|
LOG("updating Frame Home from %s", releaseUrl().c_str());
|
||||||
int rc = 0;
|
int rc = 0;
|
||||||
sh("curl -fsSL --retry 2 --max-time 1800 -o " + quoted(dir / "FrameHome-installer.zip") + " '" + releaseUrl() +
|
// The installer's SHA-256 as the release lists it (latest.json "sha256-FrameHome-installer.zip", as Frame
|
||||||
"FrameHome-installer.zip' > " + quoted(log) + " 2>&1", rc);
|
// Studio checks its own downloads): a download cut short, damaged, or from a release other than the one
|
||||||
|
// latest.json describes is never installed.
|
||||||
|
std::string want;
|
||||||
|
try {
|
||||||
|
const tinyjson::Value v = tinyjson::parse(sh("curl -fsSL --max-time 30 '" + releaseUrl() + "latest.json'", rc));
|
||||||
|
if (rc == 0 && v.isObject()) want = v["sha256-FrameHome-installer.zip"].asString();
|
||||||
|
} catch (...) {}
|
||||||
if (rc != 0) return "The update didn't download. Check the headset's Wi-Fi and try again.";
|
if (rc != 0) return "The update didn't download. Check the headset's Wi-Fi and try again.";
|
||||||
sh("unzip -q -o " + quoted(dir / "FrameHome-installer.zip") + " -d " + quoted(dir) + " >> " + quoted(log) + " 2>&1", rc);
|
// (a release that doesn't list it can't be checked: not installed)
|
||||||
|
if (want.size() != 64) return "The update can't be checked (the release lists no SHA-256 for it). Try again later.";
|
||||||
|
const fs::path zip = dir / "FrameHome-installer.zip";
|
||||||
|
sh("curl -fsSL --retry 2 --max-time 1800 -o " + quoted(zip) + " '" + releaseUrl() + "FrameHome-installer.zip' > " + quoted(log) + " 2>&1", rc);
|
||||||
|
if (rc != 0) return "The update didn't download. Check the headset's Wi-Fi and try again.";
|
||||||
|
{
|
||||||
|
std::string got = sh("sha256sum " + quoted(zip) + " | cut -c1-64", rc);
|
||||||
|
while (!got.empty() && (got.back() == '\n' || got.back() == '\r')) got.pop_back();
|
||||||
|
if (rc != 0 || got != want) {
|
||||||
|
LOG("update: the installer's SHA-256 is %s, the release says %s", got.c_str(), want.c_str());
|
||||||
|
fs::remove_all(dir, ec);
|
||||||
|
return "The update download was damaged or changed while downloading. Try again in a few minutes.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sh("unzip -q -o " + quoted(zip) + " -d " + quoted(dir) + " >> " + quoted(log) + " 2>&1", rc);
|
||||||
|
fs::remove(zip, ec); // (unpacked: not kept on the headset's disk)
|
||||||
const fs::path script = dir / "FrameHome-installer" / "payload" / "install.sh";
|
const fs::path script = dir / "FrameHome-installer" / "payload" / "install.sh";
|
||||||
if (rc != 0 || !fs::exists(script, ec)) return "The update couldn't be unpacked.";
|
if (rc != 0 || !fs::exists(script, ec)) return "The update couldn't be unpacked.";
|
||||||
|
// The new panel says it's updated: marked only now, so a download or unpack that failed never claims it.
|
||||||
|
{ std::ofstream(dir / "started") << "1\n"; }
|
||||||
// Its own session: install.sh stops this panel, and must carry on without it.
|
// Its own session: install.sh stops this panel, and must carry on without it.
|
||||||
sh("setsid sh " + quoted(script) + " >> " + quoted(log) + " 2>&1", rc);
|
sh("setsid sh " + quoted(script) + " >> " + quoted(log) + " 2>&1", rc);
|
||||||
if (rc == 0) { fs::remove(dir / "started", ec); return {}; }
|
fs::remove(dir / "started", ec); // still running: install.sh failed, or SteamVR isn't up
|
||||||
|
if (rc == 0) return {};
|
||||||
std::string tail = sh("tail -n 1 " + quoted(log), rc);
|
std::string tail = sh("tail -n 1 " + quoted(log), rc);
|
||||||
tail = tail.substr(0, tail.find('\n'));
|
tail = tail.substr(0, tail.find('\n'));
|
||||||
return "The update failed: " + (tail.empty() ? std::string("see logs/update.log") : tail);
|
return "The update failed: " + (tail.empty() ? std::string("see logs/update.log") : tail);
|
||||||
@@ -833,8 +875,9 @@ struct Updates {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The header's Update Frame Home.
|
// The header's Update Frame Home.
|
||||||
void install() {
|
// False when it can't start now (a check still running, or nothing to install).
|
||||||
if (!kSelfUpdate || busy || get() != State::Available) return;
|
bool install() {
|
||||||
|
if (!kSelfUpdate || busy || get() != State::Available) return false;
|
||||||
busy = true;
|
busy = true;
|
||||||
set(State::Updating);
|
set(State::Updating);
|
||||||
std::thread([this] {
|
std::thread([this] {
|
||||||
@@ -843,6 +886,7 @@ struct Updates {
|
|||||||
else set(State::Failed, err);
|
else set(State::Failed, err);
|
||||||
busy = false;
|
busy = false;
|
||||||
}).detach();
|
}).detach();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Updates g_updates;
|
Updates g_updates;
|
||||||
@@ -858,6 +902,9 @@ struct Panel {
|
|||||||
int page = 0; // 0 environments, 1 settings, 2 debug
|
int page = 0; // 0 environments, 1 settings, 2 debug
|
||||||
PerfSettings perf;
|
PerfSettings perf;
|
||||||
std::string stats; // a line from Frame Home's stats.json
|
std::string stats; // a line from Frame Home's stats.json
|
||||||
|
std::future<std::string> buildHash; // the installed files' hash (Updates::installedHash), for the Settings page's
|
||||||
|
std::string buildId; // last line "Frame Home v0.2 · build 1a2b3c4"; empty: not known
|
||||||
|
bool buildHashDone = false;
|
||||||
DebugSettings dbg; // debug.json: the Debug page's views
|
DebugSettings dbg; // debug.json: the Debug page's views
|
||||||
DebugInfo info; // ...and what it shows of stats.json and report.json
|
DebugInfo info; // ...and what it shows of stats.json and report.json
|
||||||
static constexpr int kShotId = 9200, kDebugOffId = 9201;
|
static constexpr int kShotId = 9200, kDebugOffId = 9201;
|
||||||
@@ -988,7 +1035,7 @@ struct Panel {
|
|||||||
bool tick(double dt) {
|
bool tick(double dt) {
|
||||||
bool moving = false;
|
bool moving = false;
|
||||||
const float fdt = static_cast<float>(std::min(dt, 0.1));
|
const float fdt = static_cast<float>(std::min(dt, 0.1));
|
||||||
for (int p = 0; p < 2; ++p) {
|
for (int p = 0; p < 3; ++p) {
|
||||||
Scroll& s = sc[p];
|
Scroll& s = sc[p];
|
||||||
const float m = maxScroll(p);
|
const float m = maxScroll(p);
|
||||||
if (!(dragging && p == page)) {
|
if (!(dragging && p == page)) {
|
||||||
@@ -1016,6 +1063,7 @@ struct Panel {
|
|||||||
}
|
}
|
||||||
if (!switchingKey.empty()) { moving = true; if (now >= switchUntil) switchingKey.clear(); }
|
if (!switchingKey.empty()) { moving = true; if (now >= switchUntil) switchingKey.clear(); }
|
||||||
if (!toast.empty()) { moving = true; if (now >= toastUntil) toast.clear(); }
|
if (!toast.empty()) { moving = true; if (now >= toastUntil) toast.clear(); }
|
||||||
|
if (buildHash.valid()) moving = true; // the build's hash is being worked out: draw it when it's in
|
||||||
if (!confirmKey.empty() && now - confirmStart < 0.3) moving = true; // the question fades in
|
if (!confirmKey.empty() && now - confirmStart < 0.3) moving = true; // the question fades in
|
||||||
return moving;
|
return moving;
|
||||||
}
|
}
|
||||||
@@ -1201,6 +1249,14 @@ struct Panel {
|
|||||||
y += 20;
|
y += 20;
|
||||||
const std::vector<std::string> note = small.wrap("Anti-aliasing, texture filtering and turning foveation on or off restart Frame Home (a few seconds).", W - 2 * kPad - 8);
|
const std::vector<std::string> note = small.wrap("Anti-aliasing, texture filtering and turning foveation on or off restart Frame Home (a few seconds).", W - 2 * kPad - 8);
|
||||||
for (const auto& l : note) { small.draw(cv, kPad + 4, kTop + y - sc[1].pos, l, kFaint); y += 26; }
|
for (const auto& l : note) { small.draw(cv, kPad + 4, kTop + y - sc[1].pos, l, kFaint); y += 26; }
|
||||||
|
// At the bottom: which Frame Home this is (the build: the installed files' hash, as updates compare them).
|
||||||
|
// (the hash reads every program: worked out once, off the panel's thread; the version alone until then)
|
||||||
|
if (!buildHash.valid() && !buildHashDone) buildHash = std::async(std::launch::async, [] { return Updates::installedHash(); });
|
||||||
|
if (buildHash.valid() && buildHash.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { buildId = buildHash.get(); buildHashDone = true; }
|
||||||
|
const std::string version = std::string("Frame Home v") + FRAME_VERSION_BASE + (buildId.size() >= 7 ? " · build " + buildId.substr(0, 7) : "");
|
||||||
|
y += 24;
|
||||||
|
small.draw(cv, kPad + 4, kTop + y - sc[1].pos, version, kFaint);
|
||||||
|
y += 26;
|
||||||
sc[1].content = y + 40;
|
sc[1].content = y + 40;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1508,7 +1564,8 @@ struct Panel {
|
|||||||
void removeEnv(size_t i) {
|
void removeEnv(size_t i) {
|
||||||
const Env e = envs[i];
|
const Env e = envs[i];
|
||||||
const DeleteResult r = deleteEnv(e, envs, true);
|
const DeleteResult r = deleteEnv(e, envs, true);
|
||||||
if (!r.ok) { say("Couldn't delete " + e.name + (r.error.empty() ? "" : " (" + r.error + ")"), true, 4); return; }
|
confirmKey.clear(); // (or the same name sent again later would come back asking "Delete?")
|
||||||
|
if (!r.ok && r.removed.empty()) { say("Couldn't delete " + e.name + (r.error.empty() ? "" : " (" + r.error + ")"), true, 4); return; }
|
||||||
envs = scanEnvs();
|
envs = scanEnvs();
|
||||||
current = currentKey();
|
current = currentKey();
|
||||||
hoverT.assign(envs.size(), 0.0f);
|
hoverT.assign(envs.size(), 0.0f);
|
||||||
@@ -1516,6 +1573,7 @@ struct Panel {
|
|||||||
sc[0].content = envContent();
|
sc[0].content = envContent();
|
||||||
sc[0].target = std::clamp(sc[0].target, 0.0f, maxScroll(0));
|
sc[0].target = std::clamp(sc[0].target, 0.0f, maxScroll(0));
|
||||||
hotHover = -1;
|
hotHover = -1;
|
||||||
|
if (!r.ok) { say("Partly deleted " + e.name + " (" + r.error + ")", true, 4); return; }
|
||||||
say("Deleted " + e.name + (r.currentChanged && !r.newCurrent.empty() ? " · now showing " + r.newCurrent : ""), false, 3);
|
say("Deleted " + e.name + (r.currentChanged && !r.newCurrent.empty() ? " · now showing " + r.newCurrent : ""), false, 3);
|
||||||
}
|
}
|
||||||
void click(int id) {
|
void click(int id) {
|
||||||
@@ -1531,8 +1589,8 @@ struct Panel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (id == kUpdateId) {
|
if (id == kUpdateId) {
|
||||||
g_updates.install();
|
if (g_updates.install()) say("Updating Frame Home: it restarts in a moment", false, 6);
|
||||||
say("Updating Frame Home: it restarts in a moment", false, 6);
|
else say("Still checking for updates: try again in a moment", false, 3);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (id >= 9000 && id < 9003) {
|
if (id >= 9000 && id < 9003) {
|
||||||
@@ -1682,14 +1740,9 @@ int runPanel() {
|
|||||||
vr::VROverlay()->SetOverlayInputMethod(ov, vr::VROverlayInputMethod_Mouse);
|
vr::VROverlay()->SetOverlayInputMethod(ov, vr::VROverlayInputMethod_Mouse);
|
||||||
const vr::HmdVector2_t mouseScale = {{static_cast<float>(W), static_cast<float>(H)}};
|
const vr::HmdVector2_t mouseScale = {{static_cast<float>(W), static_cast<float>(H)}};
|
||||||
vr::VROverlay()->SetOverlayMouseScale(ov, &mouseScale);
|
vr::VROverlay()->SetOverlayMouseScale(ov, &mouseScale);
|
||||||
{ // dashboard tab icon: a small orange tile
|
{ // dashboard tab icon: the app icon's roof and headset (dashboard_icon.h); SetOverlayRaw wants it writable
|
||||||
std::vector<uint8_t> ic(64 * 64 * 4);
|
std::vector<uint8_t> ic(kDashIcon, kDashIcon + sizeof kDashIcon);
|
||||||
for (int y = 0; y < 64; ++y) for (int x = 0; x < 64; ++x) {
|
vr::VROverlay()->SetOverlayRaw(icon, ic.data(), kDashIconSize, kDashIconSize, 4);
|
||||||
uint8_t* p = &ic[(y * 64 + x) * 4];
|
|
||||||
const bool on = (x > 10 && x < 54 && y > 14 && y < 50) && !(x > 16 && x < 48 && y > 20 && y < 44 && (y - 20) * 32 > (x - 16) * 24);
|
|
||||||
p[0] = on ? 240 : 0; p[1] = on ? 148 : 0; p[2] = on ? 58 : 0; p[3] = on ? 255 : 0;
|
|
||||||
}
|
|
||||||
vr::VROverlay()->SetOverlayRaw(icon, ic.data(), 64, 64, 4);
|
|
||||||
}
|
}
|
||||||
Panel panel;
|
Panel panel;
|
||||||
panel.loadFonts();
|
panel.loadFonts();
|
||||||
@@ -1800,11 +1853,18 @@ int runPanel() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const std::string bin = (rootDir() / "bin" / "frame-home").string();
|
const std::string bin = (rootDir() / "bin" / "frame-home").string();
|
||||||
|
// Its environment made here, before fork: between fork and exec only async-signal-safe calls (setenv
|
||||||
|
// allocates, and another thread may hold the allocator's lock).
|
||||||
|
std::vector<std::string> env = {"STEAMVR_APPKEY=framehome.environment"}; // its own app to SteamVR, not ours (our input is legacy)
|
||||||
|
for (char** e = environ; e && *e; ++e) if (std::strncmp(*e, "STEAMVR_APPKEY=", 15) != 0) env.push_back(*e);
|
||||||
|
std::vector<char*> envp;
|
||||||
|
for (std::string& e : env) envp.push_back(e.data());
|
||||||
|
envp.push_back(nullptr);
|
||||||
|
char* const argv[] = {const_cast<char*>(bin.c_str()), const_cast<char*>("--system"), nullptr};
|
||||||
const pid_t pid = fork();
|
const pid_t pid = fork();
|
||||||
if (pid == 0) {
|
if (pid == 0) {
|
||||||
setsid();
|
setsid();
|
||||||
setenv("STEAMVR_APPKEY", "framehome.environment", 1); // its own app to SteamVR, not ours (our input is legacy)
|
execve(bin.c_str(), argv, envp.data());
|
||||||
execl(bin.c_str(), bin.c_str(), "--system", static_cast<char*>(nullptr));
|
|
||||||
_exit(127);
|
_exit(127);
|
||||||
}
|
}
|
||||||
if (pid > 0) { systemPid = pid; LOG("started frame-home --system (pid %d)", static_cast<int>(pid)); }
|
if (pid > 0) { systemPid = pid; LOG("started frame-home --system (pid %d)", static_cast<int>(pid)); }
|
||||||
@@ -1872,6 +1932,9 @@ int runPanel() {
|
|||||||
fs::remove(rootDir() / "layer" / "off", ec);
|
fs::remove(rootDir() / "layer" / "off", ec);
|
||||||
fs::remove(rootDir() / "layer" / "crashes", ec);
|
fs::remove(rootDir() / "layer" / "crashes", ec);
|
||||||
LOG("Frame Home turned back on from the panel");
|
LOG("Frame Home turned back on from the panel");
|
||||||
|
// The layer makes its pipelines as the compositor starts; one that started off has none to draw
|
||||||
|
// with until SteamVR (the compositor) starts again.
|
||||||
|
panel.say("Turned on: it shows after SteamVR restarts", false, 6);
|
||||||
}
|
}
|
||||||
if (systemPid > 0 && homeos::alive(systemPid)) {
|
if (systemPid > 0 && homeos::alive(systemPid)) {
|
||||||
LOG("restarting frame-home --system (pid %d) from the panel", static_cast<int>(systemPid));
|
LOG("restarting frame-home --system (pid %d) from the panel", static_cast<int>(systemPid));
|
||||||
@@ -2020,7 +2083,8 @@ int runPanel() {
|
|||||||
const auto now = std::chrono::steady_clock::now();
|
const auto now = std::chrono::steady_clock::now();
|
||||||
if (now - lastPoll > std::chrono::seconds(2)) {
|
if (now - lastPoll > std::chrono::seconds(2)) {
|
||||||
lastPoll = now;
|
lastPoll = now;
|
||||||
if (rescan()) { dirty = true; panel.sc[0].content = panel.envContent(); }
|
// (a new list: the cards' click targets from the last drawing belong to the old one until it's redrawn)
|
||||||
|
if (rescan()) { dirty = true; panel.sc[0].content = panel.envContent(); panel.hots.clear(); panel.hotHover = panel.pressHot = -1; }
|
||||||
PerfSettings ps;
|
PerfSettings ps;
|
||||||
if (ps.fromJson(readText(rootDir() / "settings.json")) && !(ps == panel.perf)) { panel.perf = ps; if (panel.page == 1) dirty = true; }
|
if (ps.fromJson(readText(rootDir() / "settings.json")) && !(ps == panel.perf)) { panel.perf = ps; if (panel.page == 1) dirty = true; }
|
||||||
const std::string line = statsLine();
|
const std::string line = statsLine();
|
||||||
@@ -2130,6 +2194,11 @@ bool frameHomeShowing() {
|
|||||||
int runThumbs() {
|
int runThumbs() {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
fs::create_directories(homeos::thumbs(), ec);
|
fs::create_directories(homeos::thumbs(), ec);
|
||||||
|
#ifndef _WIN32
|
||||||
|
// One at a time (the panel and install.sh both start it): a second one finds the lock held and leaves it be.
|
||||||
|
const int lockFd = ::open((homeos::thumbs() / ".lock").c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600);
|
||||||
|
if (lockFd >= 0 && flock(lockFd, LOCK_EX | LOCK_NB) != 0) { LOG("thumbnails: another --thumbs is making them"); return 0; }
|
||||||
|
#endif
|
||||||
const std::string bin = (rootDir() / "bin" / homeos::exeName("frame-home")).string();
|
const std::string bin = (rootDir() / "bin" / homeos::exeName("frame-home")).string();
|
||||||
int made = 0;
|
int made = 0;
|
||||||
for (const Env& e : scanEnvs()) {
|
for (const Env& e : scanEnvs()) {
|
||||||
|
|||||||
@@ -7,8 +7,10 @@
|
|||||||
#include "tex_mips.h"
|
#include "tex_mips.h"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
@@ -28,10 +30,13 @@
|
|||||||
namespace texcache {
|
namespace texcache {
|
||||||
|
|
||||||
// FRAME_TEX_CACHE, else %LOCALAPPDATA%\FrameStudio\cache\texmips (Windows) or ~/.cache/frame-home/texmips.
|
// FRAME_TEX_CACHE, else %LOCALAPPDATA%\FrameStudio\cache\texmips (Windows) or ~/.cache/frame-home/texmips.
|
||||||
|
// (Windows: read wide; the narrow getenv is the ANSI code page, which u8path throws on for a user name like "José")
|
||||||
inline std::filesystem::path dir() {
|
inline std::filesystem::path dir() {
|
||||||
if (const char* e = std::getenv("FRAME_TEX_CACHE")) return std::filesystem::u8path(e);
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
if (const char* l = std::getenv("LOCALAPPDATA")) return std::filesystem::u8path(std::string(l) + "\\FrameStudio\\cache\\texmips");
|
if (const wchar_t* e = _wgetenv(L"FRAME_TEX_CACHE")) return std::filesystem::path(e);
|
||||||
|
if (const wchar_t* l = _wgetenv(L"LOCALAPPDATA")) return std::filesystem::path(l) / L"FrameStudio" / L"cache" / L"texmips";
|
||||||
|
#else
|
||||||
|
if (const char* e = std::getenv("FRAME_TEX_CACHE")) return std::filesystem::u8path(e);
|
||||||
#endif
|
#endif
|
||||||
if (const char* x = std::getenv("XDG_CACHE_HOME")) return std::filesystem::u8path(std::string(x) + "/frame-home/texmips");
|
if (const char* x = std::getenv("XDG_CACHE_HOME")) return std::filesystem::u8path(std::string(x) + "/frame-home/texmips");
|
||||||
const char* h = std::getenv("HOME");
|
const char* h = std::getenv("HOME");
|
||||||
@@ -58,16 +63,26 @@ inline std::filesystem::path pathFor(uint64_t k) {
|
|||||||
|
|
||||||
struct Header { char magic[4]; uint32_t version, w, h, bw, bh, mips; uint64_t bytes; };
|
struct Header { char magic[4]; uint32_t version, w, h, bw, bh, mips; uint64_t bytes; };
|
||||||
|
|
||||||
|
// (Windows: by its wide name, so a cache under a non-ASCII user folder opens)
|
||||||
|
inline FILE* open(const std::filesystem::path& p, bool write) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
return _wfopen(p.c_str(), write ? L"wb" : L"rb");
|
||||||
|
#else
|
||||||
|
return std::fopen(p.c_str(), write ? "wb" : "rb");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// The cached chain for `t`, or null.
|
// The cached chain for `t`, or null.
|
||||||
inline std::shared_ptr<TexSource> load(const TexSource& t) {
|
inline std::shared_ptr<TexSource> load(const TexSource& t) {
|
||||||
if (!texmips::wants(t)) return nullptr;
|
if (!texmips::wants(t)) return nullptr;
|
||||||
const std::filesystem::path p = pathFor(key(t));
|
const std::filesystem::path p = pathFor(key(t));
|
||||||
FILE* f = std::fopen(p.string().c_str(), "rb");
|
FILE* f = open(p, false);
|
||||||
if (!f) return nullptr;
|
if (!f) return nullptr;
|
||||||
Header hd = {};
|
Header hd = {};
|
||||||
std::shared_ptr<TexSource> out;
|
std::shared_ptr<TexSource> out;
|
||||||
if (std::fread(&hd, sizeof hd, 1, f) == 1 && std::memcmp(hd.magic, "FTXM", 4) == 0 && hd.version == 1 && hd.w == t.w && hd.h == t.h &&
|
if (std::fread(&hd, sizeof hd, 1, f) == 1 && std::memcmp(hd.magic, "FTXM", 4) == 0 && hd.version == 1 && hd.w == t.w && hd.h == t.h &&
|
||||||
hd.bw == t.bw && hd.bh == t.bh && hd.mips == astcFullMipCount(t.w, t.h) && hd.bytes < (1ull << 31)) {
|
hd.bw == t.bw && hd.bh == t.bh && hd.mips == astcFullMipCount(t.w, t.h) &&
|
||||||
|
hd.bytes == astcMipBytesForCount(t.w, t.h, t.bw, t.bh, hd.mips)) { // (exactly the chain: a damaged entry isn't used)
|
||||||
auto s = std::make_shared<TexSource>();
|
auto s = std::make_shared<TexSource>();
|
||||||
s->w = t.w; s->h = t.h; s->bw = t.bw; s->bh = t.bh; s->mips = hd.mips; s->hdr = t.hdr; s->faces = 1;
|
s->w = t.w; s->h = t.h; s->bw = t.bw; s->bh = t.bh; s->mips = hd.mips; s->hdr = t.hdr; s->faces = 1;
|
||||||
s->astc.resize(static_cast<size_t>(hd.bytes));
|
s->astc.resize(static_cast<size_t>(hd.bytes));
|
||||||
@@ -85,7 +100,12 @@ inline void prune(uint64_t limit) {
|
|||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::vector<std::pair<std::filesystem::file_time_type, std::filesystem::path>> files;
|
std::vector<std::pair<std::filesystem::file_time_type, std::filesystem::path>> files;
|
||||||
uint64_t total = 0;
|
uint64_t total = 0;
|
||||||
|
const auto stale = std::filesystem::file_time_type::clock::now() - std::chrono::hours(1);
|
||||||
for (const auto& e : std::filesystem::directory_iterator(dir(), ec)) {
|
for (const auto& e : std::filesystem::directory_iterator(dir(), ec)) {
|
||||||
|
if (e.path().extension() == ".part") { // a chain an exit left half written (an hour old: nobody's writing it)
|
||||||
|
if (e.last_write_time(ec) < stale) std::filesystem::remove(e.path(), ec);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (e.path().extension() != ".ftxm") continue;
|
if (e.path().extension() != ".ftxm") continue;
|
||||||
const uint64_t s = e.file_size(ec);
|
const uint64_t s = e.file_size(ec);
|
||||||
total += s;
|
total += s;
|
||||||
@@ -103,8 +123,10 @@ inline void prune(uint64_t limit) {
|
|||||||
inline bool store(const TexSource& chain, uint64_t k) {
|
inline bool store(const TexSource& chain, uint64_t k) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::filesystem::create_directories(dir(), ec);
|
std::filesystem::create_directories(dir(), ec);
|
||||||
const std::filesystem::path p = pathFor(k), tmp = p.string() + ".part";
|
std::filesystem::path tmp = pathFor(k);
|
||||||
FILE* f = std::fopen(tmp.string().c_str(), "wb");
|
const std::filesystem::path p = tmp;
|
||||||
|
tmp += ".part";
|
||||||
|
FILE* f = open(tmp, true);
|
||||||
if (!f) return false;
|
if (!f) return false;
|
||||||
Header hd = {{'F', 'T', 'X', 'M'}, 1, chain.w, chain.h, chain.bw, chain.bh, chain.mips, chain.astc.size()};
|
Header hd = {{'F', 'T', 'X', 'M'}, 1, chain.w, chain.h, chain.bw, chain.bh, chain.mips, chain.astc.size()};
|
||||||
const bool ok = std::fwrite(&hd, sizeof hd, 1, f) == 1 && std::fwrite(chain.astc.data(), 1, chain.astc.size(), f) == chain.astc.size();
|
const bool ok = std::fwrite(&hd, sizeof hd, 1, f) == 1 && std::fwrite(chain.astc.data(), 1, chain.astc.size(), f) == chain.astc.size();
|
||||||
@@ -119,7 +141,9 @@ inline bool store(const TexSource& chain, uint64_t k) {
|
|||||||
// threads (never in the way of a frame). Each texture once per run.
|
// threads (never in the way of a frame). Each texture once per run.
|
||||||
class Builder {
|
class Builder {
|
||||||
public:
|
public:
|
||||||
static Builder& get() { static Builder b; return b; }
|
// Never destroyed: its thread may still be encoding at exit, and must not find its mutex gone (it dies with the
|
||||||
|
// process; a texture half encoded is left as a .part file, which prune clears).
|
||||||
|
static Builder& get() { static Builder* b = new Builder; return *b; }
|
||||||
void add(std::shared_ptr<const TexSource> t) {
|
void add(std::shared_ptr<const TexSource> t) {
|
||||||
if (!t || !texmips::wants(*t)) return;
|
if (!t || !texmips::wants(*t)) return;
|
||||||
std::lock_guard<std::mutex> l(m_);
|
std::lock_guard<std::mutex> l(m_);
|
||||||
|
|||||||
@@ -1508,7 +1508,8 @@ inline std::shared_ptr<WalkGrid> decodeWalkGrid(const std::string& text) {
|
|||||||
if (text.empty() || !unb64(text, z) || z.size() < 8) return nullptr;
|
if (text.empty() || !unb64(text, z) || z.size() < 8) return nullptr;
|
||||||
uint32_t rawSize;
|
uint32_t rawSize;
|
||||||
std::memcpy(&rawSize, z.data(), 4);
|
std::memcpy(&rawSize, z.data(), 4);
|
||||||
if (rawSize > (1u << 30)) return nullptr;
|
// (deflate packs at most ~1032:1: a header claiming more is damaged, and isn't allocated)
|
||||||
|
if (rawSize > (1u << 30) || rawSize > (z.size() - 4) * 1100) return nullptr;
|
||||||
std::vector<uint8_t> raw(rawSize);
|
std::vector<uint8_t> raw(rawSize);
|
||||||
mz_ulong n = rawSize;
|
mz_ulong n = rawSize;
|
||||||
if (mz_uncompress(raw.data(), &n, z.data() + 4, static_cast<mz_ulong>(z.size() - 4)) != MZ_OK || n != rawSize) return nullptr;
|
if (mz_uncompress(raw.data(), &n, z.data() + 4, static_cast<mz_ulong>(z.size() - 4)) != MZ_OK || n != rawSize) return nullptr;
|
||||||
@@ -1535,7 +1536,12 @@ inline std::shared_ptr<WalkGrid> decodeWalkGrid(const std::string& text) {
|
|||||||
g->p.metaBounds = raw[3] >= '3' ? f32() : 1.0f; // made before walking past them was a choice
|
g->p.metaBounds = raw[3] >= '3' ? f32() : 1.0f; // made before walking past them was a choice
|
||||||
g->p.inset = raw[3] >= '4' ? f32() : g->p.radius; // made when you stopped a body radius short
|
g->p.inset = raw[3] >= '4' ? f32() : g->p.radius; // made when you stopped a body radius short
|
||||||
g->nx = static_cast<int>(vi()); g->nz = static_cast<int>(vi());
|
g->nx = static_cast<int>(vi()); g->nz = static_cast<int>(vi());
|
||||||
if (bad || g->p.cell < 0.01f || g->nx < 0 || g->nz < 0 || static_cast<uint64_t>(g->nx) * g->nz > (1u << 26)) return nullptr;
|
// NaN fails every comparison: checked as "is finite" so it can't slip through to the floor queries' casts. Each
|
||||||
|
// cell takes at least a byte of what's left.
|
||||||
|
if (bad || !(g->p.cell >= 0.01f && g->p.cell <= 100.0f) || !std::isfinite(g->ox) || !std::isfinite(g->oz) ||
|
||||||
|
std::fabs(g->ox) > 1e6f || std::fabs(g->oz) > 1e6f || g->nx < 0 || g->nz < 0 ||
|
||||||
|
static_cast<uint64_t>(g->nx) * g->nz > (1u << 26) || static_cast<uint64_t>(g->nx) * g->nz > raw.size() - at)
|
||||||
|
return nullptr;
|
||||||
const size_t cells = static_cast<size_t>(g->nx) * g->nz;
|
const size_t cells = static_cast<size_t>(g->nx) * g->nz;
|
||||||
g->start.resize(cells + 1);
|
g->start.resize(cells + 1);
|
||||||
int32_t prevFirst = 0;
|
int32_t prevFirst = 0;
|
||||||
|
|||||||
@@ -236,8 +236,13 @@ private:
|
|||||||
std::shared_ptr<const WalkGrid> grid_;
|
std::shared_ptr<const WalkGrid> grid_;
|
||||||
bool hasFloor_ = false;
|
bool hasFloor_ = false;
|
||||||
|
|
||||||
static int64_t key(int cx, int cz) { return (static_cast<int64_t>(cx) << 32) ^ static_cast<uint32_t>(cz); }
|
static int64_t key(int cx, int cz) { return static_cast<int64_t>(static_cast<uint64_t>(static_cast<uint32_t>(cx)) << 32 | static_cast<uint32_t>(cz)); }
|
||||||
static int cell(float v) { return static_cast<int>(std::floor(v)); }
|
// (clamped, NaN as 0: a damaged file's 1e30 or NaN vertex must not overflow the int, or a cell loop ending at
|
||||||
|
// INT_MAX never ends)
|
||||||
|
static int cell(float v) {
|
||||||
|
const float c = std::floor(v);
|
||||||
|
return c > -1e7f && c < 1e7f ? static_cast<int>(c) : c >= 1e7f ? 10000000 : c <= -1e7f ? -10000000 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
static void insert(std::vector<Tri>& list, std::unordered_map<int64_t, std::vector<uint32_t>>& grid, const Tri& t) {
|
static void insert(std::vector<Tri>& list, std::unordered_map<int64_t, std::vector<uint32_t>>& grid, const Tri& t) {
|
||||||
const uint32_t id = static_cast<uint32_t>(list.size());
|
const uint32_t id = static_cast<uint32_t>(list.size());
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
// settings change does); `fenv-tool tune` and Frame Studio's Calculate Best Settings work them out on the headset.
|
// settings change does); `fenv-tool tune` and Frame Studio's Calculate Best Settings work them out on the headset.
|
||||||
#include "perf_settings.h"
|
#include "perf_settings.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -71,11 +72,16 @@ struct WorldSettings {
|
|||||||
bool fromValue(const tinyjson::Value& d) {
|
bool fromValue(const tinyjson::Value& d) {
|
||||||
if (!d.isObject()) return false;
|
if (!d.isObject()) return false;
|
||||||
*this = WorldSettings();
|
*this = WorldSettings();
|
||||||
if (d.has("resolution")) resolution = static_cast<float>(d["resolution"].asFloat());
|
// (a file's numbers: inf / NaN / 1e300 as an int is undefined, and NaN would be written back as invalid JSON)
|
||||||
if (d.has("msaa")) msaa = static_cast<int>(d["msaa"].asFloat());
|
const auto num = [&](const char* k, double unset) {
|
||||||
if (d.has("anisotropy")) anisotropy = static_cast<int>(d["anisotropy"].asFloat());
|
const double v = d[k].asFloat();
|
||||||
if (d.has("foveation")) foveation = static_cast<float>(d["foveation"].asFloat());
|
return std::isfinite(v) ? std::min(1e6, std::max(-1e6, v)) : unset;
|
||||||
if (d.has("frameRate")) frameRate = static_cast<int>(d["frameRate"].asFloat());
|
};
|
||||||
|
if (d.has("resolution")) resolution = static_cast<float>(num("resolution", 0));
|
||||||
|
if (d.has("msaa")) msaa = static_cast<int>(num("msaa", 0));
|
||||||
|
if (d.has("anisotropy")) anisotropy = static_cast<int>(num("anisotropy", 0));
|
||||||
|
if (d.has("foveation")) foveation = static_cast<float>(num("foveation", 0));
|
||||||
|
if (d.has("frameRate")) frameRate = static_cast<int>(num("frameRate", -1));
|
||||||
if (d.has("tuned")) tuned = d["tuned"].asString();
|
if (d.has("tuned")) tuned = d["tuned"].asString();
|
||||||
clamp();
|
clamp();
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ endif()
|
|||||||
|
|
||||||
# The build's identity for updates (studio/updater.h): the commit CI built ($FRAME_BUILD); empty for a local build,
|
# The build's identity for updates (studio/updater.h): the commit CI built ($FRAME_BUILD); empty for a local build,
|
||||||
# which never updates itself.
|
# which never updates itself.
|
||||||
|
# Defined on studio.cpp alone (below, with FRAME_VERSION): on every file it changed every one's command line each
|
||||||
|
# commit, and the compiler cache missed on all of them.
|
||||||
set(FRAME_BUILD "$ENV{FRAME_BUILD}")
|
set(FRAME_BUILD "$ENV{FRAME_BUILD}")
|
||||||
add_compile_definitions(FRAME_BUILD="${FRAME_BUILD}")
|
|
||||||
|
|
||||||
set(ROOT "${CMAKE_CURRENT_SOURCE_DIR}/..")
|
set(ROOT "${CMAKE_CURRENT_SOURCE_DIR}/..")
|
||||||
set(TP "${ROOT}/third_party")
|
set(TP "${ROOT}/third_party")
|
||||||
|
include("${ROOT}/cmake/frame_version.cmake") # (FRAME_VERSION: on studio.cpp alone, so a new number rebuilds only it)
|
||||||
set(FRAME_HOME_PAYLOAD_DIR "$ENV{FRAME_HOME_PAYLOAD_DIR}" CACHE PATH "Built Frame Home payload to bundle with Studio")
|
set(FRAME_HOME_PAYLOAD_DIR "$ENV{FRAME_HOME_PAYLOAD_DIR}" CACHE PATH "Built Frame Home payload to bundle with Studio")
|
||||||
if(NOT FRAME_HOME_PAYLOAD_DIR)
|
if(NOT FRAME_HOME_PAYLOAD_DIR)
|
||||||
set(FRAME_HOME_PAYLOAD_DIR "${ROOT}/frame/payload")
|
set(FRAME_HOME_PAYLOAD_DIR "${ROOT}/frame/payload")
|
||||||
@@ -57,6 +59,14 @@ target_include_directories(volk PUBLIC "${TP}/volk" "${TP}/vulkan-headers/includ
|
|||||||
add_library(lzma STATIC "${TP}/lzma/LzmaEnc.c" "${TP}/lzma/LzmaDec.c" "${TP}/lzma/LzFind.c" "${TP}/lzma/LzFindOpt.c"
|
add_library(lzma STATIC "${TP}/lzma/LzmaEnc.c" "${TP}/lzma/LzmaDec.c" "${TP}/lzma/LzFind.c" "${TP}/lzma/LzFindOpt.c"
|
||||||
"${TP}/lzma/LzFindMt.c" "${TP}/lzma/Threads.c" "${TP}/lzma/CpuArch.c")
|
"${TP}/lzma/LzFindMt.c" "${TP}/lzma/Threads.c" "${TP}/lzma/CpuArch.c")
|
||||||
target_include_directories(lzma PUBLIC "${TP}/lzma")
|
target_include_directories(lzma PUBLIC "${TP}/lzma")
|
||||||
|
# zstd 1.5.6 (BSD), decompression only: version 4 .spz splat files (src/splat_import.cpp). Single-threaded, plain C
|
||||||
|
# (no amd64 assembly), so it builds the same with every compiler.
|
||||||
|
add_library(zstd STATIC "${TP}/zstd/common/debug.c" "${TP}/zstd/common/entropy_common.c"
|
||||||
|
"${TP}/zstd/common/error_private.c" "${TP}/zstd/common/fse_decompress.c" "${TP}/zstd/common/xxhash.c"
|
||||||
|
"${TP}/zstd/common/zstd_common.c" "${TP}/zstd/decompress/huf_decompress.c" "${TP}/zstd/decompress/zstd_ddict.c"
|
||||||
|
"${TP}/zstd/decompress/zstd_decompress.c" "${TP}/zstd/decompress/zstd_decompress_block.c")
|
||||||
|
target_include_directories(zstd PUBLIC "${TP}/zstd")
|
||||||
|
target_compile_definitions(zstd PRIVATE ZSTD_DISABLE_ASM)
|
||||||
add_library(miniz STATIC "${TP}/miniz/miniz.c" "${TP}/miniz/miniz_tdef.c" "${TP}/miniz/miniz_tinfl.c" "${TP}/miniz/miniz_zip.c")
|
add_library(miniz STATIC "${TP}/miniz/miniz.c" "${TP}/miniz/miniz_tdef.c" "${TP}/miniz/miniz_tinfl.c" "${TP}/miniz/miniz_zip.c")
|
||||||
target_include_directories(miniz PUBLIC "${TP}/miniz")
|
target_include_directories(miniz PUBLIC "${TP}/miniz")
|
||||||
# APKs are signed and intact; re-checksumming every extracted asset was ~20% of load time.
|
# APKs are signed and intact; re-checksumming every extracted asset was ~20% of load time.
|
||||||
@@ -135,6 +145,7 @@ if(NOT WIN32)
|
|||||||
target_link_libraries(imgui PUBLIC glfw)
|
target_link_libraries(imgui PUBLIC glfw)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
set_source_files_properties(studio.cpp PROPERTIES COMPILE_DEFINITIONS "FRAME_VERSION=\"${FRAME_VERSION}\";FRAME_BUILD=\"${FRAME_BUILD}\"")
|
||||||
add_executable(frame-studio WIN32 MACOSX_BUNDLE
|
add_executable(frame-studio WIN32 MACOSX_BUNDLE
|
||||||
studio.cpp
|
studio.cpp
|
||||||
${STUDIO_PLATFORM}
|
${STUDIO_PLATFORM}
|
||||||
@@ -143,8 +154,11 @@ add_executable(frame-studio WIN32 MACOSX_BUNDLE
|
|||||||
"${ROOT}/src/anim_bake.cpp"
|
"${ROOT}/src/anim_bake.cpp"
|
||||||
"${ROOT}/src/fenv_lzma.cpp"
|
"${ROOT}/src/fenv_lzma.cpp"
|
||||||
"${ROOT}/src/model_import.cpp"
|
"${ROOT}/src/model_import.cpp"
|
||||||
|
"${ROOT}/src/splat_import.cpp"
|
||||||
|
"${ROOT}/src/spv_opt.cpp"
|
||||||
"${TP}/ufbx/ufbx.c"
|
"${TP}/ufbx/ufbx.c"
|
||||||
"${ROOT}/src/hsl_renderer.cpp"
|
"${ROOT}/src/hsl_renderer.cpp"
|
||||||
|
"${ROOT}/src/hsl_renderer_splat.cpp"
|
||||||
"${TP}/qhe/cook/hzanim_acl.cpp"
|
"${TP}/qhe/cook/hzanim_acl.cpp"
|
||||||
"${TP}/stb/stb_image_impl.c"
|
"${TP}/stb/stb_image_impl.c"
|
||||||
"${TP}/stb/stb_vorbis_impl.c"
|
"${TP}/stb/stb_vorbis_impl.c"
|
||||||
@@ -167,15 +181,39 @@ target_include_directories(frame-studio PRIVATE
|
|||||||
"${TP}/rtm/includes"
|
"${TP}/rtm/includes"
|
||||||
"${TP}/astcenc/Source"
|
"${TP}/astcenc/Source"
|
||||||
"${TP}/tinyfiledialogs")
|
"${TP}/tinyfiledialogs")
|
||||||
target_link_libraries(frame-studio PRIVATE imgui volk miniz lzma smolv opusfile ${STUDIO_ASTCENC})
|
target_link_libraries(frame-studio PRIVATE imgui volk miniz lzma zstd smolv opusfile ${STUDIO_ASTCENC})
|
||||||
|
# The SPIRV-Tools optimizer for src/spv_opt.h (FH_SPV_OPT=1 at run time). Off: on the Frame it made no shader any
|
||||||
|
# cheaper (Turnip's compiler already does what it does), and it is a large library to build everywhere.
|
||||||
|
option(FRAME_SPV_OPT "Build in the SPIRV-Tools shader optimizer (third_party/spirv-tools)" OFF)
|
||||||
|
if(FRAME_SPV_OPT)
|
||||||
|
add_subdirectory("${TP}/spirv-tools" spirv-tools EXCLUDE_FROM_ALL)
|
||||||
|
target_link_libraries(frame-studio PRIVATE spirv-tools-opt)
|
||||||
|
target_compile_definitions(frame-studio PRIVATE FH_HAVE_SPV_OPT)
|
||||||
|
endif()
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(frame-studio PRIVATE dwmapi comdlg32 shell32)
|
target_link_libraries(frame-studio PRIVATE dwmapi comdlg32 shell32)
|
||||||
else()
|
else()
|
||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
target_link_libraries(frame-studio PRIVATE glfw Threads::Threads ${CMAKE_DL_LIBS} m)
|
target_link_libraries(frame-studio PRIVATE glfw Threads::Threads ${CMAKE_DL_LIBS} m)
|
||||||
endif()
|
endif()
|
||||||
|
# The app icon (art/icon): a Windows resource, the macOS bundle's .icns, and on Linux a PNG beside the program
|
||||||
|
# that the window sets as its icon.
|
||||||
|
if(WIN32)
|
||||||
|
# The manifest (paths over 260 characters), merged into the program's: from the build folder, since mt.exe can't
|
||||||
|
# open it from a network path such as \\wsl.localhost (a source folder in WSL).
|
||||||
|
configure_file(studio.manifest "${CMAKE_CURRENT_BINARY_DIR}/studio.manifest" COPYONLY)
|
||||||
|
target_sources(frame-studio PRIVATE studio.rc "${CMAKE_CURRENT_BINARY_DIR}/studio.manifest")
|
||||||
|
elseif(APPLE)
|
||||||
|
set(STUDIO_ICNS "${ROOT}/art/icon/frame-studio.icns")
|
||||||
|
target_sources(frame-studio PRIVATE "${STUDIO_ICNS}")
|
||||||
|
set_source_files_properties("${STUDIO_ICNS}" PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||||
|
else()
|
||||||
|
add_custom_command(TARGET frame-studio POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${ROOT}/art/icon/frame-studio-256.png" "$<TARGET_FILE_DIR:frame-studio>/frame-studio.png")
|
||||||
|
endif()
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
set_target_properties(frame-studio PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Frame Studio" MACOSX_BUNDLE_GUI_IDENTIFIER "com.framestudio.app")
|
set_target_properties(frame-studio PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Frame Studio" MACOSX_BUNDLE_GUI_IDENTIFIER "com.framestudio.app"
|
||||||
|
MACOSX_BUNDLE_ICON_FILE frame-studio.icns)
|
||||||
endif()
|
endif()
|
||||||
# Frame Home's headset side (arm64 binaries + install.sh) ships beside the exe, in frame\: Frame > Install on Steam Frame.
|
# Frame Home's headset side (arm64 binaries + install.sh) ships beside the exe, in frame\: Frame > Install on Steam Frame.
|
||||||
if(FRAME_HOME_PAYLOAD_COMPLETE)
|
if(FRAME_HOME_PAYLOAD_COMPLETE)
|
||||||
|
|||||||
@@ -7,14 +7,23 @@
|
|||||||
// home sitting still on a 164 Hz monitor.
|
// home sitting still on a 164 Hz monitor.
|
||||||
//
|
//
|
||||||
// The wait happens before the frame reads input, so pacing adds no input latency: the frame starts as late as it can.
|
// The wait happens before the frame reads input, so pacing adds no input latency: the frame starts as late as it can.
|
||||||
|
// On Windows the frames are locked to the display's refreshes (DWM's vblank clock): each starts its own measured
|
||||||
|
// cost (plus a millisecond to spare) before a refresh, so it's on screen at that refresh. A plain timer at the
|
||||||
|
// refresh rate drifts against the display: some refreshes got two frames and the next none (a hitch every few
|
||||||
|
// seconds), and input waited up to a whole refresh for no reason. Only while DWM's clock keeps to a steady grid
|
||||||
|
// (a virtual or idle display's doesn't): the plain timer otherwise, or always with FS_PACE=timer.
|
||||||
// The window code (Win32 or GLFW) supplies what the pacer can't know: the display's rate, whether the window has
|
// The window code (Win32 or GLFW) supplies what the pacer can't know: the display's rate, whether the window has
|
||||||
// focus, and how to sleep until input arrives.
|
// focus, and how to sleep until input arrives.
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <dwmapi.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
@@ -27,6 +36,8 @@ public:
|
|||||||
static constexpr double kSettleSeconds = 1.0;
|
static constexpr double kSettleSeconds = 1.0;
|
||||||
// Idle waits wake at least this often, so background work's progress (saving, uploading) still shows.
|
// Idle waits wake at least this often, so background work's progress (saving, uploading) still shows.
|
||||||
static constexpr double kIdleWakeSeconds = 0.25;
|
static constexpr double kIdleWakeSeconds = 0.25;
|
||||||
|
// Locked to the refresh: a frame starts this much before the refresh it's for, beyond what frames have cost lately.
|
||||||
|
static constexpr double kSpareMs = 1.0;
|
||||||
|
|
||||||
// isFocused: the window has the keyboard. waitForInput(seconds): sleep until input arrives or the time is up.
|
// isFocused: the window has the keyboard. waitForInput(seconds): sleep until input arrives or the time is up.
|
||||||
FramePacer(std::function<bool()> isFocused, std::function<void(double)> waitForInput)
|
FramePacer(std::function<bool()> isFocused, std::function<void(double)> waitForInput)
|
||||||
@@ -35,6 +46,15 @@ public:
|
|||||||
// High-resolution waitable timer (Windows 10 1803+); a plain one (~1-15 ms granularity) otherwise.
|
// High-resolution waitable timer (Windows 10 1803+); a plain one (~1-15 ms granularity) otherwise.
|
||||||
timer_ = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
|
timer_ = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
|
||||||
if (!timer_) timer_ = CreateWaitableTimerExW(nullptr, nullptr, 0, TIMER_ALL_ACCESS);
|
if (!timer_) timer_ = CreateWaitableTimerExW(nullptr, nullptr, 0, TIMER_ALL_ACCESS);
|
||||||
|
LARGE_INTEGER f;
|
||||||
|
QueryPerformanceFrequency(&f);
|
||||||
|
qpcHz_ = static_cast<double>(f.QuadPart);
|
||||||
|
LARGE_INTEGER q;
|
||||||
|
QueryPerformanceCounter(&q);
|
||||||
|
clockBase_ = Clock::now();
|
||||||
|
qpcBase_ = q.QuadPart;
|
||||||
|
const char* pace = std::getenv("FS_PACE");
|
||||||
|
lockToVblank_ = !(pace && std::strcmp(pace, "timer") == 0);
|
||||||
#endif
|
#endif
|
||||||
lastInput_ = nextFrame_ = Clock::now();
|
lastInput_ = nextFrame_ = Clock::now();
|
||||||
}
|
}
|
||||||
@@ -56,6 +76,43 @@ public:
|
|||||||
// Call for every input from the user (keyboard, mouse, resize, focus, file drop).
|
// Call for every input from the user (keyboard, mouse, resize, focus, file drop).
|
||||||
void noteInput() { lastInput_ = Clock::now(); }
|
void noteInput() { lastInput_ = Clock::now(); }
|
||||||
|
|
||||||
|
// What the frame just drawn cost, from its start to its picture being done (CPU, then the GPU's part): how early
|
||||||
|
// the next one starts. The second-worst of the last 64 frames counts (one stall misses its refresh whatever the
|
||||||
|
// lead; a lead set by it would add latency to every frame after), leaving out frames longer than a refresh.
|
||||||
|
void noteFrameCost(double ms) {
|
||||||
|
if (!(ms > 0)) return;
|
||||||
|
costs_[costAt_++ % kCosts] = ms;
|
||||||
|
double top = 0, second = 0;
|
||||||
|
const double cap = 1000.0 / displayFps_;
|
||||||
|
for (double c : costs_) {
|
||||||
|
if (c >= cap) continue;
|
||||||
|
if (c > top) { second = top; top = c; } else if (c > second) second = c;
|
||||||
|
}
|
||||||
|
costMs_ = second > 0 ? second : top > 0 ? top : costMs_;
|
||||||
|
}
|
||||||
|
double frameCostMs() const { return costMs_; }
|
||||||
|
|
||||||
|
// The display's last refresh and its period, from DWM's clock (Windows): false when there's none to lock to.
|
||||||
|
bool vblank(Clock::time_point& last, double& periodMs) const {
|
||||||
|
#ifdef _WIN32
|
||||||
|
DWM_TIMING_INFO ti;
|
||||||
|
std::memset(&ti, 0, sizeof ti);
|
||||||
|
ti.cbSize = sizeof ti;
|
||||||
|
if (FAILED(DwmGetCompositionTimingInfo(nullptr, &ti)) || ti.qpcRefreshPeriod == 0 || ti.qpcVBlank == 0) return false;
|
||||||
|
LARGE_INTEGER q;
|
||||||
|
QueryPerformanceCounter(&q);
|
||||||
|
const double agoMs = (static_cast<double>(q.QuadPart) - static_cast<double>(ti.qpcVBlank)) * 1000.0 / qpcHz_;
|
||||||
|
// (from one fixed pairing of the two clocks: the same refresh is the same time_point every call)
|
||||||
|
const long long hz = static_cast<long long>(qpcHz_), c = static_cast<long long>(ti.qpcVBlank) - qpcBase_;
|
||||||
|
last = clockBase_ + std::chrono::duration_cast<Clock::duration>(std::chrono::nanoseconds(c / hz * 1000000000LL + c % hz * 1000000000LL / hz));
|
||||||
|
periodMs = static_cast<double>(ti.qpcRefreshPeriod) * 1000.0 / qpcHz_;
|
||||||
|
return periodMs > 2.0 && periodMs < 100.0 && agoMs > -periodMs && agoMs < 1000.0;
|
||||||
|
#else
|
||||||
|
(void)last; (void)periodMs;
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// Blocks until the next frame is due. `sceneChanging`: something on screen moves by itself (animation playing,
|
// Blocks until the next frame is due. `sceneChanging`: something on screen moves by itself (animation playing,
|
||||||
// a home loading, a save's progress, a fly-through). An idle wait ends early when input arrives.
|
// a home loading, a save's progress, a fly-through). An idle wait ends early when input arrives.
|
||||||
void waitForNextFrame(bool sceneChanging) {
|
void waitForNextFrame(bool sceneChanging) {
|
||||||
@@ -65,10 +122,29 @@ public:
|
|||||||
// Nothing can change until the user does something: sleep until input.
|
// Nothing can change until the user does something: sleep until input.
|
||||||
waitForInput_(kIdleWakeSeconds);
|
waitForInput_(kIdleWakeSeconds);
|
||||||
nextFrame_ = Clock::now() - period(displayFps_); // the frame after waking starts at once
|
nextFrame_ = Clock::now() - period(displayFps_); // the frame after waking starts at once
|
||||||
|
targetVblank_ = Clock::time_point();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const double fps = fpsOverride_ >= 0 ? fpsOverride_ : focused ? displayFps_ : kBackgroundFps;
|
const double fps = fpsOverride_ >= 0 ? fpsOverride_ : focused ? displayFps_ : kBackgroundFps;
|
||||||
if (fps <= 0) return;
|
if (fps <= 0) return;
|
||||||
|
Clock::time_point last;
|
||||||
|
double periodMs = 0;
|
||||||
|
if (lockToVblank_ && fpsOverride_ < 0 && focused && vblank(last, periodMs) && std::fabs(1000.0 / periodMs - displayFps_) < displayFps_ * 0.25 &&
|
||||||
|
onGrid(last, periodMs)) {
|
||||||
|
// The first refresh this frame can make (its cost from now), and never the one the last frame was for.
|
||||||
|
const auto per = std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double, std::milli>(periodMs));
|
||||||
|
const auto lead = std::chrono::duration_cast<Clock::duration>(
|
||||||
|
std::chrono::duration<double, std::milli>(std::clamp(costMs_ + kSpareMs, 1.5, periodMs - 0.5)));
|
||||||
|
const Clock::time_point now = Clock::now();
|
||||||
|
Clock::time_point v = last + per;
|
||||||
|
while (v < now + lead) v += per;
|
||||||
|
if (targetVblank_ != Clock::time_point() && v < targetVblank_ + per / 2) v += per;
|
||||||
|
targetVblank_ = v;
|
||||||
|
nextFrame_ = v - lead;
|
||||||
|
sleepUntil(nextFrame_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetVblank_ = Clock::time_point();
|
||||||
// A frame that ran late starts the next period from now instead of rushing to catch up.
|
// A frame that ran late starts the next period from now instead of rushing to catch up.
|
||||||
nextFrame_ = std::max(nextFrame_ + period(fps), Clock::now());
|
nextFrame_ = std::max(nextFrame_ + period(fps), Clock::now());
|
||||||
// Input arriving meanwhile waits for the frame (at most one refresh): waking for each event would let a
|
// Input arriving meanwhile waits for the frame (at most one refresh): waking for each event would let a
|
||||||
@@ -76,19 +152,42 @@ public:
|
|||||||
sleepUntil(nextFrame_);
|
sleepUntil(nextFrame_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The refresh the frame now starting is for (none when not locked to them).
|
||||||
|
Clock::time_point targetVblank() const { return targetVblank_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
static constexpr int kCosts = 64;
|
||||||
|
|
||||||
|
// Whether DWM's refreshes keep to a grid: each new one a whole number of periods (to 0.3 ms) after the one
|
||||||
|
// before, four in a row.
|
||||||
|
bool onGrid(Clock::time_point v, double periodMs) {
|
||||||
|
if (v == lastSeenVblank_) return gridRun_ >= 4;
|
||||||
|
if (lastSeenVblank_ != Clock::time_point()) {
|
||||||
|
const double d = std::chrono::duration<double, std::milli>(v - lastSeenVblank_).count(), k = std::round(d / periodMs);
|
||||||
|
gridRun_ = k >= 1 && std::fabs(d - k * periodMs) < 0.3 ? std::min(gridRun_ + 1, 8) : 0;
|
||||||
|
}
|
||||||
|
lastSeenVblank_ = v;
|
||||||
|
return gridRun_ >= 4;
|
||||||
|
}
|
||||||
|
|
||||||
static Clock::duration period(double fps) {
|
static Clock::duration period(double fps) {
|
||||||
return std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double>(1.0 / fps));
|
return std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double>(1.0 / fps));
|
||||||
}
|
}
|
||||||
|
|
||||||
void sleepUntil(Clock::time_point when) {
|
void sleepUntil(Clock::time_point when) {
|
||||||
const auto wait = when - Clock::now();
|
auto wait = when - Clock::now();
|
||||||
if (wait <= Clock::duration::zero()) return;
|
if (wait <= Clock::duration::zero()) return;
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
// Windows' Sleep has ~1-15 ms granularity: a high-resolution timer hits the refresh.
|
// Windows' Sleep has ~1-15 ms granularity: a high-resolution timer to within a few tenths of a millisecond
|
||||||
LARGE_INTEGER due;
|
// (it wakes up to ~0.5 ms late), then a short spin to the moment itself.
|
||||||
due.QuadPart = -static_cast<LONGLONG>(std::chrono::duration_cast<std::chrono::nanoseconds>(wait).count() / 100); // relative, 100 ns units
|
const auto early = std::chrono::microseconds(600);
|
||||||
if (timer_ && SetWaitableTimer(timer_, &due, 0, nullptr, nullptr, FALSE)) { WaitForSingleObject(timer_, INFINITE); return; }
|
if (timer_ && wait > early) {
|
||||||
|
LARGE_INTEGER due;
|
||||||
|
due.QuadPart = -static_cast<LONGLONG>(std::chrono::duration_cast<std::chrono::nanoseconds>(wait - early).count() / 100); // relative, 100 ns units
|
||||||
|
if (SetWaitableTimer(timer_, &due, 0, nullptr, nullptr, FALSE)) WaitForSingleObject(timer_, INFINITE);
|
||||||
|
}
|
||||||
|
while (Clock::now() < when) YieldProcessor();
|
||||||
|
return;
|
||||||
#endif
|
#endif
|
||||||
std::this_thread::sleep_until(when);
|
std::this_thread::sleep_until(when);
|
||||||
}
|
}
|
||||||
@@ -97,8 +196,16 @@ private:
|
|||||||
std::function<void(double)> waitForInput_;
|
std::function<void(double)> waitForInput_;
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
HANDLE timer_ = nullptr;
|
HANDLE timer_ = nullptr;
|
||||||
|
double qpcHz_ = 1;
|
||||||
|
long long qpcBase_ = 0;
|
||||||
|
Clock::time_point clockBase_;
|
||||||
#endif
|
#endif
|
||||||
|
bool lockToVblank_ = false;
|
||||||
double displayFps_ = 60;
|
double displayFps_ = 60;
|
||||||
double fpsOverride_ = -1;
|
double fpsOverride_ = -1;
|
||||||
Clock::time_point lastInput_, nextFrame_;
|
double costMs_ = 4.0; // (until frames say otherwise)
|
||||||
|
double costs_[kCosts] = {};
|
||||||
|
unsigned costAt_ = 0;
|
||||||
|
int gridRun_ = 0;
|
||||||
|
Clock::time_point lastInput_, nextFrame_, targetVblank_, lastSeenVblank_;
|
||||||
};
|
};
|
||||||
@@ -152,26 +152,29 @@ int run(const std::string& commandLine, const fs::path& workingDir, std::string&
|
|||||||
// Both ends close on exec: a command started meanwhile by another thread (Send to Frame and Install run at once)
|
// Both ends close on exec: a command started meanwhile by another thread (Send to Frame and Install run at once)
|
||||||
// must not inherit this pipe, or its reader never sees the end. The child's own copies (dup2) stay open.
|
// must not inherit this pipe, or its reader never sees the end. The child's own copies (dup2) stay open.
|
||||||
int pipeFds[2];
|
int pipeFds[2];
|
||||||
{
|
|
||||||
static std::mutex creating; // (no pipe2 on macOS: made and marked as one step)
|
|
||||||
std::lock_guard<std::mutex> lock(creating);
|
|
||||||
if (::pipe(pipeFds) != 0) return -1;
|
|
||||||
::fcntl(pipeFds[0], F_SETFD, FD_CLOEXEC);
|
|
||||||
::fcntl(pipeFds[1], F_SETFD, FD_CLOEXEC);
|
|
||||||
}
|
|
||||||
posix_spawn_file_actions_t actions;
|
|
||||||
posix_spawn_file_actions_init(&actions);
|
|
||||||
posix_spawn_file_actions_addopen(&actions, 0, "/dev/null", O_RDONLY, 0);
|
|
||||||
posix_spawn_file_actions_adddup2(&actions, pipeFds[1], 1);
|
|
||||||
posix_spawn_file_actions_adddup2(&actions, pipeFds[1], 2);
|
|
||||||
posix_spawn_file_actions_addclose(&actions, pipeFds[0]);
|
|
||||||
posix_spawn_file_actions_addclose(&actions, pipeFds[1]);
|
|
||||||
// (the working directory through the shell: posix_spawn_file_actions_addchdir isn't everywhere yet)
|
// (the working directory through the shell: posix_spawn_file_actions_addchdir isn't everywhere yet)
|
||||||
const std::string line = workingDir.empty() ? commandLine : "cd " + shellQuote(workingDir.string()) + " && " + commandLine;
|
const std::string line = workingDir.empty() ? commandLine : "cd " + shellQuote(workingDir.string()) + " && " + commandLine;
|
||||||
const char* argv[] = {"/bin/sh", "-c", line.c_str(), nullptr};
|
const char* argv[] = {"/bin/sh", "-c", line.c_str(), nullptr};
|
||||||
pid_t pid = -1;
|
pid_t pid = -1;
|
||||||
const int spawned = posix_spawn(&pid, "/bin/sh", &actions, nullptr, const_cast<char* const*>(argv), environ);
|
int spawned;
|
||||||
posix_spawn_file_actions_destroy(&actions);
|
{
|
||||||
|
// (no pipe2 on macOS: made, marked and handed to the child as one step, so another thread's spawn can't
|
||||||
|
// catch the pipe in between, still inheritable, and keep it open)
|
||||||
|
static std::mutex creating;
|
||||||
|
std::lock_guard<std::mutex> lock(creating);
|
||||||
|
if (::pipe(pipeFds) != 0) return -1;
|
||||||
|
::fcntl(pipeFds[0], F_SETFD, FD_CLOEXEC);
|
||||||
|
::fcntl(pipeFds[1], F_SETFD, FD_CLOEXEC);
|
||||||
|
posix_spawn_file_actions_t actions;
|
||||||
|
posix_spawn_file_actions_init(&actions);
|
||||||
|
posix_spawn_file_actions_addopen(&actions, 0, "/dev/null", O_RDONLY, 0);
|
||||||
|
posix_spawn_file_actions_adddup2(&actions, pipeFds[1], 1);
|
||||||
|
posix_spawn_file_actions_adddup2(&actions, pipeFds[1], 2);
|
||||||
|
posix_spawn_file_actions_addclose(&actions, pipeFds[0]);
|
||||||
|
posix_spawn_file_actions_addclose(&actions, pipeFds[1]);
|
||||||
|
spawned = posix_spawn(&pid, "/bin/sh", &actions, nullptr, const_cast<char* const*>(argv), environ);
|
||||||
|
posix_spawn_file_actions_destroy(&actions);
|
||||||
|
}
|
||||||
::close(pipeFds[1]);
|
::close(pipeFds[1]);
|
||||||
if (spawned != 0) { ::close(pipeFds[0]); out = "could not start: " + commandLine; return -1; }
|
if (spawned != 0) { ::close(pipeFds[0]); out = "could not start: " + commandLine; return -1; }
|
||||||
char buf[4096];
|
char buf[4096];
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<!-- Merged into Frame Studio.exe's own manifest (studio/CMakeLists.txt). longPathAware: Windows lets the program use
|
||||||
|
paths over 260 characters (a home deep in a folder tree), where long paths are turned on (Windows 10 1607 and
|
||||||
|
later: the LongPathsEnabled setting). -->
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
|
||||||
|
<ws2:longPathAware>true</ws2:longPathAware>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
</assembly>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Frame Studio's Windows resources: the app icon (Explorer, the taskbar, Alt+Tab; the window loads it as icon 1).
|
||||||
|
1 ICON "../art/icon/frame-studio.ico"
|
||||||
@@ -1,15 +1,40 @@
|
|||||||
// Frame Studio's own updates. Every push to main replaces the GitHub release "latest", and with it latest.json:
|
// Frame Studio's own updates. Every push to main replaces the GitHub release "latest", and with it latest.json:
|
||||||
// { "build": "<commit>", "home": "<Frame Home's hash>", "files": ["FrameStudio-windows-x64.zip", ...] }
|
// { "build": "<commit>", "home": "<Frame Home's hash>", "files": ["FrameStudio-windows-x64.zip", ...] }
|
||||||
// A build from another commit than this one (FRAME_BUILD, set by CI) is offered. Installing downloads this computer's
|
// A build from another commit than this one (FRAME_BUILD, set by CI) is offered. Installing downloads this computer's
|
||||||
// file, unpacks it, and puts each thing in it where the old one was (the old one renamed aside first: the running
|
// file, unpacks it, and puts each thing in it where the old one was (the old one set aside: the running copy keeps
|
||||||
// copy keeps going, and a failure puts everything back). Frame Studio then restarts into it; the next start deletes
|
// going, and a failure puts everything back). Frame Studio then restarts into it; the next start deletes
|
||||||
// what was set aside. A local build (no FRAME_BUILD) never updates itself.
|
// what was set aside. A local build (no FRAME_BUILD) never updates itself.
|
||||||
|
//
|
||||||
|
// One update at a time: two Studios open share update/, so whichever installs holds update.lock (beside it) for the
|
||||||
|
// whole download and swap, and the other is told to wait; a start doesn't tidy update/ while another holds it. And a
|
||||||
|
// swap that stops halfway (a crash, a power cut) is finished or undone on the next start: before anything is moved,
|
||||||
|
// update/journal.txt lists every move, the program itself last. Started from the new program, everything else was
|
||||||
|
// already in place: the swap is finished. Started from the old one, it never got that far: every move is put back.
|
||||||
|
// A file's path is never empty along the way (or a crash there would leave no program to start and do that): the
|
||||||
|
// new one is staged beside it as "<name>.new", the old one gets a second name ("<name>.old", a hard link), and the
|
||||||
|
// new one then replaces it in one move. A folder is renamed aside and the new one moved in; on macOS the .app is
|
||||||
|
// swapped with the new one in one move.
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#ifdef __APPLE__
|
||||||
|
#include <stdio.h> // renamex_np
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -68,6 +93,46 @@ inline std::string assetFolder() {
|
|||||||
|
|
||||||
inline fs::path workDir() { return platform::userDataDir() / L"FrameStudio" / L"update"; }
|
inline fs::path workDir() { return platform::userDataDir() / L"FrameStudio" / L"update"; }
|
||||||
|
|
||||||
|
// update.lock, held while one Studio updates (or tidies up after one): never waited for, so a second Studio
|
||||||
|
// doesn't hang behind a download, it's just told.
|
||||||
|
class Lock {
|
||||||
|
public:
|
||||||
|
bool take() {
|
||||||
|
std::error_code ec;
|
||||||
|
const fs::path p = platform::userDataDir() / L"FrameStudio" / L"update.lock";
|
||||||
|
fs::create_directories(p.parent_path(), ec);
|
||||||
|
#ifdef _WIN32
|
||||||
|
h_ = CreateFileW(p.wstring().c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
|
||||||
|
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||||
|
if (h_ == INVALID_HANDLE_VALUE) return false;
|
||||||
|
if (!LockFileEx(h_, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0, MAXDWORD, MAXDWORD, &ov_)) { CloseHandle(h_); h_ = INVALID_HANDLE_VALUE; return false; }
|
||||||
|
#else
|
||||||
|
fd_ = ::open(p.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600);
|
||||||
|
if (fd_ < 0) return false;
|
||||||
|
if (::flock(fd_, LOCK_EX | LOCK_NB) != 0) { ::close(fd_); fd_ = -1; return false; }
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Lock() = default;
|
||||||
|
~Lock() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (h_ != INVALID_HANDLE_VALUE) { UnlockFileEx(h_, 0, MAXDWORD, MAXDWORD, &ov_); CloseHandle(h_); }
|
||||||
|
#else
|
||||||
|
if (fd_ >= 0) { ::flock(fd_, LOCK_UN); ::close(fd_); }
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
Lock(const Lock&) = delete;
|
||||||
|
Lock& operator=(const Lock&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
#ifdef _WIN32
|
||||||
|
HANDLE h_ = INVALID_HANDLE_VALUE;
|
||||||
|
OVERLAPPED ov_ = {};
|
||||||
|
#else
|
||||||
|
int fd_ = -1;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
// The string value of "key" in a small JSON object ("" when it's not there).
|
// The string value of "key" in a small JSON object ("" when it's not there).
|
||||||
inline std::string jsonString(const std::string& json, const std::string& key) {
|
inline std::string jsonString(const std::string& json, const std::string& key) {
|
||||||
size_t p = json.find("\"" + key + "\"");
|
size_t p = json.find("\"" + key + "\"");
|
||||||
@@ -80,6 +145,76 @@ inline std::string jsonString(const std::string& json, const std::string& key) {
|
|||||||
return e == std::string::npos ? std::string() : json.substr(p + 1, e - p - 1);
|
return e == std::string::npos ? std::string() : json.substr(p + 1, e - p - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A file's SHA-256, as 64 lowercase hex digits ("" when it can't be read). What latest.json says each download's is
|
||||||
|
// ("sha256-<file>"): an update is only unpacked when its download matches, so a cut-off, damaged or swapped download
|
||||||
|
// is never installed.
|
||||||
|
inline std::string sha256File(const fs::path& path) {
|
||||||
|
static const uint32_t k[64] = {
|
||||||
|
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be,
|
||||||
|
0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa,
|
||||||
|
0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85,
|
||||||
|
0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
|
||||||
|
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f,
|
||||||
|
0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};
|
||||||
|
uint32_t h[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
|
||||||
|
auto rotr = [](uint32_t x, int n) { return (x >> n) | (x << (32 - n)); };
|
||||||
|
auto block = [&](const unsigned char* p) {
|
||||||
|
uint32_t w[64];
|
||||||
|
for (int i = 0; i < 16; ++i) w[i] = (uint32_t(p[i * 4]) << 24) | (uint32_t(p[i * 4 + 1]) << 16) | (uint32_t(p[i * 4 + 2]) << 8) | p[i * 4 + 3];
|
||||||
|
for (int i = 16; i < 64; ++i) {
|
||||||
|
const uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3), s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10);
|
||||||
|
w[i] = w[i - 16] + s0 + w[i - 7] + s1;
|
||||||
|
}
|
||||||
|
uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7];
|
||||||
|
for (int i = 0; i < 64; ++i) {
|
||||||
|
const uint32_t t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + k[i] + w[i];
|
||||||
|
const uint32_t t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c));
|
||||||
|
hh = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2;
|
||||||
|
}
|
||||||
|
h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; h[5] += f; h[6] += g; h[7] += hh;
|
||||||
|
};
|
||||||
|
FILE* file = platform::openFile(path, "rb");
|
||||||
|
if (!file) return {};
|
||||||
|
std::vector<unsigned char> buf(1 << 16);
|
||||||
|
uint64_t total = 0;
|
||||||
|
size_t have = 0; // bytes in buf not yet hashed (less than a block)
|
||||||
|
for (;;) {
|
||||||
|
const size_t n = std::fread(buf.data() + have, 1, buf.size() - have, file);
|
||||||
|
if (n == 0) break;
|
||||||
|
total += n;
|
||||||
|
have += n;
|
||||||
|
size_t at = 0;
|
||||||
|
for (; at + 64 <= have; at += 64) block(buf.data() + at);
|
||||||
|
std::memmove(buf.data(), buf.data() + at, have - at);
|
||||||
|
have -= at;
|
||||||
|
}
|
||||||
|
const bool failed = std::ferror(file) != 0;
|
||||||
|
std::fclose(file);
|
||||||
|
if (failed) return {};
|
||||||
|
unsigned char tail[128] = {};
|
||||||
|
std::memcpy(tail, buf.data(), have);
|
||||||
|
tail[have] = 0x80;
|
||||||
|
const size_t len = have + 1 + 8 <= 64 ? 64 : 128;
|
||||||
|
const uint64_t bits = total * 8;
|
||||||
|
for (int i = 0; i < 8; ++i) tail[len - 1 - i] = static_cast<unsigned char>(bits >> (8 * i));
|
||||||
|
for (size_t at = 0; at < len; at += 64) block(tail + at);
|
||||||
|
std::string hex;
|
||||||
|
char b[9];
|
||||||
|
for (uint32_t v : h) { std::snprintf(b, sizeof b, "%08x", v); hex += b; }
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The program to start after an update.
|
||||||
|
inline fs::path updatedProgram() {
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
return platform::exeDir().parent_path().parent_path();
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
return platform::exeDir() / L"Frame Studio.exe";
|
||||||
|
#else
|
||||||
|
return platform::exeDir() / L"Frame Studio";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// What the update replaces: each thing the download holds, and where it goes. On macOS the whole .app.
|
// What the update replaces: each thing the download holds, and where it goes. On macOS the whole .app.
|
||||||
struct Swap { fs::path from, to; };
|
struct Swap { fs::path from, to; };
|
||||||
|
|
||||||
@@ -92,34 +227,163 @@ inline std::vector<Swap> swapsFor(const fs::path& unpacked) {
|
|||||||
#else
|
#else
|
||||||
for (const auto& e : fs::directory_iterator(unpacked, ec)) out.push_back({e.path(), platform::exeDir() / e.path().filename()});
|
for (const auto& e : fs::directory_iterator(unpacked, ec)) out.push_back({e.path(), platform::exeDir() / e.path().filename()});
|
||||||
#endif
|
#endif
|
||||||
|
// The program last (see the journal, above): while it's still the old one, a start runs the old one.
|
||||||
|
const fs::path program = updatedProgram();
|
||||||
|
std::stable_partition(out.begin(), out.end(), [&](const Swap& sw) { return sw.to != program; });
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The program to start after an update.
|
// update/journal.txt: what a swap moves, written (whole, then renamed into place) before the first move.
|
||||||
inline fs::path updatedProgram() {
|
// frame-studio-update 1
|
||||||
#if defined(__APPLE__)
|
// build <the new build>
|
||||||
return platform::exeDir().parent_path().parent_path();
|
// <1 if `to` was there before, else 0> TAB <to> TAB <where the old one goes> TAB <the new one>
|
||||||
#elif defined(_WIN32)
|
struct JournalEntry { bool had = false; fs::path to, old, from; };
|
||||||
return platform::exeDir() / L"Frame Studio.exe";
|
inline fs::path journalPath() { return workDir() / L"journal.txt"; }
|
||||||
#else
|
inline bool writeJournal(const std::string& build, const std::vector<JournalEntry>& entries) {
|
||||||
return platform::exeDir() / L"Frame Studio";
|
const fs::path tmp = workDir() / L"journal.txt.part";
|
||||||
#endif
|
FILE* f = platform::openFile(tmp, "wb");
|
||||||
|
if (!f) return false;
|
||||||
|
std::fprintf(f, "frame-studio-update 1\nbuild %s\n", build.c_str());
|
||||||
|
for (const JournalEntry& e : entries)
|
||||||
|
std::fprintf(f, "%d\t%s\t%s\t%s\n", e.had ? 1 : 0, platform::narrow(e.to.wstring()).c_str(), platform::narrow(e.old.wstring()).c_str(),
|
||||||
|
platform::narrow(e.from.wstring()).c_str());
|
||||||
|
const bool ok = std::fflush(f) == 0 && !std::ferror(f);
|
||||||
|
std::fclose(f);
|
||||||
|
std::error_code ec;
|
||||||
|
if (ok) fs::rename(tmp, journalPath(), ec);
|
||||||
|
return ok && !ec;
|
||||||
|
}
|
||||||
|
inline bool readJournal(std::string& build, std::vector<JournalEntry>& entries) {
|
||||||
|
FILE* f = platform::openFile(journalPath(), "rb");
|
||||||
|
if (!f) return false;
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
char line[8192];
|
||||||
|
while (std::fgets(line, sizeof line, f)) {
|
||||||
|
std::string s = line;
|
||||||
|
while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) s.pop_back();
|
||||||
|
lines.push_back(s);
|
||||||
|
}
|
||||||
|
std::fclose(f);
|
||||||
|
if (lines.size() < 2 || lines[0] != "frame-studio-update 1" || lines[1].rfind("build ", 0) != 0) return false;
|
||||||
|
build = lines[1].substr(6);
|
||||||
|
for (size_t i = 2; i < lines.size(); ++i) {
|
||||||
|
const std::string& l = lines[i];
|
||||||
|
const size_t a = l.find('\t'), b = a == std::string::npos ? a : l.find('\t', a + 1), c = b == std::string::npos ? b : l.find('\t', b + 1);
|
||||||
|
if (c == std::string::npos || a != 1 || (l[0] != '0' && l[0] != '1')) return false;
|
||||||
|
JournalEntry e;
|
||||||
|
e.had = l[0] == '1';
|
||||||
|
e.to = fs::path(platform::widen(l.substr(a + 1, b - a - 1)));
|
||||||
|
e.old = fs::path(platform::widen(l.substr(b + 1, c - b - 1)));
|
||||||
|
e.from = fs::path(platform::widen(l.substr(c + 1)));
|
||||||
|
if (e.to.empty() || (e.had && e.old.empty())) return false;
|
||||||
|
entries.push_back(e);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// At start: delete what the last update set aside (listed in update/aside.txt).
|
// A swap's stand-in for a crash, for the updater test: swapIn() stops dead (no undo, the journal left) after this
|
||||||
|
// many more moves. -1: never.
|
||||||
|
inline int& stopAfterMoves() { static int n = -1; return n; }
|
||||||
|
inline bool stopNow() { int& n = stopAfterMoves(); return n >= 0 && n-- == 0; }
|
||||||
|
inline constexpr const char* kStopped = "(stopped by the test)";
|
||||||
|
|
||||||
|
inline fs::path withSuffix(fs::path p, const wchar_t* suffix) { p += suffix; return p; }
|
||||||
|
|
||||||
|
// Puts one journal entry back the way it was before the swap, whatever step it stopped at.
|
||||||
|
inline void putBack(const JournalEntry& e) {
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove_all(withSuffix(e.to, L".new"), ec); // the new one, staged beside it
|
||||||
|
if (!e.had) { fs::remove_all(e.to, ec); return; } // added by the update (it wasn't there before)
|
||||||
|
fs::remove_all(withSuffix(e.old, L".part"), ec); // (a copy of the old one, cut off)
|
||||||
|
if (!fs::exists(e.old, ec)) return; // never set aside: it's still the old one
|
||||||
|
if (fs::exists(e.to, ec) && fs::equivalent(e.to, e.old, ec)) { fs::remove(e.old, ec); return; } // only linked
|
||||||
|
ec.clear();
|
||||||
|
if (!fs::is_directory(e.old, ec)) { fs::rename(e.old, e.to, ec); if (!ec) return; } // one move over the new one
|
||||||
|
ec.clear();
|
||||||
|
fs::remove_all(e.to, ec);
|
||||||
|
ec.clear();
|
||||||
|
fs::rename(e.old, e.to, ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
// Swaps two paths in one move (both stay there). False: this file system (or system) can't.
|
||||||
|
inline bool swapPaths(const fs::path& a, const fs::path& b) {
|
||||||
|
#ifdef __APPLE__
|
||||||
|
return ::renamex_np(a.c_str(), b.c_str(), RENAME_SWAP) == 0;
|
||||||
|
#else
|
||||||
|
(void)a; (void)b;
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Adds to update/aside.txt (what the next start deletes): an earlier update's leftovers stay listed too.
|
||||||
|
inline void noteAside(const std::vector<fs::path>& aside) {
|
||||||
|
if (aside.empty()) return;
|
||||||
|
if (FILE* f = platform::openFile(workDir() / L"aside.txt", "ab")) {
|
||||||
|
for (const fs::path& p : aside) std::fprintf(f, "%s\n", platform::narrow(p.wstring()).c_str());
|
||||||
|
std::fclose(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A swap a crash stopped halfway (the journal is there). Started from the new program (this build is the
|
||||||
|
// journal's): everything else was moved before it, so the swap is finished, and what was set aside goes on the list.
|
||||||
|
// From the old one: every move is put back, last first (a thing the update added is removed, one that never moved
|
||||||
|
// is left alone). (`kBuild` is passed in: the self-test plays both sides.)
|
||||||
|
inline void recover(const std::string& thisBuild = kBuild) {
|
||||||
|
std::string build;
|
||||||
|
std::vector<JournalEntry> entries;
|
||||||
|
std::error_code ec;
|
||||||
|
if (!fs::exists(journalPath(), ec)) return;
|
||||||
|
if (!readJournal(build, entries)) { // (cut off while written, or not ours: nothing was moved by it)
|
||||||
|
fs::remove(journalPath(), ec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!build.empty() && build == thisBuild) {
|
||||||
|
std::vector<fs::path> aside;
|
||||||
|
for (const JournalEntry& e : entries) if (e.had && fs::exists(e.old, ec)) aside.push_back(e.old);
|
||||||
|
noteAside(aside);
|
||||||
|
} else {
|
||||||
|
for (auto it = entries.rbegin(); it != entries.rend(); ++it) putBack(*it);
|
||||||
|
}
|
||||||
|
fs::remove(journalPath(), ec);
|
||||||
|
fs::remove_all(workDir() / L"unpacked", ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// At start: finish or undo a swap a crash interrupted, then delete what the last update set aside (listed in
|
||||||
|
// update/aside.txt). Not while another Studio is updating (it holds update.lock): update/ is in use.
|
||||||
inline void cleanUp() {
|
inline void cleanUp() {
|
||||||
|
Lock lock;
|
||||||
|
if (!lock.take()) return;
|
||||||
|
recover();
|
||||||
const fs::path list = workDir() / L"aside.txt";
|
const fs::path list = workDir() / L"aside.txt";
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (FILE* f = platform::openFile(list, "rb")) {
|
if (FILE* f = platform::openFile(list, "rb")) {
|
||||||
|
// What can't go yet stays listed for the next start: the copy that restarted into this one may still be
|
||||||
|
// running from its old program (Windows won't delete a running .exe), and forgetting it would leave it there.
|
||||||
|
std::vector<std::string> left;
|
||||||
char line[4096];
|
char line[4096];
|
||||||
while (std::fgets(line, sizeof line, f)) {
|
while (std::fgets(line, sizeof line, f)) {
|
||||||
std::string s = line;
|
std::string s = line;
|
||||||
while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) s.pop_back();
|
while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) s.pop_back();
|
||||||
if (!s.empty()) fs::remove_all(fs::path(platform::widen(s)), ec);
|
if (s.empty()) continue;
|
||||||
|
const fs::path p(platform::widen(s));
|
||||||
|
ec.clear();
|
||||||
|
fs::remove_all(p, ec);
|
||||||
|
if (fs::exists(p, ec)) left.push_back(s);
|
||||||
}
|
}
|
||||||
std::fclose(f);
|
std::fclose(f);
|
||||||
|
if (left.empty()) { fs::remove_all(workDir(), ec); return; }
|
||||||
|
if (FILE* o = platform::openFile(list, "wb")) {
|
||||||
|
for (const std::string& s : left) std::fprintf(o, "%s\n", s.c_str());
|
||||||
|
std::fclose(o);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
fs::remove_all(workDir(), ec);
|
// No update just made: only what's been lying there a while (another Studio open may be downloading one now).
|
||||||
|
const auto old = fs::file_time_type::clock::now() - std::chrono::hours(1);
|
||||||
|
for (const auto& e : fs::directory_iterator(workDir(), ec))
|
||||||
|
if (e.last_write_time(ec) < old) fs::remove_all(e.path(), ec);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class State { Idle, Checking, Current, Available, Installing, Ready, Failed };
|
enum class State { Idle, Checking, Current, Available, Installing, Ready, Failed };
|
||||||
@@ -192,14 +456,36 @@ private:
|
|||||||
// The download, unpacked and swapped in. Empty when it worked, else what went wrong (nothing changed then).
|
// The download, unpacked and swapped in. Empty when it worked, else what went wrong (nothing changed then).
|
||||||
static std::string download(Shared& s) {
|
static std::string download(Shared& s) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
|
Lock lock; // (held to the end: another Studio's start won't tidy update/ from under this one)
|
||||||
|
if (!lock.take()) return "Another Frame Studio window is installing the update now. Restart Frame Studio once it's done.";
|
||||||
|
recover(); // (a swap that stopped halfway and wasn't seen to since: put back first)
|
||||||
const fs::path dir = workDir();
|
const fs::path dir = workDir();
|
||||||
fs::remove_all(dir, ec);
|
// A fresh download, keeping the list of what earlier updates set aside (not all of it can go while a Studio
|
||||||
|
// started from an older program still runs).
|
||||||
|
for (const auto& e : fs::directory_iterator(dir, ec))
|
||||||
|
if (e.path().filename() != L"aside.txt") { std::error_code e2; fs::remove_all(e.path(), e2); }
|
||||||
|
ec.clear();
|
||||||
fs::create_directories(dir / L"unpacked", ec);
|
fs::create_directories(dir / L"unpacked", ec);
|
||||||
const fs::path file = dir / platform::widen(assetName());
|
const fs::path file = dir / platform::widen(assetName());
|
||||||
std::string out;
|
std::string out;
|
||||||
|
// What the download must be: latest.json read again now (the build may have moved on since the check), its
|
||||||
|
// checksum for this computer's file. Without one there's nothing to check the download against: not installed.
|
||||||
|
if (platform::run(platform::systemTool("curl") + "-fsSL --max-time 30 \"" + releaseUrl() + "latest.json\"", {}, out) != 0)
|
||||||
|
return "Couldn't reach GitHub for the update. Check your connection and try again.";
|
||||||
|
const std::string build = jsonString(out, "build"); // (what's downloaded: the journal names it)
|
||||||
|
std::string want = jsonString(out, "sha256-" + assetName());
|
||||||
|
for (char& c : want) c = static_cast<char>(c >= 'A' && c <= 'F' ? c - 'A' + 'a' : c);
|
||||||
|
if (want.size() != 64 || want.find_first_not_of("0123456789abcdef") != std::string::npos)
|
||||||
|
return "This update has no checksum to check the download against, so it wasn't installed. Download it from GitHub instead.";
|
||||||
|
out.clear();
|
||||||
const std::string url = releaseUrl() + assetName();
|
const std::string url = releaseUrl() + assetName();
|
||||||
if (platform::run(platform::systemTool("curl") + "-fsSL --retry 2 --max-time 1800 -o \"" + platform::narrow(file.wstring()) + "\" \"" + url + "\"", {}, out) != 0)
|
if (platform::run(platform::systemTool("curl") + "-fsSL --retry 2 --max-time 1800 -o \"" + platform::narrow(file.wstring()) + "\" \"" + url + "\"", {}, out) != 0)
|
||||||
return "The download didn't finish. Check your connection and try again.";
|
return "The download didn't finish. Check your connection and try again.";
|
||||||
|
say(s, "Checking the download…");
|
||||||
|
if (sha256File(file) != want) {
|
||||||
|
fs::remove_all(dir, ec);
|
||||||
|
return "The download was damaged or incomplete (its checksum doesn't match), so nothing was changed. Try again.";
|
||||||
|
}
|
||||||
say(s, "Unpacking the update…");
|
say(s, "Unpacking the update…");
|
||||||
out.clear();
|
out.clear();
|
||||||
if (platform::run(platform::systemTool("tar") + "-xf \"" + platform::narrow(file.wstring()) + "\" -C \"" + platform::narrow((dir / L"unpacked").wstring()) + "\"", {}, out) != 0)
|
if (platform::run(platform::systemTool("tar") + "-xf \"" + platform::narrow(file.wstring()) + "\" -C \"" + platform::narrow((dir / L"unpacked").wstring()) + "\"", {}, out) != 0)
|
||||||
@@ -209,41 +495,121 @@ private:
|
|||||||
if (swaps.empty() || !fs::exists(swaps.front().from, ec)) return "The download isn't a Frame Studio build.";
|
if (swaps.empty() || !fs::exists(swaps.front().from, ec)) return "The download isn't a Frame Studio build.";
|
||||||
|
|
||||||
say(s, "Installing the update…");
|
say(s, "Installing the update…");
|
||||||
std::vector<Swap> done; // what was put in place, to undo on a failure
|
const std::string err = swapIn(swaps, build);
|
||||||
std::vector<fs::path> aside;
|
if (!err.empty()) return err;
|
||||||
auto undo = [&] {
|
fs::remove_all(dir / L"unpacked", ec);
|
||||||
for (auto it = done.rbegin(); it != done.rend(); ++it) {
|
fs::remove(file, ec);
|
||||||
std::error_code e;
|
return {};
|
||||||
fs::remove_all(it->to, e);
|
}
|
||||||
if (!it->from.empty()) fs::rename(it->from, it->to, e);
|
|
||||||
}
|
public:
|
||||||
};
|
// Puts each `from` where its `to` is, the old one set aside: journaled (recover() finishes or undoes it if this
|
||||||
|
// stops halfway), undone on a failure. Empty when it worked, else what went wrong (nothing changed then).
|
||||||
|
static std::string swapIn(const std::vector<Swap>& swaps, const std::string& build) {
|
||||||
|
std::error_code ec;
|
||||||
|
// Where each old one goes ("<name>.old", or .old1 ... while an earlier one can't be deleted yet), all in the
|
||||||
|
// journal before anything moves.
|
||||||
|
std::vector<JournalEntry> plan;
|
||||||
for (const Swap& sw : swaps) {
|
for (const Swap& sw : swaps) {
|
||||||
fs::path old;
|
JournalEntry e;
|
||||||
if (fs::exists(sw.to, ec)) {
|
e.to = sw.to;
|
||||||
old = sw.to;
|
e.from = sw.from;
|
||||||
old += L".old";
|
e.had = fs::exists(sw.to, ec);
|
||||||
fs::remove_all(old, ec);
|
for (int n = 0; e.had && n < 10; ++n) {
|
||||||
ec.clear();
|
fs::path old = sw.to;
|
||||||
fs::rename(sw.to, old, ec);
|
old += n ? L".old" + std::to_wstring(n) : std::wstring(L".old");
|
||||||
if (ec) { undo(); return "Frame Studio's folder can't be changed (" + ec.message() + "). Download the update from GitHub instead."; }
|
std::error_code e2;
|
||||||
aside.push_back(old);
|
fs::remove_all(old, e2);
|
||||||
|
if (!fs::exists(old, e2)) { e.old = old; break; }
|
||||||
}
|
}
|
||||||
done.push_back({old, sw.to});
|
if (e.had && e.old.empty()) return "Frame Studio's folder can't be changed (an older copy is still in use). Close every Frame Studio window and try again.";
|
||||||
ec.clear();
|
std::error_code e2;
|
||||||
fs::rename(sw.from, sw.to, ec); // (another drive than the download: copied instead)
|
fs::remove_all(withSuffix(sw.to, L".new"), e2);
|
||||||
if (ec) { ec.clear(); fs::copy(sw.from, sw.to, fs::copy_options::recursive, ec); }
|
if (e.had) fs::remove_all(withSuffix(e.old, L".part"), e2);
|
||||||
if (ec) { undo(); return "The update couldn't be put in place (" + ec.message() + "). Nothing was changed."; }
|
plan.push_back(e);
|
||||||
}
|
}
|
||||||
|
if (!writeJournal(build, plan)) return "The update couldn't be started (its folder can't be written). Nothing was changed.";
|
||||||
|
std::vector<fs::path> aside;
|
||||||
|
// Stopped at plan[i]: it and everything before it put back, last first.
|
||||||
|
auto undo = [&](size_t i, const std::string& what) {
|
||||||
|
for (size_t j = i + 1; j-- > 0;) putBack(plan[j]);
|
||||||
|
std::error_code e;
|
||||||
|
fs::remove(journalPath(), e);
|
||||||
|
return what;
|
||||||
|
};
|
||||||
|
const auto cantChange = [](const std::error_code& e) { return "Frame Studio's folder can't be changed (" + e.message() + "). Download the update from GitHub instead."; };
|
||||||
|
const auto cantPut = [](const std::error_code& e) { return "The update couldn't be put in place (" + e.message() + "). Nothing was changed."; };
|
||||||
|
for (size_t i = 0; i < plan.size(); ++i) {
|
||||||
|
const JournalEntry& e = plan[i];
|
||||||
|
if (stopNow()) return kStopped;
|
||||||
|
ec.clear();
|
||||||
|
if (fs::is_directory(e.from, ec)) {
|
||||||
|
#ifndef _WIN32
|
||||||
|
// The .app (macOS): swapped with the new one in one move, then the old one (now where the new was)
|
||||||
|
// set aside. (Stopped between: the new one is in place; the old one goes with update/unpacked.)
|
||||||
|
if (e.had && e.to == updatedProgram() && swapPaths(e.from, e.to)) {
|
||||||
|
if (stopNow()) return kStopped;
|
||||||
|
ec.clear();
|
||||||
|
fs::rename(e.from, e.old, ec);
|
||||||
|
if (!ec) aside.push_back(e.old);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (e.had) {
|
||||||
|
ec.clear();
|
||||||
|
fs::rename(e.to, e.old, ec);
|
||||||
|
if (ec) return undo(i, cantChange(ec));
|
||||||
|
aside.push_back(e.old);
|
||||||
|
if (stopNow()) return kStopped;
|
||||||
|
}
|
||||||
|
ec.clear();
|
||||||
|
fs::rename(e.from, e.to, ec); // (another drive than the download: copied instead)
|
||||||
|
if (ec) { ec.clear(); fs::copy(e.from, e.to, fs::copy_options::recursive, ec); }
|
||||||
|
if (ec) return undo(i, cantPut(ec));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// A file: the new one staged beside it first (moved, or copied from another drive), so the last step
|
||||||
|
// is one move on the same drive.
|
||||||
|
const fs::path staged = withSuffix(e.to, L".new");
|
||||||
|
ec.clear();
|
||||||
|
fs::rename(e.from, staged, ec);
|
||||||
|
if (ec) { ec.clear(); fs::copy_file(e.from, staged, fs::copy_options::overwrite_existing, ec); }
|
||||||
|
if (ec) return undo(i, cantPut(ec));
|
||||||
|
if (stopNow()) return kStopped;
|
||||||
|
if (e.had) {
|
||||||
|
// The old one keeps its path and gets a second name, the one it's set aside under (Windows lets a
|
||||||
|
// running program be replaced like this: the file itself stays, under its other name).
|
||||||
|
ec.clear();
|
||||||
|
fs::create_hard_link(e.to, e.old, ec);
|
||||||
|
if (ec) { // (a drive without hard links: FAT, exFAT)
|
||||||
|
#ifdef _WIN32
|
||||||
|
// In one call: the old one renamed aside, the new one in its place.
|
||||||
|
if (!ReplaceFileW(e.to.wstring().c_str(), staged.wstring().c_str(), e.old.wstring().c_str(), REPLACEFILE_IGNORE_MERGE_ERRORS, nullptr, nullptr))
|
||||||
|
return undo(i, cantChange(std::error_code(static_cast<int>(GetLastError()), std::system_category())));
|
||||||
|
aside.push_back(e.old);
|
||||||
|
continue;
|
||||||
|
#else
|
||||||
|
// A copy instead (whole, then renamed: a copy cut off is never taken for the old one).
|
||||||
|
const fs::path part = withSuffix(e.old, L".part");
|
||||||
|
ec.clear();
|
||||||
|
fs::copy_file(e.to, part, fs::copy_options::overwrite_existing, ec);
|
||||||
|
if (!ec) fs::rename(part, e.old, ec);
|
||||||
|
if (ec) return undo(i, cantChange(ec));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
aside.push_back(e.old);
|
||||||
|
if (stopNow()) return kStopped;
|
||||||
|
}
|
||||||
|
ec.clear();
|
||||||
|
fs::rename(staged, e.to, ec); // (over the old one: its path is never empty)
|
||||||
|
if (ec) return undo(i, cantChange(ec));
|
||||||
|
}
|
||||||
|
if (stopNow()) return kStopped; // (all moved, the journal still there: the new program finishes it)
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
fs::permissions(updatedProgram(), fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec, fs::perm_options::add, ec);
|
fs::permissions(updatedProgram(), fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec, fs::perm_options::add, ec);
|
||||||
#endif
|
#endif
|
||||||
if (FILE* f = platform::openFile(dir / L"aside.txt", "wb")) {
|
noteAside(aside);
|
||||||
for (const fs::path& p : aside) std::fprintf(f, "%s\n", platform::narrow(p.wstring()).c_str());
|
fs::remove(journalPath(), ec); // (done: from here a start has nothing to finish or undo)
|
||||||
std::fclose(f);
|
|
||||||
}
|
|
||||||
fs::remove_all(dir / L"unpacked", ec);
|
|
||||||
fs::remove(file, ec);
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
// The shared slots between Frame Home and the compositor layer (src/env_share.h, layer/frame_home_layer.cpp), hammered:
|
||||||
|
// a writer publishes frames as fast as it can and resizes every few hundred (a new image generation, the layer
|
||||||
|
// importing it a while later), while the layer's own liveSlot()/fillUbo() pick and fill frames. Every uniform block the
|
||||||
|
// layer fills must be one whole frame of the generation whose images it draws: never half of one pose and half of the
|
||||||
|
// next, never the new generation's pose over the old images.
|
||||||
|
// env_share_test [seconds] [thread|process] [legacy]
|
||||||
|
// thread: the writer a thread (run it under -fsanitize=thread)
|
||||||
|
// process: the writer another process, over a memfd as the real pair share it
|
||||||
|
// legacy: also run the layer's old way (shared slot read as it is) and show the test catches what it got wrong
|
||||||
|
// Also: the State layouts of a mismatched pair (an older Frame Home with this layer, and the other way) fail safe.
|
||||||
|
//
|
||||||
|
// Build (from the repo): g++ -O1 -g -std=c++17 -fsanitize=thread -Ithird_party/vulkan-headers/include -Isrc
|
||||||
|
// tests_loco/env_share_test.cpp -o env_share_test -lpthread
|
||||||
|
#include "../layer/frame_home_layer.cpp"
|
||||||
|
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int g_fails = 0;
|
||||||
|
void expect(bool ok, const char* what) {
|
||||||
|
std::printf("%s %s\n", ok ? "PASS" : "FAIL", what);
|
||||||
|
if (!ok) ++g_fails;
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the writer puts in a slot for frame f of generation g: every word tells which frame it belongs to.
|
||||||
|
float word(uint64_t f, int i) { return static_cast<float>((f % 100000) * 64 + static_cast<uint64_t>(i)); }
|
||||||
|
envshare::Slot slotFor(uint64_t f, uint32_t g) {
|
||||||
|
envshare::Slot s;
|
||||||
|
s.frame = f;
|
||||||
|
s.generation = g;
|
||||||
|
for (int e = 0; e < 2; ++e) for (int i = 0; i < 16; ++i) s.viewProj[e][i] = word(f, e * 16 + i);
|
||||||
|
for (int e = 0; e < 2; ++e) { for (int c = 0; c < 3; ++c) s.eyePos[e][c] = static_cast<float>(g); s.eyePos[e][3] = word(f, 32 + e); }
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
envshare::Motion motionFor(uint64_t f, uint32_t g) {
|
||||||
|
envshare::Motion m;
|
||||||
|
m.targetNs = 0; // (no carrying on: the block then holds the slot's pose exactly)
|
||||||
|
m.yawRate = static_cast<float>(g);
|
||||||
|
m.depth = 0;
|
||||||
|
m.velocity[0] = word(f, 40);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared between the writer and the reader beside the State: the generation the writer has made images for (the
|
||||||
|
// "Hello" the layer imports when it gets round to it), and stop.
|
||||||
|
struct Control {
|
||||||
|
std::atomic<uint32_t> helloGen{1};
|
||||||
|
std::atomic<uint32_t> stop{0};
|
||||||
|
std::atomic<uint64_t> published{0}, resizes{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Frame Home: publishes into the slots in turn (EnvExport::publish), a resize every `every` frames. `legacy`: as a
|
||||||
|
// v6 Frame Home did (plain stores, no sequence counts).
|
||||||
|
void writer(envshare::State* st, Control* c, bool legacy, uint32_t every) {
|
||||||
|
uint64_t frame = 0;
|
||||||
|
uint32_t gen = 1, next = 0;
|
||||||
|
while (!c->stop.load(std::memory_order_relaxed)) {
|
||||||
|
if (++frame % every == 0) { // createImages(): a new generation; the Hello goes out (the layer imports later)
|
||||||
|
++gen;
|
||||||
|
c->helloGen.store(gen);
|
||||||
|
c->resizes.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
const uint32_t k = next;
|
||||||
|
next = (next + 1) % envshare::kSlots;
|
||||||
|
const envshare::Slot s = slotFor(frame, gen);
|
||||||
|
const envshare::Motion m = motionFor(frame, gen);
|
||||||
|
if (legacy) {
|
||||||
|
envshare::storeWords(&st->slot[k], &s, sizeof s); // (word by word, no count: as torn as memcpy)
|
||||||
|
envshare::storeWords(&st->motion[k], &m, sizeof m);
|
||||||
|
} else {
|
||||||
|
envshare::writeSlot(st, k, s, m);
|
||||||
|
}
|
||||||
|
st->published.store(frame);
|
||||||
|
st->latest.store(k, std::memory_order_release);
|
||||||
|
st->publishNs.store(envshare::nowNs());
|
||||||
|
c->published.store(frame, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Result { uint64_t blocks = 0, torn = 0, wrongGen = 0, imports = 0, shownStale = 0; };
|
||||||
|
|
||||||
|
// The layer: the real liveSlot() + fillUbo() (frame_home_layer.cpp), importing each new generation a random while
|
||||||
|
// after its Hello (the compositor thread gets to it at its next submit). `legacy`: the layer's old reading (the shared
|
||||||
|
// slot as it is at fill time, its generation unchecked) instead, to show what this test catches.
|
||||||
|
Result reader(envshare::State* st, Control* c, double seconds, bool seqlocked, bool legacyRead, uint32_t seed) {
|
||||||
|
Result r;
|
||||||
|
static float ubo[envshare::kSlots][kUboRing * kUboStride / 4];
|
||||||
|
Gen fake;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> ol(g_oursMutex);
|
||||||
|
g_ours = Ours();
|
||||||
|
for (uint32_t i = 0; i < envshare::kSlots; ++i) g_ours.ubo[i].mapped = ubo[i];
|
||||||
|
g_ours.cur = &fake;
|
||||||
|
g_ours.state = st;
|
||||||
|
g_ours.stateSize = sizeof(envshare::State);
|
||||||
|
g_ours.hasMotion = true;
|
||||||
|
g_ours.seqlocked = seqlocked;
|
||||||
|
g_ours.generation = 1;
|
||||||
|
g_imported = true;
|
||||||
|
g_connected = true;
|
||||||
|
g_off = false;
|
||||||
|
}
|
||||||
|
std::mt19937 rng(seed);
|
||||||
|
uint64_t importAt = 0;
|
||||||
|
const uint64_t end = envshare::nowNs() + static_cast<uint64_t>(seconds * 1e9);
|
||||||
|
uint32_t ring = 0;
|
||||||
|
while (envshare::nowNs() < end) {
|
||||||
|
std::lock_guard<std::mutex> ol(g_oursMutex);
|
||||||
|
const uint32_t hello = c->helloGen.load();
|
||||||
|
if (hello != g_ours.generation) { // importPending(), a little later than the Hello came
|
||||||
|
const uint64_t now = envshare::nowNs();
|
||||||
|
if (!importAt) importAt = now + (rng() % 3000) * 1000ull; // 0..3 ms
|
||||||
|
if (now >= importAt) {
|
||||||
|
importAt = 0;
|
||||||
|
g_ours.generation = hello;
|
||||||
|
g_ours.lastGood = -1;
|
||||||
|
for (auto& s : g_ours.shown) s = envshare::Slot();
|
||||||
|
for (auto& m : g_ours.shownMotion) m = envshare::Motion();
|
||||||
|
++r.imports;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_latchNs = envshare::nowNs();
|
||||||
|
const int k = liveSlot();
|
||||||
|
if (k < 0) continue;
|
||||||
|
ring = (ring + 1) % kUboRing;
|
||||||
|
if (legacyRead) { // the old fillUbo: whatever the shared slot holds now
|
||||||
|
g_ours.shown[k] = envshare::Slot();
|
||||||
|
std::memcpy(&g_ours.shown[k], &st->slot[k], sizeof(envshare::Slot)); // (racy on purpose: legacy mode only)
|
||||||
|
g_ours.shownMotion[k] = envshare::Motion();
|
||||||
|
}
|
||||||
|
fillUbo(k, ring);
|
||||||
|
const float* d = ubo[k] + ring * kUboStride / 4;
|
||||||
|
++r.blocks;
|
||||||
|
// One frame throughout: viewProj and eyePos's w all of the frame word 0 says.
|
||||||
|
const uint64_t f = static_cast<uint64_t>(d[0]) / 64;
|
||||||
|
bool whole = true;
|
||||||
|
for (int i = 0; i < 32; ++i) whole &= d[i] == word(f, i);
|
||||||
|
whole &= d[32 + 3] == word(f, 32) && d[36 + 3] == word(f, 33);
|
||||||
|
if (!whole) ++r.torn;
|
||||||
|
// Of the generation whose images are drawn (eyePos.xyz carry the generation the writer rendered it for).
|
||||||
|
if (d[32] != static_cast<float>(g_ours.generation) || d[36] != static_cast<float>(g_ours.generation)) ++r.wrongGen;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> ol(g_oursMutex);
|
||||||
|
g_ours.cur = nullptr;
|
||||||
|
g_ours.state = nullptr;
|
||||||
|
g_imported = false;
|
||||||
|
for (auto& u : g_ours.ubo) u.mapped = nullptr;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
void report(const char* what, const Result& r, const Control& c) {
|
||||||
|
std::printf(" %s: %llu frames published, %llu resizes, %llu imports, %llu blocks filled: %llu torn, %llu of the wrong generation\n", what,
|
||||||
|
static_cast<unsigned long long>(c.published.load()), static_cast<unsigned long long>(c.resizes.load()),
|
||||||
|
static_cast<unsigned long long>(r.imports), static_cast<unsigned long long>(r.blocks), static_cast<unsigned long long>(r.torn),
|
||||||
|
static_cast<unsigned long long>(r.wrongGen));
|
||||||
|
}
|
||||||
|
|
||||||
|
// One run: the writer as a thread or a process.
|
||||||
|
Result run(double seconds, bool process, bool legacyWriter, bool seqlocked, bool legacyRead, Control*& ctl) {
|
||||||
|
void* mem = nullptr;
|
||||||
|
const size_t size = sizeof(envshare::State) + sizeof(Control);
|
||||||
|
int fd = -1;
|
||||||
|
if (process) {
|
||||||
|
fd = memfd_create("env_share_test", MFD_CLOEXEC);
|
||||||
|
if (fd < 0 || ftruncate(fd, static_cast<off_t>(size)) != 0) { std::perror("memfd"); std::exit(2); }
|
||||||
|
mem = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||||
|
} else {
|
||||||
|
mem = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
|
||||||
|
}
|
||||||
|
auto* st = new (mem) envshare::State();
|
||||||
|
st->show = 1;
|
||||||
|
ctl = new (static_cast<char*>(mem) + sizeof(envshare::State)) Control();
|
||||||
|
Result r;
|
||||||
|
if (process) {
|
||||||
|
const pid_t pid = fork();
|
||||||
|
if (pid == 0) { writer(st, ctl, legacyWriter, 300); _exit(0); }
|
||||||
|
r = reader(st, ctl, seconds, seqlocked, legacyRead, 7);
|
||||||
|
ctl->stop = 1;
|
||||||
|
int status = 0;
|
||||||
|
waitpid(pid, &status, 0);
|
||||||
|
} else {
|
||||||
|
std::thread w(writer, st, ctl, legacyWriter, 300);
|
||||||
|
r = reader(st, ctl, seconds, seqlocked, legacyRead, 7);
|
||||||
|
ctl->stop = 1;
|
||||||
|
w.join();
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
const double seconds = argc > 1 ? std::atof(argv[1]) : 3.0;
|
||||||
|
const bool process = argc > 2 && !std::strcmp(argv[2], "process");
|
||||||
|
const bool legacy = process && argc > 3 && !std::strcmp(argv[3], "legacy"); // (racy by design: processes only)
|
||||||
|
std::printf("env_share_test: %.1f s per run, writer as a %s\n", seconds, process ? "process" : "thread");
|
||||||
|
|
||||||
|
// Mismatched pairs: what the layer maps of each State a Frame Home might send.
|
||||||
|
{
|
||||||
|
const auto v5 = envshare::stateLayout(5, envshare::kStateV5Size), v6 = envshare::stateLayout(6, envshare::kStateV6Size),
|
||||||
|
v7 = envshare::stateLayout(6, sizeof(envshare::State)), shortV6 = envshare::stateLayout(6, envshare::kStateV6Size - 8),
|
||||||
|
shortV5 = envshare::stateLayout(5, envshare::kStateV5Size - 1), v5big = envshare::stateLayout(5, sizeof(envshare::State));
|
||||||
|
expect(v5.size == envshare::kStateV5Size && !v5.motion, "an older (v5) Frame Home's State: mapped to its end, no Motion");
|
||||||
|
expect(v6.size == envshare::kStateV6Size && v6.motion, "a v6 Frame Home's State: mapped to its end (no sequence counts)");
|
||||||
|
expect(v7.size == sizeof(envshare::State) && v7.motion, "this Frame Home's State: all of it");
|
||||||
|
expect(!shortV6.size && !shortV5.size, "a State shorter than its Hello says: not mapped at all");
|
||||||
|
expect(v5big.size == envshare::kStateV5Size, "a v5 Hello with a bigger State: only v5's part read");
|
||||||
|
envshare::State s;
|
||||||
|
expect(envshare::seqlocked(&s, sizeof s), "this Frame Home's State: read under its sequence counts");
|
||||||
|
s.version = 6;
|
||||||
|
expect(!envshare::seqlocked(&s, sizeof s), "a State that says v6: read the old way (two copies compared)");
|
||||||
|
s.version = envshare::kStateVersion;
|
||||||
|
expect(!envshare::seqlocked(&s, envshare::kStateV6Size), "a v6-sized mapping: no sequence counts read past its end");
|
||||||
|
// The Hello itself didn't change: an older layer (which takes v5 and v6 Hellos only) still pairs with this one.
|
||||||
|
envshare::Hello h;
|
||||||
|
expect(h.version == 6, "the Hello is still v6 (an older layer accepts it)");
|
||||||
|
expect(offsetof(envshare::State, motion) == envshare::kStateV5Size && offsetof(envshare::State, seq) == envshare::kStateV6Size,
|
||||||
|
"the v7 State only adds at its end (an older layer reads the same offsets)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// This layer with this Frame Home.
|
||||||
|
Control* c = nullptr;
|
||||||
|
Result r = run(seconds, process, false, true, false, c);
|
||||||
|
report("sequence counts + the layer's own copies", r, *c);
|
||||||
|
expect(r.blocks > 1000 && c->resizes.load() > 10 && r.imports > 10, "...hammered (many frames, resizes and imports)");
|
||||||
|
expect(r.torn == 0, "...no uniform block mixes two frames");
|
||||||
|
expect(r.wrongGen == 0, "...no block holds another generation's pose than the images drawn");
|
||||||
|
|
||||||
|
if (process) { // (a plain-store writer is a data race by design: not under TSan's eye in thread mode)
|
||||||
|
r = run(seconds, process, true, false, false, c);
|
||||||
|
report("an older (v6) Frame Home: no counts, copies compared", r, *c);
|
||||||
|
expect(r.wrongGen == 0, "...never another generation's pose (torn copies are rarer, not ruled out: reported)");
|
||||||
|
}
|
||||||
|
if (legacy) {
|
||||||
|
r = run(seconds, process, true, false, true, c);
|
||||||
|
report("the old layer (the shared slot read as it is at fill time)", r, *c);
|
||||||
|
expect(r.torn + r.wrongGen > 0, "...the test sees what the old way got wrong");
|
||||||
|
}
|
||||||
|
std::printf("%d failed\n", g_fails);
|
||||||
|
return g_fails ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ int main() {
|
|||||||
ck(loco.seated() == 0, "landing on a seat sits you in it");
|
ck(loco.seated() == 0, "landing on a seat sits you in it");
|
||||||
ck(std::fabs(e[0] - 2) < 1e-3f && std::fabs(e[2] + 1) < 1e-3f, "your head is over the seat", e[0], e[1], e[2]);
|
ck(std::fabs(e[0] - 2) < 1e-3f && std::fabs(e[2] + 1) < 1e-3f, "your head is over the seat", e[0], e[1], e[2]);
|
||||||
ck(std::fabs(e[1] - (0.45f + 0.78f)) < 1e-3f, "your eyes are at seated height over it (standing in the room)", e[1]);
|
ck(std::fabs(e[1] - (0.45f + 0.78f)) < 1e-3f, "your eyes are at seated height over it (standing in the room)", e[1]);
|
||||||
ck(std::fabs(facing()) < 0.02f, "sitting keeps the way you face (no turn to the seat's front)", facing());
|
ck(std::fabs(facing() - 1.5708f) < 0.02f, "sitting turns you to face the seat's front (its yaw)", facing());
|
||||||
for (int i = 0; i < 30; ++i) loco.update(1.0f / 90, head);
|
for (int i = 0; i < 30; ++i) loco.update(1.0f / 90, head);
|
||||||
eye(e);
|
eye(e);
|
||||||
ck(loco.seated() == 0 && std::fabs(e[0] - 2) < 1e-3f, "sitting still keeps you in the seat", e[0], e[1], e[2]);
|
ck(loco.seated() == 0 && std::fabs(e[0] - 2) < 1e-3f, "sitting still keeps you in the seat", e[0], e[1], e[2]);
|
||||||
@@ -45,6 +45,11 @@ int main() {
|
|||||||
Locomotion l2; l2.setWorld(w); l2.setOptions(1, 2, 0, 30, 2);
|
Locomotion l2; l2.setWorld(w); l2.setOptions(1, 2, 0, 30, 2);
|
||||||
float hd[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1.70f, 0, 1};
|
float hd[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1.70f, 0, 1};
|
||||||
l2.testSit(0, hd);
|
l2.testSit(0, hd);
|
||||||
|
l2.testInput(0, 1, 0, 0); // still held from before you sat: you stay
|
||||||
|
for (int i = 0; i < 30; ++i) l2.update(1.0f / 90, hd);
|
||||||
|
ck(l2.seated() == 0, "a stick already pushed when you sit doesn't stand you straight back up");
|
||||||
|
l2.testInput(0, 0, 0, 0);
|
||||||
|
l2.update(1.0f / 90, hd);
|
||||||
l2.testInput(0, 1, 0, 0);
|
l2.testInput(0, 1, 0, 0);
|
||||||
l2.update(1.0f / 90, hd);
|
l2.update(1.0f / 90, hd);
|
||||||
l2.testInput(0, 0, 0, 0);
|
l2.testInput(0, 0, 0, 0);
|
||||||
@@ -358,6 +363,90 @@ int main() {
|
|||||||
}
|
}
|
||||||
ck(seatAimed, "the teleport arc hits the seat's volume: it targets the seat", static_cast<float>(l3.testTargetSeat()));
|
ck(seatAimed, "the teleport arc hits the seat's volume: it targets the seat", static_cast<float>(l3.testTargetSeat()));
|
||||||
}
|
}
|
||||||
|
{ // seats face their way: whichever way your head points (and however the rig was turned), you end up facing the
|
||||||
|
// seat's yaw (0 = -Z, as Studio's arrow), head over the seat; standing up lands at its exit
|
||||||
|
const float kD = 0.0174533f;
|
||||||
|
auto wrap = [](float a) { return std::remainder(a, 6.2831853f); };
|
||||||
|
auto pose = [](float y, float p, float x, float h, float z, float* m) { // tracking head: yaw y, pitch p (column-major)
|
||||||
|
const float c = std::cos(y), s = std::sin(y), cp = std::cos(p), sp = std::sin(p);
|
||||||
|
const float v[16] = {c, 0, -s, 0, s * sp, cp, c * sp, 0, s * cp, -sp, c * cp, 0, x, h, z, 1};
|
||||||
|
std::copy(v, v + 16, m);
|
||||||
|
};
|
||||||
|
auto headOf = [](Locomotion& l, const float* h, float* e, float& yaw) {
|
||||||
|
float R[16]; l.rig(R);
|
||||||
|
for (int k = 0; k < 3; ++k) e[k] = R[k] * h[12] + R[4 + k] * h[13] + R[8 + k] * h[14] + R[12 + k];
|
||||||
|
float f[3];
|
||||||
|
for (int k = 0; k < 3; ++k) f[k] = -(R[k] * h[8] + R[4 + k] * h[9] + R[8 + k] * h[10]);
|
||||||
|
if (std::hypot(f[0], f[2]) < 0.2f) { // looking down: the top of the head points the way you face
|
||||||
|
for (int k = 0; k < 3; ++k) f[k] = R[k] * h[4] + R[4 + k] * h[5] + R[8 + k] * h[6];
|
||||||
|
}
|
||||||
|
yaw = std::atan2(-f[0], -f[2]);
|
||||||
|
};
|
||||||
|
HomeWorld w = flat;
|
||||||
|
WorldPoint sp; sp.name = "Spawn"; sp.yaw = 0.7f; w.spawns.push_back(sp); // the rig starts turned
|
||||||
|
const float seatYaw[4] = {0, 90 * kD, 180 * kD, -90 * kD};
|
||||||
|
for (float y : seatYaw) {
|
||||||
|
WorldPoint st; st.name = "Chair"; st.pos[0] = 2; st.pos[1] = 0.45f; st.pos[2] = -1; st.yaw = y;
|
||||||
|
st.exit[0] = 0.3f; st.exit[2] = -0.6f;
|
||||||
|
w.seats.push_back(st);
|
||||||
|
}
|
||||||
|
const float headYaw[4] = {0, 1.0f, -2.5f, 3.0f}, headPitch[3] = {0, -0.6f, -1.5f};
|
||||||
|
int n = 0, bad = 0;
|
||||||
|
float worst = 0;
|
||||||
|
for (int i = 0; i < 4; ++i)
|
||||||
|
for (float hy : headYaw)
|
||||||
|
for (float hp : headPitch) {
|
||||||
|
Locomotion l; l.setWorld(w);
|
||||||
|
float h[16]; pose(hy, hp, 0.4f, 1.7f, -0.3f, h);
|
||||||
|
l.testSit(i, h);
|
||||||
|
float e[3], fy;
|
||||||
|
headOf(l, h, e, fy);
|
||||||
|
const float d = std::fabs(wrap(fy - seatYaw[i]));
|
||||||
|
worst = std::max(worst, d);
|
||||||
|
const bool over = std::fabs(e[0] - 2) < 1e-3f && std::fabs(e[2] + 1) < 1e-3f && l.seated() == i;
|
||||||
|
l.testWalkOff(h);
|
||||||
|
float x[3], fy2;
|
||||||
|
headOf(l, h, x, fy2);
|
||||||
|
const float c = std::cos(seatYaw[i]), s = std::sin(seatYaw[i]);
|
||||||
|
const float wx = 2 + c * 0.3f + s * -0.6f, wz = -1 - s * 0.3f + c * -0.6f;
|
||||||
|
const bool out = l.seated() == -1 && std::fabs(x[0] - wx) < 0.02f && std::fabs(x[2] - wz) < 0.02f && std::fabs(x[1] - 1.7f) < 0.02f;
|
||||||
|
++n;
|
||||||
|
if (d > kD || !over || !out) {
|
||||||
|
++bad;
|
||||||
|
std::printf(" seat %.0f deg, head yaw %.2f pitch %.2f: facing %.1f deg off, head (%.2f %.2f), out (%.2f %.2f) want (%.2f %.2f)\n",
|
||||||
|
seatYaw[i] / kD, hy, hp, d / kD, e[0], e[2], x[0], x[2], wx, wz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ck(bad == 0, "seats at 0/90/180/-90 deg, head facing any way (level, down, straight down): you face the seat within 1 deg, over it, and stand up at its exit",
|
||||||
|
static_cast<float>(n), static_cast<float>(bad), worst / kD);
|
||||||
|
// the seat's front is where its exit default goes (half a metre ahead, -Z turned by yaw): sitting faces it
|
||||||
|
{
|
||||||
|
HomeWorld w2 = flat;
|
||||||
|
WorldPoint st; st.name = "Bench"; st.pos[0] = 0; st.pos[1] = 0.45f; st.pos[2] = -3; st.yaw = 90 * kD; w2.seats.push_back(st);
|
||||||
|
float m[12];
|
||||||
|
const float lh[3] = {-0.2f, 1.2f, 0};
|
||||||
|
handPose(lh, 0.2f, -0.65f, -3, m);
|
||||||
|
for (int mode = 1; mode <= 2; ++mode) { // the trigger: 1 a blink (here: a jump), 2 a dash
|
||||||
|
Locomotion l; l.setWorld(w2); l.setOptions(1, mode, 0, 30, 2);
|
||||||
|
l.testInput(0, 0, 0, 0);
|
||||||
|
l.testButtons(false, false, m); run(l, 1);
|
||||||
|
l.testButtons(false, true, m); run(l, 40);
|
||||||
|
float e[3], fy;
|
||||||
|
headOf(l, hd, e, fy);
|
||||||
|
ck(l.seated() == 0 && std::fabs(wrap(fy - st.yaw)) < kD && std::fabs(e[0]) < 1e-3f && std::fabs(e[2] + 3) < 1e-3f,
|
||||||
|
mode == 1 ? "the trigger on a seat (blink): you land facing its front" : "the trigger on a seat (dash): you land facing its front",
|
||||||
|
fy / kD, e[0], e[2]);
|
||||||
|
// the stick: up and out to half a metre in front of it, still facing its way
|
||||||
|
l.testButtons(false, false, nullptr);
|
||||||
|
l.testInput(0, 0, 0, 0); run(l, 1);
|
||||||
|
l.testInput(0, 1, 0, 0); run(l, 1);
|
||||||
|
l.testInput(0, 0, 0, 0); run(l, 60);
|
||||||
|
headOf(l, hd, e, fy);
|
||||||
|
ck(l.seated() == -1 && std::fabs(e[0] + 0.5f) < 0.05f && std::fabs(e[2] + 3) < 0.05f && std::fabs(wrap(fy - st.yaw)) < kD,
|
||||||
|
"...and the stick stands you up in front of it, facing the same way", e[0], e[2], fy / kD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
{ // a home with no world layer: floors and walls come from its drawn meshes (walkWorld -> geometryColliders)
|
{ // a home with no world layer: floors and walls come from its drawn meshes (walkWorld -> geometryColliders)
|
||||||
HslScene sc;
|
HslScene sc;
|
||||||
auto mesh = [&](const char* name, std::vector<float> p) {
|
auto mesh = [&](const char* name, std::vector<float> p) {
|
||||||
@@ -405,6 +494,96 @@ int main() {
|
|||||||
float t;
|
float t;
|
||||||
ck(l.seatOnRay(o, d, 10, t) == 0 && t < 3.0f, "a ray stopped by the chair's own (big) collider points at the chair", t);
|
ck(l.seatOnRay(o, d, 10, t) == 0 && t < 3.0f, "a ray stopped by the chair's own (big) collider points at the chair", t);
|
||||||
}
|
}
|
||||||
|
// ── What the compositor layer carries a repeated frame on by (env_share.h Motion), and the system-mode blink ──
|
||||||
|
{ // the turn rate: a smooth turn's, never a snap's
|
||||||
|
Locomotion l; l.setWorld(flat); l.setOptions(1, 1, 1, 30, 2);
|
||||||
|
l.testInput(0, 0, 1, 0); run(l, 1);
|
||||||
|
const float smooth = l.turnRate();
|
||||||
|
l.testInput(0, 0, 0, 0); run(l, 1);
|
||||||
|
const float stopped = l.turnRate();
|
||||||
|
Locomotion s; s.setWorld(flat); s.setOptions(1, 1, 0, 30, 2);
|
||||||
|
float R0[16], R1[16]; s.rig(R0);
|
||||||
|
s.testInput(0, 0, -1, 0); run(s, 1); s.rig(R1);
|
||||||
|
ck(std::fabs(smooth + 2.0944f) < 1e-4f && stopped == 0 && s.turnRate() == 0 && std::fabs(R1[0] - R0[0]) > 0.1f,
|
||||||
|
"turn rate: a full smooth turn right is -120 degrees/s, let go 0; a snap turns but carries no rate", smooth, stopped, s.turnRate());
|
||||||
|
}
|
||||||
|
{ // a frame's yaw is linear in its target time: two 60 Hz steps turn as far as four 120 Hz ones
|
||||||
|
Locomotion a; a.setWorld(flat); a.setOptions(1, 1, 1, 30, 2);
|
||||||
|
Locomotion b; b.setWorld(flat); b.setOptions(1, 1, 1, 30, 2);
|
||||||
|
a.testInput(0, 0, 1, 0); b.testInput(0, 0, 1, 0);
|
||||||
|
for (int i = 0; i < 2; ++i) a.update(1.0f / 60, hd);
|
||||||
|
for (int i = 0; i < 4; ++i) b.update(1.0f / 120, hd);
|
||||||
|
float Ra[16], Rb[16]; a.rig(Ra); b.rig(Rb);
|
||||||
|
ck(std::fabs(std::atan2(Ra[8], Ra[10]) - std::atan2(Rb[8], Rb[10])) < 1e-5f, "smooth turn: half rate steps land where full rate's do",
|
||||||
|
std::atan2(Ra[8], Ra[10]), std::atan2(Rb[8], Rb[10]));
|
||||||
|
}
|
||||||
|
{ // system mode's own blink: fades out over 80 ms, jumps while black, fades back in over 120 ms
|
||||||
|
const float aim[12] = {1, 0, 0, 0.2f, 0, 0.866f, 0.5f, 1.2f, 0, -0.5f, 0.866f, 0};
|
||||||
|
Locomotion l; l.setWorld(flat); l.setOptions(1, 1, 0, 30, 2); l.setOwnFade(true);
|
||||||
|
float t[3], e0[3], e1[3];
|
||||||
|
l.testInput(0, 0, 0, 1, aim); run(l, 3); l.testTarget(t); eyeOf(l, e0);
|
||||||
|
ck(l.blinkLevel() == 1, "aiming: the view is open", l.blinkLevel());
|
||||||
|
l.testInput(0, 0, 0, 0); run(l, 1); // released
|
||||||
|
float lv = l.blinkLevel(), lowest = lv;
|
||||||
|
bool fell = true;
|
||||||
|
int frames = 1;
|
||||||
|
eyeOf(l, e1);
|
||||||
|
bool stayed = std::hypot(e1[0] - e0[0], e1[2] - e0[2]) < 1e-4f;
|
||||||
|
while (l.blinkLevel() > 0 && frames < 20) {
|
||||||
|
run(l, 1); ++frames;
|
||||||
|
fell &= l.blinkLevel() <= lv;
|
||||||
|
lv = l.blinkLevel();
|
||||||
|
lowest = std::min(lowest, lv);
|
||||||
|
eyeOf(l, e1);
|
||||||
|
if (lv > 0) stayed &= std::hypot(e1[0] - e0[0], e1[2] - e0[2]) < 1e-4f;
|
||||||
|
}
|
||||||
|
eyeOf(l, e1);
|
||||||
|
const bool landedBlack = l.blinkLevel() == 0 && std::fabs(e1[0] - t[0]) < 1e-3f && std::fabs(e1[2] - t[2]) < 1e-3f;
|
||||||
|
ck(fell && stayed && landedBlack && frames >= 6 && frames <= 8,
|
||||||
|
"own blink: darkens over ~80 ms, you stay put until it's black, and land then", frames * dt, e1[0] - t[0], e1[2] - t[2]);
|
||||||
|
int back = 0;
|
||||||
|
while (l.blinkLevel() < 1 && back < 30) { run(l, 1); ++back; }
|
||||||
|
ck(back >= 9 && back <= 11, "own blink: back to full brightness ~120 ms after landing", back * dt);
|
||||||
|
}
|
||||||
|
{ // the layer's carry-on (env_ps.frag) matches the rig: a point seen now, turned back by turnRate * dt and walked
|
||||||
|
// back by the rig's walk (main.cpp's velocity, the rig's rotation transposed), is where the last frame saw it
|
||||||
|
Locomotion l; l.setWorld(flat); l.setOptions(1, 1, 1, 30, 2);
|
||||||
|
l.testInput(0.3f, 1, -0.8f, 0); run(l, 30); // walking forward-right, turning right
|
||||||
|
float R0[16], R1[16];
|
||||||
|
l.rig(R0); run(l, 1); l.rig(R1);
|
||||||
|
const float P[3] = {1.0f, 1.2f, -2.5f}; // a point in the home
|
||||||
|
auto toTracking = [&](const float* R, const float* w, float* o) { // R^-1 * w (rigid)
|
||||||
|
const float d[3] = {w[0] - R[12], w[1] - R[13], w[2] - R[14]};
|
||||||
|
for (int k = 0; k < 3; ++k) o[k] = R[k * 4] * d[0] + R[k * 4 + 1] * d[1] + R[k * 4 + 2] * d[2];
|
||||||
|
};
|
||||||
|
float p0[3], p1[3];
|
||||||
|
toTracking(R0, P, p0); toTracking(R1, P, p1);
|
||||||
|
const float* eyeT = &hd[12];
|
||||||
|
float now[3], then[3];
|
||||||
|
for (int k = 0; k < 3; ++k) { now[k] = p1[k] - eyeT[k]; then[k] = p0[k] - eyeT[k]; }
|
||||||
|
const float D = std::sqrt(now[0] * now[0] + now[1] * now[1] + now[2] * now[2]);
|
||||||
|
// the rig's walk this frame, as main.cpp works it out (where the head's spot in the room went)
|
||||||
|
float dw[3], v[3];
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
dw[k] = R1[k] * eyeT[0] + R1[4 + k] * eyeT[1] + R1[8 + k] * eyeT[2] + R1[12 + k] - (R0[k] * eyeT[0] + R0[4 + k] * eyeT[1] + R0[8 + k] * eyeT[2] + R0[12 + k]);
|
||||||
|
for (int k = 0; k < 3; ++k) v[k] = R0[k * 4] * dw[0] + R0[k * 4 + 1] * dw[1] + R0[k * 4 + 2] * dw[2]; // (the shown frame's rig)
|
||||||
|
const float a = l.turnRate() * dt, c = std::cos(a), s = std::sin(a);
|
||||||
|
float d[3] = {now[0] / D, now[1] / D, now[2] / D};
|
||||||
|
const float rd[3] = {c * d[0] + s * d[2], d[1], -s * d[0] + c * d[2]};
|
||||||
|
float q[3] = {rd[0] * D + v[0], rd[1] * D + v[1], rd[2] * D + v[2]};
|
||||||
|
const float ql = std::sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2]), tl = std::sqrt(then[0] * then[0] + then[1] * then[1] + then[2] * then[2]);
|
||||||
|
const float err = std::acos(std::clamp((q[0] * then[0] + q[1] * then[1] + q[2] * then[2]) / (ql * tl), -1.0f, 1.0f));
|
||||||
|
const float moved = std::acos(std::clamp((now[0] * then[0] + now[1] * then[1] + now[2] * then[2]) / (D * tl), -1.0f, 1.0f));
|
||||||
|
ck(l.turnRate() != 0 && moved > 0.01f && err < 1e-3f, "carry-on: turning back and walking back a frame lands on where it was seen (radians)", moved, err);
|
||||||
|
}
|
||||||
|
{ // the depth guess: what's in front of the eyes
|
||||||
|
HomeWorld w = flat;
|
||||||
|
w.colliders.push_back(quadC(WorldCollider::Wall, {-5, 0, -3, 5, 0, -3, 5, 3, -3, -5, 3, -3}));
|
||||||
|
Locomotion l; l.setWorld(w);
|
||||||
|
Locomotion o; o.setWorld(flat);
|
||||||
|
ck(std::fabs(l.viewDepth(hd) - 3) < 0.05f && o.viewDepth(hd) == 8.0f, "view depth: a wall 3 m ahead is 3 m; nothing ahead, 8",
|
||||||
|
l.viewDepth(hd), o.viewDepth(hd));
|
||||||
|
}
|
||||||
std::printf("%d failed\n", fails);
|
std::printf("%d failed\n", fails);
|
||||||
return fails;
|
return fails;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// spv_check.h: every shader Frame Home ships passes, and damaged ones (cut short, an instruction running past the end,
|
||||||
|
// ids past the bound or defined twice, an unterminated string, a function never ended) are caught before the driver.
|
||||||
|
// Files given on the command line are checked too (all must pass) and timed: tests_loco/spv_check_test *.spv
|
||||||
|
#include "spv_check.h"
|
||||||
|
#include "clay_spirv.h"
|
||||||
|
#include "debug_spirv.h"
|
||||||
|
#include "guide_spirv.h"
|
||||||
|
#include "selection_spirv.h"
|
||||||
|
#include "splat_spirv.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <iterator>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
static int fails = 0;
|
||||||
|
static void expect(bool ok, const char* what) {
|
||||||
|
std::printf("%s %s\n", ok ? "PASS" : "FAIL", what);
|
||||||
|
if (!ok) ++fails;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t N> static std::vector<uint32_t> words(const uint32_t (&a)[N]) { return std::vector<uint32_t>(a, a + N); }
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
const std::vector<std::vector<uint32_t>> shipped = {
|
||||||
|
words(kClayVertSpirv), words(kClayFragSpirv), words(kClayFragCutoutSpirv), words(kClaySkinnedVertSpirv), words(kClayVColorVertSpirv),
|
||||||
|
words(kDebugVertSpirv), words(kDebugFragSpirv), words(kGuideVertSpirv), words(kGuideFragSpirv), words(kSelectionVertSpirv),
|
||||||
|
words(kSelectionFragSpirv), words(kSplatVertSpirv), words(kSplatFragSpirv)};
|
||||||
|
bool all = true;
|
||||||
|
std::string why;
|
||||||
|
for (const auto& s : shipped) {
|
||||||
|
if (!spvcheck::check(s, &why)) { all = false; std::printf(" shipped shader refused: %s\n", why.c_str()); }
|
||||||
|
}
|
||||||
|
expect(all, "every built-in shader passes");
|
||||||
|
|
||||||
|
const std::vector<uint32_t>& base = shipped[1]; // the Clay fragment shader
|
||||||
|
auto refused = [&](std::vector<uint32_t> code, const char* what) {
|
||||||
|
std::string w;
|
||||||
|
const bool ok = spvcheck::check(code, &w);
|
||||||
|
expect(!ok, what);
|
||||||
|
if (!ok) std::printf(" (%s)\n", w.c_str());
|
||||||
|
};
|
||||||
|
refused({}, "empty: refused");
|
||||||
|
refused(std::vector<uint32_t>(base.begin(), base.begin() + 4), "cut inside the header: refused");
|
||||||
|
{ auto c = base; c[0] = 0x03022307u; refused(c, "byte-swapped magic: refused"); }
|
||||||
|
{ auto c = base; c[1] = 0x00020000u; refused(c, "SPIR-V 2.0: refused"); }
|
||||||
|
{ auto c = base; c[3] = 0; refused(c, "id bound 0: refused"); }
|
||||||
|
{ auto c = base; c[3] = 0xFFFFFFFFu; refused(c, "id bound 4G: refused"); }
|
||||||
|
{ auto c = base; c[3] = 2; refused(c, "ids past a too-small bound: refused"); }
|
||||||
|
{ auto c = base; c.resize(c.size() - 1); refused(c, "last instruction cut short: refused"); }
|
||||||
|
{ auto c = base; c[5] = (0xFFFFu << 16) | (c[5] & 0xFFFFu); refused(c, "an instruction longer than the module: refused"); }
|
||||||
|
{ auto c = base; c[5] = c[5] & 0xFFFFu; refused(c, "an instruction of 0 words: refused"); }
|
||||||
|
{ // the first result id another instruction defines again: find two instructions with results, copy the id
|
||||||
|
auto c = base;
|
||||||
|
std::vector<size_t> withResult;
|
||||||
|
for (size_t i = 5; i < c.size(); i += c[i] >> 16)
|
||||||
|
if (spvcheck::detail::opFlags(c[i] & 0xFFFF) == 1) withResult.push_back(i); // (a result, no type: at word 1)
|
||||||
|
expect(withResult.size() >= 2, "(the test shader has two results to make clash)");
|
||||||
|
if (withResult.size() >= 2) {
|
||||||
|
c[withResult[1] + 1] = c[withResult[0] + 1];
|
||||||
|
refused(c, "a result id defined twice: refused");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{ // the OpEntryPoint's name with its nul overwritten
|
||||||
|
auto c = base;
|
||||||
|
for (size_t i = 5; i < c.size(); i += c[i] >> 16)
|
||||||
|
if ((c[i] & 0xFFFF) == 15) { for (uint32_t k = 3; k < (c[i] >> 16); ++k) c[i + k] = 0x41414141u; break; }
|
||||||
|
refused(c, "an entry point name never terminated: refused");
|
||||||
|
}
|
||||||
|
{ // the module ends inside its function (OpFunctionEnd dropped)
|
||||||
|
auto c = base;
|
||||||
|
if ((c.back() & 0xFFFF) == 56) { c.pop_back(); refused(c, "a function never ended: refused"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shaders given as files (a home's, SteamVR's): all must pass. Timed.
|
||||||
|
size_t n = 0, totalWords = 0, bad = 0;
|
||||||
|
double ms = 0;
|
||||||
|
for (int a = 1; a < argc; ++a) {
|
||||||
|
FILE* f = std::fopen(argv[a], "rb");
|
||||||
|
if (!f) continue;
|
||||||
|
std::vector<uint32_t> c;
|
||||||
|
uint32_t w;
|
||||||
|
while (std::fread(&w, 4, 1, f) == 1) c.push_back(w);
|
||||||
|
std::fclose(f);
|
||||||
|
const auto t0 = std::chrono::steady_clock::now();
|
||||||
|
const bool ok = spvcheck::check(c, &why);
|
||||||
|
ms += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count();
|
||||||
|
++n;
|
||||||
|
totalWords += c.size();
|
||||||
|
if (!ok) { ++bad; std::printf(" %s refused: %s\n", argv[a], why.c_str()); }
|
||||||
|
}
|
||||||
|
if (n) {
|
||||||
|
std::printf("checked %zu files, %.1f MB of SPIR-V, in %.2f ms\n", n, totalWords * 4 / 1e6, ms);
|
||||||
|
expect(bad == 0, "every given shader passes");
|
||||||
|
}
|
||||||
|
std::printf("%d failed\n", fails);
|
||||||
|
return fails ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -1533,7 +1533,9 @@ static cgltf_size cgltf_calc_index_bound(cgltf_buffer_view* buffer_view, cgltf_s
|
|||||||
case cgltf_component_type_r_16u:
|
case cgltf_component_type_r_16u:
|
||||||
for (size_t i = 0; i < count; ++i)
|
for (size_t i = 0; i < count; ++i)
|
||||||
{
|
{
|
||||||
cgltf_size v = ((unsigned short*)data)[i];
|
unsigned short u; /* Frame Studio: memcpy, the view's offset may not be 2-aligned (UB, a fault on some CPUs) */
|
||||||
|
memcpy(&u, data + i * sizeof(u), sizeof(u));
|
||||||
|
cgltf_size v = u;
|
||||||
bound = bound > v ? bound : v;
|
bound = bound > v ? bound : v;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1541,7 +1543,9 @@ static cgltf_size cgltf_calc_index_bound(cgltf_buffer_view* buffer_view, cgltf_s
|
|||||||
case cgltf_component_type_r_32u:
|
case cgltf_component_type_r_32u:
|
||||||
for (size_t i = 0; i < count; ++i)
|
for (size_t i = 0; i < count; ++i)
|
||||||
{
|
{
|
||||||
cgltf_size v = ((unsigned int*)data)[i];
|
unsigned int u;
|
||||||
|
memcpy(&u, data + i * sizeof(u), sizeof(u));
|
||||||
|
cgltf_size v = u;
|
||||||
bound = bound > v ? bound : v;
|
bound = bound > v ? bound : v;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -83,6 +83,13 @@ struct AudioPlayer {
|
|||||||
sr, chs, (unsigned long long)frames, frames / (float)(sr > 0 ? sr : 1));
|
sr, chs, (unsigned long long)frames, frames / (float)(sr > 0 ? sr : 1));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Stops the device (no audio thread running, nothing mixed) until resumed; the loop carries on where it was.
|
||||||
|
bool paused = false;
|
||||||
|
void pause(bool on) {
|
||||||
|
if (!ok || on == paused) return;
|
||||||
|
paused = on;
|
||||||
|
if (on) ma_device_stop(&device); else ma_device_start(&device);
|
||||||
|
}
|
||||||
|
|
||||||
// Interleaved s16 at any channel count -> stereo. 4+ channels: first-order ambisonics (ACN W,Y,Z,X: W carries most
|
// Interleaved s16 at any channel count -> stereo. 4+ channels: first-order ambisonics (ACN W,Y,Z,X: W carries most
|
||||||
// energy; the homes' beds, Clay and HSL alike) -> L/R = W +- 0.7 Y; 5.1 / 7.1 (FL FR C LFE SL SR [BL BR], a sound
|
// energy; the homes' beds, Clay and HSL alike) -> L/R = W +- 0.7 Y; 5.1 / 7.1 (FL FR C LFE SL SR [BL BR], a sound
|
||||||
@@ -217,6 +224,6 @@ struct AudioPlayer {
|
|||||||
gain = vol;
|
gain = vol;
|
||||||
return startPCM(out.data(), (ma_uint64)(out.size() / chs), chs, sr, evenOut);
|
return startPCM(out.data(), (ma_uint64)(out.size() / chs), chs, sr, evenOut);
|
||||||
}
|
}
|
||||||
void stop() { if (ok) { ma_device_uninit(&device); ok = false; } }
|
void stop() { if (ok) { ma_device_uninit(&device); ok = false; } paused = false; }
|
||||||
~AudioPlayer() { stop(); }
|
~AudioPlayer() { stop(); }
|
||||||
};
|
};
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <exception>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
@@ -32,6 +33,9 @@ public:
|
|||||||
std::unique_lock<std::mutex> g(m_);
|
std::unique_lock<std::mutex> g(m_);
|
||||||
done_.wait(g, [&] { return left_ == 0; });
|
done_.wait(g, [&] { return left_ == 0; });
|
||||||
fn_ = nullptr;
|
fn_ = nullptr;
|
||||||
|
// An item that threw (a damaged file's bad_alloc, a parser's throw): rethrown here, once every thread is done
|
||||||
|
// with fn. Thrown on a pool thread it ended the process; on this one it left the pool running a dead fn.
|
||||||
|
if (error_) { std::exception_ptr e = error_; error_ = nullptr; g.unlock(); std::rethrow_exception(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
~Pool() {
|
~Pool() {
|
||||||
@@ -48,7 +52,14 @@ private:
|
|||||||
}
|
}
|
||||||
void work() {
|
void work() {
|
||||||
inside_ = true;
|
inside_ = true;
|
||||||
for (size_t i; (i = next_.fetch_add(1)) < n_;) (*fn_)(i);
|
for (size_t i; (i = next_.fetch_add(1)) < n_;) {
|
||||||
|
try {
|
||||||
|
(*fn_)(i);
|
||||||
|
} catch (...) {
|
||||||
|
std::lock_guard<std::mutex> g(m_);
|
||||||
|
if (!error_) error_ = std::current_exception();
|
||||||
|
}
|
||||||
|
}
|
||||||
inside_ = false;
|
inside_ = false;
|
||||||
}
|
}
|
||||||
void loop() {
|
void loop() {
|
||||||
@@ -74,6 +85,7 @@ private:
|
|||||||
std::atomic<size_t> next_{0};
|
std::atomic<size_t> next_{0};
|
||||||
uint64_t gen_ = 0;
|
uint64_t gen_ = 0;
|
||||||
bool stop_ = false;
|
bool stop_ = false;
|
||||||
|
std::exception_ptr error_; // the first item of this forEach that threw (under m_)
|
||||||
static inline thread_local bool inside_ = false;
|
static inline thread_local bool inside_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ public:
|
|||||||
std::string asString() const { return strVal; }
|
std::string asString() const { return strVal; }
|
||||||
int64_t asInt() const {
|
int64_t asInt() const {
|
||||||
if (type == Type::Int) return intVal;
|
if (type == Type::Int) return intVal;
|
||||||
if (type == Type::Float) return (int64_t)floatVal;
|
if (type == Type::Float) // (NaN / out of range: undefined as a cast; 0 or the nearest end instead)
|
||||||
|
return floatVal >= 9.2e18 ? INT64_MAX : floatVal <= -9.2e18 ? INT64_MIN : floatVal == floatVal ? (int64_t)floatVal : 0;
|
||||||
// Asset IDs may be quoted ("targetId": "2719744159" in community shellconfig/HSTF) —
|
// Asset IDs may be quoted ("targetId": "2719744159" in community shellconfig/HSTF) —
|
||||||
// parse unsigned so u32 targetIds above INT_MAX survive the (u32) cast at call sites.
|
// parse unsigned so u32 targetIds above INT_MAX survive the (u32) cast at call sites.
|
||||||
if (type == Type::String) return (int64_t)std::strtoull(strVal.c_str(), nullptr, 10);
|
if (type == Type::String) return (int64_t)std::strtoull(strVal.c_str(), nullptr, 10);
|
||||||
@@ -154,6 +155,7 @@ private:
|
|||||||
floatVal = (double)intVal + frac;
|
floatVal = (double)intVal + frac;
|
||||||
}
|
}
|
||||||
if (peek() == 'e' || peek() == 'E') {
|
if (peek() == 'e' || peek() == 'E') {
|
||||||
|
if (!isFloat) floatVal = (double)intVal; // "1e0": no '.', so the value so far is the integer part
|
||||||
isFloat = true;
|
isFloat = true;
|
||||||
next();
|
next();
|
||||||
bool eNeg = (peek() == '-');
|
bool eNeg = (peek() == '-');
|
||||||
@@ -161,7 +163,6 @@ private:
|
|||||||
double exp = 0;
|
double exp = 0;
|
||||||
while (p < end && *p >= '0' && *p <= '9') { exp = exp * 10 + (*p - '0'); ++p; }
|
while (p < end && *p >= '0' && *p <= '9') { exp = exp * 10 + (*p - '0'); ++p; }
|
||||||
floatVal = floatVal * std::pow(10.0, eNeg ? -exp : exp);
|
floatVal = floatVal * std::pow(10.0, eNeg ? -exp : exp);
|
||||||
if (!isFloat) { floatVal = (double)intVal; isFloat = true; }
|
|
||||||
}
|
}
|
||||||
if (p == start) throw std::runtime_error("Expected number");
|
if (p == start) throw std::runtime_error("Expected number");
|
||||||
|
|
||||||
|
|||||||
@@ -463,3 +463,31 @@ inline bool parseRendMesh(const std::vector<u8>& data,
|
|||||||
|
|
||||||
return totalV > 0 && !indices.empty();
|
return totalV > 0 && !indices.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The material a mesh was exported with: LOD[0] -> Part[0] field[3] is a whole nested MATLMATL buffer (shader +
|
||||||
|
// textures + matParams). libshell draws a mesh whose entity has no MaterialPlatformComponent with it (Horror's
|
||||||
|
// SM_skybox: unlit + skybox_d.png; the hero lantern's candleFlame: candleflame.surface). Empty if it has none.
|
||||||
|
inline std::vector<u8> rendMeshPartMaterial(const std::vector<u8>& data) {
|
||||||
|
const u64 SZ = data.size();
|
||||||
|
if (SZ < 14 || SZ > 0xFFFFFFF0u || memcmp(data.data() + 4, "MESH", 4) != 0) return {};
|
||||||
|
auto rd32 = [&](u64 o) -> u32 { return o + 4 <= SZ ? loadUnaligned<u32>(data.data() + o) : 0; };
|
||||||
|
auto field = [&](u64 tbl, u32 fi) -> u64 { // absolute position of a table field, 0 if absent
|
||||||
|
if (tbl + 4 > SZ) return 0;
|
||||||
|
const int64_t vt = (int64_t)tbl - loadUnaligned<i32>(data.data() + tbl);
|
||||||
|
if (vt < 0 || (u64)vt + 4 > SZ) return 0;
|
||||||
|
const u64 slot = (u64)vt + 4 + (u64)fi * 2;
|
||||||
|
if (slot + 2 > (u64)vt + loadUnaligned<u16>(data.data() + vt) || slot + 2 > SZ) return 0;
|
||||||
|
const u16 fo = loadUnaligned<u16>(data.data() + slot);
|
||||||
|
return fo ? tbl + fo : 0;
|
||||||
|
};
|
||||||
|
auto deref = [&](u64 at) -> u64 { const u32 o = rd32(at); return at && o ? at + o : 0; }; // uoffset -> target
|
||||||
|
const u64 lods = deref(field(rd32(0), 1));
|
||||||
|
const u64 lod = lods && rd32(lods) ? deref(lods + 4) : 0;
|
||||||
|
const u64 parts = lod ? deref(field(lod, 0)) : 0;
|
||||||
|
const u64 part = parts && rd32(parts) ? deref(parts + 4) : 0;
|
||||||
|
const u64 mat = part ? deref(field(part, 3)) : 0;
|
||||||
|
if (!mat) return {};
|
||||||
|
const u64 n = rd32(mat);
|
||||||
|
if (n < 68 || mat + 4 + n > SZ || memcmp(data.data() + mat + 8, "MATL", 4) != 0) return {};
|
||||||
|
return std::vector<u8>(data.begin() + (size_t)(mat + 4), data.begin() + (size_t)(mat + 4 + n));
|
||||||
|
}
|
||||||
@@ -82,8 +82,8 @@ inline void rs_buildBind(RendSkel& s) {
|
|||||||
// i16 parent, u32 nameLen, name, T f32x3, Q(w,x,y,z) f32x4, S f32x1. HIERARCHICAL (parents precede children),
|
// i16 parent, u32 nameLen, name, T f32x3, Q(w,x,y,z) f32x4, S f32x1. HIERARCHICAL (parents precede children),
|
||||||
// e.g. whale01: global_jnt(-1)->root->chest->{head, l/r_shoulder->elbow->wrist} + tailStart->tail1->tail2->tailEnd.
|
// e.g. whale01: global_jnt(-1)->root->chest->{head, l/r_shoulder->elbow->wrist} + tailStart->tail1->tail2->tailEnd.
|
||||||
inline bool parseRendSkelV203(const std::vector<u8>& d, RendSkel& s) {
|
inline bool parseRendSkelV203(const std::vector<u8>& d, RendSkel& s) {
|
||||||
auto rd32=[&](size_t o)->u32{ return o+4<=d.size()? *(const u32*)(d.data()+o):0u; };
|
auto rd32=[&](size_t o)->u32{ return o+4<=d.size()? loadUnaligned<u32>(d.data()+o):0u; }; // (any offset: no aligned casts)
|
||||||
auto rd16=[&](size_t o)->u16{ return o+2<=d.size()? *(const u16*)(d.data()+o):(u16)0; };
|
auto rd16=[&](size_t o)->u16{ return o+2<=d.size()? loadUnaligned<u16>(d.data()+o):(u16)0; };
|
||||||
auto rdf =[&](size_t o){ float f=0; if(o+4<=d.size()) memcpy(&f,d.data()+o,4); return f; };
|
auto rdf =[&](size_t o){ float f=0; if(o+4<=d.size()) memcpy(&f,d.data()+o,4); return f; };
|
||||||
size_t o = 8; // after magic + version
|
size_t o = 8; // after magic + version
|
||||||
u32 nameLen = rd32(o); o += 4 + nameLen; // skeleton name ("Skeleton_0")
|
u32 nameLen = rd32(o); o += 4 + nameLen; // skeleton name ("Skeleton_0")
|
||||||
@@ -119,8 +119,8 @@ inline bool parseRendSkel(const std::vector<u8>& d, RendSkel& s) {
|
|||||||
// V203 vista format (parent-first, u16 jointCount) — the whale/turtle skeletons. Try it first; it
|
// V203 vista format (parent-first, u16 jointCount) — the whale/turtle skeletons. Try it first; it
|
||||||
// returns false cleanly if the bytes are the older nuxd layout, which the code below then handles.
|
// returns false cleanly if the bytes are the older nuxd layout, which the code below then handles.
|
||||||
{ RendSkel v; if (parseRendSkelV203(d, v) && v.joints.size() >= 2) { s = std::move(v); return true; } }
|
{ RendSkel v; if (parseRendSkelV203(d, v) && v.joints.size() >= 2) { s = std::move(v); return true; } }
|
||||||
auto rd32=[&](size_t o)->u32{ return o+4<=d.size()? *(const u32*)(d.data()+o):0u; };
|
auto rd32=[&](size_t o)->u32{ return o+4<=d.size()? loadUnaligned<u32>(d.data()+o):0u; };
|
||||||
auto rd16=[&](size_t o)->u16{ return o+2<=d.size()? *(const u16*)(d.data()+o):(u16)0; };
|
auto rd16=[&](size_t o)->u16{ return o+2<=d.size()? loadUnaligned<u16>(d.data()+o):(u16)0; };
|
||||||
auto rdf=[&](size_t o){ float f=0; if(o+4<=d.size()) memcpy(&f,d.data()+o,4); return f; };
|
auto rdf=[&](size_t o){ float f=0; if(o+4<=d.size()) memcpy(&f,d.data()+o,4); return f; };
|
||||||
size_t o = 8; // after magic+ver
|
size_t o = 8; // after magic+ver
|
||||||
u32 nameLen = rd32(o); o += 4 + nameLen; // skeleton name
|
u32 nameLen = rd32(o); o += 4 + nameLen; // skeleton name
|
||||||
@@ -175,7 +175,7 @@ inline bool parseRendClip(const std::vector<u8>& d, int nJoints, RendClip& c) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (d.size() < 200) return false;
|
if (d.size() < 200) return false;
|
||||||
auto rd32=[&](size_t o)->u32{ return o+4<=d.size()? *(const u32*)(d.data()+o):0u; };
|
auto rd32=[&](size_t o)->u32{ return o+4<=d.size()? loadUnaligned<u32>(d.data()+o):0u; };
|
||||||
auto rdf=[&](size_t o){ float f=0; if(o+4<=d.size()) memcpy(&f,d.data()+o,4); return f; };
|
auto rdf=[&](size_t o){ float f=0; if(o+4<=d.size()) memcpy(&f,d.data()+o,4); return f; };
|
||||||
if (rd32(0) != 0xA34912B6u) return false; // hzanim clip magic
|
if (rd32(0) != 0xA34912B6u) return false; // hzanim clip magic
|
||||||
int hdrJoints = (int)rd32(8);
|
int hdrJoints = (int)rd32(8);
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ struct RendtxtrInfo {
|
|||||||
// Cubemaps (skybox panoramas, IBL reflection maps): f7 = 6 faces, f6 = mips; the payload is
|
// Cubemaps (skybox panoramas, IBL reflection maps): f7 = 6 faces, f6 = mips; the payload is
|
||||||
// MIP-MAJOR (all six faces of mip0, then all six of mip1, ...). faces = 1 for plain 2D textures.
|
// MIP-MAJOR (all six faces of mip0, then all six of mip1, ...). faces = 1 for plain 2D textures.
|
||||||
u32 faces = 1;
|
u32 faces = 1;
|
||||||
|
// Uncompressed payload (header f2 = 4: RGBA8, 5: RGBA16F): bytes per texel, else 0 (ASTC). rawDataLen is then
|
||||||
|
// mip 0 alone (w*h*rawBpp), which every decoder takes as raw texels.
|
||||||
|
u32 rawBpp = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── FlatBuffer vtable helpers (RENDTXTR header layout differs per env, so resolve
|
// ── FlatBuffer vtable helpers (RENDTXTR header layout differs per env, so resolve
|
||||||
@@ -204,6 +207,29 @@ inline bool parseRendtxtrHeader(const std::vector<u8>& data, RendtxtrInfo& info)
|
|||||||
info.rawDataLen = (u32)(n - 64);
|
info.rawDataLen = (u32)(n - 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// f2 is the pixel format: 4 = RGBA8, 5 = RGBA16F, 15.. = ASTC. Uncompressed textures ship WITH their mip chain
|
||||||
|
// (Focused's rocky2 / homefg / midgroundb_02 basecolours: 2048^2 RGBA8 + mips = 22369620 bytes; Horror's
|
||||||
|
// t_lantern_unlit_baked_01 / t_vat_basecolor: RGBA16F + mips). No ASTC footprint tiles that, so the formatCode
|
||||||
|
// guess below decoded raw texels as ASTC: diagonal streaks and dots over the rocks, black lanterns. A payload
|
||||||
|
// that is exactly such a chain (any length of it) is cut to mip 0, which the decoders read as raw texels.
|
||||||
|
if (const u32 sFmt = fbFieldSlot(d, n, root, 2)) {
|
||||||
|
const u32 bpp = d[sFmt] == 4 ? 4 : d[sFmt] == 5 ? 8 : 0;
|
||||||
|
u64 chain = 0;
|
||||||
|
for (u32 w = info.width, h = info.height; bpp && chain < info.rawDataLen;) {
|
||||||
|
chain += (u64)w * h * bpp;
|
||||||
|
if (w == 1 && h == 1) break;
|
||||||
|
w = w > 1 ? w / 2 : 1;
|
||||||
|
h = h > 1 ? h / 2 : 1;
|
||||||
|
}
|
||||||
|
if (bpp && chain == info.rawDataLen) {
|
||||||
|
info.rawBpp = bpp;
|
||||||
|
info.rawDataLen = info.width * info.height * bpp;
|
||||||
|
info.blockW = info.blockH = 4; // (unused for raw texels; keeps astcDimsOk's footprint check true)
|
||||||
|
info.mipCount = 1;
|
||||||
|
return (size_t)info.rawDataOffset + info.rawDataLen <= n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
u32 rawLen = info.rawDataLen;
|
u32 rawLen = info.rawDataLen;
|
||||||
|
|
||||||
// Two RENDTXTR layouts exist in the wild. Older readers treated f6 as a
|
// Two RENDTXTR layouts exist in the wild. Older readers treated f6 as a
|
||||||
|
|||||||
@@ -762,6 +762,8 @@ public:
|
|||||||
if (doc.has("relationships") && doc["relationships"].isArray())
|
if (doc.has("relationships") && doc["relationships"].isArray())
|
||||||
for (const auto& r : doc["relationships"].arrVal)
|
for (const auto& r : doc["relationships"].arrVal)
|
||||||
if (r["relationshipType"].asString() == "RelationChildOf") parentOfId[r["source"].asString()] = r["destination"].asString();
|
if (r["relationshipType"].asString() == "RelationChildOf") parentOfId[r["source"].asString()] = r["destination"].asString();
|
||||||
|
std::map<std::string, size_t> indexOfId;
|
||||||
|
for (size_t i = 0; i < arr.size(); ++i) if (arr[i].has("id")) indexOfId[arr[i]["id"].asString()] = i;
|
||||||
for (size_t i = 0; i < arr.size(); ++i) {
|
for (size_t i = 0; i < arr.size(); ++i) {
|
||||||
const auto& ent = arr[i];
|
const auto& ent = arr[i];
|
||||||
if (!ent.has("type") || ent.has("__removedByDelta")) continue; // a removed instance brings nothing in
|
if (!ent.has("type") || ent.has("__removedByDelta")) continue; // a removed instance brings nothing in
|
||||||
@@ -782,7 +784,21 @@ public:
|
|||||||
// Compose the INSTANCE entity's placement onto the parent before descending, so the
|
// Compose the INSTANCE entity's placement onto the parent before descending, so the
|
||||||
// referenced template's geometry inherits where this instance sits in the world.
|
// referenced template's geometry inherits where this instance sits in the world.
|
||||||
Transform inst = parseHstfEntityTransform(ent);
|
Transform inst = parseHstfEntityTransform(ent);
|
||||||
float im[16], cm[16]; trsToMat4(inst, im); mat4mul4(parentMat, im, cm); // parent·instance (4×4)
|
float im[16], cm[16]; trsToMat4(inst, im);
|
||||||
|
// The instance's own parents in this file place it too (Haven2025 hangs a Seating_Hotspot
|
||||||
|
// under each chair: read flat, all eight sat at the origin).
|
||||||
|
{
|
||||||
|
auto p = parentOfId.find(ent.has("id") ? ent["id"].asString() : std::string());
|
||||||
|
for (int guard = 0; p != parentOfId.end() && guard < 64; ++guard) {
|
||||||
|
auto pe = indexOfId.find(p->second);
|
||||||
|
if (pe == indexOfId.end()) break;
|
||||||
|
const Transform pt = parseHstfEntityTransform(arr[pe->second]);
|
||||||
|
float pm[16], t[16]; trsToMat4(pt, pm); mat4mul4(pm, im, t); std::memcpy(im, t, sizeof t);
|
||||||
|
inst = composeTransform(pt, inst);
|
||||||
|
p = parentOfId.find(p->second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mat4mul4(parentMat, im, cm); // parent·(this file's parents)·instance (4×4)
|
||||||
// This instance's deltas target entities inside the template it places; deltas inherited from
|
// This instance's deltas target entities inside the template it places; deltas inherited from
|
||||||
// further out keep travelling down and apply after (they win).
|
// further out keep travelling down and apply after (they win).
|
||||||
HstfDeltas child;
|
HstfDeltas child;
|
||||||
@@ -807,6 +823,17 @@ public:
|
|||||||
};
|
};
|
||||||
float identMat[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
|
float identMat[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
|
||||||
loadHstfRec(*spacePath, 6, Transform{}, identMat, HstfDeltas{}, std::string());
|
loadHstfRec(*spacePath, 6, Transform{}, identMat, HstfDeltas{}, std::string());
|
||||||
|
// A hotspot on a seat (Haven2025's Seating_Hotspot under each chair) marks the seat, which seats already
|
||||||
|
// cover: kept, a teleport landing near a stool would snap onto its top, and the stool top would be walkable.
|
||||||
|
{
|
||||||
|
size_t dropped = 0;
|
||||||
|
world.hotspots.erase(std::remove_if(world.hotspots.begin(), world.hotspots.end(), [&](const WorldPoint& h) {
|
||||||
|
for (const WorldPoint& s : world.seats)
|
||||||
|
if (std::fabs(h.pos[0] - s.pos[0]) < 0.1f && std::fabs(h.pos[1] - s.pos[1]) < 0.1f && std::fabs(h.pos[2] - s.pos[2]) < 0.1f) { ++dropped; return true; }
|
||||||
|
return false;
|
||||||
|
}), world.hotspots.end());
|
||||||
|
if (dropped) log(" %zu hotspot(s) on seats dropped", dropped);
|
||||||
|
}
|
||||||
world.origin = world.empty() ? "" : "meta";
|
world.origin = world.empty() ? "" : "meta";
|
||||||
log(" World: %zu spawns, %zu seats, %zu hotspots, %zu colliders", world.spawns.size(), world.seats.size(),
|
log(" World: %zu spawns, %zu seats, %zu hotspots, %zu colliders", world.spawns.size(), world.seats.size(),
|
||||||
world.hotspots.size(), world.colliders.size());
|
world.hotspots.size(), world.colliders.size());
|
||||||
@@ -852,6 +879,7 @@ public:
|
|||||||
// ASTC-decoded ONCE; every later use copies the pixels.
|
// ASTC-decoded ONCE; every later use copies the pixels.
|
||||||
struct DecodedTex {
|
struct DecodedTex {
|
||||||
bool ok = false; std::vector<u8> rgba; u32 w = 0, h = 0, fmt = 0; std::vector<u8> cube; u32 cubeSize = 0;
|
bool ok = false; std::vector<u8> rgba; u32 w = 0, h = 0, fmt = 0; std::vector<u8> cube; u32 cubeSize = 0;
|
||||||
|
std::vector<u8> srgb; // an RGBA16F texture: its colour sRGB-encoded (rgba holds it linear)
|
||||||
std::shared_ptr<const TexSource> src; // deferred: still-compressed ASTC, rgba empty
|
std::shared_ptr<const TexSource> src; // deferred: still-compressed ASTC, rgba empty
|
||||||
std::shared_ptr<const TexSource> cubeSrc; // deferred cube: six ASTC faces, cube empty
|
std::shared_ptr<const TexSource> cubeSrc; // deferred cube: six ASTC faces, cube empty
|
||||||
};
|
};
|
||||||
@@ -931,6 +959,14 @@ public:
|
|||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
d->ok = true; d->w = ti.width; d->h = ti.height; d->fmt = ti.formatCode;
|
d->ok = true; d->w = ti.width; d->h = ti.height; d->fmt = ti.formatCode;
|
||||||
|
if (ti.rawBpp == 8) { // straight from the halves: 8-bit linear first would band the darks
|
||||||
|
d->srgb.resize(d->rgba.size());
|
||||||
|
for (size_t q = 0; q < d->srgb.size(); ++q) {
|
||||||
|
uint16_t hv; std::memcpy(&hv, texData.data() + ti.rawDataOffset + q * 2, 2);
|
||||||
|
const float v = std::clamp(astc::astcHalf2Float(hv), 0.0f, 1.0f);
|
||||||
|
d->srgb[q] = (u8)std::lround(((q & 3) == 3 ? v : v <= 0.0031308f ? v * 12.92f : 1.055f * std::pow(v, 1.0f / 2.4f) - 0.055f) * 255.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
return d;
|
return d;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -947,6 +983,7 @@ public:
|
|||||||
auto d = decodeCached(*tp, false, md.isSkybox, true);
|
auto d = decodeCached(*tp, false, md.isSkybox, true);
|
||||||
if (!d->ok) return false;
|
if (!d->ok) return false;
|
||||||
md.texRGBA = d->rgba; md.texW = d->w; md.texH = d->h; md.hasTexture = true; md.texSrc = d->src;
|
md.texRGBA = d->rgba; md.texW = d->w; md.texH = d->h; md.hasTexture = true; md.texSrc = d->src;
|
||||||
|
if (!d->srgb.empty()) md.texRGBA = d->srgb; // an RGBA16F base colour is linear; the base texture is sampled as sRGB
|
||||||
if (d->cubeSize) { md.cubeRGBA = d->cube; md.cubeSize = d->cubeSize; md.cubeSrc = d->cubeSrc; }
|
if (d->cubeSize) { md.cubeRGBA = d->cube; md.cubeSize = d->cubeSize; md.cubeSrc = d->cubeSrc; }
|
||||||
log(" Texture: %s (%ux%u fmt=%u)", tp->c_str(), d->w, d->h, d->fmt);
|
log(" Texture: %s (%ux%u fmt=%u)", tp->c_str(), d->w, d->h, d->fmt);
|
||||||
return true;
|
return true;
|
||||||
@@ -1142,29 +1179,63 @@ public:
|
|||||||
auto decodeReflCube = [&](const AssetKey& k, MeshData& md) {
|
auto decodeReflCube = [&](const AssetKey& k, MeshData& md) {
|
||||||
const std::string* rp = resolve(k);
|
const std::string* rp = resolve(k);
|
||||||
if (!rp) return;
|
if (!rp) return;
|
||||||
auto raw = readTexture(*rp);
|
// A streamed cube keeps its top levels in the streams and the rest in the base file: the cooked, blurred
|
||||||
RendtxtrInfo ri;
|
// levels the shaders read for rough surfaces (Cascadia: 1024 and 512 streamed, 256 to 16 in the base).
|
||||||
if (!parseRendtxtrHeader(raw, ri) || ri.faces != 6 || ri.width != ri.height || (size_t)ri.rawDataOffset + ri.rawDataLen > raw.size()) {
|
// readTexture takes the largest file alone, which left one sharp level and every floor a mirror; this
|
||||||
log(" [skybox] reflectionMap %s is not a cube (%u faces, %ux%u)", rp->c_str(), ri.faces, ri.width, ri.height);
|
// takes each level, largest first, from whichever file holds it.
|
||||||
|
std::vector<std::vector<u8>> files;
|
||||||
|
files.push_back(readAsset(*rp));
|
||||||
|
const size_t slash = rp->rfind('/');
|
||||||
|
auto st = slash == std::string::npos ? texStreams.end() : texStreams.find(rp->substr(0, slash));
|
||||||
|
if (st != texStreams.end() && !std::getenv("FH_BASE_TEXTURES"))
|
||||||
|
for (const std::string& entry : st->second) {
|
||||||
|
int idx = mz_zip_reader_locate_file(&sceneZip, entry.c_str(), nullptr, 0);
|
||||||
|
if (idx < 0) continue;
|
||||||
|
size_t sz = 0;
|
||||||
|
void* d = mz_zip_reader_extract_to_heap(&sceneZip, idx, &sz, 0);
|
||||||
|
if (!d) continue;
|
||||||
|
files.emplace_back((u8*)d, (u8*)d + sz);
|
||||||
|
mz_free(d);
|
||||||
|
}
|
||||||
|
std::map<u32, std::pair<const u8*, size_t>, std::greater<u32>> levels; // size -> its six faces
|
||||||
|
RendtxtrInfo first;
|
||||||
|
u32 bw = 0, bh = 0;
|
||||||
|
for (const auto& f : files) {
|
||||||
|
RendtxtrInfo ri;
|
||||||
|
if (!parseRendtxtrHeader(f, ri) || ri.faces != 6 || ri.width != ri.height || (size_t)ri.rawDataOffset + ri.rawDataLen > f.size()) continue;
|
||||||
|
if (bw && (ri.blockW != bw || ri.blockH != bh)) continue; // (one block size for the whole chain)
|
||||||
|
if (!bw) first = ri;
|
||||||
|
bw = ri.blockW; bh = ri.blockH;
|
||||||
|
size_t off = ri.rawDataOffset;
|
||||||
|
const size_t end = off + ri.rawDataLen;
|
||||||
|
for (u32 s = ri.width; s >= 1; s /= 2) {
|
||||||
|
const size_t face = (size_t)((s + bw - 1) / bw) * ((s + bh - 1) / bh) * 16;
|
||||||
|
if (off + face * 6 > end) break;
|
||||||
|
levels.emplace(s, std::make_pair(f.data() + off, face * 6));
|
||||||
|
off += face * 6;
|
||||||
|
if (s == 1) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (levels.empty()) {
|
||||||
|
log(" [skybox] reflectionMap %s is not a cube (%u faces, %ux%u)", rp->c_str(), first.faces, first.width, first.height);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
size_t bytes = 0;
|
// The chain from the largest level down, as far as it has no gap.
|
||||||
|
const u32 top = levels.begin()->first;
|
||||||
|
auto t = std::make_shared<TexSource>();
|
||||||
u32 mips = 0;
|
u32 mips = 0;
|
||||||
for (u32 s = ri.width; s >= 1; s /= 2) {
|
for (u32 s = top; s >= 1; s /= 2) {
|
||||||
const size_t face = (size_t)((s + ri.blockW - 1) / ri.blockW) * ((s + ri.blockH - 1) / ri.blockH) * 16;
|
auto it = levels.find(s);
|
||||||
if (bytes + face * 6 > ri.rawDataLen) break;
|
if (it == levels.end()) break;
|
||||||
bytes += face * 6;
|
t->astc.insert(t->astc.end(), it->second.first, it->second.first + it->second.second);
|
||||||
++mips;
|
++mips;
|
||||||
if (s == 1) break;
|
if (s == 1) break;
|
||||||
}
|
}
|
||||||
if (!mips) return;
|
t->w = t->h = top; t->bw = bw; t->bh = bh; t->mips = mips; t->hdr = true; t->faces = 6;
|
||||||
auto t = std::make_shared<TexSource>();
|
|
||||||
t->astc.assign(raw.begin() + ri.rawDataOffset, raw.begin() + ri.rawDataOffset + bytes);
|
|
||||||
t->w = t->h = ri.width; t->bw = ri.blockW; t->bh = ri.blockH; t->mips = mips; t->hdr = true; t->faces = 6;
|
|
||||||
md.reflCubeSrc = std::move(t);
|
md.reflCubeSrc = std::move(t);
|
||||||
md.reflCubeHalf.clear();
|
md.reflCubeHalf.clear();
|
||||||
md.reflCubeSize = ri.width; md.reflCubeMips = mips;
|
md.reflCubeSize = top; md.reflCubeMips = mips;
|
||||||
log(" [skybox] reflectionMap cube %ux%u, %u mips", ri.width, ri.height, mips);
|
log(" [skybox] reflectionMap cube %ux%u, %u mips (from %zu file%s)", top, top, mips, files.size(), files.size() == 1 ? "" : "s");
|
||||||
};
|
};
|
||||||
|
|
||||||
auto applyMat = [&](MatlmatlInfo& matInfo, MeshData& md) -> bool {
|
auto applyMat = [&](MatlmatlInfo& matInfo, MeshData& md) -> bool {
|
||||||
@@ -1875,6 +1946,18 @@ public:
|
|||||||
if (applyMat(matInfo, md)) { gotTex = true; break; }
|
if (applyMat(matInfo, md)) { gotTex = true; break; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path 1b: no MaterialPlatformComponent -> the material the mesh was exported with (nested in the
|
||||||
|
// RENDMESH), which is what libshell draws it with. Horror's SM_skybox (unlit + skybox_d.png) and the
|
||||||
|
// hero lantern's candleFlame have no material component; without this they had no shader and vanished.
|
||||||
|
if (!gotTex && obj.matRefs.empty() && !obj.isSkybox) {
|
||||||
|
const std::vector<u8> own = rendMeshPartMaterial(meshData);
|
||||||
|
MatlmatlInfo matInfo;
|
||||||
|
if (!own.empty() && parseMatlmatl(own, matInfo)) {
|
||||||
|
log(" Mesh's own material: shader ing=%016llX", (unsigned long long)matInfo.shaderIng);
|
||||||
|
gotTex = applyMat(matInfo, md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Path 2: Fallback — only when ALL entities lack matRefs (v79_test-like APKs).
|
// Path 2: Fallback — only when ALL entities lack matRefs (v79_test-like APKs).
|
||||||
// Cycle through pre-loaded materials so each unmaterialed entity gets a texture.
|
// Cycle through pre-loaded materials so each unmaterialed entity gets a texture.
|
||||||
// In prod APKs (where only a few entities lack matRefs), skip this — use neutral.
|
// In prod APKs (where only a few entities lack matRefs), skip this — use neutral.
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
SMOL-V (https://github.com/aras-p/smol-v) source/smolv.{h,cpp} + LICENSE.txt
|
SMOL-V (https://github.com/aras-p/smol-v) source/smolv.{h,cpp} + LICENSE.txt
|
||||||
Pinned commit: 55000efe742f56d8b51223b9ea7775a8f0501881
|
Pinned commit: 55000efe742f56d8b51223b9ea7775a8f0501881
|
||||||
|
Local changes (marked "Frame Studio:" in smolv.cpp): no undefined behaviour decoding malformed blobs (header words
|
||||||
|
read with memcpy, unsigned shifts in smolv_Read4 / smolv_ReadVarint, SpvOp spans every 16-bit opcode value).
|
||||||
@@ -362,6 +362,7 @@ enum SpvOp
|
|||||||
SpvOpGroupNonUniformLogicalXor = 364,
|
SpvOpGroupNonUniformLogicalXor = 364,
|
||||||
SpvOpGroupNonUniformQuadBroadcast = 365,
|
SpvOpGroupNonUniformQuadBroadcast = 365,
|
||||||
SpvOpGroupNonUniformQuadSwap = 366,
|
SpvOpGroupNonUniformQuadSwap = 366,
|
||||||
|
SpvOpSmolvLast_ = 0xFFFF, // Frame Studio: any 16-bit opcode a malformed blob decodes to is a valid value of the enum
|
||||||
};
|
};
|
||||||
static const int kKnownOpsCount = SpvOpGroupNonUniformQuadSwap+1;
|
static const int kKnownOpsCount = SpvOpGroupNonUniformQuadSwap+1;
|
||||||
|
|
||||||
@@ -1214,14 +1215,24 @@ static bool smolv_CheckSpirVHeader(const uint32_t* words, size_t wordCount)
|
|||||||
// in a "big endian" order. Need to byteswap all words then.
|
// in a "big endian" order. Need to byteswap all words then.
|
||||||
return smolv_CheckGenericHeader(words, wordCount, kSpirVHeaderMagic, 0xFFFFFFFF);
|
return smolv_CheckGenericHeader(words, wordCount, kSpirVHeaderMagic, 0xFFFFFFFF);
|
||||||
}
|
}
|
||||||
|
// Frame Studio: the header words are copied out (the SMOL-V data need not be 4-byte aligned).
|
||||||
|
static uint32_t smolv_Load4(const uint8_t* p)
|
||||||
|
{
|
||||||
|
uint32_t v;
|
||||||
|
memcpy(&v, p, 4);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
static bool smolv_CheckSmolHeader(const uint8_t* bytes, size_t byteCount)
|
static bool smolv_CheckSmolHeader(const uint8_t* bytes, size_t byteCount)
|
||||||
{
|
{
|
||||||
if (!smolv_CheckGenericHeader((const uint32_t*)bytes, byteCount/4, kSmolHeaderMagic, 0x00FFFFFF))
|
if (!bytes || byteCount < 24) // header, and one more word past it to store decoded length
|
||||||
return false;
|
return false;
|
||||||
if (byteCount < 24) // one more word past header to store decoded length
|
uint32_t header[5];
|
||||||
|
memcpy(header, bytes, sizeof header);
|
||||||
|
if (!smolv_CheckGenericHeader(header, 5, kSmolHeaderMagic, 0x00FFFFFF))
|
||||||
return false;
|
return false;
|
||||||
// SMOL-V version
|
// SMOL-V version
|
||||||
int smolVersion = ((const uint32_t*)bytes)[1] >> 24;
|
int smolVersion = header[1] >> 24;
|
||||||
if (smolVersion < 0 || smolVersion > kSmolCurrEncodingVersion)
|
if (smolVersion < 0 || smolVersion > kSmolCurrEncodingVersion)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
@@ -1256,7 +1267,7 @@ static bool smolv_Read4(const uint8_t*& data, const uint8_t* dataEnd, uint32_t&
|
|||||||
{
|
{
|
||||||
if (data + 4 > dataEnd)
|
if (data + 4 > dataEnd)
|
||||||
return false;
|
return false;
|
||||||
outv = (data[0]) | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
|
outv = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16) | (uint32_t(data[3]) << 24); // Frame Studio: unsigned shifts
|
||||||
data += 4;
|
data += 4;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1286,7 +1297,8 @@ static bool smolv_ReadVarint(const uint8_t*& data, const uint8_t* dataEnd, uint3
|
|||||||
while (data < dataEnd)
|
while (data < dataEnd)
|
||||||
{
|
{
|
||||||
uint8_t b = *data;
|
uint8_t b = *data;
|
||||||
v |= (b & 127) << shift;
|
if (shift < 32) // Frame Studio: unsigned, and bytes past 32 bits (malformed input) add nothing
|
||||||
|
v |= uint32_t(b & 127) << shift;
|
||||||
shift += 7;
|
shift += 7;
|
||||||
data++;
|
data++;
|
||||||
if (!(b & 128))
|
if (!(b & 128))
|
||||||
@@ -1608,8 +1620,7 @@ size_t smolv::GetDecodedBufferSize(const void* smolvData, size_t smolvSize)
|
|||||||
{
|
{
|
||||||
if (!smolv_CheckSmolHeader((const uint8_t*)smolvData, smolvSize))
|
if (!smolv_CheckSmolHeader((const uint8_t*)smolvData, smolvSize))
|
||||||
return 0;
|
return 0;
|
||||||
const uint32_t* words = (const uint32_t*)smolvData;
|
return smolv_Load4((const uint8_t*)smolvData + 20); // Frame Studio: unaligned-safe
|
||||||
return words[5];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,502 @@
|
|||||||
|
Files: All files except for those called out below.
|
||||||
|
Copyright (c) 2015-2024 The Khronos Group Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and/or associated documentation files (the
|
||||||
|
"Materials"), to deal in the Materials without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Materials, and to
|
||||||
|
permit persons to whom the Materials are furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Materials.
|
||||||
|
|
||||||
|
MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS
|
||||||
|
KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS
|
||||||
|
SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT
|
||||||
|
https://www.khronos.org/registry/
|
||||||
|
|
||||||
|
THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Files: include/spirv/spir-v.xml
|
||||||
|
Copyright (c) 2015-2024 The Khronos Group Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and/or associated documentation files (the
|
||||||
|
"Materials"), to deal in the Materials without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Materials, and to
|
||||||
|
permit persons to whom the Materials are furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Materials.
|
||||||
|
|
||||||
|
THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Files: tools/buildHeaders/jsoncpp/*
|
||||||
|
The JsonCpp library's source code, including accompanying documentation,
|
||||||
|
tests and demonstration applications, are licensed under the following
|
||||||
|
conditions...
|
||||||
|
|
||||||
|
The author (Baptiste Lepilleur) explicitly disclaims copyright in all
|
||||||
|
jurisdictions which recognize such a disclaimer. In such jurisdictions,
|
||||||
|
this software is released into the Public Domain.
|
||||||
|
|
||||||
|
In jurisdictions which do not recognize Public Domain property (e.g. Germany as of
|
||||||
|
2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur, and is
|
||||||
|
released under the terms of the MIT License (see below).
|
||||||
|
|
||||||
|
In jurisdictions which recognize Public Domain property, the user of this
|
||||||
|
software may choose to accept it either as 1) Public Domain, 2) under the
|
||||||
|
conditions of the MIT License (see below), or 3) under the terms of dual
|
||||||
|
Public Domain/MIT License conditions described here, as they choose.
|
||||||
|
|
||||||
|
The MIT License is about as close to Public Domain as a license can get, and is
|
||||||
|
described in clear, concise terms at:
|
||||||
|
|
||||||
|
http://en.wikipedia.org/wiki/MIT_License
|
||||||
|
|
||||||
|
The full text of the MIT License follows:
|
||||||
|
|
||||||
|
========================================================================
|
||||||
|
Copyright (c) 2007-2010 Baptiste Lepilleur
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person
|
||||||
|
obtaining a copy of this software and associated documentation
|
||||||
|
files (the "Software"), to deal in the Software without
|
||||||
|
restriction, including without limitation the rights to use, copy,
|
||||||
|
modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
========================================================================
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Files: **.md, WORKSPACE, .git**
|
||||||
|
|
||||||
|
Attribution 4.0 International
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||||
|
does not provide legal services or legal advice. Distribution of
|
||||||
|
Creative Commons public licenses does not create a lawyer-client or
|
||||||
|
other relationship. Creative Commons makes its licenses and related
|
||||||
|
information available on an "as-is" basis. Creative Commons gives no
|
||||||
|
warranties regarding its licenses, any material licensed under their
|
||||||
|
terms and conditions, or any related information. Creative Commons
|
||||||
|
disclaims all liability for damages resulting from their use to the
|
||||||
|
fullest extent possible.
|
||||||
|
|
||||||
|
Using Creative Commons Public Licenses
|
||||||
|
|
||||||
|
Creative Commons public licenses provide a standard set of terms and
|
||||||
|
conditions that creators and other rights holders may use to share
|
||||||
|
original works of authorship and other material subject to copyright
|
||||||
|
and certain other rights specified in the public license below. The
|
||||||
|
following considerations are for informational purposes only, are not
|
||||||
|
exhaustive, and do not form part of our licenses.
|
||||||
|
|
||||||
|
Considerations for licensors: Our public licenses are
|
||||||
|
intended for use by those authorized to give the public
|
||||||
|
permission to use material in ways otherwise restricted by
|
||||||
|
copyright and certain other rights. Our licenses are
|
||||||
|
irrevocable. Licensors should read and understand the terms
|
||||||
|
and conditions of the license they choose before applying it.
|
||||||
|
Licensors should also secure all rights necessary before
|
||||||
|
applying our licenses so that the public can reuse the
|
||||||
|
material as expected. Licensors should clearly mark any
|
||||||
|
material not subject to the license. This includes other CC-
|
||||||
|
licensed material, or material used under an exception or
|
||||||
|
limitation to copyright. More considerations for licensors:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensors
|
||||||
|
|
||||||
|
Considerations for the public: By using one of our public
|
||||||
|
licenses, a licensor grants the public permission to use the
|
||||||
|
licensed material under specified terms and conditions. If
|
||||||
|
the licensor's permission is not necessary for any reason--for
|
||||||
|
example, because of any applicable exception or limitation to
|
||||||
|
copyright--then that use is not regulated by the license. Our
|
||||||
|
licenses grant only permissions under copyright and certain
|
||||||
|
other rights that a licensor has authority to grant. Use of
|
||||||
|
the licensed material may still be restricted for other
|
||||||
|
reasons, including because others have copyright or other
|
||||||
|
rights in the material. A licensor may make special requests,
|
||||||
|
such as asking that all changes be marked or described.
|
||||||
|
Although not required by our licenses, you are encouraged to
|
||||||
|
respect those requests where reasonable. More considerations
|
||||||
|
for the public:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensees
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Attribution 4.0 International Public License
|
||||||
|
|
||||||
|
By exercising the Licensed Rights (defined below), You accept and agree
|
||||||
|
to be bound by the terms and conditions of this Creative Commons
|
||||||
|
Attribution 4.0 International Public License ("Public License"). To the
|
||||||
|
extent this Public License may be interpreted as a contract, You are
|
||||||
|
granted the Licensed Rights in consideration of Your acceptance of
|
||||||
|
these terms and conditions, and the Licensor grants You such rights in
|
||||||
|
consideration of benefits the Licensor receives from making the
|
||||||
|
Licensed Material available under these terms and conditions.
|
||||||
|
|
||||||
|
|
||||||
|
Section 1 -- Definitions.
|
||||||
|
|
||||||
|
a. Adapted Material means material subject to Copyright and Similar
|
||||||
|
Rights that is derived from or based upon the Licensed Material
|
||||||
|
and in which the Licensed Material is translated, altered,
|
||||||
|
arranged, transformed, or otherwise modified in a manner requiring
|
||||||
|
permission under the Copyright and Similar Rights held by the
|
||||||
|
Licensor. For purposes of this Public License, where the Licensed
|
||||||
|
Material is a musical work, performance, or sound recording,
|
||||||
|
Adapted Material is always produced where the Licensed Material is
|
||||||
|
synched in timed relation with a moving image.
|
||||||
|
|
||||||
|
b. Adapter's License means the license You apply to Your Copyright
|
||||||
|
and Similar Rights in Your contributions to Adapted Material in
|
||||||
|
accordance with the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
c. Copyright and Similar Rights means copyright and/or similar rights
|
||||||
|
closely related to copyright including, without limitation,
|
||||||
|
performance, broadcast, sound recording, and Sui Generis Database
|
||||||
|
Rights, without regard to how the rights are labeled or
|
||||||
|
categorized. For purposes of this Public License, the rights
|
||||||
|
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||||
|
Rights.
|
||||||
|
|
||||||
|
d. Effective Technological Measures means those measures that, in the
|
||||||
|
absence of proper authority, may not be circumvented under laws
|
||||||
|
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||||
|
Treaty adopted on December 20, 1996, and/or similar international
|
||||||
|
agreements.
|
||||||
|
|
||||||
|
e. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||||
|
any other exception or limitation to Copyright and Similar Rights
|
||||||
|
that applies to Your use of the Licensed Material.
|
||||||
|
|
||||||
|
f. Licensed Material means the artistic or literary work, database,
|
||||||
|
or other material to which the Licensor applied this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
g. Licensed Rights means the rights granted to You subject to the
|
||||||
|
terms and conditions of this Public License, which are limited to
|
||||||
|
all Copyright and Similar Rights that apply to Your use of the
|
||||||
|
Licensed Material and that the Licensor has authority to license.
|
||||||
|
|
||||||
|
h. Licensor means the individual(s) or entity(ies) granting rights
|
||||||
|
under this Public License.
|
||||||
|
|
||||||
|
i. Share means to provide material to the public by any means or
|
||||||
|
process that requires permission under the Licensed Rights, such
|
||||||
|
as reproduction, public display, public performance, distribution,
|
||||||
|
dissemination, communication, or importation, and to make material
|
||||||
|
available to the public including in ways that members of the
|
||||||
|
public may access the material from a place and at a time
|
||||||
|
individually chosen by them.
|
||||||
|
|
||||||
|
j. Sui Generis Database Rights means rights other than copyright
|
||||||
|
resulting from Directive 96/9/EC of the European Parliament and of
|
||||||
|
the Council of 11 March 1996 on the legal protection of databases,
|
||||||
|
as amended and/or succeeded, as well as other essentially
|
||||||
|
equivalent rights anywhere in the world.
|
||||||
|
|
||||||
|
k. You means the individual or entity exercising the Licensed Rights
|
||||||
|
under this Public License. Your has a corresponding meaning.
|
||||||
|
|
||||||
|
|
||||||
|
Section 2 -- Scope.
|
||||||
|
|
||||||
|
a. License grant.
|
||||||
|
|
||||||
|
1. Subject to the terms and conditions of this Public License,
|
||||||
|
the Licensor hereby grants You a worldwide, royalty-free,
|
||||||
|
non-sublicensable, non-exclusive, irrevocable license to
|
||||||
|
exercise the Licensed Rights in the Licensed Material to:
|
||||||
|
|
||||||
|
a. reproduce and Share the Licensed Material, in whole or
|
||||||
|
in part; and
|
||||||
|
|
||||||
|
b. produce, reproduce, and Share Adapted Material.
|
||||||
|
|
||||||
|
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||||
|
Exceptions and Limitations apply to Your use, this Public
|
||||||
|
License does not apply, and You do not need to comply with
|
||||||
|
its terms and conditions.
|
||||||
|
|
||||||
|
3. Term. The term of this Public License is specified in Section
|
||||||
|
6(a).
|
||||||
|
|
||||||
|
4. Media and formats; technical modifications allowed. The
|
||||||
|
Licensor authorizes You to exercise the Licensed Rights in
|
||||||
|
all media and formats whether now known or hereafter created,
|
||||||
|
and to make technical modifications necessary to do so. The
|
||||||
|
Licensor waives and/or agrees not to assert any right or
|
||||||
|
authority to forbid You from making technical modifications
|
||||||
|
necessary to exercise the Licensed Rights, including
|
||||||
|
technical modifications necessary to circumvent Effective
|
||||||
|
Technological Measures. For purposes of this Public License,
|
||||||
|
simply making modifications authorized by this Section 2(a)
|
||||||
|
(4) never produces Adapted Material.
|
||||||
|
|
||||||
|
5. Downstream recipients.
|
||||||
|
|
||||||
|
a. Offer from the Licensor -- Licensed Material. Every
|
||||||
|
recipient of the Licensed Material automatically
|
||||||
|
receives an offer from the Licensor to exercise the
|
||||||
|
Licensed Rights under the terms and conditions of this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
b. No downstream restrictions. You may not offer or impose
|
||||||
|
any additional or different terms or conditions on, or
|
||||||
|
apply any Effective Technological Measures to, the
|
||||||
|
Licensed Material if doing so restricts exercise of the
|
||||||
|
Licensed Rights by any recipient of the Licensed
|
||||||
|
Material.
|
||||||
|
|
||||||
|
6. No endorsement. Nothing in this Public License constitutes or
|
||||||
|
may be construed as permission to assert or imply that You
|
||||||
|
are, or that Your use of the Licensed Material is, connected
|
||||||
|
with, or sponsored, endorsed, or granted official status by,
|
||||||
|
the Licensor or others designated to receive attribution as
|
||||||
|
provided in Section 3(a)(1)(A)(i).
|
||||||
|
|
||||||
|
b. Other rights.
|
||||||
|
|
||||||
|
1. Moral rights, such as the right of integrity, are not
|
||||||
|
licensed under this Public License, nor are publicity,
|
||||||
|
privacy, and/or other similar personality rights; however, to
|
||||||
|
the extent possible, the Licensor waives and/or agrees not to
|
||||||
|
assert any such rights held by the Licensor to the limited
|
||||||
|
extent necessary to allow You to exercise the Licensed
|
||||||
|
Rights, but not otherwise.
|
||||||
|
|
||||||
|
2. Patent and trademark rights are not licensed under this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
3. To the extent possible, the Licensor waives any right to
|
||||||
|
collect royalties from You for the exercise of the Licensed
|
||||||
|
Rights, whether directly or through a collecting society
|
||||||
|
under any voluntary or waivable statutory or compulsory
|
||||||
|
licensing scheme. In all other cases the Licensor expressly
|
||||||
|
reserves any right to collect such royalties.
|
||||||
|
|
||||||
|
|
||||||
|
Section 3 -- License Conditions.
|
||||||
|
|
||||||
|
Your exercise of the Licensed Rights is expressly made subject to the
|
||||||
|
following conditions.
|
||||||
|
|
||||||
|
a. Attribution.
|
||||||
|
|
||||||
|
1. If You Share the Licensed Material (including in modified
|
||||||
|
form), You must:
|
||||||
|
|
||||||
|
a. retain the following if it is supplied by the Licensor
|
||||||
|
with the Licensed Material:
|
||||||
|
|
||||||
|
i. identification of the creator(s) of the Licensed
|
||||||
|
Material and any others designated to receive
|
||||||
|
attribution, in any reasonable manner requested by
|
||||||
|
the Licensor (including by pseudonym if
|
||||||
|
designated);
|
||||||
|
|
||||||
|
ii. a copyright notice;
|
||||||
|
|
||||||
|
iii. a notice that refers to this Public License;
|
||||||
|
|
||||||
|
iv. a notice that refers to the disclaimer of
|
||||||
|
warranties;
|
||||||
|
|
||||||
|
v. a URI or hyperlink to the Licensed Material to the
|
||||||
|
extent reasonably practicable;
|
||||||
|
|
||||||
|
b. indicate if You modified the Licensed Material and
|
||||||
|
retain an indication of any previous modifications; and
|
||||||
|
|
||||||
|
c. indicate the Licensed Material is licensed under this
|
||||||
|
Public License, and include the text of, or the URI or
|
||||||
|
hyperlink to, this Public License.
|
||||||
|
|
||||||
|
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||||
|
reasonable manner based on the medium, means, and context in
|
||||||
|
which You Share the Licensed Material. For example, it may be
|
||||||
|
reasonable to satisfy the conditions by providing a URI or
|
||||||
|
hyperlink to a resource that includes the required
|
||||||
|
information.
|
||||||
|
|
||||||
|
3. If requested by the Licensor, You must remove any of the
|
||||||
|
information required by Section 3(a)(1)(A) to the extent
|
||||||
|
reasonably practicable.
|
||||||
|
|
||||||
|
4. If You Share Adapted Material You produce, the Adapter's
|
||||||
|
License You apply must not prevent recipients of the Adapted
|
||||||
|
Material from complying with this Public License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 4 -- Sui Generis Database Rights.
|
||||||
|
|
||||||
|
Where the Licensed Rights include Sui Generis Database Rights that
|
||||||
|
apply to Your use of the Licensed Material:
|
||||||
|
|
||||||
|
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||||
|
to extract, reuse, reproduce, and Share all or a substantial
|
||||||
|
portion of the contents of the database;
|
||||||
|
|
||||||
|
b. if You include all or a substantial portion of the database
|
||||||
|
contents in a database in which You have Sui Generis Database
|
||||||
|
Rights, then the database in which You have Sui Generis Database
|
||||||
|
Rights (but not its individual contents) is Adapted Material; and
|
||||||
|
|
||||||
|
c. You must comply with the conditions in Section 3(a) if You Share
|
||||||
|
all or a substantial portion of the contents of the database.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 4 supplements and does not
|
||||||
|
replace Your obligations under this Public License where the Licensed
|
||||||
|
Rights include other Copyright and Similar Rights.
|
||||||
|
|
||||||
|
|
||||||
|
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||||
|
|
||||||
|
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||||
|
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||||
|
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||||
|
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||||
|
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||||
|
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||||
|
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||||
|
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||||
|
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||||
|
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||||
|
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||||
|
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||||
|
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||||
|
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||||
|
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||||
|
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||||
|
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
c. The disclaimer of warranties and limitation of liability provided
|
||||||
|
above shall be interpreted in a manner that, to the extent
|
||||||
|
possible, most closely approximates an absolute disclaimer and
|
||||||
|
waiver of all liability.
|
||||||
|
|
||||||
|
|
||||||
|
Section 6 -- Term and Termination.
|
||||||
|
|
||||||
|
a. This Public License applies for the term of the Copyright and
|
||||||
|
Similar Rights licensed here. However, if You fail to comply with
|
||||||
|
this Public License, then Your rights under this Public License
|
||||||
|
terminate automatically.
|
||||||
|
|
||||||
|
b. Where Your right to use the Licensed Material has terminated under
|
||||||
|
Section 6(a), it reinstates:
|
||||||
|
|
||||||
|
1. automatically as of the date the violation is cured, provided
|
||||||
|
it is cured within 30 days of Your discovery of the
|
||||||
|
violation; or
|
||||||
|
|
||||||
|
2. upon express reinstatement by the Licensor.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||||
|
right the Licensor may have to seek remedies for Your violations
|
||||||
|
of this Public License.
|
||||||
|
|
||||||
|
c. For the avoidance of doubt, the Licensor may also offer the
|
||||||
|
Licensed Material under separate terms or conditions or stop
|
||||||
|
distributing the Licensed Material at any time; however, doing so
|
||||||
|
will not terminate this Public License.
|
||||||
|
|
||||||
|
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 7 -- Other Terms and Conditions.
|
||||||
|
|
||||||
|
a. The Licensor shall not be bound by any additional or different
|
||||||
|
terms or conditions communicated by You unless expressly agreed.
|
||||||
|
|
||||||
|
b. Any arrangements, understandings, or agreements regarding the
|
||||||
|
Licensed Material not stated herein are separate from and
|
||||||
|
independent of the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 8 -- Interpretation.
|
||||||
|
|
||||||
|
a. For the avoidance of doubt, this Public License does not, and
|
||||||
|
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||||
|
conditions on any use of the Licensed Material that could lawfully
|
||||||
|
be made without permission under this Public License.
|
||||||
|
|
||||||
|
b. To the extent possible, if any provision of this Public License is
|
||||||
|
deemed unenforceable, it shall be automatically reformed to the
|
||||||
|
minimum extent necessary to make it enforceable. If the provision
|
||||||
|
cannot be reformed, it shall be severed from this Public License
|
||||||
|
without affecting the enforceability of the remaining terms and
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
c. No term or condition of this Public License will be waived and no
|
||||||
|
failure to comply consented to unless expressly agreed to by the
|
||||||
|
Licensor.
|
||||||
|
|
||||||
|
d. Nothing in this Public License constitutes or may be interpreted
|
||||||
|
as a limitation upon, or waiver of, any privileges and immunities
|
||||||
|
that apply to the Licensor or You, including from the legal
|
||||||
|
processes of any jurisdiction or authority.
|
||||||
|
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons is not a party to its public
|
||||||
|
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||||
|
its public licenses to material it publishes and in those instances
|
||||||
|
will be considered the “Licensor.” The text of the Creative Commons
|
||||||
|
public licenses is dedicated to the public domain under the CC0 Public
|
||||||
|
Domain Dedication. Except for the limited purpose of indicating that
|
||||||
|
material is shared under a Creative Commons public license or as
|
||||||
|
otherwise permitted by the Creative Commons policies published at
|
||||||
|
creativecommons.org/policies, Creative Commons does not authorize the
|
||||||
|
use of the trademark "Creative Commons" or any other trademark or logo
|
||||||
|
of Creative Commons without its prior written consent including,
|
||||||
|
without limitation, in connection with any unauthorized modifications
|
||||||
|
to any of its public licenses or any other arrangements,
|
||||||
|
understandings, or agreements concerning use of licensed material. For
|
||||||
|
the avoidance of doubt, this paragraph does not form part of the
|
||||||
|
public licenses.
|
||||||
|
|
||||||
|
Creative Commons may be contacted at creativecommons.org.
|
||||||
|
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
** SPDX-FileCopyrightText: 2014-2016 The Khronos Group Inc.
|
||||||
|
** SPDX-License-Identifier: MIT
|
||||||
|
**
|
||||||
|
** MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS
|
||||||
|
** KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS
|
||||||
|
** SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT
|
||||||
|
** https://www.khronos.org/registry/
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef GLSLstd450_H
|
||||||
|
#define GLSLstd450_H
|
||||||
|
|
||||||
|
static const int GLSLstd450Version = 100;
|
||||||
|
static const int GLSLstd450Revision = 3;
|
||||||
|
|
||||||
|
enum GLSLstd450 {
|
||||||
|
GLSLstd450Bad = 0, // Don't use
|
||||||
|
|
||||||
|
GLSLstd450Round = 1,
|
||||||
|
GLSLstd450RoundEven = 2,
|
||||||
|
GLSLstd450Trunc = 3,
|
||||||
|
GLSLstd450FAbs = 4,
|
||||||
|
GLSLstd450SAbs = 5,
|
||||||
|
GLSLstd450FSign = 6,
|
||||||
|
GLSLstd450SSign = 7,
|
||||||
|
GLSLstd450Floor = 8,
|
||||||
|
GLSLstd450Ceil = 9,
|
||||||
|
GLSLstd450Fract = 10,
|
||||||
|
|
||||||
|
GLSLstd450Radians = 11,
|
||||||
|
GLSLstd450Degrees = 12,
|
||||||
|
GLSLstd450Sin = 13,
|
||||||
|
GLSLstd450Cos = 14,
|
||||||
|
GLSLstd450Tan = 15,
|
||||||
|
GLSLstd450Asin = 16,
|
||||||
|
GLSLstd450Acos = 17,
|
||||||
|
GLSLstd450Atan = 18,
|
||||||
|
GLSLstd450Sinh = 19,
|
||||||
|
GLSLstd450Cosh = 20,
|
||||||
|
GLSLstd450Tanh = 21,
|
||||||
|
GLSLstd450Asinh = 22,
|
||||||
|
GLSLstd450Acosh = 23,
|
||||||
|
GLSLstd450Atanh = 24,
|
||||||
|
GLSLstd450Atan2 = 25,
|
||||||
|
|
||||||
|
GLSLstd450Pow = 26,
|
||||||
|
GLSLstd450Exp = 27,
|
||||||
|
GLSLstd450Log = 28,
|
||||||
|
GLSLstd450Exp2 = 29,
|
||||||
|
GLSLstd450Log2 = 30,
|
||||||
|
GLSLstd450Sqrt = 31,
|
||||||
|
GLSLstd450InverseSqrt = 32,
|
||||||
|
|
||||||
|
GLSLstd450Determinant = 33,
|
||||||
|
GLSLstd450MatrixInverse = 34,
|
||||||
|
|
||||||
|
GLSLstd450Modf = 35, // second operand needs an OpVariable to write to
|
||||||
|
GLSLstd450ModfStruct = 36, // no OpVariable operand
|
||||||
|
GLSLstd450FMin = 37,
|
||||||
|
GLSLstd450UMin = 38,
|
||||||
|
GLSLstd450SMin = 39,
|
||||||
|
GLSLstd450FMax = 40,
|
||||||
|
GLSLstd450UMax = 41,
|
||||||
|
GLSLstd450SMax = 42,
|
||||||
|
GLSLstd450FClamp = 43,
|
||||||
|
GLSLstd450UClamp = 44,
|
||||||
|
GLSLstd450SClamp = 45,
|
||||||
|
GLSLstd450FMix = 46,
|
||||||
|
GLSLstd450IMix = 47, // Reserved
|
||||||
|
GLSLstd450Step = 48,
|
||||||
|
GLSLstd450SmoothStep = 49,
|
||||||
|
|
||||||
|
GLSLstd450Fma = 50,
|
||||||
|
GLSLstd450Frexp = 51, // second operand needs an OpVariable to write to
|
||||||
|
GLSLstd450FrexpStruct = 52, // no OpVariable operand
|
||||||
|
GLSLstd450Ldexp = 53,
|
||||||
|
|
||||||
|
GLSLstd450PackSnorm4x8 = 54,
|
||||||
|
GLSLstd450PackUnorm4x8 = 55,
|
||||||
|
GLSLstd450PackSnorm2x16 = 56,
|
||||||
|
GLSLstd450PackUnorm2x16 = 57,
|
||||||
|
GLSLstd450PackHalf2x16 = 58,
|
||||||
|
GLSLstd450PackDouble2x32 = 59,
|
||||||
|
GLSLstd450UnpackSnorm2x16 = 60,
|
||||||
|
GLSLstd450UnpackUnorm2x16 = 61,
|
||||||
|
GLSLstd450UnpackHalf2x16 = 62,
|
||||||
|
GLSLstd450UnpackSnorm4x8 = 63,
|
||||||
|
GLSLstd450UnpackUnorm4x8 = 64,
|
||||||
|
GLSLstd450UnpackDouble2x32 = 65,
|
||||||
|
|
||||||
|
GLSLstd450Length = 66,
|
||||||
|
GLSLstd450Distance = 67,
|
||||||
|
GLSLstd450Cross = 68,
|
||||||
|
GLSLstd450Normalize = 69,
|
||||||
|
GLSLstd450FaceForward = 70,
|
||||||
|
GLSLstd450Reflect = 71,
|
||||||
|
GLSLstd450Refract = 72,
|
||||||
|
|
||||||
|
GLSLstd450FindILsb = 73,
|
||||||
|
GLSLstd450FindSMsb = 74,
|
||||||
|
GLSLstd450FindUMsb = 75,
|
||||||
|
|
||||||
|
GLSLstd450InterpolateAtCentroid = 76,
|
||||||
|
GLSLstd450InterpolateAtSample = 77,
|
||||||
|
GLSLstd450InterpolateAtOffset = 78,
|
||||||
|
|
||||||
|
GLSLstd450NMin = 79,
|
||||||
|
GLSLstd450NMax = 80,
|
||||||
|
GLSLstd450NClamp = 81,
|
||||||
|
|
||||||
|
GLSLstd450Count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // #ifndef GLSLstd450_H
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020-2024 The Khronos Group Inc.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS
|
||||||
|
// KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS
|
||||||
|
// SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT
|
||||||
|
// https://www.khronos.org/registry/
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef SPIRV_UNIFIED1_AMD_gcn_shader_H_
|
||||||
|
#define SPIRV_UNIFIED1_AMD_gcn_shader_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum {
|
||||||
|
AMD_gcn_shaderRevision = 2,
|
||||||
|
AMD_gcn_shaderRevision_BitWidthPadding = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
enum AMD_gcn_shaderInstructions {
|
||||||
|
AMD_gcn_shaderCubeFaceIndexAMD = 1,
|
||||||
|
AMD_gcn_shaderCubeFaceCoordAMD = 2,
|
||||||
|
AMD_gcn_shaderTimeAMD = 3,
|
||||||
|
AMD_gcn_shaderInstructionsMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // SPIRV_UNIFIED1_AMD_gcn_shader_H_
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020-2024 The Khronos Group Inc.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS
|
||||||
|
// KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS
|
||||||
|
// SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT
|
||||||
|
// https://www.khronos.org/registry/
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef SPIRV_UNIFIED1_AMD_shader_ballot_H_
|
||||||
|
#define SPIRV_UNIFIED1_AMD_shader_ballot_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum {
|
||||||
|
AMD_shader_ballotRevision = 5,
|
||||||
|
AMD_shader_ballotRevision_BitWidthPadding = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
enum AMD_shader_ballotInstructions {
|
||||||
|
AMD_shader_ballotSwizzleInvocationsAMD = 1,
|
||||||
|
AMD_shader_ballotSwizzleInvocationsMaskedAMD = 2,
|
||||||
|
AMD_shader_ballotWriteInvocationAMD = 3,
|
||||||
|
AMD_shader_ballotMbcntAMD = 4,
|
||||||
|
AMD_shader_ballotInstructionsMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // SPIRV_UNIFIED1_AMD_shader_ballot_H_
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020-2024 The Khronos Group Inc.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS
|
||||||
|
// KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS
|
||||||
|
// SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT
|
||||||
|
// https://www.khronos.org/registry/
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef SPIRV_UNIFIED1_AMD_shader_explicit_vertex_parameter_H_
|
||||||
|
#define SPIRV_UNIFIED1_AMD_shader_explicit_vertex_parameter_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum {
|
||||||
|
AMD_shader_explicit_vertex_parameterRevision = 4,
|
||||||
|
AMD_shader_explicit_vertex_parameterRevision_BitWidthPadding = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
enum AMD_shader_explicit_vertex_parameterInstructions {
|
||||||
|
AMD_shader_explicit_vertex_parameterInterpolateAtVertexAMD = 1,
|
||||||
|
AMD_shader_explicit_vertex_parameterInstructionsMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // SPIRV_UNIFIED1_AMD_shader_explicit_vertex_parameter_H_
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020-2024 The Khronos Group Inc.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS
|
||||||
|
// KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS
|
||||||
|
// SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT
|
||||||
|
// https://www.khronos.org/registry/
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef SPIRV_UNIFIED1_AMD_shader_trinary_minmax_H_
|
||||||
|
#define SPIRV_UNIFIED1_AMD_shader_trinary_minmax_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum {
|
||||||
|
AMD_shader_trinary_minmaxRevision = 4,
|
||||||
|
AMD_shader_trinary_minmaxRevision_BitWidthPadding = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
enum AMD_shader_trinary_minmaxInstructions {
|
||||||
|
AMD_shader_trinary_minmaxFMin3AMD = 1,
|
||||||
|
AMD_shader_trinary_minmaxUMin3AMD = 2,
|
||||||
|
AMD_shader_trinary_minmaxSMin3AMD = 3,
|
||||||
|
AMD_shader_trinary_minmaxFMax3AMD = 4,
|
||||||
|
AMD_shader_trinary_minmaxUMax3AMD = 5,
|
||||||
|
AMD_shader_trinary_minmaxSMax3AMD = 6,
|
||||||
|
AMD_shader_trinary_minmaxFMid3AMD = 7,
|
||||||
|
AMD_shader_trinary_minmaxUMid3AMD = 8,
|
||||||
|
AMD_shader_trinary_minmaxSMid3AMD = 9,
|
||||||
|
AMD_shader_trinary_minmaxInstructionsMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // SPIRV_UNIFIED1_AMD_shader_trinary_minmax_H_
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Arm Ltd.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#ifndef SPIRV_UNIFIED1_ArmExperimentalMLOperations_H_
|
||||||
|
#define SPIRV_UNIFIED1_ArmExperimentalMLOperations_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum {
|
||||||
|
ArmExperimentalMLOperationsRevision = 1,
|
||||||
|
ArmExperimentalMLOperationsRevision_BitWidthPadding = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ArmExperimentalMLOperationsInstructions {
|
||||||
|
ArmExperimentalMLOperationsCALL = 0,
|
||||||
|
ArmExperimentalMLOperationsInstructionsMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // SPIRV_UNIFIED1_ArmExperimentalMLOperations_H_
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-2025 Arm Ltd.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#ifndef SPIRV_UNIFIED1_ArmMotionEngine_100_H_
|
||||||
|
#define SPIRV_UNIFIED1_ArmMotionEngine_100_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum {
|
||||||
|
ArmMotionEngineVersion = 100,
|
||||||
|
ArmMotionEngineVersion_BitWidthPadding = 0x7fffffff
|
||||||
|
};
|
||||||
|
enum {
|
||||||
|
ArmMotionEngineRevision = 1,
|
||||||
|
ArmMotionEngineRevision_BitWidthPadding = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ArmMotionEngineInstructions {
|
||||||
|
ArmMotionEngineMIN_SAD = 0,
|
||||||
|
ArmMotionEngineMIN_SAD_COST = 1,
|
||||||
|
ArmMotionEngineRAW_SAD = 2,
|
||||||
|
ArmMotionEngineInstructionsMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // SPIRV_UNIFIED1_ArmMotionEngine_100_H_
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
// Copyright: 2017-2024 The Khronos Group Inc.
|
||||||
|
// License: MIT
|
||||||
|
//
|
||||||
|
// MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS KHRONOS
|
||||||
|
// STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS SPECIFICATIONS AND
|
||||||
|
// HEADER INFORMATION ARE LOCATED AT https://www.khronos.org/registry/
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef SPIRV_UNIFIED1_DebugInfo_H_
|
||||||
|
#define SPIRV_UNIFIED1_DebugInfo_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum {
|
||||||
|
DebugInfoVersion = 100,
|
||||||
|
DebugInfoVersion_BitWidthPadding = 0x7fffffff
|
||||||
|
};
|
||||||
|
enum {
|
||||||
|
DebugInfoRevision = 1,
|
||||||
|
DebugInfoRevision_BitWidthPadding = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
enum DebugInfoInstructions {
|
||||||
|
DebugInfoDebugInfoNone = 0,
|
||||||
|
DebugInfoDebugCompilationUnit = 1,
|
||||||
|
DebugInfoDebugTypeBasic = 2,
|
||||||
|
DebugInfoDebugTypePointer = 3,
|
||||||
|
DebugInfoDebugTypeQualifier = 4,
|
||||||
|
DebugInfoDebugTypeArray = 5,
|
||||||
|
DebugInfoDebugTypeVector = 6,
|
||||||
|
DebugInfoDebugTypedef = 7,
|
||||||
|
DebugInfoDebugTypeFunction = 8,
|
||||||
|
DebugInfoDebugTypeEnum = 9,
|
||||||
|
DebugInfoDebugTypeComposite = 10,
|
||||||
|
DebugInfoDebugTypeMember = 11,
|
||||||
|
DebugInfoDebugTypeInheritance = 12,
|
||||||
|
DebugInfoDebugTypePtrToMember = 13,
|
||||||
|
DebugInfoDebugTypeTemplate = 14,
|
||||||
|
DebugInfoDebugTypeTemplateParameter = 15,
|
||||||
|
DebugInfoDebugTypeTemplateTemplateParameter = 16,
|
||||||
|
DebugInfoDebugTypeTemplateParameterPack = 17,
|
||||||
|
DebugInfoDebugGlobalVariable = 18,
|
||||||
|
DebugInfoDebugFunctionDeclaration = 19,
|
||||||
|
DebugInfoDebugFunction = 20,
|
||||||
|
DebugInfoDebugLexicalBlock = 21,
|
||||||
|
DebugInfoDebugLexicalBlockDiscriminator = 22,
|
||||||
|
DebugInfoDebugScope = 23,
|
||||||
|
DebugInfoDebugNoScope = 24,
|
||||||
|
DebugInfoDebugInlinedAt = 25,
|
||||||
|
DebugInfoDebugLocalVariable = 26,
|
||||||
|
DebugInfoDebugInlinedVariable = 27,
|
||||||
|
DebugInfoDebugDeclare = 28,
|
||||||
|
DebugInfoDebugValue = 29,
|
||||||
|
DebugInfoDebugOperation = 30,
|
||||||
|
DebugInfoDebugExpression = 31,
|
||||||
|
DebugInfoDebugMacroDef = 32,
|
||||||
|
DebugInfoDebugMacroUndef = 33,
|
||||||
|
DebugInfoInstructionsMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
enum DebugInfoDebugInfoFlags {
|
||||||
|
DebugInfoNone = 0x0000,
|
||||||
|
DebugInfoFlagIsProtected = 0x01,
|
||||||
|
DebugInfoFlagIsPrivate = 0x02,
|
||||||
|
DebugInfoFlagIsPublic = 0x03,
|
||||||
|
DebugInfoFlagIsLocal = 0x04,
|
||||||
|
DebugInfoFlagIsDefinition = 0x08,
|
||||||
|
DebugInfoFlagFwdDecl = 0x10,
|
||||||
|
DebugInfoFlagArtificial = 0x20,
|
||||||
|
DebugInfoFlagExplicit = 0x40,
|
||||||
|
DebugInfoFlagPrototyped = 0x80,
|
||||||
|
DebugInfoFlagObjectPointer = 0x100,
|
||||||
|
DebugInfoFlagStaticMember = 0x200,
|
||||||
|
DebugInfoFlagIndirectVariable = 0x400,
|
||||||
|
DebugInfoFlagLValueReference = 0x800,
|
||||||
|
DebugInfoFlagRValueReference = 0x1000,
|
||||||
|
DebugInfoFlagIsOptimized = 0x2000,
|
||||||
|
DebugInfoDebugInfoFlagsMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
enum DebugInfoDebugBaseTypeAttributeEncoding {
|
||||||
|
DebugInfoUnspecified = 0,
|
||||||
|
DebugInfoAddress = 1,
|
||||||
|
DebugInfoBoolean = 2,
|
||||||
|
DebugInfoFloat = 4,
|
||||||
|
DebugInfoSigned = 5,
|
||||||
|
DebugInfoSignedChar = 6,
|
||||||
|
DebugInfoUnsigned = 7,
|
||||||
|
DebugInfoUnsignedChar = 8,
|
||||||
|
DebugInfoDebugBaseTypeAttributeEncodingMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
enum DebugInfoDebugCompositeType {
|
||||||
|
DebugInfoClass = 0,
|
||||||
|
DebugInfoStructure = 1,
|
||||||
|
DebugInfoUnion = 2,
|
||||||
|
DebugInfoDebugCompositeTypeMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
enum DebugInfoDebugTypeQualifier {
|
||||||
|
DebugInfoConstType = 0,
|
||||||
|
DebugInfoVolatileType = 1,
|
||||||
|
DebugInfoRestrictType = 2,
|
||||||
|
DebugInfoDebugTypeQualifierMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
enum DebugInfoDebugOperation {
|
||||||
|
DebugInfoDeref = 0,
|
||||||
|
DebugInfoPlus = 1,
|
||||||
|
DebugInfoMinus = 2,
|
||||||
|
DebugInfoPlusUconst = 3,
|
||||||
|
DebugInfoBitPiece = 4,
|
||||||
|
DebugInfoSwap = 5,
|
||||||
|
DebugInfoXderef = 6,
|
||||||
|
DebugInfoStackValue = 7,
|
||||||
|
DebugInfoConstu = 8,
|
||||||
|
DebugInfoDebugOperationMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // SPIRV_UNIFIED1_DebugInfo_H_
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
** SPDX-FileCopyrightText: 2014-2024 The Khronos Group Inc.
|
||||||
|
** SPDX-License-Identifier: MIT
|
||||||
|
**
|
||||||
|
** MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS
|
||||||
|
** KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS
|
||||||
|
** SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT
|
||||||
|
** https://www.khronos.org/registry/
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef GLSLstd450_H
|
||||||
|
#define GLSLstd450_H
|
||||||
|
|
||||||
|
static const int GLSLstd450Version = 100;
|
||||||
|
static const int GLSLstd450Revision = 3;
|
||||||
|
|
||||||
|
enum GLSLstd450 {
|
||||||
|
GLSLstd450Bad = 0, // Don't use
|
||||||
|
|
||||||
|
GLSLstd450Round = 1,
|
||||||
|
GLSLstd450RoundEven = 2,
|
||||||
|
GLSLstd450Trunc = 3,
|
||||||
|
GLSLstd450FAbs = 4,
|
||||||
|
GLSLstd450SAbs = 5,
|
||||||
|
GLSLstd450FSign = 6,
|
||||||
|
GLSLstd450SSign = 7,
|
||||||
|
GLSLstd450Floor = 8,
|
||||||
|
GLSLstd450Ceil = 9,
|
||||||
|
GLSLstd450Fract = 10,
|
||||||
|
|
||||||
|
GLSLstd450Radians = 11,
|
||||||
|
GLSLstd450Degrees = 12,
|
||||||
|
GLSLstd450Sin = 13,
|
||||||
|
GLSLstd450Cos = 14,
|
||||||
|
GLSLstd450Tan = 15,
|
||||||
|
GLSLstd450Asin = 16,
|
||||||
|
GLSLstd450Acos = 17,
|
||||||
|
GLSLstd450Atan = 18,
|
||||||
|
GLSLstd450Sinh = 19,
|
||||||
|
GLSLstd450Cosh = 20,
|
||||||
|
GLSLstd450Tanh = 21,
|
||||||
|
GLSLstd450Asinh = 22,
|
||||||
|
GLSLstd450Acosh = 23,
|
||||||
|
GLSLstd450Atanh = 24,
|
||||||
|
GLSLstd450Atan2 = 25,
|
||||||
|
|
||||||
|
GLSLstd450Pow = 26,
|
||||||
|
GLSLstd450Exp = 27,
|
||||||
|
GLSLstd450Log = 28,
|
||||||
|
GLSLstd450Exp2 = 29,
|
||||||
|
GLSLstd450Log2 = 30,
|
||||||
|
GLSLstd450Sqrt = 31,
|
||||||
|
GLSLstd450InverseSqrt = 32,
|
||||||
|
|
||||||
|
GLSLstd450Determinant = 33,
|
||||||
|
GLSLstd450MatrixInverse = 34,
|
||||||
|
|
||||||
|
GLSLstd450Modf = 35, // second operand needs an OpVariable to write to
|
||||||
|
GLSLstd450ModfStruct = 36, // no OpVariable operand
|
||||||
|
GLSLstd450FMin = 37,
|
||||||
|
GLSLstd450UMin = 38,
|
||||||
|
GLSLstd450SMin = 39,
|
||||||
|
GLSLstd450FMax = 40,
|
||||||
|
GLSLstd450UMax = 41,
|
||||||
|
GLSLstd450SMax = 42,
|
||||||
|
GLSLstd450FClamp = 43,
|
||||||
|
GLSLstd450UClamp = 44,
|
||||||
|
GLSLstd450SClamp = 45,
|
||||||
|
GLSLstd450FMix = 46,
|
||||||
|
GLSLstd450IMix = 47, // Reserved
|
||||||
|
GLSLstd450Step = 48,
|
||||||
|
GLSLstd450SmoothStep = 49,
|
||||||
|
|
||||||
|
GLSLstd450Fma = 50,
|
||||||
|
GLSLstd450Frexp = 51, // second operand needs an OpVariable to write to
|
||||||
|
GLSLstd450FrexpStruct = 52, // no OpVariable operand
|
||||||
|
GLSLstd450Ldexp = 53,
|
||||||
|
|
||||||
|
GLSLstd450PackSnorm4x8 = 54,
|
||||||
|
GLSLstd450PackUnorm4x8 = 55,
|
||||||
|
GLSLstd450PackSnorm2x16 = 56,
|
||||||
|
GLSLstd450PackUnorm2x16 = 57,
|
||||||
|
GLSLstd450PackHalf2x16 = 58,
|
||||||
|
GLSLstd450PackDouble2x32 = 59,
|
||||||
|
GLSLstd450UnpackSnorm2x16 = 60,
|
||||||
|
GLSLstd450UnpackUnorm2x16 = 61,
|
||||||
|
GLSLstd450UnpackHalf2x16 = 62,
|
||||||
|
GLSLstd450UnpackSnorm4x8 = 63,
|
||||||
|
GLSLstd450UnpackUnorm4x8 = 64,
|
||||||
|
GLSLstd450UnpackDouble2x32 = 65,
|
||||||
|
|
||||||
|
GLSLstd450Length = 66,
|
||||||
|
GLSLstd450Distance = 67,
|
||||||
|
GLSLstd450Cross = 68,
|
||||||
|
GLSLstd450Normalize = 69,
|
||||||
|
GLSLstd450FaceForward = 70,
|
||||||
|
GLSLstd450Reflect = 71,
|
||||||
|
GLSLstd450Refract = 72,
|
||||||
|
|
||||||
|
GLSLstd450FindILsb = 73,
|
||||||
|
GLSLstd450FindSMsb = 74,
|
||||||
|
GLSLstd450FindUMsb = 75,
|
||||||
|
|
||||||
|
GLSLstd450InterpolateAtCentroid = 76,
|
||||||
|
GLSLstd450InterpolateAtSample = 77,
|
||||||
|
GLSLstd450InterpolateAtOffset = 78,
|
||||||
|
|
||||||
|
GLSLstd450NMin = 79,
|
||||||
|
GLSLstd450NMax = 80,
|
||||||
|
GLSLstd450NClamp = 81,
|
||||||
|
|
||||||
|
GLSLstd450Count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // #ifndef GLSLstd450_H
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020-2024 The Khronos Group Inc.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS
|
||||||
|
// KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS
|
||||||
|
// SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT
|
||||||
|
// https://www.khronos.org/registry/
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef SPIRV_UNIFIED1_NonSemanticClspvReflection_H_
|
||||||
|
#define SPIRV_UNIFIED1_NonSemanticClspvReflection_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum {
|
||||||
|
NonSemanticClspvReflectionRevision = 7,
|
||||||
|
NonSemanticClspvReflectionRevision_BitWidthPadding = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
enum NonSemanticClspvReflectionInstructions {
|
||||||
|
NonSemanticClspvReflectionKernel = 1,
|
||||||
|
NonSemanticClspvReflectionArgumentInfo = 2,
|
||||||
|
NonSemanticClspvReflectionArgumentStorageBuffer = 3,
|
||||||
|
NonSemanticClspvReflectionArgumentUniform = 4,
|
||||||
|
NonSemanticClspvReflectionArgumentPodStorageBuffer = 5,
|
||||||
|
NonSemanticClspvReflectionArgumentPodUniform = 6,
|
||||||
|
NonSemanticClspvReflectionArgumentPodPushConstant = 7,
|
||||||
|
NonSemanticClspvReflectionArgumentSampledImage = 8,
|
||||||
|
NonSemanticClspvReflectionArgumentStorageImage = 9,
|
||||||
|
NonSemanticClspvReflectionArgumentSampler = 10,
|
||||||
|
NonSemanticClspvReflectionArgumentWorkgroup = 11,
|
||||||
|
NonSemanticClspvReflectionSpecConstantWorkgroupSize = 12,
|
||||||
|
NonSemanticClspvReflectionSpecConstantGlobalOffset = 13,
|
||||||
|
NonSemanticClspvReflectionSpecConstantWorkDim = 14,
|
||||||
|
NonSemanticClspvReflectionPushConstantGlobalOffset = 15,
|
||||||
|
NonSemanticClspvReflectionPushConstantEnqueuedLocalSize = 16,
|
||||||
|
NonSemanticClspvReflectionPushConstantGlobalSize = 17,
|
||||||
|
NonSemanticClspvReflectionPushConstantRegionOffset = 18,
|
||||||
|
NonSemanticClspvReflectionPushConstantNumWorkgroups = 19,
|
||||||
|
NonSemanticClspvReflectionPushConstantRegionGroupOffset = 20,
|
||||||
|
NonSemanticClspvReflectionConstantDataStorageBuffer = 21,
|
||||||
|
NonSemanticClspvReflectionConstantDataUniform = 22,
|
||||||
|
NonSemanticClspvReflectionLiteralSampler = 23,
|
||||||
|
NonSemanticClspvReflectionPropertyRequiredWorkgroupSize = 24,
|
||||||
|
NonSemanticClspvReflectionSpecConstantSubgroupMaxSize = 25,
|
||||||
|
NonSemanticClspvReflectionArgumentPointerPushConstant = 26,
|
||||||
|
NonSemanticClspvReflectionArgumentPointerUniform = 27,
|
||||||
|
NonSemanticClspvReflectionProgramScopeVariablesStorageBuffer = 28,
|
||||||
|
NonSemanticClspvReflectionProgramScopeVariablePointerRelocation = 29,
|
||||||
|
NonSemanticClspvReflectionImageArgumentInfoChannelOrderPushConstant = 30,
|
||||||
|
NonSemanticClspvReflectionImageArgumentInfoChannelDataTypePushConstant = 31,
|
||||||
|
NonSemanticClspvReflectionImageArgumentInfoChannelOrderUniform = 32,
|
||||||
|
NonSemanticClspvReflectionImageArgumentInfoChannelDataTypeUniform = 33,
|
||||||
|
NonSemanticClspvReflectionArgumentStorageTexelBuffer = 34,
|
||||||
|
NonSemanticClspvReflectionArgumentUniformTexelBuffer = 35,
|
||||||
|
NonSemanticClspvReflectionConstantDataPointerPushConstant = 36,
|
||||||
|
NonSemanticClspvReflectionProgramScopeVariablePointerPushConstant = 37,
|
||||||
|
NonSemanticClspvReflectionPrintfInfo = 38,
|
||||||
|
NonSemanticClspvReflectionPrintfBufferStorageBuffer = 39,
|
||||||
|
NonSemanticClspvReflectionPrintfBufferPointerPushConstant = 40,
|
||||||
|
NonSemanticClspvReflectionNormalizedSamplerMaskPushConstant = 41,
|
||||||
|
NonSemanticClspvReflectionWorkgroupVariableSize = 42,
|
||||||
|
NonSemanticClspvReflectionInstructionsMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
enum NonSemanticClspvReflectionKernelPropertyFlags {
|
||||||
|
NonSemanticClspvReflectionNone = 0x0,
|
||||||
|
NonSemanticClspvReflectionMayUsePrintf = 0x1,
|
||||||
|
NonSemanticClspvReflectionKernelPropertyFlagsMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // SPIRV_UNIFIED1_NonSemanticClspvReflection_H_
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020-2024 The Khronos Group Inc.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS
|
||||||
|
// KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS
|
||||||
|
// SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT
|
||||||
|
// https://www.khronos.org/registry/
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef SPIRV_UNIFIED1_NonSemanticDebugBreak_H_
|
||||||
|
#define SPIRV_UNIFIED1_NonSemanticDebugBreak_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum {
|
||||||
|
NonSemanticDebugBreakRevision = 1,
|
||||||
|
NonSemanticDebugBreakRevision_BitWidthPadding = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
enum NonSemanticDebugBreakInstructions {
|
||||||
|
NonSemanticDebugBreakDebugBreak = 1,
|
||||||
|
NonSemanticDebugBreakInstructionsMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // SPIRV_UNIFIED1_NonSemanticDebugBreak_H_
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020-2024 The Khronos Group Inc.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS
|
||||||
|
// KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS
|
||||||
|
// SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT
|
||||||
|
// https://www.khronos.org/registry/
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef SPIRV_UNIFIED1_NonSemanticDebugPrintf_H_
|
||||||
|
#define SPIRV_UNIFIED1_NonSemanticDebugPrintf_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum {
|
||||||
|
NonSemanticDebugPrintfRevision = 1,
|
||||||
|
NonSemanticDebugPrintfRevision_BitWidthPadding = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
enum NonSemanticDebugPrintfInstructions {
|
||||||
|
NonSemanticDebugPrintfDebugPrintf = 1,
|
||||||
|
NonSemanticDebugPrintfInstructionsMax = 0x7fffffff
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // SPIRV_UNIFIED1_NonSemanticDebugPrintf_H_
|
||||||