user@page ~/gptk-satisfactory-dlssg %

nm -g

libTheme.dylib


╰─❯ ~/libTheme.dylib (for architecture arm64e):

user@page ~/articles %

echo

$ARTICLE

Enabling DLSS-G in Satisfactory with Crossover 25 and the Apple GPTK

Initially published on 01/13/2026 (last edit on 01/13/2026)

Recently, I remembered Satisfactory, a game that I used to play on my old Windows 10 machine. Back then, it was still in Early-Access but I loved it for its simplicity, yet being a very creative game.

When the game finally reached a stable release in September 2024, there was hope on my part that it might get ported to additional platforms besides Windows (being an Unreal Engine 4 game, the groundwork for cross-platform compatibility should already be there), especially macOS. Sadly, the developers told in an interview that there are no plans to support any other desktop platforms right now: See this interview on YouTube. But: hope is not lost yet. With the rise of Asahi Linux and the inspiring work of Asahi Lina/Hoshino Lina and Alyssa Rosenzweig concerning the Apple Silicon GPUs, it became clear that the new hardware is really capable when it comes to running games.

A few remarks about gaming during the Intel Mac era

Back in the days before Apple transitioned to their arm64-based M-CPUs (Apple Silicon), gaming on a Mac was pretty much a no-go for multiple reasons:

So even if one had a Mac which on paper ticked all the boxes for minimum/recommended hardware, it usually fell apart on the software side - at least for recent games. The only option was usually to dual boot Windows (with BootCamp). Because Apple does not provide any Windows drivers for their Apple Silicon machines, this option was eventually taken away from the community.

Apple's twist on gaming

With the introduction of Apple Silicon in the Mac family, Apple also shifted its focus to enhance gaming on macOS2. One selling point from Apple (for the game-creating industry, actually) was the introduction of the Game Porting Toolkit (GPTK), a collection of tools and libraries intended to help ease the porting process from Windows to macOS. Starting from build tools (integration with Visual Studio), compatibility layers (metal-cpp) and utilites (e.g., the Metal Shader Converter), the component which sparked my interest the most was the evaluation environment for Windows games. This essentially consists of Apple's implementation of D3DMetal, a translation layer to map DirectX calls to the equivalent Metal APIs. It is usually paired with The Wine Project, mostly its paid downstream version CrossOver.

Although it is closed-source, it has one relevant advantage over the existing open-source solutions (e.g., DXMT): Since GPTK 3.0 beta and macOS Tahoe 26, Apple added the option to map NVIDIAs DLSS (including DLSS-G, i.e, frame generation) to MetalFX calls on supported GPUs.

The initial attempt

Building Wine on macOS is somewhat painful - especially, if you just want to quickly spin up a game. Some open-source options existed to ease this process (e.g., Whiskey), but most of them are currently unmaintained or ceased development altogether3, causing them to miss important upstream improvements in the various subprojects. So for simplicity, I used CrossOver and its convenient GUI, enabled D3DMetal in the settings and changed the synchronization implementation to MSync. I installed Steam, downloaded Satisfactory and gave it a try...

... it did not crash. Yay. That's a good starting point. But I noticed instantly that the resolution was somewhat off. CrossOver has an option to enable High-DPI mode, which I did indeed toggle. But after some research I noticed a post on Reddit which hinted that Wine checks if an application is High-DPI-capable before enabling this function for the app. Steam worked as expected and utilized the mode, but Satisfactory either does not expose its capability correctly or Wine's detection is flawed. But help wasn't far, as the post also provided a quick fix to force Wine to enable High-DPI mode for all applications (use it at your own risk, some apps might start doing weird things if they actually don't support High-DPI mode):

cmd.exe

eg ADD "HKCU\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers" /T REG_SZ /D "~ HIGHDPIAWARE" /F

After setting that option, Satisfactory started using the correct resolution. I ran with MTL_HUD_ENABLED=1 set, so the Metal Performance HUD told me that the translation was working correctly.

(Partly) Success?

What didn't work was the DLSS setting in Satisfactory. I could choose between AMD's FSR (performance actually increased) and Intel XeSS (absolutely horrible), but DLSS was still absent. Going back to the GPTK readme file it became obvious that reading correctly and completely can solve most problems. Exposing the DLSS capability requires another environment variable to be set: D3DM_ENABLE_METALFX=1. A restart later, DLSS indeed appeared in the list and worked right away when selected. Performance increased and the Metal Performance HUD now declared that MetalFX was performing work.

The DLSS-G curse

But: There was another issue. The checkbox for enabling Frame Generation (i.e., DLSS-G) was greyed out. I checked the game logs and noticed the following lines:

%APPDATA%/Local/FactoryGame/Saved/Logs/FactoryGame.log

[...]
[2026.01.13-14.52.13:798][  0]LogStreamlineAPI: Error: [Error]: [15-52-13][streamline][error][tid:1608][0s:163ms:677us]sl.cpp:668[operator ()] Feature 'kFeatureDLSS_G' requires GPU hardware scheduling to be enabled in the OS
[2026.01.13-14.52.13:798][  0]LogStreamlineRHI: Streamline supported by the NVIDIA D3D12 RHI in the StreamlineD3D12RHI module at runtime
[2026.01.13-14.52.13:798][  0]LogStreamlineRHI: FStreamlineRHI::PostPlatformRHICreateInit Enter
[2026.01.13-14.52.13:798][  0]LogStreamlineRHI: LoadedFeatures = kFeatureReflex (3), kFeatureDLSS_G (1000))
[2026.01.13-14.52.13:800][  0]LogStreamlineAPI: Error: [Error]: [15-52-13][streamline][error][tid:1608][0s:165ms:083us]sl.cpp:668[operator ()] Feature 'kFeatureDLSS_G' requires GPU hardware scheduling to be enabled in the OS
[2026.01.13-14.52.13:800][  0]LogStreamlineRHI: SupportedFeatures = kFeatureReflex (3))
[2026.01.13-14.52.13:800][  0]LogStreamlineRHI: FStreamlineRHI::PostPlatformRHICreateInit Leave
[2026.01.13-14.52.13:800][  0]LogStreamlineRHI: PlatformCreateStreamlineRHI Leave
[2026.01.13-14.52.13:800][  0]LogStreamlineRHI: FStreamlineRHIModule::StartupModule Leave
[...]

GPU hardware scheduling? This actually means Hardware Accelerated GPU Scheduling (HAGS): As per this Microsoft Development Blog Post, it is an advanced option in the Windows graphics settings to reduce latency and CPU usage when handling high-frequency tasks. For a moment I thought that hope was lost here, as this might be too specific and too low level for Wine to even be able to implement. While not giving me a definitive answer, internet research produced some results. I stumbled across several GitHub issues, which linked this feature directly to other DLSS translation layers, mostly used on Linux machines. The important part was this issue, which hinted that Wine indeed supports HAGS out of the box, but that the API which NVIDIA is using through its Streamline Integration Framework is yet to be implemented upstream (e.g., Proton already implements this correctly in their downstream fork). As the Streamline Framework is open-source, a quick look at the relevant source file exposed how HAGS support was queried for the underlying graphics adapter:

source/plugins/sl.common/commonInterface.cpp

// Check HWS for this LUID if NVDA adapter
if (!ctx.sysCaps.hwsSupported && enumAdapters2.NumAdapters > 0 && pfnQueryAdapterInfo)
{
    for (uint32_t k = 0; k < enumAdapters2.NumAdapters; k++)
    {
        if (adapterInfo[k].AdapterLuid.HighPart == desc.AdapterLuid.HighPart &&
            adapterInfo[k].AdapterLuid.LowPart == desc.AdapterLuid.LowPart)
        {
            D3DKMT_QUERYADAPTERINFO info{};
            info.hAdapter = adapterInfo[k].hAdapter;
            info.Type = KMTQAITYPE_WDDM_2_7_CAPS;
            D3DKMT_WDDM_2_7_CAPS data{};
            info.pPrivateDriverData = &data;
            info.PrivateDriverDataSize = sizeof(data);
            NTSTATUS err = pfnQueryAdapterInfo(&info);
            if (NT_SUCCESS(err) && data.HwSchEnabled)
            {
                ctx.sysCaps.hwsSupported = true;
            }
            break;
        }
    }
}

Besides some sanity checks, the important bits are D3DKMT_WDDM_2_7_CAPS and pfnQueryAdapterInfo(&info), where pfnQueryAdapterInfo = (PFND3DKMT_QUERYADAPTERINFO)GetProcAddress(modGDI32, "D3DKMTQueryAdapterInfo");. TL;DR: Streamline calls into the Windows graphics device interface library (GDI32.dll) and queries the OS if HAGS is (1) enabled and (2) supported by the adapter. So it was a matter of checking the CrossOver Wine sources and look at the implementation of D3DKMTQueryAdapterInfo. At the time of writing, the most recent (non-beta) version was 25.1.1. Scrolling through the GDI32.dll source, I noticed that D3DKMTQueryAdapterInfo was actually not implemented in that library, but instead is forwarded to another system library with a different function name: NtGdiDdDDIQueryAdapterInfo in win32u.dll. Though it appears there is no public documentation, it seems that this library is handling parts of the user interface. And indeed, the function in question is defined here:

dlls/win32u/d3dkmt.c

/******************************************************************************
 *           NtGdiDdDDIQueryAdapterInfo    (win32u.@)
 */
NTSTATUS WINAPI NtGdiDdDDIQueryAdapterInfo( D3DKMT_QUERYADAPTERINFO *desc )
{
    if (!desc) return STATUS_INVALID_PARAMETER;

    FIXME( "desc %p, type %d stub\n", desc, desc->Type );
    return STATUS_NOT_IMPLEMENTED;
}

Well... hardly surprising that the check always fails, as it doesn't even pass the first part of the if (NT_SUCCESS(err) && data.HwSchEnabled) condition. The function always returns a non-success code, so Streamline assumes that HAGS does not work at all. When Streamline checks for DLSS-G, which apparently requires HAGS to be supported, it fails:

source/core/sl.api/sl.cpp

// Now check HWS requirements only if adapter architecture is supported
if (cfg.contains("hws"))
{
    bool required = cfg["hws"]["required"];
    bool detected = cfg["hws"]["detected"];
    if (required && !detected)
    {
        SL_LOG_ERROR("Feature '%s' requires GPU hardware scheduling to be enabled in the OS", getFeatureAsStr(feature));
        return Result::eErrorOSDisabledHWS;
    }
}

An easy fix?

The first solution that comes to mind is to patch the implementation of NtGdiDdDDIQueryAdapterInfo. As Wine is open source, this should be straightforward. Except that is wasn't. I attempted to do the simplest patch possible:

dlls/win32u/d3dkmt.c

NTSTATUS WINAPI NtGdiDdDDIQueryAdapterInfo( D3DKMT_QUERYADAPTERINFO *desc )
{
    if (!desc) return STATUS_INVALID_PARAMETER;
    
    if (desc->Type == KMTQAITYPE_WDDM_2_7_CAPS)
    {
        D3DKMT_WDDM_2_7_CAPS *data = desc->pPrivateDriverData;;
        data->HwSchEnabled = 1;
        data->HwSchSupported = 1;
        data->HwSchEnabledByDefault = 1;
        return STATUS_SUCCESS;
    }

    FIXME( "desc %p, type %d stub\n", desc, desc->Type );
    return STATUS_NOT_IMPLEMENTED;
}

After recompiling and swapping out win32u.dll/win32u.so in CrossOver, Wine would always hit some kind of exception. I still don't know what caused it, but I never got it to work. I suspect that it might be some of Apple's security guards, maybe just a code signature mismatch or something in that ballpark. Even winedbg.exe would fail to start, so I gave up on patching Wine itself. I am no expert when it comes to Wine, especially its internals, and that might very well be a rookie mistake on my part that I blindly ran into.

Ghidra to the rescue

If patching the environment doesn't work, why don't do the opposite? We could just patch the Streamline API to never perform the check and always assume that HAGS is supported and enabled (an invariant that we can unconditionally confirm, because we know the hardware and the environment we are running in). So I started digging through the files that Steam shipped onto my machine. A short grep -r "requires GPU hardware scheduling to be enabled" later, I found the dynamic library which performed the check. Specifically, it is implemented in <STEAM_PREFIX>/Steam/steamapps/common/Satisfactory/FactoryGame/Plugins//Streamline/Binaries/ThirdParty/Win64/sl.interposer.dll, just as expected. I loaded it into Ghidra and quickly found where the string was referenced from (not that the offsets and names might vary based on multiple factors, most importantly the exact game build, which in my case was Build: ++FactoryGame+rel-main-1.1.0-CL-463028, Engine Version: 5.3.2-463028+++FactoryGame+rel-main-1.1.0, Compatible Engine Version: 5.3.2-463028+++FactoryGame+rel-main-1.1.0):

sl.interposer.dll (decompiled)

if ((local_378[0] == '\x01') &&
   (plVar8 = FUN_1800135e0(local_370,(longlong *)&local_388,&DAT_180059ae4), *plVar8 != *local_370
   )) {
  bVar5 = true;
}
else {
  bVar5 = false;
}
if (bVar5) {
  pcVar9 = FUN_18000e280(local_378,&DAT_180059ae4);
  pcVar9 = FUN_18000e280(pcVar9,"required");
  [... Some JSON decoding logic...]
  cVar2 = pcVar9[8];
  pcVar9 = FUN_18000e280(local_378,&DAT_180059ae4);
  pcVar9 = FUN_18000e280(pcVar9,"detected");
  [... Some JSON decoding logic...]
  if ((cVar2 == '\0') || (pcVar9[8] != '\0')) goto LAB_180007556;
  puVar10 = FUN_180036bc0();
  pcVar14 = *(code **)*puVar10;
  local_390 = FUN_180005a40(param_2);
  local_398 = "Feature \'%s\' requires GPU hardware scheduling to be enabled in the OS";
  local_3a0 = 1;
  local_3a8 = CONCAT44(local_3a8._4_4_,2);
  local_3b0 = "operator ()";
  local_3b8 = 0x29c;
  (*pcVar14)(puVar10,1,0xc,"c:\\buildagent\\work\\8799048ab5ab189f\\source\\core\\sl.api\\sl.cpp")
  ;
}

Comparing this to the code above:

source/core/sl.api/sl.cpp

// Now check HWS requirements only if adapter architecture is supported
if (cfg.contains("hws"))
{
    bool required = cfg["hws"]["required"];
    bool detected = cfg["hws"]["detected"];
    if (required && !detected)
    {
        SL_LOG_ERROR("Feature '%s' requires GPU hardware scheduling to be enabled in the OS", getFeatureAsStr(feature));
        return Result::eErrorOSDisabledHWS;
    }
}

It is safe to say we found the correct part in the binary. Now it was a matter of patching out the check. Ghidra has the ability to directly alter the instructions and one can write rather simple assembly which can replace existing code. I immedeatly noticed that the whole check is based on whether bVar5 equals 1. In x86 assembly, such check usually involves comparing a value and doing a conditional jump. That also applied to this case. To make it short:

Ghidra Assembly

TEST       AL,AL            ; 180007470 84 c0
JZ         LAB_180007556    ; 180007472 0f 84 de
                            ; 00 00 00

The code in question compares the lower 8 bits of the RAX register (AL) by bitwise AND-ing itself. So if AL == 0 the TEST instruction discards the value and sets the Zero Flag (ZF) inside the CPU to 1, if the result of the AND operation was 0. The following JZ instruction (Jump if Zero) then checks if the Zero Flag is set and performs a conditional jump in this case. As far as I am able to reconstruct this, this is the translation of the if (cfg.contains("hws")) condition.

When patching a binary, the length of the instructions to be patched must be equal or less than the instructions already in place, as we would otherwise overwrite the following instructions as well4. When looking at the bytes of this instruction (0f 84 de 00 00 00), we see that it decodes to a near conditional jump (condition is ZF=1) to an offset of de 00 00 00 (little-endian, 222 bytes forward) relative to the next instruction. Looking at Ghidra, we see that the label/offset indeed is the instruction after the if-block. I chose to replace the JZ instruction with an unconditional JMP instruction, bypassing the if-block completetly. This is sound, because any other code would essentially take this path as well. A small issue still exists, because the JZ instruction is a two bytes and the JMP instruction is one-byte encoding. Technically, this would cause the new instruction to be one byte short. However, Ghidra helped out here: It inserted 48 before the JMP instruction, which is a register extension (REX) prefix, which requests 64-bit operand size. Since that doesn't do any harm in this case (e9 JMP: Jump near, relative, RIP = RIP + 32-bit displacement sign extended to 64-bits.), we can get away with it. The final assembly now looks like this:

Ghidra Assembly

TEST       AL,AL            ; 180007470 84 c0
JMP        LAB_180007556    ; 180007472 48 e9 de
                            ; 00 00 00

And indeed, Ghidra's decompiler now removed the whole if-block from the pseudo-code. So that should be fixed, Streamline now skips the check related to HAGS and continues execution normally instead of returning early. So is everything fixed and working now?

The last hurdle: Binary Integrity

Unfortunately, when I ran Satisfactory, the DLSS-G checkbox was still disabled. And again, I turned to the logs and saw yet another problem:

[...]
[2026.01.07-15.18.42:766][  0]LogStreamlineRHI: Using Streamline production binaries from ../../../FactoryGame/Plugins/Streamline/Binaries/ThirdParty/Win64/. Can be overridden via -slbinaries={production,development,debug} command line switches for non-shipping builds
[2026.01.07-15.18.42:766][  0]LogStreamlineRHI: loading core Streamline functions from Streamline interposer at ../../../FactoryGame/Plugins/Streamline/Binaries/ThirdParty/Win64/sl.interposer.dll
[2026.01.07-15.18.42:786][  0]LogStreamlineRHI: File '..\..\..\FactoryGame\Plugins\Streamline\Binaries\ThirdParty\Win64\sl.interposer.dll' is NOT correctly signed - Streamline will not load unsecured modules for UE_BUILD_SHIPPING configurations.
[...]

As is good practice, NVIDIA also performs some binary integrity checks before loading the interposer module (we can largely ignore that this appears to be a downstream fork of Streamline by Unreal Engine, as indicated by UE_BUILD_SHIPPING). Again, I thought we were out of luck, because rebuilding the signature would be impossible for two reasons (as will become clear soon). I looked through the source of Streamline again and actually got pretty lucky: The code which loads the module and verfies the signature is also completely transparent to the user:

source/core/sl.security/secureLoadLibrary.cpp

HMODULE loadLibrary(const wchar_t* path)
{
    HMODULE mod = {};
#ifdef SL_PRODUCTION
    if (verifyEmbeddedSignature(path))
#endif
    {
        mod = LoadLibraryW(path);
    }
    return mod;
}

and

include/sl_security.h

//! See https://docs.microsoft.com/en-us/windows/win32/seccrypto/example-c-program--verifying-the-signature-of-a-pe-file
//! 
//! IMPORTANT: Always pass in the FULL PATH to the file, relative paths are NOT allowed!
bool verifyEmbeddedSignature(const wchar_t* pathToFile) {
    
    bool valid = true;

    // [... Loading some function pointers ...]
    // [... Initialize WinTrust data structures ...]

    // WinVerifyTrust verifies signatures as specified by the GUID  and Wintrust_Data.
    lStatus = pfnWinVerifyTrust(NULL, &WVTPolicyGUID, &WinTrustData);
    
    // First signature must be validated by the OS
    valid = lStatus == ERROR_SUCCESS;
    if (!valid)
    {
        printf("File '%S' is NOT correctly signed - Streamline will not load unsecured modules\n", pathToFile);
    }
    else
    {
        // Now there has to be a secondary one
        valid &= WinTrustData.pSignatureSettings->cSecondarySigs == 1;
        if (!valid)
        {
            printf("File '%S' does not have the secondary NVIDIA signature - Streamline will not load unsecured modules\n", pathToFile);
        }
        else
        {
            // The secondary signature must be from NVIDIA
            valid &= isSignedByNVIDIA(pathToFile);
            if (valid)
            {
                printf("File '%S' is signed by NVIDIA and the signature was verified.\n", pathToFile);
            }
            else
            {
                printf("File '%S' is NOT correctly signed - Streamline will not load unsecured modules\n", pathToFile);
            }
        }
    }
    
    // [... Some cleanup ...]
    
    return valid;
}

TL;DR: The binary must pass two signature checks. The first one being provided by the OS itself through the WinTrust API, the second one being a custom function written by NVIDIA which checks against a hard-coded public key (this function is also open-source!). But again: verifyEmbeddedSignature signals integrity by returning a bool. And returning a bool in x86 is as easy as setting EAX to 0x1 or 0x0, indicating either true or false. In consequence, this is another candidate to perform simple instruction patches on.

The first step is again finding the correct binary to patch. And if you watched closely, you might have seen that verifyEmbeddedSignature is defined in a header file. This means, that this function can literally be in any binary and is not a module itself - which makes sense, if you think about it. The least common trust that can be established without any checks is sharing a binary; so linking this funcion statically ensures the code calling it can generally assume that the returned values are trustworthy. But again a call to grep -r "is NOT correctly signed" quickly found some matching files, namely:

Given the fact that the logging output from the game ([2026.01.07-15.18.42:786][ 0]LogStreamlineRHI: File '..\..\..\FactoryGame\Plugins\Streamline\Binaries\ThirdParty\Win64\sl.interposer.dll' is NOT correctly signed - Streamline will not load unsecured modules for UE_BUILD_SHIPPING configurations.) mentions RHI, I started with FactoryGameSteam-StreamlineRHI-Win64-Shipping.dll. Luck struck again, because for every DLL in the containing folder, there was also an accompanying Program Database File (PDB), containing symbols and types. As Ghidra can utilize these, we get nearly all the function, class and variable names. And eventually, I found this function:

FactoryGameSteam-StreamlineRHI-Win64-Shipping.dll (decompiled)

bool __cdecl slVerifyEmbeddedSignature(FString *param_1) {
    
    // [... Lots of pseudo code ...]

    lVar1 = (*pfnWinVerifyTrust)((HWND__ *)0x0,&local_28,&local_d8);
    bVar5 = lVar1 == 0;
    if (lVar1 == 0) {
        if (*(int *)((longlong)local_88 + 0xc) == 1) {
        bVar5 = isSignedByNVIDIA((wchar_t *)FVar4);
        if (bVar5) {
            if (4 < (byte)LogStreamlineRHI._0_1_) {
            pFVar2 = &LOG_Static;
LAB_18000bca4:
            UE::Logging::Private::BasicLog((FLogCategoryBase *)&LogStreamlineRHI,pFVar2);
            }
        }
        else if (4 < (byte)LogStreamlineRHI._0_1_) {
            pFVar2 = &LOG_Static;
            goto LAB_18000bca4;
        }
        }
        else {
        bVar5 = false;
        if (4 < (byte)LogStreamlineRHI._0_1_) {
            pFVar2 = &LOG_Static;
            goto LAB_18000bca4;
        }
        }
    }    
    
    // [... Lots of pseudo code ...]
    
    return bVar5;
}

Success. This is the function we are looking for and although the string is not visible, it is referenced by the function. As we can see, the function mainly evolves around bVar5, which is read and written at various points in this function. We could patch every write to it of course, but I turned my attention to the very end of the function first - because bVar5 is also the variable which determines what is returned from this function. We remember that just want to unconditionally return true from this function, which is equivalent of setting EAX to 0x1.

Just before the callee-cleanup code we see the following assembly:

Ghidra Assembly

MOVZX   EAX,BL                              ; 18000bcdd 0f b6 c3
MOV     param_1, qword ptr [RBP]            ; 18000bce0 48 8b 4d 00
XOR     param_1, RSP                        ; 18000bce4 48 33 cc
CALL    __security_check_cookie             ; 18000bce7 e8 e4 0a | void __security_check_cookie(void)
LEA     R11, [RSP + 0x110]                  ; 18000bcec 4c 8d 9c | R11=>local_10
MOV     RBX, qword ptr [R11 + local_res8]   ; 18000bcf4 49 8b 5b 18
MOV     RSI, qword ptr [R11 + local_res10]  ; 18000bcf8 49 8b 73 20
MOV     RDI, qword ptr [R11 + local_res18]  ; 18000bcfc 49 8b 7b 28
MOV     RSP, R11                            ; 18000bd00 49 8b e3
POP     RBP                                 ; 18000bd03 5d
RET                                         ; 18000bd04 c3

Take a look at MOVZX EAX,BL. That is the part where the function sets the return value. Remember that we operate on a bool, which is 8 bit (or 1 byte) in size. For optimization reasons, the compiler (rightfully so) decided that it would be more efficient to operate only on the lower part of the register throughout the function instead of occupying the full 32-bit (or 4 byte) EAX register at all times. So before returning the value in EAX per the x86 C calling convention (cdecl), it must extend it to 32 bits (or 4 bytes). The x86 architecture provides the MOVZX instruction, which stores the final value in the first operand and extends the value from the second operand with zeroes. The equivalent of our case in a high level programming language would be something like:

Pseudocode

// EAX is 32 bit wide
uint32_t __cdecl return_bool_in_eax() {
    bool bl = true;
    return (uint32_t)bl;
}

Let's rethink our goal here. We want to have 1 in EAX eventually. So what if we disregard the value in BL completely, remove the MOVZX instruction and instead add something with the same encoded size that leaves a 1 in EAX? As we can see MOVZX EAX,BL takes up three bytes: 0f b6 c3. So we need to find something which unconditionally sets EAX to 1 in three bytes. And luckily, such an instruction exists: OR r/m32, imm8 or "Perform a bitwise inclusive OR operation between the destination (first) and source (second) operands and store the result in the destination operand location". Meaning that we can OR EAX with 0x1, which leaves a 0x1 in EAX. This works, because any OR where one of the operands is 1 will produce 1. So we patch the instruction in Ghidra, leaving us with:

Ghidra Assembly

OR         EAX,0x1                              ; 18000bcdd 83 c8 01
MOV        param_1,qword ptr [RBP]=>local_18    ; 18000bce0 48 8b 4d 00
XOR        param_1,RSP                          ; 18000bce4 48 33 cc
CALL       __security_check_cookie              ; 18000bce7 e8 e4 0a | void __security_check_cookie(void)
                                                ; 00 00
LEA        R11=>local_10,[RSP + 0x110]          ; 18000bcec 4c 8d 9c
                                                ; 24 10 01
                                                ; 00 00
MOV        RBX,qword ptr [R11 + local_res8]     ; 18000bcf4 49 8b 5b 18
MOV        RSI,qword ptr [R11 + local_res10]    ; 18000bcf8 49 8b 73 20
MOV        RDI,qword ptr [R11 + local_res18]    ; 18000bcfc 49 8b 7b 28
MOV        RSP,R11                              ; 18000bd00 49 8b e3
POP        RBP                                  ; 18000bd03 5d
RET                                             ; 18000bd04 c3

The pseudo code gets updated again:

FactoryGameSteam-StreamlineRHI-Win64-Shipping.dll (decompiled)

bool __cdecl slVerifyEmbeddedSignature(FString *param_1) {
    
    // [... Previous code still intact ...]
 
    return (bool)(bVar5 | 1);
    
}

As we can see, the return expression now unconditionally returns true. Note however, that the warning of the mismatched signatures will still occur, because we did not patch it out. The failing checks however are nondestructive now, because it does not affect the return value, which is the only condition the rest of the code cares about. So will it finally work now?

Finally: Success!

I started Satisfactory again, got no crash or exception and was greeted with an active checkbox, ready to enable DLSS-G. And yes, after actually re-checking it (there appears to be some kind of bug), MetalFX showed that it was interpolating frames. The FPS increased by 15-20 on average. I noticed no visual artifacts and given the high resolution of my MacBook Pro's display, I was able to safely set the resolution used for upscaling to 45%, which left me with 110 FPS on high settings (M4 Pro 12C/16G, 24GB Unified Memory). Not bad for a mobile CPU/GPU combination which operates on the wrong CPU architecture (arm64 v. x86_64), wrong memory page size (16K v. 4K), wrong operating system (Darwin/XNU v. Windows NT) and wrong Render API (Metal v. DirectX12).

So I guess it is safe to say that running Satisfactory natively on a Mac - without all the emulation and translation - would work beautifully, especially if the player utilizes MetalFX/DLSS-G to boost the framerates5. As the factories in this game can get pretty big and dense (which requires a fair amount of video and system memory), fast memory bandwidth is definitely a key aspect in ensuring good performance. And due to the Unified Memory of Apple Silicon, this can be utilized to its fullest potential, making this game a beneficiary of the Apple architecture.

Footnotes

  • Technically there was a conformant Vulkan translation layer (MoltenVK), which also supported a fair amount of Vulkan extensions. Performance however was usually sub-par compared to native and optimized Vulkan drivers found on Windows and/or Linux. As of late 2025, a new project which is set to replace MoltenVK has been upstreamed into Mesa: KosmicKrisp. Based on the works of Alyssa Rosenzweig, who wrote a Vulkan 1.4 conformant Vulkan driver for use in Asahi Linux, KosmicKrisp can benefit directly from the whole Mesa infrastructure (most importantly the NIR Intermediate Representation Compiler Stack). I hope that Vulkan performance and usage on macOS will increase as a result, but it must be noted that KosmicKrisp is still a translation layer atop Metal. It would be up to Apple to actually implement a native Vulkan driver, but considering their past efforts to push and establish Metal as the only API for GPU related work, this is unlikely to happen.

  • If this works out in the long run remains to be seen. Currently, I don't notice much movement in the industry to adapt the Mac as a future target platform.

  • That, however, happened for good reason. As stated by the Whiskey developer, they don't want to interfere with the downstream work carried out by CrossOver (CodeWeavers). The Wine Project greatly profits from the many patches and optimizations that CrossOver carries out. Still, this is only possible because CodeWeavers can pay developers to actually work on Wine full time, which nessesitates some kind of income that the company has to generate. So buying a CodeWeavers license essentially funds further work on Wine: If you can afford it, consider buying one! One can still profit from the patches (as CodeWeavers has to oblige to the GNU Lesser General Public License 2.1 [or later] when making changes to Wine itself) and build a custom Wine themselves. That is the beauty of FOSS.

  • Obviously there is a lot more to take into account, like restoring registers, internal flags, etc. Binary patching is always risky, as one cannot always reason about the implications in each and every case. Only perform such operation if you know what you are doing. Upholding any invariants the compiler had assumed is hard.

  • But to ensure maximum performance, remember to also set ROSETTA_ADVERTISE_AVX=1 (CrossOver will have an option for this). This ensures that Rosetta/Wine correctly expose support for Advanced Vector Extensions (AVX), which can greatly improve performance in some scenarios.

user@page ~/articles %

cd

$TOP


╰─❯ Top of the page

user@page ~/articles %

cd

$HOME


╰─❯ /