diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8b805c..f4aba03 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -104,6 +104,8 @@ jobs: timeout-minutes: 30 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 @@ -134,6 +136,10 @@ jobs: - uses: actions/checkout@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 # 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. diff --git a/docs/building.md b/docs/building.md index 2ddfe0c..3798b0a 100644 --- a/docs/building.md +++ b/docs/building.md @@ -173,8 +173,17 @@ headset, copy the results into it: `frame/payload/install.sh` is the installer itself; it lives in the repository and needs no build. -Commit the new payload together with the code that changed. Otherwise Studio's **Install on Steam Frame** would -install an older Frame Home than the code says. +Then record which source they were built from, and commit the new payload together with the code that changed: + +```sh +sh scripts/frame-payload.sh stamp +``` + +`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. ## Repository layout diff --git a/frame/payload/SOURCE b/frame/payload/SOURCE new file mode 100644 index 0000000..a12ddf5 --- /dev/null +++ b/frame/payload/SOURCE @@ -0,0 +1 @@ +ed589d9db53c920be66e9334cc86e88e2893c2d410437d538477109cbf2fc02d diff --git a/frame/payload/bin/fenv-tool b/frame/payload/bin/fenv-tool index c40fac8..515e2a2 100755 Binary files a/frame/payload/bin/fenv-tool and b/frame/payload/bin/fenv-tool differ diff --git a/frame/payload/bin/frame-home b/frame/payload/bin/frame-home index fe90ebd..c2a1b99 100755 Binary files a/frame/payload/bin/frame-home and b/frame/payload/bin/frame-home differ diff --git a/scripts/frame-payload.sh b/scripts/frame-payload.sh new file mode 100644 index 0000000..4360a02 --- /dev/null +++ b/scripts/frame-payload.sh @@ -0,0 +1,34 @@ +#!/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/fenv_tool.cpp b/src/fenv_tool.cpp index 532e785..38d56ee 100644 --- a/src/fenv_tool.cpp +++ b/src/fenv_tool.cpp @@ -435,6 +435,8 @@ int tune(const std::string& home, const std::string& benchArg, int seconds, doub } // namespace int main(int argc, char** argv) { + // The newest baked scene this build opens: Frame Studio asks before sending (an older headset can't open a newer one). + if (argc == 2 && !std::strcmp(argv[1], "version")) { std::printf("scene %u\n", kSceneBakeVersion); return 0; } if (argc >= 3 && !std::strcmp(argv[1], "info")) return info(argv[2]); if (argc >= 4 && !std::strcmp(argv[1], "bundle")) { std::string name; @@ -529,6 +531,7 @@ int main(int argc, char** argv) { " fenv-tool upgrade file.fenv... re-bake with this build (keeps world, edits, sound, picture)\n" " fenv-tool report [--json] ... what it costs and what's wrong with it\n" " fenv-tool settings [JSON | --clear] a world's own settings, shown or set\n" - " fenv-tool tune [--seconds 30] [--late 0.01] [--cool 80] [--dry-run] its best settings on this headset, stored in it\n"); + " fenv-tool tune [--seconds 30] [--late 0.01] [--cool 80] [--dry-run] its best settings on this headset, stored in it\n" + " fenv-tool version the newest baked scene this build opens\n"); return 2; } diff --git a/studio/studio.cpp b/studio/studio.cpp index c8d1993..6decac5 100644 --- a/studio/studio.cpp +++ b/studio/studio.cpp @@ -2234,6 +2234,7 @@ struct Studio { setTuneStatus("Copying it to the headset\u2026"); int rc = run(ssh + L"\"mkdir -p /tmp/frame-tune && rm -f /tmp/frame-tune/log\"", L"", out); if (rc != 0) { setTuneStatus("Can't reach the headset (" + host8 + "): " + lastLine(out)); tuning = false; return; } + if (const std::string why = headsetCantOpen(host8); !why.empty()) { fs::remove(f, ec); setTuneStatus("Not measured. " + why); tuning = false; return; } out.clear(); rc = run(scp + L"\"" + f.filename().wstring() + L"\" " + widen(host8) + L":/tmp/frame-tune/tune.fenv", f.parent_path().wstring(), out); fs::remove(f, ec); @@ -2382,6 +2383,20 @@ struct Studio { return png; } + // Why the headset's Frame Home can't open what this Studio saves, or "" when it can: its fenv-tool names the newest + // baked scene it opens ("scene N"; a build from before that question prints its usage instead). + std::string headsetCantOpen(const std::string& host8) { + std::string out; + const int rc = run(sshTool(L"ssh") + L"-o BatchMode=yes -o ConnectTimeout=8 " + widen(host8) + L" \"frame-home/bin/fenv-tool version\"", L"", out); + if (rc == 127 || out.find("not found") != std::string::npos) + return "Frame Home isn't installed on the headset: Headset > Install on Steam Frame first."; + const size_t at = out.find("scene "); + const long reads = at == std::string::npos ? 0 : std::strtol(out.c_str() + at + 6, nullptr, 10); + if (reads >= static_cast(kSceneBakeVersion)) return {}; + return "The headset's Frame Home is older than this Frame Studio and can't open what it saves: " + "Headset > Install on Steam Frame updates it."; + } + // A footprint with its vista goes to the Frame as one home in one file: the pair baked into a .fenv with the // edits, world layer and background sound (what Save Environment writes). The pair's separate copies an earlier // Send left there go, so the headset lists it once. @@ -2411,6 +2426,7 @@ struct Studio { setStatus("Connecting to the Frame\u2026"); int rc = run(ssh + host + L" \"mkdir -p " + dir + L"\"", L"", out); if (rc != 0) { setStatus("Can't reach the Frame: " + lastLine(out)); sending = false; return; } + if (const std::string why = headsetCantOpen(host8); !why.empty()) { setStatus("Not sent. " + why); sending = false; return; } std::error_code ec2; char msg[200]; std::snprintf(msg, sizeof msg, "Copying %s (%.0f MB)\u2026", name.c_str(), fs::file_size(f, ec2) / 1048576.0);