Compare commits

...
Author SHA1 Message Date
spoopyghosty0 423b01ee98 Site specimens: steps.html comparison page (two steps, one shot; side by side or swipe) 2026-10-12 11:44:51 -04:00
spoopyghosty0 93b62fa185 Site: polish. The walkthrough's Downloads scene shows each platform's real file and program (was '?os=linux' and FramePort.exe everywhere); site dim text #868a96 (4.77:1+) and themed placeholders; pending install-log stages stay readable; no labels under 11px; docs blockquote without a side bar; changelog version without gradient text; notice at 64ch. Closes the 2026-10-12 critique snapshot 2026-10-12 11:38:52 -04:00
spoopyghosty0 fc47dab20b Site: onboard. Get started step 3 says what the setup line does (finds FramePort on your PC and waits; nothing changes on the Frame until you click Allow; then Developer Mode turns on; no password, no root) with a link to read the script. Specimens: walkthrough step 3 shot 2026-10-12 11:24:36 -04:00
spoopyghosty0 58a936c706 Site: adapt. Below 1120px the nav links move into a Menu panel (48px rows; Esc, outside click or a link closes it) instead of wrapping at 1024 or vanishing on phones; on phones the download buttons say 'Get it on your PC'; no three.js on data saver or 2G/3G; the install log drops its detail column under 600px. Specimens: phone user agent, menu shot 2026-10-12 11:15:54 -04:00
spoopyghosty0 72f94d1754 Site: distill + layout. App tour screenshots and videos behind one disclosure, the game-maker block is one row (snippet in the docs), What's new is one line, Compare without icon tiles, shared .disclose style; calmer section padding, labels closer to their headings, the notice sits between sections at 72ch, bigger hit areas on the hero's platform links (home 10.8k -> 8.4k px) 2026-10-12 11:08:15 -04:00
spoopyghosty0 af6117ad5d Site: clarify. Section labels name what you do there instead of 01-07; plain headlines (What happens when you click Install on Frame); hero eyebrow removed; nav Install -> Get started (no clash with the /install/ link page); /download/ says why Windows and macOS warn and where the SHA-256 sums are 2026-10-12 10:54:44 -04:00
spoopyghosty0 540c71982f Site: quieter. Removed the cursor light, click ripples, word-by-word headings and gradient letters, label scramble, count-ups, scroll fade-ups, the hero's scroll fly-away, film grain, icon hover tricks, the download page's warp field and breathing glow, Compare's tilt and pointer light, the contributors' dock magnification; springs land without overshoot (--ease-settle). Motion stays where something crosses. 2026-10-12 10:47:48 -04:00
spoopyghosty0 daa71a144e Site specimens: capture script, gallery and the baseline (00-before) 2026-10-12 10:35:19 -04:00
spoopyghosty0 a64a4c6958 Critique snapshot for frameport.app (28/40): effect stack and section scaffolding read as generated, no phone nav, long home page 2026-10-12 10:27:48 -04:00
spoopyghosty0 c0b3ba960a DESIGN.md + .impeccable/design.json: the Portal design system as built (tokens, rules, components) for design work on the app and the site 2026-10-12 09:54:55 -04:00
spoopyghosty0 aaf6aba621 PRODUCT.md: product record for design work (users, purpose, positioning, brand commitments, principles) 2026-10-12 09:48:47 -04:00
spoopyghosty0 80ef9ca6a7 Site: release and contributor data refreshed (1.0.4) 2026-10-11 21:30:25 -04:00
spoopyghosty0 c744f8740b Site: the games board lists games in a random order on every visit (working games first, then with issues, then not running) 2026-10-11 21:30:13 -04:00
spoopyghosty0 859f921361 Monitor: games Steam runs that FramePort didn't install count (e.g. Job Simulator from Steam's store, named from its appmanifest; End game = Steam's Exit game), overlay apps are never the game being played, and the newest game comes first (agent v81) 2026-10-11 17:27:10 -04:00
spoopyghosty0 6acf9d048e App icon: all four corners rounded (only the top-left one was); source runs on Windows show FramePort's icon instead of Flet's (window.icon from an .ico made of it) 2026-10-11 14:40:32 -04:00
spoopyghosty0 cf18b30be9 Uninstall dialog: the delete-saves checkbox label wraps (it ran past the dialog; also the delete-files-on-this-PC, share-recipe and remove-FramePort checkboxes), games uninstalled from the Frame page show their name; Monitor: the agent's own workers aren't a running game (uninstalling showed the game as now playing while Steam restarted; agent v80) 2026-10-11 14:33:47 -04:00
spoopyghosty0 eec0218b1d FramePort 1.0.4 2026-10-11 14:13:56 -04:00
spoopyghosty0 526ba0a684 Launch tests: a game that ran until the session ended for lack of a worn headset (VrApi bridge tracking deadline) passes, even when it crashes afterwards (BlazeRush 'Failed to launch' on install, GitHub #57) 2026-10-11 14:12:04 -04:00
spoopyghosty0 23b6ff1b5e Dev: WSL source runs can share the Windows copy's data folder (FRAMEPORT_WINDOWS_DATA=1): paths saved by the other side are translated on load, Java and Cpp2IL keep one copy per OS, and such runs leave the link handler alone 2026-10-11 13:59:37 -04:00
spoopyghosty0 00c6863e54 FramePort 1.0.3 2026-10-11 13:31:33 -04:00
spoopyghosty0 4863035a05 catalog: Robo Recall gets sync_guard (vrclient xrSyncActions crash on the splash after the intro, GitHub #188) 2026-10-11 13:31:20 -04:00
spoopyghosty0 a30162ea63 Fixes from reports: update dialog links open again (#194), a relaunched update no longer inherits the old app's Flet/Python environment (black window, #195), report form says to attach the zip in a comment if the box won't take it (#196), Job Simulator and Vacation Simulator without Lepton's Android window (frozen hands, #193/#163) 2026-10-11 13:27:18 -04:00
spoopyghosty0 24eaa97fde GUI: one window per data folder (a second start brings it to the front, a windowless leftover is ended), the process ends with its window, and checked play sessions get one pop-up instead of one per game 2026-10-11 13:20:31 -04:00
spoopyghosty0 767b152423 Merge catalog recipe from #183 2026-10-11 12:29:42 -04:00
github-actions[bot] 10e7669f70 catalog: com.drawandcode.TaskmasterVR (from #183) 2026-10-11 16:29:18 +00:00
spoopyghosty0 76c4f2d225 FramePort 1.0.2 2026-10-11 12:28:58 -04:00
spoopyghosty0 3a851e911f frame.ovr_stable_user: games keep the same Meta user id between starts (OVRPort made up a new one each time, so saves filed under it were lost; GitHub #182) 2026-10-11 12:22:02 -04:00
spoopyghosty0 2912d637b5 FAQ: where a game's settings are and how to diagnose and fix games that don't start, hang, look wrong, stutter, pause, lose progress or need your files; site data refreshed for 1.0.1 2026-10-11 10:48:56 -04:00
spoopyghosty0 5c070ccf23 Merge catalog recipe from #181
# Conflicts:
#	docs/PATCHES.md
2026-10-11 10:11:56 -04:00
spoopyghosty0 b979067215 Merge catalog recipe from #176
# Conflicts:
#	docs/PATCHES.md
2026-10-11 10:11:41 -04:00
spoopyghosty0 4440b459e5 Merge catalog recipe from #175
# Conflicts:
#	docs/PATCHES.md
2026-10-11 10:11:31 -04:00
spoopyghosty0 09b8046337 Merge catalog recipe from #174 2026-10-11 10:11:21 -04:00
spoopyghosty0 0d67335775 catalog: A Fisherman's Tale works with issues (loading screen corruption) 2026-10-11 10:11:09 -04:00
github-actions[bot] 867b9977d5 catalog: com.SchellGames.DieScreaming (from #176) 2026-10-11 14:10:09 +00:00
github-actions[bot] 80092098e0 catalog: com.schellgames.ieytd3 (from #181) 2026-10-11 14:10:06 +00:00
github-actions[bot] feab03e049 catalog: com.innerspacevr.afishermanstale (from #175) 2026-10-11 14:10:06 +00:00
github-actions[bot] 9713da222a catalog: com.WhiteDoorGames.Cosmodread (from #174) 2026-10-11 14:10:05 +00:00
spoopyghosty0 82e617741e FramePort 1.0.1 2026-10-11 09:59:28 -04:00
spoopyghosty0 9de0707070 Diagnostics: a zip saved while a Quest game runs includes threads.txt (each thread's wait, syscall and CPU use, TCP sockets) to find what a hanging game waits for (agent v79, GitHub #157) 2026-10-11 09:58:09 -04:00
spoopyghosty0 1343930ae0 Triage: Android's WebView helper exit is no force-quit; new app-not-started signature for Lepton's "Target app did not start" (GitHub #179) 2026-10-11 09:44:54 -04:00
spoopyghosty0 5424e429e4 Site: the patch reference in the docs; recipe chips (How it works, games board) and patch ids in docs code spans link to their entry 2026-10-11 00:58:52 -04:00
spoopyghosty0 9149eaecd7 Patch reference: scripts/patch_docs.py generates docs/PATCHES.md (every patch and game setting, anchors per id) and the site's id map; --check test, catalog PRs regenerate it 2026-10-11 00:58:51 -04:00
spoopyghosty0 64278e9e2c README and site: recipes for 120+ games (the catalog has 126) 2026-10-11 00:54:41 -04:00
spoopyghosty0 1531acb575 Site: the Built on section only on the homepage, What's new shows only the contributors
Footer's credits defaulted to on, so privacy and 404 showed it. credits is now 'all' (homepage), 'contributors'
(What's new) or off (default).
2026-10-11 00:51:58 -04:00
spoopyghosty0 6796e006d9 Site compare: the open-source card stacks on phones
The title's grid-column 2 / -1 from the wide layout added an implicit second column in the one-column layout,
squeezing FramePort's text to one word per line beside the other tool's.
2026-10-11 00:51:56 -04:00
spoopyghosty0 45f05dd3c6 Site demo: the Library header no longer squeezes its title on narrow screens
The search and buttons sat in an auto column that took the width and left "Libr" and one word per line for the
title. Narrow (icon rail): title, full-width search, buttons and Add games each wrap below. Tablet widths: the title
keeps its own width.
2026-10-11 00:51:54 -04:00
spoopyghosty0 5a2cd45fb8 Site: the hero scene stays sharp while it's in view on phones; no click ripples on touch
Stacked (<= 900 px) the scene sits below the hero text, so --sp measured from the page's scroll had it blurred and
enlarged by the time it reached the middle of the screen. There it now follows its own position (0 until its middle
passes 40 % of the screen height). Wide layouts keep the old mapping. The empty-space click rings and the install
badge's press ripple skip touch pointers.
2026-10-11 00:51:52 -04:00
spoopyghosty0 757671b389 Merge pull request #172 from spoopyghosty0/showcase/update
Update docs screenshots and videos
2026-10-10 23:52:39 -04:00
spoopyghosty0 8a2029c079 Docs: update screenshots and videos 2026-10-11 03:38:33 +00:00
spoopyghosty0 b2688dd368 Catalog: BlazeRush verified in the headset with FramePort's fixes (owner, 2026-10-11) 2026-10-10 23:33:55 -04:00
spoopyghosty0 13de114f39 test_showcase: the fake setup server check looks at every PairingServer method the GUI touches (it missed open_asks) 2026-10-10 23:26:11 -04:00
spoopyghosty0 9016982394 Showcase: the pretend setup server has open_asks/prune like the real one (the install tutorial failed to record); test that it offers everything the GUI uses 2026-10-10 23:25:14 -04:00
spoopyghosty0 cad2cd366c BlazeRush: min_app 1.0.0 (ships with 1.0) 2026-10-10 23:24:03 -04:00
spoopyghosty0 a5820e68d7 BlazeRush works (GitHub #57, from Klownicle's guide; headset check pending): frame.blazerush (1.0.349 only, fingerprint + words checked: avatar wait NOP, avatar renderer RET, shader cache cache/br_shader_v1), device.config_sync (agent v78: saved resolution in user_config.xml from the runtime eye size x scale at finalize, settings changes and before each start), catalog recipe with the GL shim fixes and scale 1.7, triage vrapi-fov-before-tracking / gl-integer-attribs 2026-10-10 23:18:42 -04:00
spoopyghosty0 aff939a6d8 BlazeRush native fixes (GitHub #57, Klownicle's guide): GL shim gl_int_attribs (integer vertex inputs fed as floats re-set with glVertexAttribIPointer per draw, per-context reflection dropped on link/binary/delete) and gl_highp_markers (matching vertex shaders compute in highp); VrApi bridge answers FOV properties 7/8 before any session with the live FOV saved by an earlier session (also located before tracking); FrameBridge logs the runtime's unscaled eye size 2026-10-10 23:18:41 -04:00
spoopyghosty0 5e725fbd08 Issue chooser: links to Discussions (questions, ideas) and the docs 2026-10-10 23:16:30 -04:00
spoopyghosty0 1cb9e6f65b docs/brand: GitHub social preview image (1280 x 640) 2026-10-10 23:11:13 -04:00
spoopyghosty0 7a843962e8 Catalog: Time Crisis II VR (test build) works (shared working config)
Closes #173
2026-10-10 23:09:47 -04:00
spoopyghosty0 51220a4942 docs/brand: square FramePort icon (1024 px PNG + SVG) for project pages and listings 2026-10-10 23:00:24 -04:00
spoopyghosty0 981daa3c2d Unreal: start without the OBB check (frame.unreal_skip_obb_check, GitHub #159)
Epic's DownloaderActivity checks the OBB's name and size and, with the manifest's
bVerifyOBBOnStartUp=true, CRC-checks the whole file; its screens are invisible in Lepton,
so games like Contractors seem to hang at 'Displayed .../.DownloaderActivity'. The patch
flips that manifest boolean (axml.set_meta_data_bool, one 4-byte value); analysis
unreal_verify_obb (ANALYSIS_VERSION 11) suggests it. Triage unreal-obb-check-stuck (fatal)
fails such launch tests; triage keeps system_server's lines about the game's own activities.
2026-10-10 22:49:21 -04:00
spoopyghosty0 9781c2bb6b Showcase: sharper README teaser (1280 px, 20 fps, WebP drawing preset from quality 95, up to 8 MB) and tour/install videos (CRF from 17, up to 20 MB); the teaser showed compression artifacts 2026-10-10 22:32:47 -04:00
spoopyghosty0 a7c349835c Install looks for a game's data files again when it was added without them (OBBs copied next to the APK afterwards; GitHub #156) 2026-10-10 22:29:52 -04:00
spoopyghosty0 9a48eac3e6 Catalog: TRIANGLE STRATEGY works (shared working config)
Closes #171
Closes #85
2026-10-10 22:18:23 -04:00
spoopyghosty0 9082c684ed test_vr_overlay: expect systemd quotes only where the path needs them (CI's checkout path has no spaces; the v1.0.0 build failed on it) 2026-10-10 22:17:22 -04:00
spoopyghosty0 84235fbc9f FramePort 1.0.0 2026-10-10 22:04:58 -04:00
spoopyghosty0 8accd6221a Install links: the homepage's example button is a demo link (frameport.app/demo/cool-game.json, published on the site). FramePort recognises every form of it without any network request and answers with a demo install question for "Cool Game"; Install tosses its cover out of the logo's portal (easter eggs off: "Nice, it works!"), nothing is downloaded or added. The landing page marks it as a demo link; open-link says it's the demo and stops 2026-10-10 21:47:57 -04:00
spoopyghosty0 c37eebb59f Site: link the rift-on-frame branch from How it works 2026-10-10 21:47:04 -04:00
spoopyghosty0 5b0bd2589b PC Steam shortcuts: Revive games (all started by ReviveInjector.exe) are told apart by the game in the launch options; a second Revive game overwrote the first one's shortcut 2026-10-10 21:44:47 -04:00
spoopyghosty0 231e5e50cd Merge branch 'worktree-agent-ad6131f295772bee1' 2026-10-10 21:39:29 -04:00
spoopyghosty0 423b2e1c3e Rename… for every library entry: the name is kept from then on and reaches Steam without a new shortcut (agent v77 rename)
Game menu (Library right-click, game page "Name and artwork") and CLI 'frameport rename <pkg> "<name>"|--reset'.
The title is locked (title_locked + auto_title for the reset); rescans, Rift store matches and install links only
update auto_title; a chosen name gets no Quest/Rift twin suffix. Installed copies: the Frame's deployment.json title
changes and the shortcut is rewritten with the same appid (matched by Exe), one Steam restart; PC VR installs on this
PC keep their appid too (reinstalls and library updates now record the shortcut's real appid). A Frame that isn't
connected shows "Update name on Frame" on the game page until then or the next install.
2026-10-10 21:38:56 -04:00
spoopyghosty0 b6c5f7c18e Scanning: SideQuest backup folders and AXRB downloads import with their OBBs. AXRB's own PC-patched copies (patched/, *-axrb.apk, no OBBs) are skipped and a rescan replaces games added from them (they started and stopped at once: Steam showed Resume); AXRB's other store files go with the OBBs; an APK inside another game's data folder isn't a game; backups are named after the package, not the backup time. FAQ explains both layouts 2026-10-10 21:29:22 -04:00
spoopyghosty0 74320e607f Site: How it works explains PC VR games on the Frame and on the PC (automatic Revive for Rift games), plus two experimental paths (Quest games in PC VR, Rift games on the Frame alone), closed by default. Install video: download from frameport.app 2026-10-10 21:13:29 -04:00
spoopyghosty0 d87e3b3b5f Merge branch 'worktree-agent-ad5dca676e297ca5d' 2026-10-10 20:59:46 -04:00
spoopyghosty0 c4153be8bc Docs: FramePort's overlay autostart service (agent v76), dev Frame result 2026-10-10 20:58:57 -04:00
spoopyghosty0 b5a16a2680 Overlay apps start with SteamVR through FramePort's own service (agent v76)
SteamVR's auto-launch flag (SetApplicationAutoLaunch) didn't persist on the dev
Frame and a registered watch didn't start by itself. The deployment's overlay
autostart flag is now the source of truth: ensure_host_fixes installs the user
service frameport-vr-overlays.service while an installed overlay app has
autostart on (removed when none has, on uninstall/purge, or with the kill switch
~/.local/share/frameport/vr-overlays.disabled / FRAMEPORT_NO_OVERLAY_AUTOSTART).
It polls every 5 s for a new vrserver (pid + start time, remembered across agent
updates), waits until SteamVR answers, gives SteamVR's own auto-launch 15 s and
starts each autostart overlay app that isn't running (process of its folder or
GetApplicationProcessId) through LaunchApplication, in a child process with a
timeout. Log: ~/.local/share/frameport/vr-overlays.log (capped).
2026-10-10 20:53:32 -04:00
spoopyghosty0 f6870da310 Merge branch 'worktree-agent-a541d9c7207d19375' 2026-10-10 20:52:50 -04:00
spoopyghosty0 6c8cda5d7a Godot apps: start without a clipboard service (frame.godot_clipboard, GitHub #134)
Godot 4.2-4.4's Kotlin code casts getSystemService(CLIPBOARD_SERVICE) with a
non-null 'as ClipboardManager'; Lepton has no clipboard service, so the app
crashes with a NullPointerException in Godot.<init> (4.3/4.4) or on first use
(4.2's lazy property). In place in classes*.dex, matched exactly: the
Intrinsics.checkNotNull before that check-cast becomes nops (older kotlinc
branch forms: the branch is fixed), and Godot's getClipboard/hasClipboard/
setClipboard return at once (Dex.return_early gains String -> "").
Analysis field godot_clipboard (ANALYSIS_VERSION 10), triage
godot-no-clipboard, PLAYBOOK row, unit tests on synthetic dex. Checked against
godot-lib 4.1.4-4.7.2 from Maven Central and the 4.3/4.4.1 export templates:
4.2.2, 4.3.0, 4.4.1 patched (D8 re-reads the result), the others untouched.
2026-10-10 20:52:23 -04:00
spoopyghosty0 7331d9320a Catalog: VR4 zeroes a second permutation of the campaign shader that reads its loop counter before setting it (6604-byte module, found in the reporter's shader capture; GitHub #140) 2026-10-10 20:40:44 -04:00
spoopyghosty0 dccdcaab96 SteamVR overlay apps on the Frame (agent v75)
Host OpenVR overlays are composited over Lepton (Quest) games on the Frame;
Windows overlay apps register through Proton's vrclient. Detect overlay apps
(bundled .vrmanifest with is_dashboard_overlay, or OpenVR + IVROverlay +
self-registration), patch pcvr.vr_overlay and the Linux entry fields
vr_overlay/vr_overlay_autostart, agent register/unregister/launch_vr_overlay
(IVRApplications through SteamVR's libopenvr_api.so; manifest with
binary_path_linux_arm, the only key the Frame's vrserver reads). Overlay apps
skip launch tests and the Linux Steam-parent watchdog and don't count as a
running game. Probe sources in native/vroverlay_probe.
2026-10-10 20:01:07 -04:00
spoopyghosty0 99c6a88f90 Merge remote-tracking branch 'origin/main' 2026-10-10 19:19:43 -04:00
spoopyghosty0 da4f26197d Merge branch 'worktree-agent-ad1c44db9b43374d4' 2026-10-10 19:17:23 -04:00
spoopyghosty0 02361b7c99 Merge diagnostics: every shader of the newest session (agent v74, GitHub #140), issue forms valid YAML again, plain-issue fallback for sharing (GitHub #167) 2026-10-10 19:17:21 -04:00
spoopyghosty0 d6bbea05ca Sharing: fall back to a plain issue when the form opens empty (#167)
Some browsers (Flatpak ones on Linux) opened the issue form with the
title but every field empty. The issue links now also come as the
Markdown body GitHub's forms write (### sections, the recipe as a yaml
block, which catalog_from_issue.py reads) and a plain issue link with
that body, trimmed to the same URL limit. The GUI copies the body to the
clipboard when it opens the form and offers "Form empty? Open a plain
issue"; the CLI prints the plain link. A shared recipe is marked shared
only after the link was opened.
2026-10-10 19:14:00 -04:00
spoopyghosty0 65d3c2ab67 Monitor: per-module collection needs agent 73 (main's 72 lacks it); correct agent versions in comments
The {"modules": …} message and take_screenshot reached main with agent 73 (the ui-refresh
branch's v63-v66 numbers collided with main's). MIN_AGENT_MODULES was 65, so agents 65-72
were sent a message they ignore.
2026-10-10 19:13:42 -04:00
spoopyghosty0 327f213f56 serialize_flet_updates: tolerate Flet versions without Session.patch_control or Prop (log, keep the rest) 2026-10-10 19:11:49 -04:00
spoopyghosty0 05c24cc9e0 Issue forms: quote the descriptions so the templates are valid YAML
Both descriptions contain ': ' in a plain scalar since 3546232, which YAML
reads as a mapping: the issue forms didn't parse.
2026-10-10 19:10:50 -04:00
spoopyghosty0 6e31ae52c4 Auto pairing scan: skip container/VM/VPN interfaces and CGNAT, back off after empty scans
Network scans for Valve's pairing port now leave out docker/bridge/veth/tun/tap/wg/tailscale/
zerotier/vEthernet/utun-style interfaces and loopback, link-local and 100.64/10 addresses
(discovery.local_subnets, also used by the Frame search). Each scan that finds nothing doubles
the wait (30 s up to 5 min); another set of networks or opening the Frame page starts over.
2026-10-10 19:09:34 -04:00
spoopyghosty0 7830444c87 pyproject: Development Status 5 - Production/Stable 2026-10-10 19:07:56 -04:00
spoopyghosty0 9ace9d3bb3 Catalog: Vacation Simulator starts; controllers aren't tracked yet (shared working config)
Refs #163 (kept open: controllers not tracked)
2026-10-10 19:07:04 -04:00
spoopyghosty0 a994e86ba5 Catalog: The Thrill Of The Fight runs with Unity's overlay copy skipped; the right eye misses the opponent and referee (shared working config)
Closes #164
Closes #125
2026-10-10 19:07:01 -04:00
spoopyghosty0 e776116162 Catalog: Fruit Salon works (shared working config)
Closes #165
2026-10-10 19:06:59 -04:00
spoopyghosty0 1e814af1d4 Auto pairing: hold off only while a setup request is open (not for allowed, denied or expired ones) 2026-10-10 19:06:26 -04:00
spoopyghosty0 f3ed77deb0 Docs: diagrams instead of ASCII art
Architecture: the module overview and the self-update flow are SVG diagrams
(docs/images, shown on GitHub and the site); the update details the old drawing
carried are a short list under it. FAQ: the game-folder layout is a tree block
the site draws as a real file tree (scripts/filetree.ts). Diagrams scroll sideways
on phones instead of shrinking. Docs pages on phones: the sidebar no longer kept
its desktop offset, which pushed it over the page title.
2026-10-10 19:05:26 -04:00
spoopyghosty0 d5245aa496 Install links: FramePort's https://frameport.app/install?… page is the documented button link (pasted links parse too); README, docs, site and in-app texts no longer point to FrameDrop's buttons (its links still work). README and site: 1.0 drops the proof-of-concept line and says plainly that not every game runs 2026-10-10 19:05:22 -04:00
spoopyghosty0 e4a62d6e90 Pairing: setup requests expire when the Frame stops polling; setup.sh explains HTTP 429
Open requests whose /wait polling stopped for more than 2 x WAIT and decided ones older than
DECIDED_KEEP are dropped (on /hello and every few seconds); the Frame page shows only open,
unexpired requests and redraws when one expires. setup.sh tells a full queue apart from an
unreachable PC. The fallback Frame name in a request is translated.
2026-10-10 19:04:47 -04:00
spoopyghosty0 dcc57f29c8 Diagnostics: send every shader module of the newest session (#140)
Agent v74: with an index.txt, collect_diag sends every module named in the
newest session (after the last '# start'), deduplicated, last used first,
each up to 4 MB and up to 30 MB in all, plus that session's index lines.
A reporter's GPU hang came ~10 s after the last new module, so the newest
written modules missed the culprit. Without an index (fp_spirv) nothing
changes. The bundle drops the least recently used modules last when the
zip would pass its size limit.
2026-10-10 19:03:59 -04:00
spoopyghosty0 d87d085842 CLI frame pair: don't announce the pairing server over mDNS (it can't answer setup-URL requests) 2026-10-10 19:01:23 -04:00
spoopyghosty0 3930617518 Issue reports: a game's report is titled "[Game report] <game>" with the game-compatibility label; reports about FramePort itself "[App] FramePort" with bug ("[Problem]" titles read as a list of app faults) 2026-10-10 19:00:35 -04:00
spoopyghosty0 deaf1f08a6 Setup line: curl -fsSL https://frameport.app/s (fail on HTTP errors, explicit https) 2026-10-10 19:00:18 -04:00
spoopyghosty0 76ba1e4ffa Merge pull request #166 from spoopyghosty0/showcase/update
Update docs screenshots and videos
2026-10-10 18:04:55 -04:00
spoopyghosty0 62728dc562 Site: Cloudflare and Google analytics, with a cookie choice
Cloudflare Web Analytics (cookieless) runs when the CF_ANALYTICS_TOKEN repo
variable is set. Google Analytics (GA_MEASUREMENT_ID) loads only after the
visitor clicks Allow on a small card with equal Allow/Decline buttons; the choice
is remembered, Cookie settings in the footer asks again, Decline removes the
_ga cookies and Global Privacy Control counts as Decline. Downloads are counted
as file_download events. New privacy page. Without the variables the site has
no analytics.
2026-10-10 17:56:37 -04:00
spoopyghosty0 6454c7f37d Docs: update screenshots and videos 2026-10-10 21:33:45 +00:00
spoopyghosty0 a4f73a88b7 Merge origin/main into site
Conflicts kept main's features with the site branch's wording: the uninstall
dialog's new delete-saves option, hardware video decoding and last-session help,
new Game settings (plain labels), agent 73 (main's 72 plus the Take screenshot
command from v66). Main's new texts follow docs/STYLE.md (for example, patch/setting
instead of fix). Pages rebuilds on docs changes and daily; the CNAME file is
gone (the domain is a Pages setting).
2026-10-10 17:18:37 -04:00
spoopyghosty0 e5658ed441 Site: SEO for frameport.app
Sitemap and robots.txt, complete Open Graph and Twitter tags, JSON-LD
(the app as a free SoftwareApplication with its current version; docs as
articles with breadcrumbs), a written description for each docs page, a
releases Atom feed, a 404 page, and titles that say what people search for.
2026-10-10 16:54:26 -04:00
spoopyghosty0 6bfaaf788f Site: What's new page and section from GitHub releases
/changelog/ shows every release's What's new part (the same parts the app's
changelog shows): the dev build as Coming next, the latest release large,
then a timeline; the home page shows the latest changes. Release jobs
restart the pages workflow so a new release appears on the site.
2026-10-10 16:41:42 -04:00
spoopyghosty0 07da1c27a3 Site: shorter, consistent copy, served at frameport.app
Plain words on every page, one label per action, no jargon on the home
page. The site moves to the custom domain frameport.app (CNAME, no
/frameport base); the setup line is curl -sL frameport.app/s | bash.
2026-10-10 16:34:22 -04:00
spoopyghosty0 35462324bb Docs: shorter, consistent copy
README is a short overview that links to the install guide; repeated
sections are said once; Share working recipe everywhere; the setup
script names the real buttons; a shorter notice, the same as the site's.
2026-10-10 16:34:19 -04:00
spoopyghosty0 2c1149815d App: shorter, consistent interface text
Dialogs, help texts and tooltips cut to what the user does or gets, one
name per action (Start setup, Uninstall FramePort, Update art on Frame,
Rebuild only), plain launch-test results (C.test_result), plain summaries
for every Game setting. The setup line now points at frameport.app.
2026-10-10 16:34:17 -04:00
spoopyghosty0 c9b600d186 Developer docs, CLI and launch-test diagnoses: consistent terms
US spelling, patch (not fix) for patches, current button names. Every
diagnosis now opens with one plain sentence before the technical detail.
2026-10-10 16:34:04 -04:00
spoopyghosty0 ca14fad3ce Merge Steam Input gamepad for flat Android apps (device.steam_gamepad, GitHub #162) 2026-10-10 16:12:47 -04:00
spoopyghosty0 c4fa6557e1 Steam Input gamepad for 2D Android apps (device.steam_gamepad, agent v72, GitHub #162)
Lepton's Android only gets keyboard, pointer and touch from the Wayland seat. The opt-in patch (2D apps; suggested
when the manifest declares android.hardware.gamepad or LEANBACK_LAUNCHER, ANALYSIS_VERSION 9) makes the agent record
steam_gamepad in deployment.json and give the launcher a line that exports SDL's hint
(LEPTON_ENV_SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD) and puts FramePort's own Podman wrapper (agent/bin/podman,
written by ensure_host_fixes, independent of the video codec) first on PATH. For the game's podman run it bind-mounts
Steam's virtual pads (uinput, 28de, gamepad buttons) at /dev/input/eventN plus an Xbox 360 key layout, then hands on
to the next Podman after itself (the codec wrapper or Podman), so both wrappers chain in either order.
FRAMEPORT_NO_GAMEPAD=1 turns it off for one start; failures keep the arguments unchanged. Verified headless on the
dev Frame with a uinput stand-in pad: Android's EventHub opens it with our layout and dispatches BUTTON_A.
2026-10-10 16:09:16 -04:00
spoopyghosty0 1bb08bab36 Wording: style word list and length rules 2026-10-10 16:00:23 -04:00
spoopyghosty0 cccafe86bc Merge vk_shader_dump (Vulkan shim shader capture, GitHub #140) 2026-10-10 15:56:59 -04:00
spoopyghosty0 6fb4a52051 Vulkan shim: vk_shader_dump writes the game's SPIR-V modules with a creation index (GitHub #140)
Adapter setting vk_shader_dump=1: every distinct module once to files/fp_vk_shaders/<size>_<sha256>.spv, each
vkCreateShaderModule as one index.txt line (order, time), to find the shader behind a GPU hang for a vk_shader_fix.
Agent v72 collect_diag returns the newest dumped modules (Vulkan and OpenGL ES dumps); diagnostics bundle them.
Triage gpu-hang suggests the dump for the session's graphics API (FrameBridge's swapchain formats).
2026-10-10 15:56:01 -04:00
spoopyghosty0 5f42c4bb01 Triage: unity-data-missing fails a launch test when Unity can't read the game's data files (incomplete OBB or one from another version; Batman passed while stuck at boot, GitHub #155) 2026-10-10 15:50:47 -04:00
spoopyghosty0 37b8ac78a7 Game page shows Update on Frame after a newer APK of an installed game was added (builds record the APK version they were made from; GitHub #161); triage only offers fixes that can matter for the game (a Vulkan game was offered the GLES-only 360° emulation; GitHub #138) 2026-10-10 15:50:44 -04:00
spoopyghosty0 26b86f3f9d Catalog: Eleven (Quest) and GOLF+ unsupported (Meta online services / the game's login refuse the platform), Beat Saber DLC isn't detected, Doom3Quest works (walk/run switching is the game's reaction to slowdowns)
Closes #160, closes #147, closes #150, closes #133, closes #77
2026-10-10 15:44:28 -04:00
spoopyghosty0 8d50d5a065 Catalog: 13 shared working configs (Freedom, Zombieland VR, Caves, Ancient Dungeon, Containment Protocol, Down the Rabbit Hole, End Space, ExploreVR, Racket: Nx, RC Pilot Trainer, Retropolis, Shores of Loci, SynthRiders)
Closes #132, closes #141, closes #142, closes #143, closes #144, closes #145, closes #148, closes #149, closes #151, closes #152, closes #153, closes #154, closes #158, closes #122, closes #93
2026-10-10 15:44:24 -04:00
spoopyghosty0 ce257b429a Site: two neon worlds, a portal-first download page, one-eye live view
- Two more Frame-side worlds (envs-neon.ts): a rain-soaked neon megacity, and a Tron-like arena with light cycles.
- A game's world comes from a hash of its title (FNV-1a): with seven worlds the old palette mix gave every
  title the same remainder, so one world never came up and another four times in ten.
- Download page: no round countdown ring; the countdown runs round the portal's own rim, a tunnel of rings
  sinks away inside it (the rim is a masked band, so the way through stays dark).
- The trial's live view shows one eye, as the app's does.
- Headings reveal slower again.
2026-10-10 14:59:39 -04:00
spoopyghosty0 7e95c50f39 Game page: open the last launch test's log
The Steam Frame (or This PC) card gets a log button when the game's last launch test ran there: it opens the
saved log (last_test.log_path) in the launch log viewer. The showcase's demo library writes a short made-up log for
each passed test so the button shows in the docs pictures.
2026-10-10 14:44:54 -04:00
spoopyghosty0 61ecc4d2ee Site: interactive docs, a download page, two more worlds, solid rocks
- Docs: search across every page (highlights where you land), definitions for jargon on hover, tables you can
  filter and sort, step lists you can tick off, the tutorial video in place, zoomable pictures, section links that
  copy, / [ ] keys, a reading path per goal on the overview and a mark on pages you've read.
- /download/: every Download button leads here; a countdown while the package goes through the portal, then the
  download starts; a manual button stays, with version, size and SHA-256 from the release and the next steps.
- Two more Frame-side worlds: an aurora over a frozen lake, and a reef. Shared helpers moved to envkit.ts.
- Asteroids and floating islands no longer tear: corners shared by several faces move together (moveCorners).
- Built on: every project links to its page. Compare: no update row; Rift games not supported by the others.
- Headings reveal a little slower.
2026-10-10 13:27:10 -04:00
spoopyghosty0 391f43df95 Site: three hero worlds, a 3D live view, a closer app trial, real screenshots
- The Frame side of the hero picks one of three worlds per game (grid, islands, space); the particles option is gone.
- The trial's Live view shows the same worlds through two lenses (vrscene.ts), one scene per stream, CSS fallback without WebGL.
- The trial looks more like the app: window bar, Library header/filters/shelf, game page, Frame card with now playing and power.
- The install log follows the game: package, conversion, its patches, version and note; the recipe opens under board rows.
- Calmer motion: no button drift, smaller click ring and cursor light; the gallery carousel shows the real screenshots again.
2026-10-10 12:28:15 -04:00
spoopyghosty0 60d8ab021b Install walkthrough: the setup line and Allow, in the video and on the site
- Install tutorial video (docs/showcase/videos/install.yaml, re-recorded by record_video.py): Start setup, a card
  for the setup line on the Frame (with the Developer Mode route in its note), the Frame asking and Allow clicked,
  a card for what the Frame then does by itself. Showcase: the fake pairing server takes asks; hook `frame_asks`.
- Site: the Install section is a six-step walkthrough (download, Start setup, the line or Developer Mode on the
  Frame, Allow, it sets itself up, add games and play), each step with a small live scene: the download unpacks,
  Start setup shows the line, Konsole types it and finds FramePort, Allow moves on, the setup ticks off, a game
  installs. The other ways link to /setup.
2026-10-10 10:16:13 -04:00
spoopyghosty0 dfab8d68fd Pair Frames in Developer Mode without a click on the PC
While no Frame is connected, FramePort watches for Frames in Developer Mode (Valve's devkit mDNS, else a scan every
30 s for Valve's pairing port, one entry per Frame by host key). One that already lets FramePort in is connected; one
that doesn't is offered FramePort's key through Valve's pairing every few seconds (refused at once until "Pair new
host" is open on the Frame, then the request shows in the headset; a 45 s pause after an unanswered one). So the user
only opens Pair new host and approves in the headset.
- frame/autopair.py (no Flet, callbacks), app._start_auto_pair, setting frame.auto_pair (switch on the Steam Frame
  page, default on); never with FRAMEPORT_HOME or FRAMEPORT_NO_AUTO_PAIR (tests, screenshots).
- Checked on the dev Frame: found by the scan (this WSL PC doesn't hear its mDNS), FramePort's key recognized, no
  pairing request sent. The headset approval path needs a Frame that doesn't know FramePort yet.
2026-10-10 09:45:51 -04:00
spoopyghosty0 8167cb86c6 Merge the shared hardware video decoder (PR #128, adapted: per game, off switch, VP9 fixes)
# Conflicts:
#	agent/frameport_agent.py
2026-10-10 02:25:41 -04:00
spoopyghosty0 44bc689348 Shared video decoder (codec revision 8): hidden VP9 frames no longer end the EOS drain early
Iris returns an empty capture buffer (bytesused 0, no LAST flag, time 0) for every VP9 frame that isn't shown
(alt-ref frames split from superframes). FFmpeg's V4L2 wrapper takes any empty capture buffer during a drain
for the end of the stream, so the first hidden frame decoded after EOS ended it and the pictures still in the
driver were lost (two-pass VP9 4K: 573/600 and 617/640, always the last frames); outside a drain the empty
buffers were returned as pictures, which the component then guessed away by counting hidden frames.
build.py now patches v4l2_context.c to requeue empty capture buffers without LAST/ERROR; the drain ends at the
LAST buffer, or after a skipped empty buffer at one silent second, so it can't block. The component's
hidden-frame counting is removed. Dev Frame (Batman's container): two-pass VP9 4K 600/600 and 640/640 in
hardware with the same Y hashes as OMX.google.vp9.decoder (seek phase too), VP9 without hidden frames,
H.264 4K and HEVC 4K 600/600, VP9 surface output 600/600. Two builds (different dirs and NDK copies) give the
same libstagefrighthw.so 141de01f.
2026-10-10 02:22:48 -04:00
spoopyghosty0 007db344d7 Shared video decoder: remove a game's old per-game codec folder once its launcher is converted, also when the recipe (not deployment.json) asks for hardware decoding (seen with Batman on the dev Frame) 2026-10-10 01:01:33 -04:00
spoopyghosty0 3fb2621030 Shared video decoder: per game (frame.hw_video_decode) with a global off switch; 8K retry at start and VP9 fixes from the hw-video-decode branch
- Opt-in per game: only launchers of games whose recipe has frame.hw_video_decode (install stage, no APK change;
  suggested from analysis media_codec, ANALYSIS_VERSION 8) get the codec line (deployment.json hw_video_decode,
  finalize + upgrade_launchers). Games that had agent 70's per-game codec (Batman) keep it; their old
  <base>/frameport-codec folders are removed afterwards. Batman: catalog + migration batman_video_patches
  (frame.hw_video_decode + the hidden adapter setting surface_native, no more package check); 4XVR catalog.
- Off switch: Settings -> Installing "Hardware video decoding" (library video.hw_decode, default on) -> agent
  video_codec_switch (video-codec/disabled, checked by the launcher line and the wrapper); FRAMEPORT_NO_HW_VIDEO=1
  for one game.
- Connection: the codec is checked once per connection, also after a failed install; a Frame keeps the same or a
  newer revision built elsewhere (logged) instead of flip-flopping between PCs.
- Wrapper: the merged media_codecs.xml goes to $XDG_RUNTIME_DIR/frameport-video, never into the version directory.
- Decoder: refused (busy) Iris sessions retry until 20 s after the plugin loaded (SteamVR's link holds a session
  ~12 s at game start), else 2 s, before the software fallback; VP9 capped at 4096x2304 (XML + component, larger
  decodes in software); hidden VP9 frames' pictures dropped. Manifest revision 7.
- build.py builds from paths with spaces and from freshly extracted FFmpeg sources. Artifacts rebuilt here, identical
  from two trees (ext4 and the NTFS repo path, two NDK copies); PR #128's own sources rebuild to its binary.
- AGENT_VERSION 71 (matches the connection's gate); triage hw-video-decoder-busy, PLAYBOOK row, docs.
2026-10-10 00:58:24 -04:00
spoopyghosty0 39ca9a5f0d Catalog: Audica works (shared working config)
Closes #126
2026-10-10 00:28:39 -04:00
spoopyghosty0 e64e8a95e0 Uninstall dialog: option to also delete the game's saves and everything it stored on the Frame (mods, downloads); they survived every uninstall before (GitHub #130) 2026-10-10 00:28:37 -04:00
spoopyghosty0 69e888c7fe Install on this PC works on Windows again: the agent (loaded for its VDF code) imported the POSIX-only fcntl at the top (ModuleNotFoundError for every PC VR install; GitHub #131) 2026-10-10 00:28:36 -04:00
spoopyghosty0 ce809c432d Merge PR #128: shared hardware video decoding (Lucas-Mathieu) 2026-10-10 00:23:42 -04:00
spoopyghosty0 cdf900c6b7 Docs: the setup URL first, the other ways kept; what the Frame offers for it
INSTALL and FRAME_SETUP describe the setup line, the Allow step and the announcement; the setup command, USB cable
and Valve's pairing stay. FRAME_RUNTIME records what was checked on the dev Frame (Chromium via Steam's +, mDNS from
the Frame only on port 5353, avahi-browse not usable over SSH). README's quick start uses the line.
2026-10-10 00:23:09 -04:00
spoopyghosty0 d5b5b7751e Project page: /setup, the setup line in Install, the trial's connect flow
- /setup: the three steps (Start setup on the PC, the line on the Frame with Copy, Allow), a terminal that plays what
  the script prints, what it changes, and every other way (setup command, USB cable, Valve's pairing, address).
- The site serves bootstrap/setup.sh as /s and /setup.sh (copied at build time; the workflow rebuilds when it changes).
- Install step 2 starts with a "Setup page" tab; the trial's Steam Frame page and guided tour use the same flow
  (the Frame asks, matching code, Allow).
2026-10-10 00:23:08 -04:00
spoopyghosty0 351712172c Setup URL: the Frame finds FramePort, you allow it, the usual setup runs
One line for every Frame and PC, served by the project page: curl -sL spoopyghosty0.github.io/frameport/s | bash.
- bootstrap/setup.sh finds FramePort (stdlib mDNS on 5353 for _frameport-pair._tcp; else the USB cable's address and
  a /24 scan of /ping), asks with /hello, shows 4 digits, waits for Allow, then runs exactly the typed line's
  `curl -fsS <pc>/<code> | bash`: bootstrap.sh is unchanged.
- PairingServer (no second server): /ping, /hello (max 3 open asks), /wait (long poll; code only once allowed),
  decide(); announces itself over zeroconf while running (name, port, two words for this PC's key; never the code).
- Connect page: "Start setup" shows the static line, incoming asks as Allow / Deny cards (a toast elsewhere);
  "Use the setup command" keeps the address + code line; USB setup unchanged.
- The app key is created under a lock (the announcement and the page asked for it at once on a first start).
- Tests: approval/deny/limits/timeouts, the code never in the announcement, setup.sh end to end against a real server
  with a stub bootstrap (allowed and denied), digits match. ui_smoke --setup-url drives the page.
Checked on the dev Frame with a stub bootstrap: found by mDNS and by the scan fallback, allowed, code handed over.
2026-10-10 00:23:06 -04:00
spoopyghosty0 f99aded40e Project page round 4: VR worlds through the portal, live screens, a page that reacts
- Hero, Frame side: the card is gone; the game opens into an abstract VR world (three.js, look around by dragging or
  tilting the phone) or into particles that gather into a shape for the game. A TEMPORARY toggle under the scene
  switches between the two for comparison. three.js loads only when the hero is on screen.
- A Steam Frame you can turn (the logo's visor, extruded) next to "Connect the Frame".
- No pictures of the app on the page: the carousel's slides are live copies of its screens (type, drop a file, end a
  process, switch themes), the video tiles play short silent loops (made at build time with ffmpeg), the install
  button is a live control.
- Scroll story: the hero flies toward its portal, the install log completes line by line, section lines draw in.
- Everything reacts: a portal light follows the pointer, clicks on empty space ripple blue then orange, headings rise
  in word by word and light up under the pointer, labels unscramble, numbers count up, each icon moves its own way.
2026-10-09 23:15:21 -04:00
spoopyghosty0 f631e5fc1b Project page round 3: docs pages, contributors, carousel, a fuller app demo
- Hero: the Frame-side card is the game running in the headset (one view per eye, the scene moving), switching on
  as it lands while its frame rate climbs to 72 and the pacing line draws.
- App demo: working search, right-click menu, select several and install them (queued), install from a link (the
  app's link rules), Screenshots tab with a viewer, Settings with the real themes switching live, Files that take
  real files from the desktop (they never leave the tab), process table with End, Live view quality, launch-test
  result, technical patch ids, toasts; Wi-Fi or USB cable when connecting. Progress ticks no longer rebuild the page.
- Screenshots: a carousel (arrows, keys, swipe, thumbnails, autoplay that pauses for the reader, full screen).
- Compared: four headline differences (free and open source first), every difference as user questions behind
  "Show all differences"; the README says the same.
- Wi-Fi or USB cable said everywhere; the setup command explains itself part by part; first-start steps per system.
- Field notes removed, the notice kept in its own band.
- Built on: contributors wall (code, pull requests, issues; scripts/contributors.mjs, saved list as fallback).
- Docs: user-facing docs/*.md as pages (links rewritten, GitHub heading ids, contents with scroll position, copy
  buttons).
- Games board rows open to show the recipe; the install log replays with another tested game.
2026-10-09 22:29:55 -04:00
Lucas-Mathieu 26cee1551e Deploy shared hardware video decoding for compatible Lepton apps 2026-10-10 03:36:56 +02:00
spoopyghosty0 82e2a40791 Project page round 2: app demo, livelier hero, comparison, sharper videos
- "Meta Quest" only as a description (headline about the Steam Frame), trademark line in the footer.
- Hero cards: generated covers in the style of the app's placeholder art (hue from the title), the same app icon on
  both sides of the portal, a one-line APK name, stage ticker and progress, Play ripple, sparks; titles sized so
  words never break. Click the portal for the next game; the scene leans toward the pointer.
- "The app": a clickable, simplified FramePort window (Library, game page with patch switches, Steam Frame, Files,
  Live view, Type on Frame, Monitor with live sparklines) and a guided tour from a fresh start: connect the Frame,
  scan a folder, install, play. The showcase videos play in a lightbox.
- Compared: FramePort, FrameDrop and Valve's own tools, from their own pages (checked 2026-10-09); the same table in
  the README.
- record_video.py --size WxH (e.g. 2560x1440): films at that many device pixels; postprod reads the size at call
  time and draws cards at the 1920 layout scaled up. Default output unchanged.
2026-10-09 21:26:37 -04:00
spoopyghosty0 1c158930cd Project page: an Astro site for GitHub Pages, and the install button's landing page
site/ (Astro 7, static, served at spoopyghosty0.github.io/frameport/) takes the Portal look of the UI refresh:
- Hero: a catalog game crosses the logo's tilted portal, from an APK on the PC's (blue) side to a game in the
  Steam library on the Frame's (orange) side; reduced motion holds it halfway, like the logo.
- How it works as one real recipe's install log (PC stages blue, Frame stages orange), the showcase tour and
  screenshots (synced from docs/ at build time, never by hand), field notes on what doesn't work, install steps,
  credits.
- Games board from catalog/games: prebuilt at build time, then refreshed in the browser from main (one contents
  API call, only changed files fetched, 6 h localStorage cache); any failure keeps the built list.
- /install/?manifest=…|?url=…: where an "Install with FramePort" button lands; opens frameport://install, and if
  FramePort didn't take it, says how to get it (download for this OS, try again, copy the link). Link rules mirror
  deeplink.check_url; node tests cover them and the board's rows.

.github/workflows/pages.yml builds and deploys on main (Settings → Pages → Source: GitHub Actions, once).
2026-10-09 20:22:26 -04:00
spoopyghosty0 f812ea8716 Codec wrapper (PR #96): find the host's podman when Lepton calls it with the Android guest's PATH (its boot-wait, app-pid and logcat-mirror podman exec calls failed, so launch.log lost logcat and the container could be stopped early) 2026-10-09 17:54:44 -04:00
spoopyghosty0 3dc0a95379 Catalog: Doom3Quest uses frame.gl_multiview_fbo (HUD/PDA shown, no frame-rate drop: Xandrix1987's headset test, GitHub #77) 2026-10-09 16:30:33 -04:00
spoopyghosty0 f8244f582d Catalog: cubism works (shared working config)
Closes #121
2026-10-09 16:29:36 -04:00
spoopyghosty0 69fbdfdc4a SUPERHOT VR (Quest): create its cloud save folder at install, so the launcher gives it group write permission before the very first start (the game creates and checks it in the same millisecond; GitHub #120); triage text says to start again 2026-10-09 16:29:34 -04:00
spoopyghosty0 ef3272a31b Catalog: Vader Immortal III's campaign intro starts black by design (owner) 2026-10-09 15:04:18 -04:00
spoopyghosty0 b0f3ef2698 Catalog: Vader Immortal Episodes II and III work (owner's headset test) with Episode I's Unreal fixes (quest precompile, key map, thumb touch, pose_time_fix), all found in their code by the heuristics 2026-10-09 14:37:11 -04:00
spoopyghosty0 1711e7f7d9 Docs: session triage (PLAYBOOK rows for the session findings, CLAUDE.md note incl. the pac_hints survey) 2026-10-09 14:22:22 -04:00
spoopyghosty0 ff6cdf3b6a Triage real play sessions: the connection refresh fetches a finished session's log (agent v70) and triages it (launch-test signatures + new space-warp-used question and gpu-hang, find_rp_state for unreal-msrtt-crash, slow-frames from FrameBridge pacing, focus-dips), stored as last_session; game page "Last session" callout; value suggestions (adapter.key=value); FrameBridge-only fixes applied live via set_settings; CLI frameport session [--apply] 2026-10-09 14:22:20 -04:00
spoopyghosty0 28517a1999 Agent v70: session_log (the newest play session's launch.log sliced to 4 MB + its crash logcat + kernel GPU hang lines), last_play in list_installed, launch tests mark their session in plays.log 2026-10-09 14:22:17 -04:00
spoopyghosty0 af9c207ddf FrameBridge: always log the runtime's focus losses and returns with their length (focus: lost / back after N ms), before focus_hold hides a dip; for session triage 2026-10-09 14:03:23 -04:00
spoopyghosty0 88045bcf4e Merge remote-tracking branch 'origin/main' into merge/pr96 2026-10-09 13:43:24 -04:00
spoopyghosty0 5147710fad Merge Batman cutscene playback (hardware HEVC decoding, stereo composition) by Lucas-Mathieu (GitHub #96)
Adapter rebuilt here; the codec plugin rebuilt here with native/hevc/build.py (NDK r27c, Lepton SoftOMX fingerprint 456e912c) is byte-identical to the contributed one (c1c2a73e). Agent version 70 -> 69 (main was at 68).
2026-10-09 13:43:22 -04:00
spoopyghosty0 9c45cff09a Heuristics: suggest pose_time_fix for Unreal 4 games with Vader Immortal's Oculus input (the unreal_thumb_touch code match). Catalog: Phantom, Robo Recall and Time Stall get frame.unreal_thumb_touch + pose_time_fix (their code matches Vader's; not yet checked in these games), Star Wars: Tales gets frame.unreal_thumb_touch instead of proximity_emul (as Vader) 2026-10-09 13:43:07 -04:00
Lucas-Mathieu b4848c0424 Harden codec deployment and isolate native video hooks 2026-10-09 19:24:51 +02:00
spoopyghosty0 3a46b78361 Catalog: Clockwork (Quest), Beat Saber, Eleven: Table Tennis VR and Pistol Whip (PC VR, run directly without Revive) from shared working configs
Closes #116, closes #117, closes #118, closes #119
2026-10-09 13:21:50 -04:00
spoopyghosty0 75bbc6eb5f Heuristics: suggest scene_emul for every game that declares Meta's USE_SCENE permission, not only mixed-reality-only ones (VR HOT's room setup retried forever: LoadSceneModel failed / no XR_FB_spatial_entity_query; with scene_emul the room setup returned without errors). Catalog recipes still win (6 verified games declare USE_SCENE and run without it) 2026-10-09 13:06:44 -04:00
Lucas-Mathieu da638aea99 Merge remote-tracking branch 'origin/main' into fix/batman-cutscene-playback
# Conflicts:
#	agent/frameport_agent.py
#	artifacts/SHA256SUMS
#	artifacts/arm64-v8a/libopenxr_loader_generic.so
#	artifacts/armeabi-v7a/libopenxr_loader_generic.so
#	native/adapter/frame_adapter.c
2026-10-09 18:29:12 +02:00
spoopyghosty0 7c37b479db Agent v68: launch tests count their window from Lepton's 'Waiting for app' (up to 240 s more for boot + install). The first start after an APK change installs the app first; a 45 s window from the launcher stopped the container mid-install and left a broken installed APK ('base.apk is not zip') that never started again (VR HOT) 2026-10-09 11:58:54 -04:00
spoopyghosty0 fa473c5ada frame.unreal_thumb_touch: UE4 OculusInput's ThumbUp from the capacitive touches instead of near-touch, which the Frame never reports (thumbs always pointed up). Three instructions in SendControllerEvents (masks 0x2/0x8 -> 0x0f00/0x000f, NearTouches load -> Touches), matched exactly; suggested where the code matches (analysis unreal_thumb_touch, ANALYSIS_VERSION 7): Vader Immortal Ep. I, Robo Recall, Phantom: Covert Ops, Time Stall, Star Wars: Tales. Vader's recipe uses it instead of proximity_emul (the binding didn't animate the thumbs). Found by Klownicle, GitHub #49 2026-10-09 11:35:36 -04:00
spoopyghosty0 1e6ce04e4f pose_time_fix: never move a recent predicted display time; monotonic times only told apart when the clocks are more than 4 display periods apart and the time is clearly nearer the monotonic now (GitHub #49: on a Frame with XrTime only ~68 ms ahead of CLOCK_MONOTONIC, a request at the frame's display time after a hitch was taken for a monotonic time and moved +67.7 ms). Found by Klownicle, GitHub #49 2026-10-09 11:35:35 -04:00
spoopyghosty0 5cd5b119e7 Lint: extraneous parentheses (ruff UP034) 2026-10-09 11:33:00 -04:00
spoopyghosty0 186138cc3c Game page: a patch the build left out because the OVRPort runtime already has the fix (build.superseded, e.g. haptic_fix with runtime 3.4.3-aa54c3f) no longer counts as a settings change: 'Update on Frame' stayed after every update (owner's VR HOT, Klownicle's Vader, GitHub #49) 2026-10-09 11:32:40 -04:00
spoopyghosty0 98c7ca980a Triage: Lepton's own short lines survive the game-process filter. With the package known, game_lines dropped every line of 3 words or fewer, so Lepton's 'Boot complete!' vanished and the transient 'is not a running context' failed launch tests in the GUI although the games ran (owner's runs: Come Closer, AntiZeroGames CH, SKYBOX; Klownicle's Vader, GitHub #49). Only logcat lines are filtered by pid now 2026-10-09 11:29:12 -04:00
spoopyghosty0 b2470b962a Catalog: Max Mustard is listed by its name, not its package name Matilda (GitHub #110) 2026-10-09 11:10:09 -04:00
spoopyghosty0 2a705bc6d1 frame.gl_multiview_fbo: single-view twins for multiview programs drawn into flat framebuffers (GitHub #77)
Mesa enforces OVR_multiview's rule that a draw's program declares as many views as the draw framebuffer has and drops the draw silently; Doom3Quest compiles every vertex shader with layout(num_views=2) and draws its HUD/PDA into 2D-texture FBOs, which stay black on the Frame.

native/glmv (libfpglmv.so, same length as libGLESv3.so): the engine library's dlopen string is rewritten to it and it becomes the first DT_NEEDED. It records stage sources at link time and, when a multiview program draws into a framebuffer without views, draws with a lazily built single-view twin (num_views layout blanked, gl_ViewID_OVR -> 0u; attribute locations, block bindings and uniform values copied), then rebinds the original. Opt-in and experimental (analysis gl_multiview_libs, ANALYSIS_VERSION 6), setting gl_mv_debug, triage gl-multiview-twin-failed.

Host tests: the rewriter on Doom3Quest's 19 shaders (GPL-3.0 fixtures) and glmv.c against a stand-in GL that applies Mesa's rule. Untested on the device.
2026-10-09 10:54:40 -04:00
spoopyghosty0 5d44831757 Agent v67: keep launch.log filling when Lepton's logcat mirror dies ('logcat: Unexpected EOF!' right after the game starts, about 1 launch in 50: Vader Immortal on Lepton 3.0.5, Under Cover on 2.8.14). launch.sh starts _logcat_keeper, which then reads the container's logcat itself (podman exec ... logcat -T 2000, up to 5 restarts) so the dashboard auto-hide and launch tests see the game; upgrade_launchers adds it to existing launchers 2026-10-09 10:14:57 -04:00
spoopyghosty0 456880501b PC VR: a catalog recipe verified with another build (different VR APIs) keeps the build's own launch arguments; Electron launchers rank below the game; agent v67 collects Unity's Player.log / output_log.txt / crash error.log for launch tests and diagnostics; triage unity-vr-init and unity-crash (GitHub #105) 2026-10-09 10:10:33 -04:00
spoopyghosty0 63f7ae12e1 Catalog: Vader Immortal: Episode I works (owner's headset test of the GitHub #49 fixes by Klownicle) 2026-10-09 10:06:44 -04:00
spoopyghosty0 fa69ffefd2 Vader Immortal: loading card, controls, lightspeed shaders, thumbs (GitHub #49)
Klownicle's five fixes for Vader Immortal: Episode I, reimplemented as FramePort patches (found by Klownicle,
GitHub #49; their prebuilt APK is not used):

- frame.unreal_quest_precompile: UVRUtils::GetQuestShaderPrecompilePercent returned 0.0 on its non-Quest branch, so
  the menu waited forever on the loading card; that branch now returns 1.0. Located by symbol, every instruction
  around it checked (bl IsRunningOnSantaCruz; tbz w0,#0; fmov s0,wzr; ldp; ret); other builds stay untouched.
- frame.unreal_quest_keymap: the RPOC key selector (shared by AddAxisMapping and AddActionMapping, so both mapping
  kinds were affected) picked the empty Gear VR key set for an Oculus HMD; the tbz to it becomes a nop. Located as
  the common callee of both functions, pattern-checked.
- frame.zink_shader_fix + native/zinkfix (libVkLayer_fp_shaderfix.so): a Vulkan layer under Zink for GLES games
  that applies zink_shader_fix entries (vk_shader_fix format) to the SPIR-V Zink generates; it adds itself to
  GraphicsEnv's debug layer list from a constructor (the engine library loads it first). zink_shader_dump=1
  captures modules. Vader's recipe carries the two lightspeed-shader fixes (12 OpStores each).
- adapter proximity_emul (FrameBridge): OVRPlugin's thumb/index proximity actions get bindings to the capacitive
  touch inputs when the runtime lacks XR_FB_touch_controller_proximity (OVRPort offers the extension anyway):
  generic replacement for the libUE4 ThumbUp patch.
- Vader's recipe: pose_time_fix + proximity_emul, status unknown (to test in the headset), min_app 0.12.1.

Analysis records unreal_quest_gates (ANALYSIS_VERSION 4). Star Wars: Tales from the Galaxy's Edge (same studio)
has neither gate (different engine build) and gets a catalog entry with pose_time_fix + proximity_emul.

Headless on the dev Frame (Lepton 3.0.5): the layer loads into Zink's instances, the proximity bindings are
accepted, and the game passes the loading card to the "guardian smaller than recommended, press any button" screen.
2026-10-09 09:20:15 -04:00
spoopyghosty0 f78bc82cc4 frame.vivox_audio_route: Vivox voice chat without Android 12 audio routing (GitHub #101)
Newer Vivox builds (Green Hell VR) call AudioManager communication-device methods (API 31) from com.vivox.sdk.AudioChangeListener without a version check; Lepton is Android 11 -> NoSuchMethodError. The methods that call them now return at once (in-place dex edit, Dex.return_early). Detected by analysis vivox_api31 (ANALYSIS_VERSION 4), triage vivox-api31. Verified headless: Green Hell starts, Vivox initialises, ~65-70 fps.
2026-10-09 09:09:24 -04:00
spoopyghosty0 dfacf6a206 FrameBridge: serve cube swapchains the runtime refuses (GitHub #107)
Budget Cuts Ultimate asks for a 2048x2048 cube swapchain (faces=6); the Frame's runtime has no cube layers and refuses it (-2), OVRPlugin carries on with no images and crashes in ovrp_EndFrame4 (memset). cube_standin (default on, GLES) serves it as a GL cube map in the game's context and drops its layers. Triage cube-swapchain-refused (supersedes unity-render-crash/native-crash). Verified headless: the game runs on at ~70 fps.
2026-10-09 09:09:23 -04:00
spoopyghosty0 b2f14c1e2e Catalog: SUPERHOT VR (Rift) keeps the build's own launch arguments (GitHub #105: the older Oculus + SteamVR build needs -vrmode OpenVR; the catalog recipe from the OpenXR build dropped it) 2026-10-09 08:49:53 -04:00
spoopyghosty0 03d7a311bf Catalog: Batman: Arkham Shadow gets sync_guard to test (GitHub #102: quit to Steam's waiting screen after the Frame runtime crashed in xrSyncActions, the input race Myst had) 2026-10-09 08:32:50 -04:00
spoopyghosty0 cd5119278b Catalog: The Light Brigade (works with issues)
Closes #114
2026-10-09 07:56:14 -04:00
spoopyghosty0 6ab3608a44 Catalog: Gravity Lab (works)
Closes #113
2026-10-09 07:56:06 -04:00
spoopyghosty0 c0539af368 Catalog: Does it Stack? (works with issues)
Closes #112
2026-10-09 07:56:02 -04:00
spoopyghosty0 c6363140f4 Catalog: Walkabout Mini Golf (works)
Closes #111
2026-10-09 07:55:58 -04:00
spoopyghosty0 187105b729 Catalog: Matilda (works)
Closes #110
2026-10-09 07:55:55 -04:00
spoopyghosty0 b983c0f11f Catalog: The Tale of Onogoro (works)
Closes #108
2026-10-09 07:55:52 -04:00
spoopyghosty0 c7cee33889 Catalog: Please Don't Touch Anything (works)
Closes #106
2026-10-09 07:55:49 -04:00
spoopyghosty0 d9d73b8aec Triage: Lepton 3.0.5's transient 'is not a running context' (printed while its container is still starting, then 'Boot complete!') no longer fails a launch test (container-not-started unless Boot complete!; VR4 ran at 72 fps on the dev Frame but was marked failed) 2026-10-09 07:55:24 -04:00
spoopyghosty0 48455b9b72 pose_time_fix: robust clock offset (the largest of the last ~2 s of xrWaitFrame samples; hitches made single samples dip by up to 2.5 s and pushed fixed times up to 2 s into the future), far-past requests located at the frame's display time, nothing moved before 8 samples. Headless survey of 54 games: no other game asks for head/views on the monotonic clock; UE4 OVRPlugin 1.89 games and The Room VR ask for XrTime ~0 every frame and work today, so the setting stays suggested for Unity built-in Oculus games only. BattleSisters on the Frame: every moved request now at 0.0 ms from the display time 2026-10-09 01:35:15 -04:00
spoopyghosty0 af798c0bf3 Catalog: BattleSisters entry parses again (my previous edit put colons into a plain YAML text block); written with catalog.to_yaml 2026-10-08 22:55:47 -04:00
spoopyghosty0 ed714c9d65 Catalog: BattleSisters plays with hands following the controllers (adapter pose_time_fix, owner's headset test) 2026-10-08 22:51:26 -04:00
spoopyghosty0 832b401630 Pose time fix (adapter pose_time_fix, pose_debug): OVRPlugin locates its "now" poses at the monotonic clock
BattleSisters' hands lagged behind the controllers (also with ovrp_hold_physics on or off). pose_debug=1 (new,
FrameBridge: per 5 s and per located space, the requested time minus the predicted display time) showed on the dev
Frame (SteamOS 0.4.5): XrTime runs 2.56 s ahead of CLOCK_MONOTONIC, the hand spaces were located at -2564 ms (the
monotonic "now" passed on as an XrTime: OVRPort's dispatcher converts XR_KHR_convert_timespec_time 1:1 and
FrameBridge's emulation is never asked) and the head at XrTime 0.1 s (~1450 xrLocateViews/xrLocateSpace per 5 s).

pose_time_fix (default off; suggested for Unity built-in OVRPlugin games): in xrLocateSpace(s)/xrLocateViews a time
nearer the monotonic clock than XrTime's "now" is moved by the offset measured at xrWaitFrame, one more than 0.5 s
before the display time goes to "now". Headless with it on: every far-past request moved, located times -9.8..0 ms
from the display time. Host unit test for the time rules; PLAYBOOK row.
2026-10-08 22:41:34 -04:00
Lucas-Mathieu 8814b1ce56 Merge remote-tracking branch 'origin/main' into fix/batman-cutscene-playback
# Conflicts:
#	src/frameport/pipeline.py
2026-10-08 17:49:12 +02:00
Lucas-Mathieu e9eedb2241 Merge remote-tracking branch 'origin/main' into fix/batman-cutscene-playback
# Conflicts:
#	artifacts/SHA256SUMS
2026-10-08 01:57:46 +02:00
Lucas-Mathieu d3a35c307a Fix Batman native cutscene decoding and stereo composition 2026-10-08 01:57:29 +02:00
597 changed files with 70062 additions and 2293 deletions

No files matched your search

+11 -12
View File
@@ -1,31 +1,30 @@
name: Problem report
description: A game or FramePort doesn't work (FramePort → "Report a problem…" fills this in and saves a diagnostics zip).
title: "[Problem] "
labels: ["bug"]
name: Game report
description: 'A game doesn''t run right on the Frame, or FramePort itself misbehaves (FramePort fills this in: "Report a problem…").'
title: "[Game report] "
labels: ["game-compatibility"]
body:
- type: markdown
attributes:
value: |
Please attach the **diagnostics zip** FramePort saved (drag it into the "Diagnostics" box below). It contains
logs, the recipe and device details with personal data removed — no game files. Without the app, it can be
read with `frameport diag inspect <zip>`.
Please attach the **diagnostics zip** FramePort saved: drag it into the "Diagnostics" box below. It holds logs
and device details with personal data removed, and no game files.
- type: input
id: game
attributes:
label: Game
description: Title — package id (version, engine / XR API); empty for app problems
description: Title and package name; leave empty for problems with FramePort itself
- type: textarea
id: description
attributes:
label: What happens?
description: What you did, what you expected, what you saw (in the headset, if it started)
description: What you did, what you expected and what happened
validations:
required: true
- type: textarea
id: findings
attributes:
label: Launch test / triage
description: Filled in by FramePort from the last launch test
label: Launch test
description: Filled in by FramePort
- type: textarea
id: recipe
attributes:
@@ -39,6 +38,6 @@ body:
id: logs
attributes:
label: Diagnostics
description: Drag the FramePort-diag-*.zip here (max 25 MB)
description: "Drag the FramePort-diag-*.zip here (max 25 MB). If the box won't take it, submit the report and attach the zip in a comment."
validations:
required: true
+7
View File
@@ -1 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Questions and ideas
url: https://github.com/spoopyghosty0/frameport/discussions
about: Setup help, how-to questions and feature ideas go to Discussions.
- name: Docs and FAQ
url: https://frameport.app/docs/
about: Install steps, game folder layouts and common questions.
+11 -11
View File
@@ -1,32 +1,32 @@
name: Working configuration
description: Submit a recipe that works on the Steam Frame (FramePort → game menu → "Share working config…" fills this in).
title: "[Working config] "
name: Working recipe
description: 'Share a recipe that works on the Steam Frame (FramePort fills this in: game menu → "Share working recipe…").'
title: "[Working recipe] "
labels: ["working-config"]
body:
- type: markdown
attributes:
value: |
Thanks! A maintainer reviews the recipe; once it's labelled `catalog-accepted`, a pull request adding it to
`catalog/games/` is opened automatically. Please don't paste game files or links to them.
Thanks! Once a maintainer accepts the recipe, it becomes built-in for everyone. Please don't share game files
or links to them.
- type: input
id: game
attributes:
label: Game
description: Title — package id (platform, version, engine / XR API)
description: Title and package name (e.g. Lucky's Tale — com.playful.LuckysTale)
validations:
required: true
- type: input
id: result
attributes:
label: Result
description: works or issues (+ the last headless launch test)
description: works or issues
validations:
required: true
- type: textarea
id: recipe
attributes:
label: Recipe
description: The catalog entry (catalog/games/<package>.yaml). Don't edit the package line.
description: Filled in by FramePort. Don't edit the package line.
render: yaml
validations:
required: true
@@ -34,16 +34,16 @@ body:
id: environment
attributes:
label: Environment
description: FramePort, tool and SteamOS versions
description: FramePort and SteamOS versions
- type: textarea
id: notes
attributes:
label: Notes
description: What you checked in the headset, known issues, anything unusual
description: What you checked in the headset and any known issues
- type: checkboxes
id: confirm
attributes:
label: Confirmation
options:
- label: I played the game in the headset with this recipe (headless launch tests can't check the picture).
- label: I played the game in the headset with this recipe.
required: true
+12
View File
@@ -150,6 +150,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
actions: write # starts the pages workflow
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10.2.0
@@ -185,6 +186,11 @@ jobs:
echo; cat packaging/release-footer.md; } > "$notes" # short: the update dialog shows these notes
gh release delete "$tag" --yes 2>/dev/null || true
gh release create "$tag" out/* packaging/FramePort-selfsigned.cer --title "FramePort ${tag}" --notes-file "$notes"
- name: Refresh the website's changelog (a release made with github.token starts no other workflow)
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run pages.yml --ref main
dev-release:
# "Run workflow" with dev ticked: replaces the rolling `dev` pre-release (never shown by the automatic update check;
@@ -194,6 +200,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
actions: write # starts the pages workflow
steps:
- uses: actions/checkout@v7
with:
@@ -225,3 +232,8 @@ jobs:
gh release delete dev --yes --cleanup-tag 2>/dev/null || true
gh release create dev out/* packaging/FramePort-selfsigned.cer --prerelease --target "$GITHUB_SHA" \
--title "FramePort dev build ${VERSION}" --notes-file "$notes"
- name: Refresh the website's changelog (a release made with github.token starts no other workflow)
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run pages.yml --ref main
+3 -2
View File
@@ -1,5 +1,5 @@
name: catalog-from-issue
# A maintainer adds the label `catalog-accepted` to a "Working configuration" issue → this opens a PR adding the
# A maintainer adds the label `catalog-accepted` to a "Working recipe" issue → this opens a PR adding the
# recipe to catalog/games/. Never runs for unlabelled issues; the issue text is only read by the validating script
# (never interpolated into shell commands). Needs Settings → Actions → "Allow GitHub Actions to create pull requests".
on:
@@ -40,7 +40,8 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "$branch"
git add "catalog/games/$PKG.yaml"
uv run python scripts/patch_docs.py # docs/PATCHES.md lists the catalog games per patch
git add "catalog/games/$PKG.yaml" docs/PATCHES.md site/src/data/patches.json
git commit -m "catalog: $PKG (from #$ISSUE)"
git push --force origin "$branch"
if ! gh pr view "$branch" >/dev/null 2>&1; then
+72
View File
@@ -0,0 +1,72 @@
# The project page (site/, Astro) on GitHub Pages: https://frameport.app/
# Rebuilt when the site, the docs it renders, the catalog or the showcase renders change on main, when a release is
# published, and once a day (contributors, releases and the games board stay fresh). The games board also refreshes
# itself from the catalog on main in the visitor's browser, so a catalog change shows before the next build.
# One-time setup: Settings → Pages → Source: GitHub Actions; custom domain frameport.app (kept in the settings, not a
# CNAME file: GitHub ignores that file for Actions deployments).
name: pages
on:
push:
branches: [main]
paths:
- 'site/**'
- 'docs/*.md'
- 'catalog/games/**'
- 'bootstrap/setup.sh'
- 'docs/images/**'
- 'docs/media/**'
- 'docs/badges/**'
- 'src/frameport/ui/icons/**'
- '.github/workflows/pages.yml'
release: # the changelog page shows every release (CI's own releases start this through workflow_dispatch)
types: [published]
schedule:
- cron: '17 5 * * *'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: site
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
cache-dependency-path: site/package-lock.json
- uses: actions/configure-pages@v6
- run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends ffmpeg # the video tiles' short previews
- run: npm ci
- run: npm test
- run: npm run build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # the contributors list (higher API limit)
# analytics (Settings → Secrets and variables → Actions → Variables); unset = none on the site
PUBLIC_CF_ANALYTICS_TOKEN: ${{ vars.CF_ANALYTICS_TOKEN }}
PUBLIC_GA_ID: ${{ vars.GA_MEASUREMENT_ID }}
- uses: actions/upload-pages-artifact@v5
with:
path: site/dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v5
+10
View File
@@ -20,3 +20,13 @@ CLAUDE.local.md
REPORT.md
.ruff_cache/
.claude/worktrees/
# project page (site/)
site/node_modules/
site/dist/
site/.astro/
site/public/media/
site/src/assets/media/
site/media-hq/
site/public/s
site/public/setup.sh
@@ -0,0 +1,51 @@
---
target: site
total_score: 28
max_score: 40
na_heuristics:
p0_count: 0
p1_count: 3
target_identity: "file:/mnt/d/Backup Data/Misc/emus/VR_META_QUEST/frameport/.claude/worktrees/impeccable-init/site/src/pages/index.astro"
target_fingerprint: "sha256:d6703ab52ed142060a06d3e159634338b66605ed54aa67cd8a942cfcc3918f65"
target_path: /mnt/d/Backup Data/Misc/emus/VR_META_QUEST/frameport/.claude/worktrees/impeccable-init/site/src/pages/index.astro
timestamp: 2026-10-12T14-19-41Z
slug: site-src-pages-index-astro
closed: true
---
# Critique: frameport.app (site/src: home, download, install, setup, docs, changelog)
Method: dual-agent (A: design review · B: detector + overlay)
## Design Health Score: 28/40 (Good)
| # | Heuristic | Score | Key Issue |
|---|---|---|---|
| 1 | Visibility of System Status | 3 | TransitLog rows dimmed to 1.8:1 until reached; read as disabled on a quick scroll |
| 2 | Match System / Real World | 3 | Home leads with VrApi, OVRPort patch ids, AXRB, a launch-test release card |
| 3 | User Control and Freedom | 3 | /download/ starts a download on arrival |
| 4 | Consistency and Standards | 2 | Nav "Install" -> /#install, /install/ is the link handler; "Install" vs "Setup"; three step-number styles |
| 5 | Error Prevention | 3 | Phone detection, checksums, 4-digit Allow |
| 6 | Recognition Rather Than Recall | 3 | No nav at all under 760px |
| 7 | Flexibility and Efficiency | 3 | Docs search "/", page keys, ?os= links |
| 8 | Aesthetic and Minimalist Design | 2 | 10.8k px home (13.7k mobile), app shown 3x in section 02, effect stack |
| 9 | Error Recovery | 3 | Install-link error is plain with a next step |
| 10 | Help and Documentation | 3 | Good docs; hub is a uniform card grid, "93 min read" |
## Design Specificity Verdict
Mixed. Content and hero are authored (real package -> game crossing the portal, TransitLog with real recipe chips, live CompatBoard incl. "Doesn't run", /setup Konsole preview). The ambient effect layer (interactions.ts: cursorLight, clickRipples, word-rise headings + pointer-lit gradient letters, scrambling labels, countUp, global .reveal fade-ups, heroScroll) and the section scaffolding (numbered "01 ·" mono eyebrows on 8 sections, statement + dim kicker headlines, Compare bento with icon tiles, uniform docs card hub, avatar wall, glows + warp field on /download/) are the template tells. Detector agrees: kicker-above-heading x9, gradient-text x2, icon-tile-stack, radial-spotlight-glow, dark-glow, bounce-easing, marquee; plus real a11y: --text-3 #717583 below 4.5:1 for small text (~18 hits), docs search placeholders 3.6-3.9:1, 10.5px text in Compare/.pip, heading-rhythm on 4 section heads, 114-char notice line. False positives: AppMock Roboto/nested cards/grid bg (imitates the app), Carousel lightbox img, purple palette (per-game hues), Crossing svg occlusion.
## Priority Issues
- [P1] Ambient effect stack reads as generated. Fix: keep motion only where something crosses (hero, TransitLog, setup Konsole, walkthrough); delete cursorLight, clickRipples, heading word-split + .lit, label scramble; limit .reveal. Command: /impeccable quieter
- [P1] No navigation on phones (Nav.astro:45-46 nav display:none under 760px, no menu). Fix: compact menu; phone CTA = send/copy link to PC. Command: /impeccable adapt
- [P1] Home too long and repetitive. Fix: keep the mock, fold carousel+videos, move "For game makers" to docs, release list to /changelog, shorten Compare. Command: /impeccable distill + /impeccable layout
- [P2] Walkthrough Downloads mock shows "?os=linux" + always ZIP/FramePort.exe (InstallWalkthrough.astro:272). Fix: map OS -> real asset names. Command: /impeccable polish
- [P2] No self-signed/SmartScreen reassurance at the download; "Install" overloaded; --text-3 contrast. Fix: one plain line per OS next to the SHA; rename nav item; raise --text-3 for small text. Command: /impeccable clarify + /impeccable onboard
## Persona Red Flags
Jordan: pipeline jargon right below "one click"; Install vs Setup; truncated walkthrough tabs. Riley: /install/ direct = error page; ?os=linux; 93 min read. Casey: no nav; three.js hero + warp canvas on a phone; 13.7k px. Reddit Frame owner: no "send to my PC" from home; curl|bash and Developer Mode first appear in section 05 without /setup's reassurance.
## Minor Observations
Hero meta line wraps; Compare tinted gradient cards break flat panels; green glow in Compare outside the palette; changelog version pills scroll without affordance; blockquote side border; --ease-back overshoot; marquee reduced-motion unverified.
## Questions to Consider
- If the crossing is the brand's one motion, why does every heading animate too?
- Who is the home page for after the hero: owner or tinkerer? Could the pipeline be "How it works ->"?
- Would sections named by what the visitor does beat numbered eyebrows?
+578
View File
@@ -0,0 +1,578 @@
{
"schemaVersion": 2,
"generatedAt": "2026-10-12T15:37:55+00:00",
"title": "Design System: FramePort",
"extensions": {
"colorMeta": {
"frame-orange": {
"role": "primary",
"displayName": "Frame Orange",
"canonical": "#FF8A1F",
"oklch": "oklch(74.8% 0.176 55.4)",
"tonalRamp": [
"#170600",
"#3B1A00",
"#633002",
"#8E4801",
"#C16300",
"#F58108",
"#FEB98A",
"#FEEADD"
]
},
"frame-orange-soft": {
"role": "primary",
"displayName": "Frame Orange Soft",
"canonical": "#3A2512",
"oklch": "oklch(28.6% 0.044 61.7)",
"tonalRamp": [
"#150700",
"#331F0C",
"#513A27",
"#705844",
"#947B65",
"#BA9F89",
"#E0C5AE",
"#FEEBDB"
]
},
"ember-ink": {
"role": "primary",
"displayName": "Ember Ink",
"canonical": "#160C03",
"oklch": "oklch(16.5% 0.027 67.8)",
"tonalRamp": [
"#130902",
"#2D2216",
"#493D31",
"#685B4E",
"#8B7E70",
"#B0A293",
"#D7C8B8",
"#FBECDC"
]
},
"pc-blue": {
"role": "secondary",
"displayName": "Pc Blue",
"canonical": "#3AA8FF",
"oklch": "oklch(71.0% 0.160 246.9)",
"tonalRamp": [
"#000C1B",
"#002643",
"#01426F",
"#04619F",
"#0485D7",
"#46ABFE",
"#9FD0FE",
"#E2F1FE"
]
},
"pc-blue-soft": {
"role": "secondary",
"displayName": "Pc Blue Soft",
"canonical": "#0F2A40",
"oklch": "oklch(27.6% 0.052 245.9)",
"tonalRamp": [
"#000C1A",
"#0B263C",
"#27425A",
"#45617A",
"#66849E",
"#8AA9C4",
"#AFCFEB",
"#E2F1FE"
]
},
"void": {
"role": "neutral",
"displayName": "Void",
"canonical": "#0D0E12",
"oklch": "oklch(16.5% 0.009 274.3)",
"tonalRamp": [
"#0A0B0F",
"#232428",
"#3E3F44",
"#5C5D63",
"#7E8085",
"#A3A4AA",
"#C9CAD0",
"#ECEEF4"
]
},
"sidebar-night": {
"role": "neutral",
"displayName": "Sidebar Night",
"canonical": "#111217",
"oklch": "oklch(18.3% 0.010 276.4)",
"tonalRamp": [
"#0A0B10",
"#232429",
"#3E3F45",
"#5C5D64",
"#7E8087",
"#A3A4AB",
"#C8CAD1",
"#ECEEF6"
]
},
"panel": {
"role": "neutral",
"displayName": "Panel",
"canonical": "#16181E",
"oklch": "oklch(21.0% 0.012 270.8)",
"tonalRamp": [
"#090B10",
"#22242A",
"#3D4046",
"#5B5D65",
"#7D8088",
"#A1A4AD",
"#C7CAD3",
"#EBEEF7"
]
},
"panel-raised": {
"role": "neutral",
"displayName": "Panel Raised",
"canonical": "#1D1F27",
"oklch": "oklch(24.1% 0.016 274.1)",
"tonalRamp": [
"#090B12",
"#22242C",
"#3D3F48",
"#5B5D67",
"#7D808A",
"#A1A4AF",
"#C7CAD5",
"#EBEEF9"
]
},
"panel-well": {
"role": "neutral",
"displayName": "Panel Well",
"canonical": "#252832",
"oklch": "oklch(27.8% 0.019 272.0)",
"tonalRamp": [
"#080B13",
"#21242D",
"#3C3F4A",
"#5A5D69",
"#7C808C",
"#A0A4B1",
"#C6CAD7",
"#EAEEFC"
]
},
"hairline": {
"role": "neutral",
"displayName": "Hairline",
"canonical": "#2A2D37",
"oklch": "oklch(29.8% 0.019 272.1)",
"tonalRamp": [
"#080B13",
"#21242D",
"#3C3F4A",
"#5A5D68",
"#7C808C",
"#A0A4B1",
"#C6CAD7",
"#EAEEFB"
]
},
"hairline-strong": {
"role": "neutral",
"displayName": "Hairline Strong",
"canonical": "#393D49",
"oklch": "oklch(36.1% 0.021 270.8)",
"tonalRamp": [
"#080B14",
"#20242F",
"#3B3F4B",
"#595D6A",
"#7B808D",
"#9FA4B3",
"#C5CAD9",
"#E9EEFD"
]
},
"text-bright": {
"role": "neutral",
"displayName": "Text Bright",
"canonical": "#EDEEF2",
"oklch": "oklch(94.9% 0.005 275.0)",
"tonalRamp": [
"#0A0B0D",
"#232427",
"#3F4043",
"#5D5D61",
"#7F8083",
"#A3A4A8",
"#C9CACE",
"#EDEEF2"
]
},
"text-dim": {
"role": "neutral",
"displayName": "Text Dim",
"canonical": "#A7ABB7",
"oklch": "oklch(74.2% 0.018 271.2)",
"tonalRamp": [
"#080B13",
"#21242D",
"#3C3F49",
"#5A5D68",
"#7C808B",
"#A0A4B0",
"#C6CAD7",
"#EAEEFB"
]
},
"text-faint": {
"role": "neutral",
"displayName": "Text Faint",
"canonical": "#717583",
"oklch": "oklch(56.4% 0.022 273.0)",
"tonalRamp": [
"#080A14",
"#20232F",
"#3C3F4C",
"#595D6A",
"#7C808E",
"#A0A4B3",
"#C5CAD9",
"#E9EEFE"
],
"siteValue": "#868a96"
},
"ok-green": {
"role": "status",
"displayName": "Ok Green",
"canonical": "#3DD68C",
"oklch": "oklch(77.9% 0.165 157.3)",
"tonalRamp": [
"#000F06",
"#002D18",
"#004D2D",
"#017043",
"#03995D",
"#1AC27A",
"#57EA9F",
"#C4FFDA"
]
},
"warn-amber": {
"role": "status",
"displayName": "Warn Amber",
"canonical": "#F2C94C",
"oklch": "oklch(85.0% 0.146 90.5)",
"tonalRamp": [
"#100A00",
"#2E2301",
"#4F3D02",
"#725B05",
"#9C7C01",
"#C79F05",
"#EFC648",
"#FFEEBE"
]
},
"error-coral": {
"role": "status",
"displayName": "Error Coral",
"canonical": "#FF6166",
"oklch": "oklch(69.8% 0.193 21.8)",
"tonalRamp": [
"#1F0002",
"#4B000A",
"#7C0118",
"#B10227",
"#DB3F49",
"#FE7173",
"#FEB5B1",
"#FFE8E7"
]
},
"info-cyan": {
"role": "status",
"displayName": "Info Cyan",
"canonical": "#5CD0E6",
"oklch": "oklch(79.9% 0.108 212.0)",
"tonalRamp": [
"#000E12",
"#012930",
"#014853",
"#066978",
"#0290A4",
"#3EB6CC",
"#6ADDF4",
"#D1F6FE"
]
}
},
"typographyMeta": {
"display": {
"displayName": "Display",
"purpose": "Site section headings and doc titles (Sora 800)."
},
"headline": {
"displayName": "Headline",
"purpose": "App page titles and the game hero title."
},
"title": {
"displayName": "Title",
"purpose": "App section and card headings."
},
"body": {
"displayName": "Body",
"purpose": "Running text (13px app, 16px site)."
},
"label": {
"displayName": "Label",
"purpose": "Pills, meta lines, counts."
},
"section-label": {
"displayName": "Section label",
"purpose": "Mono uppercase labels followed by the portal hairline."
}
},
"shadows": [
{
"name": "art-rest",
"value": "0 8px 22px 1px rgba(0,0,0,.55)",
"purpose": "Cover and shelf cards, screenshots."
},
{
"name": "art-hover",
"value": "0 16px 36px 2px rgba(0,0,0,.75), 0 4px 30px -2px rgba(255,138,31,.22)",
"purpose": "A pointed-at cover: deeper, with the orange glow."
},
{
"name": "float",
"value": "0 18px 40px rgba(0,0,0,.55)",
"purpose": "Menus, popovers, term cards, toasts."
},
{
"name": "portal-glow",
"value": "radial-gradient(85% 85% at 50% 37%, rgba(58,168,255,.10) 0%, rgba(255,138,31,.05) 45%, rgba(13,14,18,0) 100%)",
"purpose": "Soft light behind empty states and the site hero."
}
],
"gradients": [
{
"name": "portal",
"value": "linear-gradient(90deg, #3AA8FF, #FF8A1F)",
"purpose": "The seam: sidebar edge, selection fades (22 %), section hairlines, progress, link underlines."
}
],
"motion": [
{
"name": "ease-out",
"value": "cubic-bezier(0.2, 0.8, 0.2, 1)",
"purpose": "Default for state changes, reveals and the cover lift."
},
{
"name": "ease-settle",
"value": "cubic-bezier(0.25, 1, 0.5, 1)",
"purpose": "Presses, pops and arrivals: they land without overshoot."
},
{
"name": "view-fade",
"value": "150ms",
"purpose": "App route change (AnimatedSwitcher)."
},
{
"name": "quiet-reveal",
"value": "120ms opacity",
"purpose": "Row actions that appear on hover/focus."
},
{
"name": "cover-lift",
"value": "220ms ease-out; art zoom 600ms",
"purpose": "Cover card hover."
},
{
"name": "reduce-motion",
"value": "all of the above off",
"purpose": "ui.reduce_motion / prefers-reduced-motion."
}
],
"breakpoints": [
{
"name": "site-narrow",
"value": "760px",
"purpose": "Docs diagrams and file trees switch to scroll / stacked layout."
},
{
"name": "site-page",
"value": "1180px",
"purpose": "Site content column max width."
}
],
"themes": [
{
"name": "portal",
"dual": true,
"note": "Default."
},
{
"name": "portal_oled",
"dual": true,
"note": "True black BG/sidebar; panels lift off it."
},
{
"name": "original",
"dual": false,
"note": "Violet accent #8B7CFF; no gradient touches."
}
]
},
"components": [
{
"name": "Primary Button",
"kind": "button",
"refersTo": "button-primary",
"description": "The one main action of a view (Install on Frame, Play on Frame).",
"html": "<button class=\"ds-btn-primary\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M8 5v14l11-7z\"/></svg>Play on Frame</button>",
"css": ".ds-btn-primary{display:inline-flex;align-items:center;gap:8px;padding:12px 16px;border:0;border-radius:8px;background:var(--accent,#FF8A1F);color:var(--on-accent,#160C03);font:600 13px/18px 'Segoe UI',Roboto,sans-serif;cursor:pointer;transition:filter 220ms cubic-bezier(.2,.8,.2,1),transform 320ms cubic-bezier(.25,1,.5,1)}.ds-btn-primary:hover{filter:brightness(1.08)}.ds-btn-primary:active{transform:scale(.97)}.ds-btn-primary:focus-visible{outline:2px solid var(--accent,#FF8A1F);outline-offset:3px}"
},
{
"name": "Secondary Button",
"kind": "button",
"refersTo": "button-secondary",
"description": "Secondary actions: the blue portal outline next to the orange primary.",
"html": "<button class=\"ds-btn-secondary\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M3 12a9 9 0 0 1 15-6.7L21 8M21 3v5h-5M21 12a9 9 0 0 1-15 6.7L3 16M3 21v-5h5\"/></svg>Reinstall on Frame</button>",
"css": ".ds-btn-secondary{display:inline-flex;align-items:center;gap:8px;padding:12px 14px;border:1px solid rgba(58,168,255,.55);border-radius:8px;background:transparent;color:var(--text,#EDEEF2);font:600 13px/18px 'Segoe UI',Roboto,sans-serif;cursor:pointer;transition:background-color 220ms cubic-bezier(.2,.8,.2,1),transform 320ms cubic-bezier(.25,1,.5,1)}.ds-btn-secondary svg{color:var(--secondary,#3AA8FF)}.ds-btn-secondary:hover{background:rgba(58,168,255,.08)}.ds-btn-secondary:active{transform:scale(.97)}.ds-btn-secondary:focus-visible{outline:2px solid var(--accent,#FF8A1F);outline-offset:3px}"
},
{
"name": "Ghost Button",
"kind": "button",
"refersTo": "button-ghost",
"description": "Quiet actions: Close, Cancel, inline links.",
"html": "<button class=\"ds-btn-ghost\">Cancel</button>",
"css": ".ds-btn-ghost{padding:10px 12px;border:0;border-radius:8px;background:transparent;color:var(--text-2,#A7ABB7);font:600 13px/18px 'Segoe UI',Roboto,sans-serif;cursor:pointer;transition:color 200ms,background-color 200ms}.ds-btn-ghost:hover{color:var(--text,#EDEEF2);background:rgba(237,238,242,.06)}.ds-btn-ghost:focus-visible{outline:2px solid var(--accent,#FF8A1F);outline-offset:3px}"
},
{
"name": "Danger Button",
"kind": "button",
"refersTo": "button-danger",
"description": "The final destructive button of a confirm dialog (Uninstall).",
"html": "<button class=\"ds-btn-danger\">Uninstall</button>",
"css": ".ds-btn-danger{padding:12px 16px;border:0;border-radius:8px;background:var(--error,#FF6166);color:var(--on-accent,#160C03);font:600 13px/18px 'Segoe UI',Roboto,sans-serif;cursor:pointer;transition:filter 220ms,transform 320ms cubic-bezier(.25,1,.5,1)}.ds-btn-danger:hover{filter:brightness(1.08)}.ds-btn-danger:active{transform:scale(.97)}.ds-btn-danger:focus-visible{outline:2px solid var(--error,#FF6166);outline-offset:3px}"
},
{
"name": "Search Field",
"kind": "input",
"refersTo": "input",
"description": "Filled well, no border until focus, then orange.",
"html": "<label class=\"ds-search\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-4-4\"/></svg><input placeholder=\"Type to search games and tags\"></label>",
"css": ".ds-search{display:flex;align-items:center;gap:10px;width:260px;padding:8px 12px;border:1px solid transparent;border-radius:8px;background:var(--surface-3,#252832);color:var(--text-3,#717583);transition:border-color 160ms}.ds-search:focus-within{border-color:var(--accent,#FF8A1F)}.ds-search input{flex:1;min-width:0;border:0;outline:0;background:transparent;color:var(--text,#EDEEF2);font:13px 'Segoe UI',Roboto,sans-serif;caret-color:var(--accent,#FF8A1F)}.ds-search input::placeholder{color:var(--text-2,#A7ABB7)}"
},
{
"name": "Status Pills",
"kind": "chip",
"refersTo": "pill",
"description": "Color plus a word: status, install state, platform.",
"html": "<span class=\"ds-pill ds-ok\">Works</span> <span class=\"ds-pill ds-warn\">Works with issues</span> <span class=\"ds-pill ds-solid\">On Frame</span> <span class=\"ds-pill ds-pc\">PC VR</span>",
"css": ".ds-pill{display:inline-flex;align-items:center;gap:6px;padding:3px 9px 3px 8px;border-radius:20px;font:600 11px/16px 'Segoe UI',Roboto,sans-serif;white-space:nowrap}.ds-pill::before{content:'';width:7px;height:7px;border-radius:50%;background:currentColor}.ds-ok{color:#3DD68C;background:rgba(61,214,140,.12)}.ds-warn{color:#F2C94C;background:rgba(242,201,76,.12)}.ds-pc{color:#3AA8FF;background:rgba(58,168,255,.12)}.ds-solid{color:#160C03;background:#3DD68C}"
},
{
"name": "Card",
"kind": "card",
"refersTo": "card",
"description": "Flat panel with a hairline: no shadow.",
"html": "<div class=\"ds-card\"><div class=\"ds-card-h\">Where it's installed</div><div class=\"ds-card-b\">Steam Frame · Installed</div><div class=\"ds-card-m\">Last launch test: passed</div></div>",
"css": ".ds-card{width:320px;padding:16px;border:1px solid var(--border,#2A2D37);border-radius:12px;background:var(--surface,#16181E);font-family:'Segoe UI',Roboto,sans-serif;transition:background-color 160ms}.ds-card:hover{background:var(--surface-2,#1D1F27)}.ds-card-h{font:600 16px/1.3 inherit;color:var(--text,#EDEEF2);margin-bottom:8px}.ds-card-b{font-size:13px;color:#3DD68C}.ds-card-m{font-size:12px;color:var(--text-2,#A7ABB7);margin-top:4px}"
},
{
"name": "Sidebar Navigation",
"kind": "nav",
"refersTo": "nav-item-selected",
"description": "Selected row = the blue→orange portal fade; the sidebar ends in the portal seam.",
"html": "<nav class=\"ds-nav\"><a class=\"on\" href=\"#\">Library</a><a href=\"#\">Steam Frame <i></i></a><a href=\"#\">Files</a><a href=\"#\">Monitor</a></nav>",
"css": ".ds-nav{display:grid;gap:2px;width:220px;padding:12px;background:var(--sidebar,#111217);border-right:1px solid;border-image:linear-gradient(180deg,#3AA8FF,#FF8A1F) 1;font:500 14px/1 'Segoe UI',Roboto,sans-serif}.ds-nav a{display:flex;align-items:center;justify-content:space-between;padding:12px;border-radius:8px;color:var(--text-2,#A7ABB7);text-decoration:none;transition:background-color 160ms,color 160ms}.ds-nav a:hover{background:rgba(237,238,242,.05);color:var(--text,#EDEEF2)}.ds-nav a.on{background:linear-gradient(90deg,rgba(58,168,255,.22),rgba(255,138,31,.22));color:var(--text,#EDEEF2);font-weight:600}.ds-nav a:focus-visible{outline:2px solid var(--accent,#FF8A1F);outline-offset:2px}.ds-nav i{width:8px;height:8px;border-radius:50%;background:#3DD68C}"
},
{
"name": "Callout",
"kind": "custom",
"refersTo": "callout",
"description": "Guidance in a status color: 8 % fill, 35 % border.",
"html": "<div class=\"ds-callout\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#5CD0E6\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 11v5M12 8h.01\"/></svg><span>Large data set (~27 GiB) incl. all language packs.</span></div>",
"css": ".ds-callout{display:flex;gap:12px;align-items:flex-start;max-width:520px;padding:12px 14px;border:1px solid rgba(92,208,230,.35);border-radius:8px;background:rgba(92,208,230,.08);color:var(--text,#EDEEF2);font:13px/1.5 'Segoe UI',Roboto,sans-serif}.ds-callout svg{flex:none;margin-top:1px}"
},
{
"name": "Cover Card",
"kind": "custom",
"refersTo": "card",
"description": "Signature: art-led card that lifts, zooms its art and glows orange underneath on hover.",
"html": "<div class=\"ds-cover\" tabindex=\"0\"><div class=\"ds-art\"></div><span class=\"ds-tag\">Quest</span><div class=\"ds-cap\"><b>4XVR Video Player</b><span>Works</span></div></div>",
"css": ".ds-cover{position:relative;width:180px;height:290px;border-radius:12px;overflow:hidden;border:1px solid var(--border,#2A2D37);box-shadow:0 8px 22px 1px rgba(0,0,0,.55);transition:transform 220ms cubic-bezier(.2,.8,.2,1),box-shadow 220ms;font-family:'Segoe UI',Roboto,sans-serif}.ds-art{position:absolute;inset:0;background:radial-gradient(60% 40% at 40% 45%,#3AA8FF 0 40%,transparent 41%),radial-gradient(55% 38% at 62% 47%,#FF8A1F 0 40%,transparent 41%),linear-gradient(160deg,#1a2c5a,#0d0e12);transition:transform 600ms cubic-bezier(.2,.8,.2,1)}.ds-cover:hover,.ds-cover:focus-visible{transform:translateY(-4px) scale(1.02);box-shadow:0 16px 36px 2px rgba(0,0,0,.75),0 4px 30px -2px rgba(255,138,31,.22);outline:none}.ds-cover:hover .ds-art{transform:scale(1.07)}.ds-tag{position:absolute;left:10px;top:10px;padding:3px 9px;border-radius:20px;background:rgba(0,0,0,.62);color:#EDEEF2;font:600 11px/16px inherit}.ds-cap{position:absolute;inset:auto 0 0 0;padding:40px 12px 12px;background:linear-gradient(transparent,rgba(13,14,18,.92));display:grid;gap:4px}.ds-cap b{color:#EDEEF2;font-size:15px}.ds-cap span{color:#A7ABB7;font-size:12px}@media (prefers-reduced-motion:reduce){.ds-cover,.ds-art{transition:box-shadow 220ms}.ds-cover:hover{transform:none}.ds-cover:hover .ds-art{transform:none}}"
}
],
"narrative": {
"northStar": "The Portal Between",
"overview": "FramePort is about crossing. On one side is the PC, in blue; on the other is the Frame, in orange. Between them is the\nseam where a game passes through: a horizontal gradient from blue to orange, the same one the logo's tilted portal\nmakes. Everything else is dark space, so the crossing is what glows. The desktop app and frameport.app share this\nsystem with the same color values. The site states it outright (\"FramePort Portal, for the web\").\n\nSurfaces are flat, dark, tonal steps a few lightness points apart, with hairline borders, and the chrome stays quiet.\nColor comes from three places only: cover art, the orange that marks the one thing to do now, and the blue portal\ntouches that show where you are or what's moving. Density is comfortable desktop density, built for a mouse and a\nwindow you resize. The interface scales as a whole through one scale factor, never by reflowing into a phone layout.\n\nComponents are tactile and confident. A cover lifts and its art zooms when pointed at. A button settles when pressed.\nOn the website, motion is kept for things that cross: the hero's package-to-game crossing, the install log turning\nfrom PC rows into Frame rows, the download going through the portal. Nothing moves just because the pointer or the\nscroll passed by (no cursor light, no word-by-word headings, no scroll fade-ups).\nPlay sends two rings out of the button, blue then orange. A new view fades in. Every one of these motions disappears\nunder Reduce motion, and the static state must still read on its own.",
"keyCharacteristics": [
"Dark only: a near-black void (`void`) with three tonal panel steps above it.",
"Orange is the action, blue is the place: the portal dichotomy is the color doctrine.",
"The blue→orange gradient is the system's only decoration.",
"Artwork carries the color; the chrome stays neutral.",
"Flat at rest. Artwork and floating layers lift.",
"Motion is physical (lift, spring, ripple) and always optional."
],
"rules": [
{
"name": "The Portal Rule",
"body": "Orange is the Frame and the action; blue is the PC and the secondary. Don't swap them, and don't use a third hue for an action.",
"section": "colors"
},
{
"name": "The One Orange Rule",
"body": "A view has one filled orange button: the thing to do now. Other actions are blue outlines or ghosts.",
"section": "colors"
},
{
"name": "The Seam Rule",
"body": "The blue→orange gradient (`--portal`, `C.portal_gradient`) is the only decoration. It always runs left to right (or top to bottom), blue first. It appears on the sidebar edge, the selected nav item and segment, the wordmark, section-label hairlines, link underlines on the site and progress fills. Dual themes only.",
"section": "colors"
},
{
"name": "The Dark-Only Rule",
"body": "No light mode. Installed theme files whose background is too light, or whose text falls below 4.5:1 on a panel, are refused.",
"section": "colors"
},
{
"name": "The Scale-Token Rule",
"body": "In the app every size goes through `T.T_*` tokens or `T.px()`, so the user's UI scale (75–300 %) applies everywhere. A bare pixel number is a bug.",
"section": "typography"
},
{
"name": "The Sentence-Case Rule",
"body": "Headings and buttons use sentence case. Only the mono section labels and pill-sized overlays are uppercase.",
"section": "typography"
},
{
"name": "The Lift-Is-Earned Rule",
"body": "A panel never gets a shadow just to look important. If it isn't artwork and it doesn't float over other content, it's flat with a hairline.",
"section": "elevation"
}
],
"dos": [
"Do read colors as theme tokens when building (`T.ACCENT`, `var(--accent)`), so all three built-in themes and user theme files work.",
"Do keep exactly one filled orange action per view; make everything else blue outline or ghost.",
"Do put the blue→orange gradient only where something crosses or is selected: seams, selection fades, progress, link underlines.",
"Do let cover art carry the color and keep surrounding chrome neutral.",
"Do pair every status color with a word or glyph (pills, callouts, checklists).",
"Do give motion a static fallback: under Reduce motion (`ui.reduce_motion`, `prefers-reduced-motion`) the state must still read without the animation.",
"Do scale every app size through `T.T_*` / `T.px()` and give icon-only controls a tooltip."
],
"donts": [
"Don't build a light theme or a light surface; FramePort is dark only.",
"Don't swap the poles: no orange secondary buttons, no blue primary actions.",
"Don't add shadows to ordinary panels or cards; only artwork and floating layers lift.",
"Don't use Warn Amber or another orange-adjacent hue for a warning that could be mistaken for the accent.",
"Don't use the tilted portal silhouette as general ornament; it belongs to the logo, the wordmark and empty states.",
"Don't put image bytes into controls or recreate clickable controls on progress ticks; update them in place."
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

+1
View File
@@ -0,0 +1 @@
Baseline: the site as it was before the redesign steps (critique 28/40).
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

+1
View File
@@ -0,0 +1 @@
Quieter: removed the cursor light, click ripples, word-by-word headings and gradient letters, label scramble, count-ups, scroll fade-ups, the hero's scroll fly-away, film grain, icon hover tricks, the download warp field and breathing glow, Compare's tilt/pointer light/tinted cards, the contributor dock magnification; springs no longer overshoot.
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

+1
View File
@@ -0,0 +1 @@
Clarify: section labels name what you do there (See how it works, Try the app, See what runs, Compare the tools, Get started, What's new, Credits) instead of 01-07; plain headlines (What happens when you click Install on Frame; FramePort patches the Quest games that don't run as they are); the hero's 'For the Steam Frame' eyebrow removed; nav Install -> Get started, Compared -> Compare; /download/ explains why Windows and macOS warn (self-signed) and where the SHA-256 sums are.
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

@@ -0,0 +1 @@
Distill + layout: the app tour's 9-slide carousel and videos fold into one 'Screenshots and videos' disclosure; the game-maker block is one row (demo badge, one sentence, docs link; the HTML snippet lives in the docs); What's new is one line; Compare loses its icon tiles; calmer section padding, labels sit closer to their headings, the notice sits between sections at 72ch, bigger hit areas on the hero's platform links.
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+1
View File
@@ -0,0 +1 @@
Adapt: below 1120px the eight nav links move into a Menu panel (48px rows, Esc/outside click closes, a link closes it) instead of wrapping (1024) or vanishing (phones); on phones the download buttons say 'Get it on your PC' and lead to the download page's open-on-your-PC state; no three.js on data saver or 2G/3G (the CSS scene shows); the install log drops its technical detail column under 600px. Phone shots now use a phone user agent.
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+1
View File
@@ -0,0 +1 @@
Onboard: Get started step 3 (the setup line) now says what running it does: it finds FramePort on your PC and waits, nothing changes on the Frame until you click Allow, then Developer Mode turns on; no password, no root; with a link to read the script. New shot: desktop-walkthrough-step3.
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+1
View File
@@ -0,0 +1 @@
Polish: the walkthrough's Downloads scene shows each platform's real file and program (FramePort-linux-x64.tar.gz / TAR.GZ / FramePort; .app on macOS) instead of '?os=linux' and FramePort.exe; the site's dim text is #868a96 (4.77:1+, was 3.6-3.9:1) and placeholders use the secondary text colour; install-log stages not reached yet stay readable (only their icon fades); 10.5px labels are 11px; the docs blockquote loses its side bar; the changelog version is solid orange instead of gradient text; the notice reads at 64ch.
+128
View File
@@ -0,0 +1,128 @@
"""Specimen screenshots of frameport.app for the redesign steps, and the gallery that compares them.
python capture.py <NN-name> "<what changed>" [--url http://127.0.0.1:4391]
Writes <NN-name>/*.webp + note.txt next to this file and rebuilds index.html (every step, side by side per shot).
Math.random is seeded, so the hero picks the same game every run; the page is scrolled through first so
scroll-triggered reveals have played; Astro's dev toolbar is removed."""
import argparse
import html
import io
import time
from pathlib import Path
from PIL import Image
from playwright.sync_api import sync_playwright
HERE = Path(__file__).resolve().parent
SEED = """(() => { let a = 42; Math.random = () => { a |= 0; a = a + 0x6D2B79F5 | 0;
let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296; }; })();"""
# (name, path, viewport, full page?, scale of the saved image)
SHOTS = [
("desktop-home-fold", "/", (1440, 900), False, 1.0),
("desktop-home-full", "/", (1440, 900), True, 0.5),
("desktop-download", "/download/?os=windows", (1440, 900), True, 0.6),
("desktop-setup", "/setup/", (1440, 900), True, 0.6),
("desktop-docs", "/docs/", (1440, 900), True, 0.6),
("mobile-home-fold", "/", (390, 844), False, 1.0),
("mobile-home-full", "/", (390, 844), True, 0.6),
("mobile-download", "/download/?os=windows", (390, 844), True, 0.8),
("desktop-walkthrough-step3", "/#step3", (1440, 900), False, 1.0), # Get started, step 3 (the setup line)
("mobile-menu", "/#menu", (390, 844), False, 1.0), # the nav opened (from step 04 on: there was none before)
]
PHONE_UA = ("Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0 Mobile Safari/537.36")
def settle(page):
page.evaluate("""async () => {
for (let y = 0; y < document.body.scrollHeight; y += 400) { window.scrollTo(0, y); await new Promise(r => setTimeout(r, 250)); }
window.scrollTo(0, 0);
document.querySelectorAll('astro-dev-toolbar').forEach(e => e.remove());
// the settled look of every section (a full-page shot can't wait for each reveal)
document.querySelectorAll('.reveal').forEach(e => e.classList.add('in'));
const s = document.createElement('style');
s.textContent = '.reveal,.reveal *,.words .w{opacity:1!important;transform:none!important;transition:none!important}';
document.head.append(s);
}""")
time.sleep(1.5)
def capture(step: str, note: str, base: str) -> None:
out = HERE / step
out.mkdir(parents=True, exist_ok=True)
with sync_playwright() as p:
browser = p.chromium.launch()
for name, path, (w, h), full, scale in SHOTS:
ctx = browser.new_context(viewport={"width": w, "height": h}, device_scale_factor=1,
accept_downloads=False,
is_mobile=w < 500, has_touch=w < 500,
user_agent=PHONE_UA if w < 500 else None)
ctx.add_init_script(SEED)
page = ctx.new_page()
page.goto(base + path.replace("#menu", "").replace("#step3", ""), wait_until="networkidle")
time.sleep(1.0)
if path.endswith("#menu"):
if not page.is_visible(".menu-btn"):
ctx.close()
continue
page.tap(".menu-btn")
if path.endswith("#step3"):
page.click("[data-wt] [data-go='2']")
page.evaluate("document.querySelector('[data-wt]').scrollIntoView({block: 'center'})")
if full:
settle(page)
else:
page.evaluate("document.querySelectorAll('astro-dev-toolbar').forEach(e => e.remove())")
time.sleep(2.5) # the hero's first crossing
img = Image.open(io.BytesIO(page.screenshot(full_page=full)))
if scale != 1.0:
img = img.resize((round(img.width * scale), round(img.height * scale)), Image.LANCZOS)
img.save(out / f"{name}.webp", "WEBP", quality=72, method=6)
ctx.close()
browser.close()
(out / "note.txt").write_text(note.strip() + "\n", encoding="utf-8")
build_index()
def build_index() -> None:
steps = sorted(d for d in HERE.iterdir() if d.is_dir() and (d / "note.txt").exists())
names = [s[0] for s in SHOTS]
rows = []
for name in names:
cells = "".join(
f'<figure><a href="{s.name}/{name}.webp"><img loading="lazy" src="{s.name}/{name}.webp" alt=""></a>'
f"<figcaption>{html.escape(s.name)}</figcaption></figure>"
for s in steps if (s / f"{name}.webp").exists())
rows.append(f'<section><h2>{html.escape(name)}</h2><div class="row">{cells}</div></section>')
notes = "".join(f"<li><b>{html.escape(s.name)}</b> — {html.escape((s / 'note.txt').read_text(encoding='utf-8').strip())}</li>"
for s in steps)
(HERE / "index.html").write_text(f"""<!doctype html><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Site specimens</title>
<style>
:root {{ color-scheme: dark; }}
body {{ margin: 0; padding: 24px 16px; background: #0d0e12; color: #edeef2; font: 15px/1.5 "Segoe UI", Roboto, sans-serif; }}
h1 {{ font-size: 22px; margin: 0 0 8px; }} h2 {{ font-size: 15px; color: #a7abb7; margin: 32px 0 8px; font-weight: 600; }}
ul {{ max-width: 90ch; padding-left: 20px; color: #c9ccd6; }} b {{ color: #ff8a1f; }}
.row {{ display: flex; gap: 16px; overflow-x: auto; align-items: flex-start; padding-bottom: 8px; }}
figure {{ margin: 0; flex: 0 0 auto; width: min(560px, 85vw); }}
figure img {{ width: 100%; border: 1px solid #2a2d37; border-radius: 8px; display: block; }}
figcaption {{ font-size: 13px; color: #a7abb7; margin-top: 6px; }}
a {{ color: #3aa8ff; }}
</style>
<h1>frameport.app: step-by-step specimens</h1>
<p>Each row is one shot; each column is a step, oldest on the left. Click an image for full size.</p>
<ul>{notes}</ul>
{''.join(rows)}
""", encoding="utf-8")
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("step")
ap.add_argument("note")
ap.add_argument("--url", default="http://127.0.0.1:4391")
a = ap.parse_args()
capture(a.step, a.note, a.url)
+18
View File
@@ -0,0 +1,18 @@
<!doctype html><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Site specimens</title>
<style>
:root { color-scheme: dark; }
body { margin: 0; padding: 24px 16px; background: #0d0e12; color: #edeef2; font: 15px/1.5 "Segoe UI", Roboto, sans-serif; }
h1 { font-size: 22px; margin: 0 0 8px; } h2 { font-size: 15px; color: #a7abb7; margin: 32px 0 8px; font-weight: 600; }
ul { max-width: 90ch; padding-left: 20px; color: #c9ccd6; } b { color: #ff8a1f; }
.row { display: flex; gap: 16px; overflow-x: auto; align-items: flex-start; padding-bottom: 8px; }
figure { margin: 0; flex: 0 0 auto; width: min(560px, 85vw); }
figure img { width: 100%; border: 1px solid #2a2d37; border-radius: 8px; display: block; }
figcaption { font-size: 13px; color: #a7abb7; margin-top: 6px; }
a { color: #3aa8ff; }
</style>
<h1>frameport.app: step-by-step specimens</h1>
<p>Each row is one shot; each column is a step, oldest on the left. Click an image for full size.</p>
<ul><li><b>00-before</b> — Baseline: the site as it was before the redesign steps (critique 28/40).</li><li><b>01-quieter</b> — Quieter: removed the cursor light, click ripples, word-by-word headings and gradient letters, label scramble, count-ups, scroll fade-ups, the hero&#x27;s scroll fly-away, film grain, icon hover tricks, the download warp field and breathing glow, Compare&#x27;s tilt/pointer light/tinted cards, the contributor dock magnification; springs no longer overshoot.</li><li><b>02-clarify</b> — Clarify: section labels name what you do there (See how it works, Try the app, See what runs, Compare the tools, Get started, What&#x27;s new, Credits) instead of 01-07; plain headlines (What happens when you click Install on Frame; FramePort patches the Quest games that don&#x27;t run as they are); the hero&#x27;s &#x27;For the Steam Frame&#x27; eyebrow removed; nav Install -&gt; Get started, Compared -&gt; Compare; /download/ explains why Windows and macOS warn (self-signed) and where the SHA-256 sums are.</li><li><b>03-distill-layout</b> — Distill + layout: the app tour&#x27;s 9-slide carousel and videos fold into one &#x27;Screenshots and videos&#x27; disclosure; the game-maker block is one row (demo badge, one sentence, docs link; the HTML snippet lives in the docs); What&#x27;s new is one line; Compare loses its icon tiles; calmer section padding, labels sit closer to their headings, the notice sits between sections at 72ch, bigger hit areas on the hero&#x27;s platform links.</li><li><b>04-adapt</b> — Adapt: below 1120px the eight nav links move into a Menu panel (48px rows, Esc/outside click closes, a link closes it) instead of wrapping (1024) or vanishing (phones); on phones the download buttons say &#x27;Get it on your PC&#x27; and lead to the download page&#x27;s open-on-your-PC state; no three.js on data saver or 2G/3G (the CSS scene shows); the install log drops its technical detail column under 600px. Phone shots now use a phone user agent.</li><li><b>05-onboard</b> — Onboard: Get started step 3 (the setup line) now says what running it does: it finds FramePort on your PC and waits, nothing changes on the Frame until you click Allow, then Developer Mode turns on; no password, no root; with a link to read the script. New shot: desktop-walkthrough-step3.</li><li><b>06-polish</b> — Polish: the walkthrough&#x27;s Downloads scene shows each platform&#x27;s real file and program (FramePort-linux-x64.tar.gz / TAR.GZ / FramePort; .app on macOS) instead of &#x27;?os=linux&#x27; and FramePort.exe; the site&#x27;s dim text is #868a96 (4.77:1+, was 3.6-3.9:1) and placeholders use the secondary text colour; install-log stages not reached yet stay readable (only their icon fades); 10.5px labels are 11px; the docs blockquote loses its side bar; the changelog version is solid orange instead of gradient text; the notice reads at 64ch.</li></ul>
<section><h2>desktop-home-fold</h2><div class="row"><figure><a href="00-before/desktop-home-fold.webp"><img loading="lazy" src="00-before/desktop-home-fold.webp" alt=""></a><figcaption>00-before</figcaption></figure><figure><a href="01-quieter/desktop-home-fold.webp"><img loading="lazy" src="01-quieter/desktop-home-fold.webp" alt=""></a><figcaption>01-quieter</figcaption></figure><figure><a href="02-clarify/desktop-home-fold.webp"><img loading="lazy" src="02-clarify/desktop-home-fold.webp" alt=""></a><figcaption>02-clarify</figcaption></figure><figure><a href="03-distill-layout/desktop-home-fold.webp"><img loading="lazy" src="03-distill-layout/desktop-home-fold.webp" alt=""></a><figcaption>03-distill-layout</figcaption></figure><figure><a href="04-adapt/desktop-home-fold.webp"><img loading="lazy" src="04-adapt/desktop-home-fold.webp" alt=""></a><figcaption>04-adapt</figcaption></figure><figure><a href="05-onboard/desktop-home-fold.webp"><img loading="lazy" src="05-onboard/desktop-home-fold.webp" alt=""></a><figcaption>05-onboard</figcaption></figure><figure><a href="06-polish/desktop-home-fold.webp"><img loading="lazy" src="06-polish/desktop-home-fold.webp" alt=""></a><figcaption>06-polish</figcaption></figure></div></section><section><h2>desktop-home-full</h2><div class="row"><figure><a href="00-before/desktop-home-full.webp"><img loading="lazy" src="00-before/desktop-home-full.webp" alt=""></a><figcaption>00-before</figcaption></figure><figure><a href="01-quieter/desktop-home-full.webp"><img loading="lazy" src="01-quieter/desktop-home-full.webp" alt=""></a><figcaption>01-quieter</figcaption></figure><figure><a href="02-clarify/desktop-home-full.webp"><img loading="lazy" src="02-clarify/desktop-home-full.webp" alt=""></a><figcaption>02-clarify</figcaption></figure><figure><a href="03-distill-layout/desktop-home-full.webp"><img loading="lazy" src="03-distill-layout/desktop-home-full.webp" alt=""></a><figcaption>03-distill-layout</figcaption></figure><figure><a href="04-adapt/desktop-home-full.webp"><img loading="lazy" src="04-adapt/desktop-home-full.webp" alt=""></a><figcaption>04-adapt</figcaption></figure><figure><a href="05-onboard/desktop-home-full.webp"><img loading="lazy" src="05-onboard/desktop-home-full.webp" alt=""></a><figcaption>05-onboard</figcaption></figure><figure><a href="06-polish/desktop-home-full.webp"><img loading="lazy" src="06-polish/desktop-home-full.webp" alt=""></a><figcaption>06-polish</figcaption></figure></div></section><section><h2>desktop-download</h2><div class="row"><figure><a href="00-before/desktop-download.webp"><img loading="lazy" src="00-before/desktop-download.webp" alt=""></a><figcaption>00-before</figcaption></figure><figure><a href="01-quieter/desktop-download.webp"><img loading="lazy" src="01-quieter/desktop-download.webp" alt=""></a><figcaption>01-quieter</figcaption></figure><figure><a href="02-clarify/desktop-download.webp"><img loading="lazy" src="02-clarify/desktop-download.webp" alt=""></a><figcaption>02-clarify</figcaption></figure><figure><a href="03-distill-layout/desktop-download.webp"><img loading="lazy" src="03-distill-layout/desktop-download.webp" alt=""></a><figcaption>03-distill-layout</figcaption></figure><figure><a href="04-adapt/desktop-download.webp"><img loading="lazy" src="04-adapt/desktop-download.webp" alt=""></a><figcaption>04-adapt</figcaption></figure><figure><a href="05-onboard/desktop-download.webp"><img loading="lazy" src="05-onboard/desktop-download.webp" alt=""></a><figcaption>05-onboard</figcaption></figure><figure><a href="06-polish/desktop-download.webp"><img loading="lazy" src="06-polish/desktop-download.webp" alt=""></a><figcaption>06-polish</figcaption></figure></div></section><section><h2>desktop-setup</h2><div class="row"><figure><a href="00-before/desktop-setup.webp"><img loading="lazy" src="00-before/desktop-setup.webp" alt=""></a><figcaption>00-before</figcaption></figure><figure><a href="01-quieter/desktop-setup.webp"><img loading="lazy" src="01-quieter/desktop-setup.webp" alt=""></a><figcaption>01-quieter</figcaption></figure><figure><a href="02-clarify/desktop-setup.webp"><img loading="lazy" src="02-clarify/desktop-setup.webp" alt=""></a><figcaption>02-clarify</figcaption></figure><figure><a href="03-distill-layout/desktop-setup.webp"><img loading="lazy" src="03-distill-layout/desktop-setup.webp" alt=""></a><figcaption>03-distill-layout</figcaption></figure><figure><a href="04-adapt/desktop-setup.webp"><img loading="lazy" src="04-adapt/desktop-setup.webp" alt=""></a><figcaption>04-adapt</figcaption></figure><figure><a href="05-onboard/desktop-setup.webp"><img loading="lazy" src="05-onboard/desktop-setup.webp" alt=""></a><figcaption>05-onboard</figcaption></figure><figure><a href="06-polish/desktop-setup.webp"><img loading="lazy" src="06-polish/desktop-setup.webp" alt=""></a><figcaption>06-polish</figcaption></figure></div></section><section><h2>desktop-docs</h2><div class="row"><figure><a href="00-before/desktop-docs.webLine truncated
+255
View File
@@ -0,0 +1,255 @@
<title>frameport.app Redesign</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<style>
/* Layout: one column; a comparer (two steps, one shot) leads, the step list follows. FramePort's own Portal look:
dark only (the product is dark only), orange = the action and the current choice, blue = the secondary and the PC. */
:root {
color-scheme: dark;
--bg: #0d0e12;
--panel: #16181e;
--raised: #1d1f27;
--well: #252832;
--line: #2a2d37;
--line-strong: #393d49;
--text: #edeef2;
--dim: #a7abb7;
--faint: #868a96;
--orange: #ff8a1f;
--orange-ink: #160c03;
--blue: #3aa8ff;
--blue-line: rgba(58, 168, 255, .55);
--ok: #3dd68c;
--portal: linear-gradient(90deg, #3aa8ff, #ff8a1f);
--fade: linear-gradient(90deg, rgba(58, 168, 255, .22), rgba(255, 138, 31, .22));
--display: Sora, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--sans: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--mono: ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace;
--r: 12px; --r-sm: 8px;
}
* { box-sizing: border-box; }
body { background: var(--bg); color: var(--text); font: 400 15px/1.6 var(--sans); }
.page { max-width: 1240px; margin: 0 auto; padding-inline: clamp(16px, 4vw, 40px); padding-block: 40px 72px; display: grid; gap: 48px; }
a { color: var(--orange); }
:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
h1, h2 { font-family: var(--display); margin: 0; text-wrap: balance; letter-spacing: -0.02em; }
h1 { font-size: clamp(30px, 4.6vw, 48px); line-height: 1.08; font-weight: 800; }
h2 { font-size: 22px; font-weight: 700; }
p { margin: 0; text-wrap: pretty; }
code { font-family: var(--mono); font-size: .9em; }
/* ---- head */
.head { display: grid; gap: 18px; }
.site { display: flex; align-items: center; gap: 10px; font: 600 13px/1 var(--mono); color: var(--dim); }
.site::after { content: ""; flex: 1; height: 2px; background: var(--portal); opacity: .7; }
.lead { color: var(--dim); max-width: 64ch; font-size: 16.5px; }
.facts { display: flex; flex-wrap: wrap; gap: 10px; margin: 0; padding: 0; list-style: none; }
.facts li { display: grid; gap: 2px; padding: 10px 14px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--panel); min-width: 0; }
.facts dt, .facts .k { font-size: 12.5px; color: var(--dim); }
.facts .v { font: 600 15px/1.3 var(--mono); font-variant-numeric: tabular-nums; }
.facts .v s { color: var(--faint); text-decoration-thickness: 1px; }
.facts .v b { color: var(--ok); font-weight: 600; }
/* ---- comparer */
.cmp { display: grid; gap: 16px; }
.bar { display: flex; flex-wrap: wrap; align-items: end; gap: 12px 20px; }
.pick { display: grid; gap: 4px; }
.pick label { font-size: 12.5px; color: var(--dim); }
select { appearance: none; min-height: 40px; padding: 0 34px 0 12px; border-radius: var(--r-sm); border: 1px solid var(--line-strong);
background: var(--well) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23a7abb7' stroke-width='1.6'/%3E%3C/svg%3E") no-repeat right 12px center;
color: var(--text); font: 500 14px/1 var(--sans); cursor: pointer; }
.arrow { color: var(--faint); padding-bottom: 10px; }
.seg { display: inline-flex; padding: 3px; border-radius: 999px; border: 1px solid var(--line-strong); margin-left: auto; }
.seg button { all: unset; padding: 7px 14px; border-radius: 999px; font: 600 13px/1 var(--sans); color: var(--dim); cursor: pointer; }
.seg button[aria-pressed="true"] { background: var(--fade); color: var(--text); }
.seg button:focus-visible { outline: 2px solid var(--orange); outline-offset: 1px; }
.shots { display: grid; gap: 8px; }
.grp { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.grp .dev { width: 64px; font: 600 11px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--faint); }
.chip { all: unset; padding: 7px 12px; border-radius: 999px; border: 1px solid var(--line-strong); font: 500 13px/1 var(--sans); color: var(--dim); cursor: pointer; }
.chip:hover { color: var(--text); }
.chip[aria-pressed="true"] { border-color: transparent; background: var(--fade); color: var(--text); }
.chip:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.stage { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; }
.pane { display: grid; gap: 8px; min-width: 0; }
.cap { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; font-size: 13.5px; }
.cap b { font-weight: 600; }
.cap code { color: var(--faint); }
.cap .tag { font: 600 11px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.pane.a .tag { color: var(--blue); }
.pane.b .tag { color: var(--orange); }
.view { max-height: 76vh; overflow: auto; border: 1px solid var(--line); border-radius: var(--r); background: #050608; overscroll-behavior: contain; }
.view img { display: block; width: 100%; height: auto; }
.phone .view img { max-width: 390px; margin-inline: auto; }
.none { display: grid; place-items: center; min-height: 240px; padding: 24px; text-align: center; color: var(--dim); font-size: 14px; }
.swipe { position: relative; touch-action: pan-y; cursor: ew-resize; }
.swipe .after { position: absolute; inset: 0; clip-path: inset(0 0 0 var(--x, 50%)); }
.swipe .knob { position: absolute; top: 0; bottom: 0; left: var(--x, 50%); width: 2px; margin-left: -1px; background: var(--portal); box-shadow: 0 0 0 1px rgba(0, 0, 0, .5); pointer-events: none; }
.swipe-bar { display: flex; align-items: center; gap: 12px; font-size: 13px; color: var(--dim); }
.swipe-bar input { flex: 1; accent-color: var(--orange); }
.swipe-bar .l { color: var(--blue); } .swipe-bar .r { color: var(--orange); }
/* ---- steps */
.steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.step { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 20px; align-items: start; padding: 16px; border: 1px solid var(--line);
border-radius: var(--r); background: var(--panel); }
.step img { width: 100%; height: auto; border-radius: var(--r-sm); border: 1px solid var(--line); display: block; }
.step .body { display: grid; gap: 8px; min-width: 0; }
.step .top { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; }
.step .n { font: 600 13px/1 var(--mono); color: var(--faint); font-variant-numeric: tabular-nums; }
.step h3 { margin: 0; font: 700 18px/1.25 var(--display); }
.step .top code { color: var(--faint); font-size: 12.5px; }
.step p { color: var(--dim); max-width: 75ch; }
.step button { all: unset; justify-self: start; padding: 8px 14px; border-radius: var(--r-sm); border: 1px solid var(--blue-line); color: var(--text);
font: 600 13.5px/1 var(--sans); cursor: pointer; }
.step button:hover { background: rgba(58, 168, 255, .08); }
.step button:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.note { font-size: 13px; color: var(--faint); }
@media (max-width: 760px) {
.stage { grid-template-columns: minmax(0, 1fr); }
.view { max-height: 62vh; }
.seg { margin-left: 0; }
.step { grid-template-columns: minmax(0, 1fr); }
.step img { max-width: 360px; }
}
</style>
<main class="page">
<header class="head">
<p class="site">frameport.app</p>
<h1>The site, one step at a time</h1>
<p class="lead">Six passes after the 2026-10-12 critique (28/40), each committed on its own. Pick two steps and a
shot to compare them; the screenshots come from the same capture script each time.</p>
<ul class="facts" aria-label="Before and after">
<li><span class="k">Detector findings, home (desktop)</span><span class="v"><s>90</s> → <b>46</b></span></li>
<li><span class="k">Detector findings, home (phone)</span><span class="v"><s>59</s> → <b>33</b></span></li>
<li><span class="k">Home page height</span><span class="v"><s>10.8k px</s> → <b>8.4k px</b></span></li>
<li><span class="k">Dim text on cards</span><span class="v"><s>3.87:1</s> → <b>5.15:1</b></span></li>
<li><span class="k">Navigation on phones</span><span class="v"><s>none</s> → <b>Menu</b></span></li>
</ul>
</header>
<section class="cmp" id="compare" aria-labelledby="cmp-h">
<h2 id="cmp-h">Compare</h2>
<div class="bar">
<div class="pick"><label for="sel-a">Before</label><select id="sel-a"></select></div>
<span class="arrow" aria-hidden="true">→</span>
<div class="pick"><label for="sel-b">After</label><select id="sel-b"></select></div>
<div class="seg" role="group" aria-label="View">
<button type="button" id="mode-side" aria-pressed="true">Side by side</button>
<button type="button" id="mode-swipe" aria-pressed="false">Swipe</button>
</div>
</div>
<div class="shots" id="shots" role="group" aria-label="Shot"></div>
<div id="stage"></div>
</section>
<section aria-labelledby="steps-h" style="display:grid;gap:16px">
<h2 id="steps-h">The steps</h2>
<ol class="steps" id="steps"></ol>
<p class="note">Each step is one commit on the branch <code>worktree-impeccable-init</code>. Shots are seeded so the
hero shows the same game; scroll-reveal sections are shown settled.</p>
</section>
</main>
<script>
const DATA = {"steps": [{"id": "00-before", "name": "Before", "commit": "daa71a1", "note": "The site as the critique found it: 28/40, with a cursor light, word-by-word headings, scrambling labels, scroll fade-ups and no navigation on phones.", "shots": ["desktop-home-fold", "desktop-home-full", "desktop-download", "desktop-setup", "desktop-docs", "mobile-home-fold", "mobile-home-full", "mobile-download"]}, {"id": "01-quieter", "name": "Quieter", "commit": "540c719", "note": "Removed the ambient effects (cursor light, click ripples, rising and lit headings, label scramble, count-ups, scroll fade-ups, film grain, the download warp field, Compare's tilt, the avatar magnification). Motion stays where something crosses: the hero, the install log, the download portal.", "shots": ["desktop-home-fold", "desktop-home-full", "desktop-download", "desktop-setup", "desktop-docs", "mobile-home-fold", "mobile-home-full", "mobile-download"]}, {"id": "02-clarify", "name": "Clarify", "commit": "af6117a", "note": "Section labels name what you do there instead of 01-07. Plain headlines. The nav's \"Install\" became \"Get started\". The download page says why Windows and macOS warn.", "shots": ["desktop-home-fold", "desktop-home-full", "desktop-download", "desktop-setup", "desktop-docs", "mobile-home-fold", "mobile-home-full", "mobile-download"]}, {"id": "03-distill-layout", "name": "Distill + layout", "commit": "72f94d1", "note": "Screenshots and videos fold behind one disclosure, the game-maker block is one row, What's new is one line, Compare loses its icon tiles. Calmer spacing. The home page went from 10.8k to 8.4k px.", "shots": ["desktop-home-fold", "desktop-home-full", "desktop-download", "desktop-setup", "desktop-docs", "mobile-home-fold", "mobile-home-full", "mobile-download"]}, {"id": "04-adapt", "name": "Adapt", "commit": "58a936c", "note": "Below 1120px the nav links move into a Menu panel. Phones get \"Get it on your PC\". No 3D download on slow connections. Phone shots use a phone browser from here on.", "shots": ["desktop-home-fold", "desktop-home-full", "desktop-download", "desktop-setup", "desktop-docs", "mobile-home-fold", "mobile-home-full", "mobile-menu", "mobile-download"]}, {"id": "05-onboard", "name": "Onboard", "commit": "fc47dab", "note": "The setup-line step says nothing changes on the Frame until you click Allow, and links the script.", "shots": ["desktop-home-fold", "desktop-home-full", "desktop-walkthrough-step3", "desktop-download", "desktop-setup", "desktop-docs", "mobile-home-fold", "mobile-home-full", "mobile-menu", "mobile-download"]}, {"id": "06-polish", "name": "Polish", "commit": "93b62fa", "note": "Real file names per platform in the walkthrough, readable dim text (4.77:1, was 3.6:1), themed placeholders, no labels under 11px, no gradient text or side-bar quotes left.", "shots": ["desktop-home-fold", "desktop-home-full", "desktop-walkthrough-step3", "desktop-download", "desktop-setup", "desktop-docs", "mobile-home-fold", "mobile-home-full", "mobile-menu", "mobile-download"]}], "shots": [{"id": "desktop-home-fold", "label": "Home, first screen", "device": "desktop"}, {"id": "desktop-home-full", "label": "Home, whole page", "device": "desktop"}, {"id": "desktop-walkthrough-step3", "label": "Get started, step 3", "device": "desktop"}, {"id": "desktop-download", "label": "Download page", "device": "desktop"}, {"id": "desktop-setup", "label": "Setup page", "device": "desktop"}, {"id": "desktop-docs", "label": "Docs", "device": "desktop"}, {"id": "mobile-home-fold", "label": "Home, first screen", "device": "phone"}, {"id": "mobile-home-full", "label": "Home, whole page", "device": "phone"}, {"id": "mobile-menu", "label": "Menu", "device": "phone"}, {"id": "mobile-download", "label": "Download page", "device": "phone"}]};
const $ = (s) => document.querySelector(s);
const step = (id) => DATA.steps.find((s) => s.id === id);
const shot = (id) => DATA.shots.find((s) => s.id === id);
const WHY = {
'mobile-menu': 'There was no menu on phones before Adapt (04).',
'desktop-walkthrough-step3': 'This shot was added with Onboard (05).',
};
const state = { a: DATA.steps[0].id, b: DATA.steps[DATA.steps.length - 1].id, shot: 'desktop-home-fold', mode: 'side', x: 50 };
function fillSelects() {
for (const sel of [$('#sel-a'), $('#sel-b')]) {
sel.innerHTML = DATA.steps.map((s, i) => `<option value="${s.id}">${String(i).padStart(2, '0')} · ${s.name}</option>`).join('');
}
$('#sel-a').value = state.a; $('#sel-b').value = state.b;
$('#sel-a').addEventListener('change', (e) => { state.a = e.target.value; render(); });
$('#sel-b').addEventListener('change', (e) => { state.b = e.target.value; render(); });
}
function renderShots() {
const devices = [...new Set(DATA.shots.map((s) => s.device))];
$('#shots').innerHTML = devices.map((d) => `<div class="grp"><span class="dev">${d}</span>${DATA.shots.filter((s) => s.device === d)
.map((s) => `<button type="button" class="chip" data-shot="${s.id}" aria-pressed="${s.id === state.shot}">${s.label}</button>`).join('')}</div>`).join('');
$('#shots').querySelectorAll('.chip').forEach((b) => b.addEventListener('click', () => { state.shot = b.dataset.shot; render(); }));
}
const src = (stepId) => step(stepId).shots.includes(state.shot) ? `${stepId}/${state.shot}.webp` : null;
const label = (stepId) => { const i = DATA.steps.findIndex((s) => s.id === stepId); return `${String(i).padStart(2, '0')} · ${step(stepId).name}`; };
const missing = (stepId) => `<div class="none">${WHY[state.shot] ?? `No ${shot(state.shot).label.toLowerCase()} shot in ${step(stepId).name}.`}</div>`;
const caption = (cls, tag, stepId) => `<div class="cap"><span class="tag">${tag}</span><b>${label(stepId)}</b><code>${step(stepId).commit}</code></div>`;
function render() {
$('#shots').querySelectorAll('.chip').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.shot === state.shot)));
$('#mode-side').setAttribute('aria-pressed', String(state.mode === 'side'));
$('#mode-swipe').setAttribute('aria-pressed', String(state.mode === 'swipe'));
const phone = shot(state.shot).device === 'phone';
const a = src(state.a), b = src(state.b);
const stage = $('#stage');
stage.className = phone ? 'phone' : '';
if (state.mode === 'swipe' && a && b) {
stage.innerHTML = `<div class="pane">
<div class="cap"><span class="tag" style="color:var(--blue)">Left</span><b>${label(state.a)}</b>
<span aria-hidden="true">·</span><span class="tag" style="color:var(--orange)">Right</span><b>${label(state.b)}</b></div>
<div class="view"><div class="swipe" style="--x:${state.x}%">
<img src="${a}" alt="${shot(state.shot).label}, ${label(state.a)}">
<div class="after"><img src="${b}" alt="${shot(state.shot).label}, ${label(state.b)}"></div>
<i class="knob"></i></div></div>
<div class="swipe-bar"><span class="l">${step(state.a).name}</span>
<input type="range" id="swipe-x" min="0" max="100" value="${state.x}" aria-label="Divider position">
<span class="r">${step(state.b).name}</span></div></div>`;
const sw = stage.querySelector('.swipe'), range = stage.querySelector('#swipe-x');
const set = (x) => { state.x = Math.max(0, Math.min(100, x)); sw.style.setProperty('--x', `${state.x}%`); range.value = state.x; };
range.addEventListener('input', () => set(Number(range.value)));
let drag = false;
const at = (e) => { const r = sw.getBoundingClientRect(); set(((e.clientX - r.left) / r.width) * 100); };
sw.addEventListener('pointerdown', (e) => { if (e.pointerType === 'touch') return; drag = true; sw.setPointerCapture(e.pointerId); at(e); });
sw.addEventListener('pointermove', (e) => { if (drag) at(e); });
sw.addEventListener('pointerup', () => { drag = false; });
return;
}
stage.innerHTML = `<div class="stage">
<div class="pane a">${caption('a', 'Before', state.a)}<div class="view">${a ? `<img src="${a}" alt="${shot(state.shot).label}, ${label(state.a)}">` : missing(state.a)}</div></div>
<div class="pane b">${caption('b', 'After', state.b)}<div class="view">${b ? `<img src="${b}" alt="${shot(state.shot).label}, ${label(state.b)}">` : missing(state.b)}</div></div>
</div>`;
// the two views scroll together (by proportion: the pages differ in length)
const views = [...stage.querySelectorAll('.view')];
let lock = false;
views.forEach((v, i) => v.addEventListener('scroll', () => {
if (lock) return;
const o = views[1 - i], max = v.scrollHeight - v.clientHeight, omax = o.scrollHeight - o.clientHeight;
if (max <= 0 || omax <= 0) return;
lock = true; o.scrollTop = (v.scrollTop / max) * omax; requestAnimationFrame(() => { lock = false; });
}, { passive: true }));
}
function renderSteps() {
$('#steps').innerHTML = DATA.steps.map((s, i) => `<li class="step">
<img src="${s.id}/desktop-home-fold.webp" alt="Home, first screen, after ${s.name}" loading="lazy">
<div class="body">
<div class="top"><span class="n">${String(i).padStart(2, '0')}</span><h3>${s.name}</h3><code>${s.commit}</code></div>
<p>${s.note}</p>
${i ? `<button type="button" data-cmp="${i}">Compare with ${DATA.steps[i - 1].name}</button>` : ''}
</div></li>`).join('');
$('#steps').querySelectorAll('[data-cmp]').forEach((b) => b.addEventListener('click', () => {
const i = Number(b.dataset.cmp);
state.a = DATA.steps[i - 1].id; state.b = DATA.steps[i].id;
$('#sel-a').value = state.a; $('#sel-b').value = state.b;
render();
$('#compare').scrollIntoView({ behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' });
}));
}
$('#mode-side').addEventListener('click', () => { state.mode = 'side'; render(); });
$('#mode-swipe').addEventListener('click', () => { state.mode = 'swipe'; render(); });
fillSelects(); renderShots(); renderSteps(); render();
</script>
+156 -9
View File
@@ -68,7 +68,11 @@ Read `docs/PLAYBOOK.md` (symptom → fix) before debugging a game, and `docs/FRA
mouse drags/right-clicks.
Live Frame data comes from one app-owned `app.monitor_hub` (`frame/monitor_hub.py`): subscribers name modules +
interval, the hub runs one agent `_monitor` stream (union of modules, fastest interval; per-module collection
needs agent v65), reconnects after a lost stream and stops it when nobody subscribes; `_poll` retries an "error".
needs agent v73), reconnects after a lost stream and stops it when nobody subscribes; `_poll` retries an "error".
Games in a sample (agent v81): FramePort's own (launch.sh / container / Proton folder), any other game Steam's
reaper runs (`steam:<appid>`, title from the appmanifest, e.g. Job Simulator from Steam's store; not SteamVR, not
devkit entries; End game = Steam's Exit game), overlay apps flagged `overlay` and listed last; newest first. The
UI takes `frame/monitor.main_game(sample)` (never an overlay app alone). The agent's own workers never count.
Subscribers: "monitor" (the Monitor tab, everything, only while shown) and "card" = the sidebar's live Frame card
(`ui/frame_card.py` helpers: battery ring, "now playing" row with fps + 2-min sparkline → click opens Monitor;
games + battery every 5 s, paused while a job's stage is "Upload…", only while connected and setting
@@ -154,7 +158,11 @@ Read `docs/PLAYBOOK.md` (symptom → fix) before debugging a game, and `docs/FRA
doesn't do). `VD.bat` is Virtual Desktop's launcher: ignore it except as an exe-location hint. Library migration
`rift_run_direct` resets existing recipes.
Auto launch-test is skipped on PC installs (it would start the game on the user's desktop). Launch tests collect the Unreal
game log + crash summaries from the Proton prefix; triage `unreal-crash`. Lies Beneath via Proton without Revive
game log + crash summaries from the Proton prefix; triage `unreal-crash`. Agent v67 adds Unity's logs (LocalLow/<Company>/<Product>/Player(-prev).log via `<Name>_Data/app.info`,
`output_log.txt`, Temp/…/Crashes/*/error.log; `unity_logs`) to launch tests and diagnostics; triage `unity-vr-init` /
`unity-crash`. A catalog recipe verified with another build (catalog `xr` ≠ the build's) keeps the build's own
`pcvr.launch_args` (`engine._other_build`; GitHub #105: SUPERHOT VR's Oculus+OpenVR build needs `-vrmode OpenVR`,
the catalog's is the OpenXR build); Electron launchers next to a game rank −30 (`rift.is_electron`). Lies Beneath via Proton without Revive
crashed (UE 4.23 "Unhandled exception").
- Rift scanning: `sources/rift_dump.scan` = the scanned folder's subfolders are games (one per folder; a folder is a
game if all candidate exes sit under one child), recursing into collections; `analysis/rift.py` walks once,
@@ -207,7 +215,14 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
`FRAMEPORT_DESKTOP=1` skips the Steam-parent watchdog and Steam's display; ensure_host_fixes refreshes entries
(older installs, stale ones removed), uninstall/purge remove them. Per app: library entry field `desktop_entry`
(default on; patches don't apply to Linux apps) → game page switch → agent `desktop_entry`. Untested on device.
- Scanning: SideQuest backups (`<package>/<time>/apk|obb|data`) and AXRB downloads (`AXRB/<app id>/<binary id>/base.apk`
+ OBBs/assets; its `patched/`/`*-axrb.apk` PC builds are skipped) are recognised (`sources/quest_dump.py`, FAQ).
- Quest/Rift twins stay separate entries, shown and named in Steam "Title (Quest)"/"(Rift)" (`core/titles.py`).
- Rename… (game menu "Name and artwork", Library right-click; CLI `frameport rename <pkg> "<name>"|--reset`):
`pipeline.rename_game` sets `title` + `title_locked` (+ `auto_title` = the automatic name for reset; rescans, Rift
store matches and links then only update `auto_title`; a locked name gets no twin suffix); `sync_title` → Frame
agent v77 `rename` (deployment.json title, same shortcut appid: upsert matches by Exe) + art + one Steam restart, PC
`PcReviveTarget.rename`; an unreachable Frame → `steam_name_stale` (game page "Update name on Frame", next install).
- `Recipe.as_is` = install unchanged (pre-patched libraries): `pipeline.prepare_as_is`; auto for APKs that already
contain FrameBridge (`frame_patched`). For Rift it changes nothing (the dump is never modified; the Frame copy
still gets launch fixes like the crash-reporter rename — `-nocrashreports` alone doesn't stop UE 4.23) — it does
@@ -245,8 +260,12 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
- `diag/` — user feedback without tokens (docs/DIAGNOSTICS.md): `redact.py` (every file/issue text: IPs, hosts,
home dirs, Steam ids, dump folders → placeholders), `bundle.py` (redacted diagnostics zip; agent v21
`collect_diag`; `frameport diag collect|inspect|report`), `issue.py` (prefilled GitHub issue-form links, ≤7.5k
chars). "Share working config" → `working-config.yml` issue → maintainer label `catalog-accepted` →
`catalog-from-issue.yml` workflow (`scripts/catalog_from_issue.py` validates) opens a catalog PR. App log:
chars; `*_links` = `IssueLinks(form, plain, body)`: body = the Markdown the form writes (`### <label>`, recipe in
a ```yaml block; labels checked against the templates), plain = `?title=&body=` link. GitHub #167: some (Flatpak)
browsers opened the form with empty fields → `app.open_issue` copies the body to the clipboard and offers "Form
empty? Open a plain issue"; keep the templates valid YAML, quote `: ` in descriptions). "Share working config" →
`working-config.yml` issue → maintainer label `catalog-accepted` (a plain issue needs `working-config` added by
hand) → `catalog-from-issue.yml` workflow (`scripts/catalog_from_issue.py` validates) opens a catalog PR. App log:
`core/applog.py` (`<data>/logs/app.log`, finished GUI jobs in `<data>/logs/jobs/`).
- Self-update (docs/ARCHITECTURE.md "Self-update", docs/INSTALL.md "Updating"): `_version.py` = the only version
(`frameport.__version__`; pyproject reads it via hatch `dynamic`; app log, diagnostics, User-Agent, Settings use
@@ -277,7 +296,17 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
by hand: the "Update now" button path (same apply(), called from the running app). macOS: CI smoke only.
- `agent/frameport_agent.py` — runs **on the Frame** (python3 stdlib only), JSON over SSH. Owns the install layout,
launch.sh template, Steam shortcuts (binary VDF), launch tests. Bump `AGENT_VERSION` when changing it.
- `bootstrap/bootstrap.sh` — one-time Frame setup served by the pairing server (sshd, app key, avahi service, Lepton).
- `bootstrap/bootstrap.sh` — one-time Frame setup served by the pairing server (app key, podman fix, Developer Mode, Lepton).
`bootstrap/setup.sh` — the setup URL (`curl -fsSL https://frameport.app/s | bash`, copied to the site by
`site/scripts/sync-media.mjs`): finds the PC (stdlib mDNS on 5353 for `_frameport-pair._tcp`, which the pairing
server announces while open; else the USB address and a /24 scan of `/ping`), `/hello` → the user clicks Allow (4
digits on both sides, `ask_digits`) → `/wait` hands over the code → runs bootstrap.sh exactly like the typed line.
Checked on the dev Frame 2026-10-10 (mDNS and scan paths) with a stub bootstrap; tests/test_setup_url.py.
Auto-pairing (`frame/autopair.py`, `app._start_auto_pair`, setting `frame.auto_pair`, default on; off with
FRAMEPORT_HOME/FRAMEPORT_NO_AUTO_PAIR): while no Frame is connected, Frames in Developer Mode (devkit mDNS, else a
30 s scan of the /24s for :32000/login-name, deduped by host key) are connected if FramePort's key works, else
offered the key via devkit.register every 4 s (refused at once until Pair new host is open; 45 s back-off after an
unanswered request). This WSL dev PC never hears the Frame's mDNS (Hyper-V firewall): the scan finds it.
- `catalog/games/<package>.yaml` (installed apps also fetch these from GitHub `main`, see "Catalog updates") — 38 recipes (34 verified 2026-09-28; Deadpool VR, 4XVR, NEX Player and AC Nexus's
90 Hz default confirmed later by the owner); `catalog/triage.yaml` — log signatures → fixes.
- `native/` — sources of the prebuilt binaries in `artifacts/` (adapter, VrApi bridge patches, GL shim, stubs).
@@ -298,8 +327,17 @@ Games tests: `pytest -m games` (FRAMEPORT_GAMES=<downloads dir>); on-device chec
native layer test: `FRAMEPORT_NATIVE_TESTS=1 pytest -m native` (compiles with the NDK, ~2 min on NTFS).
Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattributes`).
- `FRAMEPORT_HOME=<dir>` isolates all app data (tests use it); `FRAMEPORT_JAVA/_OVERPORT_JAR/_APKSIGNER_JAR` override
managed tools. Real app data (WSL): `~/.local/share/frameport` (tools, overport workspace + game keystores, library,
artwork, frames.json, the app's SSH key which the dev Frame authorizes).
managed tools. **The owner's real app data is the Windows copy's: `%LOCALAPPDATA%\FramePort`** (= `/mnt/c/Users/
<user>/AppData/Local/FramePort`; tools, overport workspace + game keystores, library, artwork, frames.json, the app's
SSH key which the dev Frame authorizes; moved from WSL's `~/.local/share/frameport` on 2026-10-11, which is now only
a stale backup). The desktop shortcut "FramePort (dev)" runs the source checkout natively on Windows
(`%LOCALAPPDATA%\FramePort-dev\launch-frameport-dev.vbs`: venv `FramePort-dev\venv` via `UV_PROJECT_ENVIRONMENT`,
`uv sync --frozen`, log `FramePort\logs\dev-gui.log`). **WSL runs on that data need `FRAMEPORT_WINDOWS_DATA=1`**
(set in `.claude/settings.local.json`; `paths.shares_windows_data`): the data folder becomes the Windows one, saved
paths from the other side are translated on load (`paths.localize`: /mnt/d/X <-> D:\X in library.json, tools.json,
PC deployment records), Java/Cpp2IL keep one copy per OS (tools.json `java-<os>-<arch>`), and the link handler isn't
registered (it would point the Windows copy's links at WSL). Don't run a WSL GUI and the Windows GUI at once
(the gui.lock flock/msvcrt locks don't see each other; only the gui.alive heartbeat does).
- Device checks: `frameport test <pkg>` / `frameport parity-device --results <parity.json> --baseline <launch.txt>
[--test-only]`; the pre-FramePort baseline is `PATCHED/_known-good-2026-09-28/_frame-state/baseline-launch.txt`.
- Docs screenshots + videos (docs/SHOWCASE.md): never by hand. `uv run python scripts/showcase/render_docs.py`
@@ -315,7 +353,7 @@ Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattr
give new icon-only controls a tooltip. The `showcase` workflow renders on UI pushes to main (+ the videos whose
storyboard changed, or the ones named on dispatch) and opens PR `showcase/update`; releases attach every video as
`FramePort-<name>.mp4`. `scripts/scrub_library.py` (the owner's own library) is only for private screenshots now.
README rules (owner): states the project is a proof of concept, provides no piracy tools, credits the wrapped
README rules (owner): says not every game runs (no longer "proof of concept" since 1.0), provides no piracy tools, credits the wrapped
projects (most functionality is theirs); neutral technical wording; no Quest2Frame mentions anywhere.
- GUI smoke test: `uv pip install flet-web playwright && playwright install chromium`, then
`FRAMEPORT_HOME=<test dir> python scripts/ui_smoke.py --out <dir> [--game <pkg>] [--frame steamos@<host>] [--update]` (`--update` = fake release: banner, dialog, Settings → Updates) and look
@@ -327,6 +365,11 @@ Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattr
`T.px(n)`, Material defaults through the theme's text_theme; never hardcode a bare pixel number in ui/.
`scripts/ui_smoke.py --scale 1.5 --viewport 2560x1440` renders it. Flet draws a grey box for invalid layouts (e.g.
an `expand` child in a `wrap=True` Row).
- One GUI per data folder (`core/instance.py`, 2026-10-11): `<data>/gui.lock` (flock/msvcrt, pid inside); a second
start asks the open window to come to the front (`gui.focus`, read by `_watch_links`) and exits; a lock held with no
window (no `gui.alive`) and older than 30 s is a leftover FramePort process and is ended. `main()` ends the process
(`os._exit`) once `ft.run` returns: background threads (re-analysis, session triage) used to keep a windowless
process running. Session triage shows one pop-up for all games of a burst (`_announce_sessions`).
- Stopping the GUI: `pkill -f` patterns match your own shell — use `pgrep -f "[b]in/frameport-gui|[f]let-desktop-light"`.
## Round-2 polish (2026-10-02)
@@ -604,7 +647,10 @@ shortcuts.vdf and writes once more if Steam put its old copy back (`shortcuts_lo
auto-repair (`_add_then_play`) crashed (its job got a Job, not a reporter). Uploads resume on a transient OSError.
Lepton's Android 11 has no clipboard service (134 services; checked with `podman exec … service check clipboard`):
SDL/LÖVE apps crashed at start → `frame.sdl_clipboard` (`apk/dex.py`: in-place dex edit, nops one invoke, fixes
the header checksum/signature; verified with LÖVE for Android 11.5, GitHub #24 Dramatic Shape). 2D apps (vr_kind none)
the header checksum/signature; verified with LÖVE for Android 11.5, GitHub #24 Dramatic Shape). Godot 4.2-4.4 (Kotlin `as ClipboardManager`,
GitHub #134 EndoparasiticVR) → `frame.godot_clipboard` (nops the `Intrinsics.checkNotNull` before that check-cast; Godot's
clipboard methods return at once; analysis `godot_clipboard`, ANALYSIS_VERSION 10; checked on real godot-lib/templates
4.2.2-4.4.1, not on the device; 4.1 and 4.5+ don't throw). 2D apps (vr_kind none)
keep every suggested patch that isn't about VR (`needs_vr = False`), not a fixed list. FramePort on the Frame:
`frame/local.py` (127.0.0.1, own key authorized; "This Frame (experimental)"; app data in
~/.local/share/frameport-app because ~/.local/share/frameport is the agent's) and Steam library changes wait while
@@ -694,6 +740,8 @@ FrameBridge `snapshot=N` (`snapshot_gl.c`, GLES): every N s the left-eye image t
own context and saved as `files/fb_snap_0-7.ppm` (quarter size) — headless launches show a black headset view, this
shows what the game draws. Vader: Lucasfilm logo (an OBB mp4: video works), then its loading card (portrait + segmented
bar) that never advances; the async loader thread sleeps and OBB reads stop (~168 MB of a 2.7 GB pak).
**SteamVR overlay apps (agent v75, 2026-10-10):** host OpenVR overlays (VRApplication_Overlay) are composited over Lepton (Quest) games on the Frame (probe checker seen over 4XVR in the headset view, `native/vroverlay_probe`); Windows ones register through Proton's vrclient too (Temporal Reality's Windows build created its overlays; fpsVR never reaches OpenVR under Proton). Detection `analysis/vroverlay.py` (bundled `.vrmanifest` with `is_dashboard_overlay`, else OpenVR + IVROverlay + self-registration markers, engine Other; Rift fingerprint `RIFT_ANALYSIS`), patch `pcvr.vr_overlay` (param autostart), Linux entry fields `vr_overlay`/`vr_overlay_autostart` (game page switch); agent `register_vr_overlay`/`unregister_vr_overlay`/`launch_vr_overlay` (IVRApplications via ctypes, live; manifest `<anchor>/frameport-overlay.vrmanifest`, the app's own key, binary = launch.sh). **SteamVR on the Frame reads only `binary_path_linux_arm`** (binary_path_linux alone is skipped). Overlay apps: no launch test, no Linux Steam-parent watchdog (`FRAMEPORT_OVERLAY`), not a "running game". A Steam overlay-app shortcut and a game shortcut run together. SteamVR's SetApplicationAutoLaunch didn't persist on the dev Frame (the watch never started by itself) → **agent v76: FramePort's own autostart**: the deployment's `overlay.autostart` is the source of truth; `ensure_host_fixes` (`ensure_overlay_service`) keeps the user service `frameport-vr-overlays.service` (`_vr_overlay_watch`, 5 s polls of the known vrserver's /proc stat) only while an overlay app has autostart on (register/unregister, uninstall, purge and the kill switch `~/.local/share/frameport/vr-overlays.disabled` / `FRAMEPORT_NO_OVERLAY_AUTOSTART` update it); a new vrserver (pid + start time, kept in `~/.cache/frameport-vr-overlays.json` across agent updates) → child `_vr_overlay_round`: wait for IVRApplications, 15 s grace, `launch_vr_overlay` for apps not running (folder process or GetApplicationProcessId); log `~/.local/share/frameport/vr-overlays.log`. Dev Frame: adopted the running vrserver, skipped the already running watch; a start after a real SteamVR start/boot is not yet seen. Details: docs/FRAME_RUNTIME.md "Overlay apps".
**Lepton's logcat mirror dies (agent v67, 2026-10-09):** occasionally launch.log ends with `logcat: Unexpected EOF!` right after the game starts (about 1 launch in 50, Lepton 2.8.14 and 3.0.5): no dashboard auto-hide, no launch-test result. launch.sh's `_logcat_keeper` then reads `podman exec lepton-steamlaunch-<appid> logcat` itself into launch.log (up to 5 restarts while the game runs).
**Double launch (agent v57, 2026-10-06):** a second Play while Lepton still boots (~10 s with nothing to see) made
the second Lepton stop the first one's container ("Waiting for steamlaunch-<appid> (PID …) to exit", exit 137
"(starting)", "Clearing baked app data due to early exit") and both died (Vader, BattleSisters). launch.sh now takes
@@ -721,6 +769,99 @@ head-pose deadline ends VR mode without a worn headset (not a game bug).
**Language packs (merged from PR #20, 2026-10-04):** overport's `libovrplatformloader.so` is a dispatcher that `dlopen`s Meta's own loader (`libovrplatformloader_meta.so` / `_meta_q1.so`, also `libpxrplatformloader.so`) and keeps its own message queue; `ovr_LanguagePack_GetCurrent/SetCurrent` are 8-byte `return 0` stubs in it, `ovr_AssetFile_GetList` forwards to Meta's loader. `frame.langpacks` (opt-in, `native/langpack`) serves `<tag>.lang` files from the game's data; `elf.hide_exports` marks the loader's exports STB_LOCAL (bionic and glibc only match GLOBAL/WEAK; verified for glibc, bionic's `is_symbol_global_and_defined` is from memory). `libfp_langpack.so` is built here (`python native/build.py --only langpack`) and committed. Owner-verified 2026-10-04: Deadpool VR with only `en.lang` and the patch on plays English dialogue and runs normally (headless launch tests show 2-6 fps while it loads: not a regression sign). A dispatcher answer that arrives after we answered the timed-out GetList is dropped (one answer per request); games already in a library get `lang_packs` filled after an app update (`library._refresh_data_fields`). The library logs to logcat under the tag `fp_langpack` (dirs looked in, packs found, every language-pack call), so it shows up in the game's `launch.log`; an `ovr_AssetFile_GetList` the dispatcher leaves unanswered for 1.5 s is answered with our packs alone. Deadpool VR (Unreal) accepts a pack only when its `Metadata` equals the game's own version string (`ULanguagePacksSubsystem` compares it with `%s.%s.%s.%s.%s` built from the build info, e.g. `1.0.40.356975.Quest` = versionName; found by disassembling `libUE4.so`): the patch writes the APK's versionName into the library (`@FPMETA@` slot, `with_metadata`), `FRAMEPORT_LANGPACK_META` overrides it. Deadpool VR (2026-10-04, headset): German became selectable with that Metadata, but dialogue stayed silent (even English once reported as a pack) while the path was spelled `/sdcard/Android/obb/<pkg>/x.lang`; with the `/storage/emulated/0/Android/obb/<pkg>/x.lang` spelling (Unreal's own, now listed first in `scan_dirs`) German dialogue plays. `FRAMEPORT_LANGPACK_SKIP=<tags>` or a file `fp_langpack_skip` in the obb folder leaves packs out of the list (experiments).
**Cube swapchains (GitHub #107, 2026-10-09):** the Frame's runtime refuses `faceCount=6` swapchains (-2,
XR_ERROR_RUNTIME_FAILURE); OVRPlugin ignores that ("CreateSwapchain for eye 0: 0x0, 0 stages") and crashes in
ovrp_EndFrame4 (memset). FrameBridge `cube_standin` (default on, `native/adapter/cube_standin.c`) serves a refused cube
swapchain as one GL cube-map texture in the app's context (GLES only) and drops its layers. Verified headless with
Budget Cuts Ultimate (2048² sRGB, 12 mips; runs on at ~70 fps); what the cube layer showed is simply missing.
**Hardware video decoding (PR #96, PR #128 by Lucas-Mathieu, adapted 2026-10-10, agent v71):** `native/hevc` = one
OMX plugin `OMX.frameport.{avc,hevc,vp9}.decoder` (FFmpeg v4l2m2m on Iris /dev/video-dec0, FFmpeg software decoders as
fallback inside the component, Vulkan copy for big native surfaces). The connection installs it once per Frame
(`ensure_video_codec` → agent `video_codec_status`/`install_video_codec`: `~/.local/share/frameport/video-codec/versions/
<manifest sha>` + `current` symlink). A Frame keeps the same or a newer `revision` (no flip-flop between PCs) → **bump
`revision` in native/hevc/build.py with every artifact change**; a failed install isn't retried during the connection.
Per game: only launchers of games whose recipe has `frame.hw_video_decode` (install stage, no APK change; suggested
from analysis `media_codec`, ANALYSIS_VERSION 8; deployment.json `hw_video_decode`) get the codec line → the Podman
wrapper (native/hevc/podman.py) mounts the plugin into that container (merged media_codecs.xml in
$XDG_RUNTIME_DIR/frameport-video). Off for every game: Settings → Installing (library `video.hw_decode` → agent
`video_codec_switch` = video-codec/disabled); one game: `FRAMEPORT_NO_HW_VIDEO=1 %command%`. `upgrade_launchers`
converts agent ≤70 launchers (Batman's `<base>/frameport-codec` → keeps the line, folder then removed). Batman: catalog
+ migration `batman_video_patches` (`frame.hw_video_decode` + hidden adapter setting `surface_native`). Iris: 8K
refused (ENOMEM) while any other decoder session is open; SteamVR's vrlinkrunthread holds one ~12 s at game start →
refused opens retry until 20 s after the plugin loaded (else 2 s), then software; VP9 7680x3840 never returned a
picture → VP9 ≤4096x2304; hidden VP9 frames come back as empty capture buffers (bytesused 0, no LAST):
FFmpeg's wrapper ended the EOS drain at the first one (two-pass VP9 lost its last ~25 frames) → build.py requeues
them (codec revision 8; drain ends at LAST, or 1 s after a skipped empty buffer; dev Frame 2026-10-10: 600/600 =
OMX.google.vp9 Y hashes). Rebuilds are deterministic (ext4 + NTFS path
with spaces, two NDK copies → same; rev 8 141de01f…; PR #128's own sources → its 27a2d749…). Not yet run on the device.
**Vulkan shader dump (GitHub #140, 2026-10-10):** adapter `vk_shader_dump=1` (Vulkan shim, `native/vkshim/shader_dump.h`;
applies where vk_sanitize can: Unreal/Other arm64) writes each distinct SPIR-V module once to
`files/fp_vk_shaders/<size>_<sha256>.spv` (tmp + rename) and one `index.txt` line per vkCreateShaderModule (`<seq> <ms>
<unix ms> <name> new|known|again|failed`, O_APPEND) to find the module behind a GPU hang. Agent v72 `collect_diag`
returns `shaders` (newest modules of fp_vk_shaders + fp_spirv, ≤4 MB each, base64) → bundle
`games/<pkg>/target/shaders/`. Triage `gpu-hang` suggests both dumps; `triage.graphics_api` (FrameBridge's
xrCreateSwapchain formats: <0x1000 Vulkan, else GL) keeps only the session's API's (`API_ONLY`). Host-tested only.
Agent v74: a reporter's hang came ~10 s after the last new module, so the culprit wasn't among the newest written →
with an index naming modules, `shader_dumps` sends every module of the index's **newest session** (after the last
`# start`), deduplicated, last used first, each ≤4 MB, ≤30 MB in all (`session` = how many it names) + that session's
index lines; no index (fp_spirv) = the old newest-by-mtime 4 MB. One JSON line (~40 MB base64) over the SSH channel
is read whole by `Frame.agent` (fine at that size); `_Writer.fit` drops the least recently used modules last, after
cutting logs, to stay under the 24 MB zip limit.
**Unreal OBB check (GitHub #159, 2026-10-10):** Epic's DownloaderActivity checks OBB name+size (OBBData), then with `bVerifyOBBOnStartUp=true` CRCs the whole OBB; its screens are invisible in Lepton (stuck at `Displayed …/.DownloaderActivity`, triage `unreal-obb-check-stuck`) → `frame.unreal_skip_obb_check` (analysis `unreal_verify_obb`, ANALYSIS_VERSION 11) flips the manifest boolean (`axml.set_meta_data_bool`); skips only the CRC pass. Host-tested only.
**Meta user id (GitHub #182, 2026-10-11):** OVRPort's platform loader (runtimes 3.4.3-23204ea and -aa54c3f) answers
`ovr_GetLoggedInUserID` (and two other callers) without a Meta account with a made-up id from a helper (`stp; mov x29,
sp; bl random; …; mov/movk x9 = 0x53444835ec580000 (6e18); add x0, x8, x9; ldp; ret`), kept only in memory → a new id
every start; Walkabout Mini Golf files saves under `Profiles/Oculus/<id>/` and lost all progress. `frame.ovr_stable_user`
(default_on, detect = the game has libovrplatformloader.so) rewrites the helper to `mov x0, <6e18 + 32 bits of
sha256(package, keystore)>; ret`: stable per game and per user (keystores are personal), so online games never see two
FramePort users as one. Found in every PATCHED build checked. Host-tested only; upstream candidate.
**Vivox API 31 (GitHub #101, 2026-10-09):** newer Vivox builds (Green Hell VR) call Android 12 AudioManager
communication-device methods from `com.vivox.sdk.AudioChangeListener` with no SDK check → NoSuchMethodError on Lepton's
Android 11. `frame.vivox_audio_route` (analysis `vivox_api31`, ANALYSIS_VERSION 4) makes every such method return at
once (`Dex.return_early`: return-void / `const/4 v0,0; return v0`; nopping the invoke would leave a move-result the
verifier rejects). Older Vivox (Eleven Table Tennis, BattleSisters) lacks that code and isn't matched. Verified
headless: Vivox initialises, 150 s at ~65-70 fps.
**Steam Input gamepad for 2D apps (GitHub #162, agent v72, 2026-10-10):** Lepton's Android only has the Wayland seat's `wayland_touch/keyboard/pointer`. Steam makes its virtual pad (uinput, `/devices/virtual/input`, 28de:11ff, "Microsoft X-Box 360 pad N", ACL for steamos) only while the Frame's controllers are on (controller.txt "Steam Controller reserving XInput slot 0"; gone when they sleep), also with no game running; steamos-manager's 28de:0000 keys device isn't a pad (no BTN_SOUTH). Lepton's container root (system_server too) = steamos (`keep-id:uid=0`), so a bind-mounted `/dev/input/eventN` opens. Opt-in `device.steam_gamepad` (vr_kind none; suggested for `android.hardware.gamepad`/`LEANBACK_LAUNCHER`, analysis `gamepad`, ANALYSIS_VERSION 9): deployment.json `steam_gamepad` → launcher `GAMEPAD_LINE` (after the codec line: exports FRAMEPORT_GAMEPAD + LEPTON_ENV_SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD, puts `agent/bin` first on PATH) → `PODMAN_WRAPPER` (written by ensure_host_fixes; non-`run` calls go straight on; `run` imports the agent's `podman_run_args`; hands on to the next Podman after its own PATH entry with itself removed from PATH, so it chains to the codec wrapper either way round) adds the pads + `Vendor_28de_Product_<pid>.kl` (Xbox 360 layout). Verified headless with a uinput stand-in pad (Stremio's container: EventHub `classes=0x80000141`, our key layout, KeyEvent BUTTON_A dispatched); with Steam's real pad and a game: not yet. Pads made after the start aren't seen until the next one.
**Multiview programs on flat framebuffers (GitHub #77, Doom3Quest, 2026-10-09, prototype):** Mesa enforces OVR_multiview's
"program num_views == draw framebuffer views" rule (`draw_validate.c`, the draw is dropped silently); Qualcomm doesn't.
Doom3Quest compiles every VS with `layout(num_views=2) in;` and draws its HUD/PDA into 2D-texture FBOs → black. Opt-in
`frame.gl_multiview_fbo` (analysis `gl_multiview_libs`, ANALYSIS_VERSION 6; own-engine GLES only): `native/glmv` =
`libfpglmv.so` (12 chars = "libGLESv3.so": libdoom3.so's one `.rodata` dlopen string is rewritten in place, its qgl*
table comes from dlsym on that handle; also first DT_NEEDED for its direct gl*/egl* imports). Such draws use a lazily
built single-view twin (view 0, uniforms copied per draw); eglMakeCurrent resets the per-thread cache. Host-tested only
(rewriter on all 19 Doom3Quest shaders + glmv.c against a stand-in GL, `tests/test_gl_multiview_fbo.py`); the host's
Mesa 23.2 llvmpipe has no OVR_multiview. Headset-verified by Xandrix1987 (2026-10-09: HUD/PDA shown, no fps drop) -> in the Doom3Quest catalog recipe.
**BlazeRush (GitHub #57, Klownicle's verified guide, 2026-10-10; FramePort's version headless-tested only):**
`frame.blazerush` (1.0.349 only: exec-segment SHA + every word checked, offsets from PT_LOAD) NOPs the avatar-readiness
wait (0x4b2dbc), RETs the avatar renderer (0x4b2adc), renames `cache/shader_cache` → `cache/br_shader_v1`. GL shim
(now also exports glShaderSource/glLinkProgram/glProgramBinary/glDeleteProgram/glDraw*: pass-through unless a setting
is on) `gl_int_attribs` (integer inputs fed via glVertexAttribPointer re-set with glVertexAttribIPointer per draw,
reflection cached per EGL context + program, dropped on link/binary/delete) and `gl_highp_markers` (vertex shaders with
every '|' marker: mediump→highp except on in/out/uniform lines); host test `tests/test_blazerush.py` with a stand-in
GL. FOV: the game reads VrApi props 7/8 right after vrapi_Initialize (no session; the Frame has no
XR_EPIC_view_configuration_fov) → the bridge saves the live FOV (also located before tracking, headless: 118.87×120.04
on the dev Frame) in `files/framebridge-vrapi-fov.txt` and answers with it at the next start (log `FOV properties: …
(saved by an earlier session)`). `device.config_sync` (catalog field `config_sync`, agent v78): user_config.xml
r_3dwidth/height, r_width/height from the runtime eye size (FrameBridge logs `runtime recommended eye size`) × scale,
written at finalize, set_settings and by launch.sh before Lepton starts (`_config_sync`). Headless runs at scale 1.7
end in `zink: DEVICE LOST` right after the bridge's 30 s no-tracking deadline tears the session down (gpu fault at the
teardown; not at scale 1.0): a headless-only artifact, the headset never hits that deadline. Launch tests count an exit/crash after
such a line as a pass (triage.yaml `headless_end`, finding `headless-end`; GitHub #57 "Failed to launch" on install).
**Session triage (agent v70, 2026-10-09):** launch tests never reach FOCUSED, so real play sessions are triaged
too. list_installed gives each game `last_play` {start, end, test} from `<anchor>/plays.log` (launch tests write a
`test <unix>` line first → test sessions are only marked); `session_log` returns the newest session's launch.log
(`<base>/session.log`, ≤4 MB: head + tail + FrameBridge/crash lines between) + that session's logcat-crash.log +
`journalctl -k` GPU lines ("kernel: …"). The GUI's connection refresh (`app._check_sessions` → `pipeline.sessions_due`,
background thread; sessions >7 days old are skipped) runs `pipeline.triage_session` = `validate/session.analyze`
(triage.yaml signatures incl. `space-warp-used` (info, `question:`, never auto-applied), `gpu-hang` (`report: true`)
+ computed `slow-frames` (pacing windows vs the nearest refresh rate) and `focus-dips` (FrameBridge always logs
`focus: lost` / `focus: back after N ms`, session_fixes.c)) → library `last_session` / `last_session_checked` → game
page "Last session" callout (Try this fix / Rebuild with this fix / Yes-No question / Report / Dismiss). Suggestions may
be values (`adapter.scale=0.85`, `triage.split_suggestion`); adapter-only fixes are pushed live (`apply_suggestions_live`
→ agent set_settings, like the Game settings dialog). CLI `frameport session <pkg> [--apply]`. PC VR (Proton) launchers
log no session end, so they aren't triaged yet. pac_hints stays triage-only: a survey of all 68 dump APKs found unpaired
PAC hints in 21 libraries of 18 games (OpenSSL's 38/40 in most UE4 libUE4.so and libEOSSDK.so, UE5 libUnreal.so ~400
vs +2-3, libass, libopencv, …), most of them games that work, so default-on would rewrite many working builds.
**Unresolved (as of 2026-09-28):** Arcsmith (right-eye distortion) and Time Stall (both eyes) — swap, tracking, Valve
layers, depth, pacing ruled out. Sniper Elite VR (DEVICE LOST), Espire 1 (Mesa GL upload crash), HITMAN 3 (freedreno
crash): use PC versions.
@@ -758,6 +899,10 @@ up to 4 s for the link file to be taken before trusting the heartbeat, the windo
`gui.alive`, and the Windows command runs under `conhost.exe --headless` (plain `-WindowStyle Hidden` flashed a
console). Screens: `scripts/ui_smoke.py --links --fake-frame --game <pkg>` (tall pages: `--viewport
1280x7000`; Flutter's popup menu ignores Escape).
Demo link: the homepage's example button = `deeplink.DEMO_MANIFEST` (https://frameport.app/demo/cool-game.json, published
from site/public/demo/); every form (https button page, frameport://, framedrop://, pasted) → `InstallRequest.demo`,
no network (fetch_manifest returns `demo_manifest()`, download refuses), `link_dialog.show_demo` (Install =
`easter.demo_install`: Cool Game's cover out of the portal; eggs off → "Nice, it works!"), CLI just says so.
## Releases, CI, GitHub
Maintainer-only notes (accounts, credentials, key locations) live in the git-ignored `CLAUDE.local.md`.
@@ -848,5 +993,7 @@ the unresolved eye distortion, and testing the release bundles on real Windows/m
fallback (`core/cache.py`). Don't hardcode what can be discovered (e.g. Lepton path via appmanifests).
- Keep APK edits minimal and byte-stable (parity depends on it). Every new fix: a patch module + a triage signature +
a PLAYBOOK row + a unit test.
- New or changed patch, game setting, catalog recipe or triage signature → run `python scripts/patch_docs.py`
(regenerates docs/PATCHES.md + site/src/data/patches.json, the site's id → anchor map; a test runs `--check`).
- Known-good backups of every working APK and the signing keys: `PATCHED/_known-good-2026-09-28/` (don't delete).
- Your development Frame: `frameport frame info` (remembered in `<user data>/frames.json`); SSH as `steamos@<frame>`.
+365
View File
@@ -0,0 +1,365 @@
---
name: FramePort
description: Quest games, Android apps, Linux apps and PC VR games on the Valve Steam Frame, set up and installed from your PC.
colors:
frame-orange: "#FF8A1F"
frame-orange-soft: "#3A2512"
ember-ink: "#160C03"
pc-blue: "#3AA8FF"
pc-blue-soft: "#0F2A40"
void: "#0D0E12"
sidebar-night: "#111217"
panel: "#16181E"
panel-raised: "#1D1F27"
panel-well: "#252832"
hairline: "#2A2D37"
hairline-strong: "#393D49"
text-bright: "#EDEEF2"
text-dim: "#A7ABB7"
text-faint: "#717583"
ok-green: "#3DD68C"
warn-amber: "#F2C94C"
error-coral: "#FF6166"
info-cyan: "#5CD0E6"
typography:
display:
fontFamily: "Sora, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif"
fontSize: "clamp(30px, 4.2vw, 44px)"
fontWeight: 800
lineHeight: 1.1
letterSpacing: "-0.02em"
headline:
fontFamily: "Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif"
fontSize: "28px"
fontWeight: 700
lineHeight: 1.2
title:
fontFamily: "Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif"
fontSize: "16px"
fontWeight: 600
lineHeight: 1.3
body:
fontFamily: "Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif"
fontSize: "13px"
fontWeight: 400
lineHeight: 1.5
label:
fontFamily: "Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif"
fontSize: "11px"
fontWeight: 600
lineHeight: 1.3
section-label:
fontFamily: "ui-monospace, Cascadia Mono, SF Mono, Menlo, Consolas, monospace"
fontSize: "12px"
fontWeight: 600
lineHeight: 1
letterSpacing: "0.14em"
rounded:
xs: "6px"
sm: "8px"
md: "12px"
pill: "20px"
spacing:
s1: "4px"
s2: "8px"
s3: "12px"
s4: "16px"
s5: "24px"
s6: "32px"
components:
button-primary:
backgroundColor: "{colors.frame-orange}"
textColor: "{colors.ember-ink}"
typography: "{typography.title}"
rounded: "{rounded.sm}"
padding: "12px 16px"
button-primary-big:
backgroundColor: "{colors.frame-orange}"
textColor: "{colors.ember-ink}"
rounded: "{rounded.sm}"
padding: "18px 22px"
button-primary-disabled:
backgroundColor: "{colors.panel-well}"
textColor: "{colors.text-faint}"
rounded: "{rounded.sm}"
button-secondary:
backgroundColor: "transparent"
textColor: "{colors.text-bright}"
rounded: "{rounded.sm}"
padding: "12px 14px"
button-ghost:
backgroundColor: "transparent"
textColor: "{colors.text-dim}"
rounded: "{rounded.sm}"
button-danger:
backgroundColor: "{colors.error-coral}"
textColor: "{colors.ember-ink}"
rounded: "{rounded.sm}"
padding: "12px 16px"
input:
backgroundColor: "{colors.panel-well}"
textColor: "{colors.text-bright}"
typography: "{typography.body}"
rounded: "{rounded.sm}"
padding: "10px 12px"
pill:
textColor: "{colors.text-dim}"
typography: "{typography.label}"
rounded: "{rounded.pill}"
padding: "3px 9px 3px 8px"
card:
backgroundColor: "{colors.panel}"
textColor: "{colors.text-bright}"
rounded: "{rounded.md}"
padding: "16px"
callout:
textColor: "{colors.text-bright}"
rounded: "{rounded.sm}"
padding: "12px 14px"
nav-item-selected:
textColor: "{colors.text-bright}"
rounded: "{rounded.sm}"
tooltip:
backgroundColor: "{colors.panel-well}"
textColor: "{colors.text-bright}"
rounded: "{rounded.xs}"
padding: "8px 12px"
---
# Design System: FramePort
## Overview
**Creative North Star: "The Portal Between"**
FramePort is about crossing. On one side is the PC, in blue; on the other is the Frame, in orange. Between them is the
seam where a game passes through: a horizontal gradient from blue to orange, the same one the logo's tilted portal
makes. Everything else is dark space, so the crossing is what glows. The desktop app and frameport.app share this
system with the same color values. The site states it outright ("FramePort Portal, for the web").
Surfaces are flat, dark, tonal steps a few lightness points apart, with hairline borders, and the chrome stays quiet.
Color comes from three places only: cover art, the orange that marks the one thing to do now, and the blue portal
touches that show where you are or what's moving. Density is comfortable desktop density, built for a mouse and a
window you resize. The interface scales as a whole through one scale factor, never by reflowing into a phone layout.
Components are tactile and confident. A cover lifts and its art zooms when pointed at. A button settles when pressed.
On the website, motion is kept for things that cross: the hero's package-to-game crossing, the install log turning
from PC rows into Frame rows, the download going through the portal. Nothing moves just because the pointer or the
scroll passed by (no cursor light, no word-by-word headings, no scroll fade-ups).
Play sends two rings out of the button, blue then orange. A new view fades in. Every one of these motions disappears
under Reduce motion, and the static state must still read on its own.
**Key Characteristics:**
- Dark only: a near-black void (`void`) with three tonal panel steps above it.
- Orange is the action, blue is the place: the portal dichotomy is the color doctrine.
- The blue→orange gradient is the system's only decoration.
- Artwork carries the color; the chrome stays neutral.
- Flat at rest. Artwork and floating layers lift.
- Motion is physical (lift, spring, ripple) and always optional.
## Colors
A cool, near-black neutral ramp with two saturated poles, the orange Frame and the blue PC, plus four status colors
tuned so none of them reads as the accent.
### Primary
- **Frame Orange** (`frame-orange`): the main action of a view (Install on Frame, Play on Frame, Add games), the
current selection in single-color themes, text cursors, focused input borders and links. In the wordmark it is
"Frame".
- **Frame Orange Soft** (`frame-orange-soft`): the tinted backing for orange in small areas, such as selected rows in
the `original` theme and the accent wash behind the portal scene.
- **Ember Ink** (`ember-ink`): text and glyphs on orange (and on the coral danger button). Never pure black and never
white.
### Secondary
- **PC Blue** (`pc-blue`): secondary (outlined) buttons, progress, switch tracks, anything "on this PC" or PC VR, and
"Port" in the wordmark. Outlines use it at 55 % opacity.
- **PC Blue Soft** (`pc-blue-soft`): tinted backing for blue in small areas, and the round icon wells on feature cards.
### Neutral
- **Void** (`void`): the window and page background.
- **Sidebar Night** (`sidebar-night`): the sidebar and table headers, one step above the void.
- **Panel** (`panel`): cards, sections and the game page's info blocks.
- **Panel Raised** (`panel-raised`): hover, menus, code blocks and popovers.
- **Panel Well** (`panel-well`): inputs, chips, tooltips and disabled buttons.
- **Hairline** (`hairline`) / **Hairline Strong** (`hairline-strong`): 1px card borders, dividers, menu and popover
outlines.
- **Text Bright** (`text-bright`), **Text Dim** (`text-dim`), **Text Faint** (`text-faint`): primary text, secondary
text and meta / disabled text. The website uses a lighter Text Faint (#868a96): the app's value is below 4.5:1 for
the small print the site carries (timestamps, captions). Placeholders use Text Dim there, never the browser gray.
### Status
- **OK Green** (`ok-green`): Works, Connected, Installed, the battery ring, passed checks.
- **Warn Amber** (`warn-amber`): Works with issues, warnings. Deliberately yellow, so it never reads as Frame Orange.
- **Error Coral** (`error-coral`): failures and destructive actions.
- **Info Cyan** (`info-cyan`): info callouts and a second data series next to PC Blue in Monitor charts.
### Named Rules
**The Portal Rule.** Orange is the Frame and the action; blue is the PC and the secondary. Don't swap them, and don't
use a third hue for an action.
**The One Orange Rule.** A view has one filled orange button: the thing to do now. Other actions are blue outlines or
ghosts.
**The Seam Rule.** The blue→orange gradient (`--portal`, `C.portal_gradient`) is the only decoration. It always runs
left to right (or top to bottom), blue first. It appears on the sidebar edge, the selected nav item and segment, the
wordmark, section-label hairlines, link underlines on the site and progress fills. Dual themes only.
**The Dark-Only Rule.** No light mode. Installed theme files whose background is too light, or whose text falls below
4.5:1 on a panel, are refused.
## Typography
**Display Font:** Sora 600–800 (website only; self-hosted variable woff2), falling back to Segoe UI / Roboto
**Body Font:** the platform sans (Segoe UI on Windows, Roboto via Flutter, Helvetica Neue / Arial)
**Label/Mono Font:** ui-monospace / Cascadia Mono / SF Mono / Menlo / Consolas
**Character:** the app speaks in its platform's sans, so it feels like a native tool. The site adds Sora's geometric
heavy weights for headings, which echo the rounded wordmark. Monospace appears where something is literally typed or
counted: the setup line, logs, section labels.
### Hierarchy
- **Display** (Sora 800, clamp(30px, 4.2vw, 44px), 1.1, −0.02em): site section headings and doc titles. In the app,
empty-state and setup headings use 700 at 22px.
- **Headline** (700, 28px): the app's page titles (Library, the game title on its hero).
- **Title** (600, 16px): section headings in the app (Where it's installed, About this game) and card headings.
Buttons use 600 at 13px (15px when big).
- **Body** (400, 13px app / 16px at 1.6–1.7 on the site): running text. Site prose and leads stay at 62ch or less.
- **Label** (600, 11–12px): pills, meta lines, counts.
- **Section label** (mono 600, 12px, 0.14em, uppercase): site section labels followed by the portal hairline. They
name what the visitor does there ("See what runs", "Get started"), never a number ("02 · The app" was removed: the
order carries no information). The app's "ALL GAMES" divider is the same idea.
### Named Rules
**The Scale-Token Rule.** In the app every size goes through `T.T_*` tokens or `T.px()`, so the user's UI scale
(75–300 %) applies everywhere. A bare pixel number is a bug.
**The Sentence-Case Rule.** Headings and buttons use sentence case. Only the mono section labels and pill-sized
overlays are uppercase.
## Layout
The app is a fixed-width left sidebar (navigation, then the activity card, the live Frame card and the power bar at
the bottom) next to a scrolling content column. The sidebar ends in a 1px vertical blue→orange seam. Content uses an
8-point spacing scale (4/8/12/16/24/32), with 24–32 between sections and 12 between a heading and its body. Pages
start with a title row (title and count on the left, search and actions on the right, the orange action alone at the
far right), then filter rows of segmented controls and dropdowns, then content.
Library covers sit in a wrapping grid of portrait cards, with a horizontal "On your Frame" shelf of landscape cards
above. The game page opens with a full-width art hero holding breadcrumb, meta line, title, pills and the action row.
Dialogs come in three widths (440 / 600 / 880). Selection bars sit **below** a list, never above it, so items don't
jump during a drag.
The site uses a centered 1180px column with a fluid gutter (`clamp(16px, 4vw, 40px)`) and section padding of
`clamp(56px, 7vw, 96px)`. On phones, wide doc tables and diagrams scroll sideways inside their box, never the page.
## Elevation & Depth
The system is flat at rest. Depth comes from tonal steps (void → sidebar → panel → raised → well) and 1px hairlines.
Shadows are reserved for two things: **artwork**, which sits on the page like a physical cover, and **floating
layers** (menus, popovers, term cards, toasts, dialogs), which have to separate from whatever is beneath them.
Hovering a cover deepens its shadow and adds an orange glow, the one place the accent becomes light.
### Shadow Vocabulary
- **Art at rest** (`box-shadow: 0 8px 22px 1px rgba(0,0,0,.55)`): cover and shelf cards, screenshots.
- **Art hover** (`0 16px 36px 2px rgba(0,0,0,.75), 0 4px 30px -2px rgba(255,138,31,.22)`): a pointed-at cover, with
the orange glow underneath.
- **Float** (`0 18px 40px rgba(0,0,0,.55)`, app menus: elevation 8 on black): menus, popovers, term cards, the
search-result bar.
- **Portal glow** (radial: blue 10 % → orange 5 % → transparent): the soft light behind empty states and the site
hero. Not a shadow, but the only other source of depth.
### Named Rules
**The Lift-Is-Earned Rule.** A panel never gets a shadow just to look important. If it isn't artwork and it doesn't
float over other content, it's flat with a hairline.
## Shapes
Gently rounded and consistent: 12px for cards, dialogs and the hero; 8px for buttons, inputs, callouts, thumbnails and
code blocks; 6px for tooltips and small chips; fully round (20px+) for pills, segmented tracks, the cover buttons and
toasts. Borders are 1px hairlines; a focused input or a site `:focus-visible` gets a 2px orange edge instead. Cover art
is clipped inside its card, so the art can zoom on hover without spilling. The portal itself is a tilted oval (18°,
`--tilt`), and that silhouette is used only in the logo, the wordmark and the empty-state portal scene.
## Components
### Buttons
Tactile and confident: clear fills, a firm press.
- **Shape:** gently rounded (8px).
- **Primary:** Frame Orange fill, Ember Ink 600 label, 12px × 16px (18px × 22px for the hero's big Play/Install). The
glyph switches to its `-solid` variant. Disabled turns to Panel Well with Text Faint.
- **Hover / Press:** the site brightens 8 % on hover and settles to 0.97 on press (ease-out-quart, no overshoot). The
app uses Material's overlay. Focus shows a 2px orange outline, 3px offset.
- **Secondary:** transparent with a 1px blue outline at 55 %, a blue glyph, a bright label and a faint blue wash on
hover. Single-color themes use a neutral Hairline Strong outline.
- **Ghost:** a Text Dim label only, for Close, Cancel and inline links (orange when it's a link).
- **Danger:** the primary's shape in Error Coral. The outlined variant is for entries that ask first; the filled one
is the dialog's final button.
- **Icon button:** 20px glyph in Text Dim, always with a tooltip.
### Pills
- **Style:** fully round, 11px 600 label, colored text on a 12 % tint of the same color. A leading dot (site) or glyph
(app) means the color is never the only signal.
- **Variants:** status (Works / Works with issues / Can't run), install state (solid fill + Ember Ink, e.g. "On
Frame"), platform (Quest / PC VR). Overlay pills on artwork sit on 62 % black.
### Cards / Containers
- **Corner Style:** 12px.
- **Background:** Panel on Void, with a 1px Hairline border.
- **Shadow Strategy:** none; see Elevation. Cover cards are the exception.
- **Internal Padding:** 16px.
### Cover cards (signature)
The game is the hero. A portrait (grid) or landscape (shelf) cover, clipped at 12px, has a bottom fade for its title
and status line and pills over the top-left corner. Hover lifts the card (scale 1.02, up 1.5 %, 220ms ease-out), zooms
the art inside the clip (1.07, 600ms), fades in a radial scrim and deepens the shadow into Art hover. A frosted round
**cover button** (10px blur, 60 % white ring) springs in (scale 0.72 → 1, ease-out-back) and fills with orange under
the pointer. Play sends out two rings, blue then orange, and turns into a "Starting on Frame…" pill for 8 s. Reduce
motion keeps only the shadow and scrim changes.
### Inputs / Fields
- **Style:** filled Panel Well, no border at rest, 8px corners, 13px text, dense 10px × 12px padding. Search adds a
leading magnifier.
- **Focus:** the border appears in Frame Orange; the cursor is orange.
- **Labels:** Text Dim. Dropdowns share the same filled look.
### Callouts
A line of guidance that belongs to the page: an 18px status glyph, then text. The status color is used at 8 % for the
fill and 35 % for the 1px border, with 8px corners. Kinds: info (cyan), warn, error, ok, pc (blue). Buttons inside a
callout make it a bar.
### Navigation
- **Sidebar:** glyph + label rows with Text Dim at rest. The selected row gets the portal fade (blue→orange at 22 %)
with a bright icon and label (single-color themes: the orange soft tint and an orange icon). The Steam Frame row
carries a green connected dot.
- **Segmented controls:** a round track. The selected segment takes the same portal fade.
- **Site nav:** the wordmark (orange "Frame", the portal glyph, blue "Port"), ghost links, one small primary
"Download FramePort" button at the right.
### Empty states (signature)
The portal with what the screen is about coming out of it, on a soft portal light. Then a 22px heading, one centered
sentence, the action that fills the screen, and up to three feature cards (round blue icon well, 14px heading, one
sentence) that say what will appear here.
## Do's and Don'ts
### Do:
- **Do** read colors as theme tokens when building (`T.ACCENT`, `var(--accent)`), so all three built-in themes and
user theme files work.
- **Do** keep exactly one filled orange action per view; make everything else blue outline or ghost.
- **Do** put the blue→orange gradient only where something crosses or is selected: seams, selection fades, progress,
link underlines.
- **Do** let cover art carry the color and keep surrounding chrome neutral.
- **Do** pair every status color with a word or glyph (pills, callouts, checklists).
- **Do** give motion a static fallback: under Reduce motion (`ui.reduce_motion`, `prefers-reduced-motion`) the state
must still read without the animation.
- **Do** scale every app size through `T.T_*` / `T.px()` and give icon-only controls a tooltip.
### Don't:
- **Don't** build a light theme or a light surface; FramePort is dark only.
- **Don't** swap the poles: no orange secondary buttons, no blue primary actions.
- **Don't** add shadows to ordinary panels or cards; only artwork and floating layers lift.
- **Don't** use Warn Amber or another orange-adjacent hue for a warning that could be mistaken for the accent.
- **Don't** use the tilted portal silhouette as general ornament; it belongs to the logo, the wordmark and empty
states.
- **Don't** put image bytes into controls or recreate clickable controls on progress ticks; update them in place.
+121
View File
@@ -0,0 +1,121 @@
# Product
<!-- impeccable:product-schema 1 -->
## Platform
web
Two surfaces share this record:
- **The desktop app:** Python + Flet 1.0 (Flutter), shipped as bundles for Windows x64, macOS arm64 and Linux x64, plus
a CLI wheel. It is not a website, but it renders through Flutter rather than native OS widgets, so it counts as `web`
here. Desktop window, mouse and keyboard first; no phone layout.
- **The website** [frameport.app](https://frameport.app): Astro 7 (`site/`), with pages for home, download, install,
setup, docs, changelog and privacy. It hosts the setup line (`/s`) and the demo install link.
## Users
The primary user is a **Steam Frame owner who isn't technical**. They have Quest games (often from SideQuest backups)
or PC VR games and want them in the Frame's Steam library without learning about APKs, patches or Proton. They work
from their PC with the headset nearby, and they may switch to the headset to play or to type the setup line in
Konsole.
Defaults, copy and the main paths serve that user. Depth is there for a second group: **tinkerers and recipe
contributors** who change patches and game settings, read launch tests and session triage, use Monitor and share
working recipes back to the catalog. Their tools sit behind Customize, "Show technical details" and the game's "…"
menu, and stay out of the main path.
## Product Purpose
FramePort installs Quest games, Android apps, Linux apps, PC VR games and Windows programs on the **Valve Steam
Frame**. It sets up the Frame once (one setup line, no root, no password), patches each game so it runs in Valve's
Lepton (Android) or Proton (Windows), uploads it, adds it to Steam with artwork, and tests that it starts.
Success means the user picks a game, clicks **Install on Frame**, and plays it from the Frame's Steam library. When a
game can't run, FramePort says so plainly and gives a next step instead of failing silently.
## Positioning
Other tools (FrameDrop, Valve's own tools) install an app as it is. FramePort **converts and patches** Quest games that
don't run on the Frame as they are. It uses community-tested recipes (131 catalog entries in `catalog/games/`) and
suggests patches for every other game, each explained in plain words. It is free and open source (GPL-3.0), and it
manages the Frame from the PC: Files, Screenshots, Live view, Type on Frame and Monitor.
## Operating Context
- **First run:** download, unpack, start FramePort, then **Steam Frame → Start setup**. On the Frame, run the setup
line in Konsole and click **Allow** when both sides show the same 4-digit code. Wi-Fi or a USB cable.
- **Daily use:** Add games (scan a folder, install link, Linux app, Windows program), then install, play, update and
uninstall from the Library. The game page holds the main action; patches are under Customize.
- **While playing:** the PC shows the Frame's state. The sidebar Frame card has battery and "now playing" fps, plus the
Monitor, Live view (in the browser), Screenshots and Files tabs.
- **When something breaks:** launch tests, session triage callouts ("Last session"), Report a problem (redacted
diagnostics, prefilled GitHub issue) and Share working config.
- Long jobs (uploads of several GiB) run in a background queue that can be cancelled and resumed. The window stays
responsive and shows progress in the sidebar activity cards.
## Capabilities and Constraints
- Terms and voice follow `docs/STYLE.md`: Steam Frame / the Frame, Quest game, PC VR game, patch, recipe, Install /
Update / Reinstall / Play, Set up / connect, This PC, sentence case, US spelling, short labels (four words at most).
- Every GUI text goes through `tr()` (`src/frameport/i18n.py`), so layouts must tolerate longer translated strings.
- GUI scale is user-set (`ui.scale`, auto under WSL); sizes go through theme tokens and `T.px()`, never bare pixels.
- Performance rules are binding: no image bytes in controls (artwork by URL thumbnails), no I/O in render paths, and
clickable controls are updated in place, never recreated on progress ticks.
- Reduce motion (`ui.reduce_motion`) turns off every animation.
- Docs screenshots and videos are generated by `scripts/showcase/` from a demo library, never by hand. New icon-only
controls need a tooltip so the showcase steps can find them.
- Headless tests prove that a game starts, never what it looks like. Tracking only works with the headset worn.
- Not every game runs. Some need Meta's services or hardware the Frame lacks (e.g. 32-bit-only APKs, Oculus Platform
SDK games). The product must say this, not hide it.
## Brand Commitments
- **Logo:** a monitor coming out of a tilted portal as the Frame (`src/frameport/ui/icons/logo.svg`,
`logo-solid.svg`; `docs/brand/`). The wordmark carries the portal.
- **Portal dichotomy:** orange = the main action and selection, blue = secondary actions, progress and the PC side.
`portal` is the default theme (`portal_oled` and `original` also exist).
- **Dark only.** No light mode; new themes are dark variants. User theme files that are light or unreadable are
refused.
- **Voice:** plain, technical wording that says what happens ("Steam restarts once"). No marketing adjectives
(`docs/STYLE.md`).
- **Credit** the projects FramePort wraps (OVRPort, Revive, Proton, Lepton, SteamVR and others): most of the
functionality is theirs.
- **No piracy tools.** FramePort doesn't download, share or unlock games and never bypasses entitlement checks.
- **No Quest2Frame mentions** anywhere.
- **Easter eggs** exist (`ui/easter.py`) and are never mentioned in the UI or docs.
## Evidence on Hand
- Generated screenshots in `docs/images/` (library, game, game settings, patches, install progress, frame, files,
screenshots, screenshot viewer, live view, monitor, type on Frame, appearance) and diagrams (`architecture.svg`,
`self-update.svg`).
- Videos: `docs/media/frameport-tour.mp4` and `docs/media/frameport-install.mp4` (about 90 s each), teaser
`docs/images/tour-teaser.webp`.
- Install button badges in `docs/badges/` (`docs/INSTALL_BUTTON.md`); brand icon and social preview in `docs/brand/`.
- Tested games list `docs/GAMES.md` and the recipe catalog `catalog/games/`. Real GitHub release, download and
contributor data feed the site (`site/scripts/releases.mjs`, `contributors.mjs`).
- **Absent, do not fabricate:** testimonials, user counts, press quotes, reviews or performance claims beyond what
the docs record.
## Product Principles
1. **One click for the owner, every switch for the tinkerer.** The main path never asks a question the recipe can
answer; depth lives one step away and is explained in plain words.
2. **Say what happens, including failure.** Every state, from setup to launch test to "can't run on the Frame", is
stated plainly with a next step.
3. **The Frame is a place you reach from your PC.** Files, screenshots, live view, typing and monitoring treat the
headset as connected, not remote.
4. **Never lose the user's work.** Saves, signing keys, picked artwork, renamed titles and interrupted uploads are
kept and resumable.
5. **Open and honest.** Credit the upstream projects, no piracy, no personal data in reports, and admit that not every
game runs.
## Accessibility & Inclusion
- Reduce motion turns off every animation, and the easter eggs too.
- User-adjustable GUI scale for high-DPI screens.
- Every text is translatable (`tr()`).
- Icon-only controls carry tooltips. These also feed Flutter's semantics tree, which the showcase uses.
- No formal WCAG target has been set (open decision).
+94 -103
View File
@@ -7,137 +7,127 @@
![Platforms](https://img.shields.io/badge/platforms-Windows%20%7C%20macOS%20%7C%20Linux-blue)
![Steam Frame](https://img.shields.io/badge/Steam%20Frame-supported-1b2838?logo=steam&logoColor=white)
Install games that target the Meta Quest, Android, or general PCVR onto your **Valve Steam Frame**. FramePort handles everything from uploading game files, setting up your Frame, injecting compatibility patches, and adding shortcuts to your Steam library. FramePort aims to be as simple as possible by taking advantage of the fact that the Steam Frame runs on Linux.
Install Quest games, Android apps, Linux apps and PC VR games on the **Valve Steam Frame**. FramePort sets up the
Frame, patches each game so it runs there, uploads it and adds it to your Steam library with artwork.
[![FramePort: library, one-click install, play, monitor](docs/images/tour-teaser.webp)](docs/media/frameport-tour.mp4)
▶ [Watch the full tour](docs/media/frameport-tour.mp4) · New to FramePort? [Watch the install tutorial](docs/media/frameport-install.mp4)
(about 90 seconds each, MP4; also attached to every release)
(about 90 seconds each)
> **Notice:** FramePort explicitly does NOT download, share, or unlock games. You must provide legally obtained game
> executables. Core features of FramePort simply download and wrap other published tools (see [Built on](#built-on))
> with patches provided by FramePort adding a hardware compatibility layer. This enables users to use games/apps legally
> purchased on sites like [SideQuest](https://sidequestvr.com/).
> **Notice:** FramePort doesn't download, share or unlock games. Use games you got legally, for example from
> [SideQuest](https://sidequestvr.com/). FramePort downloads published open-source tools (see [Built on](#built-on))
> and adds its own patches so games run on the Frame.
>
> Not every game runs on the Frame: recipes are tested by the community, and some games need Meta's services or
> hardware the Frame lacks.
## Features
![Library](docs/images/library.png)
- **Painless setup:** one short command on the Frame. No root, no `sudo`, no password.
[What it changes](docs/FRAME_SETUP.md).
- **Type on Frame:** use your computer's keyboard on the Frame: in VR apps, Android apps, Steam and the desktop.
[More](#type-on-frame).
- **One click per game:** convert, patch, sign, upload, add to Steam with artwork, launch test. Artwork that can't be found automatically can be picked from the stores or replaced with your own images.
- **Per-game recipes:** a tested catalog plus detection rules; every patch explained in plain words.
- **FrameBridge:** FramePort's OpenXR adapter emulates what the Frame natively lacks (passthrough, room, controller models,
curved and 360° layers); game settings as simple switches.
- **Beyond Quest:** Android apps as windows, PC VR via Proton or Revive, Windows (non-VR) games via Proton, a Files
tab with drag and drop.
- **Linux apps:** install Linux apps (AppImage, a folder, or a zip/tar archive) on the Frame with a Steam
shortcut; arm64 builds run natively on SteamOS, x86_64 builds through Valve's FEX translator.
- **Install links:** "Install with FrameDrop" buttons on web pages (the one-click protocol of the FrameDrop
sideloader) and pasted links open in FramePort, which asks, downloads, adds and installs the build.
- **Screenshots tab:** the screenshots you took in the headset, sorted by game (matched by play time) and day;
view them and download them to your computer.
- **Live view tab:** watch what the headset shows, with sound, in a browser window on your computer.
- **Monitor tab:** the running game's frame rate, the Frame's load, temperatures, power and battery live, and its
processes, which you can end. [More](#monitor).
- **Self-updating** releases, redacted diagnostics, one-click problem reports and working-config sharing.
- **Easy setup:** one line in the Frame's terminal. No root, no password. [What it changes](docs/FRAME_SETUP.md).
- **One click per game:** patch, upload, add to Steam with artwork and test that it starts.
- **Recipes:** tested patches and settings for 120+ games; other games get suggested patches, each explained in
plain words.
- **Game settings:** sharpness, refresh rate, controllers, 360° video and mixed reality as simple switches.
- **More than Quest games:** Android apps in a window, Linux apps, PC VR games and Windows programs.
- **Install links:** "Install with FramePort" buttons on websites ([frameport.app](https://frameport.app) links) open
in FramePort.
- **Your Frame from your PC:** Type on Frame (your keyboard on the Frame), Files, Screenshots, Live view (what the
headset shows, in your browser) and Monitor (frame rate, temperatures, battery, processes).
- **Updates itself**, and reports problems without personal data.
## Quick start
1. [Download](https://github.com/spoopyghosty0/frameport/releases/latest) and unzip the build for Windows, macOS
(Apple Silicon) or Linux, then start FramePort.
2. **Connect the Frame** (once):
1. In FramePort click **Steam Frame → Show setup command**. Keep FramePort open; the Frame and your computer must
be on the same Wi-Fi.
2. On the Frame open the **SteamVR dashboard → Launch a program → Desktop**: the Linux desktop opens on a virtual
screen.
3. Open the app menu (bottom-left corner of that desktop) → **System → Konsole** (or search for Konsole).
4. Type the command FramePort shows exactly as shown (on-screen keyboard or any USB/Bluetooth keyboard) and press
**Enter**. This will run the following [bash setup script](bootstrap/bootstrap.sh).
5. After a few seconds the desktop closes by itself (Steam restarts once); that's expected. If
Steam asks to install **Lepton**, confirm it. FramePort shows the Frame as connected within a minute. No
password needed.
3. **Add games → Scan a folder** with your game backups (APK + OBB, or PC VR game folders).
4. Open a game → **Install on Frame**, then play it from the Frame's Steam library.
Full guide, firewalls and troubleshooting: [docs/INSTALL.md](docs/INSTALL.md). Questions (e.g. how to lay out games
with OBB files): [docs/FAQ.md](docs/FAQ.md).
## Type on Frame
Typing in VR is painful, so FramePort turns your computer's keyboard into a keyboard for the Frame. Open **Type on
Frame** (its own tab in the sidebar), select a text field in the headset and type: searches, logins, chat, in any
app, in Steam or on the desktop. Paste longer text to type it in one go. Nothing to install: FramePort adds a virtual
keyboard on the Frame while the tab is open, without root.
![Type on Frame](docs/images/type-on-frame.png)
Unity apps whose text fields close the moment you select them on the Frame (no system keyboard there) get a per-game
fix, so Steam's on-screen keyboard and Type on Frame work in them too.
[Details](docs/INSTALL.md#typing-on-the-frame).
## Screenshots
Screenshots you take in the headset show up in FramePort's **Screenshots** tab, grouped by day and matched to the game
you were playing. Open one full size, step through them, and download single shots, a selection or all of them to
your computer.
![Screenshots](docs/images/screenshots.png)
![Screenshot viewer](docs/images/screenshot-viewer.png)
## Live view
The **Live view** tab streams what the headset shows, with its sound, to your computer: click **Start live view** and
it opens in your default web browser (full screen with a double-click; click **Sound on** to hear it, as browsers start
videos muted). Pick 360p to 1080p, or the headset view's full size. The picture comes from SteamVR's built-in headset
view on the Frame and is encoded there while you watch (about one CPU core), so stop it when you're done. It's black
while the headset sleeps.
## Monitor
The **Monitor** tab shows what the Frame is doing while it's open: the running game with its frame rate against the
display's refresh rate, CPU, graphics chip, memory, the hottest temperature with the fan speed, power draw and battery
time left, each with a 2-minute chart. **Show details** adds every CPU core, all temperature sensors, where the power
goes and the network. Below, the game's processes (or Steam's, or all of them) with their CPU, GPU and memory use:
right-click one to end it, or end the whole game. Programs that Steam, SteamVR or the desktop need are marked and ask
again. The Frame sends the numbers itself (about 1 % of one CPU core) and stops when you leave the tab.
Details for each: [Install and first steps](docs/INSTALL.md).
![Monitor](docs/images/monitor.png)
![Screenshots](docs/images/screenshots.png)
## Quick start
1. [Download](https://github.com/spoopyghosty0/frameport/releases/latest) the build for Windows, macOS (Apple
Silicon) or Linux, unpack it and start FramePort.
2. In FramePort click **Steam Frame → Start setup**. The Frame and your PC must be on the same Wi-Fi (or use a USB
cable).
3. On the Frame open the **SteamVR dashboard → Launch a program → Desktop**, then the app menu → **System →
Konsole**, and run:
```
curl -fsSL https://frameport.app/s | bash
```
4. Click **Allow** in FramePort when it shows the same 4-digit code as the Frame. Steam restarts once; if it asks to
install **Lepton** (Valve's Android runtime), confirm it.
5. **Add games → Scan a folder…** with your games, open one and click **Install on Frame**. Play it from the Frame's
Steam library.
Full guide, firewalls and troubleshooting: [Install and first steps](docs/INSTALL.md). How to lay out game folders:
[FAQ](docs/FAQ.md).
## Compatibility
If a game has already been tested with FramePort, it will automatically use the optimal game config. Otherwise, FramePort
will attempt to guess key patches. If you find a new config that works for an app you are testing, please consider submitting it to the community!
Tested games use their recipe (the patches and settings that work for them) automatically; for other games FramePort
suggests patches. See the [list of tested games](docs/GAMES.md). Got a game working? Share its recipe from the game's
**…** menu: [how](docs/INSTALL.md#share-a-recipe-or-report-a-problem).
**[List of tested games](docs/GAMES.md)**
## Compared with other tools
**Tested something? Share it.** In FramePort open the game → **…** → **Share working recipe…** (it fills in the
recipe for you) or **Report a problem…** (attaches a diagnostics zip with personal data removed). Without the app:
[share a working config](https://github.com/spoopyghosty0/frameport/issues/new?template=working-config.yml) · [report a problem](https://github.com/spoopyghosty0/frameport/issues/new?template=bug-report.yml). Shared configs become built-in recipes for everyone.
FrameDrop and Valve's own tools install an app as it is. FramePort differs in four ways:
- **Free and open source** (GPL-3.0). FrameDrop is free (donationware) without published source.
- **Quest games that don't run on the Frame as they are** get converted and patched, with a tested recipe for 120+
games. The others install the game unchanged.
- **Windows, macOS and Linux.** FrameDrop is for Windows.
- **Wi-Fi or a USB cable**, and no **Pair new host** step.
<details>
<summary>All differences</summary>
| | **FramePort** | **FrameDrop** | **By hand with Valve's tools** |
|---|---|---|---|
| Cost and source code | Free, open source (GPL-3.0) | Free (donationware), source not published | Free, from Valve |
| Runs on | Windows, macOS, Linux | Windows | Depends on the tool |
| First connection | One line in the Frame's terminal; it turns on Developer Mode itself. No password | Turn on Developer Mode, then **Pair new host** | Turn on Developer Mode and pair, or use Android's debug tool (adb) |
| Wireless or cable | Wi-Fi, the Frame's hotspot or a USB cable | Same Wi-Fi network | Wi-Fi, or adb |
| Quest games that don't run as they are | Converted and patched for the Frame | Installed as they are | Installed as they are |
| Knows which patches a game needs | Tested recipes for 120+ games, updated without an app update | Not stated | No |
| In the Steam library | Yes, with artwork and tags | As a "Devkit Game" shortcut | As "Devkit Game: &lt;title&gt;"; not with adb |
| Android apps, Linux apps, Windows programs | All three; Proton (runs Windows programs) is installed for you | All three; install Proton first | Yes; 2D Android apps need an extra file |
| PC VR games | On your PC; some also on the Frame | Not supported | Not supported |
| "Install with …" buttons on websites | Yes ([frameport.app](https://frameport.app) links) | Its own | No |
| A game doesn't start | A launch test reads the logs and suggests a patch | A log viewer | No help |
| Use the Frame from your PC | Live view, Monitor, Files, Screenshots, Type on Frame | Not stated | No |
</details>
Other tools as described on their own pages, checked 2026-10-09:
[FrameDrop about](https://framedropvr.com/about) · [how-to](https://framedropvr.com/how-to) ·
[Valve: loading games on Steam Frame](https://partner.steamgames.com/doc/steamhardware/steamframe/loadgames).
Out of date? [Report a problem](https://github.com/spoopyghosty0/frameport/issues/new).
## Built on
Most of the work is done by these projects:
[OVRPort](https://github.com/Android-XR-Bridge/OVRPort) (Quest → OpenXR, originally
[ovrport/app](https://github.com/ovrport/app)) · Valve Lepton, Proton and SteamVR ·
[Revive](https://github.com/LibreVR/Revive) · Mesa (Zink) · [Khronos OpenXR SDK](https://github.com/KhronosGroup/OpenXR-SDK)
· Eclipse Temurin, Android apksigner and NDK · [Flet](https://flet.dev) · OculusDB and Steam store data.
What FramePort adds itself: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
What FramePort adds: [Architecture](docs/ARCHITECTURE.md).
## Documentation
| | |
|---|---|
| [INSTALL.md](docs/INSTALL.md) | Install, connect, update, PC VR, files, problem reports |
| [FRAME_SETUP.md](docs/FRAME_SETUP.md) | What setup changes on the Frame, networks and firewalls, undoing it |
| [COMPATIBILITY.md](docs/COMPATIBILITY.md) | What runs and how well |
| [GAMES.md](docs/GAMES.md) | Tested games and how well they run |
| [PLAYBOOK.md](docs/PLAYBOOK.md) | Symptoms and fixes per game |
| [FRAME_RUNTIME.md](docs/FRAME_RUNTIME.md) | Steam Frame runtime facts |
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | How the code is organised |
| [CONTRIBUTING.md](CONTRIBUTING.md) | Code, recipes, translations |
| [Install and first steps](docs/INSTALL.md) | Install, connect, update, PC VR games, files, problem reports |
| [What the setup changes](docs/FRAME_SETUP.md) | What setup changes on the Frame, networks and firewalls, undoing it |
| [Compatibility](docs/COMPATIBILITY.md) | What runs and how well |
| [Tested games](docs/GAMES.md) | Tested games and how well they run |
| [FAQ](docs/FAQ.md) | Game folders and common questions |
| [Porting playbook](docs/PLAYBOOK.md) | Symptoms and fixes per game |
| [Steam Frame runtime reference](docs/FRAME_RUNTIME.md) | Facts about the Frame's runtime |
| [Architecture](docs/ARCHITECTURE.md) | How the code is organized |
| [Contributing](CONTRIBUTING.md) | Code, recipes, translations |
## Development
@@ -148,8 +138,9 @@ uv run frameport --help # command line
uv run pytest # tests
```
## AI Usage Notice
While I would like to program everything manually, I no longer have much free time for personal projects. As a result I make use of AI tools to make it significantly quicker to debug compatibility issues.
## AI usage
I don't have much free time for this project, so I use AI tools to debug compatibility problems faster.
## License
+2
View File
@@ -11,6 +11,8 @@ FramePort is GPL-3.0-only (see `LICENSE`). It includes or downloads the followin
| Android NDK runtime (statically linked libc++) | native Android libraries | Apache-2.0 with LLVM exception, plus legacy notices (`native/vrapi-bridge/licenses/ANDROID-NDK.txt`) |
| Flet and Flutter (desktop app runtime) | release bundles | Apache-2.0 / BSD-3-Clause |
| flet-dropzone / desktop_drop (drag-and-drop) | release bundles | Apache-2.0 / MIT |
| FFmpeg 7.1.1 hardware HEVC wrapper (LGPL configuration, without GPL/nonfree components) | `artifacts/hevc/libstagefrighthw.so`; source/rebuild instructions in `native/hevc/build.py` and `native/hevc/README.md` | LGPL-2.1-or-later (`artifacts/hevc/COPYING.FFmpeg`); unmodified source: https://ffmpeg.org/releases/ffmpeg-7.1.1.tar.xz |
| AOSP Android 11 native media/utility headers | `native/hevc/platform/` | Apache-2.0; copyright/license notices retained in the headers |
| Python packages (paramiko, zeroconf, psutil, pyelftools, capstone, UnityPy, PyYAML, requests, typer, pyaxmlparser, Pillow, cryptography, …) | release bundles | their own licenses (see each package's metadata) |
FramePort's own native code (the FrameBridge adapter, GL/Vulkan/OpenXR shims, the Windows helpers) is GPL-3.0-only.
+1843 -27
View File
File diff suppressed because it is too large. Load diff
+12 -5
View File
@@ -1,15 +1,22 @@
5becb96ee86fbda0ce98c0e0fd064f8ba092314aceaa400e3dcc785705dc3c23 ./arm64-v8a/libVkLayer_fp_shaderfix.so
e7554c6343ee2989b0a273ee6230e65c25bfe6499eefd00de19f5f0fee58754f ./arm64-v8a/libfp_langpack.so
321da502e0f8f46f0880aad39fe6b84bdef88f925164568814cf704a1b31a577 ./arm64-v8a/libfp_ovrp.so
40defdaddcda53bd2649eb48076fae1622bfc2bfc88e9e03c94fc8b45af2a4f2 ./arm64-v8a/libfp_ovrtrace.so
7522a7cfb236e48d3442d418c4820cd37760a9627c6bdef80fb0df82df5fd826 ./arm64-v8a/libfp_vk.so
e8c0965c188665a4e565e4a3af983ef1866d1cf9225665cc90d8df286bc96ef3 ./arm64-v8a/libfp_vk.so
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
5432674a59d02f411cd853a5dc57e3fa547d1ac518f25bf433fc54c43677177a ./arm64-v8a/libfpglmv.so
8a8f6b1da8952cb3933a5b53558424a5fc56043b2527a16bb6dd62a00d2de01a ./arm64-v8a/libframe_xrshim.so
fc559dbe0b87eecc3641256837293260cefb7c2ce7b21d808d10edd2a2dba9fc ./arm64-v8a/libglshim.so
cc5b493cadefe76ad13fd096d3f8494a140ba593e0e1692db12d284bc548c1cf ./arm64-v8a/libopenxr_loader_generic.so
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
449e13bbe58de6718d4e530177924e09552a95d748a082739899144e2cb3ccf3 ./arm64-v8a/libopenxr_loader_generic.so
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
aa4dc0020c77e12d41ef6ce80b23ad90c9cb7e882338eddf2261efe8645cf38d ./arm64-v8a/libvrapi.so
c10412dd76a65a1d587b7ed226367d1a6f2bec238c733c97ee67eb8870c868e8 ./armeabi-v7a/libopenxr_loader_generic.so
1d83eb94ec1df44f29a5f525dce4d5b608dbbb13a9c7a5efeb80a4b1dc54a721 ./arm64-v8a/libvrapi.so
47f736dfeffce6aa56be2fc049c2d11b87b0d1f7389db7ee1737fcded9457811 ./armeabi-v7a/libopenxr_loader_generic.so
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c ./hevc/libstagefrighthw.so
4516971bbb2b635e19b14d2d37b9353962876f7de02043e7457327052c1fcbc0 ./hevc/manifest.json
46c3dcad2c43cc30bea3b3680b362ed84a99c15d60714bbbbb422c80ecf90405 ./hevc/media_codecs_frameport.xml
529ea35d5d82474afe9e09ad1216d269317f56dd24a05fb100fced52b7b4dbca ./hevc/podman.py.txt
1aa733117cf57ccff7cf23f0425dbfd73b0332a3ef1c905ceeca0ccd0449c4e0 ./linux-arm64/XR_APILAYER_FRAMEPORT_timefix.json
28c2430a02bbd8902c5bfb9562c6fd0e318654f9e05c095bfeb94b0450ab1b07 ./linux-arm64/libxr_frameport_timefix.so
b10b3a5c10c3339fc63fd9f6cb4d01ea29be11a863d0ebad1389ea69fd471940 ./linux-arm64-bin/fp_venc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+502
View File
@@ -0,0 +1,502 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
{
"revision": 8,
"runtime_sha256": "456e912c75cd389abcf6a63bc80e2a53bdc334371d00b200c93680388ae955e2",
"files": {
"libstagefrighthw.so": "141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c",
"podman.py": "529ea35d5d82474afe9e09ad1216d269317f56dd24a05fb100fced52b7b4dbca",
"media_codecs_frameport.xml": "46c3dcad2c43cc30bea3b3680b362ed84a99c15d60714bbbbb422c80ecf90405",
"COPYING.FFmpeg": "b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe"
},
"codecs": [
"video/hevc",
"video/avc",
"video/x-vnd.on2.vp9"
],
"build": {
"ndk_revision": "27.2.12479018",
"ffmpeg_source_sha256": "733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1"
}
}
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<MediaCodecs>
<Decoders>
<MediaCodec name="OMX.frameport.hevc.decoder" type="video/hevc" rank="64">
<Limit name="size" min="128x128" max="8192x8192" />
<Limit name="alignment" value="2x2" />
<Limit name="block-size" value="16x16" />
<Limit name="block-count" range="1-138240" />
<Limit name="blocks-per-second" range="1-7864320" />
<Limit name="bitrate" range="1-245000000" />
<Limit name="concurrent-instances" max="1" />
</MediaCodec>
<MediaCodec name="OMX.frameport.avc.decoder" type="video/avc" rank="64">
<Limit name="size" min="128x128" max="8192x8192" />
<Limit name="alignment" value="2x2" />
<Limit name="block-size" value="16x16" />
<Limit name="block-count" range="1-138240" />
<Limit name="blocks-per-second" range="1-7864320" />
<Limit name="bitrate" range="1-245000000" />
<Limit name="concurrent-instances" max="1" />
</MediaCodec>
<MediaCodec name="OMX.frameport.vp9.decoder" type="video/x-vnd.on2.vp9" rank="64">
<Limit name="size" min="128x128" max="4096x2304" />
<Limit name="alignment" value="2x2" />
<Limit name="block-size" value="16x16" />
<Limit name="block-count" range="1-36864" />
<Limit name="blocks-per-second" range="1-2211840" />
<Limit name="bitrate" range="1-245000000" />
<Limit name="concurrent-instances" max="1" />
</MediaCodec>
</Decoders>
</MediaCodecs>
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/python3
"""Add shared codecs to Lepton containers without editing their shared rootfs."""
import hashlib
import json
import os
import re
import shutil
import sys
from pathlib import Path
from xml.etree import ElementTree as ET
def mounts(directory, config, args):
if not args or args[0] != "run":
return []
name = None
for i, arg in enumerate(args):
if arg == "--name" and i + 1 < len(args):
name = args[i + 1]
elif arg.startswith("--name="):
name = arg.partition("=")[2]
if config.get("scope") == "shared":
# The common launcher supplies these values. Do not intercept arbitrary
# Podman containers or depend on a game's package/modified APK.
appid = os.environ.get("SteamAppId", "")
if not re.fullmatch(r"[0-9]+", appid) or name != f"lepton-steamlaunch-{appid}":
return []
app = Path(os.environ.get("STEAM_COMPAT_INSTALL_PATH", ""))
if app.name != "lepton-app" or not app.is_dir():
return []
root_arg = None
for i, arg in enumerate(args):
if arg == "--rootfs" and i + 1 < len(args):
root_arg = args[i + 1]
elif arg.startswith("--rootfs="):
root_arg = arg.partition("=")[2]
if not root_arg or not root_arg.endswith(":O"):
return []
root = Path(root_arg[:-2]).resolve()
else: # old per-game deployments remain compatible during migration
if name != f"lepton-steamlaunch-{config['appid']}":
return []
root = Path(config["lepton"]).resolve().parent / "images" / "rootfs"
expected_root = str(root) + ":O"
if not any(arg in (expected_root, "--rootfs=" + expected_root) for arg in args):
return []
device = Path("/dev/video-dec0")
runtime = root / "vendor/lib64/libstagefright_softomx.so"
upstream_plugin = (root / "vendor/lib64/libstagefrighthw.so").exists()
for i, arg in enumerate(args):
if arg == "--mount" and i + 1 < len(args):
fields = dict(item.split("=", 1) for item in args[i + 1].split(",") if "=" in item)
target = fields.get("destination", fields.get("target"))
if target == "/vendor/lib64/libstagefrighthw.so":
upstream_plugin = True
if target == "/vendor/lib64/libstagefright_softomx.so" and fields.get("source"):
runtime = Path(fields["source"])
if upstream_plugin:
print("FramePort video: using the runtime's hardware codec plugin", file=sys.stderr)
return []
if not device.exists() or hashlib.sha256(runtime.read_bytes()).hexdigest() != config["runtime_sha256"]:
print("FramePort video: device or runtime ABI differs; retaining the stock codecs", file=sys.stderr)
return []
xml = ET.parse(root / "vendor/etc/media_codecs.xml")
if not any(node.get("href") == "media_codecs_frameport.xml" for node in xml.getroot().findall("Include")):
ET.SubElement(xml.getroot(), "Include", href="media_codecs_frameport.xml")
# Immutable plugin versions can serve simultaneous app launches and
# different Lepton installations. Never share a temporary XML filename.
# The version directory stays as the agent verified it: the merged list goes to the user's runtime dir.
root_key = hashlib.sha256(str(root).encode()).hexdigest()[:16]
merged = merged_dir() / f"media_codecs.{root_key}.xml"
temporary = merged.with_suffix(f".{os.getpid()}.tmp")
xml.write(temporary, encoding="utf-8", xml_declaration=True)
temporary.replace(merged)
# Lepton supplies its own /dev tmpfs. A Podman --device node disappears
# beneath it; a bind mount matches Lepton's existing GPU/sound device setup.
result = ["--mount", f"type=bind,source={device},destination=/dev/video-dec0,rw"]
for path, target in (
(directory / "libstagefrighthw.so", "/vendor/lib64/libstagefrighthw.so"),
(merged, "/vendor/etc/media_codecs.xml"),
(directory / "media_codecs_frameport.xml", "/vendor/etc/media_codecs_frameport.xml"),
):
if not path.is_file():
raise FileNotFoundError(f"missing video codec mount: {path}")
result += ["--mount", f"type=bind,source={path},destination={target},ro"]
print("FramePort video: loading the Iris hardware codec plugin for this container", file=sys.stderr)
return result
def merged_dir():
runtime = os.environ.get("XDG_RUNTIME_DIR", "")
base = Path(runtime) if runtime and Path(runtime).is_dir() else Path.home() / ".cache"
path = base / "frameport-video"
path.mkdir(mode=0o700, parents=True, exist_ok=True)
return path
def switched_off(directory):
"""FRAMEPORT_NO_HW_VIDEO=1 (e.g. in a game's Steam launch options) or the Frame-wide switch (FramePort's
setting; the agent writes video-codec/disabled) leave every container with Android's stock codecs."""
if os.environ.get("FRAMEPORT_NO_HW_VIDEO", "") not in ("", "0"):
return True
return directory.parent.name == "versions" and (directory.parent.parent / "disabled").exists()
def real_podman(directory):
"""Find Podman independently of deployment.json, without recursing into this wrapper."""
own_bin = (directory / "bin").resolve()
own_script = Path(__file__).resolve()
# Lepton runs its `podman exec` calls (boot wait, app pid, logcat mirror) with the Android guest's PATH
# (/product/bin:/system/bin:...), which has no host Podman: the system folders come after PATH. Failing there
# broke Lepton's logcat mirror and app-pid checks, and the container was stopped early.
entries = os.environ.get("PATH", os.defpath).split(os.pathsep) + ["/usr/local/bin", "/usr/bin", "/bin"]
for entry in entries:
folder = Path(entry or os.curdir).resolve()
if folder == own_bin:
continue
found = shutil.which("podman", path=str(folder))
if found:
executable = Path(found).resolve()
if executable.parent != own_bin and executable != own_script:
return str(executable)
raise RuntimeError("no real Podman executable found outside the codec wrapper directory")
def main():
directory = Path(__file__).resolve().parent.parent
args = sys.argv[1:]
fallback = real_podman(directory)
if switched_off(directory):
os.execv(fallback, [fallback, *args])
try:
config = json.loads((directory / "deployment.json").read_text())
podman = Path(config.get("podman", fallback)).resolve()
if podman.parent == (directory / "bin").resolve() or podman == Path(__file__).resolve():
raise ValueError("configured Podman points to the codec wrapper")
extra = mounts(directory, config, args)
launch_args = [args[0], *extra, *args[1:]] if extra else args
os.execv(str(podman), [str(podman), *launch_args])
except Exception as exc: # a codec/configuration failure must never prevent the stock container from starting
print(f"FramePort video: retaining stock codecs: {exc}", file=sys.stderr)
os.execv(fallback, [fallback, *args])
if __name__ == "__main__":
main()
+7 -8
View File
@@ -30,13 +30,13 @@ user_systemd() {
say "FramePort setup for $(hostname) ($(. /etc/os-release; echo "$NAME $VERSION_ID"))"
say "Authorizing the FramePort app's key"
say "Letting FramePort log in"
key=$(curl -fsS "$PC_URL/key?code=$PAIR_CODE")
[[ "$key" == ssh-ed25519\ * ]] || { echo "Could not fetch the app's key from $PC_URL (is the app still open?)"; exit 1; }
[[ "$key" == ssh-ed25519\ * ]] || { echo "Couldn't reach FramePort at $PC_URL. Is it still open?"; exit 1; }
mkdir -p ~/.ssh && chmod 700 ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys
grep -qxF "$key" ~/.ssh/authorized_keys || echo "$key" >> ~/.ssh/authorized_keys
say "Configuring podman for Lepton"
say "Setting up Lepton's containers"
# rootless podman leaks one kernel keyring per container start; ~200 game launches would exhaust the quota
mkdir -p ~/.config/containers
grep -qs '^ *keyring *=' ~/.config/containers/containers.conf || printf '[containers]\nkeyring = false\n' >> ~/.config/containers/containers.conf
@@ -117,7 +117,7 @@ JOB
if [[ -f /etc/steamos-devkit-enabled ]]; then
say "Developer Mode is on"
bash "$JOB" finish "$PC_URL" "$PAIR_CODE" "$STEAM_CONFIG" "$DEVKIT_HELPER" 2>&1 | tee "$LOG"
say "Done. Return to FramePort on your PC: this Frame should now appear as connected."
say "Done. FramePort on your PC shows this Frame as connected."
exit 0
fi
@@ -126,9 +126,8 @@ if [[ ! -x "$DEVKIT_HELPER" || ! -f "$STEAM_CONFIG" ]] || \
! user_systemd systemd-run --user --collect --quiet --unit="frameport-setup-$$" \
bash -c 'bash "$0" "$@" >"$HOME/.cache/frameport-setup.log" 2>&1' \
"$JOB" devmode "$PC_URL" "$PAIR_CODE" "$STEAM_CONFIG" "$DEVKIT_HELPER"; then
echo "Couldn't turn it on automatically. Turn it on in Settings > System > Developer, then run this command again."
echo "Couldn't turn it on. Turn it on in Settings → System → Enable Developer Mode, then run this again."
exit 1
fi
echo "Steam restarts to turn it on. That closes the desktop in a few seconds and the Frame returns to its"
echo "normal view; setup finishes on its own. If Steam asks to install Lepton, confirm it."
echo "Then return to FramePort on your PC: the Frame appears as connected within a minute."
echo "Steam restarts and the desktop closes. Confirm Lepton if asked."
echo "Then check FramePort on your PC."
Loaded 100 of 597 files, more files were not shown because too many files have changed in this diff. Show more