Headset programs rebuilt from the current source; releases can't ship them stale

The committed frame/payload still had Frame Home from Oct 21, which opens baked
scenes up to v13, while Studio has saved v14 since wall spots were removed: every
environment saved and sent from a release failed on the headset with "baked scene
from a different Frame Studio version".

- frame/payload: frame-home and fenv-tool rebuilt on the Frame from this source
  (byte-identical to what is installed there now).
- frame/payload/SOURCE + scripts/frame-payload.sh: records the source the headset
  programs were built from; CI checks it in the installer job and in the release
  job, so nothing is published beside an outdated headset build.
- fenv-tool version prints the newest baked scene it opens; Studio asks before
  Send to Frame and Calculate Best Settings and says to update Frame Home instead
  of sending a file the headset can't open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ToastconcernandClaude Opus 5.5 committed 2026-10-23 13:27:35 -05:00
1 parent fedadd5ae3
commit 5e2b42d487
8 files changed
+72 -3

No files matched your search

+6
View File
@@ -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.
+11 -2
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
ed589d9db53c920be66e9334cc86e88e2893c2d410437d538477109cbf2fc02d
Binary file not shown.
Binary file not shown.
+34
View File
@@ -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
+4 -1
View File
@@ -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] <home.apk | home.fenv | footprint.apk+vista.apk>... what it costs and what's wrong with it\n"
" fenv-tool settings <home.fenv | home.apk> [JSON | --clear] a world's own settings, shown or set\n"
" fenv-tool tune [--seconds 30] [--late 0.01] [--cool 80] [--dry-run] <home.fenv | home.apk> its best settings on this headset, stored in it\n");
" fenv-tool tune [--seconds 30] [--late 0.01] [--cool 80] [--dry-run] <home.fenv | home.apk> its best settings on this headset, stored in it\n"
" fenv-tool version the newest baked scene this build opens\n");
return 2;
}
+16
View File
@@ -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<long>(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);