diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f04d89..1318395 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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-": 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" diff --git a/.gitignore b/.gitignore index 5763715..50d3b22 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..174a2d4 --- /dev/null +++ b/CHANGELOG.md @@ -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). diff --git a/CMakeLists.txt b/CMakeLists.txt index ae18578..3fbf786 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 --target frame-tests; ctest --test-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() diff --git a/README.md b/README.md index 1f376ca..0529aa1 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ behind the Steam menu and while games load. **Frame Studio** is the desktop edit 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. +**Discord:** [join the Frame Studio server](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). + ## Quick start 1. **Download** Frame Studio for your computer from the Releases page (**Latest build**, or a version release) and diff --git a/VERSION b/VERSION index 49d5957..3b04cfb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1 +0.2 diff --git a/art/icon/frame-glyph-256.png b/art/icon/frame-glyph-256.png new file mode 100644 index 0000000..7ef7bb0 Binary files /dev/null and b/art/icon/frame-glyph-256.png differ diff --git a/art/icon/frame-glyph-64.png b/art/icon/frame-glyph-64.png new file mode 100644 index 0000000..377dd49 Binary files /dev/null and b/art/icon/frame-glyph-64.png differ diff --git a/art/icon/frame-glyph.svg b/art/icon/frame-glyph.svg new file mode 100644 index 0000000..52e4e82 --- /dev/null +++ b/art/icon/frame-glyph.svg @@ -0,0 +1 @@ + diff --git a/art/icon/frame-studio-1024.png b/art/icon/frame-studio-1024.png new file mode 100644 index 0000000..a2bdabe Binary files /dev/null and b/art/icon/frame-studio-1024.png differ diff --git a/art/icon/frame-studio-128.png b/art/icon/frame-studio-128.png new file mode 100644 index 0000000..a20bbee Binary files /dev/null and b/art/icon/frame-studio-128.png differ diff --git a/art/icon/frame-studio-16.png b/art/icon/frame-studio-16.png new file mode 100644 index 0000000..43e5a2c Binary files /dev/null and b/art/icon/frame-studio-16.png differ diff --git a/art/icon/frame-studio-24.png b/art/icon/frame-studio-24.png new file mode 100644 index 0000000..6797c75 Binary files /dev/null and b/art/icon/frame-studio-24.png differ diff --git a/art/icon/frame-studio-256.png b/art/icon/frame-studio-256.png new file mode 100644 index 0000000..2a12020 Binary files /dev/null and b/art/icon/frame-studio-256.png differ diff --git a/art/icon/frame-studio-32.png b/art/icon/frame-studio-32.png new file mode 100644 index 0000000..d32128c Binary files /dev/null and b/art/icon/frame-studio-32.png differ diff --git a/art/icon/frame-studio-48.png b/art/icon/frame-studio-48.png new file mode 100644 index 0000000..8750709 Binary files /dev/null and b/art/icon/frame-studio-48.png differ diff --git a/art/icon/frame-studio-512.png b/art/icon/frame-studio-512.png new file mode 100644 index 0000000..06b04b5 Binary files /dev/null and b/art/icon/frame-studio-512.png differ diff --git a/art/icon/frame-studio-64.png b/art/icon/frame-studio-64.png new file mode 100644 index 0000000..eeaed89 Binary files /dev/null and b/art/icon/frame-studio-64.png differ diff --git a/art/icon/frame-studio-square-1024.png b/art/icon/frame-studio-square-1024.png new file mode 100644 index 0000000..4b37fd8 Binary files /dev/null and b/art/icon/frame-studio-square-1024.png differ diff --git a/art/icon/frame-studio-square-512.png b/art/icon/frame-studio-square-512.png new file mode 100644 index 0000000..fb0fcde Binary files /dev/null and b/art/icon/frame-studio-square-512.png differ diff --git a/art/icon/frame-studio-square.svg b/art/icon/frame-studio-square.svg new file mode 100644 index 0000000..73d8f14 --- /dev/null +++ b/art/icon/frame-studio-square.svg @@ -0,0 +1 @@ + diff --git a/art/icon/frame-studio.icns b/art/icon/frame-studio.icns new file mode 100644 index 0000000..d5626b5 Binary files /dev/null and b/art/icon/frame-studio.icns differ diff --git a/art/icon/frame-studio.ico b/art/icon/frame-studio.ico new file mode 100644 index 0000000..b8eaeee Binary files /dev/null and b/art/icon/frame-studio.ico differ diff --git a/art/icon/frame-studio.svg b/art/icon/frame-studio.svg new file mode 100644 index 0000000..9233b10 --- /dev/null +++ b/art/icon/frame-studio.svg @@ -0,0 +1 @@ + diff --git a/cmake/frame_version.cmake b/cmake/frame_version.cmake new file mode 100644 index 0000000..cfd9b9b --- /dev/null +++ b/cmake/frame_version.cmake @@ -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.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}") diff --git a/docs/studio-guide.md b/docs/studio-guide.md index 091b23a..2177896 100644 --- a/docs/studio-guide.md +++ b/docs/studio-guide.md @@ -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 | diff --git a/frame/install-frame-home.ps1 b/frame/install-frame-home.ps1 index 63703d0..0d6d2d3 100755 --- a/frame/install-frame-home.ps1 +++ b/frame/install-frame-home.ps1 @@ -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" diff --git a/frame/install-frame-home.sh b/frame/install-frame-home.sh index 22ec6d5..bb27140 100755 --- a/frame/install-frame-home.sh +++ b/frame/install-frame-home.sh @@ -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) diff --git a/frame/payload/install.sh b/frame/payload/install.sh index b4f8e32..0456425 100755 --- a/frame/payload/install.sh +++ b/frame/payload/install.sh @@ -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" < 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) { diff --git a/layer/frame_home_layer.cpp b/layer/frame_home_layer.cpp index 2b9da83..52046bf 100644 --- a/layer/frame_home_layer.cpp +++ b/layer/frame_home_layer.cpp @@ -26,8 +26,10 @@ #include #include +#include #include #include +#include #include #include #include @@ -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 g_instances; std::unordered_map g_physToInstance; // physical device (dispatch key) -> instance data std::unordered_map g_devices; +std::unordered_map g_queueFamily; // (g_mutex) each queue the compositor got, its family DeviceData* dev(const void* dispatchable) { std::lock_guard l(g_mutex); @@ -176,6 +182,12 @@ std::unordered_map g_twins; // Aurora pipeline -> std::unordered_set g_skip; // other Aurora pieces: not drawn while the home shows std::unordered_set g_camera; // passthrough (the cameras' reprojection) std::atomic 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(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 g_bound; // graphics pipeline std::unordered_map g_names; // vertex + fragment shader names (trace) std::atomic g_traceDraws{0}; // ~/frame-home/layer/trace: log this many draws std::atomic g_off{false}; +std::atomic 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 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 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 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 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 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(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 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 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(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(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(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(VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT) || (subsampled && !subsampledSets)) { + if (p.hello.flags & ~static_cast(VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT) || (subsampled && !g_ours.setLayoutSubsampled)) { static_cast(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 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(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 = ⅈ - 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 = ⅈ + 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 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(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 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(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(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(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(static_cast(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(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(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(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(sizeof h) && (h.version == envshare::kVersion || h.version == 5); const bool v4 = n == static_cast(envshare::kHelloV4Size) && h.version == 4; const bool v3 = n == static_cast(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 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& draw) { +void drawTwin(VkCommandBuffer cb, const Twin& t, VkDescriptorSet set, bool subsampled, uint32_t uboOffset, const std::function& draw) { DeviceData* dd = dev(cb); VkPipeline original; { std::lock_guard 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 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 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 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 ol(g_oursMutex); + g_cbPool.erase(cbs[i]); + } + std::lock_guard 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 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(cb) = *reinterpret_cast(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 +void acquiresFor(DeviceData* d, uint32_t family, uint32_t n, CommandBufferAt at, std::vector& 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 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> acq(n); + bool any = false; + if (family != VK_QUEUE_FAMILY_IGNORED) { + std::lock_guard 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 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(acq[i].size()); + a.pCommandBuffers = acq[i].data(); + batches.push_back(a); + } + batches.push_back(si[i]); + } + return dd->QueueSubmit(q, static_cast(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> acq(n); + bool any = false; + if (family != VK_QUEUE_FAMILY_IGNORED) { + std::lock_guard 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> infos(n); + std::vector 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(infos[i].size()); + a.pCommandBufferInfos = infos[i].data(); + batches.push_back(a); + } + batches.push_back(si[i]); + } + return submit(q, static_cast(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 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 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(ci->pNext); l; l = static_cast(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(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(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 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(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(QueueSubmit2); return nullptr; diff --git a/src/clay_spirv.h b/src/clay_spirv.h index 90d328a..2df7eff 100644 --- a/src/clay_spirv.h +++ b/src/clay_spirv.h @@ -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, +}; diff --git a/src/dashboard_icon.h b/src/dashboard_icon.h new file mode 100644 index 0000000..ab72250 --- /dev/null +++ b/src/dashboard_icon.h @@ -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 + +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, +}; diff --git a/src/env_export.h b/src/env_export.h index 2ac539f..75790b3 100644 --- a/src/env_export.h +++ b/src/env_export.h @@ -76,17 +76,20 @@ public: int beginFrame() { const int k = static_cast(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(k), s, motion); // (under its sequence count: env_share.h) state_->published.store(frame_); state_->latest.store(static_cast(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(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(k), s, motion); state_->published.store(frame_); state_->latest.store(static_cast(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_; } diff --git a/src/env_share.h b/src/env_share.h index b7612ed..fb0445d 100644 --- a/src/env_share.h +++ b/src/env_share.h @@ -23,7 +23,11 @@ #include #include #include +#include #include +#ifdef _MSC_VER +#include +#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 latest{0xFFFFFFFFu}; // newest finished slot (0xFFFFFFFF none) std::atomic published{0}; // frame number of `latest` std::atomic publishNs{0}; // CLOCK_MONOTONIC when it was published @@ -97,7 +114,106 @@ struct State { Slot slot[kSlots]; std::atomic 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 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(dst); + const uint32_t* s = static_cast(src); + for (size_t i = 0; i < bytes / 4; ++i) { +#ifdef _MSC_VER + __iso_volatile_store32(reinterpret_cast(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(dst); + const uint32_t* s = static_cast(src); + for (size_t i = 0; i < bytes / 4; ++i) { +#ifdef _MSC_VER + d[i] = static_cast(__iso_volatile_load32(reinterpret_cast(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 diff --git a/src/fenv.h b/src/fenv.h index bc33f81..f941497 100644 --- a/src/fenv.h +++ b/src/fenv.h @@ -21,12 +21,15 @@ #else #include #include +#include #include #endif #include "fenv_lzma.h" #include "miniz.h" #include +#include +#include #include #include #include @@ -35,6 +38,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -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 sources; // footprint first, then the vista (or one whole home) std::vector models; // imported model files (member paths) + std::vector 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 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(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(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 readMember(mz_zip_archive& z, const char* name, uint64_t limit = uint64_t(64) << 20) { std::vector out; const int i = mz_zip_reader_locate_file(&z, name, nullptr, 0); @@ -96,11 +167,51 @@ inline std::vector readMember(mz_zip_archive& z, const char* name, uint size_t n = 0; void* p = mz_zip_reader_extract_to_heap(&z, static_cast(i), &n, 0); if (!p) return out; - try { out.assign(static_cast(p), static_cast(p) + n); } catch (const std::exception&) { out = {}; } + if (mz_crc32(MZ_CRC32_INIT, static_cast(p), n) == st.m_crc32) + try { out.assign(static_cast(p), static_cast(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(o); + k.crc = mz_crc32(k.crc, static_cast(p), n); + k.f.write(static_cast(p), static_cast(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 /.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>> held; + static HeldFolders& get() { static HeldFolders h; return h; } + void hold(const std::string& folder, std::unique_ptr l) { + if (!l) return; + std::lock_guard 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 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& 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(_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(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(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(std::max(0, e["count"].asInt())); + if (e["xform"].size() == 16) + for (size_t k = 0; k < 16; ++k) s.xform[k] = static_cast(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(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& fenvFeatures() { + static const std::vector 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* 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 /---