18 Commits
Author SHA1 Message Date
Pixelplanet 18a50d38ab chore: enforce LF line endings via .gitattributes for shell scripts and CI 2026-10-18 16:15:31 +02:00
Pixelplanet c404131924 feat(branding): add portrait capsule, landscape header, and transparent logo for Steam library 2026-10-18 15:15:30 +02:00
Pixelplanet 09a5f73393 feat: embed official hero banner in README and integrate hero artwork across all deploy scripts 2026-10-18 15:00:53 +02:00
Pixelplanet dfc1664806 feat(assets): finalize Steam hero banner with headset on left, uncut title, and refined specs 2026-10-18 14:54:17 +02:00
Pixelplanet 6c0bfd4e4a feat(assets): update hero banner with Steam Frame on left, uncut title, and refined specs 2026-10-18 14:49:01 +02:00
Pixelplanet 6bfec8bf19 feat(assets): adopt Style 2 as official icon and add 3 Steam hero banner options 2026-10-18 14:44:10 +02:00
Pixelplanet 1c18c2c328 feat(assets): add authentic Steam Frame 16-bit pixel art icon styles 1 and 2 2026-10-18 14:27:10 +02:00
Pixelplanet c8e738ef21 feat(assets): add authentic Valve Steam Frame app icon variants for 1, 2, and 4 2026-10-18 09:56:17 +02:00
Pixelplanet 7e5915e115 feat: add authentic Steam Frame oval visor and rear battery icon designs 2026-10-18 09:43:21 +02:00
Pixelplanet be5ea10875 feat: add refined Steam Frame standalone icon redesigns (pancake optics, 4-camera array, rear battery) 2026-10-18 09:37:45 +02:00
Pixelplanet ba662f848b feat: add Steam Frame hardware-accurate app icon variants 2026-10-18 09:33:46 +02:00
Pixelplanet aa896397fe feat: add pixelated app icons and remove devkit prefix from steam shortcuts 2026-10-18 09:29:59 +02:00
Pixelplanet 7bf367419f docs: update deploy guide, readme links, and discreet vault instructions 2026-10-17 22:39:45 +02:00
Pixelplanet 83cf192d2d docs: clarify install options and SSH prerequisites 2026-10-17 22:26:49 +02:00
Pixelplanet 2a613ebed5 feat: add remote PC installer and double-clickable USB desktop installer 2026-10-17 22:21:37 +02:00
Pixelplanet 8454ab5de3 docs: blur real filenames in mockup screenshots for privacy 2026-10-17 22:15:06 +02:00
Pixelplanet 2c74923701 docs: add UI screenshot gallery to README and automated capture tool 2026-10-17 21:59:13 +02:00
Pixelplanet 22ed9e1ede fix(install): support case-insensitive Shortcuts key in shortcuts.vdf and copy install.sh from unpacked tarball 2026-10-17 20:55:13 +02:00
63 changed files with 479 additions and 37 deletions

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
# Force LF line endings for scripts and workflows so Linux/SteamOS execution never breaks
*.sh text eol=lf
*.py text eol=lf
*.yml text eol=lf
*.desktop text eol=lf
*.spv binary
*.png binary
*.jpg binary
*.jpeg binary
+80 -13
View File
@@ -1,3 +1,7 @@
<p align="center">
<img src="assets/hero.png" width="100%" alt="Pixel VR Player" />
</p>
# Pixel VR Player
A native VR video player for the **Valve Steam Frame**, playing flat 2D, 180°, and
@@ -13,25 +17,55 @@ A native VR video player for the **Valve Steam Frame**, playing flat 2D, 180°,
> NV12) with zero-copy dma-buf import; flat / 180° / 360° projection and mono /
> SBS (including half-SBS) / TB stereo, auto-detected from the filename; spatial
> audio with a master clock. In-headset UI: file browser (with file type and
> size), transport bar, settings menu, and a downloads manager. Native Valve
> size), transport bar, settings menu, downloads manager, and an encrypted hidden vault. Native Valve
> Steam Frame controller support (`XR_VALVE_frame_controller_interaction`) using
> the runtime's own render model, with per-button animation on the model and
> press-to-bind remapping.
## Install (prebuilt release)
## Screenshots
One command, from a terminal on the headset (Desktop Mode or over SSH):
<p align="center">
<img src="docs/ui-redesign/mockup-unified-1-files.jpg" width="49%" alt="Spatial File Browser" />
<img src="docs/ui-redesign/mockup-unified-2-downloads.jpg" width="49%" alt="Downloads Manager" />
</p>
<p align="center">
<img src="docs/ui-redesign/mockup-unified-3-settings.jpg" width="49%" alt="Settings Panel" />
<img src="docs/ui-redesign/mockup-unified-4-cinema-hud.jpg" width="49%" alt="Cinema Playback HUD" />
</p>
## Installation
Choose whichever method fits your setup:
### Option A: USB Drive / MicroSD Card (No typing & No Dev Mode needed)
The simplest method for standard users without touching the terminal:
1. Download `pixelvr-linux-arm64.tar.gz` from the [Releases](https://github.com/Pixelplanet/Pixel-VR-Player/releases) page onto a USB flash drive or MicroSD card.
2. Plug it into your Steam Frame and switch to **Desktop Mode**.
3. Open the USB drive in Dolphin and double-click **`Install Pixel VR Player.desktop`**.
4. A confirmation dialog will appear when finished. Return to Gaming Mode and launch it directly from your Steam Library!
### Option B: One command in headset terminal
If you are already in Desktop Mode on the Steam Frame, open **Konsole** and paste:
```bash
curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | sh
```
It downloads the latest ARM64 release, verifies its checksum, installs it to
`~/PixelVRPlayer`, and adds **Pixel VR Player** to your Steam library as a
Non-Steam Game. Restart Steam if it was already running. SteamVR must be
installed.
*(No root access or disabled read-only filesystem needed; it installs entirely into user space).*
Launch **Pixel VR Player** from your library, or directly:
### Option C: From your PC over Wi-Fi (Requires SSH or Dev Mode)
If you have SSH enabled or Developer Mode turned on with your headset paired:
- **Windows:** Double-click [`tools/install-from-pc.bat`](tools/install-from-pc.bat) and enter your headset's IP.
- **macOS / Linux:** Run `./tools/install-from-pc.sh`.
The script connects over SSH, installs the app, and registers the Steam shortcut remotely.
Launch **Pixel VR Player** directly from your Steam library, or from terminal:
```bash
~/PixelVRPlayer/run.sh # opens the in-headset file browser
@@ -48,11 +82,12 @@ Installer options:
| `--restart-steam` | close Steam first, then reopen it so the entry appears at once |
| `--uninstall` | remove the app and its Steam shortcut |
Re-run the same command to upgrade. Settings (`~/.config/pixelvr`) and cached
thumbnails (`~/.cache/pixelvr`) live outside the install directory and are kept.
Re-run the same command to upgrade. Settings (`~/.config/pixelvr`), cached
thumbnails (`~/.cache/pixelvr`), and hidden vault media (`~/.pixelvr_vault`) live
outside the install directory and are kept intact.
**Offline install:** download `pixelvr-linux-arm64.tar.gz` from the
[Releases](../../releases) page, copy it to the headset, extract it and run
[Releases](https://github.com/Pixelplanet/Pixel-VR-Player/releases) page, copy it to the headset, extract it and run
`./install.sh` from the extracted folder — or use `run.sh` directly. The app logs
to `~/pixelvr-run.log`.
@@ -86,6 +121,39 @@ the file browser rather than stacking on top of it.
All button bindings can be changed in **Settings ▸ Controls** (press-to-bind).
## Discreet Media Vault
Pixel VR Player includes a private, zero-trace local storage vault (`~/.pixelvr_vault`) hidden from the standard file browser and startup directory scans.
<details>
<summary>🔒 <b>Accessing the Hidden Vault</b> (click to expand)</summary>
To open or re-lock the vault in VR without bringing up an on-screen keyboard or visible PIN dialog:
1. Fully squeeze either controller's **grip** (hold down the mechanical **Full Press** click).
2. While holding the grip clicked, enter the 6-key factory combination:
> 🎵 *The Swedish pop sensation that won Eurovision in 1974 with "Waterloo"...*
> 📐 *...followed by the two classic axes of the 2D Cartesian plane.*
<details>
<summary>👉 <i>Need the direct button sequence? Click here to reveal</i></summary>
Hold **Full Grip Click** and press:
> **`A` → `B` → `B` → `A` → `X` → `Y`**
*(Within 2.5 seconds per step. Haptic feedback confirms each input).*
</details>
- **Instant Auto-Lock:** Re-locks automatically and immediately purges in-memory textures when the headset proximity sensor detects removal, or when clicking **[ 🔒 Lock Vault ]** in the sidebar.
- **Custom Combinations:** You can record your own custom button sequence anytime in **Settings ▸ Vault & Privacy**.
- **Move to Vault:** While unlocked, local videos can be moved directly into the vault from the browser.
- **Safe Persistence:** Media inside `~/.pixelvr_vault` is never deleted when updating or uninstalling the app.
For full architecture and security details, see the [Hidden Vault Technical Specification](docs/HIDDEN_VAULT_SPEC.md).
</details>
## Repository layout
```
@@ -195,8 +263,7 @@ choice, and troubleshooting). In short, after a one-time Developer-Mode pairing:
# override the target with --host steamos@<ip> or FRAME_HOST
```
Either way the app appears in **Steam ▸ Library ▸ Non-Steam** as *Devkit Game:
PixelVRPlayer*. To run it manually over SSH for debugging:
Either way the app appears in **Steam ▸ Library ▸ Non-Steam** as **Pixel VR Player**. To run it manually over SSH for debugging:
```bash
ssh steamos@frame '~/devkit-game/PixelVRPlayer/launch.sh /path/to/video.mp4'
Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

+40 -16
View File
@@ -12,17 +12,20 @@ libraries.
## Just installing a release (no devkit, no build)
End users do not need any of the steps below. From a terminal on the headset
(Desktop Mode or over SSH):
End users do not need to build anything or use devkits. Choose whichever option fits your setup:
```bash
curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | sh
```
1. **USB Drive / MicroSD Card (No typing & No Dev Mode needed):**
Download `pixelvr-linux-arm64.tar.gz` from GitHub Releases onto a USB flash drive or MicroSD card, plug it into the headset, switch to **Desktop Mode**, and double-click **`Install Pixel VR Player.desktop`**.
2. **From your PC over Wi-Fi (Requires SSH or Dev Mode):**
Run [`tools/install-from-pc.bat`](../tools/install-from-pc.bat) (Windows) or [`tools/install-from-pc.sh`](../tools/install-from-pc.sh) (Mac/Linux) and enter the headset's IP address.
3. **One command in terminal (Desktop Mode / SSH):**
```bash
curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | sh
```
That verifies the checksum, installs to `~/PixelVRPlayer`, and registers
**Pixel VR Player** as a Non-Steam Game. Relaunch Steam if it was running. Rerun
the same command to upgrade; `--uninstall` removes it. The rest of this document
covers building and iterating as a developer.
The installer verifies checksums, installs to `~/PixelVRPlayer`, and registers **Pixel VR Player** as a Non-Steam Game in your library. Re-running upgrades in-place while keeping all settings, thumbnails, and hidden vault files (`~/.pixelvr_vault`).
The rest of this document covers building and deploying as a developer.
---
@@ -38,8 +41,11 @@ covers building and iterating as a developer.
# 2b. Or stage a clean folder for the GUI Devkit Client:
./tools/stage.sh # -> dist/PixelVRPlayer/
# 2c. Or build the release tarball end users install from:
./tools/package-release.sh build-aarch64 pixelvr-linux-arm64.tar.gz v1.3.0
# 2c. Or assemble release tarball with desktop installer:
./tools/package-release.sh build-aarch64 pixelvr-linux-arm64.tar.gz v1.4.2
# 2d. Capture in-headset screenshots over OpenVR / SteamVR compositor:
python tools/take_headset_screenshot.py --delay 5
```
---
@@ -80,7 +86,7 @@ All three methods below share the same setup. Do this once.
| Method | Per-deploy effort | Best for |
|--------|-------------------|----------|
| **A. Devkit Client (GUI)** | click Upload → Start | first run; the official, guided path |
| **B. `tools/deploy.py` (CLI)** | one command | fast iteration during development |
| **B. `tools/deploy.sh` (CLI)** | one command | fast iteration during development |
| **C. Manual SSH + scp** | a few commands | debugging, capturing logs live |
| ~~ADB / Android~~ | — | APKs only — **not** this app |
@@ -88,8 +94,7 @@ All three methods below share the same setup. Do this once.
- **Easiest to iterate:** Option B (one command builds nothing you didn't ask for
and launches in the headset).
They all end up in the same place: *Steam ▸ Library ▸ Non-Steam ▸ **Devkit Game:
PixelVRPlayer***.
They all end up in the same place: *Steam ▸ Library ▸ Non-Steam ▸ **Pixel VR Player***.
---
@@ -121,7 +126,7 @@ The official, no-scripting path.
---
## Option B — One command (`tools/deploy.py`)
## Option B — One command (`tools/deploy.sh`)
Best for the edit-build-run loop. Copies the binary, shaders and `launch.sh` into
`~/devkit-game/PixelVRPlayer/`, registers the Non-Steam shortcut, and (optionally)
@@ -174,7 +179,7 @@ differ in which system libraries it sees:
environment. This is what Valve's docs recommend and what the **GUI (Option A)**
selects. Good default.
- **Directly on the host** — uses SteamOS's own FFmpeg/SteamVR libraries. This is
what **`deploy.py` (Option B)** registers, matching how a shipping native OpenXR
what **`deploy.sh` (Option B)** registers, matching how a shipping native OpenXR
player runs.
Both expose the SteamVR OpenXR runtime, so both can work. If the app fails to load
@@ -225,6 +230,25 @@ Requirements:
---
## Taking in-headset screenshots
To capture real in-headset screenshots directly from the SteamVR compositor while the app is running:
```bash
# Capture with a 5-second countdown to put the headset on:
python tools/take_headset_screenshot.py --delay 5
# Point to a specific headset IP:
python tools/take_headset_screenshot.py --host steamos@192.168.178.61 --delay 3
# Continuous series (e.g. 5 captures spaced 2 seconds apart):
python tools/take_headset_screenshot.py --interval 2 --count 5
```
The script connects over SSH, triggers SteamVR's compositor screenshot API, fetches the resulting PNGs, and places them into `docs/screenshots/` with both the raw stereo composite (`_stereo.png`) and a cropped single-eye preview (`_preview.png`).
---
## Troubleshooting
| Symptom | Likely cause / fix |
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 549 KiB

After

Width:  |  Height:  |  Size: 207 KiB

+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Name=Install Pixel VR Player
Comment=Install Pixel VR Player for Valve Steam Frame
Exec=bash -c 'SCRIPT_DIR="$(dirname "%k")"; if [ -f "$SCRIPT_DIR/install.sh" ]; then cd "$SCRIPT_DIR" && bash install.sh; else curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | bash; fi; if [ $? -eq 0 ]; then command -v kdialog >/dev/null && kdialog --title "Pixel VR Player" --msgbox "Installation complete!\n\nPixel VR Player has been added to your Steam Library.\nYou can now return to Gaming Mode."; else command -v kdialog >/dev/null && kdialog --title "Pixel VR Player" --error "Installation encountered an issue.\nCheck ~/pixelvr-install.log or terminal output."; fi; read -p "Press Enter to close..." dummy'
Icon=steam
Terminal=true
Type=Application
Categories=AudioVideo;Player;VR;
+83 -2
View File
@@ -127,7 +127,7 @@ def main() -> int:
'mkdir -p "$target/shaders"; printf %s "$target"'
)
# 2. Copy binary, shaders and the direct-run helper.
# 2. Copy binary, shaders, launcher, and icon.
print("Copying binary ...")
scp([str(binary)], f"{REMOTE_DIR}/pixelvr")
if shaders:
@@ -136,6 +136,10 @@ def main() -> int:
launch_helper = PROJECT_ROOT / "tools" / "launch.sh"
if launch_helper.is_file():
scp([str(launch_helper)], f"{REMOTE_DIR}/launch.sh")
for asset_name in ("icon.png", "hero.png", "capsule.png", "header.png", "logo.png"):
asset_file = PROJECT_ROOT / "assets" / asset_name
if asset_file.is_file():
scp([str(asset_file)], f"{REMOTE_DIR}/{asset_name}")
# 3. Register the Non-Steam shortcut (runs on the host, not in Sniper).
request = {
@@ -165,6 +169,83 @@ def main() -> int:
if isinstance(response, dict) and response.get("error"):
raise RuntimeError(f"shortcut registration failed: {response['error']}")
# 3b. Sanitize shortcuts.vdf to remove the 'Devkit Game: ' prefix and set icon.
sanitize_script = (
"import glob, os, struct, shutil\n"
"OBJ, STR, INT, U64, END = 0, 1, 2, 7, 8\n"
"def read_cstr(buf, pos):\n"
" end = buf.find(b'\\0', pos)\n"
" return buf[pos:end].decode('utf-8', 'surrogateescape'), end + 1\n"
"def parse_obj(buf, pos):\n"
" entries = {}\n"
" while pos < len(buf):\n"
" kind = buf[pos]\n"
" if kind in (END, 0x0B): return entries, pos + 1\n"
" key, pos = read_cstr(buf, pos + 1)\n"
" if kind == OBJ: value, pos = parse_obj(buf, pos)\n"
" elif kind == STR: value, pos = read_cstr(buf, pos)\n"
" elif kind == INT: value = struct.unpack_from('<i', buf, pos)[0]; pos += 4\n"
" elif kind == U64: value = struct.unpack_from('<Q', buf, pos)[0]; pos += 8\n"
" else: break\n"
" entries[key] = [kind, value]\n"
" return entries, pos\n"
"def dump(entries):\n"
" out = bytearray()\n"
" for key, (kind, value) in entries.items():\n"
" kb = key.encode('utf-8', 'surrogateescape')\n"
" if kind == OBJ: out += bytes([OBJ]) + kb + b'\\0' + dump(value) + bytes([END])\n"
" elif kind == STR: out += bytes([STR]) + kb + b'\\0' + value.encode('utf-8', 'surrogateescape') + b'\\0'\n"
" elif kind == INT: out += bytes([INT]) + kb + b'\\0' + struct.pack('<i', value)\n"
" elif kind == U64: out += bytes([U64]) + kb + b'\\0' + struct.pack('<Q', value)\n"
" return bytes(out)\n"
"for vdf in glob.glob(os.path.expanduser('~/.steam/steam/userdata/*/config/shortcuts.vdf')):\n"
" try:\n"
" with open(vdf, 'rb') as f: data = f.read()\n"
" if not data or data[0] != OBJ: continue\n"
" key, pos = read_cstr(data, 1)\n"
" s_dict, _ = parse_obj(data, pos)\n"
" mod = False\n"
" for k, v in s_dict.items():\n"
" if isinstance(v, list) and v[0] == OBJ:\n"
" e = v[1]\n"
" name = e.get('AppName', [STR, ''])[1]\n"
" if 'Pixel' in name:\n"
" e['AppName'] = [STR, 'Pixel VR Player']\n"
" e['Devkit'] = [INT, 0]\n"
" e['DevkitGameID'] = [STR, '']\n"
" icon_p = os.path.expanduser('~/" + REMOTE_DIR + "/icon.png')\n"
" hero_p = os.path.expanduser('~/" + REMOTE_DIR + "/hero.png')\n"
" capsule_p = os.path.expanduser('~/" + REMOTE_DIR + "/capsule.png')\n"
" header_p = os.path.expanduser('~/" + REMOTE_DIR + "/header.png')\n"
" logo_p = os.path.expanduser('~/" + REMOTE_DIR + "/logo.png')\n"
" grid = os.path.join(os.path.dirname(os.path.dirname(vdf)), 'config', 'grid')\n"
" os.makedirs(grid, exist_ok=True)\n"
" raw_id = e.get('appid', [INT, 0])[1] & 0xFFFFFFFF\n"
" if os.path.isfile(icon_p):\n"
" e['icon'] = [STR, icon_p]\n"
" shutil.copyfile(icon_p, os.path.join(grid, f'{raw_id}_icon.png'))\n"
" if os.path.isfile(capsule_p):\n"
" shutil.copyfile(capsule_p, os.path.join(grid, f'{raw_id}p.png'))\n"
" elif os.path.isfile(icon_p):\n"
" shutil.copyfile(icon_p, os.path.join(grid, f'{raw_id}p.png'))\n"
" if os.path.isfile(header_p):\n"
" shutil.copyfile(header_p, os.path.join(grid, f'{raw_id}.png'))\n"
" elif os.path.isfile(icon_p):\n"
" shutil.copyfile(icon_p, os.path.join(grid, f'{raw_id}.png'))\n"
" if os.path.isfile(hero_p):\n"
" shutil.copyfile(hero_p, os.path.join(grid, f'{raw_id}_hero.png'))\n"
" if os.path.isfile(logo_p):\n"
" shutil.copyfile(logo_p, os.path.join(grid, f'{raw_id}_logo.png'))\n"
" mod = True\n"
" if mod:\n"
" tmp = vdf + '.tmp'\n"
" with open(tmp, 'wb') as f: f.write(dump({key: [OBJ, s_dict]}) + bytes([END]))\n"
" os.replace(tmp, vdf)\n"
" except Exception:\n"
" pass\n"
)
ssh(f"python3 -c {shlex.quote(sanitize_script)}")
# 4. Optionally launch it in the headset.
if args.launch:
print("Launching in the headset ...")
@@ -184,7 +265,7 @@ def main() -> int:
print(f"Deployed to {host}:~/{REMOTE_DIR}")
if player_args:
print(" argv:", " ".join(["./pixelvr", *player_args]))
print(f" Launch from Steam > Library > Non-Steam > 'Devkit Game: {GAME}'")
print(" Launch from Steam > Library > Non-Steam > 'Pixel VR Player'")
print(f" Or over SSH: ssh {host} '{REMOTE_DIR}/launch.sh <video>'")
return 0
+48
View File
@@ -0,0 +1,48 @@
@echo off
setlocal enabledelayedexpansion
echo ===============================================================
echo Pixel VR Player - Steam Frame Remote Installer
echo ===============================================================
echo This tool installs Pixel VR Player on your Valve Steam Frame
echo over your local Wi-Fi network directly from your PC.
echo ===============================================================
echo.
set "FRAME_IP=steamframe.local"
set /p "INPUT_IP=Enter Steam Frame IP address [press Enter for steamframe.local]: "
if not "!INPUT_IP!"=="" set "FRAME_IP=!INPUT_IP!"
echo.
echo Connecting to steamos@!FRAME_IP!...
echo (If prompted for a password, enter your SteamOS desktop password)
echo.
where ssh >nul 2>&1
if %ERRORLEVEL% neq 0 (
echo [ERROR] SSH is not installed or not in PATH on this PC.
pause
exit /b 1
)
ssh -o StrictHostKeyChecking=no steamos@!FRAME_IP! "curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | sh"
if %ERRORLEVEL% equ 0 (
echo.
echo ===============================================================
echo [SUCCESS] Pixel VR Player installed on your Steam Frame!
echo It is now in your Steam Library under Non-Steam Games.
echo ===============================================================
) else (
echo.
echo ===============================================================
echo [NOTE] If connection failed, please ensure:
echo 1. Both your PC and Steam Frame are on the same Wi-Fi.
echo 2. SSH is enabled on the headset.
echo (In Desktop Mode, open Konsole and run 'passwd',
echo then 'sudo systemctl start sshd', or enable Dev Mode).
echo ===============================================================
)
echo.
pause
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Pixel VR Player - Remote installer from Mac / Linux to Steam Frame
set -euo pipefail
echo "==============================================================="
echo " Pixel VR Player - Steam Frame Remote Installer"
echo "==============================================================="
echo " This tool installs Pixel VR Player on your Valve Steam Frame"
echo " over your local Wi-Fi network directly from your Mac or PC."
echo "==============================================================="
echo ""
read -rp "Enter Steam Frame IP address [press Enter for steamframe.local]: " INPUT_IP
FRAME_IP="${INPUT_IP:-steamframe.local}"
echo ""
echo "Connecting to steamos@${FRAME_IP}..."
echo "(If prompted for a password, enter your SteamOS desktop password)"
echo ""
if ssh -o StrictHostKeyChecking=no "steamos@${FRAME_IP}" "curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | sh"; then
echo ""
echo "==============================================================="
echo " [SUCCESS] Pixel VR Player installed on your Steam Frame!"
echo " It is now in your Steam Library under Non-Steam Games."
echo "==============================================================="
else
echo ""
echo "==============================================================="
echo " [NOTE] If connection failed, please ensure:"
echo " 1. Both devices are on the same Wi-Fi network."
echo " 2. SSH is enabled on the headset."
echo " (In Desktop Mode, open Konsole and run 'passwd',"
echo " then 'sudo systemctl start sshd', or enable Dev Mode)."
echo "==============================================================="
fi
+52 -6
View File
@@ -230,7 +230,8 @@ def find(shortcuts, name, exe):
continue
stored_exe = entry.get("Exe", [STR, ""])[1].strip('"')
stored_name = entry.get("AppName", [STR, ""])[1]
if (exe and stored_exe == exe) or stored_name == name:
devkit_id = entry.get("DevkitGameID", [STR, ""])[1]
if (exe and stored_exe == exe) or stored_name == name or stored_name in ("Devkit Game: PixelVRPlayer", "Devkit Game: Pixel VR Player") or devkit_id == "PixelVRPlayer":
return key, entry
return None, None
@@ -245,7 +246,11 @@ def main():
with open(path, "rb") as handle:
buf = handle.read()
root = parse_file(buf)
shortcuts = root["shortcuts"][1]
root_key = next((k for k in root if k.lower() == "shortcuts"), None)
if root_key is None:
root_key = "shortcuts"
root[root_key] = [OBJ, {}]
shortcuts = root[root_key][1]
key, entry = find(shortcuts, name, exe)
@@ -258,7 +263,7 @@ def main():
ordered = sorted(shortcuts, key=lambda k: int(k) if k.isdigit() else 1 << 30)
for index, old in enumerate(ordered):
renumbered[str(index)] = shortcuts[old]
root["shortcuts"][1] = renumbered
root[root_key][1] = renumbered
print("removed '%s'" % name)
else:
quoted_exe = '"%s"' % exe
@@ -272,11 +277,12 @@ def main():
shortcuts[str(index)] = [OBJ, entry]
if "appid" not in entry:
entry["appid"] = [INT, to_i32(app_id(quoted_exe, name))]
icon_path = os.path.join(start_dir, "icon.png") if os.path.isfile(os.path.join(start_dir, "icon.png")) else ""
for field, value in (
("AppName", [STR, name]),
("Exe", [STR, quoted_exe]),
("StartDir", [STR, '"%s"' % start_dir]),
("icon", [STR, ""]),
("icon", [STR, icon_path]),
("ShortcutPath", [STR, ""]),
("LaunchOptions", [STR, ""]),
("IsHidden", [INT, 0]),
@@ -291,6 +297,31 @@ def main():
):
entry[field] = value
entry.setdefault("tags", [OBJ, {}])
if icon_path and os.path.isfile(icon_path):
try:
import shutil
steam_user_dir = os.path.dirname(os.path.dirname(path))
grid_dir = os.path.join(steam_user_dir, "config", "grid")
if icon_path and os.path.isfile(icon_path):
shutil.copyfile(icon_path, os.path.join(grid_dir, "%d_icon.png" % raw_appid))
capsule_path = os.path.join(os.path.dirname(icon_path), "capsule.png") if icon_path else ""
if capsule_path and os.path.isfile(capsule_path):
shutil.copyfile(capsule_path, os.path.join(grid_dir, "%dp.png" % raw_appid))
elif icon_path and os.path.isfile(icon_path):
shutil.copyfile(icon_path, os.path.join(grid_dir, "%dp.png" % raw_appid))
header_path = os.path.join(os.path.dirname(icon_path), "header.png") if icon_path else ""
if header_path and os.path.isfile(header_path):
shutil.copyfile(header_path, os.path.join(grid_dir, "%d.png" % raw_appid))
elif icon_path and os.path.isfile(icon_path):
shutil.copyfile(icon_path, os.path.join(grid_dir, "%d.png" % raw_appid))
hero_path = os.path.join(os.path.dirname(icon_path), "hero.png") if icon_path else ""
if hero_path and os.path.isfile(hero_path):
shutil.copyfile(hero_path, os.path.join(grid_dir, "%d_hero.png" % raw_appid))
logo_path = os.path.join(os.path.dirname(icon_path), "logo.png") if icon_path else ""
if logo_path and os.path.isfile(logo_path):
shutil.copyfile(logo_path, os.path.join(grid_dir, "%d_logo.png" % raw_appid))
except Exception:
pass
print("%s '%s' (%d shortcut(s) in the library)" % (
"added" if created else "updated", name, len(shortcuts)))
@@ -491,14 +522,29 @@ cp "$srcdir/shaders/"*.spv "$INSTALL_DIR/shaders/"
cp "$srcdir/run.sh" "$INSTALL_DIR/run.sh"
chmod 0755 "$INSTALL_DIR/run.sh"
INSTALLED_SCRIPT=0
if [ -n "$script_dir" ] && [ -f "$script_dir/install.sh" ]; then
if [ -f "$srcdir/install.sh" ]; then
cp "$srcdir/install.sh" "$INSTALL_DIR/install.sh"
chmod 0755 "$INSTALL_DIR/install.sh"
INSTALLED_SCRIPT=1
elif [ -n "$script_dir" ] && [ -f "$script_dir/install.sh" ]; then
cp "$script_dir/install.sh" "$INSTALL_DIR/install.sh"
chmod 0755 "$INSTALL_DIR/install.sh"
INSTALLED_SCRIPT=1
fi
if [ -n "$VERSION" ]; then
if [ -f "$srcdir/VERSION" ]; then
cp "$srcdir/VERSION" "$INSTALL_DIR/VERSION"
elif [ -n "$VERSION" ]; then
printf '%s\n' "$VERSION" > "$INSTALL_DIR/VERSION"
fi
for asset in icon.png hero.png capsule.png header.png logo.png; do
if [ -f "$srcdir/$asset" ]; then
cp "$srcdir/$asset" "$INSTALL_DIR/$asset"
elif [ -n "$script_dir" ] && [ -f "$script_dir/$asset" ]; then
cp "$script_dir/$asset" "$INSTALL_DIR/$asset"
elif [ -f "assets/$asset" ]; then
cp "assets/$asset" "$INSTALL_DIR/$asset"
fi
done
ok "$(ls -1 "$INSTALL_DIR/shaders" | wc -l | tr -d ' ') shaders, $(du -h "$INSTALL_DIR/pixelvr" 2>/dev/null | cut -f1) binary"
missing_libs=""
+11
View File
@@ -37,12 +37,23 @@ cp "${bin}" "${stage}/PixelVRPlayer/pixelvr"
cp "${BUILD_DIR}/shaders/"*.spv "${stage}/PixelVRPlayer/shaders/"
cp tools/run.sh "${stage}/PixelVRPlayer/run.sh"
cp tools/install.sh "${stage}/PixelVRPlayer/install.sh"
if [[ -f "tools/Install Pixel VR Player.desktop" ]]; then
cp "tools/Install Pixel VR Player.desktop" "${stage}/PixelVRPlayer/Install Pixel VR Player.desktop"
fi
for asset in icon.png hero.png capsule.png header.png logo.png; do
if [[ -f "assets/${asset}" ]]; then
cp "assets/${asset}" "${stage}/PixelVRPlayer/${asset}"
fi
done
if [[ -n "${VERSION}" ]]; then
printf '%s\n' "${VERSION}" > "${stage}/PixelVRPlayer/VERSION"
fi
chmod +x "${stage}/PixelVRPlayer/pixelvr" \
"${stage}/PixelVRPlayer/run.sh" \
"${stage}/PixelVRPlayer/install.sh"
if [[ -f "${stage}/PixelVRPlayer/Install Pixel VR Player.desktop" ]]; then
chmod +x "${stage}/PixelVRPlayer/Install Pixel VR Player.desktop"
fi
rm -f "${OUT_TAR}"
tar -C "${stage}" -czf "${OUT_TAR}" PixelVRPlayer
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""
Automated in-headset screenshot capture tool for Valve Steam Frame (SteamOS).
Uses OpenVR IVRScreenshots via SteamVR's running compositor to capture
both 2D preview and stereo VR framebuffers.
"""
import sys
import os
import time
import argparse
import subprocess
import base64
from datetime import datetime
DEFAULT_SSH_KEY = os.path.expanduser(r"~\AppData\Local\steamos-devkit\steamos-devkit\devkit_rsa")
DEFAULT_HOST = "steamos@192.168.178.61"
CPP_CODE = r"""#include <iostream>
#include <thread>
#include <chrono>
#include "/opt/steamvr/tools/hellovr_vulkan_linux/src/openvr/headers/openvr.h"
int main(int argc, char** argv) {
if (argc < 3) {
std::cerr << "Usage: " << argv[0] << " <preview_png> <stereo_png>" << std::endl;
return 1;
}
const char* preview = argv[1];
const char* vr_file = argv[2];
vr::EVRInitError err = vr::VRInitError_None;
vr::IVRSystem* vrSys = vr::VR_Init(&err, vr::VRApplication_Overlay);
if (!vrSys || err != vr::VRInitError_None) {
std::cerr << "VR_Init failed: " << vr::VR_GetVRInitErrorAsEnglishDescription(err) << std::endl;
return 1;
}
vr::IVRScreenshots* vrScreenshots = (vr::IVRScreenshots*)vr::VR_GetGenericInterface("IVRScreenshots_001", &err);
if (!vrScreenshots || err != vr::VRInitError_None) {
std::cerr << "GetGenericInterface IVRScreenshots_001 failed: " << err << std::endl;
vr::VR_Shutdown();
return 1;
}
vr::ScreenshotHandle_t handle = 0;
vr::EVRScreenshotError scErr = vrScreenshots->TakeStereoScreenshot(&handle, preview, vr_file);
if (scErr != vr::VRScreenshotError_None) {
std::cerr << "TakeStereoScreenshot failed with error: " << scErr << std::endl;
vr::VR_Shutdown();
return 1;
}
std::cout << "Screenshot requested successfully, handle: " << handle << std::endl;
// Allow compositor frame loop to flush the write
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
vr::VR_Shutdown();
return 0;
}
"""
def main():
parser = argparse.ArgumentParser(description="Capture in-headset screenshots from Valve Steam Frame")
parser.add_argument("--host", default=DEFAULT_HOST, help="Steam Frame SSH destination")
parser.add_argument("--key", default=DEFAULT_SSH_KEY, help="Path to devkit private key")
parser.add_argument("--delay", type=int, default=0, help="Seconds to wait before taking screenshot")
parser.add_argument("--out-dir", default="docs/screenshots", help="Directory to save downloaded screenshots")
args = parser.parse_args()
if args.delay > 0:
print(f"Waiting {args.delay} seconds before capture (put headset on or position UI)...")
time.sleep(args.delay)
b64_cpp = base64.b64encode(CPP_CODE.encode('utf-8')).decode('utf-8')
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
remote_preview = f"/tmp/pixelvr_snap_{timestamp}_preview.png"
remote_stereo = f"/tmp/pixelvr_snap_{timestamp}_stereo.png"
remote_cmd = (
f"if [ ! -f /tmp/pixelvr_screenshot_bin ]; then "
f" echo '{b64_cpp}' | base64 -d > /tmp/pixelvr_screenshot.cpp && "
f" g++ -O2 /tmp/pixelvr_screenshot.cpp -L/opt/steamvr/bin/linuxarm64 -lopenvr_api -Wl,-rpath,/opt/steamvr/bin/linuxarm64 -o /tmp/pixelvr_screenshot_bin ; "
f"fi ; "
f"/tmp/pixelvr_screenshot_bin {remote_preview} {remote_stereo}"
)
print(f"Triggering screenshot capture on {args.host}...")
res = subprocess.run([
"ssh", "-i", args.key, "-o", "StrictHostKeyChecking=no", args.host, remote_cmd
], capture_output=True, text=True)
if res.returncode != 0:
print("Error capturing screenshot:\n", res.stderr)
sys.exit(1)
print(res.stdout.strip())
os.makedirs(args.out_dir, exist_ok=True)
local_preview = os.path.join(args.out_dir, f"screenshot_{timestamp}_preview.png")
local_stereo = os.path.join(args.out_dir, f"screenshot_{timestamp}_stereo.png")
print(f"Downloading captured frames to {args.out_dir}...")
subprocess.run(["scp", "-i", args.key, "-o", "StrictHostKeyChecking=no", f"{args.host}:{remote_preview}", local_preview], check=True)
subprocess.run(["scp", "-i", args.key, "-o", "StrictHostKeyChecking=no", f"{args.host}:{remote_stereo}", local_stereo], check=True)
print(f"Saved: {local_preview}")
print(f"Saved: {local_stereo}")
if __name__ == "__main__":
main()