Skip to content

Instantly share code, notes, and snippets.

@osy
Last active March 10, 2024 19:52
Show Gist options
  • Save osy/c1525eb4145a4ecd420645d3a556bd35 to your computer and use it in GitHub Desktop.
Save osy/c1525eb4145a4ecd420645d3a556bd35 to your computer and use it in GitHub Desktop.
Apple Vision Pro + ROG Ally: Portable console gaming setup guide

This guide documents the journey to build a portable gaming setup using an ROG Ally and an Apple Vision Pro with low latency streaming without any external equipment. This allows for console-level gaming with a TV-sized display while traveling or in a space constrained living environment. Technical familiarity is assumed but hopefully someone can take these steps and package it into an one-click installer in the future.

Networking

The most important component that enables this build is the MT7922 Wi-Fi 6E network card on the ROG Ally that is capable of 2x2 streams. The Windows drivers allow it to configure the card to both be managed on one antenna (connect to the internet) and AP on the other (connect to the Vision Pro). Because they are independent physical interfaces, unless you bridge the connection, your internet usage will not affect the latency of the connection.

AWDL Interference

An issue with AVP currently is that low-latency streams are impossible due to interference by the AWDL service. The workaround is to set your AP to listen on channel 149, the same channel that AWDL uses, in order to prevent the service from switching channels every second.

Unfortunately, changing the channel on a Windows Hotspot is not straightforward. After some reverse engineering, I found how to do it through registry modifications. In summary you need to set the following values:

Value Data Descriptions
WfdGOOperatingChannel 149 Channel number
P2P_GO_Band 2 80MHz bandwidth
WfdPhyMode 2 802.11ax mode

Creating a loopback adapter

By default, Windows will not allow you to start the Hotspot service unless you are connected to the internet. However, there is a workaround thanks to this post.

  1. Open Device Manager
  2. Choose Action → Add legacy hardware
  3. Install the hardware that I manually select from a list (Advanced)
  4. Network adapters → Microsoft → Microsoft KM-TEST Loopback ADapter

Once the loopback device is installed, go into Network Connections and rename the new device to "Loopback". This name will be used in our scripts.

Start and stop the hotspot

In order to start and stop the hotspot, we have to use a PowerShell script that is provided below for your convenience.

Note: if you are currently connected to Wi-Fi, the hotspot will ignore WfdGOOperatingChannel and use the current channel. You must disconnect from Wi-Fi and then start the hotspot for the channel to be used (then you can turn Wi-Fi back on). Otherwise, if you control the router as well, set it to channel 149 to not worry about the order of operations.

Display

In order to save battery and to handle arbitrary resolutions, we can create a fake monitor and use that as the source for the video stream. Switching this fake monitor on will turn off the built in display and allow you to use the ROG Ally as just a controller.

This post describes how to do this which is summarized below:

  1. Download the signed driver from https://www.amyuni.com/downloads/usbmmidd_v2.zip.
  2. Install it from the Command Line: deviceinstaller64.exe install usbmmidd.inf usbmmidd
  3. To enable the fake monitor run: deviceinstaller64.exe enableidd 1
  4. To disable the fake monitor run: deviceinstaller64.exe enableidd 0

You can add support for up to 10 resolutions in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WUDF\Services\usbmmIdd\Parameters\Monitors. It is recommended that you replace the first two entries with 1194x834 and 2388x1668 in order to support the iPad Pro 11" display that is used in Vision Pro's compatibility mode.

Hotkeys

To make our lives easier, we can use AutoHotKey v2 to automate the above tasks. An AHK script is provided below which will, on Win+Y, toggle Windows Hotspot as well as the fake display. Place both .ps1 scripts and the .ahk script in the same directory.

To launch the AHK script on boot with Administrator privileges (required) follow these instructions.

You may also want to use Armory Crate to map Win+Y to a controller button such as M2+Menu for example.

Streaming

You should install Sunshine on the ROG Ally and Moonlight on the Apple Vision Pro. Once you connect to the Ally's hotspot, you can connect to the Sunshine server on 192.168.137.1.

Steam Link also works but due to a limitation on how Steam shares its IP address, it will not find 192.168.137.1 automatically. Because the app lacks the ability for you to manually specify the IP, a workaround is to use a second router, set its subnet to 192.168.137.1/24 and manually assign the Ally to be 192.168.137.1. Then connect to it once from the Vision Pro and then you can disconnect from the second router as the Steam Link app has the IP address saved in a list of known addresses. Then, the next time you launch Steam Link while on the Ally's hotspot, it will work.

Note: Since the loopback adapter is seen by Windows as a Public network, you need to make sure Windows Firewall allows Sunshine on Public networks.

Note 2: while running your streaming client on the Vision Pro, make sure you have closed the Settings app if it is open on the Wi-Fi page. Failure to do so will cause unnecessary latency spikes every 10-15 seconds while it re-scans for Wi-Fi.

; Place required files in the same directory as this script:
; - deviceinstaller64.exe from https://www.amyuni.com/downloads/usbmmidd_v2.zip
; - TurnOffHotspot.ps1 and TurnOnHotspot.ps1
#NoTrayIcon
Persistent
OnExit ExitFunc
MonitorLoadedFile := ".MonitorLoaded"
#y::
{
if FileExist(MonitorLoadedFile) {
Run 'deviceinstaller64.exe enableidd 0'
Run 'cmd.exe /c start /min "" PowerShell.exe -NoExi -ExecutionPolicy Bypass -WindowStyle Hidden -Command .\TurnOffHotspot.ps1'
FileDelete MonitorLoadedFile
} else {
Run 'deviceinstaller64.exe enableidd 1'
Run 'cmd.exe /c start /min "" PowerShell.exe -NoExi -ExecutionPolicy Bypass -WindowStyle Hidden -Command .\TurnOnHotspot.ps1'
FileObj := FileOpen(MonitorLoadedFile, "w")
FileObj.Close()
}
}
ExitFunc(ExitReason, ExitCode)
{
if FileExist(MonitorLoadedFile) {
FileDelete MonitorLoadedFile
}
}
[Windows.System.UserProfile.LockScreen,Windows.System.UserProfile,ContentType=WindowsRuntime] | Out-Null
Add-Type -AssemblyName System.Runtime.WindowsRuntime
$asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | ? { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0]
Function Await($WinRtTask, $ResultType) {
$asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
$netTask = $asTask.Invoke($null, @($WinRtTask))
$netTask.Wait(-1) | Out-Null
$netTask.Result
}
$connectionProfile = [Windows.Networking.Connectivity.NetworkInformation,Windows.Networking.Connectivity,ContentType=WindowsRuntime]::GetConnectionProfiles() | where {$_.profilename -eq "loopback"}
$tetheringManager = [Windows.Networking.NetworkOperators.NetworkOperatorTetheringManager,Windows.Networking.NetworkOperators,ContentType=WindowsRuntime]::CreateFromConnectionProfile($connectionProfile)
Await ($tetheringManager.StopTetheringAsync()) ([Windows.Networking.NetworkOperators.NetworkOperatorTetheringOperationResult])
[Windows.System.UserProfile.LockScreen,Windows.System.UserProfile,ContentType=WindowsRuntime] | Out-Null
Add-Type -AssemblyName System.Runtime.WindowsRuntime
$asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | ? { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0]
Function Await($WinRtTask, $ResultType) {
$asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
$netTask = $asTask.Invoke($null, @($WinRtTask))
$netTask.Wait(-1) | Out-Null
$netTask.Result
}
$connectionProfile = [Windows.Networking.Connectivity.NetworkInformation,Windows.Networking.Connectivity,ContentType=WindowsRuntime]::GetConnectionProfiles() | where {$_.profilename -eq "loopback"}
$tetheringManager = [Windows.Networking.NetworkOperators.NetworkOperatorTetheringManager,Windows.Networking.NetworkOperators,ContentType=WindowsRuntime]::CreateFromConnectionProfile($connectionProfile)
Await ($tetheringManager.StartTetheringAsync()) ([Windows.Networking.NetworkOperators.NetworkOperatorTetheringOperationResult])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment