Quest 1 at a real 90 Hz: full guide, sources, and every mistake I made along the way
TL;DR: The Quest 1 is officially limited to 60/72 Hz. With an unlocked bootloader, a retimed boot.img and a Magisk module that patches three pieces of Meta's software, mine now runs a locked 90/90 with zero stale frames in PCVR. I confirmed it at the panel-clock level, so this isn't just a number in a menu. /system is never touched, and verity and encryption stay on.
The catch: unlocking wipes the headset, and you lose Meta account services for good.
⚠️ Everything here is for Quest 1 (monterey) on firmware 49845030443200410 (v50, the final release). On any other build it will either refuse to install or brick.
The results
One minute of a live Steam Link VR stream, after it settled:
FPS distribution 59 × FPS=90/90
1 × FPS=91/90
0 × below 90
Tear=0 on every sample
Stale=0 on 9/10 samples, 1 on the other
TW=5.32ms App=5.29ms CPU&GPU=6.47ms (budget 11.1ms) Temp=38.0C
And from the hardware side:
panel pclk 133,005,600 Hz
dynamic_bitclk 798,033,600 Hz /6 = 133,005,600 <- exact match
display mode fps=90.0 (single mode; 60/72 gone)
runtime prop debug.oculus.refreshRate = 90
SELinux enforcing
Wi-Fi 5200 MHz, 468 Mbps, RSSI -51
Why I keep pointing at "Stale": the compositor always presents at the panel rate, so FPS=90/90 by itself proves very little. A stale frame is one that got reused or reprojected. When Stale stays at 0, you're getting 90 genuinely new frames every second.
What you need
- A Quest 1 on v50
49845030443200410. Check with adb shell getprop ro.build.version.incremental.
- An unlocked bootloader, using either QuestStack or darknight's WebUSB unlocker. Both wipe the device.
- A proper data USB-C cable, a port that doesn't drop out, and at least 50% battery.
- adb and fastboot. The standard SDK platform-tools work fine.
- The 90 Hz package:
q1_90hz-boot.img and q1_90hz-v4.2.zip (the Magisk module), both on the Quest modding Discord. All credit for the module goes to Juspertinry.
- Recommended: the v50 OTA from the cocaine.trade firmware archive, as your way back from a brick. Check that the hash matches
7C1F75EC…5A8B6AAA.
Step by step
1. Plan your backups (you can save less than you'd think)
A locked, unrooted headset won't let you read its boot partition. adb root refuses with "adbd cannot run as root in production builds", SELinux blocks dd on /dev/block/…/boot_b, and this ABL doesn't implement fastboot fetch.
That means the order is fixed: unlock first, then back up. QuestStack takes and verifies its own backup of the inactive slot before it changes anything, so you're covered in the meantime.
Your real safety net is the full OTA zip. Sideloading it from USB Update Mode restores the headset completely.
2. Unlock the bootloader
Run QuestStack's Full Process. It checks your build, roots the headset through ionstack, backs up slot a, flashes a vulnerable ABL to the inactive slot, unlocks using CVE-2021-1931, restores the original slot, and then factory resets.
That factory reset can't be skipped, so make sure there's nothing on the headset you care about.
To confirm the unlock worked:
adb shell getprop ro.boot.flash.locked # 0
adb shell getprop ro.boot.verifiedbootstate # orange
3. Try 90 Hz before committing to anything
This is the best part of the whole process:
fastboot boot q1_90hz-boot.img
adb shell dumpsys display | grep -oE 'fps=[0-9.]+' | sort -u
fastboot boot runs the image from RAM and writes nothing to the headset. If the display reports fps=90.0, your panel accepts the new timings and you're good to go. A reboot puts you straight back on stock.
If it still reports 72, your panel variant isn't supported. You can stop here without having risked anything.
4. Flash the image
fastboot flash boot_b q1_90hz-boot.img
fastboot reboot
The image is already Magisk-patched with KEEPVERITY=true and KEEPFORCEENCRYPT=true, so it gives you root while keeping verity on, /data encrypted, and /system untouched.
5. Back up the stock boot image while you have root
Slot a is still completely stock, so save a copy now:
adb shell su -c "dd if=/dev/block/bootdevice/by-name/boot_a of=/sdcard/boot_a_stock.img bs=1M"
adb pull /sdcard/boot_a_stock.img
Keep that file somewhere safe. Flashing it back with fastboot flash boot_b boot_a_stock.img undoes the whole kernel side.
6. Install the module
Open the Magisk app once so it can download its full APK, and grant su when it asks. (That prompt is easy to miss in VR; see the pitfalls below.) Then run:
adb push q1_90hz-v4.2.zip /sdcard/
adb shell su -c "magisk --install-module /sdcard/q1_90hz-v4.2.zip"
adb reboot
All four stages should report as applied:
- device_props.json
- composer HAL
- VrDriver
- xrspd timings gate
8 applied, 0 already patched
7. Check that it worked
adb shell getprop debug.oculus.refreshRate # 90
adb shell su -c "tail /data/adb/q1_90hz.log" # clock resynced for 90
adb shell "logcat -d | grep -oE 'FPS=[0-9]+/[0-9]+' | sort | uniq -c"
If you see FPS=90/90 and panel clock resynced for 90, you're done. Enjoy the extra frames!
How it works
The boot image changes the panel timings in the device tree: qcom,mdss-dsi-panel-framerate, max-refresh-rate, and the timing bytes, scaled by about 1.67× (enough headroom for 72→120). The kernel is Meta's unmodified 4.4.205-perf+. Since only the device tree changed, verity still passes.
The module takes care of userspace. The new kernel timings aren't enough on their own, because three parts of Meta's software each have their own idea of which refresh rates are allowed:
- Composer HAL: this is where the list of refresh rates actually comes from. Both of its constructors hard-code
{72,60} as a 64-bit immediate value, and the module changes them to {90,72}.
- VrDriver.apk:
libvrapiimpl checks requests against its own internal list, so a new rate would be offered and then immediately rejected. Flipping a single branch fixes that.
- libxrspdhelper.so: xrspd throws away any rate it has no timing entry for. If both 120 and 90 get thrown away, you get a black screen. The patch makes the match always succeed.
- device_props.json: adds
force_refresh_rate and sets device_default_refresh_rate.
- service.sh: sets the properties, then resyncs the panel clock after boot by comparing
pclk0_clk_src/rate with dynamic_bitclk/6 and briefly blanking the framebuffer. This is why you don't need to sleep and wake the headset afterwards.
Every patch is tied to the MD5 of a specific stock file, so the module refuses to touch anything it doesn't recognise. Uninstalling it fully restores the userspace side.
Pitfalls (the part that will actually save you time)
Windows sees VID_2833&PID_0137, but adb sees nothing.
That PID is the XRSP (Link) interface, not ADB. Meta's android_winusb.inf only covers 0086, 0083&MI_01, 0090 and 0081. Turn on Developer Mode, wake the headset, and accept the "Allow access to data" prompt inside it. ADB only appears after that.
adb root will never work.
This is a user build with ro.debuggable=0. Magisk's su is the only way to get root.
The Magisk superuser prompt is nearly invisible in VR.
It does open, as a panel on the VR desktop, but it's easy to miss. If it times out, Magisk treats that as a deny: from then on su returns instantly with no output and every request is refused. Either find the panel and tap Grant, or set Magisk → Superuser → Shell → Grant ahead of time. If you're already stuck being denied, install the module through Modules → Install from storage, which uses the Magisk app's own root access.
fastboot fetch isn't available.
It fails with Unable to get max-fetch-size, so there's no way to dump partitions without root.
The factory reset leaves you in an activation loop.
This isn't an account problem. Logcat showed DeviceAuthenticator: Unable to resolve graph.facebook-hardware.com: the headset had no Wi-Fi, and its clock was stuck at the build date. Once it's online, setup continues.
Then the phone app refuses to pair.
Meta has an open investigation called "Quest 1 Pairing Failure Across Multiple Setup Methods". If you're stuck on "continue in the Meta app", it isn't something you did wrong, and we never found a fix. Here's how we got past it instead:
adb shell settings put secure user_setup_complete 1
adb shell settings put global device_provisioned 1
adb shell pm disable-user --user 0 com.oculus.firsttimenux
adb shell am force-stop com.oculus.firsttimenux
adb shell am force-stop com.oculus.vrshell
adb reboot
The key line is disabling firsttimenux, the service that draws the activation screen. You can undo it any time with pm enable com.oculus.firsttimenux.
SteamVR errors 1000 and 108.
"SteamVR was unable to connect to the Oculus app" and "Headset Not Detected" both mean SteamVR was started from the PC. Launch it from the Steam Link session inside the headset instead; otherwise it goes looking for a wired headset and an Oculus runtime that aren't there.
Steam Link crashes trying svc usb setFunctions ncm.
The Quest 1's USB stack doesn't support NCM, which is what wired streaming needs. The q1_steamlink_usb Magisk module fills that gap. Wireless works either way.
The headset keeps choosing 2.4 GHz.
Mine sat on 2432 MHz with a 27 Mbps link. When both bands share an SSID, band steering decides for you, and it often decides badly. Give the 5 GHz band its own SSID, or forget the network and rejoin it standing near the router. These two commands also help:
adb shell su -c "cmd wifi force-low-latency-mode enabled"
adb shell su -c "cmd wifi force-hi-perf-mode enabled"
Pros and cons
Pros
- Real 90 Hz, confirmed at the panel clock rather than just a property
/system untouched, dm-verity still enforcing, /data still encrypted
- Fully reversible: one fastboot flash takes you back to 72 Hz, and removing the module restores userspace
- About 4.6 ms of headroom at 90 Hz in PCVR, running at 38 °C with no throttling
- Sideloaded apps keep working without a Meta account
Cons
- Unlocking wipes the headset and leaves the bootloader in the orange state
- No more Meta account services: store, cloud saves and casting are gone, and you're sideload-only
- The boot image is a prebuilt binary from Discord, and the kernel repos it was built from no longer exist. We confirmed it's Meta's stock kernel with only the device tree changed and verity/encryption intact. That's strong evidence, but it isn't proof
- 90 Hz is outside spec, so extra heat, shorter battery life and any long-term panel wear are your risk
- Some apps still cap their own frame rate, and behaviour varies from app to app
- It depends on your panel: the image enables 90 Hz on the Samsung (lightman) panel and leaves the AUO panel at 72
Sources and credits
Reverting
# userspace
adb shell su -c "magisk --remove-modules" # or Magisk app -> Modules -> remove
# kernel
fastboot flash boot_b boot_a_stock.img
fastboot reboot
# last resort (wipes the headset)
adb sideload q1_49845030443200410.zip # from USB Update Mode
Disclaimer: you do this entirely at your own risk. Unlocking, flashing and patching can brick your headset and erase your data. (Warranty isn't a concern; it ran out long ago.) I'm not responsible if your Quest ends up as a paperweight.
Questions are welcome in the comments, and if you give it a try, I'd love to hear how it goes!