mirror of
https://github.com/Toastconcern/FrameStudio.git
synced 2026-10-28 21:02:20 +01:00
Frame Studio - Gaussian splats (PLY, SPZ v2-v4), PLY vertex colours, panorama skies - Copy, cut, paste and duplicate for objects, splats and world items - Walkable-area rebake moved off the UI thread; viewport one frame sooner; refresh-locked pacing (FS_PACE=timer for the old one) - Fixed: bundle thumbnail use-after-free crash, auto-save deleting environments, saves into scratch copies, edits lost on non-ASCII paths, unopenable manifests with inf/nan, bundle save data loss, opening a home marking it changed, gizmo NaN, long paths, many malformed-file crashes - Updater verifies sha256, journals the swap and never leaves the exe missing Frame Home - Seats turn you to face their way - Draw batching (Hand Tutorials up to ~20% less GPU time), cut-out EQUAL pass, late head-pose read before submit - Proper queue-family hand-over to the compositor layer; layer descriptor generations and seqlocked slots; structural SPIR-V check - Fixed: hangs/crashes on damaged .fenv files and shaders, render-pass validation error, sync hazard in the foveation smoothing pass, unverified update downloads, half-done installs Tests: tests_loco/env_share/spv_check wired into CMake (FRAME_TESTS) and CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsaXGU27gsrXjDoFtyFJP9
386 lines
18 KiB
YAML
386 lines
18 KiB
YAML
# 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 }}
|
|
cancel-in-progress: true
|
|
|
|
# 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:
|
|
include:
|
|
- { arch: x64, runner: ubuntu-24.04, extra: "" }
|
|
- { 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:
|
|
- 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:
|
|
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: |
|
|
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 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 }}
|
|
- 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: build/stage/FrameStudio-linux-${{ matrix.arch }}
|
|
|
|
windows:
|
|
name: Windows ${{ matrix.arch }}
|
|
needs: [frame-home]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
arch: [x64, arm64]
|
|
runs-on: windows-latest
|
|
timeout-minutes: 45
|
|
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:
|
|
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 }}"
|
|
- name: Package
|
|
shell: pwsh
|
|
run: |
|
|
$name = "FrameStudio-windows-${{ matrix.arch }}"
|
|
$stage = "build/stage/$name"
|
|
New-Item -ItemType Directory -Force $stage | Out-Null
|
|
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"
|
|
- name: Upload Windows build
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: FrameStudio-windows-${{ matrix.arch }}
|
|
path: build/stage/FrameStudio-windows-${{ matrix.arch }}
|
|
|
|
macos:
|
|
name: macOS ${{ matrix.arch }}
|
|
needs: [frame-home]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- { arch: arm64, runner: macos-14 }
|
|
runs-on: ${{ matrix.runner }}
|
|
timeout-minutes: 45
|
|
env:
|
|
FRAME_USE_CCACHE: "1"
|
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
|
CCACHE_MAXSIZE: 500M
|
|
CCACHE_COMPILERCHECK: content
|
|
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:
|
|
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 ccache ninja molten-vk
|
|
- name: Build
|
|
run: sh scripts/build-studio.sh macos-${{ matrix.arch }}
|
|
- 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: build/stage/FrameStudio-macos-${{ matrix.arch }}
|
|
|
|
frame-home:
|
|
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:
|
|
- 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
|
|
# (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
|
|
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: 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
|
|
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: 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
|
|
# needs them all. We don't wait on macOS: its runners can sit in GitHub's queue for hours, and since a push
|
|
# cancels the previous run, waiting meant no run ever reached this job. Both Mac builds still upload and are
|
|
# attached whenever they finish in time.
|
|
if: ${{ !cancelled() && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') }}
|
|
needs: [linux, windows, frame-home]
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 20
|
|
permissions:
|
|
contents: write
|
|
id-token: write
|
|
attestations: write
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
steps:
|
|
# 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
|
|
with:
|
|
path: src
|
|
fetch-depth: 0
|
|
- name: Version
|
|
if: github.ref == 'refs/heads/main'
|
|
run: |
|
|
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
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: FrameStudio-*
|
|
path: dist
|
|
merge-multiple: false
|
|
- name: Download Frame Home payload
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
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: |
|
|
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()]))')
|
|
# Each file's SHA-256 as "sha256-<file>": Frame Studio checks its download against it before installing.
|
|
hashes=$(cd dist && for f in *; do if [ -f "$f" ] && [ "$f" != latest.json ]; then printf ', "sha256-%s": "%s"' "$f" "$(sha256sum "$f" | cut -c1-64)"; fi; done)
|
|
printf '{"build": "%s", "home": "%s", "date": "%s", "files": %s%s}\n' "$GITHUB_SHA" "$home" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$files" "$hashes" > dist/latest.json
|
|
cat dist/latest.json
|
|
- name: Attest Studio release archives
|
|
uses: actions/attest-build-provenance@v2
|
|
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: |
|
|
# 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
|
|
# 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" --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: |
|
|
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"
|