mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-14 05:02:22 +02:00
Compare commits
13
Commits
main
...
rift-on-frame
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc6a8ba25a | ||
|
|
179a62041b | ||
|
|
cbd737f7a9 | ||
|
|
9e1ef3bfae | ||
|
|
940a2c15cb | ||
|
|
7daf14b795 | ||
|
|
26db4468ec | ||
|
|
cff8867f53 | ||
|
|
cb62dacd01 | ||
|
|
08f23f236c | ||
|
|
136656b142 | ||
|
|
82a14c3bcf | ||
|
|
11743e4b2c |
No files matched your search
@@ -470,6 +470,61 @@ maps a symlink to the already-loaded module) gets past it to **-3021 = ovrError_
|
||||
an Oculus-signed runtime. Next step (not done): make the runtime check pass without Detours, e.g. an injected helper
|
||||
that patches the game's import table (IAT) for LoadLibrary*/WinVerifyTrust, or a prefix-level override. This is
|
||||
runtime interop (what Revive does), not the Platform SDK licence check (which FramePort never touches).
|
||||
**Rift via x86_64 Wine under FEX (research, 2026-10-08, headless on the dev Frame, SteamOS 0.4.5; no product code):**
|
||||
why Revive failed is confirmed upstream: x64 Detours returns ERROR_NOT_SUPPORTED on ARM64EC targets (microsoft/Detours
|
||||
PR #388; only an ARM64EC build of Detours can hook them). A fully x86_64 Wine avoids it: GE-Proton11-7 **x86_64** runs
|
||||
under the FEX tool (`fex-compat-tool run -- <x86 binary>`; RootFS /usr/share/guestos/fex-mesa, no FreeType there)
|
||||
— prefix boot 35 s, kernelbase.dll is x86-64. Proton's script needs help without the x86_64 SLR (app 4183110, not
|
||||
installed): its Python runs natively (the RootFS has none), so `files/bin/wine{,server}` and the two direct
|
||||
`wine-preloader` argv calls must go through a FEX wrapper that also unsets `FEX_APP_CONFIG(_LOCATION)` (Proton sets
|
||||
them on aarch64 for its own ARM64EC FEX → "RootFS path set to ''"); GE's game drive needs STEAM_COMPAT_INSTALL_PATH
|
||||
unset. SteamVR now ships `bin/linux64/vrclient.so` + `bin/vrclient.so` (i386) + `vrclient_x64.dll` (Oct 6 build; not
|
||||
there on 09-30): Proton's x86_64 vrclient bridge reaches the arm64 vrserver. **Rick and Morty ran as the scene app**
|
||||
("OpenVR initialized!", controller tracking, 9176 presents / 6 dropped, CPU 11.6 / GPU 17.9 ms, target 72).
|
||||
**ReviveInjector: "Succesfully injected!"**, LibRevive64.dll loaded next to OVRPlugin.dll in Lies Beneath, which then
|
||||
died on 0xc06d007e = delay-loaded `libovrplatform64_1.dll` (Platform SDK) before connecting to SteamVR — so Revive's
|
||||
signature hook and Revive→SteamVR are still unproven. Every Rift game in the library except Rick and Morty uses the
|
||||
Platform SDK → the Meta (Link) app in the prefix, logged in with the user's own account, is the real blocker; the
|
||||
only public attempt (github.com/michauMiau/oculus-wine-linux, 2026-10) never got OculusSetup.exe (32-bit .NET)
|
||||
past its HTTPS config fetch. Scratch files on the Frame: `~/frameport-rifttest/` (scripts t1-t4.sh).
|
||||
Meta runtime in the same Wine (2026-10-09): Platform SDK games reach OVRServer's IPC on the Frame. The apparent
|
||||
intermittent handshake hang was **OVRLibrarian.exe** (.NET, Wine Mono): under FEX it finishes its work but never exits,
|
||||
its AppTracker entry stays, and the runtime then tracks no later client (Meta app, game), so the game waits forever
|
||||
for the reply event the runtime only sets after identifying it (found with wineserver `+server` traces PC vs Frame).
|
||||
Ending OVRLibrarian before the game → game tracked, "Missing entitlement" exactly as on the PC (needs a login).
|
||||
Logged in (2026-10-09, owner's own account, browser sign-in; the `oculus://` answer had to be forwarded into the Wine
|
||||
app's `\\.\pipe\oculus`), the runtime also needed a WinRT `Windows.Devices.WiFi.WiFiAdapter` stand-in (Air Link code
|
||||
fail-fasts without it), Wine's DeviceWatcher add/remove_Updated/Removed implemented, and the C: volume name
|
||||
(`\\?\Volume{...0043}`) linked in dosdevices. Copying the logged-in `sessions/` + CoreData to the Frame prefix kept the
|
||||
login. **Oculus First Contact then ran in VR on the Frame** (Revive in OpenVR mode; `/openxr` failed in wineopenxr):
|
||||
scene app at 72 Hz target, but 92 % of frames "timed out" (game CPU-bound under FEX) → stutter/flicker/lag.
|
||||
Performance (2026-10-10, headless with patched Revive visibility + SteamVR `pauseCompositorOnStandby=false`, numbers from
|
||||
`IVRCompositor::GetFrameTimings`): the frame loop waited on sync round trips through the FEX-run x86_64 wineserver.
|
||||
`native/fexwine/`: a **native aarch64 wineserver** built from GE's exact source (protocol 938) + **ntsync** (works once
|
||||
OVRLibrarian is ended) took hitches 17 → 0-4 per 20 s. The rest were Unreal's large-block allocator committing,
|
||||
filling (one `rep stos`) and freeing 514 MB every 1-8 s: ~130k 4 KB page faults each under FEX. `fp_mem.so`
|
||||
(`FP_BIGCACHE_MB=256`, guest preload via `FEX_ENV=LD_PRELOAD=…`: fex-compat-tool deletes LD_PRELOAD) keeps that block
|
||||
→ 0 hitches, steady 36 fps + motion smoothing, ~5.5 % timed out. 72 fps would need ~30 % less GPU (game ~55 % busy
|
||||
at 36 fps); THP (fragmentation), MSAA off, Turnip gmem don't help. Also needed: Unreal's
|
||||
`Slate.DeferWindowsMessageProcessing` = 0 (exe patch; the default deadlocks Wine's IME window ~1/3 of starts).
|
||||
Headset (owner, 2026-10-10): with motion smoothing off for the game (SteamVR per-app `motionSmoothingOverride=2`:
|
||||
smoothing cost ~15 % GPU) "much much better", 95 % of frames new at 72 Hz; MSAA off (DXVK `d3d11.disableMsaa`, -25 %
|
||||
GPU) helped but the owner misses smooth edges → the game's MSAA CVar is `r.MobileMSAA` (Oculus' clustered-forward
|
||||
branch; no r.MSAACount in the exe), 2x untested. Headless tests need the headset worn once per boot (else "IMU
|
||||
fallback" and the game waits at startup forever).
|
||||
**Experimental mode on branch `rift-on-frame` (the owner's single linkable branch for this work):** CLI only,
|
||||
`FRAMEPORT_EXPERIMENTAL_FEX=1 frameport fex setup | import-login <signed-in PC prefix> [--app <meta name>] | install
|
||||
[<meta name>] [--msaa N] [--force-unverified] | status` (hidden group). Generic per game: exe + arguments from Meta's
|
||||
manifest (`Software/Manifests/<app>.json` launchFile/launchParameters), the Unreal IME-deadlock fix found in the binaries
|
||||
(`find_ime_patch`: the CVar's UTF-16 name → its `lea` xref → `lea r8` member/global → `mov [reg+disp],1` / int 1;
|
||||
also modular ApplicationCore/Slate DLLs), `static_checks` (engine, imported graphics DLLs: monolithic Unreal imports
|
||||
every RHI, so it can't tell which runs). `agent/fexrift.py` `GAMES` = verified games + per-game opt-ins only (fp_mem's
|
||||
unzeroed big-block cache, `msaa_ini`); others need `--force-unverified`. PC `src/frameport/fexrift.py` uploads `agent/fexrift.py` + `artifacts/fexrift`
|
||||
(sources native/fexwine) + Revive 3.2.0 with 6 byte patches (refused for other builds); the Frame helper downloads
|
||||
GE-Proton11-7 x86_64 (SHA-512), x86 GnuTLS (Arch archive), installs Meta's runtime (Meta's signature checks kept) and
|
||||
writes the game's launcher. Agent v79 `fex_prepare/status/setup/import_login/install`, kind `frame_fex` (uninstall
|
||||
keeps the shared prefix; launch tests refused). Everything under `~/.local/share/frameport/fexrift` (purge removes it,
|
||||
login tokens included). Not yet run end to end on the device; the login still comes from a PC prefix signed in by hand.
|
||||
Uninstall (Quest, saves kept) used to leave `deployment.json` → still "installed"; fixed (agent v16).
|
||||
|
||||
**Discovery/network:** Developer-Mode SteamOS devices announce `_steamos-devkit._tcp` (TXT `login=steamos`) — use it;
|
||||
|
||||
+1217
File diff suppressed because it is too large.
Load diff
+118
-4
@@ -44,7 +44,7 @@ try:
|
||||
except ImportError: # Windows: pc_revive loads this file for its VDF code only (GitHub #131)
|
||||
fcntl = None
|
||||
|
||||
AGENT_VERSION = 78
|
||||
AGENT_VERSION = 79
|
||||
HOME = os.path.expanduser("~")
|
||||
STEAM = os.path.join(HOME, ".local/share/Steam")
|
||||
ANCHORS = os.path.join(HOME, "Applications/quest-frame")
|
||||
@@ -1102,7 +1102,7 @@ def shortcut_args(pkg):
|
||||
icon = app_icon_for(dep, anchor, steam=True)[0] # a Linux app's own icon unless the user chose one
|
||||
flat = dep.get("vr") is False
|
||||
kind = dep.get("kind")
|
||||
tag = ("Windows game on Frame" if flat else "PC VR on Frame") if kind == "pcvr" else \
|
||||
tag = ("Windows game on Frame" if flat else "PC VR on Frame") if kind in ("pcvr", "frame_fex") else \
|
||||
"Linux app on Frame" if kind == "linux" else "Quest on Frame"
|
||||
return f'"{anchor}/launch.sh"', dep["title"], anchor, icon, tag, dep.get("tags") or [], not flat
|
||||
|
||||
@@ -1831,7 +1831,7 @@ def app_media_dirs(ext):
|
||||
|
||||
def lepton_external(pkg):
|
||||
dep = deployment(pkg)
|
||||
if not dep or dep.get("kind") == "pcvr":
|
||||
if not dep or dep.get("kind") in ("pcvr", "frame_fex"):
|
||||
raise AgentError(f"{pkg} is not an installed Quest (Lepton) game")
|
||||
return os.path.join(dep["base"], "lepton-data", "external")
|
||||
|
||||
@@ -5076,8 +5076,11 @@ def cmd_uninstall(args):
|
||||
base = dep["base"]
|
||||
pcvr = dep.get("kind") == "pcvr"
|
||||
linux = dep.get("kind") == "linux"
|
||||
if (pcvr_pids(base) if pcvr or linux else container_running(dep["appid"])):
|
||||
fex = dep.get("kind") == "frame_fex"
|
||||
if (pcvr_pids(base) if pcvr or linux or fex else container_running(dep["appid"])):
|
||||
raise AgentError("the game is running")
|
||||
if fex: # the game lives in the shared Meta runtime prefix (with the user's login): only the launcher goes
|
||||
return fex_uninstall(pkg, dep, args)
|
||||
if is_overlay(dep) or os.path.exists(os.path.join(ANCHORS, pkg, OVERLAY_MANIFEST)):
|
||||
unregister_overlay(pkg) # SteamVR forgets the overlay app (before its manifest's folder goes)
|
||||
keep_data = args.get("keep_data", True)
|
||||
@@ -5427,6 +5430,8 @@ def cmd_stop(args):
|
||||
run(["systemctl", "--user", "stop", f"frameport-test-{dep['appid']}"])
|
||||
if dep.get("kind") == "pcvr":
|
||||
stop_pcvr(dep)
|
||||
elif dep.get("kind") == "frame_fex": # the launcher's EXIT trap stops Wine (wineserver -k)
|
||||
run(["pkill", "-TERM", "-f", re.escape(os.path.join(ANCHORS, dep["package"], "launch.sh"))])
|
||||
elif dep.get("kind") == "linux":
|
||||
for pid in pcvr_pids(dep["base"]):
|
||||
run(["kill", "-TERM", pid])
|
||||
@@ -5539,6 +5544,8 @@ def cmd_launch_test(args):
|
||||
if is_overlay(dep): # nothing to see without a game; register_vr_overlay's SteamVR check is the test
|
||||
return {"state": "SKIPPED", "skipped": "overlay app", "elapsed": 0, "log": None, "log_size": 0,
|
||||
"crash_log": None, "kind": dep.get("kind")}
|
||||
if dep.get("kind") == "frame_fex":
|
||||
raise AgentError("launch tests aren't available for Rift games with Meta's runtime (experimental) yet")
|
||||
if dep.get("kind") == "pcvr":
|
||||
return launch_test_pcvr(dep, anchor, log, seconds)
|
||||
if dep.get("kind") == "linux":
|
||||
@@ -7302,6 +7309,113 @@ def monitor_session(stdin, stdout, monitor=None, sleep=time.sleep, max_ticks=Non
|
||||
return 0
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------ Rift via FEX (experimental)
|
||||
# Oculus Rift games that need Meta's PC runtime, on the Frame alone: x86_64 GE-Proton under FEX + Meta's runtime +
|
||||
# Revive in one shared Wine prefix. The work is done by fexrift.py (uploaded with its binaries by the PC, see
|
||||
# src/frameport/fexrift.py and native/fexwine/README.md); these commands only start it and track games (kind
|
||||
# "frame_fex"; the game's files stay inside the prefix where Meta's app put them).
|
||||
FEX_ROOT = os.path.join(HOME, ".local/share/frameport/fexrift")
|
||||
FEX_ARTIFACTS = os.path.join(FEX_ROOT, "artifacts")
|
||||
FEX_HELPER = os.path.join(FEX_ARTIFACTS, "fexrift.py")
|
||||
|
||||
|
||||
def fex_helper(command, args=None, timeout=900):
|
||||
if not os.path.isfile(FEX_HELPER):
|
||||
raise AgentError("the files for Rift games on the Frame aren't here yet (run the setup first)")
|
||||
p = run([sys.executable, "-I", FEX_HELPER, command, json.dumps(args or {})], timeout=timeout)
|
||||
try:
|
||||
out = json.loads(p.stdout.strip().splitlines()[-1])
|
||||
except (IndexError, ValueError):
|
||||
raise AgentError(f"fexrift {command} failed: {(p.stderr or p.stdout)[-400:]}") from None
|
||||
if not out.get("ok"):
|
||||
raise AgentError(out.get("error") or f"fexrift {command} failed")
|
||||
return out["result"]
|
||||
|
||||
|
||||
def cmd_fex_prepare(args):
|
||||
"""Where the PC uploads fexrift.py + its binaries (artifacts/) and the login archive (incoming/), and what's
|
||||
already there (unchanged files aren't re-sent)."""
|
||||
incoming = os.path.join(FEX_ROOT, "incoming")
|
||||
for d in (FEX_ARTIFACTS, incoming):
|
||||
os.makedirs(d, exist_ok=True)
|
||||
os.chmod(incoming, 0o700)
|
||||
st = os.statvfs(FEX_ROOT)
|
||||
return {"root": FEX_ROOT, "artifacts": FEX_ARTIFACTS, "incoming": incoming,
|
||||
"existing": tree_manifest(FEX_ARTIFACTS), "free_bytes": st.f_bavail * st.f_frsize,
|
||||
"fex": os.path.isdir(os.path.join(STEAM, "steamapps/common/FEX-Emu"))}
|
||||
|
||||
|
||||
def cmd_fex_status(args):
|
||||
if not os.path.isfile(FEX_HELPER):
|
||||
return {"helper": False}
|
||||
res = fex_helper("status", timeout=60)
|
||||
res["helper"] = True
|
||||
return res
|
||||
|
||||
|
||||
def cmd_fex_setup(args):
|
||||
"""Start the setup (GE-Proton download, Meta's runtime from Meta's CDN, the prefix): ~10-20 min, detached;
|
||||
progress via fex_status (setup.state / step / error)."""
|
||||
if not os.path.isfile(FEX_HELPER):
|
||||
raise AgentError("upload the files first (fex_prepare)")
|
||||
steps = [s for s in args.get("steps") or [] if re.fullmatch(r"[a-z]+", str(s))]
|
||||
unit = f"frameport-fexsetup-{int(time.time())}"
|
||||
p = run(["systemd-run", "--user", "--collect", "--quiet", f"--unit={unit}", "--setenv=HOME=" + HOME,
|
||||
sys.executable, "-I", FEX_HELPER, "setup", json.dumps({"steps": steps})])
|
||||
if p.returncode != 0:
|
||||
raise AgentError(f"couldn't start the setup: {p.stderr.strip()[-300:]}")
|
||||
return {"started": True, "unit": unit}
|
||||
|
||||
|
||||
def cmd_fex_import_login(args):
|
||||
"""The login archive the PC uploaded to incoming/login.tar (fexrift.py deletes it afterwards)."""
|
||||
return fex_helper("import_login", {"tar": os.path.join(FEX_ROOT, "incoming", "login.tar")})
|
||||
|
||||
|
||||
def cmd_fex_install(args):
|
||||
"""A game Meta's app downloaded (now in the prefix) as a Steam game: per-game fixes + launch.sh +
|
||||
deployment.json (kind frame_fex). The Steam shortcut is added like for other games (shortcuts)."""
|
||||
pkg = check_pkg(args["package"])
|
||||
app = str(args.get("app") or "")
|
||||
if not re.fullmatch(r"[a-z0-9][a-z0-9_-]+", app):
|
||||
raise AgentError(f"bad app name {app!r}")
|
||||
anchor = os.path.join(ANCHORS, pkg)
|
||||
dep = deployment(pkg)
|
||||
if dep and dep.get("kind") != "frame_fex":
|
||||
raise AgentError(f"{pkg} is already installed in another way")
|
||||
msaa = args.get("msaa")
|
||||
res = fex_helper("install", {"app": app, "anchor": anchor, "msaa": msaa if msaa in (1, 2, 4) else None,
|
||||
"force_unverified": bool(args.get("force_unverified"))})
|
||||
title = str(args.get("title") or res.get("title") or pkg).replace("\n", " ")
|
||||
appid = dep["appid"] if dep else shortcut_appid(f'"{anchor}/launch.sh"', title)
|
||||
dep = {"package": pkg, "kind": "frame_fex", "app": app, "appid": int(appid), "title": title,
|
||||
"base": res["game_dir"], "exe": res["exe"], "tags": args.get("tags") or [], "vr": True,
|
||||
"fixes": {k: res.get(k) for k in ("ime_patch", "msaa")}, "verified": bool(res.get("verified")),
|
||||
"checks": res.get("checks") or {},
|
||||
"agent_version": AGENT_VERSION, "installed": int(time.time())}
|
||||
path = os.path.join(anchor, "deployment.json")
|
||||
with open(path + ".tmp", "w") as f:
|
||||
json.dump(dep, f, indent=1)
|
||||
os.replace(path + ".tmp", path)
|
||||
return {"package": pkg, "appid": dep["appid"], "title": title, "launcher": res["launcher"], "fixes": dep["fixes"],
|
||||
"verified": dep["verified"], "checks": dep["checks"]}
|
||||
|
||||
|
||||
def fex_uninstall(pkg, dep, args):
|
||||
"""Only the launcher, artwork and shortcut go: the game stays in the shared prefix (Meta's app owns it)."""
|
||||
anchor = os.path.join(ANCHORS, pkg)
|
||||
removed_sc = False
|
||||
if args.get("remove_shortcut") and steam_users():
|
||||
removed_sc = cmd_shortcuts({"remove": [{"exe": f'"{anchor}/launch.sh"', "appid": dep.get("appid")}]})["started"]
|
||||
if args.get("remove_shortcut"):
|
||||
try:
|
||||
devkit_unregister(pkg, steam_running=run(["pgrep", "-x", "steam"]).returncode == 0)
|
||||
except Exception: # noqa: BLE001 (the files go anyway at the next purge)
|
||||
pass
|
||||
remove_tree(anchor)
|
||||
return {"removed": True, "kept_saves": True, "shortcut_removed": removed_sc}
|
||||
|
||||
|
||||
COMMANDS = {n[4:]: f for n, f in globals().items() if n.startswith("cmd_")}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
3e9099b5015e8f486c00bcea9d111ee721faba355a89bcf1df69561e3dc6325c ./digicert-assured-id-root-ca.der
|
||||
e04db8044185c1c77941252e61794e84af7156c449fedcda4a826d7a9fc91989 ./dlls/crypt32.dll
|
||||
ec1387fad1f9b65503077e5aa24e3a3b60b3b04b85fd9412e0f2c451772f9b25 ./dlls/dnsapi.dll
|
||||
00ebfab0f3a066d9c85820332e0ec91cf4040797e580516dcafa048dfe758aa7 ./dlls/sechost.dll
|
||||
41584c41db41991f20a0b55011efa94618e0817ab351579f793ca01dbb488a0e ./dlls/windows.devices.enumeration.dll
|
||||
f59f409fba8f668e2316bfc3b636043fd471dd77e76944c2dfd2d4454587270b ./fp_mem.so
|
||||
13d455b700b08f40eb3e3452ab264bf535cb3d176c92ec9a3324a635ce4f8588 ./windows.devices.wifi.dll
|
||||
422f0cd556a6291d87611d7034fef716e3bc996c6f3b80b74a4dd0daca082b13 ./wineserver.arm64
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
# x86_64 Wine under FEX: Rift games with Meta's runtime on the Frame (experimental)
|
||||
|
||||
Sources of `artifacts/fexrift/` (prebuilt, with `SHA256SUMS`) for the experimental mode `frameport fex ...`
|
||||
(FRAMEPORT_EXPERIMENTAL_FEX=1; PC side `src/frameport/fexrift.py`, Frame side `agent/fexrift.py`). Research notes:
|
||||
CLAUDE.md, "Rift via x86_64 Wine under FEX". Rebuild order: `prep_ge_wine.sh` (GE's exact Wine source) ->
|
||||
`build_pe_dlls.sh` (x86_64 host, llvm-mingw) + `build_wineserver.sh` (aarch64 host, e.g. the Frame; then `strip`) +
|
||||
fp_mem.so (command below).
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `prep_ge_wine.sh` | Recreates the exact Wine source of a GE-Proton release (its Wine and wine-staging submodules plus GE's patches) by running only the WINE section of GE's `protonprep-valve-staging.sh`. |
|
||||
| `build_wineserver.sh` | Builds a **native aarch64 wineserver** from that tree on an aarch64 host (the Frame has gcc, make, flex and bison). It speaks the same protocol as GE's x86_64 server (GE-Proton11-7: protocol 938, 321 requests; plain Valve Wine at the same commit lacks 15 of GE's requests). `-DFP_EMULATED_X86_64` gives it the x86_64 machine model, because its clients are x86_64 processes under FEX. Use it by having the `wineserver` wrapper exec it instead of the FEX-run x86_64 server. |
|
||||
| `build_pe_dlls.sh` + `patches/` | The four Wine DLLs FramePort replaces in GE-Proton, built from GE's tree + only these patches: `crypt32-chain-newest-issuer` (Windows' newest-issuer rule between equal chains: Meta's OculusAppFramework signer check needs the 4-certificate chain), `sechost-realtime-trace-consumer` (OpenTraceW real-time sessions: the runtime's ETW USB tracing), `dnsapi-dnsservice-functions` (DnsService* from Wine 11.19: Air Link mDNS, OVRServer quits without them), `devenum-devicewatcher-handlers` (DeviceWatcher Updated/Removed handler lists). Exports equal GE's (+8 DnsService*). Also builds `windows.devices.wifi.dll` from `wifi_stub.c`: a Windows.Devices.WiFi.WiFiAdapter class (GetDeviceSelector matches nothing) that the logged-in runtime needs. |
|
||||
| `fp_mem.c` | x86_64 `LD_PRELOAD` for the guest. `FP_BIGCACHE_MB=<n>` keeps released anonymous regions of at least n MB (up to 12, 6 GB in total) and returns them when the same start address is mapped again; a shorter request releases the rest, a longer one maps only the extra part fresh. In the headset First Contact allocated 0.5-1.6 GB blocks several times at once, which the first, single-block version missed. `FP_THP=1` sets `MADV_HUGEPAGE` on large writable regions. Build it with `gcc -O2 -shared -fPIC -o fp_mem.so fp_mem.c -ldl -lpthread` on an x86_64 host (needs glibc 2.34). **Under FEX, pass it as `FEX_ENV=LD_PRELOAD=<path>`**: Valve's `fex-compat-tool` deletes `LD_PRELOAD`. |
|
||||
|
||||
## Measurements: Oculus First Contact, headless, dev Frame, 2026-10-10
|
||||
|
||||
SteamVR compositor frame timings were read through `IVRCompositor::GetFrameTimings`.
|
||||
|
||||
| Setup | Hitches per 20 s | Frames timed out | Game CPU per frame |
|
||||
|---|---|---|---|
|
||||
| FEX-run x86_64 wineserver, server sync | 17 | 40 % | 10.4 ms |
|
||||
| Native wineserver, server sync | 9 | 32 % | 10.4 ms |
|
||||
| Native wineserver + ntsync | 0-4 | 9-10 % | 6.2 ms |
|
||||
| Native wineserver + ntsync + `FP_BIGCACHE_MB=256` | **0** (3 of 3 runs) | 5.4-5.9 % | 5.3 ms |
|
||||
|
||||
Each remaining hitch (one app frame shown about 9 times) was Unreal's large-block allocator:
|
||||
1. It calls VirtualAlloc for 514 MB (First Contact's exe RVA 0x1a188e) every 1-8 s.
|
||||
2. A game task fills the block with a single `rep stos`/`rep movs` while the game thread waits for it.
|
||||
3. Another thread then frees the block.
|
||||
|
||||
Under FEX every 4 KiB page of it faults in and gets zeroed and charged by the kernel: about 130k faults and 110-140 ms per hitch.
|
||||
|
||||
Huge pages work in isolation (514 MB in 22 ms instead of 212 ms), but fail in practice. On a Frame that has been up for a while, memory is fragmented, so allocations fall back to 4 KiB pages (`thp_fault_fallback` far above `thp_fault_alloc`). The kernel's `defrag=madvise` setting also lets those attempts stall in compaction.
|
||||
|
||||
The block cache returns the region with stale contents instead of zeroed pages. That is only safe for games that overwrite the whole block, so it has to stay a per-game setting.
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
# Build the four patched Wine PE DLLs FramePort adds to GE-Proton for the Meta runtime (see README.md), from GE's
|
||||
# exact Wine source (prep_ge_wine.sh) + patches/*.patch, on an x86_64 Linux host with llvm-mingw on PATH.
|
||||
# Only these DLLs are built: generated headers they don't use (Vulkan thunks) are empty stand-ins, the syscall
|
||||
# tables come from Wine's own tools/make_specfiles.
|
||||
# usage: build_pe_dlls.sh <patched GE wine dir (prep_ge_wine.sh)> <work dir> <out dir>
|
||||
set -e
|
||||
SRC=$(cd "$1" && pwd); W=$2; OUT=$3
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
[ -n "$OUT" ] || { echo "usage: $0 <wine dir> <work dir> <out dir>"; exit 2; }
|
||||
command -v x86_64-w64-mingw32-clang >/dev/null || { echo "llvm-mingw (x86_64-w64-mingw32-clang) must be on PATH"; exit 2; }
|
||||
mkdir -p "$W" "$OUT"; W=$(cd "$W" && pwd); OUT=$(cd "$OUT" && pwd)
|
||||
if [ ! -d "$W/wine" ]; then
|
||||
cp -a "$SRC" "$W/wine"
|
||||
for p in "$HERE"/patches/*.patch; do patch -d "$W/wine" -p1 -s < "$p"; echo "applied $(basename "$p")"; done
|
||||
(cd "$W/wine" && perl tools/make_specfiles >/dev/null)
|
||||
for f in include/wine/vulkan.h dlls/winevulkan/loader_thunks.c dlls/winevulkan/loader_thunks.h \
|
||||
dlls/winevulkan/vulkan_thunks.c dlls/winevulkan/vulkan_thunks.h dlls/winevulkan/winevulkan.json; do
|
||||
[ -e "$W/wine/$f" ] || : > "$W/wine/$f"
|
||||
done
|
||||
fi
|
||||
mkdir -p "$W/obj"
|
||||
if [ ! -f "$W/obj/Makefile" ]; then
|
||||
# no debug info and relative source paths: the DLLs are committed (no local paths may end up in them)
|
||||
(cd "$W/obj" && CROSSCFLAGS="-O2 -ffile-prefix-map=$W/wine=wine -ffile-prefix-map=$W/obj=obj" \
|
||||
"$W/wine/configure" --enable-archs=x86_64 --disable-tests --without-x --without-freetype \
|
||||
--without-wayland --without-gstreamer --without-pulse --without-alsa --without-oss --without-cups --without-dbus \
|
||||
--without-gnutls --without-krb5 --without-netapi --without-opencl --without-pcap --without-sane --without-udev \
|
||||
--without-usb --without-v4l2 --without-vulkan --without-sdl --without-capi --without-gphoto --without-fontconfig \
|
||||
--without-coreaudio --without-opengl --without-inotify --without-unwind --without-ffmpeg > configure.log 2>&1)
|
||||
fi
|
||||
DLLS="crypt32 sechost dnsapi windows.devices.enumeration"
|
||||
cd "$W/obj"
|
||||
make -j"$(nproc)" $(for d in $DLLS; do echo "dlls/$d/x86_64-windows/$d.dll"; done) > make.log 2>&1 || { tail -20 make.log; exit 1; }
|
||||
for d in $DLLS; do cp "dlls/$d/x86_64-windows/$d.dll" "$OUT/"; done
|
||||
# Wi-Fi WinRT stand-in (not a Wine DLL: goes into the prefix's system32 + its activation class registry key)
|
||||
x86_64-w64-mingw32-clang -O2 -shared -s -Wl,--no-insert-timestamp -o "$OUT/windows.devices.wifi.dll" \
|
||||
"$HERE/wifi_stub.c" "$HERE/wifi_stub.def" -lole32 -lwindowsapp
|
||||
ls -la "$OUT"
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# Build a native aarch64 wineserver from a patched GE Wine tree (prep_ge_wine.sh), for x86_64 Wine under FEX.
|
||||
# Run it ON an aarch64 Linux host (e.g. the Frame: gcc, make, flex, bison, autoconf are in SteamOS).
|
||||
# - Wine's configure insists on a PE cross compiler for aarch64 and on generated headers this tree lacks; the server
|
||||
# needs neither, so configure only produces include/config.h here and the server sources are compiled directly.
|
||||
# - -DFP_EMULATED_X86_64: the server takes the x86_64 branch of init_supported_machines (native machine AMD64), as
|
||||
# the clients are x86_64 processes under FEX; otherwise it would advertise ARM64 and refuse/misplace x86_64 images.
|
||||
# The result talks the same protocol as GE's x86_64 wineserver (check: SERVER_PROTOCOL_VERSION and the request list).
|
||||
# usage: build_wineserver.sh <patched wine dir> <build dir> → <build dir>/ws/wineserver
|
||||
set -e
|
||||
SRC=$(cd "$1" && pwd); B=$2
|
||||
[ -n "$B" ] || { echo "usage: $0 <wine dir> <build dir>"; exit 2; }
|
||||
[ "$(uname -m)" = aarch64 ] || { echo "run this on aarch64"; exit 2; }
|
||||
mkdir -p "$B/obj" "$B/ws"; B=$(cd "$B" && pwd)
|
||||
grep -q FP_EMULATED_X86_64 "$SRC/server/registry.c" || \
|
||||
sed -i 's/^#elif defined(__x86_64__)$/#elif defined(__x86_64__) || (defined(__aarch64__) \&\& defined(FP_EMULATED_X86_64))/' "$SRC/server/registry.c"
|
||||
grep -q FP_EMULATED_X86_64 "$SRC/server/registry.c" || { echo "registry.c layout changed"; exit 1; }
|
||||
if [ ! -f "$B/obj/include/config.h" ]; then
|
||||
cp "$SRC/configure" "$B/configure.fp"
|
||||
sed -i 's/as_fn_error $? "PE cross-compilation is required/echo "(ignored) PE cross-compilation is required/' "$B/configure.fp"
|
||||
# configure.fp must run from the source dir's point of view: copy it in, run, remove
|
||||
cp "$B/configure.fp" "$SRC/configure.fp"
|
||||
(cd "$B/obj" && "$SRC/configure.fp" --srcdir="$SRC" --without-mingw --without-x --without-wayland --without-freetype \
|
||||
--without-gstreamer --without-vulkan --without-opengl --without-pulse --without-alsa --without-sdl --without-udev \
|
||||
--without-usb --without-gnutls --without-cups --without-dbus --without-krb5 --without-netapi --without-pcap \
|
||||
--without-sane --without-v4l2 --without-gphoto --without-unwind --without-ffmpeg --disable-tests > configure.log 2>&1 || true)
|
||||
rm -f "$SRC/configure.fp"
|
||||
[ -f "$B/obj/include/config.h" ] || { echo "configure did not produce include/config.h (see $B/obj/configure.log)"; exit 1; }
|
||||
fi
|
||||
cd "$B/ws"
|
||||
CF="-c -O2 -g -pipe -fno-strict-aliasing -D__WINESRC__ -D_GNU_SOURCE -DFP_EMULATED_X86_64 -I. -I$B/obj/include -I$SRC/include -I$SRC/server"
|
||||
for s in $(grep -oE "^\s+[a-z_0-9]+\.c" "$SRC/server/Makefile.in" | tr -d "\t "); do
|
||||
extra=()
|
||||
case $s in
|
||||
unicode.c) extra=('-DBINDIR="/usr/bin"' '-DDATADIR="/usr/share"') ;; # nls dir = <bin>/../share/wine/nls
|
||||
user.c) extra=(-include stdarg.h) ;;
|
||||
esac
|
||||
gcc $CF "${extra[@]}" -o "${s%.c}.o" "$SRC/server/$s"
|
||||
done
|
||||
gcc -o wineserver ./*.o -lrt -lm
|
||||
echo "built $B/ws/wineserver (protocol $(grep -oE "SERVER_PROTOCOL_VERSION [0-9]+" "$SRC/include/wine/server_protocol.h" | awk '{print $2}'))"
|
||||
@@ -0,0 +1,183 @@
|
||||
/* fp_mem.so: x86_64 LD_PRELOAD for Wine under FEX (give it to the guest with FEX_ENV=LD_PRELOAD=..., the FEX
|
||||
* compat tool strips LD_PRELOAD).
|
||||
*
|
||||
* FP_BIGCACHE_MB=<n>: keep the pages of released anonymous regions of >= n MB (up to FP_BIGCACHE_SLOTS regions,
|
||||
* default 12, and FP_BIGCACHE_MAX_MB in total, default 6144) and hand them back when Wine maps memory at the
|
||||
* same start address again. Unreal's large-block allocator gives blocks above its cache limit straight back to
|
||||
* the OS, and some games (Oculus First Contact: 0.5-1.4 GB every few seconds while playing) fill such blocks
|
||||
* completely each time: under FEX every 4 KB page then faults and is zeroed by the kernel (~130k faults, ~110 ms)
|
||||
* and releasing them costs about as much again, on a game task the game thread waits for.
|
||||
* Same start, other size: shorter = the rest is released; longer = only the extra part is mapped fresh.
|
||||
* Reused memory is NOT zeroed (Windows guarantees zeroed commits), so this is only for games that overwrite
|
||||
* the whole block; any other use of a kept range drops it first (fresh zero pages).
|
||||
* Kept regions keep their protection (they are not touched while kept), so keeping and reusing is nearly free.
|
||||
* FP_THP=1: MADV_HUGEPAGE on large writable anonymous regions (only helps while 2 MB pages are free).
|
||||
* FP_MEM_DEBUG=1: log to stderr. */
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#define MAX_SLOTS 16
|
||||
static size_t cache_min, cache_max = (size_t)6144 << 20, thp_min = (size_t)32 << 20;
|
||||
static int debug, thp, slots = 12;
|
||||
static int (*real_mprotect)(void *, size_t, int);
|
||||
static int (*real_munmap)(void *, size_t);
|
||||
static void *(*real_mmap)(void *, size_t, int, int, int, off_t);
|
||||
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
struct entry { char *addr; size_t len; int prot, via_munmap, used; };
|
||||
static struct entry cache[MAX_SLOTS];
|
||||
static size_t kept_total;
|
||||
static unsigned long hits, partial, stores, drops;
|
||||
|
||||
static void resolve(void)
|
||||
{
|
||||
if (!real_mmap) real_mmap = dlsym(RTLD_NEXT, "mmap");
|
||||
if (!real_munmap) real_munmap = dlsym(RTLD_NEXT, "munmap");
|
||||
if (!real_mprotect) real_mprotect = dlsym(RTLD_NEXT, "mprotect");
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void fp_mem_init(void)
|
||||
{
|
||||
const char *e;
|
||||
resolve();
|
||||
debug = getenv("FP_MEM_DEBUG") != NULL;
|
||||
thp = getenv("FP_THP") != NULL;
|
||||
if ((e = getenv("FP_BIGCACHE_MB")) && atol(e) > 0) cache_min = (size_t)atol(e) << 20;
|
||||
if ((e = getenv("FP_BIGCACHE_MAX_MB")) && atol(e) > 0) cache_max = (size_t)atol(e) << 20;
|
||||
if ((e = getenv("FP_BIGCACHE_SLOTS")) && atoi(e) > 0) slots = atoi(e) > MAX_SLOTS ? MAX_SLOTS : atoi(e);
|
||||
}
|
||||
|
||||
static void advise(void *addr, size_t len)
|
||||
{
|
||||
uintptr_t s = ((uintptr_t)addr + 0x1fffff) & ~(uintptr_t)0x1fffff;
|
||||
uintptr_t e = ((uintptr_t)addr + len) & ~(uintptr_t)0x1fffff;
|
||||
if (e > s) madvise((void *)s, e - s, MADV_HUGEPAGE);
|
||||
}
|
||||
|
||||
/* lock held: give part of a kept range up the way the program asked for it originally */
|
||||
static void release_range(const struct entry *c, char *addr, size_t len)
|
||||
{
|
||||
if (c->via_munmap) real_munmap(addr, len);
|
||||
else real_mmap(addr, len, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED | MAP_NORESERVE, -1, 0);
|
||||
}
|
||||
|
||||
static void drop(struct entry *c, const char *why)
|
||||
{
|
||||
if (debug) fprintf(stderr, "fp_mem: drop %p+%zx (%s)\n", c->addr, c->len, why);
|
||||
release_range(c, c->addr, c->len);
|
||||
kept_total -= c->len; c->used = 0; drops++;
|
||||
}
|
||||
|
||||
/* lock held: drop every kept range overlapping [addr, len) */
|
||||
static void drop_overlaps(const void *addr, size_t len, const char *why)
|
||||
{
|
||||
for (int i = 0; i < slots; i++)
|
||||
if (cache[i].used && (const char *)addr < cache[i].addr + cache[i].len && cache[i].addr < (const char *)addr + len)
|
||||
drop(&cache[i], why);
|
||||
}
|
||||
|
||||
static int any_kept(void)
|
||||
{
|
||||
for (int i = 0; i < slots; i++) if (cache[i].used) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* lock held: keep [addr, len) instead of releasing it (its pages and protection stay as they are) */
|
||||
static int store(void *addr, size_t len, int via_munmap, int prot)
|
||||
{
|
||||
int free_slot = -1;
|
||||
if (kept_total + len > cache_max) return 0;
|
||||
for (int i = 0; i < slots; i++) {
|
||||
if (!cache[i].used) { if (free_slot < 0) free_slot = i; continue; }
|
||||
if ((char *)addr < cache[i].addr + cache[i].len && cache[i].addr < (char *)addr + len) return 0;
|
||||
}
|
||||
if (free_slot < 0) return 0;
|
||||
cache[free_slot] = (struct entry){ addr, len, prot, via_munmap, 1 };
|
||||
kept_total += len; stores++;
|
||||
if (debug) fprintf(stderr, "fp_mem: keep %p+%zx (%s, kept %zu MB in total; stores %lu, hits %lu, partial %lu, drops %lu)\n",
|
||||
addr, len, via_munmap ? "munmap" : "release", kept_total >> 20, stores, hits, partial, drops);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* protection of a region before it is released: unknown here, so assume read/write (Wine's committed views) */
|
||||
#define KEPT_PROT (PROT_READ | PROT_WRITE)
|
||||
|
||||
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t off)
|
||||
{
|
||||
resolve();
|
||||
if (cache_min && len >= cache_min / 4 && any_kept()) {
|
||||
pthread_mutex_lock(&lock);
|
||||
for (int i = 0; i < slots; i++) {
|
||||
struct entry *c = &cache[i];
|
||||
if (!c->used || (char *)addr != c->addr || fd != -1 || !(flags & MAP_ANONYMOUS) || !(prot & PROT_WRITE)) continue;
|
||||
if (len <= c->len) {
|
||||
if (prot != c->prot && real_mprotect(addr, len, prot)) break;
|
||||
if (len < c->len) { release_range(c, c->addr + len, c->len - len); partial++; }
|
||||
kept_total -= c->len; c->used = 0; hits++;
|
||||
if (debug) fprintf(stderr, "fp_mem: reuse %p+%zx of %zx\n", addr, len, c->len);
|
||||
pthread_mutex_unlock(&lock);
|
||||
return addr;
|
||||
}
|
||||
/* longer: map the extra part fresh behind the kept one */
|
||||
void *t = real_mmap(c->addr + c->len, len - c->len, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
|
||||
if (t == (void *)(c->addr + c->len)) {
|
||||
if (prot != c->prot) real_mprotect(c->addr, c->len, prot);
|
||||
kept_total -= c->len; c->used = 0; hits++; partial++;
|
||||
if (debug) fprintf(stderr, "fp_mem: reuse %p+%zx of %zx (+ fresh tail)\n", addr, len, c->len);
|
||||
pthread_mutex_unlock(&lock);
|
||||
if (thp && len - c->len >= thp_min) advise(t, len - c->len);
|
||||
return addr;
|
||||
}
|
||||
if (t != MAP_FAILED) real_munmap(t, len - c->len);
|
||||
break;
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
if (cache_min && len >= cache_min && (flags & MAP_ANONYMOUS) && (flags & MAP_FIXED) && prot == PROT_NONE && fd == -1) {
|
||||
/* Wine releasing (part of) a view inside its reserved areas */
|
||||
pthread_mutex_lock(&lock);
|
||||
int ok = store(addr, len, 0, KEPT_PROT);
|
||||
pthread_mutex_unlock(&lock);
|
||||
if (ok) return addr;
|
||||
}
|
||||
if (any_kept()) {
|
||||
pthread_mutex_lock(&lock);
|
||||
drop_overlaps(addr, len, "mmap overlap");
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
void *r = real_mmap(addr, len, prot, flags, fd, off);
|
||||
if (thp && r != MAP_FAILED && (flags & MAP_ANONYMOUS) && (prot & PROT_WRITE) && len >= thp_min) advise(r, len);
|
||||
return r;
|
||||
}
|
||||
|
||||
void *mmap64(void *addr, size_t len, int prot, int flags, int fd, off_t off) __attribute__((alias("mmap")));
|
||||
|
||||
int munmap(void *addr, size_t len)
|
||||
{
|
||||
resolve();
|
||||
if (cache_min && (len >= cache_min || any_kept())) {
|
||||
pthread_mutex_lock(&lock);
|
||||
drop_overlaps(addr, len, "munmap overlap");
|
||||
if (len >= cache_min && store(addr, len, 1, KEPT_PROT)) { pthread_mutex_unlock(&lock); return 0; }
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
return real_munmap(addr, len);
|
||||
}
|
||||
|
||||
int mprotect(void *addr, size_t len, int prot)
|
||||
{
|
||||
resolve();
|
||||
if (any_kept()) {
|
||||
pthread_mutex_lock(&lock);
|
||||
drop_overlaps(addr, len, "mprotect overlap");
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
int r = real_mprotect(addr, len, prot);
|
||||
if (thp && !r && (prot & PROT_WRITE) && len >= thp_min) advise(addr, len);
|
||||
return r;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
diff --git a/dlls/crypt32/chain.c b/dlls/crypt32/chain.c
|
||||
--- a/dlls/crypt32/chain.c
|
||||
+++ b/dlls/crypt32/chain.c
|
||||
@@ -2578,6 +2578,36 @@
|
||||
return quality;
|
||||
}
|
||||
|
||||
+/* Between two chains of equal quality, Windows prefers the one whose issuer is
|
||||
+ * the most recent certificate (latest NotBefore) where the chains first
|
||||
+ * differ, e.g. a cross-signed root shipped with a signature (2022) over an
|
||||
+ * older self-signed copy of the same root (2013). Returns TRUE if alternate
|
||||
+ * should be preferred over chain.
|
||||
+ */
|
||||
+static BOOL CRYPT_AlternateHasNewerIssuer(const CertificateChain *chain,
|
||||
+ const CertificateChain *alternate)
|
||||
+{
|
||||
+ DWORD i, j;
|
||||
+
|
||||
+ for (i = 0; i < chain->context.cChain && i < alternate->context.cChain; i++)
|
||||
+ {
|
||||
+ const CERT_SIMPLE_CHAIN *a = chain->context.rgpChain[i];
|
||||
+ const CERT_SIMPLE_CHAIN *b = alternate->context.rgpChain[i];
|
||||
+
|
||||
+ for (j = 0; j < a->cElement && j < b->cElement; j++)
|
||||
+ {
|
||||
+ PCCERT_CONTEXT ca = a->rgpElement[j]->pCertContext;
|
||||
+ PCCERT_CONTEXT cb = b->rgpElement[j]->pCertContext;
|
||||
+
|
||||
+ if (!CertCompareCertificate(X509_ASN_ENCODING, ca->pCertInfo,
|
||||
+ cb->pCertInfo))
|
||||
+ return CompareFileTime(&cb->pCertInfo->NotBefore,
|
||||
+ &ca->pCertInfo->NotBefore) > 0;
|
||||
+ }
|
||||
+ }
|
||||
+ return FALSE;
|
||||
+}
|
||||
+
|
||||
/* Chooses the highest quality chain among chain and its "lower quality"
|
||||
* alternate chains. Returns the highest quality chain, with all other
|
||||
* chains as lower quality chains of it.
|
||||
@@ -2598,7 +2628,12 @@
|
||||
CertificateChain *alternate =
|
||||
(CertificateChain*)chain->context.rgpLowerQualityChainContext[i];
|
||||
|
||||
- if (CRYPT_ChainQuality(alternate) > CRYPT_ChainQuality(chain))
|
||||
+ DWORD alternateQuality = CRYPT_ChainQuality(alternate);
|
||||
+ DWORD chainQuality = CRYPT_ChainQuality(chain);
|
||||
+
|
||||
+ if (alternateQuality > chainQuality ||
|
||||
+ (alternateQuality == chainQuality &&
|
||||
+ CRYPT_AlternateHasNewerIssuer(chain, alternate)))
|
||||
{
|
||||
alternate->context.cLowerQualityChainContext =
|
||||
chain->context.cLowerQualityChainContext;
|
||||
@@ -0,0 +1,70 @@
|
||||
diff --git a/dlls/windows.devices.enumeration/main.c b/dlls/windows.devices.enumeration/main.c
|
||||
--- a/dlls/windows.devices.enumeration/main.c
|
||||
+++ b/dlls/windows.devices.enumeration/main.c
|
||||
@@ -118,6 +118,8 @@
|
||||
struct weak_reference_source weak_reference_source;
|
||||
|
||||
struct list added_handlers;
|
||||
+ struct list updated_handlers;
|
||||
+ struct list removed_handlers;
|
||||
struct list enumerated_handlers;
|
||||
struct list stopped_handlers;
|
||||
IUnknown *query_params;
|
||||
@@ -177,6 +179,8 @@
|
||||
if (!ref)
|
||||
{
|
||||
typed_event_handlers_clear( &impl->added_handlers );
|
||||
+ typed_event_handlers_clear( &impl->updated_handlers );
|
||||
+ typed_event_handlers_clear( &impl->removed_handlers );
|
||||
typed_event_handlers_clear( &impl->enumerated_handlers );
|
||||
typed_event_handlers_clear( &impl->stopped_handlers );
|
||||
IUnknown_Release( impl->query_params );
|
||||
@@ -225,27 +229,31 @@
|
||||
static HRESULT WINAPI device_watcher_add_Updated( IDeviceWatcher *iface, ITypedEventHandler_DeviceWatcher_DeviceInformationUpdate *handler,
|
||||
EventRegistrationToken *token )
|
||||
{
|
||||
- FIXME( "iface %p, handler %p, token %p stub!\n", iface, handler, token );
|
||||
- return S_OK;
|
||||
+ struct device_watcher *impl = impl_from_IDeviceWatcher( iface );
|
||||
+ TRACE( "iface %p, handler %p, token %p\n", iface, handler, token );
|
||||
+ return typed_event_handlers_append( &impl->updated_handlers, (ITypedEventHandler_IInspectable_IInspectable *)handler, token );
|
||||
}
|
||||
|
||||
static HRESULT WINAPI device_watcher_remove_Updated( IDeviceWatcher *iface, EventRegistrationToken token )
|
||||
{
|
||||
- FIXME( "iface %p, token %#I64x stub!\n", iface, token.value );
|
||||
- return E_NOTIMPL;
|
||||
+ struct device_watcher *impl = impl_from_IDeviceWatcher( iface );
|
||||
+ TRACE( "iface %p, token %#I64x.\n", iface, token.value );
|
||||
+ return typed_event_handlers_remove( &impl->updated_handlers, &token );
|
||||
}
|
||||
|
||||
static HRESULT WINAPI device_watcher_add_Removed( IDeviceWatcher *iface, ITypedEventHandler_DeviceWatcher_DeviceInformationUpdate *handler,
|
||||
EventRegistrationToken *token )
|
||||
{
|
||||
- FIXME( "iface %p, handler %p, token %p stub!\n", iface, handler, token );
|
||||
- return E_NOTIMPL;
|
||||
+ struct device_watcher *impl = impl_from_IDeviceWatcher( iface );
|
||||
+ TRACE( "iface %p, handler %p, token %p\n", iface, handler, token );
|
||||
+ return typed_event_handlers_append( &impl->removed_handlers, (ITypedEventHandler_IInspectable_IInspectable *)handler, token );
|
||||
}
|
||||
|
||||
static HRESULT WINAPI device_watcher_remove_Removed( IDeviceWatcher *iface, EventRegistrationToken token )
|
||||
{
|
||||
- FIXME( "iface %p, token %#I64x stub!\n", iface, token.value );
|
||||
- return E_NOTIMPL;
|
||||
+ struct device_watcher *impl = impl_from_IDeviceWatcher( iface );
|
||||
+ TRACE( "iface %p, token %#I64x.\n", iface, token.value );
|
||||
+ return typed_event_handlers_remove( &impl->removed_handlers, &token );
|
||||
}
|
||||
|
||||
static HRESULT WINAPI device_watcher_add_EnumerationCompleted( IDeviceWatcher *iface, ITypedEventHandler_DeviceWatcher_IInspectable *handler,
|
||||
@@ -592,6 +600,8 @@
|
||||
if (FAILED(hr = devquery_params_create( type, expr, prop_keys, prop_keys_len, &impl->query_params ))) goto failed;
|
||||
|
||||
list_init( &impl->added_handlers );
|
||||
+ list_init( &impl->updated_handlers );
|
||||
+ list_init( &impl->removed_handlers );
|
||||
list_init( &impl->enumerated_handlers );
|
||||
list_init( &impl->stopped_handlers );
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
diff --git a/dlls/dnsapi/dnsapi.spec b/dlls/dnsapi/dnsapi.spec
|
||||
--- a/dlls/dnsapi/dnsapi.spec
|
||||
+++ b/dlls/dnsapi/dnsapi.spec
|
||||
@@ -104,12 +104,20 @@
|
||||
@ stdcall DnsReplaceRecordSetW(ptr long ptr ptr ptr)
|
||||
@ stub DnsReplaceRecordSet_W
|
||||
@ stdcall DnsServiceBrowse(ptr ptr)
|
||||
+@ stdcall DnsServiceBrowseCancel(ptr)
|
||||
+@ stdcall DnsServiceConstructInstance(wstr wstr ptr ptr long long long long ptr ptr)
|
||||
+@ stdcall DnsServiceDeRegister(ptr ptr)
|
||||
+@ stdcall DnsServiceFreeInstance(ptr)
|
||||
@ stub DnsServiceNotificationDeregister_A
|
||||
@ stub DnsServiceNotificationDeregister_UTF8
|
||||
@ stub DnsServiceNotificationDeregister_W
|
||||
@ stub DnsServiceNotificationRegister_A
|
||||
@ stub DnsServiceNotificationRegister_UTF8
|
||||
@ stub DnsServiceNotificationRegister_W
|
||||
+@ stdcall DnsServiceRegister(ptr ptr)
|
||||
+@ stdcall DnsServiceRegisterCancel(ptr)
|
||||
+@ stdcall DnsServiceResolve(ptr ptr)
|
||||
+@ stdcall DnsServiceResolveCancel(ptr)
|
||||
@ stub DnsSetMaxNumberOfAddressesToRegister
|
||||
@ stub DnsStatusString
|
||||
@ stub DnsStringCopyAllocateEx
|
||||
diff --git a/dlls/dnsapi/main.c b/dlls/dnsapi/main.c
|
||||
--- a/dlls/dnsapi/main.c
|
||||
+++ b/dlls/dnsapi/main.c
|
||||
@@ -243,3 +243,135 @@
|
||||
FIXME( "(%p, %p) stub\n", request, cancel );
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceConstructInstance [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+DNS_SERVICE_INSTANCE * WINAPI DnsServiceConstructInstance( const WCHAR *name, const WCHAR *host,
|
||||
+ IP4_ADDRESS *ip4, IP6_ADDRESS *ip6, WORD port, WORD priority, WORD weight,
|
||||
+ DWORD count, const WCHAR **keys, const WCHAR **values )
|
||||
+{
|
||||
+ DNS_SERVICE_INSTANCE *instance;
|
||||
+ DWORD i;
|
||||
+
|
||||
+ TRACE( "(%s, %s, %p, %p, %u, %u, %u, %lu, %p, %p)\n", debugstr_w(name), debugstr_w(host),
|
||||
+ ip4, ip6, port, priority, weight, count, keys, values );
|
||||
+
|
||||
+ if (!(instance = calloc( 1, sizeof(*instance) ))) return NULL;
|
||||
+
|
||||
+ if (name) instance->pszInstanceName = wcsdup( name );
|
||||
+ if (host) instance->pszHostName = wcsdup( host );
|
||||
+ if (ip4 && (instance->ip4Address = malloc( sizeof(*ip4) ))) *instance->ip4Address = *ip4;
|
||||
+ if (ip6 && (instance->ip6Address = malloc( sizeof(*ip6) ))) *instance->ip6Address = *ip6;
|
||||
+ instance->wPort = port;
|
||||
+ instance->wPriority = priority;
|
||||
+ instance->wWeight = weight;
|
||||
+ instance->dwPropertyCount = count;
|
||||
+
|
||||
+ if (count && (instance->keys = calloc( count, sizeof(*keys) )))
|
||||
+ for (i = 0; i < count; i++) instance->keys[i] = wcsdup( keys[i] );
|
||||
+ if (count && (instance->values = calloc( count, sizeof(*values) )))
|
||||
+ for (i = 0; i < count; i++) instance->values[i] = wcsdup( values[i] );
|
||||
+
|
||||
+ return instance;
|
||||
+}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceFreeInstance [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+void WINAPI DnsServiceFreeInstance( DNS_SERVICE_INSTANCE *instance )
|
||||
+{
|
||||
+ DWORD i;
|
||||
+
|
||||
+ TRACE( "(%p)\n", instance );
|
||||
+
|
||||
+ if (!instance) return;
|
||||
+
|
||||
+ free( instance->pszInstanceName );
|
||||
+ free( instance->pszHostName );
|
||||
+ free( instance->ip4Address );
|
||||
+ free( instance->ip6Address );
|
||||
+ for (i = 0; i < instance->dwPropertyCount; i++)
|
||||
+ {
|
||||
+ if (instance->keys) free( instance->keys[i] );
|
||||
+ if (instance->values) free( instance->values[i] );
|
||||
+ }
|
||||
+ free( instance->keys );
|
||||
+ free( instance->values );
|
||||
+ free( instance );
|
||||
+}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceRegister [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+DNS_STATUS WINAPI DnsServiceRegister( DNS_SERVICE_REGISTER_REQUEST *request, DNS_SERVICE_CANCEL *cancel )
|
||||
+{
|
||||
+ FIXME( "(%p, %p) stub\n", request, cancel );
|
||||
+
|
||||
+ if (!request) return ERROR_INVALID_PARAMETER;
|
||||
+
|
||||
+ /* Registration needs mDNS support, which is not implemented. Fail
|
||||
+ * synchronously: the completion callback must only run when this returns
|
||||
+ * DNS_REQUEST_PENDING, and calling it inline would re-enter the caller
|
||||
+ * while it still believes the operation is outstanding. */
|
||||
+ return ERROR_CALL_NOT_IMPLEMENTED;
|
||||
+}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceDeRegister [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+DNS_STATUS WINAPI DnsServiceDeRegister( DNS_SERVICE_REGISTER_REQUEST *request, DNS_SERVICE_CANCEL *cancel )
|
||||
+{
|
||||
+ FIXME( "(%p, %p) stub\n", request, cancel );
|
||||
+
|
||||
+ if (!request) return ERROR_INVALID_PARAMETER;
|
||||
+
|
||||
+ return ERROR_CALL_NOT_IMPLEMENTED;
|
||||
+}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceRegisterCancel [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+DNS_STATUS WINAPI DnsServiceRegisterCancel( DNS_SERVICE_CANCEL *cancel )
|
||||
+{
|
||||
+ FIXME( "(%p) stub\n", cancel );
|
||||
+ return ERROR_CALL_NOT_IMPLEMENTED;
|
||||
+}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceResolve [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+DNS_STATUS WINAPI DnsServiceResolve( DNS_SERVICE_RESOLVE_REQUEST *request, DNS_SERVICE_CANCEL *cancel )
|
||||
+{
|
||||
+ FIXME( "(%p, %p) stub\n", request, cancel );
|
||||
+
|
||||
+ if (!request) return ERROR_INVALID_PARAMETER;
|
||||
+
|
||||
+ return ERROR_CALL_NOT_IMPLEMENTED;
|
||||
+}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceResolveCancel [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+DNS_STATUS WINAPI DnsServiceResolveCancel( DNS_SERVICE_CANCEL *cancel )
|
||||
+{
|
||||
+ FIXME( "(%p) stub\n", cancel );
|
||||
+ return ERROR_CALL_NOT_IMPLEMENTED;
|
||||
+}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceBrowseCancel [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+DNS_STATUS WINAPI DnsServiceBrowseCancel( DNS_SERVICE_CANCEL *cancel )
|
||||
+{
|
||||
+ FIXME( "(%p) stub\n", cancel );
|
||||
+ return ERROR_CALL_NOT_IMPLEMENTED;
|
||||
+}
|
||||
diff --git a/include/windns.h b/include/windns.h
|
||||
--- a/include/windns.h
|
||||
+++ b/include/windns.h
|
||||
@@ -106,7 +106,8 @@
|
||||
#define DNS_QUERY_APPEND_MULTILABEL 0x00800000
|
||||
#define DNS_QUERY_DNSSEC_OK 0x01000000
|
||||
#define DNS_QUERY_DNSSEC_CHECKING_DISABLED 0x02000000
|
||||
-#define DNS_QUERY_RESERVED 0xff000000
|
||||
+#define DNS_QUERY_DNSSEC_REQUIRED 0x04000000
|
||||
+#define DNS_QUERY_RESERVED 0xf0000000
|
||||
|
||||
#define INLINE_WORD_FLIP(out, in) { WORD _in = (in); (out) = (_in << 8) | (_in >> 8); }
|
||||
#define INLINE_HTONS(out, in) INLINE_WORD_FLIP(out, in)
|
||||
@@ -781,6 +782,73 @@
|
||||
void *reserved;
|
||||
} DNS_SERVICE_CANCEL, *PDNS_SERVICE_CANCEL;
|
||||
|
||||
+typedef struct _DNS_SERVICE_INSTANCE
|
||||
+{
|
||||
+ LPWSTR pszInstanceName;
|
||||
+ LPWSTR pszHostName;
|
||||
+ IP4_ADDRESS *ip4Address;
|
||||
+ IP6_ADDRESS *ip6Address;
|
||||
+ WORD wPort;
|
||||
+ WORD wPriority;
|
||||
+ WORD wWeight;
|
||||
+ DWORD dwPropertyCount;
|
||||
+ PWSTR *keys;
|
||||
+ PWSTR *values;
|
||||
+ DWORD dwInterfaceIndex;
|
||||
+} DNS_SERVICE_INSTANCE, *PDNS_SERVICE_INSTANCE;
|
||||
+
|
||||
+typedef void WINAPI DNS_SERVICE_REGISTER_COMPLETE(DWORD, void *, PDNS_SERVICE_INSTANCE);
|
||||
+typedef DNS_SERVICE_REGISTER_COMPLETE *PDNS_SERVICE_REGISTER_COMPLETE;
|
||||
+
|
||||
+typedef struct _DNS_SERVICE_REGISTER_REQUEST
|
||||
+{
|
||||
+ ULONG Version;
|
||||
+ ULONG InterfaceIndex;
|
||||
+ PDNS_SERVICE_INSTANCE pServiceInstance;
|
||||
+ PDNS_SERVICE_REGISTER_COMPLETE pRegisterCompletionCallback;
|
||||
+ PVOID pQueryContext;
|
||||
+ HANDLE hCredentials;
|
||||
+ BOOL unicastEnabled;
|
||||
+} DNS_SERVICE_REGISTER_REQUEST, *PDNS_SERVICE_REGISTER_REQUEST;
|
||||
+
|
||||
+typedef void WINAPI DNS_SERVICE_RESOLVE_COMPLETE(DWORD, void *, PDNS_SERVICE_INSTANCE);
|
||||
+typedef DNS_SERVICE_RESOLVE_COMPLETE *PDNS_SERVICE_RESOLVE_COMPLETE;
|
||||
+
|
||||
+typedef struct _DNS_SERVICE_RESOLVE_REQUEST
|
||||
+{
|
||||
+ ULONG Version;
|
||||
+ ULONG InterfaceIndex;
|
||||
+ LPWSTR QueryName;
|
||||
+ PDNS_SERVICE_RESOLVE_COMPLETE pResolveCompletionCallback;
|
||||
+ PVOID pQueryContext;
|
||||
+} DNS_SERVICE_RESOLVE_REQUEST, *PDNS_SERVICE_RESOLVE_REQUEST;
|
||||
+
|
||||
+typedef struct _MDNS_QUERY_HANDLE
|
||||
+{
|
||||
+ WCHAR nameBuf[DNS_MAX_NAME_BUFFER_LENGTH];
|
||||
+ WORD wType;
|
||||
+ PVOID pSubscription;
|
||||
+ PVOID pWnfCallbackParams;
|
||||
+ ULONG stateNameData[2];
|
||||
+} MDNS_QUERY_HANDLE, *PMDNS_QUERY_HANDLE;
|
||||
+
|
||||
+typedef void WINAPI MDNS_QUERY_CALLBACK(PVOID,PMDNS_QUERY_HANDLE,PDNS_QUERY_RESULT);
|
||||
+typedef MDNS_QUERY_CALLBACK *PMDNS_QUERY_CALLBACK;
|
||||
+
|
||||
+typedef struct _MDNS_QUERY_REQUEST
|
||||
+{
|
||||
+ ULONG Version;
|
||||
+ ULONG ulRefCount;
|
||||
+ PCWSTR Query;
|
||||
+ WORD QueryType;
|
||||
+ ULONG64 QueryOptions;
|
||||
+ ULONG InterfaceIndex;
|
||||
+ PMDNS_QUERY_CALLBACK pQueryCallback;
|
||||
+ PVOID pQueryContext;
|
||||
+ BOOL fAnswerReceived;
|
||||
+ ULONG ulResendCount;
|
||||
+} MDNS_QUERY_REQUEST, *PMDNS_QUERY_REQUEST;
|
||||
+
|
||||
DNS_STATUS WINAPI DnsAcquireContextHandle_A(DWORD,PVOID,PHANDLE);
|
||||
DNS_STATUS WINAPI DnsAcquireContextHandle_W(DWORD,PVOID,PHANDLE);
|
||||
#define DnsAcquireContextHandle WINELIB_NAME_AW(DnsAcquireContextHandle_)
|
||||
@@ -800,6 +868,7 @@
|
||||
#define DnsQuery WINELIB_NAME_AW(DnsQuery_)
|
||||
DNS_STATUS WINAPI DnsQueryEx(DNS_QUERY_REQUEST*,DNS_QUERY_RESULT*,DNS_QUERY_CANCEL*);
|
||||
DNS_STATUS WINAPI DnsCancelQuery(DNS_QUERY_CANCEL*);
|
||||
+BOOL WINAPI DnsGetCacheDataTable(PDNS_CACHE_ENTRY*);
|
||||
DNS_STATUS WINAPI DnsQueryConfig(DNS_CONFIG_TYPE,DWORD,PCWSTR,PVOID,PVOID,PDWORD);
|
||||
BOOL WINAPI DnsRecordCompare(PDNS_RECORD,PDNS_RECORD);
|
||||
PDNS_RECORD WINAPI DnsRecordCopyEx(PDNS_RECORD,DNS_CHARSET,DNS_CHARSET);
|
||||
@@ -813,13 +882,22 @@
|
||||
DNS_STATUS WINAPI DnsReplaceRecordSetUTF8(PDNS_RECORDA,DWORD,HANDLE,PVOID,PVOID);
|
||||
#define DnsReplaceRecordSet WINELIB_NAME_AW(DnsReplaceRecordSet)
|
||||
DNS_STATUS WINAPI DnsServiceBrowse(PDNS_SERVICE_BROWSE_REQUEST, PDNS_SERVICE_CANCEL);
|
||||
+DNS_STATUS WINAPI DnsServiceBrowseCancel(PDNS_SERVICE_CANCEL);
|
||||
+PDNS_SERVICE_INSTANCE WINAPI DnsServiceConstructInstance(PCWSTR,PCWSTR,PIP4_ADDRESS,PIP6_ADDRESS,WORD,WORD,WORD,DWORD,PCWSTR*,PCWSTR*);
|
||||
+DNS_STATUS WINAPI DnsServiceDeRegister(PDNS_SERVICE_REGISTER_REQUEST,PDNS_SERVICE_CANCEL);
|
||||
+VOID WINAPI DnsServiceFreeInstance(PDNS_SERVICE_INSTANCE);
|
||||
+DNS_STATUS WINAPI DnsServiceRegister(PDNS_SERVICE_REGISTER_REQUEST,PDNS_SERVICE_CANCEL);
|
||||
+DNS_STATUS WINAPI DnsServiceRegisterCancel(PDNS_SERVICE_CANCEL);
|
||||
+DNS_STATUS WINAPI DnsServiceResolve(PDNS_SERVICE_RESOLVE_REQUEST,PDNS_SERVICE_CANCEL);
|
||||
+DNS_STATUS WINAPI DnsServiceResolveCancel(PDNS_SERVICE_CANCEL);
|
||||
+DNS_STATUS WINAPI DnsStartMulticastQuery(PMDNS_QUERY_REQUEST,PMDNS_QUERY_HANDLE);
|
||||
+DNS_STATUS WINAPI DnsStopMulticastQuery(PMDNS_QUERY_HANDLE);
|
||||
DNS_STATUS WINAPI DnsValidateName_A(PCSTR,DNS_NAME_FORMAT);
|
||||
DNS_STATUS WINAPI DnsValidateName_W(PCWSTR, DNS_NAME_FORMAT);
|
||||
DNS_STATUS WINAPI DnsValidateName_UTF8(PCSTR,DNS_NAME_FORMAT);
|
||||
#define DnsValidateName WINELIB_NAME_AW(DnsValidateName_)
|
||||
BOOL WINAPI DnsWriteQuestionToBuffer_W(PDNS_MESSAGE_BUFFER,PDWORD,PCWSTR,WORD,WORD,BOOL);
|
||||
BOOL WINAPI DnsWriteQuestionToBuffer_UTF8(PDNS_MESSAGE_BUFFER,PDWORD,PCSTR,WORD,WORD,BOOL);
|
||||
-BOOL WINAPI DnsGetCacheDataTable(PDNS_CACHE_ENTRY*);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
diff --git a/dlls/sechost/trace.c b/dlls/sechost/trace.c
|
||||
--- a/dlls/sechost/trace.c
|
||||
+++ b/dlls/sechost/trace.c
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "evntrace.h"
|
||||
|
||||
#include "wine/debug.h"
|
||||
+#include "wine/exception.h"
|
||||
|
||||
WINE_DEFAULT_DEBUG_CHANNEL(eventlog);
|
||||
|
||||
@@ -116,14 +117,63 @@
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
+/* A real-time consumer of a session. No ETW providers exist under Wine, so the
|
||||
+ * consumer never receives events: ProcessTrace() waits until CloseTrace(). */
|
||||
+#define REALTIME_CONSUMER_MAGIC 0x52544345 /* "ECTR" */
|
||||
+#ifndef PROCESS_TRACE_MODE_REAL_TIME
|
||||
+#define PROCESS_TRACE_MODE_REAL_TIME 0x00000100 /* shares LogFileMode in Wine's header */
|
||||
+#endif
|
||||
+
|
||||
+struct realtime_consumer
|
||||
+{
|
||||
+ DWORD magic;
|
||||
+ HANDLE closed;
|
||||
+};
|
||||
+
|
||||
+static struct realtime_consumer *get_realtime_consumer( TRACEHANDLE handle )
|
||||
+{
|
||||
+ struct realtime_consumer *consumer = (struct realtime_consumer *)(ULONG_PTR)handle;
|
||||
+
|
||||
+ if (!handle || handle == INVALID_PROCESSTRACE_HANDLE) return NULL;
|
||||
+ __TRY
|
||||
+ {
|
||||
+ if (consumer->magic != REALTIME_CONSUMER_MAGIC) consumer = NULL;
|
||||
+ }
|
||||
+ __EXCEPT_PAGE_FAULT
|
||||
+ {
|
||||
+ consumer = NULL;
|
||||
+ }
|
||||
+ __ENDTRY
|
||||
+ return consumer;
|
||||
+}
|
||||
+
|
||||
/******************************************************************************
|
||||
* OpenTraceW (sechost.@)
|
||||
*/
|
||||
TRACEHANDLE WINAPI OpenTraceW( EVENT_TRACE_LOGFILEW *logfile )
|
||||
{
|
||||
static int once;
|
||||
+ struct realtime_consumer *consumer;
|
||||
|
||||
- if (!once++) FIXME("%p: stub\n", logfile);
|
||||
+ /* a session name without a log file can only be consumed in real time */
|
||||
+ if (logfile && logfile->LoggerName &&
|
||||
+ ((logfile->LogFileMode & PROCESS_TRACE_MODE_REAL_TIME) || !logfile->LogFileName))
|
||||
+ {
|
||||
+ FIXME("%p (%s): real-time consumer without events\n", logfile, debugstr_w(logfile->LoggerName));
|
||||
+ if (!(consumer = HeapAlloc( GetProcessHeap(), 0, sizeof(*consumer) )))
|
||||
+ {
|
||||
+ SetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
||||
+ return INVALID_PROCESSTRACE_HANDLE;
|
||||
+ }
|
||||
+ consumer->magic = REALTIME_CONSUMER_MAGIC;
|
||||
+ consumer->closed = CreateEventW( NULL, TRUE, FALSE, NULL );
|
||||
+ return (TRACEHANDLE)(ULONG_PTR)consumer;
|
||||
+ }
|
||||
+
|
||||
+ if (!once++) FIXME("%p: stub (logfile %s, logger %s, mode %#lx)\n", logfile,
|
||||
+ logfile ? debugstr_w(logfile->LogFileName) : "-",
|
||||
+ logfile ? debugstr_w(logfile->LoggerName) : "-",
|
||||
+ logfile ? logfile->LogFileMode : 0);
|
||||
SetLastError(ERROR_ACCESS_DENIED);
|
||||
return INVALID_PROCESSTRACE_HANDLE;
|
||||
}
|
||||
@@ -133,6 +183,25 @@
|
||||
*/
|
||||
ULONG WINAPI ProcessTrace( TRACEHANDLE *handles, ULONG count, FILETIME *start_time, FILETIME *end_time )
|
||||
{
|
||||
+ HANDLE events[MAXIMUM_WAIT_OBJECTS];
|
||||
+ ULONG i;
|
||||
+
|
||||
+ if (handles && count && count <= MAXIMUM_WAIT_OBJECTS)
|
||||
+ {
|
||||
+ for (i = 0; i < count; i++)
|
||||
+ {
|
||||
+ struct realtime_consumer *consumer = get_realtime_consumer( handles[i] );
|
||||
+ if (!consumer) break;
|
||||
+ events[i] = consumer->closed;
|
||||
+ }
|
||||
+ if (i == count)
|
||||
+ {
|
||||
+ TRACE("%p %lu: waiting for CloseTrace\n", handles, count);
|
||||
+ WaitForMultipleObjects( count, events, TRUE, INFINITE );
|
||||
+ return ERROR_SUCCESS;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
FIXME("%p %lu %p %p: stub\n", handles, count, start_time, end_time);
|
||||
return ERROR_CALL_NOT_IMPLEMENTED;
|
||||
}
|
||||
@@ -142,6 +211,15 @@
|
||||
*/
|
||||
ULONG WINAPI CloseTrace( TRACEHANDLE handle )
|
||||
{
|
||||
+ struct realtime_consumer *consumer = get_realtime_consumer( handle );
|
||||
+
|
||||
+ if (consumer)
|
||||
+ {
|
||||
+ /* the handle stays allocated: ProcessTrace() may still be waiting on it */
|
||||
+ SetEvent( consumer->closed );
|
||||
+ return ERROR_SUCCESS;
|
||||
+ }
|
||||
+
|
||||
FIXME("%s: stub\n", wine_dbgstr_longlong(handle));
|
||||
return ERROR_INVALID_HANDLE;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# Reproduce the exact Wine source of a GE-Proton release (Wine submodule + wine-staging + GE's own patches): the WINE
|
||||
# section of GE's patches/protonprep-valve-staging.sh, without the DXVK/vkd3d/protonfixes steps.
|
||||
# usage: prep_ge_wine.sh <work dir> [tag=GE-Proton11-7] → <work dir>/src/wine
|
||||
# Needs git, patch, python3, autoconf/autoreconf (the WINE section ends with autoreconf + tools/make_requests).
|
||||
set -e
|
||||
W=$1; TAG=${2:-GE-Proton11-7}
|
||||
[ -n "$W" ] || { echo "usage: $0 <work dir> [tag]"; exit 2; }
|
||||
mkdir -p "$W" && cd "$W"
|
||||
[ -d src ] || git clone -q --depth 1 --branch "$TAG" https://github.com/GloriousEggroll/proton-ge-custom src
|
||||
cd src
|
||||
git submodule update --init --depth 1 wine wine-staging
|
||||
P=patches/protonprep-valve-staging.sh
|
||||
start=$(grep -n "^### (2) WINE PATCHING ###" $P | cut -d: -f1)
|
||||
end=$(grep -n "^### END WINE PATCHING ###" $P | cut -d: -f1)
|
||||
fn_end=$(grep -n "^### (1) PREP SECTION ###" $P | cut -d: -f1)
|
||||
[ -n "$start" ] && [ -n "$end" ] && [ -n "$fn_end" ] || { echo "unexpected layout of $P"; exit 1; }
|
||||
# the WINE section reverts commits from the Wine history: fetch them into the shallow clone first
|
||||
for c in $(sed -n "${start},${end}p" $P | grep -oE "git revert [^#]*" | grep -oE "[0-9a-f]{40}"); do
|
||||
git -C wine fetch -q --depth 2 origin "$c"
|
||||
done
|
||||
{ sed -n "1,$((fn_end - 1))p" $P; sed -n "${start},${end}p" $P; } > ../wine_only.sh
|
||||
bash ../wine_only.sh > ../prep.log 2>&1
|
||||
grep -n "SERVER_PROTOCOL_VERSION" wine/include/wine/server_protocol.h
|
||||
if find wine -name "*.rej" | grep -q .; then echo "rejected hunks, see ../prep.log"; exit 1; fi
|
||||
echo "patched Wine tree: $W/src/wine"
|
||||
@@ -0,0 +1,79 @@
|
||||
/* windows.devices.wifi.dll stand-in for Wine: activation factory of Windows.Devices.WiFi.WiFiAdapter with the
|
||||
* IWiFiAdapterStatics methods. Wine has no Wi-Fi WinRT class; Meta's runtime (OculusAppFramework, Air Link "Ari"
|
||||
* dongle code) calls WiFiAdapter.GetDeviceSelector() after login, enumerates devices with it and only calls
|
||||
* FromIdAsync for devices found. Without the class, C++/WinRT throws and the runtime fail-fasts (c0000409).
|
||||
* GetDeviceSelector returns a selector that matches no device; the async methods report E_NOTIMPL (never reached
|
||||
* when nothing is found). Unknown interface requests are logged to stderr.
|
||||
*/
|
||||
#define COBJMACROS
|
||||
#include <windows.h>
|
||||
#include <winstring.h>
|
||||
#include <stdio.h>
|
||||
|
||||
typedef struct IInspectableVtbl_ { void *s[6]; } dummy_t;
|
||||
|
||||
static const GUID IID_IUnknown_ = {0x00000000,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46}};
|
||||
static const GUID IID_IInspectable_ = {0xaf86e2e0,0xb12d,0x4c6a,{0x9c,0x5a,0xd7,0xaa,0x65,0x10,0x1e,0x90}};
|
||||
static const GUID IID_IAgileObject_ = {0x94ea2b94,0xe9cc,0x49e0,{0xc0,0xff,0xee,0x64,0xca,0x8f,0x5b,0x90}};
|
||||
static const GUID IID_IActivationFactory_ = {0x00000035,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46}};
|
||||
/* Windows.Devices.WiFi.IWiFiAdapterStatics */
|
||||
static const GUID IID_IWiFiAdapterStatics = {0xda25fddd,0xd24c,0x43e3,{0xaa,0xbd,0xc4,0x65,0x9f,0x73,0x0f,0x99}};
|
||||
|
||||
typedef struct statics { const void **vtbl; } statics;
|
||||
|
||||
static HRESULT WINAPI f_QueryInterface(statics *This, REFIID iid, void **out)
|
||||
{
|
||||
if (IsEqualGUID(iid, &IID_IUnknown_) || IsEqualGUID(iid, &IID_IInspectable_) || IsEqualGUID(iid, &IID_IAgileObject_)
|
||||
|| IsEqualGUID(iid, &IID_IActivationFactory_) || IsEqualGUID(iid, &IID_IWiFiAdapterStatics))
|
||||
{
|
||||
*out = This;
|
||||
return S_OK;
|
||||
}
|
||||
fprintf(stderr, "wifi_stub: unknown interface {%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}\n", iid->Data1,
|
||||
iid->Data2, iid->Data3, iid->Data4[0], iid->Data4[1], iid->Data4[2], iid->Data4[3], iid->Data4[4],
|
||||
iid->Data4[5], iid->Data4[6], iid->Data4[7]);
|
||||
*out = NULL;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
static ULONG WINAPI f_AddRef(statics *This) { return 2; }
|
||||
static ULONG WINAPI f_Release(statics *This) { return 1; }
|
||||
static HRESULT WINAPI f_GetIids(statics *This, ULONG *count, IID **iids)
|
||||
{
|
||||
IID *p = CoTaskMemAlloc(sizeof(IID));
|
||||
if (!p) return E_OUTOFMEMORY;
|
||||
*p = IID_IWiFiAdapterStatics; *count = 1; *iids = p;
|
||||
return S_OK;
|
||||
}
|
||||
static HRESULT WINAPI f_GetRuntimeClassName(statics *This, HSTRING *name)
|
||||
{
|
||||
static const WCHAR n[] = L"Windows.Devices.WiFi.WiFiAdapter";
|
||||
return WindowsCreateString(n, ARRAYSIZE(n) - 1, name);
|
||||
}
|
||||
static HRESULT WINAPI f_GetTrustLevel(statics *This, int *level) { *level = 0; return S_OK; }
|
||||
/* slot 6: IWiFiAdapterStatics::FindAllAdaptersAsync (and IActivationFactory::ActivateInstance: not activatable) */
|
||||
static HRESULT WINAPI f_FindAllAdaptersAsync(statics *This, void **op) { if (op) *op = NULL; return E_NOTIMPL; }
|
||||
static HRESULT WINAPI f_GetDeviceSelector(statics *This, HSTRING *value)
|
||||
{
|
||||
/* an interface class no device has: device enumeration with it finds nothing */
|
||||
static const WCHAR sel[] = L"System.Devices.InterfaceClassGuid:=\"{f0e2c0a5-7b3e-4f61-9b47-3d1c0f0e2a11}\" AND "
|
||||
L"System.Devices.InterfaceEnabled:=System.StructuredQueryType.Boolean#True";
|
||||
fprintf(stderr, "wifi_stub: GetDeviceSelector\n");
|
||||
return WindowsCreateString(sel, ARRAYSIZE(sel) - 1, value);
|
||||
}
|
||||
static HRESULT WINAPI f_FromIdAsync(statics *This, HSTRING id, void **op) { if (op) *op = NULL; return E_NOTIMPL; }
|
||||
static HRESULT WINAPI f_RequestAccessAsync(statics *This, void **op) { if (op) *op = NULL; return E_NOTIMPL; }
|
||||
|
||||
static const void *vtbl[] = { f_QueryInterface, f_AddRef, f_Release, f_GetIids, f_GetRuntimeClassName,
|
||||
f_GetTrustLevel, f_FindAllAdaptersAsync, f_GetDeviceSelector, f_FromIdAsync,
|
||||
f_RequestAccessAsync };
|
||||
static statics factory = { vtbl };
|
||||
|
||||
HRESULT WINAPI DllGetActivationFactory(HSTRING classid, void **out)
|
||||
{
|
||||
const WCHAR *s = WindowsGetStringRawBuffer(classid, NULL);
|
||||
fprintf(stderr, "wifi_stub: DllGetActivationFactory %ls\n", s ? s : L"(null)");
|
||||
if (!s || wcscmp(s, L"Windows.Devices.WiFi.WiFiAdapter")) { *out = NULL; return CLASS_E_CLASSNOTAVAILABLE; }
|
||||
*out = &factory;
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT WINAPI DllCanUnloadNow(void) { return S_FALSE; }
|
||||
@@ -0,0 +1,4 @@
|
||||
LIBRARY windows.devices.wifi.dll
|
||||
EXPORTS
|
||||
DllGetActivationFactory
|
||||
DllCanUnloadNow PRIVATE
|
||||
@@ -42,6 +42,7 @@ def stage_data():
|
||||
shutil.copytree(ROOT / name, DATA / name)
|
||||
# the Frame agent is uploaded as source; `flet build` turns every .py into .pyc (issue #2): keep a non-.py copy
|
||||
shutil.copy2(ROOT / "agent/frameport_agent.py", DATA / "agent/frameport_agent.py.txt")
|
||||
shutil.copy2(ROOT / "agent/fexrift.py", DATA / "agent/fexrift.py.txt") # experimental Rift-on-Frame helper
|
||||
print(f"staged data in {DATA}")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,494 @@
|
||||
"""Static compatibility survey of Rift games for the experimental Rift-on-Frame mode (x86_64 GE-Proton under FEX +
|
||||
Meta's runtime + Revive, see native/fexwine/README.md). Read-only: looks at the game folders' PE files and the Unreal
|
||||
config inside paks; never runs anything, never touches a repack's own Oculus Platform files (only notes them).
|
||||
|
||||
Engine, VR APIs and the Platform SDK come from FramePort's own Rift analysis (analysis/rift.py); on top of that:
|
||||
the Unreal version (branch string, else the pak format's range), the RHI the game's config selects, the generic
|
||||
Unreal IME fix site (agent/fexrift.py find_ime_patch), anti-tamper sections and other runtime needs.
|
||||
|
||||
usage: uv run python scripts/rift_fex_survey.py [--folder DIR ...] [--out DIR]
|
||||
default input: the library's rift.* games; output: <user data>/fexrift/survey.{md,json} (holds local paths and folder
|
||||
names: keep it out of the repo)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from frameport.analysis import rift # noqa: E402
|
||||
from frameport.core import library # noqa: E402
|
||||
from frameport.core.paths import user_data_dir # noqa: E402
|
||||
|
||||
|
||||
def load_fexrift():
|
||||
spec = importlib.util.spec_from_file_location("fexrift_helper", ROOT / "agent" / "fexrift.py")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
FX = load_fexrift()
|
||||
MAX_PE = 400 << 20
|
||||
|
||||
DRM_SECTIONS = {".arch": "Denuvo/Arxan-style section", ".vmp0": "VMProtect", ".vmp1": "VMProtect", ".bind": "Steam DRM",
|
||||
".themida": "Themida", ".enigma1": "Enigma"}
|
||||
ANTICHEAT_FILES = ("easyanticheat", "battleye", "beclient", "eac_launcher")
|
||||
|
||||
# ------------------------------------------------------------------------------------------ Unreal paks
|
||||
PAK_MAGIC = 0x5A6F12E1
|
||||
# pak format version -> the Unreal versions that write it (when the exe doesn't name its version)
|
||||
PAK_UE = {1: "4.0-4.2", 2: "4.0-4.2", 3: "4.3-4.15", 4: "4.16-4.19", 5: "4.20", 6: "4.20", 7: "4.21",
|
||||
8: "4.22-4.24", 9: "4.25", 10: "4.26", 11: "4.26-4.27"}
|
||||
|
||||
|
||||
class Reader:
|
||||
def __init__(self, buf: bytes, pos: int = 0):
|
||||
self.buf, self.p = buf, pos
|
||||
|
||||
def take(self, fmt: str):
|
||||
v = struct.unpack_from(fmt, self.buf, self.p)
|
||||
self.p += struct.calcsize(fmt)
|
||||
return v if len(v) > 1 else v[0]
|
||||
|
||||
def fstring(self) -> str:
|
||||
n = self.take("<i")
|
||||
if n < 0:
|
||||
s = self.buf[self.p:self.p - 2 * n].decode("utf-16-le", "replace")
|
||||
self.p += -2 * n
|
||||
else:
|
||||
s = self.buf[self.p:self.p + n].decode("latin-1")
|
||||
self.p += n
|
||||
return s.rstrip("\0")
|
||||
|
||||
|
||||
def entry_header_size(ver: int, compressed: bool, nblocks: int) -> int:
|
||||
"""Size of the FPakEntry copy in front of a file's data (offset, sizes, method, hash, blocks, flags, block size)."""
|
||||
return 8 + 8 + 8 + 4 + 20 + (4 + 16 * nblocks if compressed else 0) + (1 + 4 if ver >= 3 else 0)
|
||||
|
||||
|
||||
def legacy_entry(r: Reader, ver: int):
|
||||
"""A full FPakEntry: (offset, compressed size, size, method, blocks, encrypted)."""
|
||||
off, csize, usize = r.take("<qqq")
|
||||
method = r.take("<i")
|
||||
r.p += 20
|
||||
blocks, enc = [], 0
|
||||
if ver >= 3:
|
||||
if method:
|
||||
nb = r.take("<i")
|
||||
blocks = [r.take("<qq") for _ in range(nb)]
|
||||
enc = r.buf[r.p]
|
||||
r.p += 1 + 4
|
||||
return off, csize, usize, method, blocks, enc
|
||||
|
||||
|
||||
def encoded_entry(buf: bytes, pos: int, ver: int):
|
||||
"""A v10+ bit-packed entry (FPakFile::DecodePakEntry); blocks relative to the entry like v7+ full entries."""
|
||||
r = Reader(buf, pos)
|
||||
v = r.take("<I")
|
||||
if v & 0x3f == 0x3f:
|
||||
r.take("<I") # compression block size (not needed to read)
|
||||
method = (v >> 23) & 0x3f
|
||||
off = r.take("<I" if v & (1 << 31) else "<Q")
|
||||
usize = r.take("<I" if v & (1 << 30) else "<Q")
|
||||
csize = r.take("<I" if v & (1 << 29) else "<Q") if method else usize
|
||||
enc = bool(v & (1 << 22))
|
||||
nb = (v >> 6) & 0xffff
|
||||
base = entry_header_size(ver, bool(method), nb)
|
||||
blocks = []
|
||||
if nb == 1 and not enc:
|
||||
blocks = [(base, base + csize)]
|
||||
elif nb:
|
||||
o = base
|
||||
for _ in range(nb):
|
||||
s = r.take("<I")
|
||||
blocks.append((o, o + s))
|
||||
o += (s + 15) & ~15 if enc else s
|
||||
return off, csize, usize, method, blocks, enc
|
||||
|
||||
|
||||
def pak_entries(path: Path):
|
||||
"""(version, mount point, {name: entry}, compression names) of an unencrypted UE4 pak, v1-v11; ValueError if the
|
||||
index can't be read (encrypted, unknown footer)."""
|
||||
with open(path, "rb") as f:
|
||||
f.seek(0, 2)
|
||||
size = f.tell()
|
||||
f.seek(max(0, size - 512))
|
||||
tail = f.read()
|
||||
i = tail.rfind(struct.pack("<I", PAK_MAGIC), 0, len(tail) - 40)
|
||||
while i >= 0 and len(tail) - i not in (44, 44 + 4 * 32, 44 + 5 * 32): # magic..end, 0/4/5 method names
|
||||
i = tail.rfind(struct.pack("<I", PAK_MAGIC), 0, i)
|
||||
if i < 0:
|
||||
raise ValueError("no pak footer")
|
||||
_, ver, ioff, isize = struct.unpack_from("<IIqq", tail, i)
|
||||
if ver >= 4 and i >= 1 and tail[i - 1] == 1:
|
||||
raise ValueError(f"pak v{ver}: index encrypted")
|
||||
names = [tail[j:j + 32].split(b"\0")[0].decode("latin-1") for j in range(i + 44, len(tail) - 31, 32)]
|
||||
if not 0 <= ioff < size or not 0 < isize <= size - ioff:
|
||||
raise ValueError(f"pak v{ver}: index outside the file")
|
||||
f.seek(ioff)
|
||||
r = Reader(f.read(isize))
|
||||
mount = r.fstring()
|
||||
count = r.take("<i")
|
||||
entries = {}
|
||||
if ver < 10:
|
||||
for _ in range(count):
|
||||
name = r.fstring()
|
||||
entries[name] = legacy_entry(r, ver)
|
||||
return ver, mount, entries, names
|
||||
r.take("<Q") # path hash seed
|
||||
if r.take("<i"):
|
||||
r.take("<qq")
|
||||
r.p += 20 # path hash index: hashes only, no names
|
||||
if not r.take("<i"):
|
||||
raise ValueError(f"pak v{ver}: no full directory index (names hashed only)")
|
||||
fdi_off, fdi_size = r.take("<qq")
|
||||
r.p += 20
|
||||
enc_size = r.take("<i")
|
||||
encoded = r.buf[r.p:r.p + enc_size]
|
||||
r.p += enc_size
|
||||
files = [legacy_entry(r, ver) for _ in range(r.take("<i"))]
|
||||
f.seek(fdi_off)
|
||||
d = Reader(f.read(fdi_size))
|
||||
for _ in range(d.take("<i")):
|
||||
folder = d.fstring()
|
||||
for _ in range(d.take("<i")):
|
||||
name, loc = d.fstring(), d.take("<i")
|
||||
entries[(folder + name).lstrip("/")] = encoded_entry(encoded, loc, ver) if loc >= 0 else files[-loc - 1]
|
||||
return ver, mount, entries, names
|
||||
|
||||
|
||||
def pak_read(path: Path, ver: int, entry, names: list[str] | None = None) -> bytes:
|
||||
off, csize, usize, method, blocks, enc = entry
|
||||
if enc:
|
||||
raise ValueError("entry encrypted")
|
||||
with open(path, "rb") as f:
|
||||
if not method:
|
||||
f.seek(off + entry_header_size(ver, False, 0))
|
||||
return f.read(usize)
|
||||
kind = (names[method - 1] if ver >= 8 and names and 0 < method <= len(names) else "zlib").lower()
|
||||
if kind not in ("zlib", "gzip"):
|
||||
raise ValueError(f"{kind}-compressed")
|
||||
out = b""
|
||||
for s, e in blocks:
|
||||
if ver >= 7:
|
||||
s, e = s + off, e + off
|
||||
f.seek(s)
|
||||
out += zlib.decompress(f.read(e - s), 31 if kind == "gzip" else 15)
|
||||
return out
|
||||
|
||||
|
||||
CONFIG_KEYS = ("r.MobileMSAA", "r.MSAACount", "r.DefaultFeature.AntiAliasing", "r.ForwardShading",
|
||||
"r.useclusteredforward", "vr.InstancedStereo", "r.MobileHDR", "DefaultGraphicsRHI", "TargetedRHIs",
|
||||
"bUseVulkan")
|
||||
|
||||
|
||||
def parse_engine_ini(text: str) -> dict:
|
||||
"""The keys survey cares about; repeated keys (+TargetedRHIs=…) become lists."""
|
||||
want = {k.lower(): k for k in CONFIG_KEYS}
|
||||
vals: dict = {}
|
||||
for line in text.splitlines():
|
||||
k, _, v = line.strip().partition("=")
|
||||
key = want.get(k.lstrip("+-.!").lower())
|
||||
if not key:
|
||||
continue
|
||||
if key == "TargetedRHIs":
|
||||
vals.setdefault(key, [])
|
||||
if k.startswith("-"):
|
||||
vals[key] = [x for x in vals[key] if x != v]
|
||||
elif v not in vals[key]:
|
||||
vals[key].append(v)
|
||||
else:
|
||||
vals[key] = v
|
||||
return vals
|
||||
|
||||
|
||||
def unreal_config(game_dir: Path) -> dict:
|
||||
"""Pak format + rendering/AA settings from the project's DefaultEngine.ini (paks, else a loose file)."""
|
||||
paks = sorted(game_dir.rglob("*.pak"), key=lambda p: -p.stat().st_size)
|
||||
notes, ver_seen = [], None
|
||||
for pak in paks[:6]:
|
||||
try:
|
||||
ver, _, entries, names = pak_entries(pak)
|
||||
except (ValueError, OSError, struct.error, IndexError) as e:
|
||||
notes.append(f"{pak.name}: {e}")
|
||||
continue
|
||||
ver_seen = ver_seen or ver
|
||||
name = next((n for n in entries if n.endswith("Config/DefaultEngine.ini")), None)
|
||||
if not name:
|
||||
continue
|
||||
try:
|
||||
text = pak_read(pak, ver, entries[name], names).decode("utf-8-sig", "replace")
|
||||
except (ValueError, zlib.error, OSError) as e:
|
||||
notes.append(f"{pak.name}: DefaultEngine.ini {e}")
|
||||
continue
|
||||
return {"pak": f"v{ver}", "config": parse_engine_ini(text), "pak_notes": notes}
|
||||
loose = next((p for p in game_dir.rglob("DefaultEngine.ini") if p.parent.name == "Config"), None)
|
||||
if loose:
|
||||
text = loose.read_text("utf-8", "replace")
|
||||
return {"pak": f"v{ver_seen}" if ver_seen else "", "config": parse_engine_ini(text),
|
||||
"pak_notes": notes + ["config from a loose DefaultEngine.ini"]}
|
||||
return {"pak": f"v{ver_seen}" if ver_seen else "unreadable", "pak_notes": notes or ["no DefaultEngine.ini found"]}
|
||||
|
||||
|
||||
RHI_NAMES = {"DefaultGraphicsRHI_DX11": "d3d11", "DefaultGraphicsRHI_DX12": "d3d12",
|
||||
"DefaultGraphicsRHI_Vulkan": "vulkan"}
|
||||
|
||||
|
||||
def unreal_rhi(cfg: dict) -> tuple[str, str]:
|
||||
"""(RHI the Windows build starts with, where that came from). Unreal 4 defaults to D3D11."""
|
||||
if cfg.get("DefaultGraphicsRHI") in RHI_NAMES:
|
||||
return RHI_NAMES[cfg["DefaultGraphicsRHI"]], "config"
|
||||
rhis = cfg.get("TargetedRHIs") or []
|
||||
if rhis and not any(r.startswith("PCD3D") for r in rhis):
|
||||
if any("VULKAN" in r for r in rhis):
|
||||
return "vulkan", "config (no D3D shaders)"
|
||||
if any(r.startswith("GLSL") for r in rhis):
|
||||
return "opengl", "config (no D3D shaders)"
|
||||
if rhis and all(r == "PCD3D_SM6" for r in rhis if r.startswith("PCD3D")):
|
||||
return "d3d12", "config (SM6 shaders only)"
|
||||
return "d3d11", "config" if rhis else "Unreal default"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------ PE checks
|
||||
def pe_files(game_dir: Path, limit: int = 600):
|
||||
out = []
|
||||
for p in game_dir.rglob("*"):
|
||||
if p.suffix.lower() in (".exe", ".dll") and p.is_file():
|
||||
out.append(p)
|
||||
if len(out) >= limit:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
_BRANCH = re.compile(r"\+\+[A-Za-z0-9_+.\-]{3,60}")
|
||||
_VER = re.compile(r"(?<![\d.])([45])\.(\d{1,2})(?![\d])")
|
||||
|
||||
|
||||
def unreal_version(data: bytes) -> str | None:
|
||||
""""4.13" etc. from Unreal's branch name (++UE4+Release-4.13, ++unreal+4.10+UE4-Studio, ++UE4+Studio-4.27; ASCII
|
||||
or UTF-16). Branches without a number (++depot+Game) give None."""
|
||||
texts = [m.group(0).decode() for m in re.finditer(_BRANCH.pattern.encode(), data)]
|
||||
key = "++".encode("utf-16-le")
|
||||
i = data.find(key)
|
||||
while i >= 0 and len(texts) < 2000:
|
||||
m = _BRANCH.match(data[i:i + 128].decode("utf-16-le", "replace"))
|
||||
if m:
|
||||
texts.append(m.group(0))
|
||||
i = data.find(key, i + 4)
|
||||
for t in texts:
|
||||
letters = sum(c.isalpha() for c in t)
|
||||
if letters < 4 or not re.search(r"ue[45]|unreal|release", t, re.I):
|
||||
continue
|
||||
m = _VER.search(t)
|
||||
if m:
|
||||
return f"{m.group(1)}.{m.group(2)}"
|
||||
return None
|
||||
|
||||
|
||||
def sections(data: bytes) -> list[str]:
|
||||
try:
|
||||
return [s[0] for s in FX.pe_sections(data)]
|
||||
except struct.error:
|
||||
return []
|
||||
|
||||
|
||||
def imports_of(p: Path) -> set[str]:
|
||||
try:
|
||||
info = rift.read_pe(p, limit=80 << 20)[0]
|
||||
return set(info.imports) | set(info.delay_imports)
|
||||
except (rift.PEError, OSError, struct.error, ValueError):
|
||||
return set()
|
||||
|
||||
|
||||
GRAPHICS = {"d3d11.dll": "d3d11", "d3d12.dll": "d3d12", "opengl32.dll": "opengl", "vulkan-1.dll": "vulkan",
|
||||
"d3d9.dll": "d3d9"}
|
||||
|
||||
|
||||
def survey_game(title: str, game_dir: Path, exe_rel: str | None) -> dict:
|
||||
res = {"title": title, "folder": str(game_dir)}
|
||||
if not game_dir.is_dir():
|
||||
res["verdict"] = ("missing", "folder not found")
|
||||
return res
|
||||
exe = game_dir / exe_rel if exe_rel else None
|
||||
if not exe or not exe.is_file():
|
||||
res["verdict"] = ("unknown", "main exe not known")
|
||||
return res
|
||||
a = rift.analyze(game_dir, exe_rel)
|
||||
files = pe_files(game_dir)
|
||||
names = {p.name.lower(): p for p in files}
|
||||
info, data = rift.read_pe(exe, limit=MAX_PE)
|
||||
data = bytes(data)
|
||||
res["exe"] = exe_rel
|
||||
res["arch"] = info.machine
|
||||
res["vr"] = [x for x in a.xr.split("+") if x != "?"] or ["?"]
|
||||
res["platform_sdk"] = bool(a.extra.get("platform_sdk"))
|
||||
res["own_platform_dll"] = any(n.startswith("libovrplatform") for n in names) # noted only, never used
|
||||
imports = set(info.imports) | set(info.delay_imports)
|
||||
# engine
|
||||
if a.engine == "Unreal":
|
||||
cores = [p for n, p in names.items() if re.search(r"-core(-win64-shipping)?\.dll$", n)]
|
||||
ue = unreal_version(data) or next((v for v in map(lambda p: unreal_version(p.read_bytes()), cores) if v), None)
|
||||
cfg = unreal_config(game_dir)
|
||||
res.update(cfg)
|
||||
pak_v = int(cfg["pak"][1:]) if re.fullmatch(r"v\d+", cfg.get("pak", "")) else None
|
||||
res["ue_version"] = ue or (PAK_UE.get(pak_v) and f"{PAK_UE[pak_v]} (pak v{pak_v})") or "?"
|
||||
res["engine"] = f"Unreal {res['ue_version']}"
|
||||
rhi, src = unreal_rhi(cfg.get("config") or {})
|
||||
res["graphics"] = [rhi]
|
||||
res["graphics_from"] = src
|
||||
# Unreal IME deadlock fix: exe (monolithic), else modular ApplicationCore/Slate
|
||||
patch, why = FX.find_ime_patch(data)
|
||||
if not patch:
|
||||
for n, p in sorted(names.items()):
|
||||
if "applicationcore" in n or re.search(r"-slate(-win64-shipping)?\.dll$", n):
|
||||
p2, why2 = FX.find_ime_patch(p.read_bytes())
|
||||
if p2:
|
||||
patch, why = dict(p2, module=n), None
|
||||
break
|
||||
if not why2.startswith("no Slate"):
|
||||
why = why2
|
||||
res["ime_patch"] = {"rva": hex(patch["rva"]), "module": patch.get("module", "exe")} if patch else why
|
||||
lo = (res["ue_version"].split(" ")[0].split("-")[0]).split(".")
|
||||
try:
|
||||
major_minor = (int(lo[0]), int(lo[1]))
|
||||
res["large_block_alloc"] = "FMallocBinned2 (UE>=4.18): fp_mem candidate" if major_minor >= (4, 18) \
|
||||
else "FMallocBinned (older): fp_mem likely unneeded"
|
||||
except (ValueError, IndexError):
|
||||
res["large_block_alloc"] = "?"
|
||||
else:
|
||||
for p in files: # graphics imports of the game's own DLLs (Unity players, custom engines)
|
||||
if p.parent == exe.parent and p.stat().st_size <= 80 << 20:
|
||||
imports |= imports_of(p)
|
||||
res["graphics"] = sorted({v for k, v in GRAPHICS.items() if k in imports}) or ["?"]
|
||||
res["graphics_from"] = "imports"
|
||||
if a.engine == "Unity":
|
||||
src = names.get("unityplayer.dll")
|
||||
blob = rift.read_pe(src, limit=MAX_PE)[1] if src else data
|
||||
uv = re.search(rb"(20\d\d\.\d+\.\d+[fp]\d+|5\.\d\.\d+[fp]\d+)", blob)
|
||||
res["engine"] = f"Unity {uv.group(1).decode() if uv else '?'} " + \
|
||||
("IL2CPP" if "gameassembly.dll" in names else "Mono")
|
||||
elif any(n.startswith("crysystem") for n in names) or b"CryEngine" in data or b"CRYENGINE" in data:
|
||||
res["engine"] = "CryEngine"
|
||||
else:
|
||||
res["engine"] = "custom"
|
||||
# anti-tamper / anti-cheat
|
||||
drm = sorted({DRM_SECTIONS[s] for s in sections(data) if s in DRM_SECTIONS})
|
||||
if any(any(x in n for x in ANTICHEAT_FILES) for n in names):
|
||||
drm.append("anti-cheat files")
|
||||
res["drm"] = drm
|
||||
# other runtime needs
|
||||
other = []
|
||||
if "mscoree.dll" in imports:
|
||||
other.append(".NET")
|
||||
if any(i.startswith("xaudio2") for i in imports):
|
||||
other.append("XAudio2")
|
||||
if any(i in imports for i in ("mfplat.dll", "mfreadwrite.dll")):
|
||||
other.append("Media Foundation")
|
||||
if any(n.startswith("bink") for n in names):
|
||||
other.append("Bink video")
|
||||
winrt_re = rb"W\x00i\x00n\x00d\x00o\x00w\x00s\x00\.\x00(?:[A-Z]\x00[a-zA-Z\x00\.]{4,80})"
|
||||
winrt = sorted({w.decode("utf-16-le") for w in re.findall(winrt_re, data)})[:5]
|
||||
if winrt:
|
||||
other.append("WinRT: " + ", ".join(winrt))
|
||||
res["other"] = other
|
||||
res["size_gib"] = round(sum(p.stat().st_size for p in game_dir.rglob("*") if p.is_file()) / 2**30, 1)
|
||||
res["verdict"] = verdict(res)
|
||||
return res
|
||||
|
||||
|
||||
def verdict(r: dict) -> tuple[str, str]:
|
||||
"""(likely OK | needs work | blocked, why). "likely OK" = the profile First Contact has on the Frame: x64,
|
||||
D3D11, Unreal with the IME fix site found (or an engine without that deadlock), a VR API Revive/SteamVR serve."""
|
||||
blockers, work = [], []
|
||||
if r.get("arch") not in ("x86_64", "amd64", "AMD64"):
|
||||
work.append(f"{r.get('arch')} exe: needs GE's 32-bit (wow64) path under FEX (untested)")
|
||||
g = r.get("graphics", [])
|
||||
if "d3d11" not in g:
|
||||
if "d3d12" in g:
|
||||
work.append("D3D12: needs vkd3d-proton on Turnip under FEX (untested)")
|
||||
elif "opengl" in g:
|
||||
work.append("OpenGL: wined3d on Zink (untested)")
|
||||
elif "vulkan" in g:
|
||||
work.append("Vulkan: Turnip through FEX's Vulkan thunk (untested with a game)")
|
||||
else:
|
||||
work.append(f"graphics API unclear ({', '.join(g)})")
|
||||
if "anti-cheat files" in r.get("drm", []):
|
||||
blockers.append("anti-cheat")
|
||||
elif r.get("drm"):
|
||||
work.append("anti-tamper section (" + ", ".join(r["drm"]) + "): heavier under FEX, may break")
|
||||
if r.get("vr") == ["?"]:
|
||||
work.append("VR API not detected")
|
||||
if r.get("engine", "").startswith("Unreal") and isinstance(r.get("ime_patch"), str) \
|
||||
and "older Unreal" not in r["ime_patch"]:
|
||||
work.append(f"IME fix site not found ({r['ime_patch']})")
|
||||
if r.get("engine") in ("custom", "CryEngine"):
|
||||
work.append(f"{r['engine']} engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown")
|
||||
if blockers:
|
||||
return "blocked", "; ".join(blockers + work)
|
||||
if work:
|
||||
return "needs work", "; ".join(work)
|
||||
return "likely OK", "same profile as First Contact (x64, D3D11, " + "/".join(r.get("vr", [])) + ")"
|
||||
|
||||
|
||||
def library_games():
|
||||
for g in library.games():
|
||||
if not str(g.get("package", "")).startswith("rift."):
|
||||
continue
|
||||
extra = (g.get("analysis") or {}).get("extra") or {}
|
||||
gd = g.get("game_dir") or extra.get("folder") or ""
|
||||
yield g.get("title") or g["package"], Path(gd), g.get("exe") or extra.get("exe")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--folder", action="append", default=[])
|
||||
ap.add_argument("--out", default=str(user_data_dir() / "fexrift"))
|
||||
args = ap.parse_args()
|
||||
if args.folder:
|
||||
games = []
|
||||
for f in args.folder:
|
||||
a = rift.analyze(Path(f))
|
||||
games.append((a.label, Path(f), a.extra.get("exe")))
|
||||
else:
|
||||
games = list(library_games())
|
||||
results = []
|
||||
for title, d, exe in sorted(games, key=lambda t: t[0].lower()):
|
||||
try:
|
||||
r = survey_game(title, d, exe)
|
||||
except Exception as e: # noqa: BLE001 - one broken folder shouldn't stop the survey
|
||||
r = {"title": title, "folder": str(d), "verdict": ("error", f"{type(e).__name__}: {e}")}
|
||||
results.append(r)
|
||||
print(f"{r['verdict'][0]:11s} {title}", flush=True)
|
||||
out = Path(args.out)
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
(out / "survey.json").write_text(json.dumps(results, indent=1))
|
||||
lines = ["# Rift-on-Frame static survey", "",
|
||||
"| Game | Verdict | Engine | Graphics | VR | Platform SDK | IME fix | AA config | Notes |",
|
||||
"|---|---|---|---|---|---|---|---|---|"]
|
||||
for r in results:
|
||||
cfg = r.get("config") or {}
|
||||
aa = ", ".join(f"{k}={v}" for k, v in cfg.items() if "MSAA" in k or "AntiAliasing" in k) or r.get("pak", "")
|
||||
ime = r.get("ime_patch")
|
||||
if isinstance(ime, dict):
|
||||
ime = ime["rva"] + ("" if ime.get("module") == "exe" else f" ({ime['module']})")
|
||||
else:
|
||||
ime = str(ime)[:40] if r.get("engine", "").startswith("Unreal") else "n/a"
|
||||
graphics = ", ".join(r.get("graphics", [])) + (f" ({r['graphics_from']})" if r.get("graphics_from") else "")
|
||||
lines.append(f"| {r['title']} | {r['verdict'][0]} | {r.get('engine', '')} | {graphics} | "
|
||||
f"{', '.join(r.get('vr', []))} | {'yes' if r.get('platform_sdk') else 'no'} | {ime} | {aa} | "
|
||||
f"{r['verdict'][1]} |")
|
||||
(out / "survey.md").write_text("\n".join(lines) + "\n")
|
||||
print(f"report: {out / 'survey.md'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -54,6 +54,9 @@ app.add_typer(tools_app, name="tools")
|
||||
app.add_typer(frame_app, name="frame")
|
||||
app.add_typer(pc_app, name="pc")
|
||||
app.add_typer(diag_app, name="diag")
|
||||
fex_app = typer.Typer(help="Experimental (FRAMEPORT_EXPERIMENTAL_FEX=1): Oculus Rift games with Meta's PC runtime on "
|
||||
"the Frame alone (x86_64 GE-Proton under FEX). Verified game: Oculus First Contact.")
|
||||
app.add_typer(fex_app, name="fex", hidden=True)
|
||||
|
||||
|
||||
def _show_version(value: bool):
|
||||
@@ -947,5 +950,72 @@ def _describe(exc: BaseException) -> str:
|
||||
return explain(exc)
|
||||
|
||||
|
||||
def _fex_target(frame: Optional[str]):
|
||||
from . import fexrift
|
||||
|
||||
try:
|
||||
fexrift.require_enabled()
|
||||
except RuntimeError as e:
|
||||
typer.echo(str(e), err=True)
|
||||
raise typer.Exit(2) from None
|
||||
return _target(frame)
|
||||
|
||||
|
||||
@fex_app.command("status")
|
||||
def fex_status(frame: Optional[str] = typer.Option(None, help=FRAME_HELP)):
|
||||
"""What's set up on the Frame: GE-Proton, Meta's runtime, the login, the games in the prefix."""
|
||||
t = _fex_target(frame)
|
||||
typer.echo(json.dumps(t.frame.agent("fex_status", timeout=90), indent=1))
|
||||
|
||||
|
||||
@fex_app.command("setup")
|
||||
def fex_setup(frame: Optional[str] = typer.Option(None, help=FRAME_HELP)):
|
||||
"""Upload FramePort's files, then the Frame downloads GE-Proton (x86_64) and Meta's runtime and builds the prefix
|
||||
(10-20 minutes, about 12 GiB)."""
|
||||
from . import fexrift
|
||||
|
||||
t = _fex_target(frame)
|
||||
fexrift.setup(t.frame, printing_reporter(False))
|
||||
typer.echo("setup done")
|
||||
|
||||
|
||||
@fex_app.command("import-login")
|
||||
def fex_import_login(prefix: str = typer.Argument(..., help="a Wine prefix on this PC where you signed in to Meta's "
|
||||
"app (and downloaded the game with it)"),
|
||||
app: Optional[list[str]] = typer.Option(None, "--app", help="Meta's name of a game to copy "
|
||||
"(repeatable; default: the verified games)"),
|
||||
frame: Optional[str] = typer.Option(None, help=FRAME_HELP)):
|
||||
"""Copy your Meta login and the games Meta's app downloaded into the Frame's prefix (sign-in tokens: sent over
|
||||
SSH, deleted on both sides afterwards)."""
|
||||
from pathlib import Path
|
||||
|
||||
from . import fexrift
|
||||
|
||||
t = _fex_target(frame)
|
||||
res = fexrift.import_login(t.frame, Path(prefix).expanduser(), printing_reporter(False), apps=app or None)
|
||||
typer.echo(json.dumps(res.get("imported"), indent=1))
|
||||
|
||||
|
||||
@fex_app.command("install")
|
||||
def fex_install(app_name: str = typer.Argument("oculus-first-contact", metavar="APP",
|
||||
help="Meta's name for the game (verified: oculus-first-contact)"),
|
||||
msaa: Optional[int] = typer.Option(None, help="MSAA samples for a verified game that allows it (1, 2 "
|
||||
"or 4; game default 4). UNVERIFIED: with 2 the game never "
|
||||
"rendered in two launches"),
|
||||
no_steam: bool = typer.Option(False, "--no-steam", help="don't add it to the Steam library"),
|
||||
force_unverified: bool = typer.Option(False, "--force-unverified",
|
||||
help="install a game that isn't verified with this mode "
|
||||
"(only the generic fixes; may not start)"),
|
||||
frame: Optional[str] = typer.Option(None, help=FRAME_HELP)):
|
||||
"""Make a game from the prefix playable from the Steam library (fixes, launcher, shortcut). Without
|
||||
--force-unverified an unverified game is refused with what the Frame's static checks found."""
|
||||
from . import fexrift
|
||||
|
||||
t = _fex_target(frame)
|
||||
res = fexrift.install(t.frame, printing_reporter(False), app_name, msaa=msaa, add_to_steam=not no_steam,
|
||||
force_unverified=force_unverified)
|
||||
typer.echo(json.dumps(res, indent=1))
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -0,0 +1,228 @@
|
||||
"""Experimental: Oculus Rift games that need Meta's PC runtime, on the Steam Frame alone.
|
||||
|
||||
x86_64 GE-Proton under FEX runs Meta's runtime, Revive and the game in one Wine prefix on the Frame (research and
|
||||
measurements: native/fexwine/README.md, CLAUDE.md "Rift via x86_64 Wine under FEX"). CLI only, behind
|
||||
FRAMEPORT_EXPERIMENTAL_FEX=1 (`frameport fex ...`); verified game: Oculus First Contact.
|
||||
|
||||
Steps: setup (uploads agent/fexrift.py + artifacts/fexrift + a patched Revive 3.2.0, then the Frame downloads
|
||||
GE-Proton and Meta's runtime itself) -> import-login (the Meta login and the games Meta's app downloaded, from a
|
||||
signed-in Wine prefix on this PC) -> install (per-game fixes, launcher, Steam shortcut).
|
||||
The login archive holds sign-in tokens: it is written owner-only, sent over SSH and deleted on both sides.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tarfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from .core.events import Reporter
|
||||
from .core.paths import agent_dir, artifacts_dir, user_data_dir
|
||||
|
||||
EXPERIMENT_ENV = "FRAMEPORT_EXPERIMENTAL_FEX"
|
||||
|
||||
# Revive 3.2.0's LibRevive64.dll: the visibility fixes (IsVisible / HmdMounted / HasInputFocus always true: no pauses
|
||||
# on the Frame's brief focus dips) and eye textures at 67 % of the recommended size (ovr_GetFovTextureSize divides by
|
||||
# 1.5: the Frame's GPU can't fill the full size at 72 Hz). (file offset, original bytes, new bytes)
|
||||
REVIVE_VERSION = "3.2.0"
|
||||
REVIVE_DLL_SHA256 = "ef9ffd5342bb4afad9b1c0f4fee1ca3bf32f9d3159d49dd841cc9e22f02663e4"
|
||||
REVIVE_PATCHES = [
|
||||
(0x1008c, "740d", "9090"),
|
||||
(0x122af, "15", "f1"),
|
||||
(0x122b1, "04", "03"),
|
||||
(0x151f8, "ff9030010000", "b00190909090"),
|
||||
(0x1563a, "0f94c0", "b00190"),
|
||||
(0x15807, "ff9230010000", "b00190909090"),
|
||||
]
|
||||
|
||||
# games verified with this mode: Meta app name (folder under Software/Software) -> FramePort package + title
|
||||
GAMES = {"oculus-first-contact": {"package": "rift.oculus_first_contact", "title": "Oculus First Contact"}}
|
||||
|
||||
# what a signed-in prefix contributes (relative to drive_c), tar top-level name -> path
|
||||
META = "Program Files/Meta Horizon"
|
||||
|
||||
|
||||
def enabled() -> bool:
|
||||
return os.environ.get(EXPERIMENT_ENV) == "1"
|
||||
|
||||
|
||||
def require_enabled() -> None:
|
||||
if not enabled():
|
||||
raise RuntimeError(f"Rift games with Meta's runtime on the Frame are experimental: set {EXPERIMENT_ENV}=1")
|
||||
|
||||
|
||||
def artifact_files() -> list[tuple[Path, str]]:
|
||||
"""(local file, path under the Frame's artifacts/) for everything fexrift.py needs, except Revive."""
|
||||
root = artifacts_dir() / "fexrift"
|
||||
out = [(p, p.relative_to(root).as_posix()) for p in sorted(root.rglob("*")) if p.is_file()]
|
||||
helper = agent_dir() / "fexrift.py" # release bundles carry a .txt copy (their .py files are compiled away)
|
||||
out.append((helper if helper.exists() else agent_dir() / "fexrift.py.txt", "fexrift.py"))
|
||||
return out
|
||||
|
||||
|
||||
def patch_revive(src: Path, dest: Path) -> Path:
|
||||
"""Copy Revive's runtime files to dest with REVIVE_PATCHES applied to LibRevive64.dll (Revive 3.2.0 only)."""
|
||||
from .tools import revive as revive_tool
|
||||
|
||||
dll = src / "LibRevive64.dll"
|
||||
if not dll.is_file() or hashlib.sha256(dll.read_bytes()).hexdigest() != REVIVE_DLL_SHA256:
|
||||
raise RuntimeError(f"this mode needs Revive {REVIVE_VERSION} (its LibRevive64.dll is patched byte by byte); "
|
||||
f"found another build in {src}")
|
||||
tmp = dest.with_name(dest.name + ".tmp")
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
for f in revive_tool.runtime_files(src):
|
||||
target = tmp / f.relative_to(src)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(f, target) # content only: a system Revive's files are read-only
|
||||
data = bytearray((tmp / "LibRevive64.dll").read_bytes())
|
||||
for off, old, new in REVIVE_PATCHES:
|
||||
o, n = bytes.fromhex(old), bytes.fromhex(new)
|
||||
if bytes(data[off:off + len(o)]) != o:
|
||||
raise RuntimeError(f"Revive {REVIVE_VERSION}: unexpected bytes at {off:#x}")
|
||||
data[off:off + len(n)] = n
|
||||
(tmp / "LibRevive64.dll").write_bytes(bytes(data))
|
||||
shutil.rmtree(dest, ignore_errors=True)
|
||||
tmp.replace(dest)
|
||||
return dest
|
||||
|
||||
|
||||
def setup(frame, reporter: Reporter, wait: float = 3600) -> dict:
|
||||
"""Upload fexrift.py, its binaries and the patched Revive, then run the setup on the Frame and follow it."""
|
||||
from .install import installer
|
||||
from .tools import revive as revive_tool
|
||||
|
||||
require_enabled()
|
||||
reporter.stage("Prepare the Frame")
|
||||
prep = frame.agent("fex_prepare")
|
||||
if not prep.get("fex"):
|
||||
raise RuntimeError("FEX isn't installed on the Frame (Steam app 3127680, installed with Proton for x86 games)")
|
||||
if prep["free_bytes"] < 12 << 30:
|
||||
raise RuntimeError(f"needs about 12 GiB free on the Frame, have {prep['free_bytes'] / 2**30:.1f} GiB")
|
||||
src = revive_tool.revive_dir() or revive_tool.install()
|
||||
revive = patch_revive(src, user_data_dir() / "fexrift" / "revive")
|
||||
items = [(p, rel, p.stat().st_size) for p, rel in artifact_files()]
|
||||
items += [(p, "revive/" + p.relative_to(revive).as_posix(), p.stat().st_size)
|
||||
for p in sorted(revive.rglob("*")) if p.is_file()]
|
||||
total = sum(i[2] for i in items)
|
||||
reporter.stage(f"Upload FramePort's files ({len(items)} files, {total / 2**20:.0f} MiB)")
|
||||
installer.upload_files(frame, items, prep["artifacts"], reporter, max(total, 1))
|
||||
reporter.stage("Set up GE-Proton, FEX and Meta's runtime on the Frame (10-20 minutes)")
|
||||
frame.agent("fex_setup")
|
||||
end = time.time() + wait
|
||||
last = None
|
||||
while time.time() < end:
|
||||
time.sleep(10)
|
||||
try:
|
||||
st = frame.agent("fex_status", timeout=90)
|
||||
except Exception: # noqa: BLE001 - SSH hiccups while the Frame works hard
|
||||
continue
|
||||
setup_st = st.get("setup") or {}
|
||||
if setup_st.get("step") != last and setup_st.get("step"):
|
||||
last = setup_st["step"]
|
||||
reporter.log(f"setup step: {last}")
|
||||
if setup_st.get("state") == "failed":
|
||||
raise RuntimeError(f"setup failed: {setup_st.get('error')}")
|
||||
if setup_st.get("state") == "done":
|
||||
for d in setup_st.get("done", []):
|
||||
reporter.check(f"setup: {d['step']}", True, str(d.get("result")))
|
||||
return st
|
||||
raise RuntimeError("the setup on the Frame didn't finish in time; check again with `frameport fex status`")
|
||||
|
||||
|
||||
def find_user_dir(prefix: Path) -> Path:
|
||||
users = prefix / "drive_c" / "users"
|
||||
for u in sorted(users.iterdir()) if users.is_dir() else []:
|
||||
if (u / "AppData" / "Roaming" / "Oculus" / "sessions").is_dir():
|
||||
return u
|
||||
raise RuntimeError(f"no Meta login in {prefix} (sign in with Meta's app in that prefix first)")
|
||||
|
||||
|
||||
def login_archive(prefix: Path, apps: list[str], out: Path) -> dict:
|
||||
"""tar of the login (sessions/, CoreData/), the app manifests and the given apps, from a signed-in prefix."""
|
||||
user = find_user_dir(prefix)
|
||||
meta = prefix / "drive_c" / META
|
||||
trees = {"sessions": user / "AppData" / "Roaming" / "Oculus" / "sessions", "CoreData": meta / "CoreData"}
|
||||
for name, path in trees.items():
|
||||
if not path.is_dir() or not any(path.iterdir()):
|
||||
raise RuntimeError(f"{name} missing in {prefix}: sign in with Meta's app there first")
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
old_umask = os.umask(0o077)
|
||||
apps_found = []
|
||||
try:
|
||||
with tarfile.open(out, "w") as t:
|
||||
for name, path in trees.items():
|
||||
t.add(path, arcname=name)
|
||||
manifests = meta / "Software" / "Manifests"
|
||||
for app in apps:
|
||||
for f in sorted(manifests.glob(f"{app}.json*")) if manifests.is_dir() else []:
|
||||
t.add(f, arcname=f"Manifests/{f.name}")
|
||||
game = meta / "Software" / "Software" / app
|
||||
if game.is_dir():
|
||||
t.add(game, arcname=f"Software/{app}")
|
||||
apps_found.append(app)
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
return {"archive": out, "apps": apps_found, "bytes": out.stat().st_size}
|
||||
|
||||
|
||||
def import_login(frame, prefix: Path, reporter: Reporter, apps: list[str] | None = None) -> dict:
|
||||
require_enabled()
|
||||
apps = apps or list(GAMES)
|
||||
for app in apps:
|
||||
package_for(app) # Meta's names only: they are globbed and become tar paths
|
||||
tmp =user_data_dir() / "fexrift" / "login.tar"
|
||||
try:
|
||||
reporter.stage("Pack the Meta login")
|
||||
info = login_archive(prefix, apps, tmp)
|
||||
missing = [a for a in apps if a not in info["apps"]]
|
||||
if missing:
|
||||
reporter.check("Games from the prefix", None, f"not downloaded there: {', '.join(missing)}")
|
||||
prep = frame.agent("fex_prepare")
|
||||
reporter.stage(f"Send the login ({info['bytes'] / 2**20:.0f} MiB)")
|
||||
frame.put(tmp, prep["incoming"] + "/login.tar", None)
|
||||
res = frame.agent("fex_import_login", timeout=1200)
|
||||
finally:
|
||||
tmp.unlink(missing_ok=True)
|
||||
reporter.check("Meta login on the Frame", bool((res.get("status") or {}).get("login")))
|
||||
return res
|
||||
|
||||
|
||||
def package_for(app: str) -> str:
|
||||
"""FramePort package of a Meta app name (verified games keep theirs)."""
|
||||
if app in GAMES:
|
||||
return GAMES[app]["package"]
|
||||
if not re.fullmatch(r"[a-z0-9][a-z0-9_-]+", app):
|
||||
raise ValueError(f"bad Meta app name {app!r}")
|
||||
return "rift." + app.replace("-", "_")
|
||||
|
||||
|
||||
def install(frame, reporter: Reporter, app: str = "oculus-first-contact", msaa: int | None = None,
|
||||
add_to_steam: bool = True, force_unverified: bool = False) -> dict:
|
||||
"""A game from the Frame's prefix as a Steam game. Games that aren't verified (GAMES) need force_unverified:
|
||||
they get only the generic fixes the Frame finds in their binaries (the agent refuses otherwise and reports
|
||||
its static checks)."""
|
||||
from .install import installer
|
||||
|
||||
require_enabled()
|
||||
pkg = package_for(app)
|
||||
title = GAMES.get(app, {}).get("title")
|
||||
reporter.stage(f"Install {title or app}")
|
||||
res = frame.agent("fex_install", package=pkg, app=app, title=title, msaa=msaa,
|
||||
force_unverified=force_unverified)
|
||||
for k, v in (res.get("fixes") or {}).items():
|
||||
if v is not None:
|
||||
reporter.check(f"fix: {k}", v not in ("unknown build",), str(v))
|
||||
checks = res.get("checks") or {}
|
||||
if not res.get("verified"):
|
||||
reporter.check("Verified on the Frame", None, f"not yet: {app} gets only the generic fixes")
|
||||
if checks.get("engine") == "unreal" and isinstance(checks.get("ime"), str):
|
||||
reporter.check("Unreal IME fix", None, checks["ime"])
|
||||
apis = checks.get("graphics_imports") or []
|
||||
if apis and not {"d3d11", "vulkan"} & set(apis):
|
||||
reporter.check("Graphics API", None, f"imports only {', '.join(apis)}: untested with this mode")
|
||||
if add_to_steam:
|
||||
installer.add_to_steam(frame, [pkg], reporter)
|
||||
return res
|
||||
@@ -0,0 +1,367 @@
|
||||
"""Experimental Rift-on-Frame mode (fexrift): the Frame helper, the agent commands and the PC side, without a Frame."""
|
||||
import base64
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import struct
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load(monkeypatch, tmp_path, name):
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
spec = importlib.util.spec_from_file_location(name.replace(".py", ""), ROOT / "agent" / name)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fx(monkeypatch, tmp_path):
|
||||
return load(monkeypatch, tmp_path, "fexrift.py")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------ Meta's signatures
|
||||
def _sign(payload: bytes, n: int, d: int) -> str:
|
||||
k = (n.bit_length() + 7) // 8
|
||||
m = b"\x00\x01" + b"\xff" * (k - 3 - len(payload)) + b"\x00" + payload
|
||||
return base64.b64encode(pow(int.from_bytes(m, "big"), d, n).to_bytes(k, "big")).decode()
|
||||
|
||||
|
||||
def test_rsa_public_decrypt_type1(fx):
|
||||
p, q, e = 2 ** 127 - 1, 2 ** 89 - 1, 65537 # Mersenne primes: a small test key
|
||||
n, d = p * q, pow(e, -1, (p - 1) * (q - 1))
|
||||
sig = _sign(b"abc123", n, d)
|
||||
assert fx.rsa_public_decrypt(sig, keys=[(n, e)]) == "abc123"
|
||||
other = (2 ** 61 - 1) * (2 ** 107 - 1) # wrong key -> refused
|
||||
with pytest.raises(fx.FexError):
|
||||
fx.rsa_public_decrypt(sig, keys=[(other, e)])
|
||||
|
||||
|
||||
def test_meta_keys_parse(fx):
|
||||
keys = [fx.rsa_key(k) for k in fx.META_KEYS_B64]
|
||||
assert [n.bit_length() for n, _ in keys] == [4096, 4096] and all(e == 65537 for _, e in keys)
|
||||
|
||||
|
||||
def test_dawn_manifest_order_and_keys(fx):
|
||||
m = fx.dawn_manifest({"files": {"a/b.dll": {"size": 1}}, "version": "1.2", "canonicalName": "x", "extra": 5})
|
||||
keys = list(m)
|
||||
assert keys[0] == "canonicalName" and keys.index("launchFile2D") < keys.index("version") < keys.index("files")
|
||||
assert "a\\b.dll" in m["files"]
|
||||
assert m["dlcs"] == {} and m["extra"] == 5 and list(m)[-1] == "extra"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------ game fixes
|
||||
def _pe(code_at: int, code: bytes) -> bytearray:
|
||||
"""Minimal PE: one section mapping RVA 0x1000.. to file offset 0x200.."""
|
||||
data = bytearray(0x1200)
|
||||
data[:2] = b"MZ"
|
||||
struct.pack_into("<I", data, 0x3c, 0x80)
|
||||
data[0x80:0x84] = b"PE\0\0"
|
||||
struct.pack_into("<H", data, 0x86, 1) # one section
|
||||
struct.pack_into("<H", data, 0x94, 0xf0) # optional header size
|
||||
sec = 0x80 + 24 + 0xf0
|
||||
struct.pack_into("<8sIIII", data, sec, b".text", 0x1000, 0x1000, 0x1000, 0x200)
|
||||
data[0x200 + code_at - 0x1000:0x200 + code_at - 0x1000 + len(code)] = code
|
||||
return data
|
||||
|
||||
|
||||
def test_byte_patch_states(fx, tmp_path):
|
||||
patch = {"rva": 0x1010, "old": "c7862001000001000000", "new": "c7862001000000000000"}
|
||||
exe = tmp_path / "game.exe"
|
||||
exe.write_bytes(_pe(0x1010, bytes.fromhex(patch["old"])))
|
||||
assert fx.apply_byte_patch(str(exe), patch) == "patched"
|
||||
assert exe.read_bytes()[0x210:0x21a] == bytes.fromhex(patch["new"])
|
||||
assert (tmp_path / "game.exe.orig").read_bytes()[0x210:0x21a] == bytes.fromhex(patch["old"])
|
||||
assert fx.apply_byte_patch(str(exe), patch) == "already"
|
||||
other = tmp_path / "other.exe"
|
||||
other.write_bytes(_pe(0x1010, b"\x90" * 10))
|
||||
assert fx.apply_byte_patch(str(other), patch) == "unknown build"
|
||||
assert not (tmp_path / "other.exe.orig").exists()
|
||||
|
||||
|
||||
def test_first_contact_patch_matches_the_real_instruction(fx):
|
||||
p = fx.GAMES["oculus-first-contact"]["ime_patch"]
|
||||
old, new = bytes.fromhex(p["old"]), bytes.fromhex(p["new"])
|
||||
# mov dword ptr [rsi+0x120], imm32: only the immediate changes (1 -> 0)
|
||||
assert old[:6] == new[:6] == bytes.fromhex("c78620010000") and old[6:] == (1).to_bytes(4, "little")
|
||||
assert new[6:] == bytes(4)
|
||||
|
||||
|
||||
CVAR_AT, VAR_AT = 0x1800, 0x1900
|
||||
|
||||
|
||||
def _ime_pe(form="member", value=1):
|
||||
"""Synthetic Unreal-like code: the CVar's UTF-16 name, its registration (lea r8 <variable>; lea rdx <name>) and
|
||||
for the member form the constructor's `mov dword ptr [rsi+0x120], value` before it."""
|
||||
code = bytearray(0x1000)
|
||||
|
||||
def put(rva, b):
|
||||
code[rva - 0x1000:rva - 0x1000 + len(b)] = b
|
||||
put(CVAR_AT, ("Slate.DeferWindowsMessageProcessing" + "\0").encode("utf-16-le"))
|
||||
if form == "member":
|
||||
put(0x1100, bytes.fromhex("c78620010000") + value.to_bytes(4, "little"))
|
||||
put(0x1200, bytes.fromhex("4c8d8620010000")) # lea r8, [rsi+0x120]
|
||||
elif form == "global":
|
||||
put(VAR_AT, value.to_bytes(4, "little"))
|
||||
put(0x1200, bytes.fromhex("4c8d05") + struct.pack("<i", VAR_AT - 0x1207)) # lea r8, [rip+var]
|
||||
if form != "unreferenced":
|
||||
put(0x1207, bytes.fromhex("488d15") + struct.pack("<i", CVAR_AT - 0x120e)) # lea rdx, [rip+name]
|
||||
return _pe(0x1000, bytes(code))
|
||||
|
||||
|
||||
def test_find_ime_patch_forms(fx, tmp_path):
|
||||
patch, why = fx.find_ime_patch(bytes(_ime_pe("member")))
|
||||
assert why is None and patch == {"rva": 0x1100, "old": "c7862001000001000000", "new": "c7862001000000000000"}
|
||||
patch, _ = fx.find_ime_patch(bytes(_ime_pe("global")))
|
||||
assert patch == {"rva": VAR_AT, "old": "01000000", "new": "00000000"}
|
||||
patch, _ = fx.find_ime_patch(bytes(_ime_pe("member", value=0))) # already patched: same site
|
||||
assert patch["rva"] == 0x1100
|
||||
exe = tmp_path / "g.exe"
|
||||
exe.write_bytes(_ime_pe("member", value=0))
|
||||
assert fx.apply_byte_patch(str(exe), patch) == "already"
|
||||
assert fx.find_ime_patch(bytes(_ime_pe("unreferenced")))[0] is None
|
||||
assert "no Slate" in fx.find_ime_patch(bytes(_pe(0x1000, b"\x90" * 16)))[1]
|
||||
|
||||
|
||||
def test_find_ime_patch_first_contact(fx):
|
||||
"""The real exe (FRAMEPORT_FIRST_CONTACT_EXE, original or patched): the finder lands on the known site."""
|
||||
import os
|
||||
|
||||
path = os.environ.get("FRAMEPORT_FIRST_CONTACT_EXE")
|
||||
if not path or not os.path.isfile(path):
|
||||
pytest.skip("set FRAMEPORT_FIRST_CONTACT_EXE to First Contact's TouchNUX-Win64-Shipping.exe")
|
||||
patch, why = fx.find_ime_patch(open(path, "rb").read())
|
||||
assert why is None and patch["rva"] == fx.GAMES["oculus-first-contact"]["ime_patch"]["rva"]
|
||||
|
||||
|
||||
def _meta_game(fx, app, exe_rel, manifest=True, params='-mode="vr" -x'):
|
||||
game = Path(fx.META_DIR) / "Software" / "Software" / app
|
||||
exe = game / exe_rel
|
||||
exe.parent.mkdir(parents=True)
|
||||
exe.write_bytes(_ime_pe("member"))
|
||||
if manifest:
|
||||
man = Path(fx.META_DIR) / "Software" / "Manifests"
|
||||
man.mkdir(parents=True, exist_ok=True)
|
||||
(man / f"{app}.json").write_text(json.dumps({"canonicalName": app, "launchFile": exe_rel.replace("/", "\\"),
|
||||
"launchParameters": params}))
|
||||
return exe
|
||||
|
||||
|
||||
def test_manifest_launch(fx):
|
||||
_meta_game(fx, "some-game", "Proj/Binaries/Win64/Proj-Win64-Shipping.exe")
|
||||
m = fx.manifest_launch("some-game")
|
||||
assert m == {"exe": "Proj/Binaries/Win64/Proj-Win64-Shipping.exe", "args": ["-mode=vr", "-x"],
|
||||
"title": "some-game"}
|
||||
assert fx.manifest_launch("missing") is None
|
||||
(Path(fx.META_DIR) / "Software" / "Manifests" / "bad.json").write_text(json.dumps({"launchFile": "..\\x.exe"}))
|
||||
with pytest.raises(fx.FexError):
|
||||
fx.manifest_launch("bad")
|
||||
assert fx.unreal_project("Proj/Binaries/Win64/P.exe") == "Proj" and fx.unreal_project("Game.exe") is None
|
||||
|
||||
|
||||
def test_install_unverified_needs_force(fx, tmp_path):
|
||||
exe = _meta_game(fx, "some-game", "Proj/Binaries/Win64/Proj-Win64-Shipping.exe")
|
||||
anchor = str(tmp_path / "anchor")
|
||||
with pytest.raises(fx.FexError, match="force_unverified"):
|
||||
fx.cmd_install({"app": "some-game", "anchor": anchor})
|
||||
assert not (exe.parent / (exe.name + ".orig")).exists() # refused before anything changed
|
||||
res = fx.cmd_install({"app": "some-game", "anchor": anchor, "force_unverified": True, "msaa": 2})
|
||||
assert res["verified"] is False and res["ime_patch"] == "patched" and "msaa" not in res # msaa: per game only
|
||||
assert res["checks"]["engine"] == "unreal" and res["checks"]["ime"]["rva"] == 0x1100
|
||||
text = (Path(anchor) / "launch.sh").read_text()
|
||||
assert "Proj-Win64-Shipping.exe' -mode=vr -x" in text and "FP_MEM" not in text # fp_mem: per game only
|
||||
assert fx.cmd_install({"app": "some-game", "anchor": anchor, "force_unverified": True})["ime_patch"] == "already"
|
||||
|
||||
|
||||
def test_install_verified_uses_the_finder(fx, tmp_path):
|
||||
_meta_game(fx, "oculus-first-contact", fx.GAMES["oculus-first-contact"]["exe"], manifest=False)
|
||||
res = fx.cmd_install({"app": "oculus-first-contact", "anchor": str(tmp_path / "a"), "msaa": 2})
|
||||
assert res["verified"] and res["ime_patch"] == "patched" and res["msaa"] == 2 # the finder's site, not 0x22a605
|
||||
ini = Path(fx.USER_DIR) / "AppData/Local/TouchNUX/Saved/Config/WindowsNoEditor/Engine.ini"
|
||||
assert "r.MobileMSAA=2" in ini.read_text()
|
||||
with pytest.raises(fx.FexError, match="no manifest"):
|
||||
fx.cmd_install({"app": "not-here", "anchor": str(tmp_path / "b"), "force_unverified": True})
|
||||
|
||||
|
||||
def test_ini_values_keep_crlf_and_replace(fx, tmp_path):
|
||||
ini = tmp_path / "Engine.ini"
|
||||
ini.write_bytes(b"[Core.System]\r\nPaths=x\r\n\r\n[SystemSettings]\r\nr.MobileMSAA=4\r\nr.Other=1\r\n")
|
||||
fx.set_ini_values(str(ini), "SystemSettings", {"r.MobileMSAA": "2"})
|
||||
fx.set_ini_values(str(ini), "/Script/Engine.RendererSettings", {"r.MobileMSAA": "2"})
|
||||
text = ini.read_bytes().decode()
|
||||
assert "\n" not in text.replace("\r\n", "")
|
||||
assert text.count("r.MobileMSAA=2") == 2 and "r.MobileMSAA=4" not in text and "r.Other=1" in text
|
||||
assert "[/Script/Engine.RendererSettings]\r\nr.MobileMSAA=2" in text
|
||||
|
||||
|
||||
def test_launcher_renders(fx, tmp_path):
|
||||
anchor = tmp_path / "anchor"
|
||||
anchor.mkdir()
|
||||
exe = fx.write_launcher("oculus-first-contact", fx.GAMES["oculus-first-contact"], str(anchor))
|
||||
text = (anchor / "launch.sh").read_text()
|
||||
assert exe.endswith("TouchNUX-Win64-Shipping.exe")
|
||||
for needle in ("flock -n 9", "FP_NATIVE_WSERVER=1", "FP_MEM=1", "FP_BIGCACHE_MB=256", "taskset -c 4-7",
|
||||
"ReviveInjector.exe", "OVRLibrarian", "wineserver\" -k", "motionSmoothingOverride",
|
||||
"-gamemode=experienceonly", "plays.log", "ENABLE_GAMESCOPE_WSI=0", "unset LD_PRELOAD"):
|
||||
assert needle in text, needle
|
||||
assert (anchor / "launch.sh").stat().st_mode & 0o100
|
||||
|
||||
|
||||
def test_login_archive_members_are_checked(fx, tmp_path):
|
||||
def tar_with(name, kind="file"):
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(fileobj=buf, mode="w") as t:
|
||||
info = tarfile.TarInfo(name)
|
||||
if kind == "sym":
|
||||
info.type, info.linkname = tarfile.SYMTYPE, "/etc/passwd"
|
||||
t.addfile(info, io.BytesIO(b"") if kind == "file" else None)
|
||||
buf.seek(0)
|
||||
return tarfile.open(fileobj=buf)
|
||||
assert [m.name for m in fx.safe_members(tar_with("sessions/a"))] == ["sessions/a"]
|
||||
for bad in ("../x", "/abs", "other/x", "sessions/../../x"):
|
||||
with pytest.raises(fx.FexError):
|
||||
list(fx.safe_members(tar_with(bad)))
|
||||
with pytest.raises(fx.FexError):
|
||||
list(fx.safe_members(tar_with("sessions/link", "sym")))
|
||||
|
||||
|
||||
def test_fixes_reg_has_meta_library_and_root(fx):
|
||||
der = (ROOT / "artifacts" / "fexrift" / "digicert-assured-id-root-ca.der").read_bytes()
|
||||
reg = fx.fixes_reg(der)
|
||||
assert "\r\n" in reg and fx.DIGICERT_ROOT_SHA1 in reg
|
||||
# Meta's default library exactly as Meta's app writes it in user.reg (lab prefix, where imported games run)
|
||||
assert f'"DefaultLibrary"="{fx.LIBRARY_GUID}"' in reg
|
||||
assert '"OriginalPath"="C:\\\\Program Files\\\\Meta Horizon\\\\Software"' in reg
|
||||
assert ('"Path"="\\\\\\\\?\\\\Volume{00000000-0000-0000-0000-000000000043}\\\\Program Files\\\\Meta Horizon'
|
||||
'\\\\Software"') in reg
|
||||
assert "Windows.Devices.WiFi.WiFiAdapter" in reg and "[HKEY_LOCAL_MACHINE\\Software\\Revive]" in reg
|
||||
|
||||
|
||||
def test_status_without_setup(fx):
|
||||
st = fx.cmd_status({})
|
||||
assert st["proton"] is False and st["prefix"] is False and st["login"] is False and st["apps"] == []
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------ agent commands
|
||||
def test_agent_fex_install_and_uninstall(monkeypatch, tmp_path):
|
||||
a = load(monkeypatch, tmp_path, "frameport_agent.py")
|
||||
game_dir = tmp_path / "pfx/drive_c/Program Files/Meta Horizon/Software/Software/oculus-first-contact"
|
||||
calls = []
|
||||
|
||||
def fake_helper(cmd, args=None, timeout=900):
|
||||
calls.append((cmd, args))
|
||||
(Path(args["anchor"])).mkdir(parents=True, exist_ok=True)
|
||||
(Path(args["anchor"]) / "launch.sh").write_text("#!/bin/sh\n")
|
||||
return {"exe": str(game_dir / "x.exe"), "game_dir": str(game_dir), "title": "Oculus First Contact",
|
||||
"launcher": args["anchor"] + "/launch.sh", "ime_patch": "patched"}
|
||||
monkeypatch.setattr(a, "fex_helper", fake_helper)
|
||||
game_dir.mkdir(parents=True)
|
||||
res = a.cmd_fex_install({"package": "rift.oculus_first_contact", "app": "oculus-first-contact", "msaa": 2})
|
||||
assert calls[0][0] == "install" and calls[0][1]["msaa"] == 2
|
||||
dep = a.deployment("rift.oculus_first_contact")
|
||||
assert dep["kind"] == "frame_fex" and dep["base"] == str(game_dir) and dep["fixes"]["ime_patch"] == "patched"
|
||||
launcher = f'"{a.ANCHORS}/rift.oculus_first_contact/launch.sh"'
|
||||
assert res["appid"] == a.shortcut_appid(launcher, "Oculus First Contact")
|
||||
assert a.shortcut_args("rift.oculus_first_contact")[4] == "PC VR on Frame"
|
||||
assert calls[0][1]["force_unverified"] is False
|
||||
with pytest.raises(a.AgentError):
|
||||
a.cmd_fex_install({"package": "rift.oculus_first_contact", "app": "../evil"})
|
||||
monkeypatch.setattr(a, "pcvr_pids", lambda base: [])
|
||||
out = a.cmd_uninstall({"package": "rift.oculus_first_contact", "keep_data": False})
|
||||
assert out["removed"] and game_dir.is_dir() # the prefix (and the game in it) stays
|
||||
assert a.deployment("rift.oculus_first_contact") is None
|
||||
|
||||
|
||||
def test_agent_fex_launch_test_refused(monkeypatch, tmp_path):
|
||||
a = load(monkeypatch, tmp_path, "frameport_agent.py")
|
||||
anchor = Path(a.ANCHORS) / "rift.oculus_first_contact"
|
||||
anchor.mkdir(parents=True)
|
||||
(anchor / "deployment.json").write_text(json.dumps({"package": "rift.oculus_first_contact", "kind": "frame_fex",
|
||||
"appid": 1, "base": str(anchor), "title": "X"}))
|
||||
with pytest.raises(a.AgentError, match="launch tests"):
|
||||
a.cmd_launch_test({"package": "rift.oculus_first_contact", "seconds": 5})
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------ PC side
|
||||
def test_gate(monkeypatch):
|
||||
from frameport import fexrift
|
||||
|
||||
monkeypatch.delenv(fexrift.EXPERIMENT_ENV, raising=False)
|
||||
assert not fexrift.enabled()
|
||||
with pytest.raises(RuntimeError, match=fexrift.EXPERIMENT_ENV):
|
||||
fexrift.require_enabled()
|
||||
monkeypatch.setenv(fexrift.EXPERIMENT_ENV, "1")
|
||||
assert fexrift.enabled()
|
||||
|
||||
|
||||
def test_package_for():
|
||||
from frameport import fexrift
|
||||
|
||||
assert fexrift.package_for("oculus-first-contact") == "rift.oculus_first_contact"
|
||||
assert fexrift.package_for("some-game") == "rift.some_game"
|
||||
for bad in ("../x", "a*", "A"):
|
||||
with pytest.raises(ValueError):
|
||||
fexrift.package_for(bad)
|
||||
|
||||
|
||||
def test_patch_revive_refuses_other_builds(tmp_path):
|
||||
from frameport import fexrift
|
||||
|
||||
src = tmp_path / "revive"
|
||||
src.mkdir()
|
||||
(src / "LibRevive64.dll").write_bytes(b"not revive 3.2.0")
|
||||
with pytest.raises(RuntimeError, match=fexrift.REVIVE_VERSION):
|
||||
fexrift.patch_revive(src, tmp_path / "out")
|
||||
|
||||
|
||||
def test_revive_patch_list_is_consistent():
|
||||
from frameport import fexrift
|
||||
|
||||
offs = [o for o, _, _ in fexrift.REVIVE_PATCHES]
|
||||
assert offs == sorted(offs)
|
||||
assert all(len(a) == len(b) for _, a, b in fexrift.REVIVE_PATCHES)
|
||||
|
||||
|
||||
def test_login_archive(tmp_path):
|
||||
from frameport import fexrift
|
||||
|
||||
pfx = tmp_path / "pfx"
|
||||
user = pfx / "drive_c/users/someone/AppData/Roaming/Oculus/sessions"
|
||||
user.mkdir(parents=True)
|
||||
(user / "token").write_text("secret")
|
||||
meta = pfx / "drive_c/Program Files/Meta Horizon"
|
||||
(meta / "CoreData").mkdir(parents=True)
|
||||
(meta / "CoreData" / "x").write_text("1")
|
||||
(meta / "Software/Manifests").mkdir(parents=True)
|
||||
(meta / "Software/Manifests/oculus-first-contact.json").write_text("{}")
|
||||
(meta / "Software/Software/oculus-first-contact/TouchNUX").mkdir(parents=True)
|
||||
(meta / "Software/Software/oculus-first-contact/TouchNUX/a.exe").write_text("MZ")
|
||||
out = tmp_path / "login.tar"
|
||||
info = fexrift.login_archive(pfx, ["oculus-first-contact", "missing-app"], out)
|
||||
assert info["apps"] == ["oculus-first-contact"]
|
||||
assert out.stat().st_mode & 0o077 == 0 # owner-only (holds sign-in tokens)
|
||||
with tarfile.open(out) as t:
|
||||
names = set(t.getnames())
|
||||
assert {"sessions/token", "CoreData/x", "Manifests/oculus-first-contact.json",
|
||||
"Software/oculus-first-contact/TouchNUX/a.exe"} <= names
|
||||
with pytest.raises(RuntimeError, match="sign in"):
|
||||
fexrift.login_archive(tmp_path / "empty", ["x"], tmp_path / "l2.tar")
|
||||
|
||||
|
||||
def test_artifact_files_listed():
|
||||
from frameport import fexrift
|
||||
|
||||
rels = {rel for _, rel in fexrift.artifact_files()}
|
||||
assert {"fexrift.py", "fp_mem.so", "wineserver.arm64", "windows.devices.wifi.dll",
|
||||
"digicert-assured-id-root-ca.der", "dlls/crypt32.dll", "dlls/sechost.dll", "dlls/dnsapi.dll",
|
||||
"dlls/windows.devices.enumeration.dll"} <= rels
|
||||
root = ROOT / "artifacts" / "fexrift"
|
||||
sums = dict(reversed(line.split(None, 1)) for line in (root / "SHA256SUMS").read_text().splitlines())
|
||||
for rel, digest in ((r.lstrip("./"), d) for r, d in sums.items()):
|
||||
assert hashlib.sha256((root / rel).read_bytes()).hexdigest() == digest, rel
|
||||
@@ -0,0 +1,119 @@
|
||||
"""scripts/rift_fex_survey.py: Unreal version strings, pak indexes (v3, v11) on synthetic paks, RHI and verdicts."""
|
||||
import importlib.util
|
||||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def sv():
|
||||
spec = importlib.util.spec_from_file_location("rift_fex_survey", ROOT / "scripts" / "rift_fex_survey.py")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
def fstr(s: str) -> bytes:
|
||||
b = s.encode("latin-1") + b"\0"
|
||||
return struct.pack("<i", len(b)) + b
|
||||
|
||||
|
||||
INI = b"[/Script/WindowsTargetPlatform.WindowsTargetSettings]\r\n+TargetedRHIs=PCD3D_SM5\r\n" \
|
||||
b"[/Script/Engine.RendererSettings]\r\nr.MobileMSAA=4\r\nr.ForwardShading=True\r\n"
|
||||
|
||||
|
||||
def legacy_entry(off: int, data: bytes) -> bytes:
|
||||
return struct.pack("<qqqi", off, len(data), len(data), 0) + bytes(20) + b"\0" + struct.pack("<I", 0)
|
||||
|
||||
|
||||
def pak_v3(path: Path):
|
||||
body = legacy_entry(0, INI) + INI
|
||||
index = fstr("../../../") + struct.pack("<i", 1) + fstr("Proj/Config/DefaultEngine.ini") + legacy_entry(0, INI)
|
||||
footer = struct.pack("<IIqq", 0x5A6F12E1, 3, len(body), len(index)) + bytes(20)
|
||||
path.write_bytes(body + index + footer)
|
||||
|
||||
|
||||
def pak_v11(path: Path, encrypted_index=False):
|
||||
"""Two files: the ini stored, a text zlib-compressed (one block), both bit-packed entries + full directory index."""
|
||||
other = b"hello " * 50
|
||||
packed = zlib.compress(other)
|
||||
head = 8 + 8 + 8 + 4 + 20 # entry copy in front of the data
|
||||
stored_at, comp_at = 0, head + 1 + 4 + len(INI)
|
||||
body = bytes(head + 1 + 4) + INI + bytes(head + 4 + 16 + 1 + 4) + packed
|
||||
flags32 = (1 << 31) | (1 << 30) | (1 << 29)
|
||||
enc = struct.pack("<III", flags32, stored_at, len(INI))
|
||||
second = len(enc)
|
||||
enc += struct.pack("<IIII", flags32 | (1 << 23) | (1 << 6), comp_at, len(other), len(packed))
|
||||
fdi = struct.pack("<i", 1) + fstr("Proj/Config/") + struct.pack("<i", 2) + \
|
||||
fstr("DefaultEngine.ini") + struct.pack("<i", 0) + fstr("notes.txt") + struct.pack("<i", second)
|
||||
fdi_off = len(body)
|
||||
index = fstr("../../../") + struct.pack("<iQii", 2, 0, 0, 1) + struct.pack("<qq", fdi_off, len(fdi)) + bytes(20) \
|
||||
+ struct.pack("<i", len(enc)) + enc + struct.pack("<i", 0)
|
||||
ioff = fdi_off + len(fdi)
|
||||
names = b"".join(n.ljust(32, b"\0") for n in (b"Zlib", b"", b"", b"", b""))
|
||||
footer = bytes(16) + bytes([1 if encrypted_index else 0]) + \
|
||||
struct.pack("<IIqq", 0x5A6F12E1, 11, ioff, len(index)) + bytes(20) + names
|
||||
path.write_bytes(body + fdi + index + footer)
|
||||
return other
|
||||
|
||||
|
||||
def test_unreal_version(sv):
|
||||
assert sv.unreal_version(b"x\0++UE4+Release-4.19\0") == "4.19"
|
||||
assert sv.unreal_version(b"\0\0" + "++UE4+Studio-4.27-CL-0".encode("utf-16-le") + b"\0\0") == "4.27"
|
||||
assert sv.unreal_version(b"\0++unreal+4.10+UE4-Studio\0") == "4.10"
|
||||
assert sv.unreal_version(b"\0++depot+Game-CL-55578\0") is None # no number in the branch
|
||||
assert sv.unreal_version(b"\0++5.6+5\0++CrossGPUFence-\0") is None
|
||||
|
||||
|
||||
def test_pak_v3(sv, tmp_path):
|
||||
pak = tmp_path / "a.pak"
|
||||
pak_v3(pak)
|
||||
ver, mount, entries, _ = sv.pak_entries(pak)
|
||||
assert ver == 3 and mount == "../../../"
|
||||
assert sv.pak_read(pak, ver, entries["Proj/Config/DefaultEngine.ini"]) == INI
|
||||
|
||||
|
||||
def test_pak_v11(sv, tmp_path):
|
||||
pak = tmp_path / "b.pak"
|
||||
other = pak_v11(pak)
|
||||
ver, _, entries, names = sv.pak_entries(pak)
|
||||
assert ver == 11 and names[0] == "Zlib"
|
||||
assert set(entries) == {"Proj/Config/DefaultEngine.ini", "Proj/Config/notes.txt"}
|
||||
assert sv.pak_read(pak, ver, entries["Proj/Config/DefaultEngine.ini"], names) == INI
|
||||
assert sv.pak_read(pak, ver, entries["Proj/Config/notes.txt"], names) == other
|
||||
cfg = sv.unreal_config(tmp_path)
|
||||
assert cfg["pak"] in ("v3", "v11") and cfg["config"]["TargetedRHIs"] == ["PCD3D_SM5"]
|
||||
enc = tmp_path / "enc" / "c.pak"
|
||||
enc.parent.mkdir()
|
||||
pak_v11(enc, encrypted_index=True)
|
||||
with pytest.raises(ValueError, match="encrypted"):
|
||||
sv.pak_entries(enc)
|
||||
assert "encrypted" in sv.unreal_config(enc.parent)["pak_notes"][0]
|
||||
|
||||
|
||||
def test_engine_ini_and_rhi(sv):
|
||||
cfg = sv.parse_engine_ini(INI.decode())
|
||||
assert cfg == {"TargetedRHIs": ["PCD3D_SM5"], "r.MobileMSAA": "4", "r.ForwardShading": "True"}
|
||||
assert sv.unreal_rhi(cfg) == ("d3d11", "config")
|
||||
assert sv.unreal_rhi({}) == ("d3d11", "Unreal default")
|
||||
assert sv.unreal_rhi({"DefaultGraphicsRHI": "DefaultGraphicsRHI_DX12"})[0] == "d3d12"
|
||||
assert sv.unreal_rhi({"TargetedRHIs": ["SF_VULKAN_SM5"]})[0] == "vulkan"
|
||||
assert sv.unreal_rhi({"TargetedRHIs": ["PCD3D_SM6"]})[0] == "d3d12"
|
||||
removed = sv.parse_engine_ini("+TargetedRHIs=PCD3D_SM5\n+TargetedRHIs=SF_VULKAN_SM5\n-TargetedRHIs=PCD3D_SM5\n")
|
||||
assert removed["TargetedRHIs"] == ["SF_VULKAN_SM5"]
|
||||
|
||||
|
||||
def test_verdict(sv):
|
||||
ok = {"arch": "x86_64", "graphics": ["d3d11"], "vr": ["LibOVR"], "drm": [], "engine": "Unreal 4.19",
|
||||
"ime_patch": {"rva": "0x1", "module": "exe"}}
|
||||
assert sv.verdict(ok)[0] == "likely OK"
|
||||
assert sv.verdict(dict(ok, graphics=["d3d12"]))[0] == "needs work"
|
||||
assert sv.verdict(dict(ok, ime_patch="CVar name found but not its reference"))[0] == "needs work"
|
||||
assert sv.verdict(dict(ok, ime_patch="no Slate.DeferWindowsMessageProcessing (older Unreal or not Unreal)"))[0] \
|
||||
== "likely OK"
|
||||
assert sv.verdict(dict(ok, engine="custom", ime_patch=None))[0] == "needs work"
|
||||
assert sv.verdict(dict(ok, drm=["anti-cheat files"]))[0] == "blocked"
|
||||
Reference in new issue
Block a user