[Перевод] Классическая Mac OS на Nintendo Wii: какие баги Apple пришлось обойти по дороге

Сегодня один из моих проектов дошёл до приятной вехи. Я портирую классическую Mac OS на PowerPC-системы с чипсетами, которые ласково называю ArtX: Nintendo GameCube, Wii, а со временем и Wii U. Интегрированный чипсет Flipper в GameCube проектировала именно ArtX: он был практически готов, когда компанию купила ATI. Vegas в Wii (и его поздняя ревизия Bollywood) и Latte в Wii U — прямые его потомки. Так вот, порт впервые дошёл до рабочего стола. Скорее всего, это первый случай со времён клонов на CHRP, когда кто-то за пределами Apple довёл классическую Mac OS на новой системе до рабочего стола. До публичного релиза ещё очень далеко, но про трудности, с которыми я уже столкнулся, рассказать хочется.
Видео: Mac OS 7.6 загружается на Wii

Но сначала: зачем вообще портировать классическую Mac OS на это железо? NT я туда, разумеется, уже портировал. Попутно у меня завелось несколько ноутбуков Mac на PowerPC, и на них я тоже в итоге портировал NT; работа над одним портом улучшала другие, и так далее. Так я познакомился с классической Mac OS и влюбился в её корявость, если можно так выразиться: хаки поверх хаков поверх хаков, тянущиеся с 1980-х, я иначе как корявостью назвать не могу. Не говоря уже о том, что m68k-часть TBXI весь период PowerPC называлась «SuperMario». Интересно, кто-то из разработчиков в начале 90-х был фанатом?
И для протокола: я начал проект 22 июня 2025 года (тогда я форкнул апстримный OpenBIOS). Никаких агентов для кода и LLM я не использовал: весь код, реверс и отладка сделаны мной самим. Я занимаюсь такими проектами ради удовольствия и чтобы учиться. С «ИИ» не было бы ни удовольствия, ни учёбы.
Первая загрузка
Начнём с самого начала загрузки. Классическая Mac OS эпохи «New World» стартует с загрузчика Trampoline, который работает в среде Open Firmware. Значит, нужно было портировать OpenBIOS. К счастью, примерно тогда же Goldfish64 начал свой порт Mac OS X на Wii и сделал порт OpenBIOS, из которого я мог взять часть драйверов. Напрямую его порт я использовать не хотел по ряду причин: он целился только в Wii и Wii U, а мне нужен был ещё и GameCube. Чтобы упростить разработку драйверов (прежде всего драйвера дисплея), я решил переиспользовать существующий код и переделал свой загрузчик ARC-прошивки от порта NT, чтобы он грузил big-endian ELF.
Для первого запуска хватало пары дисковых драйверов, поэтому я портировал свои два драйвера для устройств на шине EXI (так Nintendo и Macronix называют SPI). Первый — для SD Gecko, это просто сквозной переходник в слот карты памяти GameCube, через который SD-карта работает по SPI. Второй — для EXI-IDE. Этот драйвер, честно говоря, написан под мою неточную эмуляцию в моей ветке Dolphin, с настоящим железом он расходится: так можно тестировать в эмуляторе со всеми вытекающими возможностями низкоуровневой отладки.
С форматом данных на диске я не определился. В моём прошлом порте NT эти драйверы работали целиком через образы дисков на файловой системе FAT: нужна была совместимость с существующими homebrew-загрузчиками, которые запускают исполняемые файлы с FAT. Но у FAT32 образ ограничен 4 ГБ, а поздним версиям классической Mac OS нужно больше. Поэтому для первых тестов я просто взял файл TBXI «Mac OS ROM» и положил прямо на FAT как \ppc\bootinfo.txt, чтобы OpenBIOS загрузил его при старте.
Так у меня заработал Trampoline. Я решил накладывать как можно больше патчей на образ как можно ближе к моменту выполнения, чтобы в итоге можно было использовать почти нетронутые существующие образы компакт-дисков.
Патчи загрузчика
Первая проблема: Trampoline делает предположения о раскладке физического адресного пространства, которые для этой системы неверны. Физический адрес одного буфера вычислялся как недопустимый, поэтому я пропатчил его на адрес, который гарантированно допустим, а заодно и соответствующий код, который строит для ядра список участков памяти и записи таблицы страниц.
Кроме того, Trampoline ожидает, что физические адреса начиная с 0x8000_0000 относятся к вводу-выводу, отображённому в память (на системах, где этот код работал раньше, это было адресное пространство шины PCI). На этих системах там ничего подобного нет. На Wii U этот адрес вообще приходится на часть оперативной памяти. Сломалось и ещё одно предположение: на этих системах между областями оперативной памяти в физическом адресном пространстве есть дыры, и часть из них частично занята MMIO.
Исправил я это так: пропатчил инициализацию этого MMIO-отображения в списке участков памяти ядра, чтобы физический адрес 0x0100_0000 (середина одной из областей RAM) отображался на виртуальный 0xE000_0000 с длиной 0x1000_0000. Это отображение целиком покрывает MMIO, плюс конец одной области RAM и (на системах после GameCube) начало другой. (Возможно, позже я это поменяю: на Wii U физические адреса 0xD000_0000-0xEFFF_FFFF отображены на часть GPU Radeon R7xx под названием HDP. Через неё можно смотреть на физическую память с определённым перемешиванием (swizzling) и/или сменой порядка байтов, так что текстура в каком-нибудь GPU-специфичном формате выглядит как линейный фреймбуфер. Зависит от того, понадобится ли мне этот блок.) Функция, которая в Trampoline настраивает отображение, в какой-то момент поменялась, так что в итоге я распознаю старую версию и перехватываю функцию, чтобы обойти разницу: ранние версии использовали 16-битный счётчик страниц, поздние — 32-битный. Вдобавок старые версии Trampoline безусловно создают отображения 1:1 для физической памяти выше 0x8000_0000; их нужно убрать, потому что они конфликтуют с отображениями, настроенными раньше.
Дальше идёт набор разнородных патчей против кода, который рассчитывает на конкретное железо. Среди них: убрать установку битовых флагов, означающих наличие микроконтроллера, который есть во всех Macintosh на PowerPC; убрать попытку пересчитать частоту шины по определённому аппаратному таймеру; убрать разные записи в регистры и поправки, связанные с конкретным контроллером прерываний (он little-endian, потому что сидит на шине PCI, и здесь его, понятно, нет).
Наконец, перехватывается последний вызов, который прыгает в код инициализации загруженного ядра, чтобы позже, когда всё уже будет в памяти, наложить дополнительные патчи.
Ещё нужно немного покромсать дерево устройств, чтобы Trampoline его принял. Контроллер прерываний подделывается под один из известных ему: под тот самый little-endian из абзаца выше, потому что на уровне регистров он ближе всего к контроллеру прерываний Flipper, который есть во всех чипсетах ArtX (хотя на Wii U его немного изменили, чтобы поддержать дополнительные ядра PowerPC). Адрес фреймбуфера в дереве устройств меняется на некэшируемый виртуальный адрес (в отображении 0xExxx_xxxx). И в конце в дерево добавляется поддельное устройство управления питанием, совместимое с Mac99, чтобы Trampoline подготовил для него драйвер, который потом можно будет заменить.
Патчи ядра и кода Apple
Как я уже говорил, инициализация ядра перехватывается, чтобы наложить дополнительные патчи. В самом ядре патч всего один. В ядре есть несколько драйверов контроллеров прерываний; выбранный пропатчен под контроллер Flipper, а один из неиспользуемых принесён в жертву, чтобы освободить под это место. Главная разница в том, что у ожидаемого контроллера (чей драйвер патчится) есть legacy-режим, в котором он поднимает прерывание PowerPC при любом изменении прерываний, включая их снятие. Обходится это так: поднятые прерывания маскируются в ядре и потом размаскируются в обработчиках прерываний драйверов.
Дополнительные патчи нужны в m68k-коде «SuperMario» и в нативных исполняемых файлах PowerPC. В m68k-коде есть несколько путей, которые напрямую читают и пишут регистры нативного контроллера прерываний, — их надо пропатчить. Указатели на функции включения и выключения аудиопрерываний нужно заменить заглушками (в той таблице уже есть запись, указывающая на заглушку, что удобно). Функция, которая рано при загрузке инициализирует ADB и микроконтроллер управления питанием, не проверяет, есть ли это железо на самом деле, и если его нет, затирает часть нижней памяти. В функции инициализации аппаратных прерываний есть баг из-за неудачного копипаста. Задумано было пропустить код инициализации прерывания второго последовательного порта, если такого железа нет, но разработчики, копируя код из блока для первого порта, забыли поменять метку перехода. В результате, если второго последовательного порта нет, получается бесконечный цикл. В нативном коде тоже хватает мест, которые читают и пишут регистры контроллера прерываний, и их нужно правильно пропатчить. Код инициализации драйвера USBShimKeyboard не обрабатывает ситуацию, когда USB-контроллера нет вовсе, и в этом случае вызывает NULL-указатель внутри эмулятора m68k. Здесь же целиком заменяется упомянутый выше драйвер управления питанием (пока я использую драйвер-заглушку из BlueBox, но собираюсь написать свой, чтобы нормально поддержать выключение и перезагрузку).
Всего этого хватает, чтобы TBXI догрузился до мигающей дискеты. Потом я написал драйвер дисплея (спасибо Elliott Nunn за его драйвер virtio, он очень помог), чтобы видеть картинку правильно, а за ним драйвер портов контроллеров GameCube (портировал свой драйвер из NT), чтобы двигать курсор мыши без USB.
Диск, который притворяется SCSI
На этом месте я начал писать драйвер блочного ввода-вывода для устройств EXI, но застрял, размышляя, как должен быть устроен формат диска и как должен работать драйвер. Потом выложили Mac OS для CHRP, и я отвлёкся на то, чтобы пропатчить поздние образы TBXI для её загрузки: думал, что запуск 7.x пригодится на GameCube, у которого не хватает памяти даже на минимальные требования 8.0. А потом отвлёкся на другие проекты. Почти через год я вернулся, реализовал в OpenBIOS свой формат диска и решил сколотить драйвер, который притворяется SCSI (так делали и другие драйверы, прежде всего драйверы классической Mac OS для разных SATA-контроллеров).
С поддельным SCSI тоже всплыло несколько проблем. Пришлось добавить ещё один патч в m68k-код, чтобы драйвер диска принудительно загружался как драйвер нового типа. Речь именно о драйвере, который загружается с самого диска и реализует настоящие дисковые устройства поверх какого-нибудь размеченного блочного устройства. Старый тип по сути устарел уже к выходу первых PowerPC-систем, а старые вызовы мой поддельный SCSI-драйвер даже не поддерживает; наверное, поэтому он и падал, когда его пытались загрузить как драйвер старого типа. А ещё нашлась любопытная особенность: драйвер оптических дисков (тоже загружаемый с самого блочного устройства) эмулирует 512-байтовые секторы на устройствах с секторами по 2048 или 2352 байта. Для этого он делает вызов чтения с тремя буферами scatter-gather и при этом никогда не проверяет, поддерживает ли нижележащий SCSI-драйвер scatter-gather вообще. Пришлось реализовать это для чтения.
Мигающая дискета всё равно никуда не делась. Отладка показала, что именно отбраковывается: для получения загрузочного устройства вызывалась PowerPC-функция StartLib!GetStartupDevice, а она делала предположения и о дереве устройств, и о том, что есть какое-то встроенное SCSI-устройство, требующее отдельной обработки. С первым всё просто: поправить дерево устройств в коде инициализации моего драйвера. Второе интереснее: пришлось копаться во внутренних структурах и в трёх разных местах (включая дерево устройств) исправить индекс, чтобы он был ненулевым и выполнялась нужная ветка кода.
Ошибка на один байт
После этого я впервые увидел довольный значок Mac, но дальше меня отверг загрузочный сектор HFS: что-то вернуло ошибку, и он просто вышел. Оказалось, баг в моём собственном драйвере. Я отлаживался в Dolphin с переделанным устройством EXI-IDE. В режиме PIO чтение и запись у IDE-устройств идут 16-битными словами, а я не ожидал, что моему коду передадут совершенно невыровненный нечётный адрес буфера (младшие 4 бита равны 1). Пришлось поменять низкоуровневый драйвер, чтобы для таких адресов он даже не пытался использовать EXI DMA. Проявлялся баг так: один байт фактически терялся при записи, и все остальные данные съезжали на байт. По крайней мере, так было при чтении заголовка resource fork, и падала проверка корректности этих данных, что сильно облегчило отладку (в итоге я сдампил оперативную память и сравнил с ожидаемыми данными из образа диска, который использовал).
После этого Mac OS 9.2.2 начала грузиться в моей переделанной ветке Dolphin, очень медленно. Прошёл час, прежде чем она наконец упала. Тогда я попробовал 7.6 — она тоже упала, но раньше. Я сообразил, что падает, видимо, на загрузке MacsBug, убрал его, и минут через 20 смотрел на рабочий стол.

Настоящее железо
На следующее утро я проверил на настоящем железе (SD по SPI). Mac OS 7.6 загрузилась секунд за 40, а это ведь должен быть самый медленный режим доступа к SD-карте! Правда, мой предыдущий опыт — драйвер SD по SPI для NT, и, возможно, как раз он скорее исключение. Посмотрите это видео, где NT ставится на GameCube через такое блочное устройство: копирование файлов в текстовой части установки NT там ускорено в 64 раза — оно ОЧЕНЬ медленное. Разница в том, что здесь я на время передачи занимаю весь процессор. Другого варианта, собственно, нет: если я верну значение, означающее асинхронную передачу, вызывающий код начнёт в цикле активно ждать прерываний до её завершения (хотя при этом он проверяет все контроллеры в системе).
Позже я прикрутил самодельный обработчик сбоев, чтобы разобраться, что происходит с MacsBug… Оказалось, это ошибка эмуляции: на настоящем железе MacsBug загружается без всякого сбоя. А потом выяснилось, что Mac OS 9.2.2 падала при загрузке из-за Open Transport: стоило убрать его из папки Extensions, и система загрузилась без проблем. Подозреваю, что Open Transport не умеет жить, когда в системе нет вообще ни последовательного, ни сетевого устройства.
Как я уже говорил, до релиза порту ещё очень далеко. Дальше на очереди — поднять SD-контроллер Wii, затем USB-контроллер (кое-какой реверс по нему я уже сделал; знаю, что теперь есть навайбкоженный драйвер EHCI, но для реверса он, скорее всего, будет разве что подспорьем), а затем запустить порт на Wii U. А когда-нибудь хочу поиграть с патчами, которые пробьют хотя бы часть ограничений по объёму памяти.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.