Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
423b01ee98 | ||
|
|
93b62fa185 | ||
|
|
fc47dab20b | ||
|
|
58a936c706 | ||
|
|
72f94d1754 | ||
|
|
af6117ad5d | ||
|
|
540c71982f | ||
|
|
daa71a144e | ||
|
|
a64a4c6958 | ||
|
|
c0b3ba960a | ||
|
|
aaf6aba621 | ||
|
|
80ef9ca6a7 | ||
|
|
c744f8740b | ||
|
|
859f921361 | ||
|
|
6acf9d048e | ||
|
|
cf18b30be9 | ||
|
|
eec0218b1d | ||
|
|
526ba0a684 | ||
|
|
23b6ff1b5e | ||
|
|
00c6863e54 | ||
|
|
4863035a05 | ||
|
|
a30162ea63 | ||
|
|
24eaa97fde | ||
|
|
767b152423 | ||
|
|
10e7669f70 | ||
|
|
76c4f2d225 | ||
|
|
3a851e911f | ||
|
|
2912d637b5 | ||
|
|
5c070ccf23 | ||
|
|
b979067215 | ||
|
|
4440b459e5 | ||
|
|
09b8046337 | ||
|
|
0d67335775 | ||
|
|
867b9977d5 | ||
|
|
80092098e0 | ||
|
|
feab03e049 | ||
|
|
9713da222a | ||
|
|
82e617741e | ||
|
|
9de0707070 | ||
|
|
1343930ae0 | ||
|
|
5424e429e4 | ||
|
|
9149eaecd7 | ||
|
|
64278e9e2c | ||
|
|
1531acb575 | ||
|
|
6796e006d9 | ||
|
|
45f05dd3c6 | ||
|
|
5a2cd45fb8 | ||
|
|
757671b389 | ||
|
|
8a2029c079 | ||
|
|
b2688dd368 | ||
|
|
13de114f39 | ||
|
|
9016982394 | ||
|
|
cad2cd366c | ||
|
|
a5820e68d7 | ||
|
|
aff939a6d8 | ||
|
|
5e725fbd08 | ||
|
|
1cb9e6f65b | ||
|
|
7a843962e8 | ||
|
|
51220a4942 | ||
|
|
981daa3c2d | ||
|
|
9781c2bb6b | ||
|
|
a7c349835c | ||
|
|
9a48eac3e6 | ||
|
|
9082c684ed | ||
|
|
84235fbc9f | ||
|
|
8accd6221a | ||
|
|
c37eebb59f | ||
|
|
5b0bd2589b | ||
|
|
231e5e50cd | ||
|
|
423b2e1c3e | ||
|
|
b6c5f7c18e | ||
|
|
74320e607f | ||
|
|
d87e3b3b5f | ||
|
|
c4153be8bc | ||
|
|
b5a16a2680 | ||
|
|
f6870da310 | ||
|
|
6c8cda5d7a | ||
|
|
7331d9320a | ||
|
|
dccdcaab96 | ||
|
|
99c6a88f90 | ||
|
|
da4f26197d | ||
|
|
02361b7c99 | ||
|
|
d6bbea05ca | ||
|
|
65d3c2ab67 | ||
|
|
327f213f56 | ||
|
|
05c24cc9e0 | ||
|
|
6e31ae52c4 | ||
|
|
7830444c87 | ||
|
|
9ace9d3bb3 | ||
|
|
a994e86ba5 | ||
|
|
e776116162 | ||
|
|
1e814af1d4 | ||
|
|
f3ed77deb0 | ||
|
|
d5245aa496 | ||
|
|
e4a62d6e90 | ||
|
|
dcc57f29c8 | ||
|
|
d87d085842 | ||
|
|
3930617518 | ||
|
|
deaf1f08a6 | ||
|
|
76ba1e4ffa | ||
|
|
62728dc562 | ||
|
|
6454c7f37d | ||
|
|
a4f73a88b7 | ||
|
|
e5658ed441 | ||
|
|
6bfaaf788f | ||
|
|
07da1c27a3 | ||
|
|
35462324bb | ||
|
|
2c1149815d | ||
|
|
c9b600d186 | ||
|
|
ca14fad3ce | ||
|
|
c4fa6557e1 | ||
|
|
1bb08bab36 | ||
|
|
cccafe86bc | ||
|
|
6fb4a52051 | ||
|
|
5f42c4bb01 | ||
|
|
37b8ac78a7 | ||
|
|
26b86f3f9d | ||
|
|
8d50d5a065 | ||
|
|
ce257b429a | ||
|
|
7e95c50f39 | ||
|
|
61ecc4d2ee | ||
|
|
391f43df95 | ||
|
|
60d8ab021b | ||
|
|
dfab8d68fd | ||
|
|
8167cb86c6 | ||
|
|
44bc689348 | ||
|
|
007db344d7 | ||
|
|
3fb2621030 | ||
|
|
39ca9a5f0d | ||
|
|
e64e8a95e0 | ||
|
|
69e888c7fe | ||
|
|
ce809c432d | ||
|
|
cdf900c6b7 | ||
|
|
d5b5b7751e | ||
|
|
351712172c | ||
|
|
f99aded40e | ||
|
|
f631e5fc1b | ||
|
|
26cee1551e | ||
|
|
82e2a40791 | ||
|
|
1c158930cd | ||
|
|
f812ea8716 | ||
|
|
3dc0a95379 | ||
|
|
f8244f582d | ||
|
|
69fbdfdc4a | ||
|
|
7d9ef9c6f8 | ||
|
|
e042d1f1ee | ||
|
|
2bb88b211b | ||
|
|
27694243f2 | ||
|
|
e6c00ec4b8 | ||
|
|
ef3272a31b | ||
|
|
b0f3ef2698 | ||
|
|
1711e7f7d9 | ||
|
|
ff6cdf3b6a | ||
|
|
28517a1999 | ||
|
|
2480d203ec | ||
|
|
af9c207ddf | ||
|
|
88045bcf4e | ||
|
|
5147710fad | ||
|
|
9c45cff09a | ||
|
|
c4983b6e18 | ||
|
|
b4848c0424 | ||
|
|
3a46b78361 | ||
|
|
75bbc6eb5f | ||
|
|
da638aea99 | ||
|
|
7c37b479db | ||
|
|
fa473c5ada | ||
|
|
1e6ce04e4f | ||
|
|
5cd5b119e7 | ||
|
|
186138cc3c | ||
|
|
98c7ca980a | ||
|
|
13736d2524 | ||
|
|
b2470b962a | ||
|
|
2a705bc6d1 | ||
|
|
a069c79c73 | ||
|
|
5d44831757 | ||
|
|
456880501b | ||
|
|
63f7ae12e1 | ||
|
|
fa69ffefd2 | ||
|
|
f78bc82cc4 | ||
|
|
dfacf6a206 | ||
|
|
b2f14c1e2e | ||
|
|
9ee9355348 | ||
|
|
0a2dbc3140 | ||
|
|
03d7a311bf | ||
|
|
f7108c0ca4 | ||
|
|
cd5119278b | ||
|
|
6ab3608a44 | ||
|
|
c0539af368 | ||
|
|
c6363140f4 | ||
|
|
187105b729 | ||
|
|
b983c0f11f | ||
|
|
c7cee33889 | ||
|
|
d9d73b8aec | ||
|
|
48455b9b72 | ||
|
|
21d72e51ef | ||
|
|
222633e462 | ||
|
|
f64c93b338 | ||
|
|
429899d6fc | ||
|
|
f1486609ab | ||
|
|
e35170d4f9 | ||
|
|
80a5c0b6d6 | ||
|
|
63f0125954 | ||
|
|
af798c0bf3 | ||
|
|
ed714c9d65 | ||
|
|
832b401630 | ||
|
|
5e7c07c98e | ||
|
|
00d2b3bf6b | ||
|
|
02c697c2f0 | ||
|
|
511051952c | ||
|
|
114b7cb4d6 | ||
|
|
a4d3ce5e00 | ||
|
|
b3095b110a | ||
|
|
dce2362cbb | ||
|
|
fb284b6fc4 | ||
|
|
c0df52b981 | ||
|
|
6782ace43d | ||
|
|
71447c932f | ||
|
|
a1b0a3f350 | ||
|
|
df864e9fed | ||
|
|
45bcc76436 | ||
|
|
6595a31048 | ||
|
|
1a6c3321a1 | ||
|
|
b21ef9ece6 | ||
|
|
9b3c3b330a | ||
|
|
46d8c5c379 | ||
|
|
f4bcb658e1 | ||
|
|
76542b53e5 | ||
|
|
088dac8fb6 | ||
|
|
4a511ca6e3 | ||
|
|
35da9c569a | ||
|
|
dec7b2a9ec | ||
|
|
ba1ed72a55 | ||
|
|
e4f70144e9 | ||
|
|
020334b37b | ||
|
|
24a98ab263 | ||
|
|
ba2ddef525 | ||
|
|
58563b99a5 | ||
|
|
9013a7d13c | ||
|
|
f36a17b46a | ||
|
|
7335ff2ce5 | ||
|
|
21c039f9b3 | ||
|
|
b20591540b | ||
|
|
8814b1ce56 | ||
|
|
f3dee5425e | ||
|
|
676b025c5b | ||
|
|
fac72d17c8 | ||
|
|
a2fa6021f5 | ||
|
|
c492e662cb | ||
|
|
ce803340b0 | ||
|
|
3e8a480ea6 | ||
|
|
4be34d20ca | ||
|
|
ea2cd893b4 | ||
|
|
a90a8c27dc | ||
|
|
9d0d72c40a | ||
|
|
56d2ac117f | ||
|
|
cb8202d891 | ||
|
|
029b0674d1 | ||
|
|
c852473a43 | ||
|
|
a4e8e20fcd | ||
|
|
fc1bee2673 | ||
|
|
acf1ef1da2 | ||
|
|
5fe120e897 | ||
|
|
41624c8dbd | ||
|
|
e8ec39db86 | ||
|
|
936d4b188b | ||
|
|
e9ab1489cc | ||
|
|
1043eb1d1b | ||
|
|
0f444da595 | ||
|
|
fda644d036 | ||
|
|
fa0fbbd29a | ||
|
|
ad8847509b | ||
|
|
627c05bf98 | ||
|
|
b82b714165 | ||
|
|
3446999109 | ||
|
|
0bfdfbd0e8 | ||
|
|
19addc1570 | ||
|
|
1f35c77a69 | ||
|
|
5973b6c85c | ||
|
|
d74dc1aa61 | ||
|
|
7b14207f81 | ||
|
|
e9eedb2241 | ||
|
|
d3a35c307a | ||
|
|
d6f87f79a6 | ||
|
|
24a1ef9f1d | ||
|
|
ae4904c7d9 | ||
|
|
b142f7c0ac | ||
|
|
934f356927 | ||
|
|
a61764960d | ||
|
|
55a376cdfd | ||
|
|
da7655ee25 | ||
|
|
0ae754fa5d | ||
|
|
ecfd2408cf | ||
|
|
44dfd43692 | ||
|
|
29876ec046 | ||
|
|
3667f504ec | ||
|
|
375b976dcd | ||
|
|
2a74e5ff64 | ||
|
|
0c623c2efc | ||
|
|
d49b3ae884 | ||
|
|
f811261507 | ||
|
|
ec701e6e56 | ||
|
|
360dc105e1 | ||
|
|
892c7c13bd | ||
|
|
3b6128a1b0 | ||
|
|
4c42a528bd | ||
|
|
0e7c5264a0 | ||
|
|
69a4fa6fa7 | ||
|
|
e6b9808beb | ||
|
|
b34330dc85 | ||
|
|
ae69fe3831 | ||
|
|
6d2dc95cc8 | ||
|
|
474be9d34b | ||
|
|
7b4dff7d84 | ||
|
|
885070a00c | ||
|
|
67b58238a0 | ||
|
|
a1c87618f6 | ||
|
|
b4cec19283 | ||
|
|
109c7e449c | ||
|
|
05ad7e4d25 | ||
|
|
1e96abebbb | ||
|
|
69da709ea2 | ||
|
|
d5f9c248e0 | ||
|
|
cc39547e33 | ||
|
|
096d24e42b | ||
|
|
2995413434 | ||
|
|
4b34dbd218 | ||
|
|
a775e63e6d | ||
|
|
3cd4affcf6 | ||
|
|
51e553039a | ||
|
|
27ff49a137 | ||
|
|
e70acf9269 | ||
|
|
1f6fce0fcc | ||
|
|
188cb8fe68 | ||
|
|
0744a126cb | ||
|
|
1c8a0d63e7 | ||
|
|
124ec594a6 | ||
|
|
90b528f9f2 | ||
|
|
2da6082166 | ||
|
|
3e61b07254 | ||
|
|
48c3716ef5 | ||
|
|
dca2d8b762 | ||
|
|
2c4bbdbfec | ||
|
|
8a08733be8 | ||
|
|
d7193bea19 | ||
|
|
c3522a05bb | ||
|
|
8a9a428f1e | ||
|
|
919d059add | ||
|
|
674c26f2d9 | ||
|
|
a6daa5cccb | ||
|
|
6f89123017 | ||
|
|
518c94d79f | ||
|
|
edf9f494e8 | ||
|
|
0db053c31f | ||
|
|
7439f082db | ||
|
|
f12f4aec4c | ||
|
|
8d95592f53 | ||
|
|
44d23f778d | ||
|
|
7a40e47e67 | ||
|
|
9401b7fc87 | ||
|
|
e510ea88f0 | ||
|
|
4c2b6c9dee | ||
|
|
cb689c576a | ||
|
|
bd2e1a8410 | ||
|
|
608fac9d68 | ||
|
|
919c0e291e | ||
|
|
23f4192cfd | ||
|
|
3391a981b8 | ||
|
|
271ce56d08 | ||
|
|
0e47ce8f1a | ||
|
|
ad924669d3 | ||
|
|
311277ae40 | ||
|
|
bad59bfb5b | ||
|
|
9b57c4753f | ||
|
|
1301e60be1 | ||
|
|
069a74f97e | ||
|
|
683ae5bcf6 | ||
|
|
af904c62e6 | ||
|
|
f2d93af973 | ||
|
|
59fb47a827 | ||
|
|
9e169e069b | ||
|
|
eed6e4aaed | ||
|
|
9829ee1666 | ||
|
|
13069db28a | ||
|
|
7058105056 | ||
|
|
df5b949f1a | ||
|
|
3929ff0a8b | ||
|
|
4910440f56 | ||
|
|
bda9045be6 | ||
|
|
a7890d46de | ||
|
|
50ab3eb341 | ||
|
|
4313b52792 | ||
|
|
4f9ecf4801 | ||
|
|
9a5efa28f5 | ||
|
|
cf900cd9d4 | ||
|
|
ef21b33ca2 | ||
|
|
799f1bd9f1 | ||
|
|
44a056095d | ||
|
|
6037b13e88 | ||
|
|
c3a34ace99 | ||
|
|
05843633a7 | ||
|
|
5198f704e9 | ||
|
|
eeee7d2b84 | ||
|
|
772c26d91a | ||
|
|
db0a78ccc3 | ||
|
|
01d764e05c | ||
|
|
0c5c1638ed | ||
|
|
a49cfb0b57 | ||
|
|
0f975d2233 | ||
|
|
ccff9ab486 | ||
|
|
f2cf7db507 | ||
|
|
2f740bbb41 | ||
|
|
1462d3d471 | ||
|
|
22d10c1ff0 | ||
|
|
8b9657b2e6 | ||
|
|
bc6f6b7fe2 | ||
|
|
8f3c8b33ba | ||
|
|
8187c79d17 | ||
|
|
2ecf7f8624 | ||
|
|
6200662bc7 | ||
|
|
d2c72264be | ||
|
|
f37f7be9c7 | ||
|
|
d05dd27971 | ||
|
|
fd478f4c70 | ||
|
|
d834ffac42 | ||
|
|
3e11175001 | ||
|
|
85a11061d0 | ||
|
|
e26cb8b0db | ||
|
|
208d7bb4dd | ||
|
|
cf1a2c9181 | ||
|
|
ba9ccc3606 | ||
|
|
83a614d645 | ||
|
|
5734c1fdee | ||
|
|
af791a45b4 | ||
|
|
11c8802b07 | ||
|
|
70d519a5a9 | ||
|
|
4e00c88949 | ||
|
|
1b62d643f3 | ||
|
|
6bd4eb523f | ||
|
|
d22123c657 | ||
|
|
0137cd993b | ||
|
|
7b8e7c2c5d | ||
|
|
990331e5b6 | ||
|
|
56b370f582 | ||
|
|
ac628cd284 | ||
|
|
c12b2f6992 | ||
|
|
a110e8573f | ||
|
|
4fdeeaf829 | ||
|
|
dbabb37343 | ||
|
|
7afee4bf59 | ||
|
|
7367a35176 | ||
|
|
9948f946c5 | ||
|
|
3870500972 | ||
|
|
a7b02c00ee | ||
|
|
09c7bb6ea7 | ||
|
|
3761dfd62d | ||
|
|
f86d9021f9 | ||
|
|
83e901a682 | ||
|
|
246e5fc5ee | ||
|
|
17403779cb | ||
|
|
0e03e061a4 | ||
|
|
d8cf19fb20 | ||
|
|
7cd9a38eaf | ||
|
|
4f3593192f | ||
|
|
974426d1d0 | ||
|
|
b7a0e0cb5b | ||
|
|
38edc26092 | ||
|
|
e96e25bade | ||
|
|
67c991ca4f | ||
|
|
d2017194f3 | ||
|
|
7a11706b3a | ||
|
|
76d4390b19 | ||
|
|
2022d9986a | ||
|
|
9ff7edf93b | ||
|
|
a5722b50a0 | ||
|
|
0a13ca72a2 | ||
|
|
c53d754007 | ||
|
|
a1662da17d | ||
|
|
0141bad259 | ||
|
|
6ed95684dc | ||
|
|
a6ca8385a9 | ||
|
|
210410f125 | ||
|
|
7e2502a552 | ||
|
|
a57d89e874 | ||
|
|
aade29982f | ||
|
|
d94cab6048 | ||
|
|
f17fdbf271 | ||
|
|
fb70c09d72 | ||
|
|
69bf2700e1 | ||
|
|
0b28da5521 | ||
|
|
9e9a4c0f6c | ||
|
|
bb3f6b9e7c | ||
|
|
dc7533e4e1 | ||
|
|
3660167bd8 | ||
|
|
6a47fad8f7 | ||
|
|
0f8062f881 | ||
|
|
65369f154c | ||
|
|
aefb5adc07 | ||
|
|
5603edeef7 | ||
|
|
5c47185938 | ||
|
|
fefdc19cac | ||
|
|
db6ccece32 | ||
|
|
59018cb396 | ||
|
|
70216de397 | ||
|
|
bef61563d5 | ||
|
|
1b728d7c75 | ||
|
|
60cde5f846 | ||
|
|
c3a63bbd85 | ||
|
|
bbe76ab888 | ||
|
|
b89e48cec0 | ||
|
|
dc020a2cb7 | ||
|
|
cc3febfae4 | ||
|
|
e729f3f4bc | ||
|
|
2fecfbf6ea | ||
|
|
6e0bb8ee59 | ||
|
|
26679114ed | ||
|
|
b8e49a3419 | ||
|
|
f196cf556d | ||
|
|
1b0345b15b | ||
|
|
4508608dcf | ||
|
|
3e16199e64 | ||
|
|
a7fa1c06d7 | ||
|
|
dabd9b2095 | ||
|
|
0774536473 | ||
|
|
4a7fe3645c | ||
|
|
204aa70ada | ||
|
|
8bb3df0ec8 | ||
|
|
d9f9695360 | ||
|
|
d45d391683 | ||
|
|
b0682ee570 | ||
|
|
d1cf305c0a | ||
|
|
d22f416489 | ||
|
|
2dc92a090f | ||
|
|
23a47880e0 | ||
|
|
da399ef77a | ||
|
|
f0192c0e9d | ||
|
|
cad54bb2dd | ||
|
|
507dd4ca6e | ||
|
|
a361cdc84d | ||
|
|
fa7567503f | ||
|
|
1c90669eb4 | ||
|
|
942a71dd54 | ||
|
|
e462907c49 | ||
|
|
60efbbe7a9 | ||
|
|
321588d94b | ||
|
|
b4ae5e0023 | ||
|
|
4ba148908a | ||
|
|
7af21e17ce | ||
|
|
fa3544565a | ||
|
|
f7b59a75b3 | ||
|
|
58eaab3c19 | ||
|
|
3d32be1a98 | ||
|
|
8da5a44efb | ||
|
|
51f0a5d75c | ||
|
|
b06cc9659b | ||
|
|
bc40325c2d | ||
|
|
82c2209b6f | ||
|
|
e645ca86d1 | ||
|
|
75c7b2bf20 | ||
|
|
ba68dc76c8 | ||
|
|
22205d17a4 | ||
|
|
2e5fcd9860 | ||
|
|
428e8ffe05 | ||
|
|
dfb4fc4ffd | ||
|
|
fc4aa9c8e5 | ||
|
|
1d83eae3ed | ||
|
|
c1c33d8482 |
No files matched your search
@@ -0,0 +1,43 @@
|
||||
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 holds logs
|
||||
and device details with personal data removed, and no game files.
|
||||
- type: input
|
||||
id: game
|
||||
attributes:
|
||||
label: Game
|
||||
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 and what happened
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: findings
|
||||
attributes:
|
||||
label: Launch test
|
||||
description: Filled in by FramePort
|
||||
- type: textarea
|
||||
id: recipe
|
||||
attributes:
|
||||
label: Recipe
|
||||
render: yaml
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Diagnostics
|
||||
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
|
||||
@@ -0,0 +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.
|
||||
@@ -0,0 +1,49 @@
|
||||
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! 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 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
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: recipe
|
||||
attributes:
|
||||
label: Recipe
|
||||
description: Filled in by FramePort. Don't edit the package line.
|
||||
render: yaml
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: FramePort and SteamOS versions
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Notes
|
||||
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.
|
||||
required: true
|
||||
@@ -1,9 +1,19 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dev:
|
||||
description: "Publish the bundles as the dev build (testers: Settings → Updates → Install the latest dev build)"
|
||||
type: boolean
|
||||
default: true
|
||||
notes:
|
||||
description: "What testers should try (shown before they install it)"
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -12,10 +22,13 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: astral-sh/setup-uv@v10.2.0
|
||||
- run: uv sync --extra dev
|
||||
- name: Lint
|
||||
run: uv run ruff check .
|
||||
- run: uv run pytest -m "not games and not device"
|
||||
|
||||
bundle:
|
||||
needs: test
|
||||
if: github.event_name != 'push' || startsWith(github.ref, 'refs/tags/v') # pushes to main: tests only
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # build provenance attestations (Sigstore)
|
||||
@@ -31,10 +44,13 @@ jobs:
|
||||
include:
|
||||
- os: windows-latest
|
||||
archive: FramePort-windows-x64.zip
|
||||
- os: macos-latest
|
||||
- os: macos-26 # Apple Silicon + Xcode 26 (macos-15's Xcode 16.4 fails a Flutter plugin; macos-latest
|
||||
# went unassigned on 2026-10-02)
|
||||
archive: FramePort-macos-arm64.zip
|
||||
- os: ubuntu-latest
|
||||
- os: ubuntu-22.04 # oldest supported: the bundle needs the build host's GLib/glibc or newer
|
||||
archive: FramePort-linux-x64.tar.gz
|
||||
- os: ubuntu-22.04-arm # Linux ARM64 (same base as x64)
|
||||
archive: FramePort-linux-arm64.tar.gz
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
@@ -43,11 +59,14 @@ jobs:
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y ninja-build libgtk-3-dev
|
||||
- run: uv sync --extra dev
|
||||
- name: Dev build version (next patch + .dev<run number>; not committed)
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.dev
|
||||
run: uv run --no-project python scripts/dev_version.py ${{ github.run_number }}
|
||||
- id: flet
|
||||
run: uv run python scripts/package.py
|
||||
continue-on-error: ${{ runner.os == 'macOS' }}
|
||||
- name: macOS fallback (PyInstaller bundle)
|
||||
if: runner.os == 'macOS' && steps.flet.outcome == 'failure'
|
||||
continue-on-error: ${{ runner.os == 'macOS' || runner.arch == 'ARM64' }}
|
||||
- name: macOS / Linux ARM64 fallback (PyInstaller bundle)
|
||||
if: (runner.os == 'macOS' || runner.arch == 'ARM64') && steps.flet.outcome == 'failure'
|
||||
run: uv run --with pyinstaller python scripts/package.py --pyinstaller
|
||||
|
||||
# ---- signing: self-signed certificate on Windows, ad-hoc on macOS ----
|
||||
@@ -77,8 +96,27 @@ jobs:
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
app=$(find dist -maxdepth 3 -name "*.app" -type d | head -1)
|
||||
codesign --force --deep --sign - "$app"
|
||||
# Re-signing replaces the signature, entitlements included: pass them again, or the file picker refuses to
|
||||
# open ("ENTITLEMENT_NOT_FOUND", GitHub #63). flet build writes them to Release.entitlements; the
|
||||
# PyInstaller fallback has none, so it gets the same set.
|
||||
ent=build/flutter/macos/Runner/Release.entitlements
|
||||
if [ ! -f "$ent" ]; then
|
||||
ent="$RUNNER_TEMP/FramePort.entitlements"
|
||||
cat > "$ent" <<'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>com.apple.security.cs.allow-jit</key><true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
|
||||
<key>com.apple.security.network.client</key><true/>
|
||||
<key>com.apple.security.network.server</key><true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key><true/>
|
||||
</dict></plist>
|
||||
EOF
|
||||
fi
|
||||
codesign --force --deep --sign - --entitlements "$ent" "$app"
|
||||
codesign --verify --deep --strict --verbose=2 "$app"
|
||||
codesign -d --entitlements - "$app" 2>/dev/null | tee /dev/stderr | grep -q "files.user-selected.read-write"
|
||||
|
||||
# ---- package (archives keep permissions/symlinks) + provenance ----
|
||||
- name: Package (Windows)
|
||||
@@ -90,7 +128,12 @@ jobs:
|
||||
run: ditto -c -k --keepParent "$(find dist -maxdepth 3 -name '*.app' -type d | head -1)" ${{ matrix.archive }}
|
||||
- name: Package (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: tar czf ${{ matrix.archive }} -C dist --transform 's,^linux,FramePort,' linux
|
||||
run: | # flet build -> dist/linux; the PyInstaller fallback -> dist/FramePort (one folder)
|
||||
if [ -d dist/linux ]; then tar czf ${{ matrix.archive }} -C dist --transform 's,^linux,FramePort,' linux
|
||||
else tar czf ${{ matrix.archive }} -C dist FramePort; fi
|
||||
# self-update with this archive: unpack, verify the layout (+ the signer on Windows), run the real swap script
|
||||
- name: Update smoke test
|
||||
run: uv run python scripts/update_smoke.py ${{ matrix.archive }}
|
||||
- name: Attest build provenance
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/attest-build-provenance@v4
|
||||
@@ -107,12 +150,22 @@ 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
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: out
|
||||
merge-multiple: true
|
||||
- name: Wheel (CLI install via uv tool / pipx / pip, and its self-update)
|
||||
run: uv build --wheel -o out && ls out
|
||||
- name: Videos (docs/media, rendered by the showcase workflow; frameport-<name>.mp4 → FramePort-<name>.mp4)
|
||||
run: |
|
||||
for f in docs/media/frameport-*.mp4; do
|
||||
[ -f "$f" ] || continue
|
||||
n=$(basename "$f" .mp4); cp "$f" "out/FramePort-${n#frameport-}.mp4"
|
||||
done
|
||||
- name: Checksums
|
||||
run: cd out && sha256sum * > SHA256SUMS.txt && cat SHA256SUMS.txt
|
||||
- name: Publish release
|
||||
@@ -121,7 +174,66 @@ jobs:
|
||||
run: |
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
notes=$(mktemp)
|
||||
{ echo "FramePort ${tag}: Windows (x64), macOS (Apple Silicon) and Linux (x64)."; echo;
|
||||
sed -n '/^## First launch/,$p' docs/INSTALL.md; } > "$notes"
|
||||
# "What's new" = the annotated tag's message (FramePort's update dialog shows it); checkout may leave the
|
||||
# tag lightweight, so fetch the tag object itself
|
||||
git fetch --force origin "refs/tags/${tag}:refs/tags/${tag}"
|
||||
whatsnew=$(git tag -l --format='%(contents:body)' "$tag")
|
||||
[ -n "$whatsnew" ] || whatsnew=$(git tag -l --format='%(contents:subject)' "$tag")
|
||||
{ echo "FramePort ${tag}: Windows (x64), macOS (Apple Silicon) and Linux (x64, ARM64)."; echo;
|
||||
if [ -n "$whatsnew" ] && [ "$(git cat-file -t "$tag")" = tag ]; then
|
||||
echo "## What's new"; echo; echo "$whatsnew"; echo; fi
|
||||
echo "Already have FramePort? It offers this update itself (Library → **Update now**)."
|
||||
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;
|
||||
# testers install it from Settings → Updates). Same assets + SHA256SUMS as a release.
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.dev
|
||||
needs: bundle
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write # starts the pages workflow
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: astral-sh/setup-uv@v10.2.0
|
||||
- id: version
|
||||
run: echo "version=$(uv run --no-project python scripts/dev_version.py ${{ github.run_number }})" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: out
|
||||
merge-multiple: true
|
||||
- name: Wheel
|
||||
run: uv build --wheel -o out && ls out
|
||||
- name: Checksums
|
||||
run: cd out && sha256sum * > SHA256SUMS.txt && cat SHA256SUMS.txt
|
||||
- name: Publish the dev build
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
NOTES: ${{ inputs.notes }}
|
||||
run: |
|
||||
notes=$(mktemp)
|
||||
last=$(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null || true)
|
||||
{ echo "Dev build **${VERSION}** from commit ${GITHUB_SHA::7} ($(date -u +%Y-%m-%d)). For testing fixes before"
|
||||
echo "the next release; you get that release as a normal update."; echo
|
||||
if [ -n "$NOTES" ]; then echo "## Please test"; echo; echo "$NOTES"; echo; fi
|
||||
if [ -n "$last" ]; then echo "## Changes since ${last}"; echo
|
||||
git log --no-merges --format='- %s' "${last}..HEAD" | cut -c1-160 | head -40; fi; } > "$notes"
|
||||
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
|
||||
@@ -0,0 +1,53 @@
|
||||
name: catalog-from-issue
|
||||
# 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:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
recipe-pr:
|
||||
if: github.event.label.name == 'catalog-accepted' && contains(github.event.issue.labels.*.name, 'working-config')
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
ISSUE: ${{ github.event.issue.number }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: astral-sh/setup-uv@v10.2.0
|
||||
- run: uv sync
|
||||
- name: Write the recipe
|
||||
id: recipe
|
||||
run: |
|
||||
gh issue view "$ISSUE" --json body -q .body > "$RUNNER_TEMP/body.md"
|
||||
uv run python scripts/catalog_from_issue.py "$RUNNER_TEMP/body.md" --issue "$ISSUE" | tee "$RUNNER_TEMP/out.txt"
|
||||
grep '^package=' "$RUNNER_TEMP/out.txt" >> "$GITHUB_OUTPUT"
|
||||
- name: Check it loads
|
||||
env:
|
||||
PKG: ${{ steps.recipe.outputs.package }}
|
||||
run: uv run python -c "import os, sys; from frameport.recommend import catalog; sys.exit(os.environ['PKG'] not in catalog.load(refresh=True))"
|
||||
- name: Open the pull request
|
||||
env:
|
||||
PKG: ${{ steps.recipe.outputs.package }}
|
||||
run: |
|
||||
branch="catalog/issue-$ISSUE"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git checkout -b "$branch"
|
||||
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
|
||||
gh pr create --head "$branch" --title "catalog: $PKG" \
|
||||
--body "Recipe submitted in #$ISSUE (working configuration). Closes #$ISSUE."
|
||||
fi
|
||||
- name: Report a failure on the issue
|
||||
if: failure()
|
||||
run: gh issue comment "$ISSUE" --body "Couldn't turn this into a catalog recipe — see the workflow run ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}."
|
||||
@@ -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
|
||||
@@ -0,0 +1,107 @@
|
||||
name: showcase
|
||||
# Docs screenshots and the videos (the demo tour, the install tutorial, …), rendered from the demo library
|
||||
# (docs/showcase/, scripts/showcase/; see docs/SHOWCASE.md). Opens or updates one pull request (branch
|
||||
# showcase/update) when a picture really changed; the renders are also kept as a workflow artifact either way.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
videos:
|
||||
description: "Videos to record too: names from docs/showcase/videos (e.g. tour install), all, or empty"
|
||||
type: string
|
||||
default: ""
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "src/frameport/ui/**"
|
||||
- "src/frameport/locales/**"
|
||||
- "docs/showcase/**"
|
||||
- "scripts/showcase/**"
|
||||
|
||||
concurrency:
|
||||
group: showcase-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
render:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
PYTHONUTF8: "1"
|
||||
PYTHONIOENCODING: utf-8
|
||||
TZ: UTC
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0 # the push's changed files decide whether the video is recorded again
|
||||
- uses: astral-sh/setup-uv@v10.2.0
|
||||
- name: ffmpeg and fonts
|
||||
run: sudo apt-get update && sudo apt-get install -y ffmpeg fonts-roboto fonts-noto-color-emoji
|
||||
- run: uv sync --extra dev --python 3.12 # the Python the renders are made with locally
|
||||
- name: Chromium for Playwright
|
||||
run: uv run playwright install --with-deps chromium
|
||||
- name: Store art cache (fetched once from the stores the app uses)
|
||||
uses: actions/cache@v6.1.0
|
||||
with:
|
||||
path: ~/.cache/frameport-showcase
|
||||
key: showcase-art-${{ hashFiles('docs/showcase/demo-library.yaml') }}
|
||||
restore-keys: showcase-art-
|
||||
|
||||
- name: Docs screenshots
|
||||
run: uv run python scripts/showcase/render_docs.py --keep showcase-out/screenshots
|
||||
|
||||
- name: Videos
|
||||
# a push records the videos whose storyboard (docs/showcase/videos/<name>.yaml) changed, or all of them when
|
||||
# the recorder changed (UI tweaks alone don't re-record ~10 MB files); Run workflow records what it's asked
|
||||
env:
|
||||
INPUT_VIDEOS: ${{ inputs.videos }}
|
||||
BEFORE: ${{ github.event.before }}
|
||||
run: |
|
||||
if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ]; then
|
||||
read -ra names <<< "$INPUT_VIDEOS"
|
||||
if [ ${#names[@]} -eq 0 ]; then echo "no videos asked for"; exit 0; fi
|
||||
[ "${names[0]}" = all ] && names=()
|
||||
uv run python scripts/showcase/record_video.py "${names[@]}" --work showcase-out/videos
|
||||
else
|
||||
uv run python scripts/showcase/record_video.py --changed-since "$BEFORE" --work showcase-out/videos
|
||||
fi
|
||||
|
||||
- name: Keep the renders
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: showcase
|
||||
path: |
|
||||
showcase-out/screenshots
|
||||
showcase-out/videos/*/video.json
|
||||
docs/media
|
||||
docs/images/tour-teaser.webp
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
|
||||
- name: What changed
|
||||
id: diff
|
||||
run: |
|
||||
git status --short -- docs/images docs/media
|
||||
if [ -n "$(git status --porcelain -- docs/images docs/media)" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"; fi
|
||||
- name: Pull request
|
||||
if: steps.diff.outputs.changed == 'true' && github.ref == 'refs/heads/main'
|
||||
uses: peter-evans/create-pull-request@v8.1.1
|
||||
with:
|
||||
branch: showcase/update
|
||||
delete-branch: true
|
||||
add-paths: |
|
||||
docs/images/*
|
||||
docs/media/*
|
||||
commit-message: "Docs: update screenshots and videos"
|
||||
title: "Update docs screenshots and videos"
|
||||
body: |
|
||||
Rendered by the `showcase` workflow from the demo library (docs/showcase/demo-library.yaml) with a
|
||||
pretend Frame: only pictures that visibly changed are included. Look them over before merging (the
|
||||
run's `showcase` artifact has every render).
|
||||
|
||||
Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@@ -11,3 +11,22 @@ parity-report.md
|
||||
.pytest_cache/
|
||||
native/.cache/
|
||||
native/**/build/
|
||||
# per-machine Claude Code settings (contain home paths)
|
||||
.claude/settings.local.json
|
||||
|
||||
# maintainer-local notes for Claude Code (accounts, credentials)
|
||||
CLAUDE.local.md
|
||||
# outputs of `frameport report` / `parity` / ruff
|
||||
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?
|
||||
@@ -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."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 169 KiB |
@@ -0,0 +1 @@
|
||||
Baseline: the site as it was before the redesign steps (critique 28/40).
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 170 KiB |
@@ -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.
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 171 KiB |
@@ -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.
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
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.
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -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.
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -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.
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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'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.</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'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.</li><li><b>03-distill-layout</b> — 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.</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 '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.</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'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.</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
|
||||
@@ -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>
|
||||
@@ -10,40 +10,159 @@ Read `docs/PLAYBOOK.md` (symptom → fix) before debugging a game, and `docs/FRA
|
||||
## Layout
|
||||
- `src/frameport/` — Python package. `pipeline.py` is the API the CLI (`cli.py`) and GUI (`ui/`, Flet 1.0) share.
|
||||
- `ui/` — `app.py` shell (sidebar with Frame connection + activity cards, routing, actions, 30 s connection poll),
|
||||
`theme.py` (dark design tokens; change colours/spacing only there), `components.py` (pill, card, callout,
|
||||
status_row, art_fill, confirm, `update()` = safe update: in Flet 1.0 reading `.page` of an unmounted control
|
||||
`theme.py` (design tokens; change colours/spacing only there. Dark-only colour themes (owner: no light mode):
|
||||
built-ins `portal` (default), `portal_oled` (true black) and `original` (the old violet; alias `classic`). Portal =
|
||||
the logo's dichotomy: orange ACCENT = main action + selection, blue SECONDARY = secondary (outlined) buttons,
|
||||
progress, switch tracks, PC; `dual` themes add the blue→orange sidebar edge, two-colour wordmark (`C.wordmark`) and
|
||||
selected-tab fade (`C.portal_gradient`). Installable theme files `<data>/themes/*.json` (docs/THEMES.md: name,
|
||||
base, dual, any subset of `T.TOKENS`; refused if light/unreadable, `T.ThemeError`), Settings → Appearance: cards
|
||||
switch live via `app.restyle()` (rebuilds the shell, drops cached views; setting `ui.theme`), "Install theme
|
||||
file…", "Copy this theme as a file", bin icon removes. Read tokens as `T.NAME` when building — never as a default argument or module-level constant (they
|
||||
keep the import-time theme; module maps refill through `T.on_change`, `tests/test_theme.py` checks defaults)),
|
||||
`glyphs.py` + `icons/` (the logo `logo.svg`/`logo-solid.svg` = a monitor that comes out of a tilted portal as the
|
||||
Frame, and 24 px line glyphs `fp:frame|port|patch|container|test|pc|sync|live|shot|keys|recipe|link`, copied into
|
||||
the assets dir and shown as tinted `ft.Image`s: pass `G.FRAME` … wherever an icon goes, `C.as_icon` / the button
|
||||
helpers handle both kinds; `src/assets/icon.png` = the bundles' app icon), `components.py` (pill, card, callout,
|
||||
checklist, art_fill, confirm, `update()` = safe update: in Flet 1.0 reading `.page` of an unmounted control
|
||||
raises), `jobs.py` (background FIFO job queue, one at a time, cancel via Reporter; no Flet), `views/`
|
||||
(library: search/filters/tags/sort as pure tested helpers; game: hero + one-click install, patches under
|
||||
"Customize"; frame: device + readiness + installed, or connect wizard; settings; welcome; activity panel).
|
||||
"Customize"; frame: device + readiness + installed, or connect wizard; files: the Frame's file manager (persistent
|
||||
like the library; locations = Videos/Downloads/Documents from agent `storage_targets`, each installed Quest game's
|
||||
storage, the home folder; SFTP via `install/files.py` list_dir/upload/download/make_dir/rename/delete, all
|
||||
confined to the location by `files.inside`; Lepton's links in a game's storage can't be renamed/deleted; replaced
|
||||
the "Send files" dialog, game menu → "Add videos & files…" = `go("files", pkg)`; multi-select bar (download/
|
||||
delete); drag-and-drop from the OS via the `flet-dropzone` extension (Apache-2.0, Flutter `desktop_drop`), which
|
||||
only a `flet build` bundle contains: `files.dropzone_available()` keeps it out of source runs and the PyInstaller
|
||||
fallback, which would show an unknown control); screenshots (Steam screenshots on the Frame, `install/screenshots.py`
|
||||
+ agent v46 `list_screenshots`/`delete_screenshots`: the Frame files every headset shot under SteamVR 250820, so
|
||||
launch.sh logs `start/end <unix>` to `<anchor>/plays.log` (upgrade_launchers adds it; Proton launchers exec → start
|
||||
only) and shots are matched by time; thumbnails cached in `<data>/screenshots-cache/<frame>/`, shown by asset URL;
|
||||
delete leaves screenshots.vdf alone (Steam rewrites it at exit); game menu → "Screenshots" = `go("screenshots",
|
||||
pkg)`; Copy image (card hover button, right-click, viewer) = `core/clipboard.copy_image` (Windows/WSL: PowerShell
|
||||
bitmap + file via %TEMP%, macOS osascript PNG, Linux wl-copy/xclip; Flet's set_image only in web mode)); live view (`views/live.py` + `install/livestream.py`: the Frame's built-in SteamVR "headset view" webcam
|
||||
(`steamvr-v4l2cam.service` → v4l2loopback "SteamVR" /dev/video99, see docs/FRAME_RUNTIME.md) + the default
|
||||
output's pulse monitor (sound) → `fp_venc` (`native/venc`, own clean-room V4L2 driver of the Frame's iris
|
||||
hardware encoder: RGB24→NV12 box downscale with NEON, H.264 CBR, one frame per slot of an even fraction of the
|
||||
panel rate read from DRM, e.g. 96 Hz → 32 fps; stdin `k` = keyframe, EOF = stop; `--probe` / `--selftest`;
|
||||
artifact `linux-arm64-bin/`, synced to `~/.local/share/frameport/bin` by sha256 at stream start, no agent change)
|
||||
piped into ffmpeg (`-c:v copy` + AAC), else the old ffmpeg/x264 30 fps path (qualities scale down only; "full" =
|
||||
SteamVR's size; stderr `live: encoder=… fps=…` → status line) →
|
||||
fragmented MP4 on an SSH exec channel's stdout (stdin EOF stops it) → relay on 127.0.0.1 (a new viewer asks
|
||||
the hardware encoder for a keyframe and waits for it, else gets init + fragments since the last keyframe of the
|
||||
*video* track) → player page
|
||||
`install/live_player.py` (MSE; starts muted as browsers require, "Sound on" button; 0.3 s cushion, catches up at
|
||||
1.1x, seeks only when >2 s behind: seeking to the very edge starved it) opened
|
||||
in the user's default browser: Flet can't show video outside `flet build` bundles; the stream outlives the tab and
|
||||
stops on disconnect/window close); monitor (`views/monitor.py` + `frame/monitor.py` + agent v62 `_monitor`: one
|
||||
JSON sample per tick over an SSH exec channel while the tab is shown (stopped in go/disconnect/on_close, the
|
||||
agent ends at EOF); sources and costs in docs/FRAME_RUNTIME.md "Monitoring sources"; game card (fps from
|
||||
FrameBridge pacing; End game = Steam's Exit game, then cmd_stop), tiles with `C.Sparkline` (Flet canvas, no charts
|
||||
extension), details, a pooled process table (Game / Steam & SteamVR / All; right-click: end / force kill / end
|
||||
game; MON_CRITICAL needs force, MON_NEVER is refused)); settings; welcome; activity panel). Files, Screenshots and the Library share right-click menus
|
||||
(one `ft.ContextMenu` per view, filled on right-click; on one of several selected items they act on the whole
|
||||
selection, `C.menu_targets`) and click-and-drag multi-select (`C.DragSelect`: pan start/end on the area + item
|
||||
hover events, which Flutter also sends with the button held; Flet can't report item positions, so no rubber band).
|
||||
Selection bars sit below the list: above it, their appearing shifted the items mid-drag. Async handlers (they
|
||||
await a FilePicker) must be coroutine functions or go through `page.run_task`: Flet doesn't await a lambda's
|
||||
coroutine (the Files row Download button silently did nothing). `ui_smoke.py --fake-frame --gestures` drives real
|
||||
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 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
|
||||
`ui.live_frame_card` (Settings → Appearance, default on) is on; else the battery comes from the 30 s poll).
|
||||
ui_smoke injects `FakeMonitorSession` as the hub's `session_factory`.
|
||||
User tags live in library entries (`tags`), filters in library setting `ui.library`.
|
||||
Easter eggs (`ui/easter.py`, owner's wish; never mentioned in the UI or docs): seven quick clicks on the sidebar
|
||||
logo (the *wordmark's* portal glows + rings, a cover is tossed out of it and falls across the window on a random
|
||||
arc: `toss_path`, `page.overlay` at the computed window position `_wordmark_portal`; the logo's portal in
|
||||
themes without one), the Monitor's fps number has a mood tooltip (above the number, clear of the pointer) and
|
||||
2.5 minutes within 0.5 fps of target (`PERFECT_SECONDS`) make the game card glow and pulse blue/orange for 5 s,
|
||||
the number grow in the portal gradient and the sparkline take it; the "Perfect pacing" pill + line stay until the
|
||||
rate slips (`PerfectPacing.done`, `_show_perfect`, `Sparkline.portal`), holiday badges on the logo that dance on
|
||||
hover and shower the window on click (`HOLIDAY_MOTION`; `SHOWERS`/`shower_plan`: New Year confetti, winter snow,
|
||||
Valentine hearts, Pi digits, clovers, hopping eggs, bouncing pumpkins; `holiday`: Valentine's
|
||||
heart, Pi Day, St Patrick's clover, Easter Sat-Mon egg (computus), April 1 = the logo upside down until hovered,
|
||||
Halloween pumpkin, Dec 31-Jan 1 party popper, else Dec 20-Jan 2 snowflake; on those days the logo's box grows to
|
||||
hold the badge: Flutter only hit-tests inside a control's bounds; `FRAMEPORT_TODAY` pretends a date),
|
||||
Library search "frameport" (logo spins, "That's me!"), Type on Frame "hello" (keys still go to the Frame; a
|
||||
headset peeks up from the window's corner and waves), the battery ring (only the ring, with a glow behind
|
||||
it; not the percentage) empties at once and spins while its bar refills, then pulses, on reaching 100 % plugged (`charged_now`, `ring_pulse`, `ring_glow`), a long upload (>2.5 min, `HOP_AFTER`) makes the transit cover hop, the Live view
|
||||
tab shows a blinking ON AIR sign while streaming
|
||||
(`app.sync_on_air`), install milestones (confetti +
|
||||
message at the 1st/10th/100th *different* game: settings `fun.installed` = game ids, `fun.milestones` = shown;
|
||||
each milestone shows once ever, recorded before it's shown; updates/reinstalls never count; the first use seeds
|
||||
from games already on a Frame and marks passed milestones shown). Setting `ui.easter_eggs` (no UI) turns them
|
||||
off; the showcase demo library has them off (steps hook `easter_eggs` for egg demos). Reduce motion: no animation.
|
||||
**Performance rules** (the app froze before): never put image bytes in controls — artwork is served by URL from the
|
||||
GUI assets dir (= user data dir; `ft.run(assets_dir=…)`), as thumbnails (`artwork/thumbs.py`, Pillow); the
|
||||
library view is persistent, streams cards in batches from a background thread and filters by toggling visibility;
|
||||
job/connection events call `app.refresh_view()` (targeted), not `render()`; no I/O in render paths.
|
||||
**Never recreate clickable controls on progress ticks** (sidebar, activity tiles): update their properties —
|
||||
replacing them 5×/s swallowed clicks (couldn't leave the Library during an upload).
|
||||
Labelled switches: `C.switch(label, …)` (Material's default label colour is dark on our dark theme).
|
||||
Cover cards (Library grid + shelf): `library.card_hover`/`hover_motion` (lift, art zoom inside the clipped frame,
|
||||
radial `scrim`, accent-tinted shadow) and `C.CoverButton` (frosted round button, accent under the pointer; Play =
|
||||
blue then orange ring ripple + "Starting on Frame…" pill for STARTING_S; its `.status` layer must sit *below*
|
||||
the button in the Stack, else it takes the clicks); the game page's Play turns into "Starting on …" with a spinner
|
||||
(`GameView._starting`). Reduce motion: no lift/zoom/rings. Filled buttons use a glyph's `-solid` variant
|
||||
(`glyphs.solid`, e.g. `frame-solid.svg`: the outline glyph faded into the orange).
|
||||
Empty states (`C.empty_state(..., features=[(icon, heading, sentence)])`): with features the portal is bigger, a
|
||||
soft portal light (`C.portal_glow`) fills the screen and `C.feature_row` says what the screen will show (the
|
||||
not-connected Files/Screenshots/Live/Type/Monitor tabs and the empty Library); the idle Live view is a framed
|
||||
"screen" with its own Start button + `live_features()`, Type on Frame shows `keyboard_tips()` under the box.
|
||||
Setup checklists (Frame → Ready to play, Settings → Tools / This PC) = `C.checklist([C.Check(ok, title, detail,
|
||||
help, fix=(label, icon, handler), extra=control)])`: ok True/False/"warn"/None (= spinner); the fix button shows
|
||||
inline only while an item isn't ready — wire existing actions only. Motion: `app.body` holds an
|
||||
`ft.AnimatedSwitcher` (`app._show_view`): a new route gets a new wrapper (150 ms fade), a same-route redraw swaps
|
||||
the wrapper's content (no animation; persistent views just remount); the wordmark's portal (`wordmark().data`)
|
||||
pulses once when frame_state turns "connected"; both off with setting `ui.reduce_motion` (Settings → Appearance
|
||||
"Reduce motion", `app.reduce_motion`). Settings has a fixed index column (`settings.SECTIONS` order, Appearance
|
||||
first, Remove FramePort last): entries scroll the section column via `scroll_to(scroll_key=…)` (sections wrapped
|
||||
in Containers with `ft.ScrollKey`), the header stays; `app.settings_view.show_section(key)` (ui_smoke
|
||||
`settings-index`).
|
||||
Help hints: wording for non-obvious terms lives in `ui/help.py` (`HELP`); show it with `C.help_icon(key)` or the
|
||||
`help=` argument of `section`/`status_row`/`kv`, tooltips via `C.tip()` (wraps). Game actions for the Library
|
||||
right-click menu (one `ft.ContextMenu` around the grid, filled on right-click) and the game page's "…" menu come
|
||||
from `app.game_actions()`. Picking art (`sources.apply_choice`) downloads into a staging dir and keeps the old
|
||||
`help=` argument of `section`/`kv` (`C.Check` help key), tooltips via `C.tip()` (wraps). Game actions for the Library
|
||||
right-click menu (one `ft.ContextMenu` around the grid, filled on right-click; short: `menus.quick_menu`, key actions
|
||||
+ "More actions…" = the game page with its full "…" menu opened from code, `app.open_game_menu`) and the game
|
||||
page's "…" menu (`menus.menu_sections` quick=False, sectioned) come from `app.game_actions()`. Picking art (`sources.apply_choice`) downloads into a staging dir and keeps the old
|
||||
art if nothing came back; "Update Steam art on Frame" re-sends the art set to the game's anchor (agent ≥ 12).
|
||||
Patch descriptions/reasons describe the general case, naming games only as "e.g. …".
|
||||
- Installs: queued/cancelled/failed ones are remembered (library setting `ui.installs`) → Library "Resume" bar;
|
||||
uploads are interruptible (Cancel checked per MiB) and resumable (big files via SFTP `.part` append, small files
|
||||
streamed in tar batches; the agent counts files already in `incoming/`). Multi-select in the Library queues
|
||||
installs after asking every needed question up front. Failures end in one pop-up (Resume / Uninstall / log).
|
||||
- Other drives (GitHub #90, agent v63): anchors stay in ~/Applications/quest-frame, a game's files may live in
|
||||
`<mount>/FramePort/<pkg>` (`deployment.json` base). Agent `drives` (/proc/mounts, /run/media + Steam library
|
||||
drives; vfat/exfat/ntfs/read-only refused), prepare*/finalize_linux `dest` (unmounted = error, never a fallback;
|
||||
installed games keep their base), `move` (detached systemd-run + `move_status`; cp -a under podman unshare,
|
||||
count+bytes check, symlink retarget, launch.sh: Quest app_dir line / Linux+PC VR rewritten from the record's
|
||||
`launcher` field, else text swap), list_installed `drive`/`drive_missing` (install_state keeps such games
|
||||
"installed"). PC: `install/drives.py`, library setting `install.drive` (Frame page → Storage), game menu
|
||||
"Move to…" (job kind tool-frame), CLI `frame drives`/`frame move`/`install --dest`. Untested on the device.
|
||||
- **PC VR repacks are pre-patched to run directly** (proven: Rick and Morty, Vader Immortal run when the exe is
|
||||
launched directly; Revive breaks them). So Rift games default to `as_is` = install the copy unchanged and launch
|
||||
the exe directly (`pcvr.xr_timefix` for the Frame OpenXR-1.1→1.0 fix, `pcvr.no_crash_reporter` for Unreal).
|
||||
**Revive is off by default, opt-in** (`pcvr.revive`, and `pcvr.oculus_unreal`): only for an un-cracked Oculus game
|
||||
that fails at "Initializing OVR session". Those (Lone Echo, Robo Recall, Lies Beneath: crack .7z not extracted /
|
||||
**Revive is only suggested for games with Oculus (LibOVR) code** (`pcvr.revive`, and `pcvr.oculus_unreal`; repacks
|
||||
with a bundled Revive and SteamVR/OpenXR games run directly): needed by an un-cracked Oculus game that fails at
|
||||
"Initializing OVR session". Those (Lone Echo, Robo Recall, Lies Beneath: crack .7z not extracted /
|
||||
Platform SDK) hit Revive's Oculus-runtime **signature check** under Proton-arm64 — Revive's LoadLibrary/WinVerifyTrust
|
||||
hooks don't install (ARM64EC), and the game's Oculus SDK shim rejects the unsigned Revive runtime (wintrust +
|
||||
crypt32 signer "Oculus VR") — so they don't run on the Frame without extracting the repack's crack (which FramePort
|
||||
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,
|
||||
@@ -54,21 +173,56 @@ Read `docs/PLAYBOOK.md` (symptom → fix) before debugging a game, and `docs/FRA
|
||||
universal-in-Unreal `IVRSystem`/`VR_InitInternal` strings). `frame_native = (openxr or openvr) and not libovr` =
|
||||
confidently runs on the Frame via wineopenxr (SteamVR), no Revive. Any LibOVR game → `needs_revive` → **PC only**
|
||||
on the Frame (Revive's ARM64EC hooks don't work under Proton-arm64, and FramePort does **not** defeat the Oculus
|
||||
runtime Authenticode signature check). Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
|
||||
runtime Authenticode signature check). Patch `pcvr.libovr_redirect` (Frame, default on for Revive games, agent v19 `set_libovr_redirect`) symlinks Revive's
|
||||
runtime as `LibOVRRT{64,32}_1.dll` in the game's exe dir **and next to every OVRPlugin.dll** (Unreal's OVR shim
|
||||
searches its own module dir, not the exe dir) — the LoadLibrary redirect (pure runtime substitution), which
|
||||
does NOT touch the game's runtime signature check (a checking build still fails at `-3021`; only non-checking builds
|
||||
run). PoC verified on-device (2026-09-30): with the redirect, Lone Echo's Oculus SDK now loads Revive's runtime and
|
||||
reaches Oculus API init (`-3021`) instead of failing to load a runtime at all — i.e. the substitution works; `-3021`
|
||||
is the downstream signature/runtime-init stage the redirect doesn't touch. (OVRPlugin/Unreal builds use a more
|
||||
restrictive LibOVRRT search; the next-to-OVRPlugin placement covers the common case.)
|
||||
Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
|
||||
not via static detection. PC installs default to Revive's **OpenVR** backend (`pcvr.revive_openvr` on) and
|
||||
auto-start SteamVR on Play (`winhost.start_steamvr`); the Frame launcher always uses `/openxr`. UI (`game.py`
|
||||
where()) states per-game where it runs; installing an Oculus game on the Frame shows a warning. Migration
|
||||
`rift_frame_native` re-analyzes + re-derives existing entries.
|
||||
- Own artwork (0.8.0): game menu → "Use your own artwork…" (also from the Find artwork dialog) =
|
||||
`views/art_dialog.show_custom_art_dialog`: one slot per kind (portrait/landscape/hero/logo/icon), FilePicker →
|
||||
`sources.apply_custom` (Pillow check, ≥64 px, ≤40 MB, scaled to ≤3840 px, PNG if alpha/logo/icon else JPEG,
|
||||
replaces only that kind + its thumbnails, drops the generated cover/banner, writes `.picked` = "custom") /
|
||||
`remove_custom`; Steam shapes are still composed from what exists (`steam.py` PREFER).
|
||||
- Art for Rift games: `artwork/sources.py` — Quest version package (OculusDB packageName, exact name or +
|
||||
"Unplugged"-type suffix, never sequels) → Meta art; OculusDB square cover; Steam (exact names only); exe icon.
|
||||
- Store details (`artwork/details.py`, entry `details`): OculusDB (description, genres, publisher, website; by Quest
|
||||
package or Rift match) + Steam appdetails (exact title: developer, release date, up to 6 screenshots → artwork
|
||||
`shot_N.jpg`). Meta store pages reject scraping, so Oculus exclusives have no screenshots. Genres become automatic
|
||||
tags. Steam shortcuts get a complete composed art set (`artwork/steam.py`: 600×900 / 920×430 / 1920×620 / logo /
|
||||
256 icon, blurred-backdrop compositing for square-only covers) and tags: how it runs, the original platform
|
||||
256 icon, blurred-backdrop compositing for square-only covers; no store art (at most the APK icon) → a placeholder
|
||||
set: the name on a colour from the title hash + the APK icon, `steam_set_for`; 2D Android apps get no store lookups) and tags: how it runs, the original platform
|
||||
(Meta Quest / Oculus Rift), genres, user tags — merged with tags set in Steam (non-Steam shortcuts can't hold a
|
||||
description).
|
||||
- Linux apps (GitHub #31, library kind `linux`, `linux.<slug>`; x86_64 builds run through FEX (app 3127680, no SLR:
|
||||
RootFS /usr/share/guestos/fex-mesa from the OS image; needs `STEAM_COMPAT_DATA_PATH`, see docs/FRAME_RUNTIME.md):
|
||||
agent v61 `linux_x86_tools`/`pick_tool`, `install_proton`/`proton_status` `kind: linux_x86`, `finalize_linux
|
||||
x86_64`, `installer.ensure_proton(kind=)`; an arm64 program wins over an x86_64 one): GUI = Add
|
||||
games → "Add a Linux app…" /
|
||||
"…folder…" (`app.add_linux` job → `pipeline.add_linux_app`); game page `linux_summary` (program + Change…, AppImage,
|
||||
OpenXR, source) instead of recipe/patches, `C.missing_libraries` callout (Frame deployment, else last install);
|
||||
no Analyze/Rebuild/recipe/share/Game settings actions, Frame only; platform "Linux" badge + library filter; Steam
|
||||
tags "Linux app on Frame"/"Linux"; `_follow_catalog` skips them; local files of a lone AppImage = the file only.
|
||||
Desktop Mode entries (GitHub #84, agent v63): finalize_linux writes `frameport-<slug>.desktop` to
|
||||
~/.local/share/applications (+ ~/Desktop if it exists; `X-FramePort-Package` marks ours), launch.sh with
|
||||
`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
|
||||
@@ -83,11 +237,17 @@ Read `docs/PLAYBOOK.md` (symptom → fix) before debugging a game, and `docs/FRA
|
||||
- `patches/` — **the unit of modularity**. `base.py` (Patch interface, registry), `overport.py` (overport CLI patch ids,
|
||||
discovered dynamically via `overport patches`), `frame/*.py` (one module per Frame fix), `settings.py` (FrameBridge
|
||||
adapter keys + device files as patches). Add a patch = add a module that calls `register(...)`.
|
||||
`upstream.py`: upstream fixes that replace a workaround per build (a probe finds the fix in OVRPort's output →
|
||||
the build leaves the workaround out, `build.superseded`; recipes unchanged). Registered: `ovrport.haptic_envelope`
|
||||
(→ `adapter.haptic_fix`, `frame/haptic_envelope.py`) and `ovrport.microphone_stream` (→ `frame.ovr_microphone`),
|
||||
both fixed in OVRPort runtime 3.4.3-aa54c3f (ovrport/app#73; haptics owner-verified with Lucky's Tale 2026-10-07).
|
||||
- `analysis/` — APK/ELF inspection (`detect.py`), `elf.py` (pyelftools reads; own DT_NEEDED writer), `stubgen.py`
|
||||
(generates the ovr_* stub .so without a compiler).
|
||||
- `apk/` — `axml.py` (binary manifest editor), `workspace.py` (staged zip edits), `sign.py` (apksigner; it aligns too).
|
||||
- `recommend/` — `catalog.py` (known-good recipes: user > remote `FRAMEPORT_CATALOG_URL` > bundled), `engine.py`.
|
||||
- `tools/` — portable toolchain (Temurin JRE, overport jar, apksigner) downloaded dynamically into the user data dir.
|
||||
The overport CLI comes from the downstream fork **Android-XR-Bridge/OVRPort** (stable `vX.Y.Z` releases,
|
||||
`OVRPort-<ver>-stable-cli.jar`; fallback ovrport/app `cli-jar.zip`), see "overport" below.
|
||||
- `frame/` — SSH (paramiko), mDNS discovery, pairing server; `install/installer.py`; `validate/` (static, device, triage).
|
||||
- `targets/` — `Target` interface; `frame_lepton.py` (Quest via Lepton + Rift via Proton), `pc_revive.py` (Rift games
|
||||
on this Windows/WSL PC via Revive + local Steam shortcut; `core/winhost.py` = Windows/WSL helpers).
|
||||
@@ -97,10 +257,58 @@ Read `docs/PLAYBOOK.md` (symptom → fix) before debugging a game, and `docs/FRA
|
||||
release matching the DLL build date, since Revive's version resources are stale) > portable copy unpacked from
|
||||
ReviveInstaller.exe in pure Python; never replaces the user's install). Library ids `rift.<slug>`, entries have `kind: rift`.
|
||||
- `parity.py` — rebuild catalog games from dumps and classify every APK entry difference vs known-good builds.
|
||||
- `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; `*_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
|
||||
it — never `importlib.metadata`, bundles have no dist-info). `updates.py` (no Flet): `check()` = GitHub
|
||||
releases/latest via `cached_json("app-release.json")`, 6 h, skips drafts/prereleases and the user's skipped version
|
||||
(settings `update.last_check`/`skipped`/`auto_check`/`auto_install`/`cli_hint`; env `FRAMEPORT_NO_UPDATE_CHECK`);
|
||||
`install_kind()` = `bundle` (psutil exe path → FramePort.exe's folder / FramePort.app / FramePort/FramePort),
|
||||
`source` (`.git` checkout: `git pull --ff-only` + `uv sync`, refused on a dirty tree), `wheel` (`uv tool`/pipx/pip
|
||||
reinstall of the release's `.whl`); `prepare()` → `<data>/updates/<ver>/` (SHA256SUMS.txt required + checked,
|
||||
layout checked, Windows: the new FramePort.exe must have the running exe's Authenticode signer when that one is
|
||||
signed, `ready.json`); `apply()` writes + starts detached `apply.ps1`/`apply.sh` (wait for pid → Windows: copy over,
|
||||
replaced files kept in `updates/<ver>/previous` because the zip has no top folder; macOS/Linux: mv to `.old` + swap,
|
||||
rollback, `xattr -dr` quarantine → relaunch; `<data>/logs/update.log`). `ui/updater.py`: background check (10 s,
|
||||
then 6 h), sidebar card (hidden control, toggled), Library bar (`library_bar`), changelog dialog, job
|
||||
`app-update` that restarts only when no other job is active, `apply_pending_at_start()` in `main()` for
|
||||
"Install updates automatically"; Settings → Updates. Changelog: `updates.fetch_changelog` (releases list,
|
||||
`app-releases.json`, same 6 h cache, offline → cached / []) → `ChangelogEntry` (`whats_new()` keeps only the tag
|
||||
message's "What's new" part: CI's intro, update hint and release-footer.md stripped); `between`/`changelog_for`
|
||||
(installed < v ≤ offered, dev entry only when on/choosing dev, skipped versions included), `recent_history`;
|
||||
the update/dev dialogs show one collapsible section per version (`_changes_column`, newest open); after an update
|
||||
`pending_news` (setting `update.last_seen_version`; first start only records it; offline → retried next start)
|
||||
→ Library "was updated" bar → "See what's new…"; Settings → Updates "What's new…" = recent history. CLI: `--version`, `frameport update [--check (exit 10)]
|
||||
[--yes]`, once-a-day stderr hint read from the cache only (`refresh_cache()` in a daemon thread writes no
|
||||
settings, to avoid read-modify-write races). `scripts/update_smoke.py <archive>` runs the real extract + swap script
|
||||
(no relaunch) — CI runs it on all three OS with the archive it just built.
|
||||
Verified end to end (2026-10-02): real 0.3.3 bundles on Windows and Linux (Ubuntu 22.04/WSLg) found, staged and
|
||||
installed 0.3.4 ("Install updates automatically" path) and relaunched as 0.3.4 with settings kept. Not yet clicked
|
||||
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).
|
||||
- `catalog/games/<package>.yaml` — 34 recipes verified 2026-09-28; `catalog/triage.yaml` — log signatures → fixes.
|
||||
- `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).
|
||||
`native/build.py` rebuilds them with NDK r27c (downloaded on demand into `native/.cache`, git-ignored; uses
|
||||
`-ffile-prefix-map` so no local paths get embedded; zip symlinks are restored as copies). Users never need the NDK.
|
||||
@@ -113,23 +321,76 @@ Read `docs/PLAYBOOK.md` (symptom → fix) before debugging a game, and `docs/FRA
|
||||
UV_LINK_MODE=copy uv sync --extra dev # creates/updates ./.venv
|
||||
uv run pytest # unit tests (no device, no game files)
|
||||
uv run frameport --help # CLI; uv run frameport-gui for the GUI
|
||||
uv run frameport parity --known-good <PATCHED/_known-good-*> --sources "<VR CyberDeck downloads>"
|
||||
uv run frameport parity --known-good <PATCHED/_known-good-*> --sources "<folder with the game dumps>"
|
||||
```
|
||||
Games/device tests: `pytest -m games` (FRAMEPORT_GAMES=<downloads dir>), `pytest -m device` (FRAMEPORT_FRAME=steamos@host);
|
||||
Games tests: `pytest -m games` (FRAMEPORT_GAMES=<downloads dir>); on-device checks are CLI commands (below);
|
||||
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`
|
||||
renders `docs/showcase/shots.yaml` into `docs/images/` (every shot twice, kept when both agree; only visibly changed
|
||||
files are rewritten; `--check` for CI) and `scripts/showcase/record_video.py [names] [--draft]` records the
|
||||
storyboards `docs/showcase/videos/<name>.yaml` (tour = FramePort in use + README teaser; install = the install
|
||||
tutorial: instruction cards for the download / the command on the Frame, the rest filmed from a first start) into
|
||||
`docs/media/frameport-<name>.mp4`. Both build the demo library first (`demo_home.py`, profile `demo` or `fresh`:
|
||||
catalog games from `demo-library.yaml` + `demo-analyses.json`, art/details cached in ~/.cache/frameport-showcase, a
|
||||
pretend Frame from `showcase-frame.json`) in a temp FRAMEPORT_HOME: no personal data, refuses the real data dir
|
||||
and placeholder art; discovery is off and the setup command shows placeholders for the address and code. Steps find elements
|
||||
by name through Flutter's semantics tree (`scripts/showcase/web.py`: text, tooltip; the innermost match wins) —
|
||||
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): 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>]` and look
|
||||
`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
|
||||
at the PNGs. Flet 1.0 notes: `ft.run` must own the main thread; background work via `page.run_thread`; FilePicker is
|
||||
awaited (`await ft.FilePicker().get_directory_path()`); dialogs via `page.show_dialog/pop_dialog`; running from
|
||||
source needs `flet-desktop` (declared) and web mode needs `flet-web`.
|
||||
- GUI scale: `theme.set_scale()` (library setting `ui.scale`, "auto" = halfway to Windows' AppliedDPI/96 under WSL (150 % → 125 %; full was too large), where the
|
||||
Linux window gets no Windows scaling; Settings → Appearance; applied at start). Sizes go through tokens or
|
||||
`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)
|
||||
- `errors.py`: `explain(exc)` (plain sentence for GUI + CLI), `is_connection_error`. Install/test jobs for the Frame
|
||||
(`Job.needs_frame`) that lose the connection go back to the queue front and the queue pauses (`jobs.paused`);
|
||||
`app._poll` retries every 10 s and resumes (owner-verified on the device 2026-10-02: the queue resumed after the
|
||||
connection was lost). Wake lock: agent v30 `keep_awake` (systemd-inhibit idle:sleep, idle-only
|
||||
fallback because polkit `inhibit-block-sleep` is auth_admin for non-local sessions), held while Frame jobs exist.
|
||||
- **Already converted inputs** (`analysis.is_overport_output`, e.g. the owner's library points at `PATCHED/` copies)
|
||||
are not converted again: a second OVRPort run replaced libovrplatformloader.so and dropped its DT_NEEDED on
|
||||
libovrstubs.so (Wallace & Gromit / Espire 2 crashed: cannot locate symbol ovr_…). Alt builds come from the saved
|
||||
`<pkg>.alt-noforcequit.apk`, else one OVRPort run (frame.ovrstubs relinks). `missing_ovr_symbols` only counts
|
||||
linked stub/compat libs. Converted copies in `output/` are removed after a Frame install (`build.keep_copies`).
|
||||
- Art: `.picked` marker = user's pick, automatic fetches never overwrite it and only fill missing kinds; a Meta
|
||||
result's picture is the OculusDB image shown in the picker (the ovrp image service served "dogfooding" placeholder
|
||||
covers for some packages, e.g. Asgard's Wrath 2). `steam_art_stale` → game page reminder. No store art → generated
|
||||
`cover.jpg`/`banner.jpg` (APK icon via `fetch.apk_icon`).
|
||||
- Game settings dialog (`ui/views/adapter_dialog.py`, metadata `patches/settings.UI`); plain patch summaries
|
||||
(`patches/summaries.py`, `Patch.summary`, "Show technical details" = setting `ui.patch_details`); share nudge
|
||||
(`views/game.should_ask_to_share`); saving a recipe keeps its status (was always "works").
|
||||
|
||||
## Hard-won facts (don't re-learn these)
|
||||
**Frame runtime (SteamOS 0.3.0, build 20260922):**
|
||||
- No AArch32: 32-bit-only APKs fail with `INSTALL_FAILED_NO_MATCHING_ABIS`. Unfixable; point to Rift + Revive.
|
||||
@@ -146,7 +407,31 @@ Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattr
|
||||
- **Tracking only works with the headset worn.** SSH/headless launches never reach VISIBLE/FOCUSED and poses have
|
||||
flags 0x3. So automated tests prove startup (process alive, instance/session created, frames paced), never visuals.
|
||||
|
||||
**Setup / pairing (2026-10-02, verified on the device):** Developer Mode = `"DevModeEnabled"` in
|
||||
`~/.local/share/Steam/config/config.vdf` (InstallConfigStore/developer), applied by Valve's
|
||||
`/usr/bin/steamos-polkit-helpers/steamos-devkit-mode --enable|--disable` (polkit allow_any: no password; enables/
|
||||
disables sshd, xrdp, steamos-devkit-service, debug port forwards; sentinel `/etc/steamos-devkit-enabled`). Steam
|
||||
re-asserts the config value at every start and rewrites config.vdf on exit → stop Steam, edit, helper, start.
|
||||
**Desktop Mode is a nested Plasma inside steam.service** (own XDG_RUNTIME_DIR `/run/user/1000/nested_plasma` + private
|
||||
D-Bus): `systemd-run --user` from Konsole fails ("Failed to connect to user scope bus") unless XDG_RUNTIME_DIR/
|
||||
DBUS_SESSION_BUS_ADDRESS point at `/run/user/$UID`, and stopping Steam ends the desktop and everything started in
|
||||
it → bootstrap.sh runs the Dev Mode job + Lepton request + `/paired` as a user unit (log `~/.cache/frameport-setup.log`);
|
||||
no sudo/password anywhere (verified: naive Frame → connected, password never set). Valve's devkit pairing (fallback,
|
||||
PC → Frame only): `POST :32000/register` with an **ssh-rsa** key (`connection.devkit_key`, `frame/devkit.py`) works only
|
||||
while Steam is in pairing mode (Settings → Developer → Pair new host), else 403 at once; approve hook waits 30 s.
|
||||
The Frame runs firewalld (22 and 32000 open). PC side: the setup server needs inbound TCP 8765–8767 — WSL's Hyper-V
|
||||
firewall blocks it silently (`DefaultInboundAction Block`): `pairing.ensure_reachable` adds a temporary rule via one
|
||||
UAC prompt, removed when the server stops (flag file in %TEMP%, max 35 min); hints per OS after 45 s without a request
|
||||
(`pairing.firewall_hint`). Flet 1.0 patches aren't thread-safe → `app.serialize_flet_updates()` (a dialog shown while a
|
||||
scan redraw ran never closed: "dropped a patch for unknown control"); it also makes property writes (Flet's
|
||||
`Prop.__set__`) take the same `app.FLET_LOCK`: a property set on a background thread while the loop packed a patch
|
||||
changed the control's `_values` mid-iteration ("dictionary changed size during iteration", a sidebar click's page
|
||||
change died while the live Frame card took a sample). Anything that serves the app (ui_smoke, showcase) calls it too.
|
||||
**Lepton:** needs an activity with category **LAUNCHER** (Quest apps often only have INFO → "APP_ACTIVITY is empty").
|
||||
**2D apps:** Lepton runs every app headless (`lepton.headless=true`, only OpenXR output reaches the headset) unless the app folder (`<base>/lepton-app/`) has a `lepton-show-flatscreen` file (liblepton/app_metadata.sh) → Waydroid window on gamescope; agent v28 `set_flatscreen` at finalize for `vr_kind == "none"`. Android 11's navbar covered the
|
||||
app's controls → patch `device.hide_navbar` (default on for vr_kind none, migration `flat_hide_navbar`) exports
|
||||
`qemu.hw.mainkeys=1` as a second line of `LEPTON_GFXRECON_FP_PROPS` (runtime alternatives failed, see
|
||||
docs/FRAME_RUNTIME.md). Verified in the headset 2026-10-02 (2048).
|
||||
Lepton = Steam app 3029110 (+ "Lepton Development" 3056000, needs Developer Mode). Per-game env: STEAM_COMPAT_INSTALL_PATH
|
||||
/DATA_PATH/SHADER_PATH, SteamAppId. Logs: `<base>/launch.log` and `~/.local/share/Steam/logs/lepton-logcats/steamlaunch-<appid>`.
|
||||
Containers are podman `lepton-steamlaunch-<appid>`. Some Unreal games create save dirs without u+rwx → launcher repairs every 2 s.
|
||||
@@ -218,6 +503,22 @@ stopping Steam kills them → always run that work via `systemd-run --user` (the
|
||||
**overport:** always `--version=latest`; `--workspace` holds runtimes and **per-package keystores (password
|
||||
"password", alias "key") — never lose them**: updates must be signed with the same key or saves are lost on reinstall.
|
||||
Output is deterministic (same input + runtime → same bytes), which is what makes parity testing possible.
|
||||
**OVRPort 1.2.5 (2026-10-01, the fork's first release; CLI-only):** same commands (`patches [--json]`, `patch`, `help`,
|
||||
`install`), but `patch` rejects unknown/duplicate args, unknown patch ids and an empty `--patches=`
|
||||
(`tools/overport.patch` refuses empty lists). After patching it adds `libovrplatformcompat.so` itself when the platform
|
||||
loader lacks `ovrMessageType_ToString` (our `frame.ovrplatformcompat` then skips: same library). New patches (all off;
|
||||
`patches/overport.py`): `patch_ac_nexus_no_appsw_72/_90` (AC Nexus build 207706 only, exclusive),
|
||||
`patch_disable_meta_xr_audio_telemetry` (x86_64 emulators: hidden), `patch_vrapi_openxr` = OVRPort's VrApi adapter =
|
||||
the **unpatched** upstream of our `frame.vrapi_bridge` (`native/vrapi` unchanged since our 5e7df52), only usable with an
|
||||
experimental CLI built with `-PwithVrApi=true` (stable jars list it but fail). The owner prefers OVRPort's fixes over
|
||||
ours where they work as well (less to maintain): compare in the headset before switching a default.
|
||||
`frameport install <pkg> --apk <file>` installs a specific (test) build signed with the game's key.
|
||||
Headset results (2026-10-02, `PATCHED/_test-ovrport-1.2.5/TESTING.md`): OVRPort's own VrApi translator fails on the
|
||||
Frame (Climb 2: requests VkFormat 37 → crash; POTW: no GLES path, missing `vrapi_GetTextureSwapChainHandle`) → keep
|
||||
`frame.vrapi_bridge`; those two changes are upstream candidates. 1.2.5 builds (its platform compat, new permissions)
|
||||
work. AC Nexus: `patch_ac_nexus_no_appsw_90` is the catalog default (owner preferred 90 Hz); `STRICT` patches are only
|
||||
taken from a catalog recipe where `applies()` holds (the AC Nexus ones need build MAIN.450412.207706.final, else the
|
||||
whole overport run fails).
|
||||
|
||||
**Patching gotchas:**
|
||||
- UnityPy re-serialization breaks scene loading → patch QualitySettings ints in place.
|
||||
@@ -230,27 +531,426 @@ Output is deterministic (same input + runtime → same bytes), which is what mak
|
||||
- VrApi-direct engines (CryEngine Climb 2, POTW) need the VrApi bridge; the bridge drops whole frames on unknown layer
|
||||
types (→ black screen with audio).
|
||||
|
||||
**Steam Frame controller models (2026-09-30, not yet seen in a game):** adapter setting `controller_models`
|
||||
(`native/adapter/render_model.c`) emulates XR_FB_render_model and serves `files/framebridge/controller_{left,right}.glb`;
|
||||
agent v20 (`install_controller_models`, command `controller_models`) converts the Frame's SteamVR render models
|
||||
(folder name matching "frame" + left/right, OBJ+PNG, `openxr_grip` component → grip space) at finalize/set_settings.
|
||||
Valve's models are never committed or copied off the Frame. Only games that use Meta's runtime controller models
|
||||
benefit (manifest `RENDER_MODEL` permission/feature → suggested); **none of the 34 catalog games do** (they ship their
|
||||
own meshes: that would need per-game asset replacement). Model discovery and conversion
|
||||
checked on the device 2026-09-30: `/opt/steamvr/drivers/frame_controller/resources/rendermodels/frame_controller_{left,right}`
|
||||
(component OBJs in model space = the whole `<name>.obj`, one `_color.png` 2048² near-black, `openxr_grip` rotates about
|
||||
X only; hidden-by-default components like `status` are skipped) → ~2 MB glb each, ~1 s. Not yet seen in a game (none
|
||||
of the installed builds has the new adapter, and none requests runtime models), so whether Meta's SDK attaches runtime models at the grip pose is unverified.
|
||||
**overport's dispatcher (`libopenxr_loader.so`) only forwards functions in its own table** (`overportOXR: Unknown
|
||||
proc addr: …`), so adapter emulations of functions it doesn't know are unreachable. For XR_FB_render_model,
|
||||
`native/xrshim` fills the gap: OVRPlugin's `dlopen("libopenxr_loader.so")` string is rewritten to the shim (see
|
||||
native/README). Verified on the device with Toy Master (2026-09-30): `extension shim: xrLoadRenderModelFB -> FrameBridge`.
|
||||
Toy Master doesn't request a model after that (it uses its own), so the glb loading path is only unit-tested.
|
||||
|
||||
**Crash backtraces (2026-09-30):** Lepton writes tombstones only to `lepton-logcats/steamlaunch-<appid>/logcat-crash.log` (after "Dumping logcat"), not launch.log; agent v23 launch tests wait for it and return `crash_log`, triage gets it via `triage(..., crash=)` (not pid-filtered: tombstones come from crash_dump's pid). The guest is `userdebug` (`ro.debuggable=1`), so Lepton's Fossilize layer loads into every app whatever the APK's debuggable flag; it has no off switch. Fossilize crashed Deadpool VR on uninitialized attachment-reference pNexts (several, in FVulkanRenderPass and the render-target layout) → `frame.vk_sanitize` = `native/vkshim` (engine's `dlopen("libvulkan.so")` string → `libfp_vk.so`, wraps vkCreateRenderPass2/KHR, drops unreadable/wrong-sType pNexts; verified: game runs). overport's dispatcher aborts on swapchains > 4096 px (`cmp wN,#4096` in xrCreateSwapchain) → `frame.swapchain_limit` (verified with 4XVR, 7680×3840 accepted by the runtime). Both are default-on (parity classifies their rewrites as
|
||||
"expected"; library migration `quest_binary_fixes_v2`). The adapter shows cylinder layers (4XVR's movie screen) as ~15° quad strips (`cylinder_strips`, setting
|
||||
`cylinder_strips`); the runtime composites them at full resolution. Equirect (360°) layers are dropped: drawing
|
||||
them ourselves (GLES renderer in xrEndFrame: stutter, never showed 4XVR's VR video; Vulkan renderer: hung the
|
||||
Frame's GPU in AC Nexus; reading the newest *released* image broke 4XVR's theatre) was removed again on the owner's
|
||||
request — don't retry without a new approach. Converted layers must count as "swapped" or the original layer list
|
||||
is submitted (runtime returns -2 for every frame). A focus debounce (hiding the Frame's brief
|
||||
FOCUSED→VISIBLE→SYNCHRONIZED dips, which make 4XVR recenter) was also removed: AC Nexus stayed black with it (it
|
||||
hid 521 ms focus changes at start). The new approach (below) is per-game, off the app's render thread, GLES-only.
|
||||
|
||||
**360° layers / 4XVR (2026-09-30):** the Frame's Android SteamVR runtime (`/opt/steamvr/bin/androidarm64/vrclient.so`,
|
||||
mounted in Lepton as `/data/steamvr/runtime`) only composites quad + projection layers (cube/cylinder/equirect(2)
|
||||
are enum names only) — nothing to unlock. Per-game adapter settings (off by default, hooks installed only when on):
|
||||
`equirect_emul` (GLES only: a worker thread with a shared EGL context converts each 360° image to a cube map when it
|
||||
changes and draws one adapter projection layer from it for every frame with exactly that frame's views (the app's own
|
||||
projection views, else xrLocateViews at displayTime); xrEndFrame waits ≤6 ms for the worker's CPU submit, never the
|
||||
GPU. Learned in the headset: quads can't be a background (the Frame draws quad layers above all projection layers
|
||||
whatever the order: hid 4XVR's balcony/controllers), and the Frame doesn't reproject a projection layer from its own
|
||||
pose (images drawn only after head turns, or ahead from xrWaitFrame and sometimes late, wobbled);
|
||||
`equirect_flip/face/res/fps/stereo`), `stable_local`, `focus_hold` (only after 3 s FOCUSED, dips <600 ms),
|
||||
`aim_pitch/aim_yaw/aim_forward`, `refresh_rate`, `layer_debug` (diagnostics); focus_hold is default-on since 2026-10-05. 4XVR re-creates LOCAL spaces every 2–4 s
|
||||
(menu recentring suspect); its theatres are baked 7680×3840 equirect2 images (`assets/100.png` …). Test 360° videos
|
||||
(NASA, public domain) are in the Frame's ~/Videos; copies in `~/Downloads/frameport-360`. 4XVR's "Internal Storage"
|
||||
lists its own `/sdcard/4XPlayer`, not Movies: agent v25 `link_media` hard-links sent files into an app's own top-level
|
||||
folder (`app_media_dirs`), `frameport frame send --app <pkg>`, GUI game page "Add videos" (players) / "Add videos &
|
||||
files…". A 4XVR webm stereo swapchain was refused with XR_ERROR_LIMIT_REACHED (-10) → right eye grey; our projection
|
||||
swapchain was halved (1536/eye) in case memory is the limit (unverified). Frame data snapshot
|
||||
(SteamVR runtimes, Lepton scripts, logs; never commit Valve binaries): `~/frameport-research/frame-data-2026-09-30/`.
|
||||
Not yet verified in the headset.
|
||||
|
||||
**Companion apps / intents (2026-10-03, tested with Stremio + 4XVR):** a second APK can be installed into a running
|
||||
Lepton container (`podman exec -i lepton-steamlaunch-<appid> pm install -g -S <size> < apk`) and runs there, VR
|
||||
included (4XVR in Stremio's flatscreen container: FrameBridge 72 fps, settings via LEPTON_ENV_FRAMEBRIDGE_CONFIG). But
|
||||
Lepton's services.jar (`ActivityStarter.execute`) intercepts **every** `android.intent.action.VIEW` with data (any
|
||||
scheme, explicit component or not, no property to disable): it writes `steam://openurl/<uri>` to `/lepton/steam.pipe`
|
||||
(= the host Steam client's `~/.steam/steam.pipe`) and starts nothing. So "open in external player" hand-offs (Stremio
|
||||
→ 4XVR) can't work inside Lepton; don't retry without Valve changing it. Lepton installs exactly one `*.apk` per app
|
||||
folder (two break `get_apk_path`).
|
||||
**Text input (2026-10-03, verified in the headset with Stremio VR):** Lepton's Android has no IME (`ime list` empty)
|
||||
and runs VR apps headless (`lepton.headless=true`): no Android window has input focus (`dumpsys input` FocusedWindows
|
||||
empty), so neither `input text`, a USB keyboard nor Steam's keyboard reach the app. `lepton-show-flatscreen` on a VR
|
||||
app keeps VR working (FrameBridge 72 fps) and gives the window focus; Steam's on-screen keyboard then opens for text
|
||||
fields. Unity's TMP_InputField on non-Quest Android waits for the system keyboard (`TouchScreenKeyboardShouldBeUsed`)
|
||||
and deselects a frame later unless `isKeyboardUsingEvents` (Android: `InPlaceEditing() && m_HideSoftKeyboard`);
|
||||
uGUI InputField's LateUpdate keeps the field when `InPlaceEditing()`. Patch `frame.unity_text_input` rewrites them
|
||||
(`mov w0,#0|#1; ret`) at Cpp2IL's **Offset** (= file offset; RVA differs by 0x4000 in Stremio's lib), Cpp2IL
|
||||
2022.1 pre-release (Unity 6 / metadata v31; Il2CppDumper can't), cached per libil2cpp sha. `/dev/uinput` has an ACL
|
||||
for steamos (Steam Input) → agent v33 `_keyboard` uinput keyboard ("Type on Frame") reaches everything with focus.
|
||||
Installing a second APK into a container with `pm install` re-runs Lepton's post-install hook on the **main** app
|
||||
(`lepton.active_app_id`; moves its files to /data/steam_app) and corrupts it on the next start (fix: touch the APK →
|
||||
re-bake); `cmd_real package install` skips the hook.
|
||||
**User reports (2026-10-04, issues #4-#10):** Steam shortcuts go to the signed-in account (`loginusers.vdf`
|
||||
MostRecent) else every account (agent v35 `library_users`; it used to refuse with >1 account → Play gave Steam's "Game
|
||||
configuration unavailable"); `launch` uses the shortcut's own appid and errors `NOT_IN_LIBRARY`, the app then adds it
|
||||
and plays. Unity IL2CPP fixes share one Cpp2IL run (`unity_text_input.Il2cppReturnPatch`, `ALL_TARGETS`, cache keyed by
|
||||
global-metadata.dat): `frame.unity_runtime_msaa_off` (OVRManager raises MSAA to 4x at runtime: "Switching to the
|
||||
recommended level" → Lucky's Tale restarted the headset; `OVRDisplay.get_recommendedMSAALevel` → 0) and
|
||||
`frame.unity_multipass` (Oculus XR Plugin multiview → MultiPass via `OculusSettings.GetStereoRenderingMode`; I Am Cat's
|
||||
right eye grey). Both checked against Toy Master's Cpp2IL output, not yet in a headset. VR4 quits itself (System.exit
|
||||
after the intro movie) → catalog `use_alt`. **Catalog changes reach existing games** without hardcoded migrations:
|
||||
recipes store `catalog_rev` (= `CatalogEntry.rev()`), `library._follow_catalog` re-derives non-user recipes whose entry
|
||||
changed; a maintained entry beats the user's shared copy when its `updated`/verified date is later (`catalog._newer`).
|
||||
**Round 2 of reports (2026-10-04):** VR4's campaign hang = one fragment shader reading an uninitialized loop counter
|
||||
(reporter's capture): vkshim `vk_shader_fix` setting (`<size>:<sha256>:<offset>:<words>`, from the recipe's `adapter:`;
|
||||
matched by size + SHA-256, words inserted into a copy) — on the device the module is fixed at load ("fixed shader
|
||||
module (6488 -> 6512 bytes)"); campaign itself not yet seen in a headset by us. Text settings (kind "str") stay out of
|
||||
the Game settings dialog (saving it dropped them). I Am Cat: `GetStereoRenderingMode` is inlined (no call sites) →
|
||||
`Il2cppReturnPatch.field_loads` rewrites the field read in `OculusLoader.Initialize` (Cpp2IL gives the field offset,
|
||||
`field:<name>`); verified with Toy Master: eye swapchains array=1 ×2 instead of array=2. Builds record `recipe_fp`
|
||||
(`patches/base.recipe_fingerprint`, `Patch.revision`): a changed recipe or revised patch shows "Update on Frame".
|
||||
Remembered Frames are found again after an address change (`Frame.relocate`: scan, same SSH host key, before
|
||||
login). Packaged apps show artwork by file path (`thumbs.use_file_paths`, #16). Diagnostics: this boot's kernel log.
|
||||
Device tests of requested games (2026-10-04, headless): I Am Cat, Myst 3.3.0, BattleGlide, Blade & Sorcery: Nomad
|
||||
start (RUNNING, frames). Roblox: SIGSEGV in je_free from libroblox.so ~3 s in (likely its anti-tamper vs the re-signed
|
||||
APK). BONELAB (Unity 2021.3 Vulkan, OVRPlugin 1.94): SIGSEGV with pc == fault addr in vkCreateInstance, called from
|
||||
libSLZQuestNative.so's Vulkan hooks into an unmapped (unloaded) library right after OVRPlugin's pre-init
|
||||
xrDestroyInstance; pinning openxr/vrclient libraries (RTLD_NODELETE) did NOT help — find the unloaded library with the
|
||||
linker's dlopen/dlclose logging next. Accounting+ (Unity 2017.4 built-in Oculus, libOVRPlugin + libvrapi): Unity
|
||||
never starts VR on the Frame (no OVRPlugin/OpenXR lines; runs as a 2D app), which is why its "controller setup"
|
||||
screen can't be passed — Unity 2017's Oculus device check, not the controller profiles. Cpp2IL can't read Unity 2017.
|
||||
The Frame's Android runtime (vrclient.so) knows oculus/touch_controller but not Meta's Touch Plus/Pro profiles
|
||||
(OVRPlugin suggests those too, routinely) → adapter `profile_remap`.
|
||||
Headset round 2 (2026-10-04): the Frame drops focus often (ms to 7 s; only some are presence/standby) → games pause
|
||||
or recentre (B&S Player.OnVRPresence → Teleport, BattleGlide/Unreal pauses); focus_hold now 1 s focused / 1 s max dip
|
||||
(longer must still pause). Myst crash = vrclient xrSyncActions race after FOCUSED → `sync_guard` (confirmed); its
|
||||
object glitches: app space warp suspected (FB_space_warp is advertised and used) → recipe `patch_disable_space_warp`.
|
||||
I Am Cat multipass: poses/times consistent, but every frame is submitted after the next xrWaitFrame (one period late;
|
||||
Zink + doubled draws) and the game clamps its physics step to 10 ms → judder while still; testing scale 0.8. Roblox's
|
||||
newest crash is on Fossilize's recording thread → vk_sanitize extended to own-engine libs (engine "Other").
|
||||
Accounting+: unity_oculus_check + native/ovrpshim (libfp_ovrp.so waits via ovrp_WaitToBeginFrame before Unity
|
||||
2017's ovrp_Update2) → VR in the headset at ~72 fps (2026-10-04), but stuck at "press any button" although input
|
||||
reaches its OVRInput cleanly (probe: both Touch connected 0x63, individual buttons, input focus 1) → works with issues.
|
||||
"outside of frame bounds" warnings stay (~2/frame) and are harmless.
|
||||
Steam library (GitHub #4/#21/#27, agent v38): shortcuts go to every Steam account (signed-in first; MostRecent isn't
|
||||
always the account on the Frame); stop_steam also waits for Steam's helpers; after the restart the agent re-reads
|
||||
shortcuts.vdf and writes once more if Steam put its old copy back (`shortcuts_lost`), else reports it. 0.6.3's Play
|
||||
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). 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
|
||||
Desktop Mode is open. Verified on the device 2026-10-04 (install + launch test from the Frame). Library updates on the
|
||||
Frame run one at a time (agent v40 flock): two quick uninstalls used to bring a removed shortcut back (Roblox).
|
||||
Headset round 3 (owner's verdicts, 2026-10-04): Blade & Sorcery works — its freezes were the headset's wear sensor
|
||||
flickering off while worn (vrserver.txt "HMD off/on" 0.5-2 s) → per-game `focus_hold_ms=2500` (only this game reacts;
|
||||
not a default). BattleGlide works (focus_hold). Myst works with issues (object glitches; space warp ruled out, its
|
||||
recipe keeps it on). I Am Cat works with issues (judder; scale 0.8 no help). Roblox unsupported (je_free in
|
||||
libroblox, also with the Vulkan shim). Installs skip the Steam restart when the shortcut is unchanged and wait while a
|
||||
game runs (agent v37). New default fixes in FramePort's code reach games already in a library: after every app update
|
||||
`library._follow_catalog` re-derives each non-user recipe once (setting `recipes.app_version`; tests switch it off via
|
||||
`library.REFRESH_ON_UPDATE`). Analysis fields added later: **bump `analysis/detect.ANALYSIS_VERSION`** (stored as `analysis.extra.analysis_version`); older entries whose APK is still there are analysed again at the GUI's start (background thread, not a job: ~8 s per 900 MB APK) and before a build (`pipeline.refresh_analyses`, GitHub #104); only `analysis`/`suggested` change, user recipes stay; an unreadable APK gets `analysis_failed` = the version (not retried until the next bump). Troubleshooting techniques: docs/PLAYBOOK.md "Debugging techniques". Unity `boot.config` "vulkan" substring mislabels GLES games as Vulkan
|
||||
(I Am Cat ran GLES); OVRPlugin's "Unavailable OpenXR extension: XR_FB_scene" is routine (no longer triaged).
|
||||
**Round 4 (2026-10-04):** XR_KHR_android_surface_swapchain is listed by the Frame's runtime but returns
|
||||
FUNCTION_UNSUPPORTED → adapter `surface_emul` (default on, `native/adapter/surface_swapchain.c`): an ordinary runtime
|
||||
swapchain is returned, the game gets a Surface from a SurfaceTexture on a worker thread (own EGL context, JavaVM from
|
||||
XrInstanceCreateInfoAndroidKHR), frames are read back (glReadPixels) and uploaded in xrEndFrame (Vulkan: own command
|
||||
buffer + fence per panel, never waits; GLES: glTexSubImage2D). I Am Monkey's intro works (owner-confirmed). SUPERHOT
|
||||
(Quest) quit at start: its cloud save folder was mode 1700 and Lepton's app writes through the folder's group →
|
||||
launch.sh `fix_perms` adds u+rwx,g+rwx (agent v41). Lucky's Tale: Unity's Loading.PreloadManager thread grows the
|
||||
native heap ~1 GB/s until the OOM killer (Frame freezes); no fix. `perf` works as steamos (paranoid 2; ptrace_scope 1
|
||||
blocks gdb/eu-stack/debuggerd) — see PLAYBOOK "Debugging techniques". QuestCraft downloads its JRE at first run into
|
||||
`files/runtimes/JRE` (Pojlib Installer); on the Frame it was missing (unpacked by hand, unverified). WiiCompiled: its launcher shows
|
||||
(lepton-show-flatscreen) but imports .wcgame files through Android's document picker (OPEN_DOCUMENT), which Lepton
|
||||
lacks (ActivityNotFoundException) → imported by hand: libmain.so + game.json → internal files/game/<profile>,
|
||||
DATA/ → external files/WiiCompiledOpenXRVR/DATA, MOD/ → .../RetroRewind6 (decompiled with jadx). Lepton has no
|
||||
picker at all: a FramePort fix would be an injected picker activity. Generated cover/banner art used to suppress the
|
||||
Steam placeholder set (no Steam art at all) → fixed in artwork/steam.py.
|
||||
**Steam library failures (GitHub #21/#30):** on some Frames Steam never loads FramePort's shortcuts.vdf entries (same SteamOS build and arm64 beta client as the dev Frame; IDs correct; file written before Steam starts): Play → console_log `GameAction [AppID <id>] … RequestingLicense → UpdatingAppInfo → LaunchApp failed with AppError_9` (= "Game configuration unavailable", Steam treats the id as a store app). Cause unknown; agent v42/43 diagnostics: `launch` returns `steam` (started / error + code / silent from console_log), `collect_diag` adds `steam_library` (real Steam dir, beta, Steam start vs vdf write time, entries per account, devkit games) + `steam-console.txt` (incl. Steam's `logs/shortcuts.previous.txt` if present; steamclient.so has "LoadShortcuts: rejecting attempt to load shortcuts: invalid account ID"). **Fallback (agent v43):** when Play gets AppError_9 the agent registers the game through Steam's devkit interface (what Valve's Devkit Management Tool does, MIT: `devkit-1 steam://devkit-1/<~/.steam/steam.token>/create-shortcut?response=<file>&gameid=<id>&directory=~/devkit-game` written to ~/.steam/steam.pipe; answer file / `.error`; `~/devkit-game/<id>/launch.sh` = link to the game's launch.sh, `<id>-argv.json` ["launch.sh"] (relative to the folder), `<id>-settings.json` {steam_play 0, compat_tool ""}). Steam adds it live (no restart), picks its own appid (read back from shortcuts.vdf by DevkitGameID), shows it as a normal non-Steam game named "Devkit Game: <id>" (OpenVR 0, but VR works: owner-verified 2026-10-04), art copied to grid/<appid>*. Ids: letter first, then letters/digits/_ only (spaces, '-', leading digit → "missing/invalid arguments"). Later Plays use the devkit entry; uninstall (`devkit_unregister`, `delete-shortcut` live) and purge remove it. Verified on the dev Frame: register/launch/unregister; the AppError_9 trigger itself only in unit tests. GitHub #42 (two Steam accounts, neither MostRecent): Steam rewrote the active account's shortcuts.vdf without FramePort's entries right after starting, and added the devkit entry live without saving it, so `devkit_appid` (vdf only) failed ("Steam added the devkit entry but didn't save it"); agent v55 `devkit_appid_from_log` reads Steam's `sanitize shortcut app id "~/devkit-game/<id>/launch.sh": replacing 0 with N` console line. GitHub #41 (one account): same Steam behaviour, and every install restarts Steam, which forgets the never-saved devkit entry. Agent v56: the log fallback reads only the current session's console_log.txt; Play re-registers a devkit entry that gets AppError_9; art updates also write the devkit entry's grid art (`copy_grid_art`). Open: why some Frames' Steam never loads or saves shortcuts.vdf (single account too).
|
||||
**Exit game (GitHub #36, 2026-10-04):** `Apps.TerminateApp` (what Exit game calls; reachable for tests through Steam's CEF devtools on 127.0.0.1:8080, SharedJSContext, `Runtime.evaluate` — a stdlib websocket client is enough) closed both a normal shortcut and a devkit entry on the dev Frame within 1-3 s. SIGTERM to Steam's `reaper` alone left launch.sh + Lepton (setsid) + the container running (an earlier headset launch ran on for 87 min); SIGTERM to launch.sh cleans up in ~4 s. Agent v44: launch.sh's 2 s loop ends the game when its parent (the reaper) is gone; `upgrade_launchers` (from ensure_host_fixes) adds that to existing launchers in place. Which in-headset exit path fails is still unknown. Screenshots on the Frame: all under SteamVR's appid 250820 (`760/remote/250820/screenshots`, screenshots.vdf has creation time, no game).
|
||||
**SteamVR dashboard at game start (2026-10-04):** the dashboard (Resume game / controller / VR options) was open whenever a FramePort game started (also over Lepton's 2D launcher). Steam's UI exposes `SteamClient.OpenVR.VROverlay.{IsDashboardVisible,HideDashboard,ShowDashboard}` (CEF devtools, `steam_js` in the agent). Agent v44: launch.sh starts `_dashboard_worker` (waits for FrameBridge's first `pacing:` line in launch.log, then hides a visible dashboard for 25 s, at most 3 times; log `<base>/dashboard.log`; opt out with env FRAMEPORT_KEEP_DASHBOARD=1). Verified on the dev Frame (dashboard shown → 4XVR launched via Steam → hidden ~5 s after the first frames); not yet seen in the headset. **Headset (ITR2, 2026-10-05): too late** — Steam shows its frame menu (`valve.steam.gamepadui.frame.menu`, vrwebhelper_systemui.txt `[PooledPopups] Showing`) ~0.3 s after the game's first submitted frame (FrameBridge `new layer:`), but the first `pacing:` summary comes ~8 s later; the owner had pressed Resume (`[HideDashboard] return_to_game`) before the worker looked. Agent v52: watches from the first `new layer:` line (incremental log reads, 0.5 s polls, 30 s window). Agent v59: watches 120 s and hides up to 10 times (the menu came back after 30 s in the owner's sessions). Agent v53: stops for good once the player opens the dashboard with the controller (`toggle_dashboard_action` in vrwebhelper_systemui.txt since the worker started): v52 closed it 60 ms after each press, and the game, paused for it, stayed paused.
|
||||
**Space warp / ITR2 (2026-10-04):** Into The Radius 2 (UE5, `libUnreal.so` + OVRPlugin, Vulkan) uses Application SpaceWarp (extra swapchains `376x376 format=97` motion vectors + `format=129` depth); #35 reports flickering textures. OVRPort's `patch_disable_space_warp` has no effect on UE5, and hiding XR_FB_space_warp in FrameBridge's enumerate doesn't reach the game either: **OVRPort's dispatcher offers/enables XR_FB_space_warp itself**. FrameBridge `hide_space_warp` (per game, off) hides it and strips `XrCompositionLayerSpaceWarpInfoFB` (1000171000) from the projection views (log `hide_space_warp: removed space warp info`); the game still renders its MV swapchains. Test build installed on the dev Frame; flicker not yet checked in a headset. Heuristic: UE5 + OVRPlugin shows it as an option (off). `equirect_emul` is suggested for Unreal games whose graphics API isn't detected ("GLES or unknown"); on Vulkan it switches itself off (Myst/Riven/ITR2): harmless, but Unreal Vulkan detection is a known gap.
|
||||
**PR #34 (merged 2026-10-04, Lucas-Mathieu):** `native/adapter/audio_metadata.h` patches Meta XR Audio Wwise (only build ID e1619e7f…, Batman: Arkham Shadow 1.4.1) so queued audio metadata isn't freed while the current audio frame references it (smoke-bomb crash). Verified headless: installs ("metadata reclamation follows …"), 72 fps; we added a once-per-second scan limit. Outside PRs: review source, rebuild `artifacts/` ourselves, merge locally (contributor's commit kept), push main.
|
||||
**Heuristics eval (2026-10-04):** `scripts/eval_heuristics.py "<VR CyberDeck Portable>"` → 19/44 recipes exact, 322/352 fields (most diffs: `frame.unity_text_input` suggested where older catalog recipes lack it; the rest are runtime-only findings: use_alt, vk_shader_fix, focus_hold, sync_guard, multipass). `PATCHED/` copies aren't scored (already converted).
|
||||
**Power (0.9, agent v47):** sidebar bar below the Frame card: Sleep / Restart / Shut down, each confirmed (`app.frame_power`, a running game asks again). logind answers CanSuspend/CanReboot/CanPowerOff = "challenge" for an SSH session but "yes" inside a user unit, so agent `power` runs `systemctl suspend|reboot|poweroff` from a transient `systemd-run --user --on-active=3` timer (the command answers first). Restart verified on the device (back in ~84 s, new boot id).
|
||||
**Proton default (agent v51, owner's choice 2026-10-05):** the newest **stable** ARM64 Proton is the default for PC VR games (`pick_proton`: asked-for name/alias, else newest installed stable, else any stable). Agent v49-50 defaulted to Experimental; the owner's headset A/B with Rick and Morty (Experimental 69 % reprojected frames, stable felt much smoother) reverted it. The game page's Customize shows `pcvr.proton_tool` as a dropdown (Stable (default) / Experimental = alias `proton-experimental`); `ensure_proton` installs a chosen one (it asks once more for the runtime Steam only names after Proton is installed; Experimental installed unattended on the device in 53 s). A PC VR launch test that fails on stable suggests Experimental (`pipeline.PROTON_TOOL`, `proton_alternative_worth_trying`, button "Try Proton Experimental and reinstall"). Remaining Rick and Morty stutter on head turns = the game rendering below the refresh rate (reprojection), not Proton.
|
||||
**USB cable link (verified 2026-10-05, cabled to the dev PC):** `usb0` is a USB **NCM** gadget (configfs g1: `ncm.usb0` + `ffs.adb`, Valve 28de:2460) set up by `usb-ncm-gadget@usb0.service`, which only runs with `ConditionPathExists=|/etc/systemd/system/adbd.service.d/steamos-devkit-enabled` = **Developer Mode only**. `usb-ncm-dnsmasq@usb0` serves DHCP with fixed `dhcp-host` entries: Frame 10.86.200.233, PC **10.86.200.234** (`frame/usb.py` FRAME_USB_IP/PC_USB_IP). Windows 11 binds its built-in "UsbNcm Host Device" (no driver); WSL mirrored sees eth 10.86.200.234. USB is high-speed (USB 2.0): upload 36.9 MB/s vs 11.3 MB/s home Wi-Fi. Frame→PC over the cable worked without a firewall change. `scripts/usb_autotest.py` (Windows via powershell.exe from WSL), `frameport frame usb-check`. Wizard "Set up with a USB cable" (`PairingServer(host=PC_USB_IP)`; the real bootstrap ran end to end over USB). Discovery dedupes by SSH host key and prefers USB > Frame hotspot > network (`discovery.dedupe`); a lost address is re-found by key (`Frame.relocate`, tested with a dead saved address).
|
||||
**Catalog updates without a release (2026-10-05):** apps fetch `catalog/games/*.yaml` from GitHub `main` (`recommend/catalog.refresh_remote`: one API call `git/trees/main?recursive=1` + raw.githubusercontent downloads of changed blobs, cached as `catalog-gh-<sha>.yaml`, every 6 h; GUI at start + Settings → Data "Check now"; CLI background thread + `frameport catalog-update`). `catalog.load()` only reads the cache. A remote entry is skipped (`unusable_reason`) when it names a patch/adapter key this app doesn't register, has a field `CatalogEntry` doesn't know, or `min_app` > this version: add `min_app` (or rely on new patch ids) when a config needs an unreleased FramePort. Remote beats bundled unless the bundled entry has a later updated/verified date; the user's own entries still win as before. Off: setting `catalog.auto_update`, env FRAMEPORT_NO_CATALOG_UPDATE; FRAMEPORT_CATALOG_URL still overrides the source. So: **pushing a catalog change to main reaches users within 6 h** (their recipes follow via catalog_rev → Update on Frame).
|
||||
**Start activity / QuestCraft / ITR2 pop-in (2026-10-05):** WiiCompiled's VR part (`QuestActivity`, SDL, process `:game`) stays behind Lepton's flat window when the launcher is shown (`device.text_input_window`). `frame.start_activity` (opt-in; analysis `extra.vr_activity` = the original APK's activity with Meta's VR category when it isn't the launcher) gives that activity's filter LAUNCHER and every other LAUNCHER becomes INFO, per element (`axml.set_start_activity`); Lepton's own `apk-info-extractor --print-activity-name` (on the Frame under Lepton/liblepton/apk_extractor) then answers QuestActivity. **OVRPort gives every MAIN activity LAUNCHER + VR categories**, so the VR activity must come from the original APK's analysis, not the converted manifest. Lepton has no activity override (`APP_ACTIVITY` is reset when app_metadata.sh is sourced). QuestCraft reaches Minecraft 1.21.5 (Fabric, Vivecraft, Sodium; LTW "Large Thin Wrapper" GL 3.0 on the Frame's Zink) and stops at `OpenGL error 1282` in `WindowFramebuffer` createTexture (Minecraft's crash report in `files/instances/1.21.5/crash-reports/`, Pojlib's `files/latestlog.txt`); the vrclient SIGSEGV / "pthread_mutex_lock called on a destroyed mutex" afterwards is only the shutdown (Unity's render thread still in vrclient while the instance is destroyed). ITR2 "models popping in and out" in game: Unreal reads user config from **internal** storage (`lepton-data/internal/<pkg>/files/UnrealGame/<Project>/<Project>/Saved/Config/Android/Engine.ini`, not external); `r.AllowOcclusionQueries=0` there broke its menus (removed again). Vulkan shim `vk_query_slots` (two slots per occlusion query) never saw ITR2 create an occlusion query pool, and the run with it crashed (Unreal RenderThread SIGSEGV, caught by sentry-native: no tombstone; the backtrace is in `cache/sentry/*/.sentry-native/*.run/*.envelope` with module offsets) - left off. Next suspect: Unreal 5 mobile HZB occlusion reading depth stored with Valve's FDM injection layer; test = launcher `VK_INSTANCE_LAYERS=''` (Lepton then loads neither VALVE_rpo nor VALVE_fdm_injection, Fossilize still). QuestCraft: LTW overrides glGetError and returns 0 with `LIBGL_NOERROR` set; Lepton passes `LEPTON_ENV_<NAME>` to the app as `<NAME>`, so catalog `lepton_env: {LEPTON_ENV_LIBGL_NOERROR: '1'}` gets Minecraft past the GL error 1282; then black picture (see the catalog entry). FrameBridge logs `new layer` once per layer type per process and projection views only for the first frames, so a second session in the same process (Vivecraft after Unity) needs `layer_debug=1`.
|
||||
**Vulkan validation / ITR2 solved (2026-10-05):** Khronos' validation layer can't come from Lepton (its layer dir is in the read-only guest image; the Linux copies on the Frame are glibc) and a global `debug.vulkan.layers` property (via the LEPTON_GFXRECON_FP_PROPS trick) kills the container at boot. Working recipe: bundle `libVkLayer_khronos_validation.so` (Khronos android-binaries release, arm64-v8a, stored uncompressed) into a test APK re-signed with the game's key (`apk.sign.sign`), install with `frameport install <pkg> --apk`, and set the Vulkan shim's `vk_validation=1` (it adds the layer at vkCreateInstance; findings under the logcat tag VALIDATION in launch.log, each message ID capped at 10). ITR2's flicker + windows behind models = Unreal's Qualcomm shader-resolve subpasses with a depth resolve of a single-sampled depth attachment (VUID-04908/-03179); `vk_spec_fixes=1` drops that resolve (its TRANSFER_DST half never applies to ITR2: the main depth is transient). Valve's VALVE_rpo layer removed ITR2's fog (catalog `lepton_env: VK_INSTANCE_LAYERS: VK_LAYER_VALVE_fdm_injection`). `r.ViewDistanceScale=2` in the internal Saved/Config/Android/Engine.ini only exists on the owner's Frame (the catalog can't write internal storage yet). Launch tests: Lepton prints a transient "is not a running context" on the first start after an APK change (agent v54 `not_started` waits 30 s for "Boot complete!").
|
||||
**GitHub #38/#39 (2026-10-05):** PowerWash Simulator stuck at "Waiting.." on a reporter's Frame = Unity sized the right-eye image rect past its swapchain (rect 268+1656 on a 1920-wide swapchain; the runtime's recommended size differs per Frame) and SteamVR rejected every frame (`xrEndFrame failed -25`, XR_ERROR_SWAPCHAIN_RECT_INVALID), after which the game stopped calling xrEndFrame. FrameBridge `rect_clamp` (default on; sizes noted in xrCreateSwapchain; clamped layers count as swapped) keeps projection/quad rects inside their swapchain; triage `swapchain-rect-invalid`. Not reproducible on the dev Frame with `scale` (Unity's eye sizing isn't just the recommended width); no regression there (72 fps). The adapter's revision was NOT bumped (that would mark every installed game outdated): affected users rebuild. The Room VR (#38) = Unity's render thread SIGSEGV in libgallium_dri.so ~30 ms after the game recreates its eye swapchain with samples=4 (FrameBridge retries samples=1); the game's own code turns MSAA on (not OVRManager); candidate fix: hide GL_EXT_multisampled_render_to_texture (GL shim) - needs the APK to test. The launch test passed both (render thread dead / no frames): triage gap.
|
||||
**Headset round 2026-10-05 (evening):** I Am Cat's judder = repeated xrLocateViews of one display time returning
|
||||
slightly different poses → FrameBridge `pose_consistency` (owner-verified, works). Lucky's Tale freeze on touching a
|
||||
save slot = OVRPort's OpenXR dispatcher converting XR_FB_haptic_amplitude_envelope with ns read as s (GitHub #9) →
|
||||
xrshim `haptic_fix` (owner-verified); its remaining short pauses were the wear sensor ("HMD off/on" 0.5-1 s in
|
||||
`logs/eyetracking.txt`, not vrserver.txt) → **`focus_hold` is on by default with 5 s** (owner's choice; C defaults
|
||||
too). The Room VR works (`unity_no_overlay_copy`). BattleSisters (Unity 2019.4 built-in VR, no libOculusXRPlugin.so)
|
||||
flooded "outside of frame bounds" ~2400/s and hung the GPU (kernel hangcheck, zink DEVICE LOST) → the ovrpshim frame
|
||||
wait now also applies to Unity 2019 without the Oculus XR Plugin (`UnityOculusCheck.legacy_loop`). Vader Immortal
|
||||
(UE4, stuck on the loading image after the intro, GitHub #49): VRP repacks carry a Frida gadget (`libfrda.so`,
|
||||
config `hijack_responses`), but OVRPort's `patch_clean_up_frida` removes its loadLibrary call, so it never runs on
|
||||
the Frame; `frame.ovr_trace` (opt-in, `native/ovrtrace`: 1138 exported ovr_* stubs → real loader functions, logcat
|
||||
tag `fp_ovrtrace`: calls, PopMessage answers, unanswered requests every 10 s) is the next diagnostic.
|
||||
**Unity built-in VR input (2026-10-05):** probe builds (`FRAMEPORT_INPUT_PROBE=1 frameport build …`; it logs
|
||||
trigger/grip crossings too) showed OVRPlugin returns the full Touch state for every controller mask (also Go masks);
|
||||
presses reach the games' OVRInput. Accounting+ (Il2CppDumper v6.7.46 reads its Unity 2017 metadata v24, run on Windows)
|
||||
passes stance selection (NewtonVR grip/trigger) but its motion warning waits for `Input.GetMouseButtonDown(0/1)`,
|
||||
which Lepton never delivers (no focused Android window) → ovrpshim registers its own
|
||||
`UnityEngine.Input::GetMouseButtonDown(System.Int32)` icall (il2cpp_add_internal_call, after Unity's: resolve first)
|
||||
that adds a click in the frame a Touch trigger or A/B/X/Y is newly pressed (`unity_oculus_check` revision 3).
|
||||
BattleSisters (Unity 2019 InputSystem/XR InputDevices via `VrHandInput`): libunity's Oculus module (OVRPlugin
|
||||
function table: a global pointer at 0x16b57c0 in this build, slots filled by name, e.g. +0x160 GetControllerState,
|
||||
+0x168 State2, +0xe8 GetNodePresent) only reports controllers when `strncmp(deviceModel, "Oculus", 6) == 0` (next to
|
||||
the Go check `deviceModel == "Oculus Pacific"`); Lepton's model is "Valve Lepton" → Go/unknown → only Go masks polled,
|
||||
buttons dead. `unity_oculus_check` revision 4 (`oculus_model_checks`, Unity 2019 only) turns the 17 compares' length
|
||||
into 0 (`orr w2, wzr, #6` → `mov w2, #0` after the adrp/add of the "Oculus" literal; the string stays: it is also
|
||||
Unity's VR device name). Device result: libunity now polls `ovrp_GetControllerState(0x3)` (Touch); owner-confirmed
|
||||
2026-10-06: buttons work. Its first controller vibration then hit OVRPort's haptic-envelope bug (11 GB, OOM kill) →
|
||||
`haptic_fix` is now suggested for every game with libOVRPlugin.so (settings detect), BattleSisters works. Ruled out before: ProductName, GetNodePresent, the device-model string itself, exports.
|
||||
Accounting+ works (owner, 2026-10-06).
|
||||
**Vader Immortal (UE4, GitHub #49, 2026-10-06, headless):** stuck after the intro on an in-game image (the splash
|
||||
quad ends ~6 s in; then the game's own projection frames, 72 fps, balanced xrBeginFrame/xrEndFrame). Not the Platform
|
||||
SDK (`frame.ovr_trace`: only user + entitlement, both answered) and not the repack's Frida gadget (OVRPort's
|
||||
`patch_clean_up_frida` removes its loadLibrary). It **leaks ~430 GPU mappings (/dev/dri/renderD128) and ~20 MB a
|
||||
second** (6.5 GB + swap after 5 min, then 26 fps): page-fault stacks (perf -e page-faults, offsets resolved with the
|
||||
process maps + vrclient.so's own symbols; its text segment is at file offset + 0x4000) end in SteamVR's runtime:
|
||||
`xrBeginFrame → CSxrCompositorOpenVR::BeginFrame → SubmitExplicitTimingData → CVRCompositorSharedTextures::
|
||||
BeginGPUTimingCommandBuffer` and `xrEndFrame → CVRCompositorClient::SubmitWithArrayIndexAndTime`. Ruled out: the
|
||||
layer color scale/bias + image layout structs (FrameBridge `strip_color_bias` 1/2, diagnostic), Valve's Vulkan
|
||||
layers (VK_INSTANCE_LAYERS=""), array swapchains in general (Lucky's Tale/I Am Cat flat). Kernel tracepoints aren't
|
||||
allowed for steamos. FrameBridge: `layer_debug` logs xrDestroySwapchain and per-5 s xrBeginFrame/xrEndFrame counts;
|
||||
`frame_balance` (ends an open frame before the next begin) exists but Vader never leaves one open. Next (built,
|
||||
not yet run: the Frame slept): `layer_debug` also counts xrAcquire/Wait/ReleaseSwapchainImage per 5 s (hooked only
|
||||
with layer_debug, after the other conditional hooks) to see whether Vader skips a wait or release: it doesn't (361/361/361 per 5 s).
|
||||
FrameBridge `snapshot=N` (`snapshot_gl.c`, GLES): every N s the left-eye image the game submits is read back on its
|
||||
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
|
||||
`flock` on `<base>/.launch.lock` (fd 9, inherited by Lepton; the watchdog and dashboard helpers close it with
|
||||
`9>&-`); a second launch exits 0 and logs to `<base>/launch-dup.log`. `upgrade_launchers` adds it to existing
|
||||
launchers. Verified on the device (second launch during boot: ignored, the first kept running). Vader on its loading
|
||||
card ignores input too (owner pressed/held every button: presses reach the runtime, the card never changes).
|
||||
**Issue triage round (2026-10-06, owner's headset):** vibration in OVRPlugin games = 2 s vibrations updated per frame and
|
||||
stopped with amplitude 0; FrameBridge turns amplitude 0 into xrStopHapticFeedback (else each buzz ran 2 s), haptic_fix
|
||||
uses the envelope RMS (was its peak), per-game `haptic_scale` → Creed/The Boys/Jurassic World fine. Lambda1VR (TBXR):
|
||||
loader named by Build.MANUFACTURER (`frame.tbxr_vendor`), no graphics extension enabled (FrameBridge adds
|
||||
XR_KHR_opengl_es_enable + asks for the requirements on -50), always multisampled render-to-texture (GL shim gives
|
||||
single-sampled stand-ins when GL_EXT_multisampled_render_to_texture is hidden) + its xash/ data → works. Jurassic World:
|
||||
`frame.vrapi_stub`. Metro Awakening: contributor's vkshim fixes + hide_space_warp + no VALVE_rpo (jumping polygons like
|
||||
ITR2). Pinball FX VR: hide_space_warp (stutters). Eleven: a Meta online request fails after platform init → needs Meta
|
||||
services. Star Wars Tales: top half black + freeze after loading; ruled out: asset-file paks (never requested), Valve
|
||||
foveation (off: no change), GL errors (MESA_DEBUG=1: none), EGL_BAD_ACCESS once in OVRPlugin init (harmless); the game's
|
||||
eye image reads back black. BlazeRush: VrApi bridge exports + ovr*_ToString stubs + avatar stub get it to the menu room
|
||||
at 72 fps, full input reaches it (diagnostics `input 5s`: Touch type only, sticks 1.0, buttons, poses 0x8f), but the room
|
||||
draws no controllers and ignores input; avatar loader forced to its "Failed" path (no logged-in user) changed nothing.
|
||||
`frame.ovr_trace` can't trace Unity games (P/Invoke dlsym; nothing imports ovr_*). Headless: the VrApi bridge's 30 s
|
||||
head-pose deadline ends VR mode without a worn headset (not a game bug).
|
||||
**Lepton storage (2026-09-30):** each app's /sdcard (= /storage/emulated/0 → `<base>/lepton-data/external`) has `Movies`/`Download`/`Documents` symlinked to the Frame's `~/Videos`/`~/Downloads`/`~/Documents` (liblepton/mounting.sh, only if they exist at start); agent v24 `storage_targets` reads that mapping. Android's MediaProvider canonicalises paths to /home/steamos/... and rejects every file ("doesn't appear under [/system/media...]"), `sm list-volumes` is empty: the media index never works, apps must browse folders. Lepton installs with `adb install -g` (runtime permissions granted, MANAGE_EXTERNAL_STORAGE too). Files: `install/files.py`, `frameport frame send|storage`, GUI Files tab (formerly Frame → Send files).
|
||||
**SteamVR per-app settings (2026-09-30):** editing steamvr.vrsettings while SteamVR runs is lost; the web API (127.0.0.1:27062 /app/setsettings) needs `x-steamvr-secret`. `native/vrsettings` = `fp_vrsettings.exe` (freestanding, OpenVR `FnTable:IVRSettings_003` as a Utility app, loads SteamVR's bin/win64/openvr_api.dll) sets them live and SteamVR persists them: section `steam.app.<shortcut appid>`, keys `preferredRefreshRate` (float) and `motionSmoothingOverride` (0 global, 1 on, 2 off, 3 always). Steam Link (vrlink) lists the Frame's rates 72/80/90/96/108/120/144 in vrserver.txt and follows the per-app preference ("host preferred N Hz"; whether the key is honoured is unverified in-headset yet). Judder metric: vrcompositor.txt session summary dropped + "Timed out. N total" (Stormland: 0 dropped but 313 timeouts in 2 min); fpsVR (`%LOCALAPPDATA%\fpsVR\*.json`, 0.1 ms histograms) gives p99 CPU/GPU ms. `pcvr.steamvr_tuning` (default on, PC only) applies on Play: highest rate whose budget ≥ p99×1.05, at least one step down, smoothing on.
|
||||
|
||||
**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.
|
||||
|
||||
**Install links / FrameDrop button protocol (2026-10-07, not yet clicked end to end from a browser):** FrameDrop
|
||||
(framedropvr.com, a closed-source Windows sideloader) defines "Install with FrameDrop" buttons:
|
||||
`https://framedropvr.com/install?manifest=<url>|url=<file>` → that page opens `framedrop://install?…` (1.6 s, else its
|
||||
home page); manifest `{"schema":"framedrop.install/v1","name","files":[{"url","sha256"}]}` (name = Steam title; .apk
|
||||
or Linux .zip). `deeplink.py` (no Flet) parses framedrop://, frameport:// and the pasted https link, enforces its
|
||||
rules (https; http only on loopback; no credentials; no LAN/loopback/link-local IPs, also after DNS and redirects; URL
|
||||
ends in a file name), caps manifests at 256 KiB, ignores non-hex sha256 (FrameDrop's own example has a placeholder),
|
||||
downloads into `<data>/downloads/<slug>-<hash>/` (OBBs → `obb/` next to the APK, `.part` removed on cancel/mismatch);
|
||||
`pipeline.add_from_link` routes APK / Linux / exe and sets `title` + `title_locked` + `link` (a bare file link keeps
|
||||
FramePort's title). GUI: `views/link_dialog.py` (always asks first), Add games → "Install from a link…", CLI
|
||||
`frameport open-link [--yes --no-install --gui]`. **A `flet build` bundle can't take a URL argument** (the Flutter
|
||||
host treats any argv as a developer page URL), so `urlhandler.py` registers a script, not FramePort.exe: Windows
|
||||
HKCU `Software\Classes\{framedrop,frameport}` → hidden PowerShell `frameport-link-handler.ps1`; WSL (source runs)
|
||||
the same keys → `wsl.exe -d <distro> -e sh frameport-link-handler.sh`; Linux `frameport-links.desktop` +
|
||||
`xdg-mime`; macOS unsupported (Apple Events, paste instead). The script drops the link into `<data>/links/*.link`
|
||||
and starts FramePort unless `<data>/gui.alive` is < 10 s old (`gui.starting` stops double starts); the GUI's
|
||||
`_watch_links` thread touches the heartbeat and opens links (newest window session). Settings → Install links: one
|
||||
switch per scheme (`links.framedrop`, `links.frameport`, default on); a scheme another program owns (FrameDrop) is
|
||||
only taken with "Use FramePort for these links" (`register([s], force=True)`); off removes only FramePort's own
|
||||
registration (`MARK` in the command). Never registered with FRAMEPORT_HOME/FRAMEPORT_NO_LINK_HANDLER (tests,
|
||||
screenshots). Same round: files dropped on the Library (`ui/dropped.py`, bundles only like the Files tab), "Add a
|
||||
Windows program (.exe)…" (`pipeline.add_windows_exe`: exe in Downloads/home/drive root copied alone into
|
||||
`<data>/windows-apps/<slug>/`), patch `device.display_mode` (Automatic / VR / Flat window → `InstallContext.display`,
|
||||
`installer.show_window`). FramePort-only manifest extension `"frameport": {"description", "icon"}` (bad values ignored; icon: same URL rules,
|
||||
≤2 MiB, Pillow-checked, ≥32 px, saved as PNG in `<data>/downloads/icons/`, shown in the question by asset URL, then
|
||||
`sources.apply_custom(pkg, "icon")` unless `.picked` exists; description fills `details.description` only when
|
||||
empty). Bare file links get a title guessed from the file name (`title_from_filename`: version/arch dropped, package
|
||||
names → last part). Windows test of 0.12.1.dev191 (2026-10-07): a button click opened the dialog; but a click right
|
||||
after closing FramePort did nothing (the closed window's heartbeat was < 10 s old) → the handler scripts now wait
|
||||
up to 4 s for the link file to be taken before trusting the heartbeat, the window's CLOSE event / atexit delete
|
||||
`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`.
|
||||
Public repo `github.com/spoopyghosty0/frameport` (branch `main`). Push a `v*` tag → CI (`.github/workflows/build.yml`)
|
||||
tests, builds Windows x64 / macOS arm64 / Linux x64 bundles, signs, attests and publishes a GitHub Release
|
||||
(`FramePort-*.zip/.tar.gz`, `SHA256SUMS.txt`, `FramePort-selfsigned.cer`, notes from `docs/INSTALL.md`). v0.1.0 exists.
|
||||
(`FramePort-*.zip/.tar.gz`, the CLI wheel `frameport-<ver>-py3-none-any.whl`, `SHA256SUMS.txt`,
|
||||
`FramePort-selfsigned.cer`; notes = "What's new" from the annotated tag message + the short `packaging/release-footer.md`; owner: keep release
|
||||
notes short — a few "What's new" bullets, nothing long after them).
|
||||
Installed apps find the release themselves (self-update), so the notes are what users see in the update dialog.
|
||||
- **Dev builds (for testers, no release):** Actions → build → Run workflow (main), "dev" ticked, optional "notes"
|
||||
(what to test). The bundles get version `<next patch>.dev<run number>` (`scripts/dev_version.py`, not committed) and
|
||||
replace the rolling `dev` pre-release (`dev-release` job; same assets + SHA256SUMS). Automatic update checks ignore
|
||||
pre-releases; testers use Settings → Updates → "Install the latest dev build…" (`updates.check_dev`,
|
||||
`Updater.install_dev`). `parse_version` sorts 0.9.0 < 0.9.1.devN < 0.9.1, so testers get the next release normally.
|
||||
- **Upstream trackers:** issues labelled `upstream` (#73 microphone, #74 haptics, #75 VrApi bridge) list our workarounds for OVRPort bugs and how to drop them; check them against OVRPort's latest release before each release.
|
||||
- **Release checklist:** bump `src/frameport/_version.py` (the only version; `scripts/package.py` fails a tag build
|
||||
whose tag ≠ `v<_version>`), commit, `git tag -a vX.Y.Z -m "FramePort X.Y.Z" -m "<What's new, Markdown bullets>"`,
|
||||
push the commit and the tag. Never publish a release without its `SHA256SUMS.txt` (the updater refuses it) and keep
|
||||
the asset names (`updates.ASSETS`) — renaming them breaks updating for every installed copy.
|
||||
- Signing is **free/self-signed by the owner's choice** (no paid certs, no SignPath): Windows binaries are signed with
|
||||
a self-signed "FramePort (self-signed)" code-signing cert (RSA 3072, valid to 2031, SHA-256
|
||||
`4E:12:98:91:62:C0:E4:50:FB:65:1D:34:BB:73:00:09:7B:78:BE:88:5C:A7:6C:42:23:46:9B:92:A1:59:A7:6E`); secrets
|
||||
`WINDOWS_CODESIGN_PFX` (base64) + `WINDOWS_CODESIGN_PASSWORD`. Private key: `~/.config/frameport-signing/` (WSL) and
|
||||
the backup `PATCHED/_signing-keys/frameport-app-codesign/` — never commit it. macOS is ad-hoc signed only (Gatekeeper
|
||||
`WINDOWS_CODESIGN_PFX` (base64) + `WINDOWS_CODESIGN_PASSWORD`. The private key is never committed (its
|
||||
location is in `CLAUDE.local.md`). macOS is ad-hoc signed only (Gatekeeper
|
||||
needs right-click → Open; notarization would need the paid Apple program). Users still see SmartScreen unless they
|
||||
import the .cer into Trusted Root.
|
||||
- CI gotchas: `flet build` needs `--yes --no-rich-output` (it prompts to install Flutter; rich output crashes the
|
||||
Windows console) plus PYTHONUTF8; macOS builds need `--python-version 3.12 --arch arm64` (cryptography has no wheels
|
||||
for flet's default Python / x86_64 cross-build), with a PyInstaller fallback step; `astral-sh/setup-uv` has no
|
||||
floating major tags after v7 → pin the exact version; force-moving a tag starts duplicate runs (cancel one).
|
||||
- `gh` is a local install at `~/.local/bin/gh` (logged in as spoopyghosty0 with `workflow` scope; token in
|
||||
`~/.config/gh/hosts.yml`). Commit as `spoopyghosty0 <336754034+spoopyghosty0@users.noreply.github.com>` (set in the repo config).
|
||||
- `jni` (2026-10-06): jni_flutter 1.0.4/1.0.4+1 generate bindings that require jni ^1.1.0 while Flet's build template
|
||||
pins jni 1.0.0 → every `flet build` bundle failed (`JniVersionCheck`, "generated bindings expect package:jni
|
||||
^1.1.0"). pyproject `[tool.flet.flutter.pubspec.dependency_overrides] jni = "1.1.0"` fixes it (pinning
|
||||
jni_flutter 1.0.4 does not); drop it once Flet's template moves to jni 1.1. A failed tag build publishes nothing:
|
||||
delete and re-push the tag on the fixed commit.
|
||||
- macOS runner (2026-10-02): `macos-latest` jobs went unassigned (cancelled after 15 min, no steps); `macos-15`'s Xcode
|
||||
16.4 fails a Flutter plugin (`NWPath has no member`); `macos-26` (Xcode 26) builds with `flet build`. The PyInstaller
|
||||
fallback (`package.py --pyinstaller`) passes `--yes` so a failed `flet build`'s folder doesn't stop it at a prompt.
|
||||
- Windows PowerShell calls from Python (`updates._powershell()`): use `%SystemRoot%\System32\WindowsPowerShell\v1.0\
|
||||
powershell.exe` with `PSModulePath` removed from the environment — started under PowerShell 7 (CI's default shell,
|
||||
or a user's pwsh terminal) it couldn't run Get-AuthenticodeSignature (v0.3.0's Windows update smoke failed on it).
|
||||
`v0.3.0` is a tag without a release (that failed build); the updater shipped first in v0.3.1.
|
||||
- **Never start PowerShell with `DETACHED_PROCESS` from the packaged app**: it exits 0 without running anything (found
|
||||
in the 0.3.1→0.3.2 end-to-end test: the app quit, nothing updated). `updates.spawn_hidden` = CREATE_NEW_CONSOLE +
|
||||
hidden window; `apply()` waits until the script has logged that it runs and raises otherwise (the app then stays
|
||||
open). Windows installs of 0.3.1/0.3.2 can't update themselves: they need one manual download.
|
||||
- The Linux bundle is built on ubuntu-22.04: a 24.04-built Flutter bundle needs GLib 2.80 (`undefined symbol:
|
||||
g_once_init_enter_pointer` on 22.04).
|
||||
- **The repo is public: never commit personal data** — the Frame's IP address, the Steam user id, the owner's email,
|
||||
local home paths (native builds use `-ffile-prefix-map`). History was rewritten once to remove them.
|
||||
|
||||
@@ -273,13 +973,16 @@ GLAD/GLES); Unreal → alternate no-ForceQuit build. Score changes with
|
||||
- Reinstalls keep one rollback copy (`<base>/previous-game.apk`, `settings.conf.previous`); `frameport frame cleanup`
|
||||
removes them (and `--path ~/X` extra folders under home).
|
||||
|
||||
## Project status (2026-09-29)
|
||||
## Project status (2026-10-02)
|
||||
Self-update, the Files tab, OVRPort 1.2.5, non-Quest Android apps (vr_kind) and GUI localisation (tr(), 0.3.x) are
|
||||
in; see the sections above. Earlier state (2026-09-29):
|
||||
34 Quest games ported; the owner confirmed in the headset that all FramePort-rebuilt games work: 23 work, 5 work with
|
||||
issues (Arcsmith/Time Stall eye distortion, AC Nexus some flipped launch text, Phantom DLC button, Silhouette hands),
|
||||
6 can't run (Sniper Elite VR, Espire 1, HITMAN, and the 32-bit Journey of the Gods / Shadow Point / Sports Scramble).
|
||||
Parity: all 34 rebuilt from the dumps match the known-good builds (`docs/parity-report.md`) and were reinstalled +
|
||||
Parity: all 34 rebuilt from the dumps match the known-good builds (`docs/parity-report.md`, generated locally and
|
||||
git-ignored; 34/34 again with OVRPort 1.2.5 on 2026-10-02) and were reinstalled +
|
||||
launch-tested with 0 regressions (`docs/parity-device-report.md`). `PATCHED/` holds exactly the installed builds.
|
||||
Owner preferences: manual installs (no FrameDrop, no Quest2Frame app), Python + Flet, dynamic data over hardcoding,
|
||||
Owner preferences: manual installs (no third-party installer apps), Python + Flet, dynamic data over hardcoding,
|
||||
free tooling only, public repo scrubbed of personal data, keep the known-good backups.
|
||||
Rift/PC VR support (2026-09-29): implemented + unit-tested, not yet tried with a real Rift game on the PC or Frame
|
||||
(needs a Rift dump, and Proton installed on the Frame). Open ideas: exe-icon artwork for Rift games, macOS x86_64 bundle, USB-cable connection (Frame `usb0`, untested),
|
||||
@@ -290,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>`.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Code of conduct
|
||||
|
||||
This project follows the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
|
||||
In short: be respectful and constructive, assume good faith, and keep discussions about the project. Harassment and
|
||||
personal attacks aren't tolerated.
|
||||
|
||||
Report unacceptable behaviour privately through a GitHub security advisory on this repository (it reaches the
|
||||
maintainer only). Reports are handled confidentially.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for helping. The quickest contributions:
|
||||
|
||||
- **A game works on the Frame?** In FramePort: the game's menu → **Share working config…** opens a prefilled issue
|
||||
with its recipe. Accepted configs become built-in recipes.
|
||||
- **Something doesn't work?** The game's menu → **Report a problem…** saves a diagnostics zip (personal data
|
||||
removed) and opens a prefilled issue to attach it to.
|
||||
|
||||
## Development
|
||||
```
|
||||
uv sync --extra dev # Python 3.11+, uv (https://docs.astral.sh/uv/)
|
||||
uv run frameport-gui # the app
|
||||
uv run frameport --help # the command line
|
||||
uv run pytest # tests (no headset or game files needed)
|
||||
uv run ruff check . # lint
|
||||
```
|
||||
- Code and architecture: `CLAUDE.md`, `docs/ARCHITECTURE.md`; per-game symptoms and fixes: `docs/PLAYBOOK.md`.
|
||||
- Interface wording: `docs/STYLE.md`. GUI texts go through `tr()`; after changing them run
|
||||
`python scripts/i18n_extract.py`.
|
||||
- Translations: see [Translating FramePort](#translating-frameport) below.
|
||||
- New fixes are patch modules (`src/frameport/patches/`), with a triage signature (`catalog/triage.yaml`), a PLAYBOOK
|
||||
row and a unit test.
|
||||
- Native binaries in `artifacts/` are rebuilt with `python native/build.py` (downloads the NDK).
|
||||
|
||||
## Adding a tested game
|
||||
|
||||
Tested a game on the Steam Frame? The easiest way is in FramePort: open the game → **…** → **Share working config…**.
|
||||
It opens a prefilled GitHub issue with the exact recipe; after review a workflow turns it into
|
||||
`catalog/games/<package>.yaml`. Without the app: [working config form](https://github.com/spoopyghosty0/frameport/issues/new?template=working-config.yml). Something doesn't run: the game's
|
||||
**Report a problem…** (diagnostics zip, personal data removed) or the [problem report form](https://github.com/spoopyghosty0/frameport/issues/new?template=bug-report.yml).
|
||||
After catalog changes, regenerate the games list: `python scripts/compat_list.py`.
|
||||
|
||||
## Translating FramePort
|
||||
The app's texts (screens, help, patch and setting descriptions) can be translated; the command line, logs and
|
||||
diagnostics stay English. No programming needed:
|
||||
|
||||
1. Copy `src/frameport/locales/template.json` to `src/frameport/locales/<code>.json`, where `<code>` is the language
|
||||
code (`de`, `fr`, `pt`, `ja`, …).
|
||||
2. Add `"_language": "<the language's own name>"` (e.g. `"Deutsch"`); it's shown in Settings → Language.
|
||||
3. Fill in the values. Each key is the English text; leave a value empty to keep the English text. Plural entries
|
||||
have a list value: `["<one>", "<other>"]`. Keep `{placeholders}` such as `{title}` or `{n}` exactly as they are,
|
||||
and keep `…`, `·` and line breaks where the English has them.
|
||||
4. Try it: `uv run frameport-gui`, choose the language in Settings → Language, restart FramePort.
|
||||
5. Open a pull request with the file. When the English texts change, `python scripts/i18n_extract.py` refreshes the
|
||||
template; new texts then show in English until they're translated.
|
||||
|
||||
Wording rules for English (and a guide for tone) are in `docs/STYLE.md`.
|
||||
|
||||
## Ground rules
|
||||
- Use only games you own. Contributions that help bypass DRM, licence or entitlement checks, or that add game files,
|
||||
are not accepted.
|
||||
- Never commit personal data (IP addresses, user names, home paths, Steam IDs).
|
||||
- By contributing you agree to license your work under GPL-3.0-only.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -1,36 +1,147 @@
|
||||
# FramePort
|
||||
|
||||
Play your Meta Quest standalone games on the **Valve Steam Frame**. FramePort patches a Quest APK so it runs in the
|
||||
Frame's Lepton Android runtime, validates the result, installs it over Wi-Fi and adds it to your Steam library with
|
||||
artwork. Think Morphe/ReVanced Manager, for Quest → Steam Frame:
|
||||
[](https://github.com/spoopyghosty0/frameport/releases/latest)
|
||||
[](https://github.com/spoopyghosty0/frameport/actions/workflows/build.yml)
|
||||
[](https://github.com/spoopyghosty0/frameport/releases)
|
||||
[](LICENSE)
|
||||

|
||||

|
||||
|
||||
1. **Connect your Frame** once: the app shows one command to paste into the Frame's desktop terminal (it turns on SSH,
|
||||
trusts the app, and installs Lepton). After that the app finds the Frame on your network by itself.
|
||||
2. **Add games**: scan a folder of Quest dumps (APK + OBB) or add an APK.
|
||||
3. **Review patches**: every game gets suggested patches (overport's plus Steam Frame fixes), each with a reason.
|
||||
34 games have verified recipes; unknown games get heuristics.
|
||||
4. **Patch, validate, install**: signature/alignment/dependency checks, upload (resumable, skips data already on the
|
||||
Frame), Steam shortcut + artwork, and a headless launch test with log triage that suggests fixes.
|
||||
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 downloads and manages its own tools (Java runtime, [overport](https://github.com/ovrport/app) CLI,
|
||||
apksigner). Nothing is installed system-wide, and no Android SDK/NDK is needed.
|
||||
[](docs/media/frameport-tour.mp4)
|
||||
|
||||
## Install
|
||||
- **App bundles**: download from [Releases](https://github.com/spoopyghosty0/frameport/releases) (Windows x64, macOS
|
||||
Apple Silicon, Linux x64); see `docs/INSTALL.md` for the first-launch warning (self-signed / ad-hoc signed) and
|
||||
how to verify a download. Built by CI (`.github/workflows/build.yml`, `flet build`).
|
||||
- **From source**: `uv sync && uv run frameport-gui` (CLI: `uv run frameport --help`).
|
||||
▶ [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)
|
||||
|
||||
## Status of tested games
|
||||
See `catalog/games/` (and the Library screen): 23 work, 5 work with issues, 6 can't run on the Frame (3 are 32-bit
|
||||
only; PC VR alternatives are listed). Details and how problems were solved: `docs/PLAYBOOK.md`.
|
||||
All 34 recipes were verified by rebuilding from the original dumps (`docs/parity-report.md`: 34/34 match the
|
||||
hand-made known-good builds) and reinstalling + launch-testing on a Frame (`docs/parity-device-report.md`: 0 regressions).
|
||||
> **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.
|
||||
|
||||
## Legal
|
||||
Use only with games you own. FramePort does not download games. It is GPL-3.0-only because it bundles code derived
|
||||
from GPL-3.0 projects (Quest2Frame's hook technique in the adapter, the Android-XR-Bridge/OVRPort VrApi bridge).
|
||||
overport is downloaded from its official releases and is not redistributed.
|
||||
## Features
|
||||
|
||||

|
||||
|
||||
- **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.
|
||||
|
||||
Details for each: [Install and first steps](docs/INSTALL.md).
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||
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).
|
||||
|
||||
## Compared with other tools
|
||||
|
||||
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: <title>"; 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: [Architecture](docs/ARCHITECTURE.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| [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
|
||||
See `CLAUDE.md` (architecture, commands, findings) and `docs/ARCHITECTURE.md`.
|
||||
|
||||
```
|
||||
uv sync --extra dev
|
||||
uv run frameport-gui # the app
|
||||
uv run frameport --help # command line
|
||||
uv run pytest # tests
|
||||
```
|
||||
|
||||
## AI usage
|
||||
|
||||
I don't have much free time for this project, so I use AI tools to debug compatibility problems faster.
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0-only (includes GPL-3.0 code from OVRPort). Not affiliated with Valve or Meta.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Security
|
||||
|
||||
FramePort talks to your Steam Frame over SSH with its own key and runs a small agent there; it also downloads tools
|
||||
and its own updates (checked against the release's SHA-256 sums and, on Windows, its signature).
|
||||
|
||||
To report a vulnerability, please open a GitHub security advisory for this repository (Security → Report a
|
||||
vulnerability) instead of a public issue. Include the FramePort version (`frameport --version`) and how to reproduce
|
||||
it. Fixes are released as normal updates; FramePort offers them automatically.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Third-party notices
|
||||
|
||||
FramePort is GPL-3.0-only (see `LICENSE`). It includes or downloads the following third-party work.
|
||||
|
||||
## Included in this repository / the release bundles
|
||||
| Component | Where | License |
|
||||
|---|---|---|
|
||||
| OVRPort VrApi → OpenXR adapter (`native/vrapi`, github.com/Android-XR-Bridge/OVRPort), with FramePort's changes | `native/vrapi-bridge/`, `artifacts/arm64-v8a/libvrapi.so` | GPL-3.0 (`native/vrapi-bridge/LICENSE.upstream`) |
|
||||
| OVRPort platform compatibility library (`native/platform`) | `native/platformcompat/`, `artifacts/arm64-v8a/libovrplatformcompat.so` | GPL-3.0; Meta Platform SDK headers under the Meta Platform Technologies SDK License (`native/platformcompat/licenses/OCULUS-PLATFORM-SDK.txt`) |
|
||||
| Khronos OpenXR SDK headers | used to build the native layers | Apache-2.0 / MIT (`native/vrapi-bridge/licenses/OPENXR-SDK.txt`) |
|
||||
| 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.
|
||||
|
||||
## Downloaded at run time (not redistributed)
|
||||
| Tool | Source | License |
|
||||
|---|---|---|
|
||||
| OVRPort / overport CLI | github.com/Android-XR-Bridge/OVRPort releases (fallback github.com/ovrport/app) | GPL-3.0 |
|
||||
| Eclipse Temurin JRE | api.adoptium.net | GPL-2.0 with Classpath Exception |
|
||||
| apksigner (Android build-tools) | dl.google.com Android repository | Apache-2.0 |
|
||||
| Revive (portable copy, only if Revive isn't installed) | github.com/LibreVR/Revive releases | GPL-3.0 |
|
||||
|
||||
Valve's Lepton, Proton and SteamVR, and Meta's software, are used on your devices as installed by Steam / Meta;
|
||||
FramePort doesn't distribute them.
|
||||
@@ -1,9 +1,24 @@
|
||||
3dfcb0f2bec47857bcecc7261c58e076d872d4649718c40850f29e10c75477dd ./arm64-v8a/libglshim.so
|
||||
ca27ffa2122def910697ddcaff64f25cfdbc63e2017c738155866ff2fcdb5e12 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
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
|
||||
e8c0965c188665a4e565e4a3af983ef1866d1cf9225665cc90d8df286bc96ef3 ./arm64-v8a/libfp_vk.so
|
||||
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
|
||||
5432674a59d02f411cd853a5dc57e3fa547d1ac518f25bf433fc54c43677177a ./arm64-v8a/libfpglmv.so
|
||||
8a8f6b1da8952cb3933a5b53558424a5fc56043b2527a16bb6dd62a00d2de01a ./arm64-v8a/libframe_xrshim.so
|
||||
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
|
||||
449e13bbe58de6718d4e530177924e09552a95d748a082739899144e2cb3ccf3 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
|
||||
32525cd0a9ee3d9993d8871cd6905bdcf22cde4f4ba28f5db6434ce7d6554758 ./arm64-v8a/libvrapi.so
|
||||
f3aa9419155b8970617fc0d68527a73c39abc82303214c159194c439bb776fd6 ./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
|
||||
c2c70f4af1a5c3e6089dff1130161dd417f8044f99ec639eeecf1083930aab16 ./win-x64/fp_oculushmd.exe
|
||||
60ebc5fea05b8945082e2ca8b9f2eb74616012a26eb50133af273dff0fcddc42 ./win-x64/fp_vrsettings.exe
|
||||