Skip to content

Instantly share code, notes, and snippets.

@stratomancer
Last active March 2, 2026 11:04
Show Gist options
  • Select an option

  • Save stratomancer/c9488cdc80dfedf9e33213e17aa2dd61 to your computer and use it in GitHub Desktop.

Select an option

Save stratomancer/c9488cdc80dfedf9e33213e17aa2dd61 to your computer and use it in GitHub Desktop.
Linux Mint on MacBookPro 11.5: force Intel iGPU + power off Radeon dGPU (repro guide)

Linux Mint on MacBookPro 11,5: force Intel iGPU + power off Radeon dGPU (repro guide)

This is a guide for improving the power consumption on Linux Mint 22.2 by disabling the Radeon GPU and enabling the Intel GPU on a 2015 MacBook Pro 11,5 (Intel Haswell iGPU + AMD Radeon dGPU + Apple GMUX). It is based on the work of Bruno Bierbaumer https://github.com/0xbb/apple_set_os.efi and credits belong to Andreas Heider who originally discovered this hack: https://lists.gnu.org/archive/html/grub-devel/2013-12/msg00442.html

I simply adapted it to work on Linux Mint 22.2.

Goal:

  • Boot in a way that exposes/keeps the Intel iGPU usable (some Mac firmware disables or hides it for non-macOS boots).
  • Ensure the internal panel (eDP) is driven by i915.
  • Power off the Radeon dGPU so it stops draining the battery.

The configuration uses:

  1. rEFInd as the first-stage boot manager.
  2. A small UEFI app that calls Apple’s private apple_set_os protocol (spoofs macOS vendor/version) and then chainloads GRUB/shim.
  3. A systemd oneshot service that runs on every boot to switch to Intel and power off the dGPU via vga_switcheroo.

0) Assumptions / notes

  • You are booting in UEFI mode.
  • Your EFI System Partition (ESP) is mounted at /boot/efi.
  • You have sudo privileges.
  • Secure Boot: this guide chainloads shimx64.efi first, so it works with Secure Boot setups too (assuming shim is properly installed).

1) Install required packages

sudo apt update
sudo apt install -y git make gcc gnu-efi efibootmgr pciutils tlp

(You may already have some of these.)


2) Confirm your GPUs and that GMUX exists

lspci -nnk | awk 'BEGIN{IGNORECASE=1} /(vga|3d|display)/{p=1} p{print} /^$/{p=0}'

dmesg | egrep -i 'apple_gmux|gmux|i915|radeon|amdgpu' | tail -n 200

You should see something like:

  • Intel iGPU: 00:02.0 (i915)
  • AMD dGPU: 01:00.0 (radeon)
  • apple_gmux: Found gmux ...

3) Build the EFI wrapper: apple_set_os_grub_chain.efi

3.1 Get the apple_set_os source

mkdir -p /tmp/apple-set-os-chain
cd /tmp/apple-set-os-chain
rm -rf apple-set-os
git clone --depth 1 https://github.com/kekrby/apple-set-os.git
cd apple-set-os

3.2 Add the chainloading variant source

Create a new file apple_set_os_grub_chain.c:

cat > apple_set_os_grub_chain.c <<'EOF'
// apple_set_os_grub_chain
// 1) Call Apple's apple_set_os protocol to set OS vendor/version
// 2) Chainload GRUB/shim from the same ESP/device

#include <efibind.h>
#include <efidef.h>
#include <efidevp.h>
#include <eficon.h>
#include <efiprot.h>
#include <efiapi.h>
#include <efierr.h>

#define APPLE_SET_OS_VENDOR  "Apple Inc."
#define APPLE_SET_OS_VERSION "Mac OS X 10.9"

static EFI_GUID APPLE_SET_OS_GUID = { 0xc5c5da95, 0x7d5c, 0x45e6, { 0xb2, 0xf1, 0x3f, 0xd5, 0x2b, 0xb1, 0x00, 0x77 } };
static EFI_GUID EFI_LOADED_IMAGE_GUID = EFI_LOADED_IMAGE_PROTOCOL_GUID;
static EFI_GUID EFI_DEVICE_PATH_GUID  = EFI_DEVICE_PATH_PROTOCOL_GUID;

typedef struct _APPLE_SET_OS_INTERFACE {
    UINT64 Version;
    EFI_STATUS (EFIAPI *SetOsVersion) (IN CHAR8 *Version);
    EFI_STATUS (EFIAPI *SetOsVendor) (IN CHAR8 *Vendor);
} APPLE_SET_OS_INTERFACE;

static UINTN str_len16(const CHAR16 *s) {
    UINTN n = 0;
    while (s && s[n]) n++;
    return n;
}

static void mem_copy(void *dst, const void *src, UINTN n) {
    UINT8 *d = (UINT8 *)dst;
    const UINT8 *s = (const UINT8 *)src;
    while (n--) *d++ = *s++;
}

static void say(SIMPLE_TEXT_OUTPUT_INTERFACE *ConOut, const CHAR16 *msg) {
    if (ConOut && msg) ConOut->OutputString(ConOut, (CHAR16 *)msg);
}

static UINTN device_path_size(EFI_DEVICE_PATH_PROTOCOL *dp) {
    UINTN sz = 0;
    if (!dp) return 0;
    while (!IsDevicePathEnd(dp)) {
        sz += DevicePathNodeLength(dp);
        dp = NextDevicePathNode(dp);
    }
    sz += END_DEVICE_PATH_LENGTH;
    return sz;
}

static EFI_DEVICE_PATH_PROTOCOL *build_full_device_path(EFI_BOOT_SERVICES *BS,
                                                        EFI_DEVICE_PATH_PROTOCOL *device_dp,
                                                        const CHAR16 *file_path) {
    // Build full device path: [device_dp without END] + FILEPATH node + END

    UINTN dev_sz = device_path_size(device_dp);
    if (dev_sz < END_DEVICE_PATH_LENGTH) return NULL;

    UINTN file_path_bytes = (str_len16(file_path) + 1) * sizeof(CHAR16);
    UINTN file_node_sz = sizeof(FILEPATH_DEVICE_PATH) + file_path_bytes - sizeof(CHAR16);

    UINTN total = (dev_sz - END_DEVICE_PATH_LENGTH) + file_node_sz + END_DEVICE_PATH_LENGTH;

    EFI_DEVICE_PATH_PROTOCOL *out = NULL;
    if (EFI_ERROR(BS->AllocatePool(EfiLoaderData, total, (VOID **)&out)) || !out)
        return NULL;

    mem_copy(out, device_dp, dev_sz - END_DEVICE_PATH_LENGTH);

    FILEPATH_DEVICE_PATH *fp = (FILEPATH_DEVICE_PATH *)((UINT8 *)out + (dev_sz - END_DEVICE_PATH_LENGTH));
    fp->Header.Type = MEDIA_DEVICE_PATH;
    fp->Header.SubType = MEDIA_FILEPATH_DP;
    SetDevicePathNodeLength(&fp->Header, file_node_sz);
    mem_copy(fp->PathName, file_path, file_path_bytes);

    EFI_DEVICE_PATH_PROTOCOL *end = (EFI_DEVICE_PATH_PROTOCOL *)((UINT8 *)fp + file_node_sz);
    SetDevicePathEndNode(end);

    return out;
}

static EFI_STATUS try_chainload(EFI_HANDLE Image, EFI_SYSTEM_TABLE *SystemTable, const CHAR16 *path) {
    EFI_STATUS Status;

    EFI_LOADED_IMAGE *LoadedImage = NULL;
    Status = SystemTable->BootServices->HandleProtocol(Image, &EFI_LOADED_IMAGE_GUID, (VOID **) &LoadedImage);
    if (EFI_ERROR(Status) || LoadedImage == NULL)
        return Status;

    EFI_DEVICE_PATH_PROTOCOL *DeviceDp = NULL;
    Status = SystemTable->BootServices->HandleProtocol(LoadedImage->DeviceHandle, &EFI_DEVICE_PATH_GUID, (VOID **) &DeviceDp);
    if (EFI_ERROR(Status) || DeviceDp == NULL)
        return Status;

    EFI_DEVICE_PATH_PROTOCOL *FullDp = build_full_device_path(SystemTable->BootServices, DeviceDp, path);
    if (FullDp == NULL)
        return EFI_OUT_OF_RESOURCES;

    EFI_HANDLE ChildImage = NULL;
    Status = SystemTable->BootServices->LoadImage(FALSE, Image, FullDp, NULL, 0, &ChildImage);
    SystemTable->BootServices->FreePool(FullDp);

    if (EFI_ERROR(Status))
        return Status;

    say(SystemTable->ConOut, L"Starting: ");
    say(SystemTable->ConOut, path);
    say(SystemTable->ConOut, L"\r\n");

    Status = SystemTable->BootServices->StartImage(ChildImage, NULL, NULL);
    return Status;
}

EFI_STATUS
efi_main(EFI_HANDLE Image, EFI_SYSTEM_TABLE *SystemTable)
{
    EFI_STATUS Status;
    APPLE_SET_OS_INTERFACE *SetOs = NULL;

    say(SystemTable->ConOut, L"apple_set_os_grub_chain started\r\n");

    // Apple protocol: set vendor/version to "Mac OS X" values
    Status = SystemTable->BootServices->LocateProtocol(&APPLE_SET_OS_GUID, NULL, (VOID **) &SetOs);
    if (!EFI_ERROR(Status) && SetOs != NULL) {
        if (SetOs->Version != 0)
            SetOs->SetOsVersion((CHAR8 *) APPLE_SET_OS_VERSION);
        SetOs->SetOsVendor((CHAR8 *) APPLE_SET_OS_VENDOR);
    }

    // Try common distro paths on ESP
    const CHAR16 *targets[] = {
        L"\\EFI\\ubuntu\\shimx64.efi",
        L"\\EFI\\ubuntu\\grubx64.efi",
        L"\\EFI\\linuxmint\\shimx64.efi",
        L"\\EFI\\linuxmint\\grubx64.efi",
        L"\\EFI\\debian\\grubx64.efi",
        L"\\EFI\\BOOT\\BOOTX64.EFI",
        NULL
    };

    for (int i = 0; targets[i] != NULL; i++) {
        Status = try_chainload(Image, SystemTable, targets[i]);
        if (!EFI_ERROR(Status))
            return Status;
    }

    say(SystemTable->ConOut, L"No GRUB target could be chainloaded.\r\n");
    return EFI_NOT_FOUND;
}
EOF

3.3 Update the Makefile to build both EFI binaries

perl -0777 -i -pe 's/^TARGET\t=\s*apple_set_os\.efi\s*$/TARGETS\t= apple_set_os.efi apple_set_os_grub_chain.efi\n\n# Backward compatible single-target alias\nTARGET\t= apple_set_os.efi/m;
               s/^all:\s*\$\(TARGET\)\s*$/all: \$\(TARGETS\)/m;
               s/^clean:\n\t.*$/clean:\n\trm -f \$\(TARGETS\) *.so *.o/m;' Makefile

3.4 Build

make clean
make -j"$(nproc)"
ls -la *.efi

You should have:

  • apple_set_os.efi
  • apple_set_os_grub_chain.efi

4) Install the wrapper into the ESP

This guide installs it to: /boot/efi/EFI/tools/apple_set_os_grub_chain.efi

sudo mkdir -p /boot/efi/EFI/tools
sudo cp -f apple_set_os_grub_chain.efi /boot/efi/EFI/tools/apple_set_os_grub_chain.efi
sudo chmod 0644 /boot/efi/EFI/tools/apple_set_os_grub_chain.efi

Confirm your GRUB/shim path exists (Mint often uses EFI/ubuntu if installed in Ubuntu-compatible mode):

sudo find /boot/efi/EFI -maxdepth 3 -type f -iname '*.efi' | sed -n '1,200p'

5) Configure rEFInd to boot through the wrapper

5.1 Install rEFInd (if not already installed)

Mint package (simple):

sudo apt install -y refind
sudo refind-install

5.2 Add a rEFInd menu entry

Edit:

  • /boot/efi/EFI/refind/refind.conf

Append:

# Chainload: set Apple OS vendor/version then boot GRUB
menuentry "Apple Set OS -> GRUB" {
    icon /EFI/refind/icons/os_linux.png
    loader /EFI/tools/apple_set_os_grub_chain.efi
}

Reboot and select Apple Set OS -> GRUB.


6) Power off the Radeon dGPU after boot (persistent)

Create a systemd service:

sudo tee /etc/systemd/system/apple-set-os-dgpu-off.service >/dev/null <<'UNIT'
[Unit]
Description=Power off Radeon dGPU on dual-GPU Mac after boot (vga_switcheroo)
After=multi-user.target

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'mountpoint -q /sys/kernel/debug || mount -t debugfs none /sys/kernel/debug'
ExecStart=/bin/sh -c 'test -e /sys/kernel/debug/vgaswitcheroo/switch && echo IGD > /sys/kernel/debug/vgaswitcheroo/switch || true'
ExecStart=/bin/sh -c 'test -e /sys/kernel/debug/vgaswitcheroo/switch && echo OFF > /sys/kernel/debug/vgaswitcheroo/switch || true'
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
UNIT

sudo systemctl daemon-reload
sudo systemctl enable --now apple-set-os-dgpu-off.service

7) Verification checklist

7.1 Verify internal panel is on Intel

for s in /sys/class/drm/card*-eDP-*/status; do
  [ -r "$s" ] && echo "$(basename "$(dirname "$s")") : $(cat "$s")"
done | sort

dmesg | egrep -i 'fbcon:.*primary device|i915' | tail -n 50

Expected:

  • Intel card?-eDP-? shows connected
  • fbcon: i915drmfb (fb0) is primary device

7.2 Verify dGPU is off

sudo cat /sys/kernel/debug/vgaswitcheroo/switch

Expected:

  • DIS shows Off
  • IGD shows + and Pwr

7.3 Use TLP to verify state

sudo tlp-stat -s
sudo tlp-stat -g

TLP will show the vgaswitcheroo status.


8) Rollback / recovery

Remove the dGPU-off service

sudo systemctl disable --now apple-set-os-dgpu-off.service
sudo rm -f /etc/systemd/system/apple-set-os-dgpu-off.service
sudo systemctl daemon-reload

Remove the wrapper

sudo rm -f /boot/efi/EFI/tools/apple_set_os_grub_chain.efi

Remove the rEFInd menu entry

Edit /boot/efi/EFI/refind/refind.conf and delete the menuentry "Apple Set OS -> GRUB" { ... } stanza.

If you can’t boot

Use the Mac’s firmware boot picker (Option key) to boot your normal “Ubuntu” shim entry, or boot rEFInd and pick the default Mint/Ubuntu loader directly.


9) Notes / tuning

  • On this hardware, vgaswitcheroo shows DIS: Off even if PCI runtime PM still reads active. The important indicator is the switcheroo state and the fact that Intel eDP is connected.
  • For additional idle power tuning, use:
    • sudo tlp-stat -w (warnings)
    • sudo tlp-stat -e (PCIe runtime PM)
    • sudo tlp-stat -d (SATA ALPM)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment