
Всем известный Doom запускали на большом наборе различной техники и с различной степенью его портирования на выбранную платформу. Вот и я решил, что смогу что-нибудь куда-нибудь портировать.
Введение
Портирование игр на различные платформы это далеко не новость. Многие игры уже портировали, тот же Doom, Quake, Wolfenstein. Особенно Doom обладает популярностью в этой области. А раз портирование игры достаточно частое явление, то почему бы и мне что-нибудь не портировать? Тем более меня интересует подобное.
Мой выбор пал на Wolfenstein 3D. Просто пальцем ткнул в интернет и обнаружил ее.) Поэтому аргументированно объяснить почему именно Wolfenstein, а не тот же Doom - не могу. Зато могу объяснить мою мотивацию, которая двигала меня, ведь все мы понимаем, что портирование занимает не на 1 день:
Я был в поиске пет-проекта. Пет-проект — это не только куча кода, но и встреченные трудности, решив которые получаешь знания(опыт).
Мне это интересно. Интересны проекты, которые решаются на ПЛИС.
Интересны игры на ПЛИС (не эмуляторы приставок), поэтому я потихоньку копаю в этом направлении и иногда что-то получается, например статья «Поиграем на ПЛИС?»
Разумеется, начнем сразу с демонстрации результата. На видео ниже я запускаю Wolfenstein на ПЛИС, и прохожу первый уровень. Управление происходит с использованием клавиатуры.
А теперь по порядку …
О портировании игры я задумывался уже давным-давно и стал для этого подготавливать почву, результат подготовок выражен в статьях: Xilinx AXI DMA v7.1 (Simple Mode), Тестирование MicroBlaze и И снова про VGA. Все эти статьи не только несли свое прямое предназначение - рассказать и показать сообществу что-то интересное - но и имели скрытые планы. В моем случае я пробовал, тестировал и проверял различные модули, чтобы в будущем знать и уметь…
Поэтому, приняв решение о портировании игры, я уже знал, что делать, осталось только собрать и заставить работать.
Мы знаем, что игра Wolfenstein написана на С-коде, а компилятор MicroBlaze отлично его собирает, поэтому берем исходники игры и засовываем в процессор, подключаем вывод и ввод и получаем игровой процесс. В целом план работ ясен, поэтому начнем.
Забыл сказать про имеющиеся ресурсы: Vivado 2019.1, отладочная плата Nexys A7 с ПЛИС xc7a100tcsg324-1. Из основного, на борту имеется DDR2 на 128 Мбайт, VGA выход, USB для подключения клавиатуры либо мыши.
Этап 1. Подготовка начального проекта в Vivado.
Очевидно, что с чего-то нужно начать, и мне видится, что самый первый вариант проекта — это проект с выводом на VGA монитор. К сожалению, скриншота этой версии проекта у меня нет, опишу на словах.
Собрал систему на кристалле. Во главе угла MicroBlaze, через AXI Interconnect подключил MIG7 DDR, AXI DMA, AXI UartLite. Добавил модуль VGA, который принимает поток данных от DMA и выводит их на монитор. Из DMA вывел прерывание об окончании транзакции и завел его в MicroBlaze.
Запустил SDK и на стороне MB реализовал простой код: фрейм буфер разместил в DDR, DMA после завершения передачи одного кадра вызывает прерывание, в котором перезапускает себя снова. Во фрейм буфер вывел тестовую картинку, математически рассчитанные полосы и линии.

На данном этапе прошивка заливалась в память BRAM ПЛИС, а фрейм буфер в ДДР. И поэтому я столкнулся с интересным явлением. Если прошить ПЛИС и увидеть картинку, а затем ненадолго отключить питание (до 5 секунд), и снова включить плату, но прошиться без инициализации фрейм буфера, то на мониторе отображалась старая картинка, точнее картинка была сильно испорчена, но ее контуры легко угадывались. К сожалению, этой фотографии тоже не сохранилось.
Далее я проверил работу UART, скорректировал размер FIFO между DMA и VGA модулем. FIFO решает вопрос переноса данных на другой клоковый домен, и сглаживает неточности перезапуска DMA.
Самому MicroBlaze поставил все галочки, что ускоряют его работу, это умножитель, делитель, и операции сдвига. Кэш включать не стал, так как в предыдущей своей статье я не смог выявить прироста производительности при его включении.
Частота всего проекта изначально 160МГц, в дальнейшем я ее понижу до 120МГц чтобы уложиться в тайминги у разросшегося проекта. Размер фрейм буфера 640х480х2байта.
Этап 2. Сборка исходников.
Итак, начальный проект готов, подключены все памяти, имеется вывод на монитор, и вывод в UART для отладки. Поэтому теперь можно подключать исходники и пытаться ими прошить MB.
Исходники качал отсюда:
https://github.com/libsdl-org/SDL/releases?q=2.0.22&expanded=true — это промежуточная прослойка, которая позволяет исходникам самой игры запускаться на различных платформах.
https://github.com/KS-Presto/Wolf4SDL - исходники на саму игру. Данная игра изменена для использования ее вместе с SDL библиотекой.
Таким образом имеется два набора файлов: SDL - является набором функций, которые работают с реальным железом и Wolf4SDL - код игры который использует функции SDL.
Все исходники как есть добавил в проект SDK. Это выглядит так:

Пути до папок sdl и game прописал в настройках проекта:

А дальше началась череда попыток сборки проекта. Как правило все сводилось к тому, что в исходниках имелось много лишних файлов, предназначенных для поддержки других платформ, например Raspberry PI, Android, Linux, Windows, и другие. Все эти файлы мне были не нужны, и я их активно удалял, оставив только папки Dummy. Хотя оно само так получилось, я удалял все на что ругалось.
На этом этапе я активно пользовался ИИ. Вопросы были из разряда «компилятор ругается на это… я могу удалить?», в ответе читал что куда и зачем эти файлы нужны и в итоге, как правило, удалял.
Это был долгий и нудный процесс, так как компилятор спотыкался на первом ненужном файле и не выдавал их все разом. А файлов было много.
Возможно, где-то в настройках нужно было лишь указать нужный параметр или дефайн и все было бы хорошо, подскажите если знаете…
Этап 3. DDR вместо BRAM.
Поначалу я планировал использовать DDR только для видеобуфера, но результатом второго этапа стало то, что сборка весит 2 Мбайта.
Для моей ПЛИС это недостижимая роскошь. На борту имеется только 4860 Кбит памяти, а это лишь 607 Кбайт.

В принципе на этом можно было бы закончить портирование, если бы я не знал о XSCT Console и о том, что прошивка ZYNQ из SDK кладется в DDR. Значит есть способ залить прошивку для MicroBlaze в DDR.
Для решения этой задачи тут нужно определиться с некоторыми вещами:
Как MB будет стучаться в DDR за инструкциями?
Как JTAG достучится до DDR?
В первом случае все просто, в настройках MicroBlaze есть отдельная шина для этого, ее необходимо включить и подключить к общему интерконнекту:

Таким образом MB сможет обращаться к внешней памяти за инструкциями.
Во втором случае нужно подключить IP-ядро MicroBlaze Debug Module (MDM), через него происходит отладка кода «на живую». Один интерфейс в Debug процессора, а второй в общий интерконнект:

Теперь есть возможность достучаться до DDR памяти и передать команду в процессор о запуске.
Затем идем в настойки проекта в SDK. Понадобится файл lscript.ld, в нем задаем область памяти, и переносим в нее все разделы прошивки.

В LD файле, если память ДДР не отображается, то ее нужно самостоятельно добавить (номер 1), указать базовый адрес и размер. Базовый адрес берется из Vivado раздел Addres Editor, либо в файле xparameters.h в SDK. Размер можно посчитать самому, либо подглядеть там же где и базовый адрес.
Потом для всех секций программы выбираем нужную область памяти (номер 2), в нашем случае это только что добавленная DDR.
Здесь стоит отметить размер регионов Stack и Heap, это области памяти, которые используются программой при ее выполнении. В Stack улетают все локальные переменные функций, а в Heap улетает динамически выделенная память. В коде игры и SDL выделение динамической памяти используется много, сам фрейм буфер так же создается. И так как DDR больше ни для чего не нужна, только для работы кода в MB, то всю память я отдал под эти две секции. Соотношение выбрано эмпирически.
Дальше в ход вступает XSCT Console.
Сначала из Vivado прошиваем ПЛИС, затем открываем XSCT Console в SDK и пишем следующее:
Connect – подключение к программатору
Target – посмотреть список устройств и увидеть, какое устройство выбрано на текущий момент.
Target 3 – как правило целевое устройство, в моем случае MicroBlaze, имеет индекс 3, поэтому пишем target 3, затем повторяем команду target, чтобы убедиться, что выбор сделан верно, там будет стоять звездочка.
Dow path_to_file_elf - команда для загрузки прошивки MB. Вместо path_to_file_elf указываем полный путь до файла. Обращаем внимание на направление слэша (разделительная черта). В windows я его всегда меняю. Начнется этап загрузки.
Con - команда запуска прошивки. Запускает только что залитую прошивку в процессоре. По команде con начинается ее выполнение.
Так выглядит процесс загрузки прошивки в DDR:

Стоит отметить, что я сразу написал target 3, так как был уверен, что MB имеет индекс 3. И только потом ввел target без индекса, чтобы увидеть корректность выбора. Все пользовательские команды подсвечены синим цветом, остальное вывод системы.
Этап 4. Первые попытки запуска кода.
Теперь, когда разобрались с железом, и сборкой исходников, можно приступить к отладке и запуску самого кода.
Точкой входа в игру является функция int wolf_main (int argc, char *argv[]), из файла wl_main.c.
Эту функцию вызываем после предварительной настройки системы: после настройки прерываний, запуска DMA, и предварительной инициализация промежуточного фрейм буфера, чтобы не смотреть на пустой монитор.
С помощью ИИ, начал разбираться в исходниках. В первую очередь прописал ряд #define в файле SDL_config.h, параметров было много и они все имели смысл об отключении мыши, джойстика, звука, наличие таких функций как memcp(), malloc() и другое.
Функции CheckParameters() и CheckForEpisodes() проверяли параметры запуска wolf_main() и пересчитывали локальные параметры, которые задают режим игры. От них я избавился, вручную все просчитал и задал фиксированные значения.
В функции InitGame() происходит первая инициализация экрана и вывод картинки во фрейм буфер. На эт��м этапе я еще плохо разобрался с тем, как строится и выделяется память под фрейм буфер, поэтому отсчитал от конца DDR область памяти под 640х480х2Байт памяти и варварским методом после выделения памяти просто присвоил указатель этой области. Позже я разобрался с тонкостями и смог выделить уже программными средствами 640х480х2Байт памяти, и с областью отрисовки в левом верхнем углу на 320х240 пикселей. 320х240 - целевое разрешение картинки игры, которое я выбрал.
Для того чтобы отслеживать ход выполнения кода, я использовал вывод в UART, по началу это были просто текстовые сообщения вида xil_printf(“text”), а затем я понял, что важно как-то отмечать файл и строку, для этого нашел такой вариант:
xil_printf("[%s:%d] text", FILE, LINE);
FILE, LINE — это переменные, которые подставляет компилятор, указывает имя файла и номер текущей строки. Что сильно помогло при копании в коде.
На этой стадии уже стала появляться первая картинка, которая отображает режим запуска.
По началу она выглядела страшно, не сходились цвета, картинка расползалась по всему экрану, но было понятно, что это УЖЕ не мусор из ДДР.

Этап 5. Подключение файлов.
Постепенно проходя функцию за функцией, я добрался до стадии работы с файлами. Функция которая fopen().
Всем и так понятно, что игра Wolfenstein использует сторонние файлы, которые содержат уровни, их текстуры, музыку и другое… А это значит, что без подключения этих файлов не обойтись. Реализовывать файловую систему для MicroBlaze совершенно не хотелось. Конечно, есть на отладочной плате SD разъем, через который можно достучаться до флешки, но это не облегчает ситуацию.
Как быть? Я на долго завис на этом этапе, пока не наткнулся на функцию fmemopen() которая возвращает тоже самое что и функция fopen(), нужно лишь файлы представить как массив в памяти, а вот это я легко могу реализовать.
Для каждого файла я создал файл с инициализацией массива, и параметром размера этого массива. А затем, везде, где компилятор спотыкался об fopen(), я подключал нужный файл и менял функцию на fmemopen(file, size, “rb”).
Вроде бы подключил файлы, и алгоритм сделает все за меня, но все оказалось сложнее. Эти файлы имеют сжатие методом ��афмана, и распаковка каждого файла занимала длительное время. Да и весь процесс запуска занимал порядка 5 часов!!!
Я во всем винил MicroBlaze, что он не способен на такое и в будущем придется пилить какие-то ускорители на ПЛИС. Пока главное запустить код. И вот, чисто случайно захотел как-то визуализировать процесс работы DMA по выводу кадров на экран. Залез в функцию прерывания и поставил банальный счетчик прерываний; и каждые 30 прерываний выводил инверсию светодиода. В общем - классическое моргание лампочкой раз в секунду. Какое же было мое удивление, что после всех расчетов я обнаружил, что прерывание срабатывает не 60 раз в секунду, а 2000 раз. Я банальным образом забыл очистить регистр прерывания в самом DMA.
После исправления ситуации, начальная загрузка и инициализация превратилась в считанные минуты. Примерно 5-10 минут. Это по-прежнему все еще много, но пока еще только идет инициализация.
В процессе распаковки файлов вылетала ошибка о проблемном чанке 144, проблему решил банальным пропуском). Всего 158 чанков. Возможно, это повлияло на дальнейшие баги, но этап был пройден, работа с файлами закончена.
На этом этапе есть видео процесса загрузки первого кадра. Сначала кадр формируется во фрейм буфере, а затем выравнивается в одной четверти экрана. Процесс занимает много времени, позже я разобрался с параметрами и отрисовка стала иной:
Этап 6. Подключение клавиатуры.
И вот я добрался до стадии, когда все функции отработали и игра готова запускаться. На экране я вижу вот такую картинку:

Изначально на ней не было статус-бара (все объекты этого бара были разбросаны по экрану), но в любом случае теперь в терминал мне стали валиться сообщения о том, что игра ждет нажатия клавиш.
Этот этап занял у меня больше времени, чем другие. Я долго разбирался, каким образом вклиниться в очередь событий. В конечном счете я всё-таки понял, что конечная функция SDL_PollEvent() вытаскивает из очереди событие… и возвращает его обработчику IN_HandleEvent(), таким образом, мне не важно знать, где находится очередь событий, главное правильно прописать значения в переменной события, которое возвращает SDL_PollEvent().
Для пробы работоспособности я сделал модуль с интерфейсом AXI4Lite и подключил его к общему интерконнекту. Завел в него пять кнопок с платы, это те, что крестовина. И не хитрым алгоритмом складывал в локальное FIFO события «нажата кнопка» и «отпущена кнопка», промежуточные события о том, что кнопка еще удерживается не нужны. Алгоритмы игры это сами просчитывают. В этой системе FIFO становился очередью событий.
Теперь я наконец то смог увидеть, как часто генерируются кадры, по сути, получалось в районе 1 кадра в секунду. Играть в такое невозможно, но я не оставлял надежды, что ускорители мне помогут.
Ну, пятью кнопками не поиграть, поэтому пришло время подключить клавиатуру через USB порт. На данной плате USB заходит в микроконтроллер PIC, микроконтроллер отрабатывает как host для usb, и реализует поддержку клавиатуры и мыши. В сторону ПЛИС, микроконтроллер выдает интерфейс ps/2, который и нужно написать.
На первый взгляд интерфейс ps/2 кажется не простым, но это лишь потому, что рассматривается передача данных в оба направления. Если рассмотреть передачу данных от клавиатуры к хосту, то все становится намного проще. Его протокол схож с i2c и uart, имеется стартовый бит, поле данных, бит четности и стоповый бит.
При нажатии кнопки, клавиатура возвращает код клавиши, при отпускании возвращается код клавиши и байт 0xF0, этот байт обозначает отпускание клавиши.
В коде игры использовался такой термин, как scancode для клавиши, и в различных мануалах на подключение клавиатуры тоже упоминается этот термин, поэтому после успеха с клавиатурой я напрямую соединил значения с клавиатуры с очередью событий для процессора. Это оказалось ошибкой. В коде игры все кнопки были проинициализированы значениями по порядку списка в массиве. Если кто-то пойдет в этом копаться, то это структуры SDL_Event, SDL_KeyboardEvent, SDL_Keysym, SDL_Scancode в файлах SDL_events.h:231, SDL_scancode.h:409.
На стороне ПЛИС сделал таблицу соответствия кодов клавиатуры с кодами, которые ждет игра, можно, конечно, было в самом коде поправить значения, но не стал.
Этап 7. Попытки повысить FPS.
Ну что ж, код запускается, код отрисовывает картинку, и даже получается двигаться.
Вот как это выглядело (обратите внимание на линию отрисовки, которая медленно спускается вниз):
Теперь можно приступить к каким-то мерам по ускорению игры. Во-первых, я обнаружил, что если включить #define HAVE_MEMCPY 1, то в коде будет использоваться функция memcpy(), которая значительно лучше, чем тот цикл, который реализован взамен. Тем более memcpy() поддерживается MicroBlaze. Во-вторых, я обратил внимание на параметры компилятора, степень оптимизации -O0 и степень отладки -g3, я предположил, что если этим параметрам подкрутить гайки, то можно получить некий профит. Собственно, я заменил -O0 на -O2 и -g3 на -g0. И результат был, получилось сэкономить порядка 500кбайт, а частота кадров слегка возросла. Главное слово «слегка», даже я бы сказал «чуть-чуть».
Взявшись за ускорение отдельных функций, выявил, что функция Blit1to2() банально перегоняет данные из одной области в другую. А именно, у алгоритма имеется внутренний буфер 320х240 байт, каждый пиксель хранит значение индекса из таблицы цветов. Функция Blit1to2() занимается преобразованием исходного «цвет-индекс» в конечный RGB444 цветовой формат (именно этот цветовой формат я выбрал). Я заменил эту функцию на DMA и самописный блок. По сути, самописный модуль получал на вход индекс массива, а на выход отдавал значение этого индекса. Но теперь данные прогонялись напрямую из памяти в память без промежуточных чтений инструкций для процессора.
В результате преобразование цвета стало мгновенным и глазу не заметным.
К сожалению, без багов не обошлось:

Картинка стала в вертикальную полосочку, эта проблема была вызвана тем, что массив, в котором хранилась таблица цветов строился на памяти, и не позволял одновременно вычитывать два значения. Пришлось принудительно прописать атрибут (* ram_style="register" *).
Стоит отметить, что при добавлении ускорителя вылезла другая проблема — это конкуренция за DDR. На данный момент в проекте уже два DMA и MicroBlaze с двумя интерфейсами AXI. И все эти устройства одновременно стучатся в один DDR. Больше всех пострадал DMA, который отвечал за вывод изображения в VGA. Для решения этой проблемы пришлось лезть в настройки AXI Interconnect и выставлять приоритет интерфейсам.

Чем выше число, тем выше приоритет у интерфейса. Доступ к приоритетам открывается при включении галочки Enable Advanced Configuration Options.
Этап 8. Работа с WallRefresh()
WallRefresh() – ключевая функция, которая отнимает много времени. В рамках адаптации кода для ПЛИС, я реализовал свой таймер. Благодаря нему, я смог замерять время выполнения функций. Но эта идея ко мне пришла только на этом этапе, так как на предыдущих и так было видно, где код застревал, на основе логов в UART.
Эта функция рисует 3D сцену в свой внутренний буфер 320х240 байт, который потом пройдет через цветовую таблицу. Она проходит по столбцам и зарисовывает их. Получается цикл на 320 итераций. Внутри каждой итерации имеются бесконечные циклы while(), выход из которых осуществляется через break. Еще функция была полна goto переходов, что усложняло понимание алгоритма. Я попросил ИИ переписать эту функцию таким образом, чтобы она избавилась от goto, это понадобилось больше для того, чтобы мне самому было проще понять логику работы и оценить как можно перенести этот код на ПЛИС.
Браться за перенос этой функции в ПЛИС совсем не хотелось, функция огромная и требует обращения к памяти за текстурами. Поэтому я попытался пойти другим путем, на данный момент самое узкое место это DDR. Попробуем его ускорить.
На данном моменте частота клока в проекте уже 120МГц.
В MIG7 я увеличил шину AXI с 32 до 64 бит. Изменил адресацию, теперь номер банки находится в центре адреса, а не в старших битах. Увеличил число автоматов обработки банков памяти с 4 до 8. Выставил «лимитированный приоритет чтения», то есть чтение имеет приоритет, но при достижении лимита пропускает команду записи. В результате временные замеры изменились с 67115 на 65691 отсчетов таймера. Улучшение конечно есть, но этого недостаточно. Нужны какие-то радикальные решения, а решение одно - это перенести WallRefresh() на ПЛИС.
Спустя время я сел за изучение этой здоровенной функции. Перенес эту структуру на бумагу, отметил все ключевые переходы. В конце концов, я заметил, что функция по сути своей гоняет одни и те же переменные по кругу, и только в отдельных уголках кода она обращается за текстурой, чтобы скопировать ее в итоговый буфер.
И если свести факт, что идет конкуренция за ДДР и факт, что происходит многочисленное обращение к одним и тем же переменным, получаем явную нехватку кэша в системе. Кэш я отключил еще в самом начале проекта (через disable_caches()), и даже в самом MicroBlaze не включал. Ну, что мне стоит закомментировать одну строчку, верно? Пробуем….
Удалил вызов функции disable_caches(), и о чудо, время выполнения снизилось до 65132 тактов таймера, и это при том, что сам кэш я не добавлял. Вроде бы, кажется, что 65691 и 65132 более-менее сопоставимы и прирост не большой, но этот прирост образовался практически на пустом месте. Я кинулся включать кэш в MB, сразу выставил по 16кб памяти для инструкций и для данных. Собираю, запускаю и ... таймер выдает 1100 – 1300 тактов таймера, ускорение в 65 раз!!! Кэш работает…
Этап 9. Работа с игровым циклом PlayLoop()
Теперь, когда кэш включен и ускорение есть, число кадров увеличилось до 4-5 в секунду, с этим можно уже даже поиграть. Но, по-прежнему, еще остаются места, где можно оптимизировать код. Например, это вывод в UART. Сообщениями Xil_print() я обложил почти весь код, и явно, что эти сообщения тормозят систему. Добавил в проект #define XIL_PRINTF_EN 0, который должен отключать вывод сообщений в UART.
Сделал замер времени выполнения одного цикла игры, оно составило 19500 тактов таймера (не путать с замерами WallRefresh()). После отключения xil_printf() время выполнения сократилось аж до 3000 тактов таймера.
Как видно из видео (вторая половина), при отключении вывода в UART вылазят проблемы, связанные с кэшем. Обратно включать вывод и терять FPS совершенно не хочется, поэтому иду решать проблему с самим кэшем.
Этап 10. Борьба с кэшем.
В работе кэша до конца я не разбираюсь. Я знаю, что кэш нужен для ускорения доступа к труднодоступной памяти, за счет того, что этот фрагмент памяти хранится рядом с процессором и легко доступен. А DDR как раз-таки является труднодоступной памятью, и локальная BRAM предоставляет куда большую скорость доступа, а также кэш разгружает интерфейс до DDR: меньше обращений - легче жить.
А вот в остальных тонкостях хотелось бы разобраться:
Как часто надо вызвать Flush() ?
Насколько выгоднее использовать FlushRange() ?
Какое должно быть соотношение кэша и DDR ? Либо просто какой размер кэша нужен?
На момент подхода к этому вопросу я уже вызывал FlushRange() в прерывании, когда перезапускался DMA. Эффект вроде бы был, но все не то.
Потом я стал мыслить в сторону, если кэш маленький, то и обновляться он будет чаще. Изменил кэш до 1 кбайта, проблема не исчезла, только число FPS упало, где-то в 2-3 раза. После нескольких итераций остановился на 4кб кэша для данных и для инструкций, при таком объеме и число кадров приемлемое и тайминги встали. В общем игра с размером кэша не помогла.
Попробовал сделать двойную буферизацию, в конце игрового цикла с помощью memcpy() копировал сгенерированный фрейм буфер в буфер который выводится на монитор. FPS просели незначительно, но и результата не добился. Не помогло.
Стал беспорядочно втыкать Flush() после разных функций, и допер, что мерцает не сам кадр, а вывод рук на этот кадр. Размышляя в ключе, что нужно чистить кэш после обильного обновления данных, пришел к тому, что в начале функции VW_UpdateScreen () вызываю flush(), а все остальные flush() удалил. По логике, как раз получается так, что процессор обсчитывает кадр в локальной памяти 320х240, прорисовывает все, а потом передает его на обновление уже конечного фрейм буфера, как раз та самая функция Blit1to2(), которая пересчитывает цвета для конечного кадра. Все починилось и работает).
Время работы одного игрового цикла составляет 5к-7к тактов таймера, что соответствует 16-23 кадрам в секунду.
Этап 11. Добавление статус бара персонажа.
Эту стадию я выполнил еще до 10 этапа. Поэтому в видео выше можно наблюдать наличие целого статус-бара. Собирать статус-бар пришлось по кусочкам. Во-первых, координаты отрисовки элементов были напрочь сбиты, и смысл у них отсутствовал. Во-вторых, индексы картинок тоже были не верные. Пришлось сделать промежуточную функцию чтобы сопоставить картинки и индексы между собой. Затем подобрать координаты. Например, аватарка главного героя должна меняться, но из-за этой сложности пришлось оставить статичную.
Этап 12. Планы на будущее.
В данном портировании не реализован звук. Я его намеренно отключил. На каком-то этапе мне выдало ошибку, что не хватает памяти при ее динамическом выделении.
Еще, у меня достаточно кривой интерфейс меню, который видимо тоже придется собирать по кусочкам, как статус-бар, пока нет такого желания.
Дополнительное видео о том, как выглядит меню, включение читов:
Вот как выглядит сам проект в Vivado:

Красный прямоугольник — ускоритель Blit1to2(), DMA и самописный модуль.
Зеленые прямоугольники — вывод картинки на монитор. DMA->FIFO->DataWidthConvertor->VGA.
Оранжевый прямоугольник — модуль работы с клавиатурой. Так же оставил возможность получать управление с кнопок.
Голубой прямоугольник — самописный таймер. Изначально я планировал считать на разных клоках, потом эта идея не понадобилась.
Вот как выглядит распределение ресурсов на ПЛИС:

Голубой – два DMA в проекте
Желтый – MicroBlaze
Красный – AXI Interconnect
Зеленый – MIG7 (DDR)
И общее потребление ресурсов:

Кстати, ПЛИС грелась в пике до 50 градусов.
Весь проект как есть выложил на GitHub: https://github.com/mifa1234/Wolfenstein_on_fpga
Заключение
Портирование этой игры у меня заняло чуть больше пяти месяцев. В процессе я столкнулся с рядом задач, которые для меня оказались новыми. Оборачиваясь назад, вспоминаю, что я не верил в то, что у меня получится добиться запуска. Я впервые что-то портировал.
В ходе портирования я столкнулся с:
Приоритет AXI интерфейсов в интерконнекте.
Работа с кэшем, я его по-прежнему не понимаю, но уже образовались какие-то принципы.
Проверил в деле работу MIG7 при разных настройках.
Оценил на собственном опыте влияние xil_printf() на производительность в С-коде.
Обновил опыт работы с загрузкой прошивки через XSCT Console.
Теперь на всю жизнь запомню, что прерывания нужно очищать.)
Спасибо что дочитали!
