hardware

Go the F to sleep

With respect to Samuel L. Jackson’s perfection

In the AMD Hackintosh land, successfully configuring your build to perform sleep / wake is one of the “Holly shit! It works!” moments.
Even though Intel and AMD CPUs are largely compatible (with few very notable differences), their corresponding chipsets are different story. Until recently, Apple exclusively used Intel CPUs and their chipsets (to some degree). Thus it’s not surprising that OpenCore builds feature sleep / wake pretty much out of the box if you use the identical components as per your chosen SMBIOS value.

Same with many other aspects of computing: in recent Big Sur builds, my Z490+i7-10700K+RX-5500XT build started randomly showing black screen for a second or two before getting back to normal. While harmless in general, it was super annoying and seriously disrupted day to day work of my wife (she’s using that build). Originally I used iMacPro1,1 as SMBIOS even though the hardware build was pretty much replica of iMac20,1. Lô and behold — simply changing the SMBIOS to the latter made all these black screen problems disappear.
I am sure some other solution could be found with enough poking around the ACPI but as we will see – that’s a long and painstaking road.

AMD chipsets are a different story, since they use different device components and their respective configurations and firmwares. Especially with USB controllers: X570 chipset has 3 USB controllers in itself. Coupled with the fact that manufacturers can write significantly different ACPI for the same darn thing — for some reason MSI-made boards can’t boot Monterey beyond beta 3 — and you could find yourself in quite a mess.

For almost a year now, my own build using AMD 5900X running on ASRock X570 ITX board had sleep disabled. It’s such a glorious CPU, giving you 12 cores / 24 threads running at 3.7GHz (4.9GHz boost) and costing just ~€750 with the motherboard included (try speccing that config with Intel, I dare you). But sleep…sleep was simply “a bridge too far”.

Even after I mapped my USB, added USB power properties, switched from iMacPro1,1 to MacPro7,1 SMBIOS…nah, did not budge. Machine would turn off screen but would never actually enter sleep. What’s worse, it would lock-up so heavily that nothing responded anymore. Not even power button worked so the only way to turn it off would be pull the wall power. After some half-working attempts with HibernationFixup and some random SSDTs I found online, I decided to dig in deep and learn how ACPI sleep actually works.

ACPI spec is huge and not easy to dig into. First thing is defining the goal: what exactly I am after. In desktop machines, I want to achieve S3 sleep state, which means “suspend to RAM”. S4 is hibernation or “suspend to disk” which is very useful for laptops but for desktop machines it is not. S0 is fully awake state.

After reading through Power Management section and coupled with knowledge from previous attempts — the key to the sleep was proper implementation of _PRW method. Through implementations of Power resources for Wake method, devices tell the OS what sleep states they can be put into while still keeping the ability to wake the system. It also specifies what (if any) other power resources are needed for the device to properly wake up.

It’s always useful to look at examples. So here’s typical one taken from actual MacPro DSDT table:

Method (_PRW, 0, NotSerialized)
{
    If (OSDW ())
    {
        Return (Package (0x02)
        {
            0x69, 
            0x03
        })
    }
    Else
    {
        Return (Package (0x02)
        {
            0x69, 
            0x04
        })
    }
}

OSDW is ACPI method Apple uses to check if the active OS is Darwin (macOS). So we see that return value from this method is package of two elements: first argument is the same while second is 3 for macOS, 4 for others (Windows, Linux etc).

ACPI spec tells that Package format from this method means this:

Package {
   EventInfo                      // Integer
   DeepestSleepState              // Integer
}

DeepestSleepState is an Integer that contains the deepest power system sleeping state that can be entered while still providing wake functionality.

Okie, so this device can go into S3 sleep state for macOS and S4 for others. Looking across many other _PRW implementations in MacPro ACPI, the similar pattern is present.

OK, let us now look at DSDT for my ASRock X570 motherboard ACPI. It has ~20 _PRW implementations and in all but one case that second argument is 0x04. Here’s one:

Method (_PRW, 0, NotSerialized)  // _PRW: Power Resources for Wake
{
    Return (GPRW (0x08, 0x04))
}

Taking cue from Apple’s implementation, we should override this second parameter to be 0x03. How to do that in OpenCore?

We employ trickery that experienced iOS developers will know as swizzling: replace existing API with our own, providing custom implementation. We need to hot-patch manufacturer-provided ACPI and rename existing GPRW method into something else, say XPRW.

(1) In OpenCore’s config.plist add this dictionary under ACPI/Patch:

<dict>
	<key>Base</key>
	<string></string>
	<key>BaseSkip</key>
	<integer>0</integer>
	<key>Comment</key>
	<string>GPRW to XPRW Rename</string>
	<key>Count</key>
	<integer>0</integer>
	<key>Enabled</key>
	<true/>
	<key>Find</key>
	<data>
	R1BSVwI=
	</data>
	<key>Limit</key>
	<integer>0</integer>
	<key>Mask</key>
	<data>
	</data>
	<key>OemTableId</key>
	<data>
	</data>
	<key>Replace</key>
	<data>
	WFBSVwI=
	</data>
	<key>ReplaceMask</key>
	<data>
	</data>
	<key>Skip</key>
	<integer>0</integer>
	<key>TableLength</key>
	<integer>0</integer>
	<key>TableSignature</key>
	<data>
	</data>
</dict>

Thus with this, system provided GPRW method is now called XPRW.

(2) Create custom SSDT table with new implementation of GPRW:

DefinitionBlock ("", "SSDT", 2, "ATNV", "GPRW", 0x00000000)
{
    External (XPRW, MethodObj)    // 2 Arguments

    Method (GPRW, 2, NotSerialized)
    {
        If (_OSI ("Darwin"))
        {
            If ((0x04 == Arg1))
            {
                Return (XPRW (Arg0, 0x03))
            }
        }

        Return (XPRW (Arg0, Arg1))
    }
}

In cases where second argument (Arg1) is 0x04, replace it with 0x03 while passing the first argument (Arg0) as it was. In all other cases just pass to original method implementation.

I called this SSDT-GPRW.aml and included it under ACPI/Add in config.plist.

So…is it working? I am hesitant to claim with 100% certainty but so far — it is. With keyboard directly attached to USB port, with keyboard working as Bluetooth, both across multiple attempts. My USB ports usually have external USB disk, Glorious and Logitech USB mice, Blue Yeti USB microphone and powered USB hub in the LG 27GN950 monitor. Monitor display goes from RX 570 GPU over DisplayPort.

Build goes into sleep and wakes up when mouse is moved or keyboard key is pressed. There’s still possibility something could be problematic but I am hopeful. Fingers crossed.