diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4aba03..8294fb7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,14 +1,18 @@ -# Builds Frame Studio for every computer it runs on, and the Frame Home installer, on every push (any branch or tag), -# every pull request, and by hand (Actions > build > Run workflow). Each build is a downloadable artifact of the run. -# A push to main also replaces the "latest" pre-release with them (one place to download the newest of everything); -# a tag "v*" publishes them as that version's release. +# Builds Frame Studio for every computer it runs on, plus a same-commit Frame Home payload, on branch pushes, +# pull requests, and by hand (Actions > build > Run workflow). A push to main replaces the Studio "latest" pre-release +# and includes the Frame Home installer. A tag "v*" publishes a versioned release containing both products. name: build on: push: + branches: ['**'] + tags: ['v*'] pull_request: workflow_dispatch: +permissions: + contents: read + # A newer push to the same branch replaces a run still going (its builds would be out of date anyway). concurrency: group: build-${{ github.ref }} @@ -17,10 +21,12 @@ concurrency: # Every Studio build knows the commit it's from (studio/updater.h compares it with the latest release's). env: FRAME_BUILD: ${{ github.sha }} + FRAME_HOME_PAYLOAD_DIR: ${{ github.workspace }}/build/frame-home/payload jobs: linux: name: Linux ${{ matrix.arch }} + needs: [frame-home] strategy: fail-fast: false matrix: @@ -31,23 +37,54 @@ jobs: - { arch: arm64, runner: ubuntu-24.04-arm, extra: "" } runs-on: ${{ matrix.runner }} timeout-minutes: 45 + env: + FRAME_USE_CCACHE: "1" + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 500M + CCACHE_COMPILERCHECK: content + APT_CACHE_DIR: ${{ github.workspace }}/.apt-cache steps: - - uses: actions/checkout@v4 + - name: Check out source + uses: actions/checkout@v4 + - name: Restore compiler cache + uses: actions/cache@v5 + with: + path: ${{ github.workspace }}/.ccache + key: ccache-studio-linux-${{ matrix.arch }}-${{ github.sha }} + restore-keys: ccache-studio-linux-${{ matrix.arch }}- + - name: Restore apt package cache + uses: actions/cache@v5 + with: + path: .apt-cache/archives + key: apt-ubuntu-24.04-${{ matrix.arch }}-${{ hashFiles('.github/workflows/build.yml') }} + restore-keys: apt-ubuntu-24.04-${{ matrix.arch }}- + - name: Download Frame Home payload + uses: actions/download-artifact@v4 + with: + name: FrameHome-payload + path: build/frame-home/payload - name: Tools and X11 headers run: | # x86 links against 32-bit X11, so apt needs the i386 packages too if [ "${{ matrix.i386 }}" = "true" ]; then sudo dpkg --add-architecture i386; fi + sudo mkdir -p "$APT_CACHE_DIR/archives/partial" + sudo chown _apt:root "$APT_CACHE_DIR/archives/partial" sudo apt-get update - sudo apt-get install -y ninja-build cmake libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxkbcommon-dev ${{ matrix.extra }} + sudo apt-get -o Dir::Cache::archives="$APT_CACHE_DIR/archives" install -y ccache ninja-build cmake libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxkbcommon-dev ${{ matrix.extra }} + sudo chown -R "$(id -u):$(id -g)" "$APT_CACHE_DIR/archives" - name: Build run: sh scripts/build-studio.sh linux-${{ matrix.arch }} - - uses: actions/upload-artifact@v4 + - name: Show compiler cache statistics + run: ccache --show-stats + - name: Upload Linux build + uses: actions/upload-artifact@v4 with: name: FrameStudio-linux-${{ matrix.arch }} - path: dist/FrameStudio-linux-${{ matrix.arch }}.tar.gz + path: build/stage/FrameStudio-linux-${{ matrix.arch }} windows: name: Windows ${{ matrix.arch }} + needs: [frame-home] strategy: fail-fast: false matrix: @@ -55,7 +92,13 @@ jobs: runs-on: windows-latest timeout-minutes: 45 steps: - - uses: actions/checkout@v4 + - name: Check out source + uses: actions/checkout@v4 + - name: Download Frame Home payload + uses: actions/download-artifact@v4 + with: + name: FrameHome-payload + path: build/frame-home/payload - name: Build (MSVC) shell: cmd run: studio\build_windows.bat ${{ matrix.arch }} "%GITHUB_WORKSPACE%\build\windows-${{ matrix.arch }}" @@ -68,15 +111,15 @@ jobs: Copy-Item "build/windows-${{ matrix.arch }}/Frame Studio.exe" $stage Copy-Item -Recurse "build/windows-${{ matrix.arch }}/frame" "$stage/frame" Copy-Item LICENSE "$stage/LICENSE.txt" - New-Item -ItemType Directory -Force dist | Out-Null - Compress-Archive -Path $stage -DestinationPath "dist/$name.zip" -Force - - uses: actions/upload-artifact@v4 + - name: Upload Windows build + uses: actions/upload-artifact@v4 with: name: FrameStudio-windows-${{ matrix.arch }} - path: dist/FrameStudio-windows-${{ matrix.arch }}.zip + path: build/stage/FrameStudio-windows-${{ matrix.arch }} macos: name: macOS ${{ matrix.arch }} + needs: [frame-home] strategy: fail-fast: false matrix: @@ -87,32 +130,90 @@ jobs: - { arch: x64, runner: macos-15-intel } runs-on: ${{ matrix.runner }} timeout-minutes: 45 + env: + FRAME_USE_CCACHE: "1" + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 500M + CCACHE_COMPILERCHECK: content steps: - - uses: actions/checkout@v4 + - name: Check out source + uses: actions/checkout@v4 + - name: Restore compiler cache + uses: actions/cache@v5 + with: + path: ${{ github.workspace }}/.ccache + key: ccache-studio-macos-${{ matrix.arch }}-${{ github.sha }} + restore-keys: ccache-studio-macos-${{ matrix.arch }}- + - name: Restore Homebrew package cache + uses: actions/cache@v5 + with: + path: ~/Library/Caches/Homebrew/downloads + key: brew-macos-${{ matrix.arch }}-${{ hashFiles('.github/workflows/build.yml') }} + restore-keys: brew-macos-${{ matrix.arch }}- + - name: Download Frame Home payload + uses: actions/download-artifact@v4 + with: + name: FrameHome-payload + path: build/frame-home/payload - name: Tools and MoltenVK (Vulkan on Metal, bundled into the app) - run: brew install ninja molten-vk + run: brew install ccache ninja molten-vk - name: Build run: sh scripts/build-studio.sh macos-${{ matrix.arch }} - - uses: actions/upload-artifact@v4 + - name: Show compiler cache statistics + run: ccache --show-stats + - name: Upload macOS build + uses: actions/upload-artifact@v4 with: name: FrameStudio-macos-${{ matrix.arch }} - path: dist/FrameStudio-macos-${{ matrix.arch }}.zip + path: build/stage/FrameStudio-macos-${{ matrix.arch }} frame-home: - name: Frame Home installer - runs-on: ubuntu-24.04 - timeout-minutes: 30 + name: Build Frame Home payload + runs-on: ubuntu-24.04-arm + timeout-minutes: 45 + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 500M + CCACHE_COMPILERCHECK: content + APT_CACHE_DIR: ${{ github.workspace }}/.apt-cache steps: - - uses: actions/checkout@v4 - - name: Headset programs match the source - run: sh scripts/frame-payload.sh check - - name: Package - run: sh scripts/build-studio.sh frame-home - - uses: actions/upload-artifact@v4 + - name: Check out source + uses: actions/checkout@v4 + - name: Restore compiler cache + uses: actions/cache@v5 + with: + path: ${{ github.workspace }}/.ccache + key: ccache-frame-home-arm64-${{ github.sha }} + restore-keys: ccache-frame-home-arm64- + - name: Restore apt package cache + 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- + - name: Install Frame Home build dependencies + run: | + set -euo pipefail + sudo mkdir -p "$APT_CACHE_DIR/archives/partial" + sudo chown _apt:root "$APT_CACHE_DIR/archives/partial" + sudo apt-get update + sudo apt-get -o Dir::Cache::archives="$APT_CACHE_DIR/archives" install -y ccache cmake ninja-build gcc-13 g++-13 glslc zip libdeflate-dev + 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: Show compiler cache statistics + run: ccache --show-stats + - name: Upload Frame Home payload + uses: actions/upload-artifact@v4 + with: + name: FrameHome-payload + path: build/frame-home/payload + - name: Upload Frame Home installer + uses: actions/upload-artifact@v4 with: name: FrameHome-installer - path: dist/FrameHome-installer.zip - + path: build/frame-home/FrameHome-installer-stage/FrameHome-installer + if-no-files-found: error release: name: Publish # "latest" gets every build that finished (a failed or still-slow platform just isn't in it); a version release @@ -125,30 +226,88 @@ jobs: timeout-minutes: 20 permissions: contents: write + id-token: write + attestations: write env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} steps: - - uses: actions/download-artifact@v4 + - name: Download Studio build artifacts + uses: actions/download-artifact@v4 with: + pattern: FrameStudio-* path: dist - merge-multiple: true - - uses: actions/checkout@v4 + merge-multiple: false + - name: Download Frame Home payload + uses: actions/download-artifact@v4 with: - path: src - # Every download carries frame/payload: none is published beside headset programs older than the source - # (Studio would save environments the headset can't open). - - name: Headset programs match the source - run: sh src/scripts/frame-payload.sh check + name: FrameHome-payload + path: ${{ runner.temp }}/frame-home-payload + - name: Download Frame Home installer + uses: actions/download-artifact@v4 + with: + name: FrameHome-installer + path: dist/FrameHome-installer + - name: Rebuild release archives + run: | + for package in dist/FrameStudio-linux-*; do + [ -d "$package" ] || continue + name=${package##*/} + # (artifacts don't keep file modes: the program itself has to be executable again) + chmod +x "$package/Frame Studio" + if [ -d "$package/frame/bin" ]; then + chmod +x "$package/frame/install.sh" "$package/frame/bin/"* + fi + if [ -d "$package/pc/bin" ]; then + chmod +x "$package/pc/install.sh" "$package/pc/bin/"* + fi + tar -czf "dist/$name.tar.gz" -C dist "$name" + rm -rf "$package" + done + for package in dist/FrameStudio-windows-* dist/FrameStudio-macos-*; do + [ -d "$package" ] || continue + name=${package##*/} + if [ -d "$package/frame/bin" ]; then + chmod +x "$package/frame/install.sh" "$package/frame/bin/"* + fi + if [[ "$name" == FrameStudio-macos-* ]]; then + chmod +x "$package/Frame Studio.app/Contents/MacOS/Frame Studio" + fi + (cd dist && zip -qr "$name.zip" "$name") + rm -rf "$package" + done + chmod +x dist/FrameHome-installer/install-frame-home.sh \ + dist/FrameHome-installer/payload/install.sh \ + dist/FrameHome-installer/payload/bin/* + (cd dist && zip -qr FrameHome-installer.zip FrameHome-installer) + rm -rf dist/FrameHome-installer + python3 - <<'PY' + from pathlib import Path + from zipfile import ZipFile + + for path in Path("dist").glob("*.zip"): + with ZipFile(path) as archive: + nested = [name for name in archive.namelist() if name.lower().endswith(".zip")] + if nested: + raise SystemExit(f"{path} contains nested ZIP files: {nested}") + PY # What Frame Studio (studio/updater.h) and the headset's Environments panel check for updates: this commit, # Frame Home's identity (a hash of the headset programs, the same one the panel works out for what's installed) # and the files in this release. - name: latest.json run: | - home=$(cd src/frame/payload && sha256sum bin/frame-home bin/frame-switcher bin/fenv-tool layer/libframe_home_layer.so layer/env_ps.spv | cut -c1-64 | sha256sum | cut -c1-64) + home=$(cd "$RUNNER_TEMP/frame-home-payload" && sha256sum bin/frame-home bin/frame-switcher bin/fenv-tool layer/libframe_home_layer.so layer/env_ps.spv | cut -c1-64 | sha256sum | cut -c1-64) files=$(ls dist | python3 -c 'import json, sys; print(json.dumps([l.strip() for l in sys.stdin if l.strip()]))') 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 cat dist/latest.json + - name: Attest Studio release archives + uses: actions/attest-build-provenance@v2 + with: + subject-path: dist/FrameStudio-* + - name: Attest Frame Home installer + uses: actions/attest-build-provenance@v2 + with: + 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 diff --git a/.gitignore b/.gitignore index eb0062c..5763715 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ # build output /build*/ /dist/ +/frame/payload/bin/ +/frame/payload/layer/libframe_home_layer.so +/frame/payload/layer/env_ps.spv +/frame/payload/SOURCE studio/build*/ tools/*/build*/ # editor / OS clutter diff --git a/CMakeLists.txt b/CMakeLists.txt index 50df305..ae18578 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,8 +94,9 @@ 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) -# libdeflate when the system has it (the Frame): APK scene.zips inflate several times faster than with miniz. -find_library(LIBDEFLATE_LIB deflate) +# 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) find_path(LIBDEFLATE_INC libdeflate.h) if(LIBDEFLATE_LIB AND LIBDEFLATE_INC) target_compile_definitions(frame-scene PUBLIC QHE_HAVE_LIBDEFLATE) diff --git a/README.md b/README.md index fb7bc6a..405409f 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,9 @@ headset runs, and lets you edit, save and send them to the headset. ## Downloads -Every push builds everything. The newest build of `main` is always on the Releases page as **Latest build**; tagged -versions (`v1.0`, ...) get their own release. Builds of other branches are under Actions (open a run, then -Artifacts). Once installed, Frame Studio and Frame Home both update themselves from **Latest build**: Studio from its -top bar, Frame Home from the Environments panel on the headset. +Every push builds Frame Studio and its matching Frame Home payload. The **Latest build** release contains both Studio +packages and `FrameHome-installer.zip`; each `v*` version release contains both products too. Studio updates from its +top bar, and Frame Home updates from the Environments panel on the headset. Both use the same release channel. | Computer | Frame Studio | |---|---| @@ -39,6 +38,8 @@ Studio needs a GPU with Vulkan 1.2: any recent NVIDIA, AMD or Intel driver on Wi runs on Metal through MoltenVK, which comes inside the app. The 32-bit builds have at most 4 GB to work in, so the very largest homes may not fit. +See [Installing Frame Home](docs/install-frame-home.md) for how to install the runtime on a Steam Frame. + ## Highlights **Frame Home (on the headset)** @@ -75,7 +76,7 @@ very largest homes may not fit. | [Performance](docs/performance.md) | Reaching 120 fps, checking it, fixing stutter | | [Frame Studio guide](docs/studio-guide.md) | The Studio window, tools and panels | | [Bundles](docs/bundles.md) | Several environments in one `.fbundle` | -| [Building from source](docs/building.md) | Building on each system, CI, the headset side | +| [Building from source](docs/building.md) | Building on each system, CI, and Frame Home payload builds | | [Troubleshooting](docs/troubleshooting.md) | Symptoms and fixes, logs, the self-test | ## Building from source @@ -87,8 +88,8 @@ Everything Studio needs is in `third_party/`; you need CMake 3.16+ and Ninja. In | Windows (Visual Studio 2022 with C++) | `studio\build_windows.bat x64` (or `x86`, `arm64`) | | Linux, macOS | `scripts/build-studio.sh ...` or `scripts/build-studio.sh all` | -The headset side is built on the Steam Frame; a ready-built copy is in `frame/payload/`. Full details, prerequisites -and the CI: [docs/building.md](docs/building.md). +Frame Home binaries are built in GitHub Actions and are not checked into `frame/payload/`. Full details, prerequisites +and CI: [docs/building.md](docs/building.md). ## Repository layout @@ -97,7 +98,7 @@ and the CI: [docs/building.md](docs/building.md). | `studio/` | Frame Studio: `studio.cpp` (the editor), `platform*.{h,cpp}` (Windows, Linux, macOS), `frame_pacer.h` | | `src/` | Shared engine: renderer (`hsl_renderer`), scene loading and baking, `.fenv`, walkable area, Frame Home (`main.cpp`), the Environments panel (`switcher.cpp`), `fenv-tool` | | `layer/` | The Vulkan layer inside SteamVR's compositor that shows Frame Home in place of its environment | -| `frame/` | The headset payload and the stand-alone installers | +| `frame/` | Frame Home installer source and stand-alone installer scripts | | `scripts/`, `cmake/` | Build script and the llvm-mingw toolchain file | | `tools/`, `tests_loco/` | Test and measuring tools, movement tests | | `third_party/` | Vendored libraries (each with its own license) | diff --git a/docs/building.md b/docs/building.md index 3798b0a..f291a9b 100644 --- a/docs/building.md +++ b/docs/building.md @@ -3,12 +3,11 @@ You only need this page if you want to change the code or build a version that isn't on the Releases page. Ready builds for every computer are on GitHub (see [getting-started.md](getting-started.md#download)). -There are two parts: +There are two build surfaces: - **Frame Studio** builds on Windows, Linux and macOS (and cross-builds for Windows from Linux). -- **The headset side** (Frame Home, the Environments panel, `fenv-tool` and the compositor layer) builds on the - Steam Frame itself. A ready-built copy is in the repository, in `frame/payload/`, so you only rebuild it when you - change that code. +- **Frame Home** (the renderer, Environments panel, `fenv-tool` and compositor layer) is built by GitHub Actions on + ARM64 Linux, and can also be built locally on the Steam Frame. Everything Studio needs is in `third_party/`. Vulkan is loaded when Studio starts, so you don't need the Vulkan SDK to build it. All builds use CMake 3.16 or later and Ninja. @@ -25,9 +24,14 @@ CMake and Ninja. studio\build_windows.bat x64 ``` + To bundle Frame Home with it, download the `FrameHome-payload` artifact from an Actions run, extract it, and set + `FRAME_HOME_PAYLOAD_DIR` to its payload directory before running the batch file, or + [build it manually](#frame-home-payload). Without one, Studio still builds (CMake warns), but its + **Install on Steam Frame** can't install Frame Home. Release builds (CI) require it. + Use `x86` for 32-bit Windows or `arm64` for Windows on ARM instead of `x64`. 3. The program is `%LOCALAPPDATA%\FrameStudio\build-x64\Frame Studio.exe` (the folder ends in your architecture). - The headset files are copied beside it, in `frame\`. + The headset files, when bundled, are copied beside it, in `frame\`. To build into another folder, give it as the second argument: `studio\build_windows.bat x64 C:\builds\studio`. @@ -41,6 +45,8 @@ scripts/build-studio.sh TARGET... [--build-dir DIR] ``` Each target is built and packed into `dist/`. `scripts/build-studio.sh --help` lists them. +The script uses `build/frame-home/payload` when present. Set `FRAME_HOME_PAYLOAD_DIR` to use a payload from another +location. | Target | Build on | Output | |---|---|---| @@ -48,12 +54,10 @@ Each target is built and packed into `dist/`. `scripts/build-studio.sh --help` l | `linux-x86` | 64-bit Linux with `gcc-multilib g++-multilib` | `dist/FrameStudio-linux-x86.tar.gz` | | `windows-x64`, `windows-x86`, `windows-arm64` | Linux with [llvm-mingw](https://github.com/mstorsjo/llvm-mingw) | `dist/FrameStudio-windows-.zip` | | `macos-arm64`, `macos-x64` | A Mac with Xcode's command line tools | `dist/FrameStudio-macos-.zip` | -| `frame-home` | Anywhere | `dist/FrameHome-installer.zip` (the headset files and install scripts, no Studio) | | `all` | Anywhere | Every target this computer can build | -`all` builds: on a Mac, both macOS targets and `frame-home`; on 64-bit x86 Linux, `linux-x64`, `linux-x86` and -`frame-home`; on ARM Linux, `linux-arm64` and `frame-home`. On Linux it adds the three Windows targets when it finds -llvm-mingw. +`all` builds the targets supported by this computer. On Linux it adds the three Windows targets when it finds +llvm-mingw. The standalone Frame Home installer is built separately by GitHub Actions. Build folders go in `build/` unless you pass `--build-dir`. @@ -108,18 +112,22 @@ The Apple Silicon build runs on macOS 11 and later, the Intel build on macOS 10. ## Continuous builds (GitHub Actions) -The workflow is `.github/workflows/build.yml`. +The `.github/workflows/build.yml` workflow builds both products and publishes their assets together. There is one +rolling `latest` release and one shared `v*` tag namespace; each release contains the Studio packages and +`FrameHome-installer.zip`. | When | What happens | |---|---| -| Every push, to any branch or tag, and every pull request | All targets build: Linux x64, x86, arm64; Windows x64, x86, arm64 (MSVC); macOS arm64 and x64; the Frame Home installer. Each is kept as an artifact of the run. | -| A push to `main` | The **Latest build** pre-release is replaced with the new builds. Builds that worked are published even if another platform failed. | -| A tag starting with `v` (for example `v1.0`) | A release with that name is published, with all builds. It is only made when every build succeeded. | +| Every push and pull request | Studio builds for Linux, Windows and macOS; Frame Home is built on ARM64 and bundled into Studio packages. Successful outputs are kept as run artifacts. | +| A push to `main` | The **Latest build** pre-release is replaced with Studio builds and the standalone Frame Home installer. | +| A tag starting with `v` (for example `v1.0`) | One version release is published with both Studio packages and the Frame Home installer. | | By hand | **Actions > build > Run workflow** | A newer push to the same branch cancels a run that is still going. +Linux, macOS, and Frame Home jobs cache compiler outputs between runs; Windows/MSVC builds are not cached. +Linux jobs also cache downloaded apt packages, and macOS caches Homebrew downloads; apt repository metadata is still refreshed each run. -To publish a version: +To publish versions: ```sh git tag v1.0 @@ -132,58 +140,45 @@ To download a build from any run: 2. Click the run (each push is one). 3. Scroll down to **Artifacts** and download the one you need. You must be signed in. -## The headset side +## Frame Home payload -Frame Home (`frame-home`), the Environments panel (`frame-switcher`), `fenv-tool` and the compositor layer are built -on the Steam Frame itself, against SteamVR's own OpenVR library (`/opt/steamvr/bin/linuxarm64`, set by -`OPENVR_LIB_DIR`). You need a C++17 compiler, CMake, Ninja and the Vulkan headers on the headset. +The release payload is built from source by GitHub Actions on ARM64 Linux, currently pinned to glibc 2.39 for compatibility with SteamOS stable/beta. The same artifact is used by Studio packages and the standalone installer. -1. Copy the repository to the headset and log in: `ssh frame`. -2. Build the programs with the top-level `CMakeLists.txt`: - - ```sh - cd ~/frame-env - cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release - cmake --build build - ``` - - This makes `build/frame-home`, `build/frame-switcher` and `build/fenv-tool`. -3. Build the compositor layer (it is not part of CMake): - - ```sh - mkdir -p build/layer - g++ -O2 -shared -fPIC -std=c++17 -fvisibility=hidden layer/frame_home_layer.cpp -o build/layer/libframe_home_layer.so -lpthread - ``` - -4. Compile the layer's shader `layer/env_ps.frag` to SPIR-V, for example with glslc: - - ```sh - glslc layer/env_ps.frag -o build/layer/env_ps.spv - ``` - -### Refreshing frame/payload - -`frame/payload/` is the ready-built copy that every Studio build and the installer carry. After building on the -headset, copy the results into it: - -| From (on the headset) | To (in the repository) | -|---|---| -| `build/frame-home`, `build/frame-switcher`, `build/fenv-tool` | `frame/payload/bin/` | -| `build/layer/libframe_home_layer.so`, `build/layer/env_ps.spv` | `frame/payload/layer/` | - -`frame/payload/install.sh` is the installer itself; it lives in the repository and needs no build. - -Then record which source they were built from, and commit the new payload together with the code that changed: +To build and test on the Steam Frame, run: ```sh -sh scripts/frame-payload.sh stamp +sh scripts/build-frame-home.sh ``` -`frame/payload/SOURCE` holds a hash of everything the headset side is built from (`src/`, `third_party/`, `layer/`, -`CMakeLists.txt`). `sh scripts/frame-payload.sh check` tells you whether the payload is still current. The release -build runs it too and publishes nothing while the payload is older than the source: a Studio beside an older Frame -Home saves environments the headset can't open. Studio also asks the headset (`fenv-tool version`) before -**Send to Frame** and says when its Frame Home needs updating. +This uses the headset's native OpenVR library and Vulkan SDK, normally at `/opt/steamvr/bin/linuxarm64`, and requires +CMake, Ninja, a C/C++ compiler, `glslc`, `zip`, and Python 3. Set `OPENVR_LIB_DIR` if SteamVR is installed elsewhere. The +payload, installer ZIP, and build manifest are written under `build/frame-home/`; the build manifest is included in the +payload artifact as `build/frame-home/payload/build-info.txt`. This mode accepts the headset's +installed glibc version, including Preview's 2.43; those local outputs are for development and are not the release +build for stable/beta. + +CI selects release mode explicitly with `sh scripts/build-frame-home.sh --ci` (or `FRAME_HOME_BUILD_MODE=ci`). It +requires ARM64 Linux with glibc 2.39 and GCC 13, then checks the ELF architecture and GLIBC symbol ceiling before +packaging. The build manifest records compiler/tool versions, dynamic dependencies, and +GLIBC/GLIBCXX/CXXABI requirements in `build/frame-home/payload/build-info.txt`; CI keeps it with the payload artifact. Studio can +bundle a locally built or downloaded payload by setting `FRAME_HOME_PAYLOAD_DIR` to that payload directory. The +installer ZIP is checked against a Frame Home-only file allowlist before it is published. + +Studio checks the headset's `fenv-tool version` before **Send to Frame** and warns when the headset cannot open the +scene format Studio would save. + +### Attestations + +Frame Home installer and Studio release archives carry GitHub build-provenance attestations. With GitHub CLI, verify +an asset from the repository root: + +```sh +gh attestation verify FrameHome-installer.zip --repo Toastconcern/FrameStudio +gh attestation verify FrameStudio-linux-x64.tar.gz --repo Toastconcern/FrameStudio +``` + +The attestation identifies the repository, workflow, source ref and commit that produced the archive. It does not +replace runtime signature verification by the installer or updater. ## Repository layout diff --git a/docs/install-frame-home.md b/docs/install-frame-home.md index d8d0144..443255f 100644 --- a/docs/install-frame-home.md +++ b/docs/install-frame-home.md @@ -1,10 +1,10 @@ # Installing Frame Home -Frame Home is the part that runs on the Steam Frame. There are three ways to install it. All three do the same +Frame Home is the part that runs on the Steam Frame. There are four ways to install it. All four do the same thing on the headset: they copy the files and run the same installer (`install.sh`). -Before you start, the headset needs developer mode and SSH: see [headset-setup.md](headset-setup.md). The third way -(on the headset itself) needs no SSH. +Before you start, the headset needs developer mode and SSH for the computer-based methods: see +[headset-setup.md](headset-setup.md). The two methods on the headset itself need no SSH. ## Way 1: from Frame Studio @@ -23,10 +23,28 @@ Environments already on the headset with the same name and size are not copied a Studio connects without asking for passwords, so it needs an SSH key (see [headset-setup.md](headset-setup.md)). -## Way 2: with FrameHome-installer.zip, from any computer +## Way 2: Install script from GitHub on the Steam Frame -Download `FrameHome-installer.zip` from the Releases page (see [getting-started.md](getting-started.md#download)) -and unpack it. Inside are: +In Desktop Mode, open Konsole and run: + +```sh +curl -fsSL https://raw.githubusercontent.com/Toastconcern/FrameStudio/main/frame/install-frame-home-online.sh | bash +``` + +This downloads the installer from the shared **Latest build** release. To install a versioned release instead, set +`FRAME_HOME_TAG` to its tag, for example: + +```sh +curl -fsSL https://raw.githubusercontent.com/Toastconcern/FrameStudio/main/frame/install-frame-home-online.sh | FRAME_HOME_TAG=v1.0 bash +``` + +The bootstrap requires `curl` and `unzip`, verifies the downloaded ZIP, and runs the same `payload/install.sh` used +by the other install methods. Put `.fenv` files in `~/frame-home/envs`; the Environments panel lists them. + +## Way 3: with FrameHome-installer.zip, from any computer + +Download `FrameHome-installer.zip` from **Latest build** or a `v*` version release, then unpack it. Frame Home and +Studio share the same releases and tags, but this ZIP contains only Frame Home files. Inside are: | File | For | |---|---| @@ -78,7 +96,7 @@ sh install-frame-home.sh --host steamos@192.168.1.50 my-home.fenv another.fenv Copied environments get a headset-friendly name: lower case, spaces become dashes, and only letters, digits, `.`, `_` and `-` are kept. `My Home.fenv` becomes `my-home.fenv`. -## Way 3: directly on the headset +## Way 4: from a downloaded folder on the headset 1. Switch the headset to Desktop Mode. 2. Copy the unpacked `FrameHome-installer` folder to the headset (USB stick, network share, or a download in the diff --git a/frame/Install Frame Home.bat b/frame/Install Frame Home.bat old mode 100644 new mode 100755 diff --git a/frame/install-frame-home-online.sh b/frame/install-frame-home-online.sh new file mode 100755 index 0000000..0d1c946 --- /dev/null +++ b/frame/install-frame-home-online.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ "$(uname -m)" != aarch64 ]]; then + echo "Frame Home must be installed on the Steam Frame (ARM64)." >&2 + exit 1 +fi + +for tool in curl unzip; do + command -v "$tool" >/dev/null 2>&1 || { + echo "Missing required command: $tool" >&2 + exit 1 + } +done + +tag=${FRAME_HOME_TAG:-latest} +if [[ -z "$tag" || "$tag" == *[^a-zA-Z0-9._-]* ]]; then + echo "Invalid release tag: $tag" >&2 + exit 1 +fi + +url="https://github.com/Toastconcern/FrameStudio/releases/download/$tag/FrameHome-installer.zip" +tmp=$(mktemp -d "${TMPDIR:-/tmp}/frame-home-install.XXXXXXXX") +trap 'rm -rf "$tmp"' EXIT + +archive="$tmp/FrameHome-installer.zip" +echo "Downloading Frame Home release $tag..." +curl --fail --location --silent --show-error --retry 2 --output "$archive" "$url" +unzip -tq "$archive" +unzip -q "$archive" -d "$tmp" + +installer="$tmp/FrameHome-installer/payload" +if [[ ! -f "$installer/install.sh" || ! -x "$installer/bin/frame-home" ]]; then + echo "The downloaded release does not contain a valid Frame Home installer." >&2 + exit 1 +fi + +sh "$installer/install.sh" "$@" diff --git a/frame/install-frame-home.ps1 b/frame/install-frame-home.ps1 old mode 100644 new mode 100755 diff --git a/frame/payload/SOURCE b/frame/payload/SOURCE deleted file mode 100644 index a12ddf5..0000000 --- a/frame/payload/SOURCE +++ /dev/null @@ -1 +0,0 @@ -ed589d9db53c920be66e9334cc86e88e2893c2d410437d538477109cbf2fc02d diff --git a/frame/payload/bin/fenv-tool b/frame/payload/bin/fenv-tool deleted file mode 100755 index 515e2a2..0000000 Binary files a/frame/payload/bin/fenv-tool and /dev/null differ diff --git a/frame/payload/bin/frame-home b/frame/payload/bin/frame-home deleted file mode 100755 index c2a1b99..0000000 Binary files a/frame/payload/bin/frame-home and /dev/null differ diff --git a/frame/payload/bin/frame-switcher b/frame/payload/bin/frame-switcher deleted file mode 100755 index 4571621..0000000 Binary files a/frame/payload/bin/frame-switcher and /dev/null differ diff --git a/frame/payload/install.sh b/frame/payload/install.sh old mode 100644 new mode 100755 diff --git a/frame/payload/layer/env_ps.spv b/frame/payload/layer/env_ps.spv deleted file mode 100644 index d90ac2c..0000000 Binary files a/frame/payload/layer/env_ps.spv and /dev/null differ diff --git a/frame/payload/layer/libframe_home_layer.so b/frame/payload/layer/libframe_home_layer.so deleted file mode 100644 index 9f27e2d..0000000 Binary files a/frame/payload/layer/libframe_home_layer.so and /dev/null differ diff --git a/scripts/build-frame-home.sh b/scripts/build-frame-home.sh new file mode 100755 index 0000000..a7f9ee3 --- /dev/null +++ b/scripts/build-frame-home.sh @@ -0,0 +1,137 @@ +#!/bin/sh +# Build the Steam Frame payload and its standalone installer on an ARM64 glibc 2.39 runner. +set -eu +ROOT=$(cd "$(dirname "$0")/.." && pwd) +OUT=${FRAME_HOME_OUTPUT_DIR:-"$ROOT/build/frame-home"} +PAYLOAD="$OUT/payload" +MODE=${1:-${FRAME_HOME_BUILD_MODE:-local}} +case "$MODE" in + ci|--ci) + MODE=ci + BUILD=${FRAME_HOME_BUILD_DIR:-"$ROOT/build/frame-home-cmake-ci"} + CC=${CC:-gcc-13} + CXX=${CXX:-g++-13} + ;; + local|--local) + MODE=local + BUILD=${FRAME_HOME_BUILD_DIR:-"$ROOT/build/frame-home-cmake-local"} + CC=${CC:-gcc} + CXX=${CXX:-g++} + ;; + *) echo "Usage: $0 [--local|--ci]" >&2; exit 2 ;; +esac +export CC CXX + +[ "$(uname -m)" = aarch64 ] || { echo "Frame Home requires an ARM64 runner" >&2; exit 1; } +if [ "$MODE" = ci ] && [ "$(getconf GNU_LIBC_VERSION)" != "glibc 2.39" ]; then + echo "Release builds require glibc 2.39 (found: $(getconf GNU_LIBC_VERSION))" >&2 + exit 1 +fi +for tool in cmake ninja "$CC" "$CXX" glslc readelf zip python3; do + command -v "$tool" >/dev/null 2>&1 || { echo "Missing build tool: $tool" >&2; exit 1; } +done +if [ "$MODE" = ci ]; then + command -v ccache >/dev/null 2>&1 || { echo "Release builds require ccache" >&2; exit 1; } + "$CXX" --version | grep -q '13\.' || { echo "Release builds require GCC 13" >&2; exit 1; } +fi + +if [ "$MODE" = local ]; then + cmake -S "$ROOT" -B "$BUILD" -G Ninja -DCMAKE_BUILD_TYPE=Release -DFRAME_HOME_PC=OFF \ + "-DOPENVR_LIB_DIR=${OPENVR_LIB_DIR:-/opt/steamvr/bin/linuxarm64}" +else + cmake -S "$ROOT" -B "$BUILD" -G Ninja -DCMAKE_BUILD_TYPE=Release -DFRAME_HOME_PC=ON \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache +fi +cmake --build "$BUILD" --target frame-home frame-switcher fenv-tool --parallel + +rm -rf "$PAYLOAD" "$OUT/FrameHome-installer-stage" "$OUT/FrameHome-installer.zip" +mkdir -p "$PAYLOAD/bin" "$PAYLOAD/layer" +cp "$BUILD/frame-home" "$BUILD/frame-switcher" "$BUILD/fenv-tool" "$PAYLOAD/bin/" +compile_layer() { + if [ "$MODE" = ci ]; then ccache "$CXX" "$@"; else "$CXX" "$@"; fi +} +compile_layer -O2 -shared -fPIC -std=c++17 -fvisibility=hidden \ + -I"$ROOT/third_party/vulkan-headers/include" -I"$ROOT/src" \ + "$ROOT/layer/frame_home_layer.cpp" -o "$PAYLOAD/layer/libframe_home_layer.so" -lpthread +glslc "$ROOT/layer/env_ps.frag" -o "$PAYLOAD/layer/env_ps.spv" +cp "$ROOT/frame/payload/install.sh" "$PAYLOAD/install.sh" +chmod +x "$PAYLOAD/install.sh" "$PAYLOAD/bin/"* + +for binary in "$PAYLOAD/bin/"* "$PAYLOAD/layer/libframe_home_layer.so"; do + readelf -h "$binary" | grep -q 'Machine:.*AArch64' || { + echo "Not an AArch64 ELF: $binary" >&2; exit 1; + } + highest=$(readelf --version-info "$binary" | grep -oE 'GLIBC_[0-9]+(\.[0-9]+)+' | sort -Vu | tail -n 1 || true) + if [ "$MODE" = ci ] && [ -n "$highest" ] && [ "$(printf '%s\n' "$highest" GLIBC_2.39 | sort -V | tail -n 1)" != GLIBC_2.39 ]; then + echo "$binary requires $highest, newer than the glibc 2.39 baseline" >&2 + exit 1 + fi + # Release builds load only what every SteamOS has (OpenVR and Vulkan are found at run time, libdeflate is linked in). + if [ "$MODE" = ci ]; then + extra=$(readelf -d "$binary" | sed -n 's/.*(NEEDED).*\[\(.*\)\]/\1/p' | + grep -vxE 'libc\.so\.6|libm\.so\.6|libdl\.so\.2|libpthread\.so\.0|libstdc\+\+\.so\.6|libgcc_s\.so\.1|ld-linux-aarch64\.so\.1' || true) + if [ -n "$extra" ]; then + echo "$binary needs libraries a headset may not have: $(echo $extra)" >&2 + exit 1 + fi + fi +done + +{ + printf 'mode=%s\n' "$MODE" + printf 'architecture=%s\n' "$(uname -m)" + printf 'libc=%s\n' "$(getconf GNU_LIBC_VERSION)" + printf 'c_compiler=%s\n' "$("$CC" --version | head -n 1)" + printf 'cxx_compiler=%s\n' "$("$CXX" --version | head -n 1)" + printf 'cmake=%s\n' "$(cmake --version | head -n 1)" + printf 'ninja=%s\n' "$(ninja --version)" + printf 'glslc=%s\n' "$(glslc --version | head -n 1)" +} > "$OUT/build-info.txt" +for binary in "$PAYLOAD/bin/"* "$PAYLOAD/layer/libframe_home_layer.so"; do + printf '\n[%s]\n' "${binary#"$PAYLOAD/"}" >> "$OUT/build-info.txt" + readelf -d "$binary" | grep -E 'NEEDED|RPATH|RUNPATH' >> "$OUT/build-info.txt" || true + readelf --version-info "$binary" | grep -oE '(GLIBC|GLIBCXX|CXXABI)_[0-9]+(\.[0-9]+)+' | sort -Vu | tr '\n' ' ' >> "$OUT/build-info.txt" || true + printf '\n' >> "$OUT/build-info.txt" +done +cp "$OUT/build-info.txt" "$PAYLOAD/build-info.txt" + +STAGE="$OUT/FrameHome-installer-stage/FrameHome-installer" +mkdir -p "$STAGE" +cp -R "$PAYLOAD" "$STAGE/payload" +cp "$ROOT/LICENSE" "$STAGE/LICENSE.txt" +cp "$ROOT/frame/install-frame-home.sh" "$ROOT/frame/install-frame-home.ps1" \ + "$ROOT/frame/Install Frame Home.bat" "$ROOT/frame/README-frame-home.txt" "$STAGE/" +chmod +x "$STAGE/install-frame-home.sh" +(cd "$OUT/FrameHome-installer-stage" && zip -qr "$OUT/FrameHome-installer.zip" FrameHome-installer) +python3 - "$OUT/FrameHome-installer.zip" <<'PY' +import sys +import zipfile + +required = { + "FrameHome-installer/payload/install.sh", + "FrameHome-installer/payload/build-info.txt", + "FrameHome-installer/payload/bin/frame-home", + "FrameHome-installer/payload/bin/frame-switcher", + "FrameHome-installer/payload/bin/fenv-tool", + "FrameHome-installer/payload/layer/libframe_home_layer.so", + "FrameHome-installer/payload/layer/env_ps.spv", +} +allowed_files = { + "FrameHome-installer/LICENSE.txt", + "FrameHome-installer/Install Frame Home.bat", + "FrameHome-installer/install-frame-home.ps1", + "FrameHome-installer/install-frame-home.sh", + "FrameHome-installer/README-frame-home.txt", +} +with zipfile.ZipFile(sys.argv[1]) as archive: + names = { + info.filename.rstrip("/") + for info in archive.infolist() + if not info.is_dir() + } + missing = required - names + unexpected = names - required - allowed_files + if missing or unexpected: + raise SystemExit(f"Invalid Frame Home installer: missing={sorted(missing)}, unexpected={sorted(unexpected)}") +PY +echo "Built $OUT/payload (including build-info.txt), $OUT/FrameHome-installer.zip" diff --git a/scripts/build-studio.sh b/scripts/build-studio.sh index c30a970..13c1cb7 100755 --- a/scripts/build-studio.sh +++ b/scripts/build-studio.sh @@ -10,14 +10,13 @@ # (on Windows itself: studio\build_windows.bat x64|x86|arm64) # macos-arm64 macos-x64 on a Mac with Xcode's command line tools; MoltenVK is bundled into the # app from $MOLTENVK (a libMoltenVK.dylib) or Homebrew's molten-vk -# frame-home the Steam Frame installer on its own (no Studio needed): the headset -# files from frame/payload and the install scripts # all every target this computer can build -# Output: dist/FrameStudio-.zip (.tar.gz on Linux) and dist/FrameHome-installer.zip. +# Output: dist/FrameStudio-.zip (.tar.gz on Linux). set -eu ROOT=$(cd "$(dirname "$0")/.." && pwd) DIST="$ROOT/dist" BUILD="$ROOT/build" +FRAME_PAYLOAD_DIR=${FRAME_HOME_PAYLOAD_DIR:-} CMAKE=${CMAKE:-cmake} TARGETS="" while [ $# -gt 0 ]; do @@ -28,15 +27,33 @@ while [ $# -gt 0 ]; do esac done [ -n "$TARGETS" ] || { sed -n '2,19p' "$0"; exit 1; } +if [ -z "$FRAME_PAYLOAD_DIR" ]; then + for candidate in "$BUILD/frame-home/payload" "$ROOT/build/frame-home/payload"; do + if [ -d "$candidate" ]; then FRAME_PAYLOAD_DIR=$candidate; break; fi + done + [ -n "$FRAME_PAYLOAD_DIR" ] || FRAME_PAYLOAD_DIR="$ROOT/frame/payload" +fi mkdir -p "$DIST" "$BUILD" say() { echo "build-studio: $*"; } +say "using Frame Home payload: $FRAME_PAYLOAD_DIR" + +configure() { + source=$1; dir=$2; shift 2 + if [ "${FRAME_USE_CCACHE:-0}" = 1 ]; then + command -v ccache >/dev/null 2>&1 || { say "FRAME_USE_CCACHE is set but ccache is unavailable"; exit 1; } + "$CMAKE" -S "$source" -B "$dir" -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache "$@" + else + "$CMAKE" -S "$source" -B "$dir" -DCMAKE_BUILD_TYPE=Release "$@" + fi +} if [ "$TARGETS" = " all" ]; then case "$(uname -s)" in - Darwin) TARGETS="macos-arm64 macos-x64 frame-home" ;; + Darwin) TARGETS="macos-arm64 macos-x64" ;; *) case "$(uname -m)" in - aarch64|arm64) TARGETS="linux-arm64 frame-home" ;; - *) TARGETS="linux-x64 linux-x86 frame-home" ;; + aarch64|arm64) TARGETS="linux-arm64" ;; + *) TARGETS="linux-x64 linux-x86" ;; esac [ -x "${LLVM_MINGW:-$HOME/.local/opt/llvm-mingw}/bin/x86_64-w64-mingw32-clang" ] && TARGETS="$TARGETS windows-x64 windows-x86 windows-arm64" ;; esac @@ -47,7 +64,7 @@ build() { target=$1; shift dir="$BUILD/$target" say "building $target" - "$CMAKE" -S "$ROOT/studio" -B "$dir" -DCMAKE_BUILD_TYPE=Release "$@" > "$dir.configure.log" 2>&1 || { + configure "$ROOT/studio" "$dir" "-DFRAME_HOME_PAYLOAD_DIR=$FRAME_PAYLOAD_DIR" "$@" > "$dir.configure.log" 2>&1 || { tail -20 "$dir.configure.log"; say "$target: configure failed (full log: $dir.configure.log)"; exit 1; } "$CMAKE" --build "$dir" --target frame-studio --parallel } @@ -58,7 +75,7 @@ build_pc_home() { target=$1; shift dir="$BUILD/$target-home" say "building Frame Home for $target's SteamVR" - "$CMAKE" -S "$ROOT" -B "$dir" -DCMAKE_BUILD_TYPE=Release -DFRAME_HOME_PC=ON "$@" > "$dir.configure.log" 2>&1 || { + configure "$ROOT" "$dir" -DFRAME_HOME_PC=ON "$@" > "$dir.configure.log" 2>&1 || { tail -20 "$dir.configure.log"; say "$target: Frame Home configure failed (full log: $dir.configure.log)"; exit 1; } "$CMAKE" --build "$dir" --target frame-home frame-switcher --parallel } @@ -73,8 +90,8 @@ stage_pc() { cp "$home/frame-home" "$home/frame-switcher" "$stage/pc/bin/" ${CXX:-g++} -O2 -shared -fPIC -std=c++17 -fvisibility=hidden -I"$ROOT/third_party/vulkan-headers/include" -I"$ROOT/src" \ "$ROOT/layer/frame_home_layer.cpp" -o "$stage/pc/layer/libframe_home_layer.so" -lpthread - cp "$ROOT/frame/payload/layer/env_ps.spv" "$stage/pc/layer/" - cp "$ROOT/frame/payload/install.sh" "$stage/pc/" && chmod +x "$stage/pc/install.sh" "$stage"/pc/bin/* ;; + cp "$FRAME_PAYLOAD_DIR/layer/env_ps.spv" "$stage/pc/layer/" + cp "$FRAME_PAYLOAD_DIR/install.sh" "$stage/pc/" && chmod +x "$stage/pc/install.sh" "$stage"/pc/bin/* ;; esac } @@ -112,18 +129,6 @@ bundle_moltenvk() { fi } -# dist/FrameHome-installer.zip: Frame Home for the Steam Frame, installed without Frame Studio -package_frame_home() { - stage="$BUILD/stage/FrameHome-installer" - rm -rf "$stage" && mkdir -p "$stage" - cp -R "$ROOT/frame/payload" "$stage/payload" - cp "$ROOT/LICENSE" "$stage/LICENSE.txt" - cp "$ROOT/frame/install-frame-home.sh" "$ROOT/frame/install-frame-home.ps1" "$ROOT/frame/Install Frame Home.bat" "$ROOT/frame/README-frame-home.txt" "$stage/" - chmod +x "$stage/install-frame-home.sh" "$stage/payload/install.sh" "$stage"/payload/bin/* 2>/dev/null || true - (cd "$BUILD/stage" && rm -f "$DIST/FrameHome-installer.zip" && zip -qr "$DIST/FrameHome-installer.zip" FrameHome-installer) - say "frame-home -> dist/FrameHome-installer.zip" -} - for t in $TARGETS; do case "$t" in linux-x64) build "$t" -G Ninja && build_pc_home "$t" -G Ninja && package "$t" ;; @@ -135,7 +140,6 @@ for t in $TARGETS; do windows-arm64) build "$t" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/llvm-mingw.cmake" -DWINDOWS_ARCH=aarch64 && package "$t" ;; macos-arm64) build "$t" -G Ninja -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 && package "$t" ;; macos-x64) build "$t" -G Ninja -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 && package "$t" ;; - frame-home) package_frame_home ;; *) say "unknown target: $t (see --help)"; exit 1 ;; esac done diff --git a/scripts/frame-payload.sh b/scripts/frame-payload.sh deleted file mode 100644 index 4360a02..0000000 --- a/scripts/frame-payload.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/sh -# The Steam Frame programs in frame/payload are built on the headset (arm64 SteamOS, its compositor's Vulkan driver) -# and committed; every Frame Studio download and the Frame Home installer ship them. frame/payload/SOURCE records the -# source they were built from, so Studio is never released beside an older headset build (Studio would save -# environments the headset can't open). -# -# scripts/frame-payload.sh stamp after copying freshly built programs into frame/payload -# scripts/frame-payload.sh check fails when the headset's source changed since (CI runs it before releasing) -set -eu -ROOT=$(cd "$(dirname "$0")/.." && pwd) -STAMP="$ROOT/frame/payload/SOURCE" - -# What the headset programs are built from (studio/ is Studio's alone), as the files are now: committed or not. -source_hash() { - list=$(mktemp) - (cd "$ROOT" && git -c core.quotepath=off ls-files -- src third_party layer CMakeLists.txt > "$list" && - git hash-object --stdin-paths < "$list" | paste -d ' ' - "$list" | sha256sum | cut -c1-64) - rm -f "$list" -} - -case "${1:-}" in - stamp) source_hash > "$STAMP" - echo "frame/payload/SOURCE: $(cat "$STAMP")" ;; - check) want=$(source_hash) - have=$(cat "$STAMP" 2>/dev/null || true) - if [ "$want" != "$have" ]; then - echo "frame/payload is out of date: the headset's source changed since its programs were built." >&2 - echo "Rebuild frame-home, frame-switcher and fenv-tool on the Frame, copy them into frame/payload/bin," >&2 - echo "then run scripts/frame-payload.sh stamp (docs/building.md)." >&2 - exit 1 - fi - echo "frame/payload matches the source ($want)" ;; - *) sed -n '2,8p' "$0"; exit 2 ;; -esac diff --git a/src/openvr_loader.cpp b/src/openvr_loader.cpp index e084c65..4fb67eb 100644 --- a/src/openvr_loader.cpp +++ b/src/openvr_loader.cpp @@ -75,7 +75,11 @@ const char* kLibName = "openvr_api.dll"; using Lib = void*; Lib open(const fs::path& p) { return dlopen(p.c_str(), RTLD_NOW | RTLD_LOCAL); } void* symbol(Lib l, const char* name) { return dlsym(l, name); } +#if defined(__aarch64__) +const char* kLibInRuntime = "bin/linuxarm64/libopenvr_api.so"; +#else const char* kLibInRuntime = "bin/linux64/libopenvr_api.so"; +#endif const char* kLibName = "libopenvr_api.so"; #endif diff --git a/src/switcher.cpp b/src/switcher.cpp index a3e02a5..4d1cf06 100644 --- a/src/switcher.cpp +++ b/src/switcher.cpp @@ -708,8 +708,8 @@ DebugInfo readDebugInfo() { } // ── Updates (the Steam Frame only) ── -// Frame Home updates from GitHub. The release "latest" carries latest.json, whose "home" is a hash of the headset -// programs (frame-home, frame-switcher, fenv-tool and the compositor layer, worked out by CI from frame/payload). When +// Frame Home updates from GitHub. The shared "latest" release carries latest.json, whose "home" is a hash of the +// headset programs (frame-home, frame-switcher, fenv-tool and the compositor layer, worked out by CI). When // it isn't the hash of what's installed here, the header offers Update Frame Home: that downloads // FrameHome-installer.zip and runs its install.sh, which puts the new programs in place and restarts Frame Home and // this panel. Checked a minute after the panel starts, then every 6 hours (30 minutes after a failed check). diff --git a/studio/CMakeLists.txt b/studio/CMakeLists.txt index 2086eb5..d2d1255 100644 --- a/studio/CMakeLists.txt +++ b/studio/CMakeLists.txt @@ -18,6 +18,27 @@ add_compile_definitions(FRAME_BUILD="${FRAME_BUILD}") set(ROOT "${CMAKE_CURRENT_SOURCE_DIR}/..") set(TP "${ROOT}/third_party") +set(FRAME_HOME_PAYLOAD_DIR "$ENV{FRAME_HOME_PAYLOAD_DIR}" CACHE PATH "Built Frame Home payload to bundle with Studio") +if(NOT FRAME_HOME_PAYLOAD_DIR) + set(FRAME_HOME_PAYLOAD_DIR "${ROOT}/frame/payload") +endif() +# A release must carry it (CI: GitHub sets CI); a developer's build without one still builds, and its Install on Steam +# Frame says the headset files are missing. +option(FRAME_HOME_REQUIRE_PAYLOAD "Fail when the Frame Home payload is incomplete" "$ENV{CI}") +set(FRAME_HOME_PAYLOAD_COMPLETE ON) +foreach(FRAME_HOME_FILE install.sh bin/frame-home bin/frame-switcher bin/fenv-tool layer/libframe_home_layer.so layer/env_ps.spv) + if(NOT EXISTS "${FRAME_HOME_PAYLOAD_DIR}/${FRAME_HOME_FILE}") + set(FRAME_HOME_PAYLOAD_COMPLETE OFF) + set(FRAME_HOME_MISSING "${FRAME_HOME_FILE}") + endif() +endforeach() +if(NOT FRAME_HOME_PAYLOAD_COMPLETE) + set(FRAME_HOME_PAYLOAD_HELP "build or download it and set FRAME_HOME_PAYLOAD_DIR (${FRAME_HOME_MISSING} missing)") + if(FRAME_HOME_REQUIRE_PAYLOAD) + message(FATAL_ERROR "Frame Home payload is incomplete; ${FRAME_HOME_PAYLOAD_HELP}") + endif() + message(WARNING "Frame Home payload is incomplete, so this Studio can't install Frame Home; ${FRAME_HOME_PAYLOAD_HELP}") +endif() if(WIN32) add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN _CRT_SECURE_NO_WARNINGS VK_USE_PLATFORM_WIN32_KHR) @@ -157,8 +178,10 @@ if(APPLE) set_target_properties(frame-studio PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Frame Studio" MACOSX_BUNDLE_GUI_IDENTIFIER "com.framestudio.app") endif() # Frame Home's headset side (arm64 binaries + install.sh) ships beside the exe, in frame\: Frame > Install on Steam Frame. -add_custom_command(TARGET frame-studio POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory "${ROOT}/frame/payload" "$/frame") +if(FRAME_HOME_PAYLOAD_COMPLETE) + add_custom_command(TARGET frame-studio POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory "${FRAME_HOME_PAYLOAD_DIR}" "$/frame") +endif() if(MINGW) # MinGW builds (x64, x86 and ARM64): wWinMain entry point, no runtime DLLs to ship target_link_options(frame-studio PRIVATE -municode -static) endif() diff --git a/studio/build_windows.bat b/studio/build_windows.bat index b97b984..4407019 100644 --- a/studio/build_windows.bat +++ b/studio/build_windows.bat @@ -35,6 +35,8 @@ call "%VSPATH%\VC\Auxiliary\Build\vcvarsall.bat" %VCARCH% >nul || ( echo ERROR: where cmake >nul 2>nul || set "PATH=%VSPATH%\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin;%PATH%" where ninja >nul 2>nul || set "PATH=%VSPATH%\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja;%PATH%" -cmake -S "%SRC%." -B "%BDIR%" -G Ninja -DCMAKE_BUILD_TYPE=Release %CROSS% || exit /b 1 +set "PAYLOAD_ARG=" +if defined FRAME_HOME_PAYLOAD_DIR set PAYLOAD_ARG="-DFRAME_HOME_PAYLOAD_DIR=%FRAME_HOME_PAYLOAD_DIR%" +cmake -S "%SRC%." -B "%BDIR%" -G Ninja -DCMAKE_BUILD_TYPE=Release %CROSS% %PAYLOAD_ARG% || exit /b 1 cmake --build "%BDIR%" --target frame-studio || exit /b 1 echo Built "%BDIR%\Frame Studio.exe" (%ARCH%)