3 Commits
Author SHA1 Message Date
ToastconcernandClaude Opus 5.5 ba332a16d8 docs: README Frame Home-only install, Steam Frame download row [skip ci]
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:10:22 -05:00
ToastconcernandClaude Opus 5.5 0601a8a436 docs: reorganize README [skip ci]
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:57:24 -05:00
ToastconcernandClaude Opus 5.5 6d0b4fcd2f v0.2.0: splats, copy/paste, faster homes, seat facing, and a full bug sweep
Frame Studio
- Gaussian splats (PLY, SPZ v2-v4), PLY vertex colours, panorama skies
- Copy, cut, paste and duplicate for objects, splats and world items
- Walkable-area rebake moved off the UI thread; viewport one frame sooner;
  refresh-locked pacing (FS_PACE=timer for the old one)
- Fixed: bundle thumbnail use-after-free crash, auto-save deleting
  environments, saves into scratch copies, edits lost on non-ASCII paths,
  unopenable manifests with inf/nan, bundle save data loss, opening a home
  marking it changed, gizmo NaN, long paths, many malformed-file crashes
- Updater verifies sha256, journals the swap and never leaves the exe missing

Frame Home
- Seats turn you to face their way
- Draw batching (Hand Tutorials up to ~20% less GPU time), cut-out EQUAL pass,
  late head-pose read before submit
- Proper queue-family hand-over to the compositor layer; layer descriptor
  generations and seqlocked slots; structural SPIR-V check
- Fixed: hangs/crashes on damaged .fenv files and shaders, render-pass
  validation error, sync hazard in the foveation smoothing pass, unverified
  update downloads, half-done installs

Tests: tests_loco/env_share/spv_check wired into CMake (FRAME_TESTS) and CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VsaXGU27gsrXjDoFtyFJP9
2026-10-25 13:15:07 -05:00
544 changed files with 210730 additions and 835 deletions

No files matched your search

+61 -12
View File
@@ -44,6 +44,8 @@ jobs:
steps:
- name: Check out source
uses: actions/checkout@v4
with:
fetch-depth: 0 # all of main: the version shown is VERSION + its commit count
- name: Restore compiler cache
uses: actions/cache@v5
with:
@@ -90,6 +92,8 @@ jobs:
steps:
- name: Check out source
uses: actions/checkout@v4
with:
fetch-depth: 0 # all of main: the version shown is VERSION + its commit count
- name: Download Frame Home payload
uses: actions/download-artifact@v4
with:
@@ -131,6 +135,8 @@ jobs:
steps:
- name: Check out source
uses: actions/checkout@v4
with:
fetch-depth: 0 # all of main: the version shown is VERSION + its commit count
- name: Restore compiler cache
uses: actions/cache@v5
with:
@@ -182,8 +188,9 @@ jobs:
uses: actions/cache@v5
with:
path: .apt-cache/archives
key: apt-ubuntu-24.04-arm64-${{ hashFiles('.github/workflows/build.yml') }}
restore-keys: apt-ubuntu-24.04-arm64-
# (its own key: the Linux arm64 Studio job caches other packages, and the first to save a key keeps it)
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
run: |
set -euo pipefail
@@ -194,6 +201,11 @@ jobs:
sudo chown -R "$(id -u):$(id -g)" "$APT_CACHE_DIR/archives"
- name: Build payload and installer
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
run: ccache --show-stats
- name: Upload Frame Home payload
@@ -225,7 +237,8 @@ jobs:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
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)
if: github.ref == 'refs/heads/main'
uses: actions/checkout@v4
@@ -235,7 +248,8 @@ jobs:
- name: Version
if: github.ref == 'refs/heads/main'
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"
- name: Download Studio build artifacts
@@ -302,9 +316,12 @@ jobs:
# and the files in this release.
- name: latest.json
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)
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
- name: Attest Studio release archives
uses: actions/attest-build-provenance@v2
@@ -316,21 +333,53 @@ jobs:
subject-path: dist/FrameHome-installer.zip
- name: Version release (tag v*)
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
# 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)
if: ${{ github.ref == 'refs/heads/main' && !contains(needs.*.result, 'failure') }}
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')
# 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 \
--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).
# 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)
if: github.ref == 'refs/heads/main'
run: |
gh release delete latest --repo "$REPO" --yes --cleanup-tag 2>/dev/null || true
gh release create latest dist/* --repo "$REPO" --prerelease --target "${{ github.sha }}" \
--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."
set -euo pipefail
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."
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"
+42 -1
View File
@@ -1,24 +1,65 @@
# build output
/build*/
/dist/
/out/
/frame/payload/bin/
/frame/payload/layer/libframe_home_layer.so
/frame/payload/layer/env_ps.spv
/frame/payload/SOURCE
studio/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
.vscode/
.vs/
.idea/
.fleet/
*.swp
*.swo
*~
.DS_Store
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
*.fenv
*.fbundle
*.pem
*.key
*.p12
*.pfx
id_rsa*
id_ed25519*
.env
*Zone.Identifier
# astcenc's own test framework (not built here)
third_party/astcenc/Source/GoogleTest/
+155
View File
@@ -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).
+56 -1
View File
@@ -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")
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.
add_library(frame-scene STATIC
src/hsl_scene.cpp
@@ -77,6 +86,8 @@ add_library(frame-scene STATIC
src/anim_bake.cpp
src/fenv_lzma.cpp
src/model_import.cpp
src/splat_import.cpp
src/spv_opt.cpp
"${TP}/ufbx/ufbx.c"
"${TP}/qhe/cook/hzanim_acl.cpp"
"${TP}/stb/stb_image_impl.c")
@@ -93,7 +104,15 @@ target_include_directories(frame-scene PUBLIC
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set_source_files_properties("${TP}/qhe/cook/hzanim_acl.cpp" PROPERTIES COMPILE_OPTIONS "-flax-vector-conversions")
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
# static library first, so a release build doesn't need it installed on the headset.
find_library(LIBDEFLATE_LIB NAMES libdeflate.a deflate)
@@ -122,6 +141,7 @@ target_link_libraries(opusfile PUBLIC opus ogg)
add_executable(frame-home
src/main.cpp
src/hsl_renderer.cpp
src/hsl_renderer_splat.cpp
src/panorama.cpp
"${TP}/stb/stb_vorbis_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.
add_executable(frame-switcher src/switcher.cpp)
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)
target_include_directories(frame-switcher PRIVATE "${TP}/vulkan-headers/include")
endif()
@@ -160,3 +185,33 @@ endif()
if(NOT FRAME_HOME_PC)
set_target_properties(frame-switcher PROPERTIES BUILD_RPATH "${OPENVR_LIB_DIR}" INSTALL_RPATH "${OPENVR_LIB_DIR}")
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()
+73 -39
View File
@@ -1,45 +1,80 @@
# Frame Studio and Frame Home
**Frame Home** makes your homes the Steam Frame's environment. Meta Quest homes (HSL, Clay `.opa`, glTF) and models
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
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.
Turn Meta Quest homes and 3D models into native environments for the Steam Frame.
| | Runs on | What it does |
|---|---|---|
| **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
1. **Download** Frame Studio for your computer from the Releases page (**Latest build**, or a version release) and
unpack it. See [Getting started](docs/getting-started.md).
1. **Download** Frame Studio for your computer from the Releases page and unpack it.
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).
3. **Install Frame Home**: in Studio, **Headset > Install on Steam Frame...**, then **Install Frame Home**.
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.
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
Every push builds Frame Studio and its matching Frame Home payload. The **Latest build** release contains both Studio
packages and `FrameHome-installer.zip`; each `v*` version release contains both products too. Studio updates from its
top bar, and Frame Home updates from the Environments panel on the headset. Both use the same release channel.
Every release on the Releases page (**Latest build**, or a numbered `v*` version) contains Frame Studio for each
computer below and the matching Frame Home payload.
| Computer | Frame Studio |
| Computer | File |
|---|---|
| Windows, 64-bit PCs | `FrameStudio-windows-x64.zip` |
| Windows on ARM (Snapdragon, Surface Pro X...) | `FrameStudio-windows-arm64.zip` |
| 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` |
| 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
runs on Metal through MoltenVK, which comes inside the app. 32-bit Windows and Linux and Intel Macs have no ready-made
download; Studio can still be built for them from source ([docs/building.md](docs/building.md)).
**Requirements.** Studio needs a GPU with Vulkan 1.2: any recent NVIDIA, AMD or Intel driver on Windows and Linux.
On a Mac, Vulkan runs on Metal through MoltenVK, which comes inside the app. 32-bit Windows and Linux and Intel Macs
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.
- 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.
- 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.
- Move, rotate, scale, hide and delete objects, with undo and auto-saved versions.
- Shape the walkable area: auto-generated surfaces, drawn floors, spawns, seats, hotspots, gravity zones.
- Add a background sound and a picture; set and calculate per-world settings.
- Saves baked `.fenv` files and leaves out what can't be seen; sends them to the headset in one click.
- Packs several environments into one `.fbundle` to share or install together ([bundles](docs/bundles.md)).
- Debug views (wireframe, meshes, GPU cost, overdraw), a home report, and the headset's settings and debug page
from your desk.
- A navigation gizmo in the viewport corner: drag it to orbit, click an axis to snap to that view.
- Themes, from dark to light, drawn from the MetaDB palette (**Window > Theme**).
- **Open** Meta homes (HSL, Clay, glTF), `.fenv` environments and Blender or Unity models; pair rooms with vistas.
- **Edit**: move, rotate, scale, hide and delete objects, with undo and auto-saved versions. A navigation gizmo in
the viewport corner orbits the view or snaps it to an axis.
- **Shape the walkable area**: auto-generated surfaces, drawn floors, spawns, seats, hotspots, gravity zones.
- **Set the mood**: a background sound, a picture, and per-world settings you can set or calculate.
- **Save and send**: baked `.fenv` files that leave out what can't be seen, sent to the headset in one click, or
packed with others into one `.fbundle` to share or install together ([bundles](docs/bundles.md)).
- **Check it**: debug views (wireframe, meshes, GPU cost, overdraw), a home report, and the headset's settings and
debug page from your desk.
- Themes, from dark to light (**Window > Theme**).
## 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 |
| [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 |
| [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 |
| [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` |
| [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 |
| [Building from source](docs/building.md) | Building on each system, CI, and Frame Home payload builds |
## 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 |
|---|---|
| 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` |
Frame Home binaries are built in GitHub Actions and are not checked into `frame/payload/`. Full details, prerequisites
and CI: [docs/building.md](docs/building.md).
Frame Home binaries are built in GitHub Actions and are not checked into `frame/payload/`. Prerequisites, CI and the
headset side: [docs/building.md](docs/building.md).
## Repository layout
### Repository layout
| 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.
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`
started from Quest Home Editor and keep its MIT license for the original code; see
[third_party/qhe/LICENSE.md](third_party/qhe/LICENSE.md).
the SIL Open Font License (`third_party/inter/LICENSE.txt`). The home loaders in `third_party/qhe` started from Quest
Home Editor and keep its MIT license for the original code; see [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
Meta content is included in this repository or its downloads.
+1 -1
View File
@@ -1 +1 @@
0.1
0.2
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

+1
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

+1
View File
@@ -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

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+1
View File
@@ -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

+32
View File
@@ -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}")
+1 -1
View File
@@ -111,7 +111,7 @@ edges, grey are floors you can't reach. **W** shows or hides it.
| Item | What it does on the headset |
|---|---|
| **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 |
| **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 |
+21 -3
View File
@@ -33,9 +33,22 @@ $scp = Find-Tool "scp"
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).
# 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) {
$n = ([IO.Path]::GetFileName($path)).ToLowerInvariant() -replace " ", "-"
return ($n -replace "[^a-z0-9._-]", "")
$base = [IO.Path]::GetFileName($path)
$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"
@@ -62,6 +75,11 @@ if ($Uninstall) {
Say "copying $([IO.Path]::GetFileName($f)) -> $name"
Push-Location (Split-Path -Parent (Resolve-Path $f))
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 "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
if ($first -and (Test-Path $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"
+20 -5
View File
@@ -26,6 +26,20 @@ $1"; shift ;;
done
case "$HOST" in *@*) ;; *) HOST="steamos@$HOST" ;; esac
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; }
say "connecting to $HOST"
@@ -40,17 +54,18 @@ if [ "$MODE" = install ]; then
printf '%s\n' "$ENVS" | while IFS= read -r f; do
[ -n "$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=$(basename "$f" | tr 'A-Z ' 'a-z-' | tr -cd 'a-z0-9._-')
name=$(frame_name "$f")
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
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)
FIRST=$(printf '%s\n' "$ENVS" | grep -i '\.fenv$' | sed -n '1p')
if [ -n "$FIRST" ] && [ -f "$FIRST" ]; then
name=$(basename "$FIRST" | tr 'A-Z ' 'a-z-' | tr -cd 'a-z0-9._-')
ssh "$HOST" "printf '%s\n' '$name' > frame-home/current"
name=$(frame_name "$FIRST")
# (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
else
# (never installed from a copy like this one: Frame Home itself puts SteamVR's environment back)
+26 -4
View File
@@ -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
fi
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
[ -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
# The compositor layer and its shader (renamed over the old ones: a running compositor keeps its copy).
layerChanged=0
@@ -74,7 +87,8 @@ if [ -f "$P/layer/libframe_home_layer.so" ]; then
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/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"
[ -f "$LAYERJSON" ] || layerChanged=1
cat > "$LAYERJSON" <<JSON
@@ -94,7 +108,15 @@ if [ -f "$P/layer/libframe_home_layer.so" ]; then
JSON
fi
# 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.
for f in "$P"/envs/*.fenv; do [ -f "$f" ] && mv -f "$f" "$H/envs/"; done
+9 -1
View File
@@ -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
// 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).
// 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.
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)
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 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;
layout(location = 1) in vec3 worldPos;
@@ -30,8 +34,12 @@ layout(location = 0) out vec4 color;
void main() {
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;
// 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);
vec3 rgb = vec3(0.0);
if (c.w > 1e-5 && homeFrame.params.y > 0.5) {
+532 -136
View File
@@ -26,8 +26,10 @@
#include <sys/un.h>
#include <unistd.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
@@ -138,13 +140,16 @@ struct InstanceData {
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(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 {
VkDevice device = VK_NULL_HANDLE;
VkPhysicalDevice physical = VK_NULL_HANDLE;
InstanceData* inst = 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
#define FH_DECL(n) PFN_vk##n n = nullptr;
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_physToInstance; // physical device (dispatch key) -> instance data
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) {
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_camera; // passthrough (the cameras' reprojection)
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
// too: they're told apart by their index count, read from the model itself.
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::atomic<int> g_traceDraws{0}; // ~/frame-home/layer/trace: log this many draws
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.
//
// 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 {
DeviceData* d = nullptr;
VkShaderModule fragment = VK_NULL_HANDLE;
VkDescriptorSetLayout setLayout = VK_NULL_HANDLE;
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;
// The imported frames (one generation per Hello).
struct Slot { VkImage image = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE;
VkBuffer ubo = VK_NULL_HANDLE; VkDeviceMemory uboMem = VK_NULL_HANDLE; void* uboMapped = nullptr;
VkDescriptorSet set = VK_NULL_HANDLE, setSubsampled = VK_NULL_HANDLE; uint64_t uboFrame = ~0ull; };
Slot slot[envshare::kSlots];
bool imported = false;
bool subsampled = false; // the imported images are subsampled (drawn with the subsampled twins)
// Each slot's uniforms are a ring of kUboRing blocks, one per compositor frame (a dynamic offset picks it): what a
// frame is drawn with changes every compositor frame (the turn carried on, the fade), and the block the last one is
// still drawing with on the GPU mustn't change under it. Shared by every generation (only the offset is bound).
struct Ubo { VkBuffer buffer = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; void* mapped = nullptr; };
Ubo ubo[envshare::kSlots];
Gen* cur = nullptr; // the generation drawn now
std::vector<Gen*> retired; // older ones, until no command buffer uses them
uint32_t generation = 0; // of the imported images
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;
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;
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;
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
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;
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.
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;
}
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;
if (!d) return;
for (auto& s : g_ours.slot) {
if (s.view) d->DestroyImageView(d->device, s.view, nullptr);
if (s.image) d->DestroyImage(d->device, s.image, nullptr);
if (s.mem) d->FreeMemory(d->device, s.mem, nullptr);
s.view = VK_NULL_HANDLE; s.image = VK_NULL_HANDLE; s.mem = VK_NULL_HANDLE;
s.uboFrame = ~0ull;
auto& r = g_ours.retired;
for (size_t i = 0; i < r.size();) {
if (r[i]->users.empty()) { destroyGen(d, r[i]); r.erase(r.begin() + static_cast<std::ptrdiff_t>(i)); }
else ++i;
}
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);
g_ours.state = nullptr;
g_ours.imported = false;
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) {
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;
@@ -268,7 +396,7 @@ bool ensureBase(DeviceData* d) {
if (d->CreateShaderModule(d->device, &smi, nullptr, &g_ours.fragment) != VK_SUCCESS) return false;
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[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};
li.bindingCount = 2;
li.pBindings = b;
@@ -287,47 +415,34 @@ bool ensureBase(DeviceData* d) {
if (d->CreateDescriptorSetLayout(d->device, &sli, nullptr, &g_ours.setLayoutSubsampled) != VK_SUCCESS)
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)");
}
// 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};
si.magFilter = si.minFilter = VK_FILTER_LINEAR;
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;
for (auto& s : g_ours.slot) {
for (auto& u : g_ours.ubo) {
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;
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;
d->GetBufferMemoryRequirements(d->device, s.ubo, &req);
d->GetBufferMemoryRequirements(d->device, u.buffer, &req);
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
ai.allocationSize = req.size;
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;
d->BindBufferMemory(d->device, s.ubo, s.uboMem, 0);
d->MapMemory(d->device, s.uboMem, 0, VK_WHOLE_SIZE, 0, &s.uboMapped);
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)");
}
}
if (ai.memoryTypeIndex == UINT32_MAX || d->AllocateMemory(d->device, &ai, nullptr, &u.mem) != VK_SUCCESS) return false;
d->BindBufferMemory(d->device, u.buffer, u.mem, 0);
if (d->MapMemory(d->device, u.mem, 0, VK_WHOLE_SIZE, 0, &u.mapped) != VK_SUCCESS) return false;
}
g_ours.baseFailed = false;
g_ours.baseReady = true;
@@ -335,7 +450,7 @@ bool ensureBase(DeviceData* d) {
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 importPending(DeviceData* d, VkQueue) {
@@ -356,35 +471,44 @@ void importPending(DeviceData* d, VkQueue) {
d->inst->GetPhysicalDeviceProperties2(d->physical, &pp);
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 last ones (the sets in this very submit never point at destroyed images).
const size_t stateSize = sizeof(envshare::State);
// the last ones.
// 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);
::close(p.stateFd);
p.stateFd = -1;
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):
// otherwise Frame Home is told, and sends plain ones.
const bool subsampled = (p.hello.flags & VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT) != 0;
bool subsampledSets = g_ours.setLayoutSubsampled != VK_NULL_HANDLE;
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)) {
if (p.hello.flags & ~static_cast<uint32_t>(VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT) || (subsampled && !g_ours.setLayoutSubsampled)) {
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);
::munmap(st, stateSize);
closeAll();
return;
}
struct Made { VkImage image = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; } made[envshare::kSlots];
auto dropMade = [&] {
for (Made& m : made) {
if (m.view) d->DestroyImageView(d->device, m.view, nullptr);
if (m.image) d->DestroyImage(d->device, m.image, nullptr);
if (m.mem) d->FreeMemory(d->device, m.mem, nullptr);
}
::munmap(st, stateSize);
};
Gen* g = new Gen();
g->generation = p.hello.generation;
g->layers = p.hello.layers;
g->subsampled = subsampled;
auto dropMade = [&] { destroyGen(d, g); ::munmap(st, stateSize); };
for (uint32_t i = 0; i < envshare::kSlots; ++i) {
Made& s = made[i];
VkExternalMemoryImageCreateInfo ext = {VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
ext.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
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.flags = p.hello.flags;
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) {
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};
imp.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
imp.fd = p.fds[i];
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;
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
ai.pNext = &imp;
ai.allocationSize = p.hello.memorySize[i];
ai.memoryTypeIndex = p.hello.memoryType;
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
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) {
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.format = ci.format;
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; }
}
closeAll();
std::lock_guard<std::mutex> ol(g_oursMutex);
// Swap: nothing of the compositor's (any queue) may still be reading the old frames.
if (g_ours.imported) { d->DeviceWaitIdle(d->device); releaseImports(); }
g_ours.state = static_cast<envshare::State*>(st);
g_ours.stateSize = stateSize;
// This generation's own sets, written now, before any command buffer can bind them (and never written again).
if (!makeSets(d, subsampled ? g_ours.setLayoutSubsampled : g_ours.setLayout, g->pool, g->set)) {
logf("no descriptor sets for the new frames: keeping the last frames");
dropMade();
return;
}
for (uint32_t i = 0; i < envshare::kSlots; ++i) {
auto& s = g_ours.slot[i];
s.image = made[i].image; s.mem = made[i].mem; s.view = made[i].view;
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;
VkDescriptorImageInfo ii = {g_ours.sampler, g->view[i], VK_IMAGE_LAYOUT_GENERAL}; // (the subsampled kind's sampler is in its layout)
VkDescriptorBufferInfo bi = {g_ours.ubo[i].buffer, 0, kUboStride};
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 = &ii;
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[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 = &ii;
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);
}
g_ours.imported = true;
g_ours.subsampled = subsampled;
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,
p.hello.flags, p.hello.generation);
std::lock_guard<std::mutex> ol(g_oursMutex);
// Swap: the old generation stays alive (images, views, sets) until the command buffers drawn with it are done.
retireCurrent();
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.lastGood = -1;
for (auto& s : g_ours.shown) s = envshare::Slot();
for (auto& m : g_ours.shownMotion) m = envshare::Motion();
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();
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
// 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.
std::chrono::steady_clock::time_point g_disconnectedAt;
int liveSlot() {
if (g_off || !g_ours.imported || !g_ours.state) return -1;
// (written by the socket thread, read by the compositor's: steady_clock ticks, atomic)
std::atomic<std::chrono::steady_clock::rep> g_disconnectedAt{0};
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
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;
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);
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;
}
@@ -482,28 +628,45 @@ int frameSlot() {
g_latchSlot = liveSlot();
g_latchNs = now;
g_latchStamped = false;
g_latchRing = (g_latchRing + 1) % kUboRing;
return g_latchSlot;
}
void fillUbo(int k) {
auto& slot = g_ours.slot[k];
// Slot k's uniforms for this compositor frame (block `ring`): the frame's eye rotations, the brightness now, and how
// 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;
const envshare::Slot& src = s->slot[k];
if (slot.uboFrame == src.frame) return;
float data[16 * 2 + 4 * 2 + 4];
const envshare::Slot& src = g_ours.shown[k];
float data[16 * 2 + 4 * 2 + 4 + 4 + 4] = {};
std::memcpy(data, src.viewProj, sizeof src.viewProj);
std::memcpy(data + 32, src.eyePos, sizeof src.eyePos);
data[40] = s->fade.load() / 1000.0f;
data[41] = s->show.load() ? 1.0f : 0.0f;
data[42] = data[43] = 0;
std::memcpy(slot.uboMapped, data, sizeof data);
slot.uboFrame = src.frame;
float turn = 0, depth = 0, dt = 0;
const envshare::Motion m = g_ours.shownMotion[k];
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 ──────────────────────────────────────────────────────────
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
// before it got healthy showing them. A compositor that never got that far, or closed cleanly, doesn't count.
int crashes = 0;
@@ -515,7 +678,7 @@ void startHealth() {
if (crashes >= 2) {
if (FILE* f = std::fopen((dir() + "/off").c_str(), "w")) std::fclose(f);
::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)");
return;
}
@@ -566,11 +729,11 @@ void serve() {
if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_RIGHTS) {
const size_t cnt = (cm->cmsg_len - CMSG_LEN(0)) / sizeof(int);
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
// flags; v3 (an older Frame Home): made for copying into.
const bool v5 = n == static_cast<ssize_t>(sizeof h) && h.version == envshare::kVersion;
// v6: the State carries Motion (the Hello is v5's); v5: the images' create flags ride along too; v4: their
// 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 || h.version == 5);
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;
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];
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;
::close(c);
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); }
g_skip.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}) {
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);
}
// What to do with a draw: 0 as is, 1 replace with the twin, 2 skip.
int drawAction(VkCommandBuffer cb, Twin& t, int& slot) {
if (g_off || !g_ours.imported) return 0;
// What to do with a draw: 0 as is, 1 replace with the twin (with `set`, of the kind `subsampled`), 2 skip. A twin
// drawn holds the generation whose set it binds until the command buffer is re-begun, reset or freed (Gen::users).
int drawAction(VkCommandBuffer cb, Twin& t, VkDescriptorSet& set, bool& subsampled, uint32_t& uboOffset) {
if (g_off || !g_imported) return 0;
VkPipeline p;
bool skip;
{
@@ -741,29 +906,37 @@ int drawAction(VkCommandBuffer cb, Twin& t, int& slot) {
}
if (!t.plain.pipeline && !skip) return 0;
std::lock_guard<std::mutex> ol(g_oursMutex);
slot = frameSlot();
if (slot < 0) return 0;
const int slot = frameSlot();
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.
if (envshare::nowNs() - g_cameraNs.load() < 150000000ull) return 2;
if (cameraRecent()) 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)
fillUbo(slot);
if (!g_latchStamped) { g_latchStamped = true; g_ours.state->sampledNs.store(g_latchNs); } // the moment it took the frame
Gen* g = g_ours.cur;
if (g->subsampled && !t.subsampled.pipeline) return 0; // (never imported so: import checks the twins can)
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;
}
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);
VkPipeline original;
{
std::lock_guard<std::mutex> l(g_mutex);
original = g_bound[cb];
}
const bool subsampled = g_ours.subsampled;
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->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();
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) {
Twin t;
int slot = -1;
const int act = drawAction(cb, t, slot);
VkDescriptorSet set = VK_NULL_HANDLE;
bool subsampled = false;
uint32_t off = 0;
const int act = drawAction(cb, t, set, subsampled, off);
traceDraw(cb, "draw", vc, 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;
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) {
Twin t;
int slot = -1;
int act = drawAction(cb, t, slot);
if (act == 0 && g_starsIndices && n == g_starsIndices && g_ours.imported) { // the stars, while the home shows
VkDescriptorSet set = VK_NULL_HANDLE;
bool subsampled = false;
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);
if (liveSlot() >= 0 && envshare::nowNs() - g_cameraNs.load() > 150000000ull) act = 2;
if (liveSlot() >= 0 && !cameraRecent()) act = 2;
}
traceDraw(cb, "drawIndexed", n, 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;
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)) {
g_lastOffCheck = now;
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 (!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;
importPending(dd, q);
if (g_ours.imported && liveSlot() >= 0 && ++g_ours.healthyFrames == 900) markHealthy(); // ~10 s showing the home
if (g_ours.cur && 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) {
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) {
onSubmit(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 ──────────────────────────────────────────────────────────────────
@@ -883,6 +1254,9 @@ VKAPI_ATTR VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceC
PFN_vkGetInstanceProcAddr gipa = link->u.pLayerInfo->pfnNextGetInstanceProcAddr;
PFN_vkGetDeviceProcAddr gdpa = link->u.pLayerInfo->pfnNextGetDeviceProcAddr;
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"));
// 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.
@@ -924,6 +1298,7 @@ VKAPI_ATTR VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceC
dd.device = *out;
dd.physical = pd;
dd.gdpa = gdpa;
dd.setLoaderData = setLoaderData;
#define FH_LOAD(n) dd.n = reinterpret_cast<PFN_vk##n>(gdpa(*out, "vk" #n));
FH_DEVICE_FUNCS(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);
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_latchNs = 0;
g_latchSlot = -1;
// a clean close (SteamVR quitting or restarting the compositor): not a crash
if (g_armed.exchange(false)) ::unlink((dir() + "/starting").c_str());
}
@@ -975,6 +1369,8 @@ PFN_vkVoidFunction intercept(const char* name) {
if (!isCompositor()) return nullptr;
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(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
if (!std::strcmp(name, "vkQueueSubmit2") || !std::strcmp(name, "vkQueueSubmit2KHR")) return reinterpret_cast<PFN_vkVoidFunction>(QueueSubmit2);
return nullptr;
+76
View File
@@ -270,3 +270,79 @@ static const uint32_t kClaySkinnedVertSpirv[] = {
0x00000027,0x0000003e,0x0004003d,0x00000007,0x0000002b,0x00000029,0x0003003e,0x0000002a,
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,
};
+72
View File
@@ -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,
};
+14 -5
View File
@@ -76,17 +76,20 @@ public:
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]) {
envshare::Slot& s = state_->slot[k];
void publish(int k, const float viewProj[2][16], const float eyePos[2][3], const envshare::Motion& motion) {
envshare::Slot s;
s.frame = ++frame_;
s.generation = generation_;
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; }
envshare::writeSlot(state_, static_cast<uint32_t>(k), s, motion); // (under its sequence count: env_share.h)
state_->published.store(frame_);
state_->latest.store(static_cast<uint32_t>(k), std::memory_order_release);
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)
double sinceSampled() const {
@@ -282,17 +285,23 @@ public:
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.
void publish(int k, const float viewProj[2][16], const float eyePos[2][3]) {
envshare::Slot& s = state_->slot[k];
void publish(int k, const float viewProj[2][16], const float eyePos[2][3], const envshare::Motion& motion) {
envshare::Slot s;
s.frame = ++frame_;
s.generation = generation_;
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_->latest.store(static_cast<uint32_t>(k), std::memory_order_release);
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).
uint32_t refusedFlags() const { return state_->refusedFlags.load() & flags_; }
+118 -2
View File
@@ -23,7 +23,11 @@
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <ctime>
#ifdef _MSC_VER
#include <intrin.h>
#endif
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
// generation suffix, so a new one is distinct)
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)
// 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.
constexpr uint32_t kSlots = 4;
@@ -85,8 +90,20 @@ struct Slot {
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 {
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<uint64_t> published{0}; // frame number of `latest`
std::atomic<uint64_t> publishNs{0}; // CLOCK_MONOTONIC when it was published
@@ -97,7 +114,106 @@ struct State {
Slot slot[kSlots];
std::atomic<uint32_t> refusedFlags{0}; // the layer: Hello flags it can't import with (Frame Home then sends
// 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() {
#ifdef _WIN32
+320 -30
View File
@@ -21,12 +21,15 @@
#else
#include <fcntl.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
#include "fenv_lzma.h"
#include "miniz.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
@@ -35,6 +38,9 @@
#include <fstream>
#include <exception>
#include <functional>
#include <iterator>
#include <memory>
#include <mutex>
#include <string>
#include <system_error>
#include <vector>
@@ -43,14 +49,31 @@ struct FenvSource {
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 {
std::string name;
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<FenvSplats> splats; // Gaussian splat objects (written with needs = {"splats"})
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 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
// 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 {
@@ -65,8 +88,33 @@ inline std::string quote(const std::string& s) {
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) {
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) {
const FenvSource& s = m.sources[i];
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\": [";
for (size_t i = 0; i < m.models.size(); ++i) j += std::string(i ? "," : "") + "\n { \"file\": " + quote(m.models[i]) + " }";
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.thumbnail.empty()) j += ",\n \"thumbnail\": " + quote(m.thumbnail);
if (!m.scene.empty()) j += ",\n \"scene\": " + quote(m.scene);
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();
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
// fill the disk or memory. The largest homes are a few hundred MB.
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) {
std::vector<uint8_t> out;
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;
void* p = mz_zip_reader_extract_to_heap(&z, static_cast<mz_uint>(i), &n, 0);
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);
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.
inline std::string pathTag(const std::filesystem::path& p) {
std::error_code ec;
@@ -138,6 +249,80 @@ struct ExtractLock {
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() {
#ifdef _WIN32
return static_cast<long>(_getpid());
@@ -146,9 +331,27 @@ inline long processId() {
#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) {
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();) {
size_t b = m.find_first_of("/\\", a);
if (b == std::string::npos) b = m.size();
@@ -161,11 +364,14 @@ inline bool safeMember(const std::string& m) {
} // namespace fenv_detail
// 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() {
if (const char* e = std::getenv("FRAME_FENV_CACHE")) return e;
#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
if (const char* x = std::getenv("XDG_CACHE_HOME")) return std::string(x) + "/frame-home/fenv";
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();
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("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("world") && fenv_detail::safeMember(doc["world"].asString())) m.world = doc["world"].asString();
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 (...) {
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.
inline bool fenvReadManifest(const std::string& path, FenvManifest& m, std::vector<uint8_t>* thumb = nullptr) {
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
// 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,
std::vector<std::string>& sources, std::vector<std::string>& models, std::string& editsPath,
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)
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 (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 fs::path root = fs::u8path(cacheRoot);
const fs::path dir = root / (key + "-" + std::to_string(size) + "-" + std::to_string(time));
const fs::path done = dir / ".complete";
fs::create_directories(root, ec);
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
// 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).
@@ -257,7 +504,7 @@ inline bool fenvExtract(const std::string& path, const std::string& cacheRoot, F
ours = sizeTime(rest);
}
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
// 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);
fs::create_directories(out.parent_path(), ec);
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;
try {
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;
}
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);
if (!ok) error = "couldn't extract " + name + " from " + path;
ok = written <= fenv_detail::kExtractBudget && fenv_detail::extractMember(z, i, st, out);
if (!ok) error = "couldn't extract " + name + " from " + path + " (damaged, or the disk is full)";
}
mz_zip_reader_end(&z);
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) { 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();
models.clear();
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();
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();
for (FenvSplats& s : m.splats) s.file = (dir / fs::u8path(s.file)).u8string();
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.
struct FenvMember {
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 = {}) {
namespace fs = std::filesystem;
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 = {};
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);
bool ok = mz_zip_writer_add_mem(&z, "fenv.json", json.data(), json.size(), MZ_DEFAULT_COMPRESSION);
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);
std::error_code ec;
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 (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; }
if (!fenv_detail::replaceWith(fs::u8path(tmp), fs::u8path(outPath), ec)) { error = "couldn't replace " + outPath + ": " + ec.message(); return false; }
return true;
}
@@ -385,7 +672,11 @@ inline bool fenvRewriteManifest(const std::string& path, const FenvManifest& m,
mz_zip_archive in = {}, out = {};
if (!mz_zip_reader_init_file(&in, path.c_str(), 0)) { error = "can't read " + path; return false; }
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);
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);
@@ -401,8 +692,7 @@ inline bool fenvRewriteManifest(const std::string& path, const FenvManifest& m,
mz_zip_reader_end(&in);
std::error_code ec;
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 (ec) { fs::remove(fs::u8path(tmp), ec); error = "couldn't replace " + path + ": " + ec.message(); return false; }
if (!fenv_detail::replaceWith(fs::u8path(tmp), fs::u8path(path), ec)) { error = "couldn't replace " + path + ": " + ec.message(); return false; }
return true;
}
@@ -414,12 +704,9 @@ inline WorldSettings worldSettingsFor(const std::string& homePath) {
if (fenvReadManifest(homePath, m)) w = m.settings;
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;
std::string j;
char b[4096];
for (size_t k; (k = std::fread(b, 1, sizeof b, f)) > 0;) j.append(b, k);
std::fclose(f);
const std::string j((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
if (!w.fromJson(j)) w = WorldSettings();
return w;
}
@@ -436,9 +723,12 @@ inline bool setWorldSettings(const std::string& homePath, const WorldSettings& w
std::error_code ec;
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";
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::fclose(f);
{
std::ofstream f(std::filesystem::u8path(tmp), std::ios::binary | std::ios::trunc);
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);
if (ec) { error = "can't write " + p + ": " + ec.message(); return false; }
return true;
+3 -3
View File
@@ -242,9 +242,9 @@ inline bool fenvBundleWrite(const std::string& outPath, const std::vector<std::s
mz_zip_writer_end(&zip);
std::error_code ec;
if (!ok) { fs::remove(fs::u8path(tmp), ec); return false; }
fs::rename(fs::u8path(tmp), fs::u8path(outPath), ec);
if (ec) { fs::remove(fs::u8path(outPath), ec); fs::rename(fs::u8path(tmp), fs::u8path(outPath), ec); }
if (ec) { fs::remove(fs::u8path(tmp), ec); error = "couldn't replace " + outPath + ": " + ec.message(); return false; }
// (the old bundle set aside, not deleted, until the new one is in its place: removing it first and then failing
// the rename lost both)
if (!fenv_detail::replaceWith(fs::u8path(tmp), fs::u8path(outPath), ec)) { error = "couldn't replace " + outPath + ": " + ec.message(); return false; }
return true;
}
+44 -9
View File
@@ -92,17 +92,36 @@ int pack(const std::string& out, const std::vector<std::string>& inputs, std::st
b.packing = g_packing;
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) {
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 (baked) continue; // its meshes are in scene.bin
const fs::path base = fs::u8path(inputs[i]).parent_path();
const std::string dir = "models/" + stem(inputs[i]) + "/";
const std::string dir = unique("models/", stem(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) {
std::error_code ec;
fs::path rel = fs::relative(fs::u8path(f), base, ec);
if (ec || rel.empty() || rel.u8string().find("..") == 0) rel = fs::u8path(f).filename();
fs::path rel = fs::u8path(f).lexically_relative(base);
if (rel.empty() || rel.u8string().rfind("..", 0) == 0 || rel.is_absolute()) rel = fs::u8path(f).filename();
FenvMember mm;
mm.name = dir + rel.generic_u8string();
mm.file = f;
@@ -112,7 +131,7 @@ int pack(const std::string& out, const std::vector<std::string>& inputs, std::st
continue;
}
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;
m.sources.push_back(src);
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);
}
}
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 (!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; }
std::error_code ec;
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);
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");
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 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(" settings: %s%s\n", m.settings.describe().c_str(), m.settings.tuned.empty() ? "" : (" (" + m.settings.tuned + ")").c_str());
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; }
std::vector<FenvMember> members;
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;
b.name = m.scene;
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 >= 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;
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;
}
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) {
const std::string a = argv[i];
if (a == "--no-optimize") { g_optimize = false; continue; }
if (a == "--fast") { g_packing = FenvPacking::Fast; continue; }
const std::vector<std::string> group = homeFiles(a);
std::string base;
for (const auto& g : group) base += (base.empty() ? "" : "+") + stem(g);
+6 -2
View File
@@ -232,7 +232,11 @@ private:
mutable std::vector<uint32_t> stamp_;
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); }
// 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.
@@ -381,7 +385,7 @@ public:
}
origin_ = origin_ + 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
// head inside a cabinet or under a beam would otherwise find that and pop you up onto it.
+23 -8
View File
@@ -19,9 +19,11 @@
// "title": "Rainy Haven", "image": "haven.apk.image-1a2b3c4d.jpg" (the image beside this file, like the sound)
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <sstream>
#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.image.empty()) o << ",\n \"image\": " << quote(e.image);
o << "\n}\n";
std::ofstream f(path, std::ios::binary);
if (!f) return false;
f << o.str();
return static_cast<bool>(f);
// By its UTF-8 name (a narrow path is the ANSI code page on Windows: C:\Users\José\... wasn't found), written
// beside it and renamed over it once whole: a crash or a full disk midway never leaves a cut-off edits file.
namespace fs = std::filesystem;
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) {
std::ifstream f(path, std::ios::binary);
std::ifstream f(std::filesystem::u8path(path), std::ios::binary);
if (!f) return false;
std::stringstream ss;
ss << f.rdbuf();
@@ -213,7 +226,8 @@ inline bool loadHomeEdits(const std::string& path, HomeEdits& out) {
std::string k;
if (!r.str(k) || !r.eat(':')) return false;
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 == "deleted" ? r.boolean(x.deleted)
: k == "pivot" ? r.floats(x.pivot, 16)
@@ -234,8 +248,9 @@ inline bool loadHomeEdits(const std::string& path, HomeEdits& out) {
double d = 0;
const bool ok = k == "file" ? r.str(out.sound.file)
: 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))
: k == "homeVolume" ? (r.num(d) && (out.sound.homeVolume = 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 == "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();
if (!ok) return false;
} while (r.eat(','));
+3 -1
View File
@@ -257,6 +257,8 @@ inline int run(const std::vector<std::string>& args, int nice, bool wait) {
argv.push_back(nullptr);
const pid_t pid = fork();
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);
const int null = ::open("/dev/null", O_RDWR);
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);
}
if (pid < 0) return -1;
if (!wait) return 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;
}
// This program again with the same arguments, in this process (SteamVR knows it by its path: still the environment).
+396 -55
View File
@@ -10,6 +10,8 @@
#include "core/parallel.h" // par::forEach: animated meshes rebuilt across cores
#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 "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
#ifdef _WIN32
@@ -26,6 +28,7 @@
#include <cmath>
#include <cstdio>
#include <cstring>
#include <tuple>
#include <unordered_map>
#define LOG(...) do { std::fprintf(stderr, "[render] " __VA_ARGS__); std::fputc('\n', stderr); } while (0)
@@ -33,6 +36,15 @@
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) {
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
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);
}
// 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,
VkSampleCountFlagBits samples, VkFormat fmt, VkImageUsageFlags usage,
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.
std::vector<char> data;
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);
const long n = std::ftell(f);
std::fseek(f, 0, SEEK_SET);
@@ -959,6 +986,7 @@ bool HslRenderer::init(uint32_t eyeWidth, uint32_t eyeHeight) {
createTargets();
createSharedResources();
createGuidePipelines();
createSplatPipeline();
ring_ = createBuffer(kRingSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true);
// 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")) {
@@ -1057,11 +1085,13 @@ void HslRenderer::createTargets() {
dep.srcSubpass = 0;
dep.dstSubpass = VK_SUBPASS_EXTERNAL;
dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dep.dstStageMask = external ? VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT : VK_PIPELINE_STAGE_TRANSFER_BIT;
if (finalColor == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) dep.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
// One dependency for every kind (copied out, read by the smoothing pass, or the compositor's): render passes are
// 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.dstAccessMask = external ? VK_ACCESS_MEMORY_READ_BIT : VK_ACCESS_TRANSFER_READ_BIT;
if (finalColor == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) dep.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_MEMORY_READ_BIT;
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};
mv.subpassCount = 1;
@@ -1195,18 +1225,20 @@ VkRenderPass HslRenderer::colorOnlyPass(VkImageLayout finalLayout) const {
sub.colorAttachmentCount = 1;
sub.pColorAttachments = &cref;
VkSubpassDependency deps[2] = {};
deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; // the foveated pass's writes before this one reads them
deps[0].dstSubpass = 0;
deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; // the foveated pass's writes before this one reads them, and the
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].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].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].dstSubpass = VK_SUBPASS_EXTERNAL;
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].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;
VkRenderPassMultiviewCreateInfo mv = {VK_STRUCTURE_TYPE_RENDER_PASS_MULTIVIEW_CREATE_INFO};
mv.subpassCount = 1;
@@ -1534,20 +1566,23 @@ int HslRenderer::programFor(const ShaderProgramSrc& src) {
plci.pPushConstantRanges = &pcr;
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};
ci.codeSize = code.size() * 4;
ci.pCode = code.data();
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;
};
// (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.src = &src;
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 (!p.vs || !p.fs) { // SPIR-V the driver rejects: this program's meshes are skipped
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.depthWriteEnable = blend == 0 && !sky && cut != kCutShade ? VK_TRUE : VK_FALSE;
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 = {};
cba.colorWriteMask = cut == kCutDepth ? 0 : 0xF;
if (blend != 0) {
@@ -2351,23 +2396,32 @@ void HslRenderer::setSelection(int mesh) {
if (!nv || md.indices.size() < 3) return;
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);
// 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::unordered_set<uint64_t> seen;
seen.reserve(md.indices.size());
std::vector<uint64_t> keys;
keys.reserve(md.indices.size());
tris.reserve(md.indices.size());
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]};
if (idx[0] >= nv || idx[1] >= nv || idx[2] >= nv) continue;
tris.insert(tris.end(), idx, idx + 3);
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]);
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;
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_.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);
if (batch) flushUploadBatch();
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_.triCount = static_cast<uint32_t>(tris.size());
@@ -2442,7 +2496,8 @@ void HslRenderer::setStaticGuide(const std::vector<GuideVertex>& tris) {
if (!device_) return;
if (guide_.staticVbo.buf) { vkDeviceWaitIdle(device_); destroy(guide_.staticVbo); }
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);
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).
measureMesh(gm, md, nv);
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.
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);
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);
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 {
VkDescriptorImageInfo ii = {};
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) {
slotPending_ = false;
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
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});
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).
const auto tp0 = std::chrono::steady_clock::now();
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();
// 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();
meshes_.push_back(std::move(gm));
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 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;
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};
ci.codeSize = c.size() * 4;
ci.pCode = c.data();
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;
};
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;
bool baked = false;
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;
if (!baked) {
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) {
beginLoad(scene);
while (!loadStep(1e9)) {}
return !meshes_.empty();
return !meshes_.empty() || !splatObjs_.empty();
}
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 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
// (256^2, 5 levels). One shipped with fewer (Cascadia's 1024^2 has mip 0 alone, every metal a sharp mirror)
// gets the rest of that chain blitted.
// (256^2, 5 levels; a streamed one like Cascadia's has 1024^2 and 512^2 in its streams, which the loader puts
// 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;
while ((size >> levels) >= 16) ++levels;
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)
loadNext_ = 0;
loadSkipped_ = 0;
loadDuplicates_ = 0;
dedupe_.clear();
loadStart_ = std::chrono::steady_clock::now();
}
@@ -3308,16 +3426,45 @@ bool HslRenderer::loadStep(double budgetMs) {
}
flushUploadBatch();
if (loadNext_ < scene_->meshes.size()) return false;
if (!splatsUploaded_) uploadSplats(); // (hsl_renderer_splat.cpp)
if (loadNext_ == scene_->meshes.size() && !loadDone_) {
loadDone_ = true;
buildBatches();
size_t variants = 0;
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(),
loadSkipped_, programs_.size(), variants, specBaked_, specMade_, std::chrono::duration<double>(std::chrono::steady_clock::now() - loadStart_).count());
size_t batched = 0;
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;
}
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 {
if (!scene_ || scene_->meshes.empty()) return 1.0f;
return static_cast<float>(loadNext_) / static_cast<float>(scene_->meshes.size());
@@ -3328,6 +3475,7 @@ void HslRenderer::unload() {
vkDeviceWaitIdle(device_);
for (Buffer& b : frameStaging_) destroy(b);
frameStaging_.clear();
destroySplats();
for (auto& m : meshes_) {
destroy(m.vbo);
destroy(m.ibo);
@@ -3339,6 +3487,12 @@ void HslRenderer::unload() {
sel_.mesh = -1;
sel_.edgeCount = sel_.triCount = 0;
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();
costMin_.clear(); costSeen_.clear(); std::fill(std::begin(costBaseRing_), std::end(costBaseRing_), 1e9f);
doubled_ = -1;
@@ -3381,6 +3535,7 @@ void HslRenderer::unload() {
void HslRenderer::setMeshWorld(size_t i, const float world[16]) {
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);
placeMesh(gm);
}
@@ -3410,6 +3565,159 @@ void HslRenderer::placeMesh(GpuMesh& 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) {
if (!device_ || (eyeWidth == width_ && eyeHeight == height_) || !eyeWidth || !eyeHeight) return;
vkDeviceWaitIdle(device_);
@@ -3659,7 +3967,10 @@ void HslRenderer::render(const EyeView eyes[2], float timeSec) {
ta = clk::now();
updateAnimations(timeSec); // bone matrices for skinned meshes (the GPU is idle here)
tb = clk::now();
updateGlobals(eyes, timeSec);
{
EyeView late[2];
updateGlobals(latePose && latePose(late) ? late : eyes, timeSec);
}
tg = clk::now();
refreshDynamicMeshes(!early, true); // animated Clay / glTF meshes: this frame's vertices + tint
tc = clk::now();
@@ -3831,12 +4142,28 @@ void HslRenderer::render(const EyeView eyes[2], float timeSec) {
const int frameNo_ = ++cullFrame;
for (auto& gm : meshes_) {
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.debug && frameNo_ == 120)
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.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
// (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;
if (opaqueMode == 0) {
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 {
opaqueOrder_.clear();
for (size_t i = 0; i < meshes_.size(); ++i) {
const GpuMesh& gm = meshes_[i];
for (size_t i = 0; i < meshes_.size() + batches_.size(); ++i) { // (past the meshes: the batches, at their first part)
const GpuMesh& gm = meshAt(i);
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
d = 0;
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();
if (twoPass)
for (size_t k = 0; k < opaqueOrder_.size(); ++k)
if (meshes_[opaqueOrder_[k].second].alphaToCoverage) { firstCut = k; break; }
for (size_t k = 0; k < firstCut; ++k) draw(meshes_[opaqueOrder_[k].second]);
if (meshAt(opaqueOrder_[k].second).alphaToCoverage) { firstCut = k; break; }
for (size_t k = 0; k < firstCut; ++k) draw(meshAt(opaqueOrder_[k].second));
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];
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);
}
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);
}
}
@@ -3975,15 +4303,18 @@ void HslRenderer::render(const EyeView eyes[2], float timeSec) {
}
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;
for (size_t i = 0; i < meshes_.size(); ++i) {
if (meshes_[i].blend == 0 || meshes_[i].skybox) continue;
for (size_t i = 0; i < meshes_.size() + batches_.size(); ++i) { // (past the meshes: the see-through batches, at their box's centre)
const GpuMesh& gm = meshAt(i);
if (gm.blend == 0 || gm.skybox || (i >= meshes_.size() && !gm.inView)) continue;
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});
}
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) {
// 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 (!direct && exportSlot >= 0 && static_cast<size_t>(exportSlot) < exportImages_.size()) { // the frame for the compositor (system mode), copied
VkImage dst = exportImages_[exportSlot];
// GENERAL between frames: the layout both processes agree on (the compositor samples it as GENERAL).
imageBarrier(cmd_, dst, VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, viewCount, exportInited_[exportSlot] ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
// GENERAL between frames: the layout both processes agree on (the compositor samples it as GENERAL). What the
// slot held is discarded (UNDEFINED): the copy overwrites all of it, and the compositor never hands it back.
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,
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 = {};
cp.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, viewCount};
cp.dstSubresource = cp.srcSubresource;
cp.extent = {width_, height_, 1};
vkCmdCopyImage(cmd_, resolve_.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 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;
vkCmdCopyImage(cmd_, resolve_.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst, VK_IMAGE_LAYOUT_GENERAL, 1, &cp);
}
// 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; }
timedSlot_ = exportSlot;
if (perfThis) { vkCmdEndQuery(cmd_, perfPool_, 0); perfRecorded_ = true; }
@@ -4216,7 +4554,6 @@ bool HslRenderer::createExportImages(uint32_t n, std::vector<ExportImage>& out,
}
exportImages_.clear();
for (const ExportImage& e : out) exportImages_.push_back(e.image);
exportInited_.assign(out.size(), false);
if (exportDirect) createExportFramebuffers();
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);
exportImages_.clear();
importMem_.clear();
exportInited_.clear();
exportSlot = -1;
exportDirect = false; // copied in (the imported textures aren't our render targets)
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);
importMem_.push_back(mem);
}
exportInited_.assign(n, false);
LOG("system mode: imported %u compositor textures (%ux%u, %u layers)", n, w, h, viewCount);
return true;
}
@@ -4342,7 +4677,6 @@ void HslRenderer::destroyExportImages(std::vector<ExportImage>& images) {
destroyExportFramebuffers();
images.clear();
exportImages_.clear();
exportInited_.clear();
exportSlot = -1;
}
@@ -4355,9 +4689,15 @@ bool HslRenderer::readback(uint32_t layer, std::vector<u8>& rgba) {
VkBufferImageCopy copy = {};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1};
copy.imageExtent = {width_, height_, 1};
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, &copy);
else vkCmdCopyImageToBuffer(c, resolve_.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, b.buf, 1, &copy);
if (lastExportSlot_ >= 0 && static_cast<size_t>(lastExportSlot_) < exportImages_.size()) { // exportDirect: the frame is there
// (released to the compositor: taken back for the copy, then handed over again)
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, &copy);
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, &copy);
});
rgba.assign(static_cast<u8*>(b.mapped), static_cast<u8*>(b.mapped) + size);
destroy(b);
@@ -4369,6 +4709,7 @@ void HslRenderer::shutdown() {
stopWorkers();
unload();
destroyGuide();
destroySplatPipeline();
destroyDebug();
for (VkImageView v : layerViews_) if (v) vkDestroyImageView(device_, v, nullptr);
for (Image& i : pictures_) destroy(i);
@@ -4404,7 +4745,7 @@ void HslRenderer::shutdown() {
if (!pipelineCachePath.empty() && vkGetPipelineCacheData(device_, pipelineCache_, &n, nullptr) == VK_SUCCESS && n) {
std::vector<char> data(n);
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);
}
+77 -4
View File
@@ -6,13 +6,15 @@
// set2 per material: matParams (cooked constants bound by MurmurHash3 name), lightprobesParams,
// 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.
#include <unordered_map>
#include <unordered_set>
#include "guide_geom.h"
#include "hsl_scene.h"
#include "shader_bake.h"
#include "splat_sort.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)
#endif
#ifdef FH_USE_VOLK
@@ -25,6 +27,7 @@
#include <chrono>
#include <condition_variable>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
@@ -55,8 +58,17 @@ public:
bool loadStep(double budgetMs);
float loadProgress() const;
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.
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.
bool readback(uint32_t layer, std::vector<u8>& rgba);
void shutdown();
@@ -92,7 +104,10 @@ public:
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.
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().
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,
@@ -111,6 +126,12 @@ public:
void setEarlyRecord(bool on) { earlyRecord_ = on; }
bool guideAvailable() const { return guide_.visible != VK_NULL_HANDLE; }
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
// 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)
bool debug = false; // FH_DEBUG matched: cull state logged once
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
uint32_t memoryType(uint32_t bits, VkMemoryPropertyFlags flags);
@@ -395,7 +440,6 @@ private:
void destroyExportFramebuffers();
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
std::vector<bool> exportInited_;
#ifdef _WIN32
std::vector<VkDeviceMemory> importMem_; // importWin32Images: the imported memories, freed on shutdown
uint8_t deviceLUID_[8] = {};
@@ -426,6 +470,34 @@ private:
void createGuidePipelines();
void destroyGuide();
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();
// 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.
@@ -513,7 +585,8 @@ private:
std::vector<GpuMesh> meshes_;
std::map<uint64_t, Image> textureCache_; // content hash -> shared texture (freed on unload)
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;
std::chrono::steady_clock::time_point loadStart_;
float ambient_[3] = {1, 1, 1};
+266
View File
@@ -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;
}
}
+162 -17
View File
@@ -30,11 +30,13 @@
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) {
mz_zip_archive_file_stat st;
if (!mz_zip_reader_file_stat(&z, static_cast<mz_uint>(index), &st)) return {};
std::vector<u8> out(static_cast<size_t>(st.m_uncomp_size)); // unpacked straight into place
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;
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 {};
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
// 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
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);
// 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) {
std::vector<u8> data = extract(z, static_cast<int>(i));
std::vector<SpirvBlob> blobs;
@@ -93,6 +97,45 @@ void scanSceneZip(const std::string& apkPath, HslScene& scene) {
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.
static void readManifest(const std::string& apkPath, std::string& package, std::string& version) {
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* kClaySkinnedProgram = "builtin/clay_skinned.surface/0/shader"; // skinned on the GPU
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).
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));
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.
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 {
std::vector<std::string> all = sources;
all.insert(all.end(), models.begin(), models.end());
if (all.empty()) { error = "empty environment: " + path; return false; }
if (!loadHslScene(all, scene, error)) return false;
if (all.empty() && m.splats.empty()) { error = "empty environment: " + path; 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.fenvEdits = edits;
@@ -286,7 +362,8 @@ static bool loadFenv(const std::string& path, HslScene& scene, std::string& erro
HomeWorld 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;
}
@@ -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<char> ok(apkPaths.size(), 0);
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());
scene.sourceFiles.resize(apkPaths.size());
std::vector<std::thread> threads;
for (size_t i = 0; i < apkPaths.size(); ++i) {
loaders[i] = std::make_unique<SceneLoader>();
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] {
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 (importModel(apkPaths[i], models[i], scene.sourceFiles[i], modelErrors[i])) ok[i] = 4;
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)?
if (gltfs[i]->load(apkPaths[i])) ok[i] = 3;
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();
@@ -340,20 +434,20 @@ bool loadHslScene(const std::vector<std::string>& apkPaths, HslScene& scene, std
for (size_t i = 0; i < apkPaths.size(); ++i) {
HslSourceInfo info;
readManifest(apkPaths[i], info.package, info.version);
info.format = ok[i] == 1 ? "HSL" : ok[i] == 2 ? "Clay (.opa)" : ok[i] == 3 ? "Clay (.ovrscene)" : "Model";
if (ok[i] == 4) {
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 || ok[i] == 5) {
const std::string file = apkPaths[i].substr(apkPaths[i].find_last_of("/\\") + 1);
info.package = file;
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)));
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
// 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;
for (char& c : n) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
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 ? ".vista." : "vista") != std::string::npos ? "Vista" : "Home";
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());
}
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) {
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
addClayPrograms(scene);
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.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
addClayPrograms(scene);
// 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();
// 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
@@ -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
// 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.
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()) {
std::shared_ptr<OpaLoader> L(std::move(clays[i]));
scene.animDuration = L->animDuration();
@@ -496,6 +607,15 @@ bool loadHslScene(const std::vector<std::string>& apkPaths, HslScene& scene, std
}
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
// (Haven's footprint carries a stand-in bed of its own, Central's).
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(),
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());
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);
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> out;
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
// %LOCALAPPDATA%\FrameStudio\cache\walk or ~/.cache/frame-home/walk; the 300 most recent are kept.
static std::filesystem::path walkCacheDir() {
#ifdef _WIN32
if (const char* l = std::getenv("LOCALAPPDATA")) return std::filesystem::u8path(std::string(l) + "\\FrameStudio\\cache\\walk");
#ifdef _WIN32 // (wide: the narrow getenv is the ANSI code page, which u8path throws on)
if (const wchar_t* l = _wgetenv(L"LOCALAPPDATA")) return std::filesystem::path(l) / L"FrameStudio" / L"cache" / L"walk";
#endif
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");
+19
View File
@@ -8,9 +8,11 @@
#include "core/world.h"
#include "walkgrid.h"
#include "loaders/rendskel_parser.h"
#include "splat_import.h"
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <vector>
@@ -35,6 +37,16 @@ struct HslAnimGroup {
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).
struct HslSourceInfo {
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<MeshData> meshes; // all APKs' meshes, in apkPaths order
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::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
@@ -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).
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).
const ShaderProgramSrc* findProgram(const HslScene& scene, const std::string& shaderPath);
+90 -18
View File
@@ -10,7 +10,8 @@
// 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
// 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
// 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
@@ -212,6 +213,30 @@ public:
// 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_); }
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).
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).
void update(float dt, const float headT[16]) {
dt = std::min(dt, 0.05f);
turnRate_ = 0;
if (blinkInT_ > 0) blinkInT_ = std::max(0.0f, blinkInT_ - dt);
checkEyeHeight(dt, headT[13]);
guide_.seats.clear();
guide_.pointer = false;
@@ -277,13 +304,17 @@ public:
float lx = 0, ly = 0;
if (walk) grav::shapeStick(l.x, l.y, lx, ly);
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)
if (seated_ >= 0 && walk_ == 0 && teleport_ == 0 && l.bActive) {
float sx = 0, sy = 0;
grav::shapeStick(l.x, l.y, sx, sy);
if ((sx != 0 || sy != 0) && slideT_ < 0 && blinkT_ < 0) { leaveSeat(headT, head, R); rig(R); apply(R, &headT[12], head); }
// Seated, the stick stands you up (walking and teleport both off too: else only stepping away would). Only
// once it has come back to centre since you sat, and pushed well past the deadzone: a stick still held
// from walking there, or a worn one drifting, would jump you straight back out of the seat.
if (seated_ >= 0) {
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.
grav::V want;
if (lx != 0 || ly != 0) {
@@ -307,7 +338,7 @@ public:
float f;
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];
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;
} else {
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
// your real height: you may be standing in the room).
// 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:
// 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 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)
@@ -495,9 +528,30 @@ private:
rigX_ += st.pos[0] - head[0];
rigZ_ += st.pos[2] - head[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];
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());
}
@@ -597,17 +651,26 @@ private:
// held slightly off-centre shouldn't spin you).
static constexpr float kTurnRate = 2.0944f; // 120 degrees/s
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.75f && std::fabs(ry) < 0.6f) { turnArmed_ = false; return rx > 0 ? -snap_ : snap_; }
return 0;
}
// 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.
static constexpr float kBlinkOut = 0.08f, kSlide = 0.25f;
bool blinkOut() const { if (!ok_ || !vr::VRCompositor()) return false; vr::VRCompositor()->FadeToColor(kBlinkOut, 0, 0, 0, 1); return true; }
void blinkIn() const { if (ok_ && vr::VRCompositor()) vr::VRCompositor()->FadeToColor(0.12f, 0, 0, 0, 0); }
// the compositor ignores its fade (the blink was an unfaded 80 ms freeze, then the jump): there the fade is
// Frame Home's own (setOwnFade), blinkLevel() darkening the picture the layer draws.
static constexpr float kBlinkOut = 0.08f, kBlinkIn = 0.12f, kSlide = 0.25f;
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); }
// 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;
int respawns_ = 0;
int seated_ = -1;
bool standArmed_ = false; // seated: the left stick has been back at centre
float seatRigY_ = 0;
bool ownInput_ = false;
bool menuAim_ = false;
@@ -697,6 +761,9 @@ private:
float jump_[3] = {0, 0, 0}, jumpN_[3] = {0, 1, 0};
int jumpSeat_ = -1;
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};
float testIn_[4] = {}, testAim_[12] = {}, testLeft_[12] = {};
@@ -820,7 +887,10 @@ private:
const WorldCollider* shape = nullptr;
const WorldRuntime::Hit h = runtime_.trace(p, seg, t, n, &shape);
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};
keep(q);
seat_ = si;
@@ -831,7 +901,9 @@ private:
if (h != WorldRuntime::None) {
const float q[3] = {p[0] + seg[0] * t, p[1] + seg[1] * t, p[2] + seg[2] * t};
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_);
valid_ = true;
} else if (runtime_.landingFor(h, q, n, target_)) {
+275 -39
View File
@@ -326,6 +326,7 @@ struct SystemPacer {
uint64_t vsyncNs = 0; // a recent vsync
static constexpr size_t kLeads = 240; // two seconds at 120 Hz
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
// 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.
@@ -356,16 +357,19 @@ struct SystemPacer {
void plan(uint64_t now, float vsyncToPhotons, uint64_t& start, uint64_t& target, float& predict) {
const double p = period * 1e9, lead = leadMs() * 1e6;
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);
lastTarget = start;
predict = static_cast<float>(2 * period) + vsyncToPhotons;
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;
while (t < static_cast<double>(now) + lead) t += p;
while (lastTarget && t < static_cast<double>(lastTarget) + p * 0.5) t += p;
// (whole periods at once: a vsync seen long ago, after the display slept, is many thousands of them back)
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);
start = target - static_cast<uint64_t>(lead);
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
// 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 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; };
HslRenderer* r = nullptr;
EnvExport* env = nullptr;
@@ -398,8 +402,16 @@ struct Publisher {
const Job j = jobs.front();
working = true;
l.unlock();
while (!r->waitFrameSerial(j.serial, 100000000ull)) { std::lock_guard<std::mutex> g(m); if (stop) break; }
env->publish(j.slot, j.vp, j.eye);
// A GPU that never finishes it (the device lost): after 3 s the frame is let go, so drain() and
// 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();
l.lock();
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
// 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).
// 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(); }
// 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) {
const fs::path settingsPath = rootDir() / "settings.json";
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
FrameProfile prof;
prof.on = true;
Window make, cpu, gpu, gap;
Window make, cpu, gpu, gap, work;
const double period = 1.0 / hz;
uint32_t over = 0, hitches = 0, frames = 0, earlyFrames = 0;
float animTime = 0;
const uint64_t t0 = envshare::nowNs();
uint64_t next = t0, lastDone = 0, gpuSeen = r.gpuResults();
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_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;
// 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)) {
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;
// 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);
@@ -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; }();
(void)early;
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 (r.deviceLost()) { LOG("bench: device lost (a GPU reset) after %u frames", frames); break; }
const uint64_t done = envshare::nowNs();
const double makeMs = (done - fs0) / 1e6;
make.add(makeMs);
cpu.add(r.lastStats().totalMs + animMs);
work.add((threadCpuNs() - cpu0) / 1e6);
const bool gpuNew = r.gpuResults() != gpuSeen;
gpuSeen = r.gpuResults();
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});
}
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;
}
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::string worst;
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,"
"\"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,"
"\"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,
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(),
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());
@@ -1056,7 +1169,7 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
}
std::string debugStamp = stamp(debugPath.string());
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
r.msaa = perf.msaa;
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.debugHide = dbg.hideBits();
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",
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).
@@ -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");
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.setOwnFade(system); // (SteamVR's fade is the scene app's: a blink fades through the layer)
std::vector<GuideVertex> guideVerts;
// 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.
@@ -1125,7 +1239,15 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
const auto began = std::chrono::steady_clock::now();
auto s = std::make_unique<HslScene>();
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
// 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()));
@@ -1244,7 +1366,9 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
{
std::ifstream in(rootDir() / "autoscale.txt");
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 rememberScale = [&] {
@@ -1286,6 +1410,12 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
prof.published(ns, leadMs, late);
};
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);
if (system) LOG("display: %.0f Hz, %.1f ms from vsync to photons", hz, vsyncToPhotons * 1000);
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;
float frozenT = -1; // Settings > Animations off: the clock the home holds at
bool dashboard = false; // system mode: the Steam menu is open
uint64_t dashFrames = 0;
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) {
vr::VREvent_t 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
// 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).
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) {
const bool late = std::chrono::steady_clock::now() > texturesUntil;
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) {
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;
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();
@@ -1456,7 +1587,7 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
// Debug tools changed? All of them apply now.
const std::string ds = stamp(debugPath.string());
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;
if (!(nd == dbg)) {
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;
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);
env.state()->fade = static_cast<uint32_t>(fade * 1000);
// 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;
env.state()->fade = static_cast<uint32_t>(fade * loco.blinkLevel() * 1000); // (a teleport blink too)
// 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.
// 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)) {
dashChecked = now;
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);
// Settings > Frame rate: Half draws every other frame all the time (as Meta's home does).
if (!idle && ((dashboard && perf.menuRate == 0) || perf.frameRate == 1) && (++dashFrames & 1)) {
std::this_thread::sleep_for(std::chrono::duration<double>(1.0 / hz));
continue;
}
// A game loading (launched, or connected and waiting to be shown): the home is what's on screen meanwhile,
// so it keeps going at full rate, never idle.
const bool gameLoading = sceneState == vr::EVRSceneApplicationState_Starting || sceneState == vr::EVRSceneApplicationState_Waiting;
// Nobody looking (the compositor is idle, passthrough, 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); 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)
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));
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));
@@ -1672,22 +1833,61 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
}
frameStartNs = envshare::nowNs();
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);
r.exportSlot = env.beginFrame();
} else {
vr::VRCompositor()->WaitGetPoses(poses, vr::k_unMaxTrackedDeviceCount, nullptr, 0);
frameStartNs = envshare::nowNs();
}
loco.setPoseValid(poses[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid);
if (poses[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid)
head = fromHmd34(poses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking);
{ // A pose with a NaN or infinity in it (tracking glitch) is taken as no pose: one would poison the rig for good.
const vr::TrackedDevicePose_t& hp = poses[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]);
loco.setPoseValid(valid);
if (valid) head = fromHmd34(hp.mDeviceToAbsoluteTracking);
}
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);
lastFrame = frameNow;
Mat4 rig;
loco.rig(rig.m);
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) {
// head turn rate (rad/s) from the rotation since the last frame: angle of R_prev^T * R
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;
// 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).
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 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
@@ -1789,7 +1989,33 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
r.setGuide(guideVerts);
}
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.latePose = nullptr;
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");
restart = lostDevice = true;
@@ -1800,6 +2026,9 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
++statFrames;
if (dbg.shot != shotSeen) { // Debug > Save a Picture: the left eye as it was just drawn, to logs/
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;
if (dbg.shot >= startedAt && r.readback(0, px) && px.size() >= static_cast<size_t>(r.width()) * r.height() * 4) {
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);
j.startNs = frameStartNs;
j.target = frameTarget;
j.motion = motion;
publisher.push(j);
} else { // no timeline semaphores: wait here
r.waitFrame();
env.publish(r.exportSlot, vpOut, eyeOut);
env.publish(r.exportSlot, vpOut, eyeOut, motion);
const uint64_t pubNs = envshare::nowNs();
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 double loopMs = std::chrono::duration<double, std::milli>(le - loopStart).count();
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);
loopStart = le;
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");
std::fflush(stderr);
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();
}
return 0;
+373 -19
View File
@@ -6,13 +6,28 @@
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#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" 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" 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 {
@@ -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>());
}
// 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; };
bool decode(const u8* data, size_t n, Image& img) {
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);
if (!px) return false;
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) {
cgltf_options opt = {};
opt.file.read = gltfRead;
opt.file.release = gltfRelease;
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_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);
const fs::path dir = fs::u8path(path).parent_path();
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;
auto image = [&](const cgltf_texture_view& view) -> const Image* {
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) {
const char* comma = std::strchr(im->uri, ',');
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;
if (cgltf_load_buffer_base64(&opt, size, comma + 1, &bytes) == cgltf_result_success) {
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) : "");
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;
md.positions.resize(pos->count * 3);
md.uvs.assign(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);
}
md.positions = unpackFloats(pos, pos->count, 3, 0.0f);
md.uvs = unpackFloats(uv, pos->count, 2, 0.0f);
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);
for (cgltf_size v = 0; v < pos->count; ++v) {
float c[4] = {1, 1, 1, 1};
cgltf_accessor_read_float(col, v, c, cgltf_num_components(col->type));
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);
}
for (cgltf_size v = 0; v < pos->count; ++v)
for (size_t k = 0; k < 4; ++k)
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);
}
if (prim.indices) {
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);
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);
md.hasWorldMatrix = true;
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.space_conversion = UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY;
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_scene* scene = ufbx_load_file(path.c_str(), &opts, &err);
if (!scene) {
@@ -279,17 +335,315 @@ bool importUfbx(const std::string& path, std::vector<MeshData>& meshes, std::vec
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
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) {
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) {
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);
error = "unsupported model type: " + path;
return false;
// Each runs on a loader thread of its own: a model too big for memory (bad_alloc, length_error) must come back as
// an error, not escape the thread and end the program (with the open home's unsaved changes).
const size_t meshesBefore = meshes.size(), filesBefore = files.size();
try {
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;
}
}
+6 -1
View File
@@ -1,6 +1,7 @@
#pragma once
// 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,
// alpha blending or cut-out from the material. Units become metres and up becomes +Y.
#include "core/types.h"
@@ -10,5 +11,9 @@
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.
bool importModel(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error);
+4 -1
View File
@@ -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);
Writer w{out};
w.u(kMagic); w.u(kSceneBakeVersion);
w.u(kMagic); w.u(sceneBakeVersionFor(s));
w.u(static_cast<u32>(s.apkPaths.size()));
for (size_t i = 0; i < s.apkPaths.size(); ++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
while ((std::max(ts->w, ts->h) >> chain) != 0) ++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;
}
std::vector<std::shared_ptr<const std::vector<u16>>> half(r.count(4));
+5 -1
View File
@@ -11,7 +11,11 @@
#include <string>
#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 ObjectEdit;
+17 -2
View File
@@ -7,12 +7,16 @@
// compute every position bit for bit alike)
// - 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)
// - 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
// assumes are the bytes the renderer uploads.
#include "core/types.h"
#include "hsl_scene.h"
#include "spv_reflect.h"
#include "spv_specialize.h"
#include "spv_opt.h"
#include <algorithm>
#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).
inline int floatsMode() {
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;
for (auto& kv : programs) {
ShaderProgramSrc& p = kv.second;
p.vertInvariant = invariantPosition(p.vert);
p.vertInvariant = spvopt::run(invariantPosition(p.vert));
p.fragAlpha = alphaOnly(p.frag);
if (!p.fragAlpha.empty()) p.fragAlpha = spvopt::run(p.fragAlpha);
p.variants.clear();
p.prepared = true;
Info& in = info[kv.first];
@@ -400,7 +413,9 @@ inline void bakePrograms(const std::vector<MeshData>& meshes, const std::functio
v.key = key;
v.frag = spvspec::specialize(p.frag, fetch, &folded, -1, floats);
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));
}
}
+428
View File
@@ -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;
}
+34
View File
@@ -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]);
+139
View File
@@ -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;
};
+265
View File
@@ -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);
+206
View File
@@ -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
+336
View File
@@ -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
+51
View File
@@ -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
+30 -11
View File
@@ -84,18 +84,28 @@ inline uint32_t scalarBytes(const Module& m, uint32_t t) {
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`.
inline bool step(const Module& m, Ptr& p, uint32_t index) {
const auto it = m.types.find(p.type);
if (it == m.types.end()) return false;
const Type& t = it->second;
if (t.a.empty()) return false; // (a type with no operands: a damaged module)
if (t.op == OpTypeStruct) {
if (index >= t.a.size()) return false;
const auto ms = m.members.find(p.type);
if (ms == m.members.end()) return false;
const auto mm = ms->second.find(index);
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.rowMajor = mm->second.rowMajor;
p.type = t.a[index];
@@ -104,20 +114,20 @@ inline bool step(const Module& m, Ptr& p, uint32_t index) {
if (t.op == OpTypeArray) {
const auto st = m.arrayStride.find(p.type);
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];
return true;
}
if (t.op == OpTypeMatrix) {
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];
return true;
}
if (t.op == OpTypeVector) {
const uint32_t b = scalarBytes(m, t.a[0]);
if (!b) return false;
p.offset += index * b;
if (!advance(p.offset, uint64_t(index) * b)) return false;
p.type = t.a[0];
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).
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);
if (it == m.types.end()) return 0;
const Type& t = it->second;
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;
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 == OpTypeStruct) n = static_cast<uint32_t>(t.a.size());
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) {
Ptr q = p;
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;
end = std::max(end, q.offset - p.offset + e);
}
return end;
return end <= kMaxOffset ? end : 0;
}
} // namespace detail
@@ -174,7 +192,8 @@ inline std::vector<uint32_t> specialize(const std::vector<uint32_t>& code, const
uint32_t bound = code[3];
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::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);
if (it == m.types.end()) return 0;
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) {
Ptr q = p;
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;
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 uint32_t ext = have[sb] ? extent(m, p) : 0;
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});
++*folded;
i += words;
+9 -2
View File
@@ -154,10 +154,17 @@ inline bool inside(const std::vector<float>& tris, const float* p) {
return n & 1;
}
// 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) {
if (t.size() < 9) return 0;
double v = 0;
for (size_t i = 0; i + 8 < t.size(); i += 9)
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]);
const double o[3] = {t[0], t[1], t[2]};
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);
}
inline void bounds(const std::vector<float>& tris, float* lo, float* hi) {
+98 -29
View File
@@ -21,6 +21,7 @@
#include "overlays.h"
#include "perf_settings.h"
#include "home_platform.h"
#include "dashboard_icon.h"
#include <openvr.h>
@@ -41,6 +42,7 @@
#include <cstring>
#include <filesystem>
#include <fstream>
#include <future>
#include <map>
#include <mutex>
#include <set>
@@ -177,7 +179,9 @@ std::vector<Env> scanEnvs() {
std::vector<uint8_t> thumb;
if (!fenvReadManifest(p.string(), m, &thumb)) continue;
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);
} else {
e.name = pretty(p.stem().string());
@@ -205,7 +209,8 @@ bool setCurrent(const Env& e) {
{
std::ofstream f(tmp);
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;
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);
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;
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;
}
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;
}
@@ -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).
std::vector<CacheDoomed> caches;
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) {
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);
for (const char* comp : {".world.json", ".edits.json"})
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();
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());
r.ok = true;
LOG("deleted %s (%zu files)%s", e.name.c_str(), r.removed.size(), r.error.empty() ? "" : (": " + r.error).c_str());
r.ok = r.error.empty();
return r;
}
@@ -725,6 +736,10 @@ std::string releaseUrl() {
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 {
enum class State { Idle, Available, Updating, Failed };
std::mutex m;
@@ -757,6 +772,10 @@ struct Updates {
// What's installed, hashed as CI hashes frame/payload.
static std::string installedHash() {
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 "
"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();
@@ -795,18 +814,41 @@ struct Updates {
fs::remove_all(dir, ec);
fs::create_directories(dir, 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());
int rc = 0;
sh("curl -fsSL --retry 2 --max-time 1800 -o " + quoted(dir / "FrameHome-installer.zip") + " '" + releaseUrl() +
"FrameHome-installer.zip' > " + quoted(log) + " 2>&1", rc);
// The installer's SHA-256 as the release lists it (latest.json "sha256-FrameHome-installer.zip", as Frame
// 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.";
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";
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.
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);
tail = tail.substr(0, tail.find('\n'));
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.
void install() {
if (!kSelfUpdate || busy || get() != State::Available) return;
// False when it can't start now (a check still running, or nothing to install).
bool install() {
if (!kSelfUpdate || busy || get() != State::Available) return false;
busy = true;
set(State::Updating);
std::thread([this] {
@@ -843,6 +886,7 @@ struct Updates {
else set(State::Failed, err);
busy = false;
}).detach();
return true;
}
};
Updates g_updates;
@@ -858,6 +902,9 @@ struct Panel {
int page = 0; // 0 environments, 1 settings, 2 debug
PerfSettings perf;
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
DebugInfo info; // ...and what it shows of stats.json and report.json
static constexpr int kShotId = 9200, kDebugOffId = 9201;
@@ -988,7 +1035,7 @@ struct Panel {
bool tick(double dt) {
bool moving = false;
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];
const float m = maxScroll(p);
if (!(dragging && p == page)) {
@@ -1016,6 +1063,7 @@ struct Panel {
}
if (!switchingKey.empty()) { moving = true; if (now >= switchUntil) switchingKey.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
return moving;
}
@@ -1201,6 +1249,14 @@ struct Panel {
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);
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;
}
@@ -1508,7 +1564,8 @@ struct Panel {
void removeEnv(size_t i) {
const Env e = envs[i];
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();
current = currentKey();
hoverT.assign(envs.size(), 0.0f);
@@ -1516,6 +1573,7 @@ struct Panel {
sc[0].content = envContent();
sc[0].target = std::clamp(sc[0].target, 0.0f, maxScroll(0));
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);
}
void click(int id) {
@@ -1531,8 +1589,8 @@ struct Panel {
return;
}
if (id == kUpdateId) {
g_updates.install();
say("Updating Frame Home: it restarts in a moment", false, 6);
if (g_updates.install()) 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;
}
if (id >= 9000 && id < 9003) {
@@ -1682,14 +1740,9 @@ int runPanel() {
vr::VROverlay()->SetOverlayInputMethod(ov, vr::VROverlayInputMethod_Mouse);
const vr::HmdVector2_t mouseScale = {{static_cast<float>(W), static_cast<float>(H)}};
vr::VROverlay()->SetOverlayMouseScale(ov, &mouseScale);
{ // dashboard tab icon: a small orange tile
std::vector<uint8_t> ic(64 * 64 * 4);
for (int y = 0; y < 64; ++y) for (int x = 0; x < 64; ++x) {
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);
{ // dashboard tab icon: the app icon's roof and headset (dashboard_icon.h); SetOverlayRaw wants it writable
std::vector<uint8_t> ic(kDashIcon, kDashIcon + sizeof kDashIcon);
vr::VROverlay()->SetOverlayRaw(icon, ic.data(), kDashIconSize, kDashIconSize, 4);
}
Panel panel;
panel.loadFonts();
@@ -1800,11 +1853,18 @@ int runPanel() {
return;
}
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();
if (pid == 0) {
setsid();
setenv("STEAMVR_APPKEY", "framehome.environment", 1); // its own app to SteamVR, not ours (our input is legacy)
execl(bin.c_str(), bin.c_str(), "--system", static_cast<char*>(nullptr));
execve(bin.c_str(), argv, envp.data());
_exit(127);
}
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" / "crashes", ec);
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)) {
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();
if (now - lastPoll > std::chrono::seconds(2)) {
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;
if (ps.fromJson(readText(rootDir() / "settings.json")) && !(ps == panel.perf)) { panel.perf = ps; if (panel.page == 1) dirty = true; }
const std::string line = statsLine();
@@ -2130,6 +2194,11 @@ bool frameHomeShowing() {
int runThumbs() {
std::error_code 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();
int made = 0;
for (const Env& e : scanEnvs()) {
+31 -7
View File
@@ -7,8 +7,10 @@
#include "tex_mips.h"
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <filesystem>
@@ -28,10 +30,13 @@
namespace texcache {
// 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() {
if (const char* e = std::getenv("FRAME_TEX_CACHE")) return std::filesystem::u8path(e);
#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
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");
@@ -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; };
// (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.
inline std::shared_ptr<TexSource> load(const TexSource& t) {
if (!texmips::wants(t)) return nullptr;
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;
Header hd = {};
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 &&
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>();
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));
@@ -85,7 +100,12 @@ inline void prune(uint64_t limit) {
std::error_code ec;
std::vector<std::pair<std::filesystem::file_time_type, std::filesystem::path>> files;
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)) {
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;
const uint64_t s = e.file_size(ec);
total += s;
@@ -103,8 +123,10 @@ inline void prune(uint64_t limit) {
inline bool store(const TexSource& chain, uint64_t k) {
std::error_code ec;
std::filesystem::create_directories(dir(), ec);
const std::filesystem::path p = pathFor(k), tmp = p.string() + ".part";
FILE* f = std::fopen(tmp.string().c_str(), "wb");
std::filesystem::path tmp = pathFor(k);
const std::filesystem::path p = tmp;
tmp += ".part";
FILE* f = open(tmp, true);
if (!f) return false;
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();
@@ -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.
class Builder {
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) {
if (!t || !texmips::wants(*t)) return;
std::lock_guard<std::mutex> l(m_);
+8 -2
View File
@@ -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;
uint32_t rawSize;
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);
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;
@@ -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.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());
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;
g->start.resize(cells + 1);
int32_t prevFirst = 0;
+7 -2
View File
@@ -236,8 +236,13 @@ private:
std::shared_ptr<const WalkGrid> grid_;
bool hasFloor_ = false;
static int64_t key(int cx, int cz) { return (static_cast<int64_t>(cx) << 32) ^ static_cast<uint32_t>(cz); }
static int cell(float v) { return static_cast<int>(std::floor(v)); }
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)); }
// (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) {
const uint32_t id = static_cast<uint32_t>(list.size());
+11 -5
View File
@@ -6,6 +6,7 @@
// settings change does); `fenv-tool tune` and Frame Studio's Calculate Best Settings work them out on the headset.
#include "perf_settings.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <string>
@@ -71,11 +72,16 @@ struct WorldSettings {
bool fromValue(const tinyjson::Value& d) {
if (!d.isObject()) return false;
*this = WorldSettings();
if (d.has("resolution")) resolution = static_cast<float>(d["resolution"].asFloat());
if (d.has("msaa")) msaa = static_cast<int>(d["msaa"].asFloat());
if (d.has("anisotropy")) anisotropy = static_cast<int>(d["anisotropy"].asFloat());
if (d.has("foveation")) foveation = static_cast<float>(d["foveation"].asFloat());
if (d.has("frameRate")) frameRate = static_cast<int>(d["frameRate"].asFloat());
// (a file's numbers: inf / NaN / 1e300 as an int is undefined, and NaN would be written back as invalid JSON)
const auto num = [&](const char* k, double unset) {
const double v = d[k].asFloat();
return std::isfinite(v) ? std::min(1e6, std::max(-1e6, v)) : unset;
};
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();
clamp();
return true;
+41 -3
View File
@@ -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,
# 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}")
add_compile_definitions(FRAME_BUILD="${FRAME_BUILD}")
set(ROOT "${CMAKE_CURRENT_SOURCE_DIR}/..")
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")
if(NOT FRAME_HOME_PAYLOAD_DIR)
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"
"${TP}/lzma/LzFindMt.c" "${TP}/lzma/Threads.c" "${TP}/lzma/CpuArch.c")
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")
target_include_directories(miniz PUBLIC "${TP}/miniz")
# 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)
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
studio.cpp
${STUDIO_PLATFORM}
@@ -143,8 +154,11 @@ add_executable(frame-studio WIN32 MACOSX_BUNDLE
"${ROOT}/src/anim_bake.cpp"
"${ROOT}/src/fenv_lzma.cpp"
"${ROOT}/src/model_import.cpp"
"${ROOT}/src/splat_import.cpp"
"${ROOT}/src/spv_opt.cpp"
"${TP}/ufbx/ufbx.c"
"${ROOT}/src/hsl_renderer.cpp"
"${ROOT}/src/hsl_renderer_splat.cpp"
"${TP}/qhe/cook/hzanim_acl.cpp"
"${TP}/stb/stb_image_impl.c"
"${TP}/stb/stb_vorbis_impl.c"
@@ -167,15 +181,39 @@ target_include_directories(frame-studio PRIVATE
"${TP}/rtm/includes"
"${TP}/astcenc/Source"
"${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)
target_link_libraries(frame-studio PRIVATE dwmapi comdlg32 shell32)
else()
find_package(Threads REQUIRED)
target_link_libraries(frame-studio PRIVATE glfw Threads::Threads ${CMAKE_DL_LIBS} m)
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)
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()
# 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)
+113 -6
View File
@@ -7,14 +7,23 @@
// 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.
// 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
// focus, and how to sleep until input arrives.
#ifdef _WIN32
#include <windows.h>
#include <dwmapi.h>
#endif
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <thread>
@@ -27,6 +36,8 @@ public:
static constexpr double kSettleSeconds = 1.0;
// Idle waits wake at least this often, so background work's progress (saving, uploading) still shows.
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.
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.
timer_ = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, 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
lastInput_ = nextFrame_ = Clock::now();
}
@@ -56,6 +76,43 @@ public:
// Call for every input from the user (keyboard, mouse, resize, focus, file drop).
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,
// a home loading, a save's progress, a fly-through). An idle wait ends early when input arrives.
void waitForNextFrame(bool sceneChanging) {
@@ -65,10 +122,29 @@ public:
// Nothing can change until the user does something: sleep until input.
waitForInput_(kIdleWakeSeconds);
nextFrame_ = Clock::now() - period(displayFps_); // the frame after waking starts at once
targetVblank_ = Clock::time_point();
return;
}
const double fps = fpsOverride_ >= 0 ? fpsOverride_ : focused ? displayFps_ : kBackgroundFps;
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.
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
@@ -76,19 +152,42 @@ public:
sleepUntil(nextFrame_);
}
// The refresh the frame now starting is for (none when not locked to them).
Clock::time_point targetVblank() const { return targetVblank_; }
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) {
return std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double>(1.0 / fps));
}
void sleepUntil(Clock::time_point when) {
const auto wait = when - Clock::now();
auto wait = when - Clock::now();
if (wait <= Clock::duration::zero()) return;
#ifdef _WIN32
// Windows' Sleep has ~1-15 ms granularity: a high-resolution timer hits the refresh.
LARGE_INTEGER due;
due.QuadPart = -static_cast<LONGLONG>(std::chrono::duration_cast<std::chrono::nanoseconds>(wait).count() / 100); // relative, 100 ns units
if (timer_ && SetWaitableTimer(timer_, &due, 0, nullptr, nullptr, FALSE)) { WaitForSingleObject(timer_, INFINITE); return; }
// Windows' Sleep has ~1-15 ms granularity: a high-resolution timer to within a few tenths of a millisecond
// (it wakes up to ~0.5 ms late), then a short spin to the moment itself.
const auto early = std::chrono::microseconds(600);
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
std::this_thread::sleep_until(when);
}
@@ -97,8 +196,16 @@ private:
std::function<void(double)> waitForInput_;
#ifdef _WIN32
HANDLE timer_ = nullptr;
double qpcHz_ = 1;
long long qpcBase_ = 0;
Clock::time_point clockBase_;
#endif
bool lockToVblank_ = false;
double displayFps_ = 60;
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_;
};
+19 -16
View File
@@ -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)
// must not inherit this pipe, or its reader never sees the end. The child's own copies (dup2) stay open.
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)
const std::string line = workingDir.empty() ? commandLine : "cd " + shellQuote(workingDir.string()) + " && " + commandLine;
const char* argv[] = {"/bin/sh", "-c", line.c_str(), nullptr};
pid_t pid = -1;
const int spawned = posix_spawn(&pid, "/bin/sh", &actions, nullptr, const_cast<char* const*>(argv), environ);
posix_spawn_file_actions_destroy(&actions);
int spawned;
{
// (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]);
if (spawned != 0) { ::close(pipeFds[0]); out = "could not start: " + commandLine; return -1; }
char buf[4096];
+2249 -243
View File
File diff suppressed because it is too large. Load diff
+11
View File
@@ -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>
+2
View File
@@ -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"
+410 -44
View File
@@ -1,15 +1,40 @@
// 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", ...] }
// 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
// copy keeps going, and a failure puts everything back). Frame Studio then restarts into it; the next start deletes
// file, unpacks it, and puts each thing in it where the old one was (the old one set aside: the running copy keeps
// 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.
//
// 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
#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 <cstdlib>
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
@@ -68,6 +93,46 @@ inline std::string assetFolder() {
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).
inline std::string jsonString(const std::string& json, const std::string& 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);
}
// 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.
struct Swap { fs::path from, to; };
@@ -92,34 +227,163 @@ inline std::vector<Swap> swapsFor(const fs::path& unpacked) {
#else
for (const auto& e : fs::directory_iterator(unpacked, ec)) out.push_back({e.path(), platform::exeDir() / e.path().filename()});
#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;
}
// 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
// update/journal.txt: what a swap moves, written (whole, then renamed into place) before the first move.
// frame-studio-update 1
// build <the new build>
// <1 if `to` was there before, else 0> TAB <to> TAB <where the old one goes> TAB <the new one>
struct JournalEntry { bool had = false; fs::path to, old, from; };
inline fs::path journalPath() { return workDir() / L"journal.txt"; }
inline bool writeJournal(const std::string& build, const std::vector<JournalEntry>& entries) {
const fs::path tmp = workDir() / L"journal.txt.part";
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() {
Lock lock;
if (!lock.take()) return;
recover();
const fs::path list = workDir() / L"aside.txt";
std::error_code ec;
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];
while (std::fgets(line, sizeof line, f)) {
std::string s = line;
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);
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 };
@@ -192,14 +456,36 @@ private:
// The download, unpacked and swapped in. Empty when it worked, else what went wrong (nothing changed then).
static std::string download(Shared& s) {
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();
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);
const fs::path file = dir / platform::widen(assetName());
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();
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.";
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…");
out.clear();
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.";
say(s, "Installing the update…");
std::vector<Swap> done; // what was put in place, to undo on a failure
std::vector<fs::path> aside;
auto undo = [&] {
for (auto it = done.rbegin(); it != done.rend(); ++it) {
std::error_code e;
fs::remove_all(it->to, e);
if (!it->from.empty()) fs::rename(it->from, it->to, e);
}
};
const std::string err = swapIn(swaps, build);
if (!err.empty()) return err;
fs::remove_all(dir / L"unpacked", ec);
fs::remove(file, ec);
return {};
}
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) {
fs::path old;
if (fs::exists(sw.to, ec)) {
old = sw.to;
old += L".old";
fs::remove_all(old, ec);
ec.clear();
fs::rename(sw.to, old, ec);
if (ec) { undo(); return "Frame Studio's folder can't be changed (" + ec.message() + "). Download the update from GitHub instead."; }
aside.push_back(old);
JournalEntry e;
e.to = sw.to;
e.from = sw.from;
e.had = fs::exists(sw.to, ec);
for (int n = 0; e.had && n < 10; ++n) {
fs::path old = sw.to;
old += n ? L".old" + std::to_wstring(n) : std::wstring(L".old");
std::error_code e2;
fs::remove_all(old, e2);
if (!fs::exists(old, e2)) { e.old = old; break; }
}
done.push_back({old, sw.to});
ec.clear();
fs::rename(sw.from, sw.to, ec); // (another drive than the download: copied instead)
if (ec) { ec.clear(); fs::copy(sw.from, sw.to, fs::copy_options::recursive, ec); }
if (ec) { undo(); return "The update couldn't be put in place (" + ec.message() + "). Nothing was changed."; }
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.";
std::error_code e2;
fs::remove_all(withSuffix(sw.to, L".new"), e2);
if (e.had) fs::remove_all(withSuffix(e.old, L".part"), e2);
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
fs::permissions(updatedProgram(), fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec, fs::perm_options::add, ec);
#endif
if (FILE* f = platform::openFile(dir / L"aside.txt", "wb")) {
for (const fs::path& p : aside) std::fprintf(f, "%s\n", platform::narrow(p.wstring()).c_str());
std::fclose(f);
}
fs::remove_all(dir / L"unpacked", ec);
fs::remove(file, ec);
noteAside(aside);
fs::remove(journalPath(), ec); // (done: from here a start has nothing to finish or undo)
return {};
}
};
+247
View File
@@ -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;
}
+180 -1
View File
@@ -18,7 +18,7 @@ int main() {
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[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);
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]);
@@ -45,6 +45,11 @@ int main() {
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};
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.update(1.0f / 90, hd);
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()));
}
{ // 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)
HslScene sc;
auto mesh = [&](const char* name, std::vector<float> p) {
@@ -405,6 +494,96 @@ int main() {
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);
}
// ── 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);
return fails;
}
+99
View File
@@ -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;
}
+6 -2
View File
@@ -1533,7 +1533,9 @@ static cgltf_size cgltf_calc_index_bound(cgltf_buffer_view* buffer_view, cgltf_s
case cgltf_component_type_r_16u:
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;
}
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:
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;
}
break;
+8 -1
View File
@@ -83,6 +83,13 @@ struct AudioPlayer {
sr, chs, (unsigned long long)frames, frames / (float)(sr > 0 ? sr : 1));
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
// 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;
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(); }
};
+13 -1
View File
@@ -7,6 +7,7 @@
#include <atomic>
#include <condition_variable>
#include <cstddef>
#include <exception>
#include <functional>
#include <mutex>
#include <thread>
@@ -32,6 +33,9 @@ public:
std::unique_lock<std::mutex> g(m_);
done_.wait(g, [&] { return left_ == 0; });
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() {
@@ -48,7 +52,14 @@ private:
}
void work() {
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;
}
void loop() {
@@ -74,6 +85,7 @@ private:
std::atomic<size_t> next_{0};
uint64_t gen_ = 0;
bool stop_ = false;
std::exception_ptr error_; // the first item of this forEach that threw (under m_)
static inline thread_local bool inside_ = false;
};
+3 -2
View File
@@ -43,7 +43,8 @@ public:
std::string asString() const { return strVal; }
int64_t asInt() const {
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) —
// 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);
@@ -154,6 +155,7 @@ private:
floatVal = (double)intVal + frac;
}
if (peek() == 'e' || peek() == 'E') {
if (!isFloat) floatVal = (double)intVal; // "1e0": no '.', so the value so far is the integer part
isFloat = true;
next();
bool eNeg = (peek() == '-');
@@ -161,7 +163,6 @@ private:
double exp = 0;
while (p < end && *p >= '0' && *p <= '9') { exp = exp * 10 + (*p - '0'); ++p; }
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");
+28
View File
@@ -463,3 +463,31 @@ inline bool parseRendMesh(const std::vector<u8>& data,
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));
}
+5 -5
View File
@@ -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),
// 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) {
auto rd32=[&](size_t o)->u32{ return o+4<=d.size()? *(const u32*)(d.data()+o):0u; };
auto rd16=[&](size_t o)->u16{ return o+2<=d.size()? *(const u16*)(d.data()+o):(u16)0; };
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()? 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; };
size_t o = 8; // after magic + version
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
// 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; } }
auto rd32=[&](size_t o)->u32{ return o+4<=d.size()? *(const u32*)(d.data()+o):0u; };
auto rd16=[&](size_t o)->u16{ return o+2<=d.size()? *(const u16*)(d.data()+o):(u16)0; };
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()? 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; };
size_t o = 8; // after magic+ver
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;
}
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; };
if (rd32(0) != 0xA34912B6u) return false; // hzanim clip magic
int hdrJoints = (int)rd32(8);
+26
View File
@@ -30,6 +30,9 @@ struct RendtxtrInfo {
// 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.
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
@@ -204,6 +207,29 @@ inline bool parseRendtxtrHeader(const std::vector<u8>& data, RendtxtrInfo& info)
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;
// Two RENDTXTR layouts exist in the wild. Older readers treated f6 as a
+99 -16
View File
@@ -762,6 +762,8 @@ public:
if (doc.has("relationships") && doc["relationships"].isArray())
for (const auto& r : doc["relationships"].arrVal)
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) {
const auto& ent = arr[i];
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
// referenced template's geometry inherits where this instance sits in the world.
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
// further out keep travelling down and apply after (they win).
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};
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";
log(" World: %zu spawns, %zu seats, %zu hotspots, %zu colliders", world.spawns.size(), world.seats.size(),
world.hotspots.size(), world.colliders.size());
@@ -852,6 +879,7 @@ public:
// ASTC-decoded ONCE; every later use copies the pixels.
struct DecodedTex {
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> cubeSrc; // deferred cube: six ASTC faces, cube empty
};
@@ -931,6 +959,14 @@ public:
return d;
}
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;
};
@@ -947,6 +983,7 @@ public:
auto d = decodeCached(*tp, false, md.isSkybox, true);
if (!d->ok) return false;
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; }
log(" Texture: %s (%ux%u fmt=%u)", tp->c_str(), d->w, d->h, d->fmt);
return true;
@@ -1142,29 +1179,63 @@ public:
auto decodeReflCube = [&](const AssetKey& k, MeshData& md) {
const std::string* rp = resolve(k);
if (!rp) return;
auto raw = readTexture(*rp);
RendtxtrInfo ri;
if (!parseRendtxtrHeader(raw, ri) || ri.faces != 6 || ri.width != ri.height || (size_t)ri.rawDataOffset + ri.rawDataLen > raw.size()) {
log(" [skybox] reflectionMap %s is not a cube (%u faces, %ux%u)", rp->c_str(), ri.faces, ri.width, ri.height);
// A streamed cube keeps its top levels in the streams and the rest in the base file: the cooked, blurred
// levels the shaders read for rough surfaces (Cascadia: 1024 and 512 streamed, 256 to 16 in the base).
// readTexture takes the largest file alone, which left one sharp level and every floor a mirror; this
// 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;
}
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;
for (u32 s = ri.width; s >= 1; s /= 2) {
const size_t face = (size_t)((s + ri.blockW - 1) / ri.blockW) * ((s + ri.blockH - 1) / ri.blockH) * 16;
if (bytes + face * 6 > ri.rawDataLen) break;
bytes += face * 6;
for (u32 s = top; s >= 1; s /= 2) {
auto it = levels.find(s);
if (it == levels.end()) break;
t->astc.insert(t->astc.end(), it->second.first, it->second.first + it->second.second);
++mips;
if (s == 1) break;
}
if (!mips) return;
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;
t->w = t->h = top; t->bw = bw; t->bh = bh; t->mips = mips; t->hdr = true; t->faces = 6;
md.reflCubeSrc = std::move(t);
md.reflCubeHalf.clear();
md.reflCubeSize = ri.width; md.reflCubeMips = mips;
log(" [skybox] reflectionMap cube %ux%u, %u mips", ri.width, ri.height, mips);
md.reflCubeSize = top; md.reflCubeMips = 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 {
@@ -1875,6 +1946,18 @@ public:
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).
// 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.
+2
View File
@@ -1,2 +1,4 @@
SMOL-V (https://github.com/aras-p/smol-v) source/smolv.{h,cpp} + LICENSE.txt
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).
+18 -7
View File
@@ -362,6 +362,7 @@ enum SpvOp
SpvOpGroupNonUniformLogicalXor = 364,
SpvOpGroupNonUniformQuadBroadcast = 365,
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;
@@ -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.
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)
{
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;
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;
// SMOL-V version
int smolVersion = ((const uint32_t*)bytes)[1] >> 24;
int smolVersion = header[1] >> 24;
if (smolVersion < 0 || smolVersion > kSmolCurrEncodingVersion)
return false;
return true;
@@ -1256,7 +1267,7 @@ static bool smolv_Read4(const uint8_t*& data, const uint8_t* dataEnd, uint32_t&
{
if (data + 4 > dataEnd)
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;
return true;
}
@@ -1286,7 +1297,8 @@ static bool smolv_ReadVarint(const uint8_t*& data, const uint8_t* dataEnd, uint3
while (data < dataEnd)
{
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;
data++;
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))
return 0;
const uint32_t* words = (const uint32_t*)smolvData;
return words[5];
return smolv_Load4((const uint8_t*)smolvData + 20); // Frame Studio: unaligned-safe
}
+502
View File
@@ -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_
Loaded 100 of 544 files, more files were not shown because too many files have changed in this diff. Show more