Введение
Этим проектом я хотел ответить на один вопрос: возможно ли написать Java API для Playstation 2 и создать на нём графическое демо. Не хочу раскрывать спойлеры, но ответом будет «да».
Несколько лет назад я начал проект Java Grinder, получающий скомпилированные файлы .class Java и фактически работающий в качестве дизассемблера. Но вместо того, чтобы дизассемблировать в ассемблерный код Java, он выполняет дизассемблирование в ассемблерный исходный код для реальных процессоров. Если файлу класса нужны другие файлы классов, то они тоже считываются и обрабатываются. Все вызовы методов API записываются в вывод, или как встроенный ассемблерный код, или как вызовы предварительно написанных функций, выполняющих предназначенную им задачу.
Так как Java Grinder был написан на C++, объектно-ориентированным, абстрагированным, полиморфным и ещё много громких любимых HR слов способом, для его расширения в основном требовалось создание класса Playstation2, расширявшего новый класс R5900, расширявшего основной класс Generator.
В результате проект оказался больше, чем я предполагал. Сама по себе система довольно проста, но мне ещё многому предстоит научиться, а качественную информацию искать не так просто. На самом деле в этом проекте я впервые занялся настоящим 3D-программированием. В другом посте я уже рассказывал о том, чему научился, на своей странице Playstation 2 Programming.
Ниже представлены видео и подробное объяснение процесса разработки.
Видео
Я записал демо на PS2 slim, подключив кабели аудио и видео к DVD-рекордеру. Я немного волновался, что у PS2 есть какая-нибудь защита Macrovision, которая бы испортила видеосигнал, но она или была отключена, или DVD-рекордер её проигнорировал. Видео начинается с демонстрации настоящей Playstation 2, на которой запущено демо. Консоль подключена к преобразователю композитного сигнала в VGA, соединена с ЖК-дисплеем, доказывающим, что демо запущено на настоящей машине. Затем я добавил склейку с настоящим видео, записанным непосредственно с PS2 в DVD-рекордере.
YouTube: https://youtu.be/AjM069oKUGs
Похожие проекты на mikekohn.net
Java Grinder: | Playstation 2 Java, Sega Genesis Java, Apple IIgs Java, TI99/4A Java, C64 Java, dsPIC Mandelbrots, Atari 2600 Java, chipKIT Java, Java Grinder, naken_asm |
Демо
Вспоминая демо Sega Genesis Java, я немного жалею, что не сделал его более интересным. Тогда мне интереснее было продемонстрировать возможности Java API. Когда я начал этот проект, то решил сделать нечто более серьёзное. К сожалению, я снова так выгорел в процессе изучения системы и создания API, что не сил на большое демо не хватило.
- 3 Billion Devices Logo: это логотип 3 billion devices run Java в низком разрешении, который Joe Davisson создал для своего демо Commodore 64 Java.
- Логотипы: я нарисовал их маркером и отсканировал (за исключением логотипа Java).
- Звёзды: на самом деле я скопировал код из демо Sega Genesis Java и модифицировал его, чтобы он работал с Playstation 2 API. Текст здесь тоже написан маркером и отсканирован.
- Фракталы Мандельброта: они демонстрируются с помощью vector unit 0, вычисляющего фракталы, а vector unit 1 выполняет 3D-вычисления. MIPS управляет тем, что делают оба векторных устройства.
- Кубы: я нарисовал эти кубы в Wings3d и написал код на C для преобразования файлов STL в массивы, которые мог бы использовать Java Grinder. Цвета я добавил вручную.
- Кольцо из квадратов: просто попытка отрисовать на экране множество движущихся объектов. Вероятно, стоило добавлять всё больше объектов, пока система не начала бы тормозить.
Музыка
Для демо я сочинил и записал три композиции, но в результате использовал только две. Первая композиция — это на самом деле мелодия, которую я написал для другого проекта, опубликованного на моём сайте (проект монетоприёмника) примерно год назад… изначально там была только часть. После завершения проекта я подумал, что будет интересно наложить на неё гитарное соло, и после записи я представил, что эта музыка играет, пока в демо летают звёзды. Мне удалось использовать её только через несколько месяцев. Гитара в композиции — это Fender Strat I scalloped.
Вторую композицию я записал всего за день до публикации статьи. Гитарное соло звучит немного… пьяно, потому что исполняется на гитаре, которую я превратил в безладовую. Я не очень хорош в игре на ней, и высокие ноты очень быстро затухают, но слайды звучат довольно круто. Ритмическая часть игралась на моём Yngwie wanna-be kit (дешёвая scalloped Squier Strat, DOD YJM308 overdrive и Mini-Marshall с питанием от 9-вольтных батарей).
Ударные для обеих композиций я программировал с помощью уже давно написанной программы Drums++. Она получает на входе текстовые файлы, записанные на придуманном мной языке, и превращает их в файлы .mid, которые я импортировал в Apple Garage Band, после чего можно записывать базовые и гитарные дорожки. Файлы исходников fretless.dpp и shoebox.dpp находятся в папке assets репозитория моего демо.
Музыка воспроизводится устройством SPU2 консоли Playstation 2, и благодаря чип R5900 может выполнять другую работу. Из-за нехватки хорошей документации я едва не закончил демо вообще без музыки. Подробнее об этом ниже.
Вот две композиции в формате MP3:
Разработка
Проект разрабатывался достаточно долго. Я начал добавлять инструкции R5900 Emotion Engine в ассемблер MIPS в naken_asm, затем занялся инструкциями с плавающей запятой и инструкциями макро/микрорежима vector unit. Делая большие передышки, чтобы поработать над другими проектами, я изучил все остальные аспекты, необходимые для этого демо, и приступил к добавлению их поддержки в Java Grinder. Если кому-то интересны низкоуровневые детали, то я создал страницу, на которой попытался задокументировать всю собранную информацию: Playstation 2 programming.
В основном я программировал с помощью эмулятора PCXS2. Он довольно удобен, потому что в нём я на экране могу исследовать регистры и тому подобное. Но он определённо не так гибок и прост, как MAME при разработке Sega Genesis. Например, в MAME проще исследовать память, и ОЗУ, и регистры видео/звука, чтобы убедиться в правильной работе Java Grinder.
При работе с кодом для Sega я совершил одну ошибку — не тестировал его на машине до момента написания демо. В коде Sega было как минимум три странности, которые эмулятор игнорировал, но реальной машине они не понравились. На этот раз после написания отдельных частей кода я тестировал их на реальной машине, чтобы после завершения демо оно работало и на настоящем оборудовании, и в эмуляторе. Я снова столкнулся с вещами, которые работали в эмуляторе, но не запускались на настоящей PS2. Также я обнаружил то, что работало на реальной Playstation 2, но неверно выполнялось в эмуляторе.
Возможности API
- Vector Unit 0 имеет методы для загрузки/запуска кода и загрузки/выгрузки данных.
- Vector Unit 1 выполняет 3D-повороты и проецирование.
- Текстуры, использующие 16- или 24-битный формат (прозрачность обозначается чёрным цветом).
- Текстуры в 16-битном формате можно кодировать RLE.
- Код для отрисовки точек, линий, треугольников, с текстурами и без них.
- Туман и затенение по Гуро.
- Методы для доступа к генератору случайных чисел.
- Использование двух контекстов (замена страниц)
- Вставка крупных двоичных данных в скомпилированных ассемблерный код.
- Воспроизведение музыки.
API
Основная часть API задаётся в классе Playstation2. Изначально я собирался предоставить ему большую степень свободы — возможность задавать видеорежимы и тому подобное, но потом подумал, что может быть лучше скрыть все эти сложности. По сути, он просто задаёт дисплей 640x448 с чересстрочной развёрткой. Как и в других проектах Java Grinder, я в основном добавлял методы/возможности просто по мере необходимости.
Есть и ещё один набор классов, которому я дал скучное название Draw3D. По сути, они задают все типы примитивов, которые может отрисовывать Graphics Synthesizer с поддержкой 16-, 24- и 32-битных текстур. Я размышлял о добавлении 8-битных текстур, но решил пока не заниматься этим. Draw3D обеспечивает 3D-повороты, проецирование, аппаратную передачу DMA, текстуры и т.д. Вероятно, вы спросите, почему я не создал его на основе OpenGL, но я раньше никогда не работал с OpenGL. Когда-то давно я занимался простым программированием ps2dev, но там не было ничего серьёзного и я едва помню тот проект, поэтому повторюсь — можно считать, что это первый раз, когда я делаю что-то серьёзное в 3D.
Есть примеры использования всех этих вещей, не только в демо, но и в папке samples.
Почти все сложности скрыты в API. Разработчику не нужно беспокоиться об очистке кэша, о 3D-вычислениях и т.д. Однако расплатой за это стало снижение универсальности. Поэтому если, например, текстура была изменена процессором, но изменились только первые 64 пикселя, то необходимо очистить только одну 64-байтную строку кэша, но Java Grinder очищает всё изображение. Он помечает объекты, поэтому они очищаются только при необходимости, но очищает весь фрагмент памяти. С большой вероятностью при изменении 64 байтов меняется и всё изображение целиком.
Vector Unit 0 (VU0)
Пользователь Java Grinder может свободно использовать VU0. Я использовал часть демо под названием «Two Vector Units, One MIPS» для отрисовки фракталов Мандельброта. Код можно оптимизировать и получше, например, большинство инструкций vector unit с плавающей запятой имеет время отработки 1 и латентность 4. Насколько я понимаю, это значит, что если регистр является целевым для инструкции, то она может выполниться за 1 цикл, но для того, чтобы результат стал доступен, требуется 4 цикла. Если этот регистр используется, то vector unit будет простаивать, пока тот не будет готов. Поэтому идея состоит в том, что надо расставить инструкции так, чтобы можно было выполнять каждую инструкцию за 1 цикл без простоя. Когда в прошлом году я создавал фракталы Мандельброта на Playstation 3, я оптимизировал этот код, одновременно вычисляя по 8 пикселей (2 регистра SIMD), заметив при этом большой прирост скорости. В нынешнем случае я стремился к тому, чтобы код было проще читать, поэтому не заморачивался его оптимизацией.
В VU0 содержится всего 4 КБ памяти данных, и туда не записать всё изображение фрактала, поэтому MIPS отправляет ему за раз координаты всего 1/8 изображения.
Странность, с которой я столкнулся при работе с VU0: изначально я запускал код VU0 с помощью инструкций и использовал VIF0_STAT для проверки завершения их выполнения. Похоже, что VIF0_STAT не работает, если не запустить VU0 пакетом VIF. Это исправлено в эмуляторе, но ошибка по-прежнему есть в реальном железе. В результате я выяснил, что vcallms и использование cfc2 в регистре 29 работает в обоих случаях.
Мне кажется, что в наборах инструкций vector unit не хватает инструкции параллельного сравнения, которая есть в Intel X86_64, Playstation 3, и даже в наборе инструкций MIPS R5900 Emotion Engine. Фракталы Мандельброта должны итеративно вычислять формулу, пока результат не превзойдёт, поэтому при другом наборе инструкций я бы просто выполнял параллельное сравнение, которое бы создавало маску для всех двоичных 1 или 0. Маску можно использовать для того, чтобы прекратить инкремент счётчиков цветов. Для Playstation 2 мне пришлось вывести очень неуклюжую формулу, которая достаточно точно приближена к формуле фрактала. Я задокументировал это в исходном коде mandelbrot_vu0.asm в строках с закомментированным Python.
Я считаю, что здорово в устройствах vector unit консоли Playstation 2 здорово то, что я не видел никаких других наборов SIMD-инструкций, в которых все FPU-инструкции могут иметь атрибут .xyzw, сообщающий инструкции, на какие из четырёх чисел с плавающей запятой она влияет. То есть если бы мне нужно было, чтобы результат инструкции влиял только на x-компонент вектора, то я бы просто добавил в конце инструкции .x. Ещё одна интересная вещь заключается в том, что это набор инструкций VLIW, то есть в каждом цикле одновременно выполняются две инструкции. На самом деле модуль больше похож на DSP, чем на процессор общего назначения.
Vector Unit 1 (VU1)
VU1 зарезервирован Java Grinder для выполнения 3D-поворотов, перемещений и проекций. Объекты Draw3D передаются VU1 с помощью метода draw(), который использует ассемблер vector unit для преобразования точек и передачи их в Graphics Synthesizer. Ассемблерный код в VU1 можно гораздо лучше оптимизировать для обеспечения скорости, но для моих целей он подходит, поэтому я решил оставить код легко читаемым (неоптимизированным).
Для изучения формул проекций и поворотов я воспользовался Википедией: проекции и повороты.
Код 3D-преобразований также есть в репозитории naken_asm в виде простого файла .asm: rotation_vu1.asm.
MIPS R5900
Мне не очень нравился набор инструкций MIPS, пока я не приступил к работе над этим проектом. На самом деле с этим ЦП довольно просто работать. Версия Emotion Engine этого ЦП имеет очень удобные функции. Примечательнее всего то, что регистры имеют длину 128 бит, однако на самом деле они просто используются для загрузки/хранения и SIMD. То есть в реальности это 128-битные регистры, 64-битное АЛУ и 32-битные указатели.
В основной код MIPS тоже можно было внести оптимизации, но я не стал этим заниматься, чтобы сохранить читаемость кода или из-за нехватки времени. Например ЦП MIPS простаивает один цикл, если целевой регистр инструкции использовался сразу после его задания. Такое поведение можно было бы и улучшить.
Java-хаки
В самом Java Grinder тоже есть свои… странности, а кое-чего просто не хватает, в основном потому, что изначально я нацеливался на MSP430 и по большей части это было экспериментом. Одним из недостающих элементов было отсутствие возможности выделять память под объекты. Я добавил эту возможность на Playstation 2 для создания экземпляров нескольких объектов, в основном с помощью Draw3D API. Я не писал никаких распределителей памяти или сборщиков мусора, поэтому все вызовы «new» выполняются в стеке. Я думал о реализации чего-то наподобие распределителя динамической памяти, но в конце концов решил не усложнять. Также я добавил для Playstation 2 расширенную поддержку чисел с плавающей запятой (float) (частично эта поддержка была ещё в коде Epiphany / Parallella). Некоторые другие вещи, такие как типы «long» и «double», всё ещё не поддерживаются.
Вероятно, самое неприятное, что я делал, связано с ужасным ограничением файлов классов Java. Метод Java не может быть больше 64 КБ, если я правильно помню. Возможно, это нормально, но проблема возникает, когда в файле класса есть статический массив и он не дампится в файл класса как двоичные данные. Он помещается в файл класса как ассемблерная инструкция Java в статическом инициализаторе для создания массива. Я пытался сохранять изображения в файлы классов как статические массивы byte[], но некоторые из них не поместились, поэтому в файл класса Memory Java Grinder я добавил метод:
byte[] Memory.preloadByteArray(String filename);
Он не загружает этот файл во время выполнения приложения, а загружает его во время сборки с помощью директивы .binfile naken_asm. Изображения копируются в выходной двоичный файл во время сборки.
С учётом всего этого, я очень надеюсь, что Джеймс Гослинг никогда не наткнётся на мой проект.
Изображения
Draw3D API умеет использовать 16-, 24- и 32-битные текстуры. Пиксели текстуры могут задаваться попиксельно или загрузкой с помощью массивов byte[]. Также я добавил возможность RLE-сжатия изображений в формате { length, lo16, hi16 }, где lo16 и hi16 — это 16-битный цвет в формате little endian, который копируется в текстуру «length» раз.
Инструменты
При работе над Sega для создания инструментов создания изображений, музыки и подобного я пользовался языком Google Go, просто для того, чтобы выучить новый язык. На этот раз я попробовал Rust. Первый инструмент преобразует двоичные файлы в исходный код Java, а второй преобразует BMP в двоичный формат, который можно загружать в текстуры, в том числе и в формате RLE. В результате я написал их на Python, на тот случай, если кто-то захочет присоединиться ко мне в создании демо.
Звук
Разобравшись с тем, как работают графика и устройства vector unit, последним этапом стал звук. Я посчитал, что это будет самая простая часть, особенно после изучения PDF с описанием SPU2 Sony. Как же я ошибался. Эта часть системы очень слабо документирована.
Первое, что я выяснил — SPU2 (sound processing unit) соединён с IOP (I/O processor, он же процессор Playstation 1). ЦП Playstation 2 соединён с этим IOP через нечто под названием SIF. В PDF Sony упоминается только SIF DMA, но не говорится ничего о его использовании.
В результате я отказался от использования SIF, но решил добавить линкер на naken_asm, чтобы можно было использовать kernel.a из PS2DEV SDK. Линкер заработал, но ничего не получилось.
На этом этапе я уже решил, что мне не удастся заставить звук работать, и просто хотел закончить демо без него. Но это меня мучило, поэтому я решил взглянуть на исходный код разных эмуляторов Playstation 2, чтобы разобраться, как работает SIF. Наконец я разобрался, как выполнять прямой доступ к памяти из кода MIPS R3000 в IOP и запустить его (в папке samples репозитория naken_asm есть пример). Мне удалось заставить звук работать в эмуляторе.
В конце концов я выяснил, что память IOP (в том числе и SPU2) была размещена в пространстве Emotion Engine, поэтому приложив множество усилий (документации чрезвычайно мало и ни в одном из эмуляторов это не реализовано полностью правильно, но для их работы это не важно), я научился работать со звуком.
Сравнение эмулятора и железа
Я обнаружил некоторые различия между выполнением на реальной машине и в эмуляторе.
- Если пакет GIF задаёт регистру PRIM оба значения IIP (метод затенения), а биты FIX все равны 1, то эмулятор учитывает бит IIP и выполняет затенение по Гуро, в то время как реальное оборудование выполняет плоское затенение.
- Если пакет GIF передаётся через PATH3 (EE direct to GS), а флаг EOP (end of packet) не задан, то если VU1 пытается отправить пакет GIF через PATH 1 (VU1 to GS), то это приведёт к зависанию в реальном железе, но будет работать в эмуляторе.
- Пропуск очистки кэша ЦП перед DMA-передачей не обязателен, но на реальной машине приводит к странному поведению.
- При размещении SPU2 в пространстве EE эмулятор может просто записать звуковые данные в FIFO SPU2. На реальной Playstation 2 после записи 32 полуслов необходимо выполнить запись в регистр, чтобы дать команду на очистку FIFO. Также на реальном железе при задании адреса передачи/начала SPU2 режим передачи должен иметь значение 0. Эмуляторы не волнует, имеет ли режим значение 0.
- Запись в регистры выделенной памяти IOP из EE приводит на реальной машине к сбою, несмотря на то, что она находится в режиме ядра. Эмулятор позволяет таким операциям работать вне зависимости от текущего режима ЦП.
- Использование каналов SIF DMA работает в эмуляторе, но мне так и не удалось добитсья их работы на реальном оборудовании. Я получал ошибку доступа к памяти для регистров SIF DMA даже в режиме ядра.
- Эмулятор слишком медленный, чтобы выполнять демо при вычислении фракталов с помощью VU0, поэтому звук рассинхронизируется.
Подведём итог
Я хотел написать какую-нибудь программу для Playstation 2 почти с самого момента её покупки. На самом деле у меня уже давно был Linux-комплект для PS2 (думаю, именно по этой причине я приобрёл Playstation 2), и я даже пробовал работать с библиотекой PS2DEV на C, но это совершенно иной опыт по сравнению с программированием на языке ассемблера непосредственно для железа.
Должен поблагодарить Лукаша за сохранение старого ассемблерного исходного кода и документов PS2. Не уверен, смог ли бы я вообще начать работу без демо 3 Star разработчика Duke, которое помогло мне инициализировать оборудование. Также я благодарен разработчикам эмулятора PCSX2, сильно ускорившего процесс отладки. К тому же я не смог бы разобраться со звуком, если бы не посмотрел на исходный код эмулятора и не понял, в чём ошибался.
И спасибо Sony за этот прекрасный маленький компьютер. Если кто-то из Sony читает эту статью, то вот вам совет: почему бы не ужать её до размеров Rapsberry Pi и не продавать как плату для хобби-проектов? :).
Сборка демо
git clone https://github.com/mikeakohn/playstation2_demo.git
git clone https://github.com/mikeakohn/java_grinder.git
git clone https://github.com/mikeakohn/naken_asm.git
cd naken_asm
./configure
make
cd ..
cd java_grinder
make java
make
cd ..
cd playstation2_demo
make