LD/PUSH в стиле Apple IIgs

Совсем недавно на хабре появилась переводная статья про ускорение графики на машине с ЦП Motorola 6809 за счёт использования трюка с двумя стеками этого процессора.
Возможно это совпадение, но оригинальную статью совсем недавно упоминали на nesdev.com, но при этом рядом упоминалась другая статья (англ.) про на мой взгляд еще более изощрённое ускорение графики на машине Apple IIgs.
Перескажу её как можно более вкратце…

В основе трюка лежит по сути своей техника известная у спектрумистов (я буду проводить аналогии со спектрумом) как LD/PUSH.
Это когда мы предполагаем сверхбыстро перелить данные в предварительно настроенный стек потоком инструкций вида:

LD HL, const16
PUSH HL

Здесь за 4 байта команд мы вдвигаем в стек 2 байта полезных в них данных. Настроив указатель стека куда надо и повторяя эти инструкции в развёрнутом цикле мы наверное максимально эффективно по тактам можем залить заранее подготовленными данными какой то буфер, например экран.

Трудился в Apple IIgs 16-битный процессор 65C816, а видеопамять была линейной с нужным видеорежимом в 4 бита на пиксель, т.е. один байт — два пикселя.
А еще в процессоре была инструкция PEA const16 (Push Effective Address) которая сразу пушила в стек непосредственное данное не загрязняя регистры, так что работало такое еще быстрее.
В результате получалось то, что автор называет «PEA field» и за один присест очень быстро заливало экран пикселями заднего фона:

Здесь автор изобразил по 2 пикселя графических данных стоящих за каждой командой PEA. Движок сайта немного смазал картинку — белые столбцы это инструкции PEA, а пиксели — это их непосредственные данные.

Самое приятное тут то, что зациклив эти инструкции в циклическом буфере и управляя тем куда мы прыгаем в нём прыгаем и откуда выходим — мы получаем «бесплатный» скроллинг:

Немного модифицируем буфер и обновляя на каждом кадре только узкую полоску пикселей в нужном столбце PEA авторы (речь вообще в статье идёт о любительском порте Супер Марио) реализовали сравнительно дешёвый скроллинг с полным затиранием экрана так что остаётся только залить сверху спрайты и движок готов. Скроллинг по горизонтали как понятно гранулярен двум пикселям. А вот скроллинг по вертикали (авторам он был не нужен) может быть попиксельным. Могу отметить, что на спектруме горизонтальный скроллинг уже познакоместный, но самое неприятное что мешается нелинейная раскладка видеопамяти и по первому размышлению я прихожу к выводу что таким образом возможен только вертикальный скроллинг и то с некоторыми неприятными накладными расходами на обновление перенастроек стека на каждой строке. Действительно я такое из любопытства с полгода назад сам реализовывал и техника весьма быстра, хотя конечно гарантирует филлрейт, что в других движках может уже стать недостатком.

Однако авторы на этом не остановились и пошли еще дальше реализовав два независимо прокручиваемых задних фона:

(её уже только в виде демки)
И это не два PEA-трюка выполняемых один после другого (с банальной реализацией прозрачности), но всё тоже одиное PEA-поле модифицированное еще одним способом.

В 65C816 есть еще одна инструкция: PEI (Push Effective Indirect). Она тоже помещает слово в стек, но считывает его из адреса в direct page (расширение концепции zero page во многих 8-битках). Эта инструкция двухбайтовая и параметр-байт складывается с регистром базы DP (direct page) и из полученного адреса берётся слово и помещается в стек.
И вот тут делается следующий трюк — инструкции PEA в пикселях буфера где должен быть второй задний фон заменяются на инструкции PEI:

Их приходится добивать операцией NOP, но вот почему игра стоит свеч — управляя регистром DP мы теперь можем контролировать откуда именно «пиксели» с DP по строкам берут информацию о цветах и если залить некоторую область циклически повторяемыми паттернами (на что самый задний фон весьма хороший кандидат) мы можем создать полную иллюзию двух независимо скроллируемых задних фонов!
Это уже меня удивило и порадовало своей изобретательностью! Даже не думал, что наткнусь на то, что технику LD/PUSH (уже без LD конечно в Apple IIgs) можно так усовершенствовать! :)

14 комментариев

avatar
Ссылку на пост на Хабре мне прислали товарищи, и хотя читать её было весело, по сути ничего нового для квалифицированного Z80 кодера там не сообщалось. А вот этот трюк со вторым планом я пока нигде не встречал, спасибо за пересказ. Нужно будет прочесть первоисточник целиком, похоже что там у ребят всё очень благополучно с креативным мышлением.
avatar
По ссылке там есть еще вторая часть статьи где описывается трюк с туманом. Я глубоко не вчитывался, так поверхностно текст просканировал глазами и похоже что какие то трюки с лукап-таблицами и вроде тоже непростые.
avatar
Да, я прочёл. Ближе всего идея тумана похожа на нюковский запилятор, но применённый не к графике, а напрямую к коду.
avatar
Спасибо за статейку! Читать такие вещи очень интересно, на хабре читал про 6809 с огромным интересом и продолжение про apple вполне себе дополняет. Особенно интересно было узнать что у 65С816 есть такие инструкции и DP.

Как то с другом мы обсуждали возможности в разрезе Clear reander engine vs Sprite/Tile Engine. Ну и закусились на тему, а что было бы если бы у бабушки были бы… если бы было три спрайтовых плана с приоритетом отрисовки (подобие Z-буфера). Было бы мол тогда интересней программить в ЯВУ те же танчики? Ну сказано сделано, примерно недели две возни с верилогом и готовый адаптер для отрисовки такой графики появился. Повозится конечно пришлось, что бы выйти на времянки 1024х768 (ЕМНИП). 12 стадий в конвейере устройства, причем регистры на стадии в ФУ конвейера, вовсе нет — простенькие мультиплексоры даже пришлось огораживать. Какое то время мне казалось что я не вывожу на этой ПЛИС задачу и как бы вообще на голую шину не пришлось ставить регистры в отрезки между ФУ. Но нет обошлось 12 стадиями. Жуткое чудо юдо.


Да, но это я к тому что вот это вот убер устройство хотели повесить на корку 6502, ну и оно не потянуло. Потом я рассматривал 6809 и тоже нет, не потянуло :) Потому что, если бы только проблемы отрисовки спрайтов поверх других спрайтов с аппаратным перекрытием, а ведь еще и скроллинг этих спрайтов попиксельно в памяти самих спрайтов. После этого как то утвердился в мысли о том что такого рода аппаратура мало что даст для программера, нужен именно прокаченный процессор и желательно такой который может за такт не одну инструкцию, и при это еще бы и асинхронно.
  • SAA
  • +1
avatar
если бы было три спрайтовых плана с приоритетом отрисовки
Непонятно что это точно означает.
avatar
т.е. стандартно мы бы накладывали поверх спрайт скажем с травой, спрайт с дорогой, поверх которой накладывался бы «танчик». Т.е. все три спрайта находятся в своих пространствах по одной координате X,Y. Но маскируются друг другом с приоритетом по каналу. Канал 1 перекрывает канал 2, канал 2 перекрывает канал 3, пиксель с цветом «прозрачный» может вылезти на канал 1 из любого канала. Наверное я все еще сильней запутал, но смысл очень простой. Есть три экрана спрайтов заданных байтом 32х24 знакоместа. Это три независимых плана, которые контроллер объединяет в один видимый на экране монитора. Спрайты находятся в своих областях ОЗУ как объекты 32х32х256. Прозрачный цвет не маскируется (допустим 0x00) и через него мы можем увидеть любой пиксель спрайта нижнего плана (2,3).
avatar
Такое ощущение, что тут речь не про спрайты как они преимущественно понимаются — массив имеющих независимые координаты объектов на экране где мы выбираем какие тайлы в спрайтах отображаются, но их количество как правило не покрывает все пиксели экрана и главное что они все могут быть выведены хоть в одну координату оставив остальной экран пустой. Тут же больше похоже что речь про тайловые фоны — квадратно-гнездовые сетки из тайлов иногда аппаратно скроллируемые, но как одно целое. Они по определению всегда покрывают весь экран и образуют квадратно-гнездовую сетку. В последнем случае становится понятно почему нужно попиксельно возится со скроллингом «спрайтов» в памяти спрайтов. Но даже если и так, то всё-равно не очень понятно что тут 8-битные процессоры не вывозили — если выводом такой графики заведует отдельная микросхема то они хотя бы небольшое количество тайлов и не вижу куда бы тут танчики не влезли — подвижных там не очень много… Не кажется чем то сверхъестественным.
avatar
Спрайт и Тайл — думал это одно и тоже. Представим себе массивы 32 x 24 таких массивов 3 штуки.

char layer[3][32*24];

Я перешел на Си, что было понятно о чем я. Это не сам спрайт, разумеется. Но, ячейка массива содержит число от 0 до 255, являющееся индексом (idx) спрайта. До спрайта мы еще не добрались, так как лежит он в каком то линейном участке памяти по смещению idx*sizeof(sprite). Если спрайт 32х32 8 бит (говорю «если» потому что уже не помню точно что там было) цвета каждой точки то его sizeof = 1024 байта или 1Кб. Вопрос доступны ли они все в пространстве процессора? Конечно нет, так как даже 64 таких спрайта займут все адресное пространство к примеру 6502. Но железке на это наплевать так как считанный адрес спрайта, например address = layer[0][idx] << 10, это просто шина нужной разрядности с которой адрес приходит на ША блочного ОЗУ ПЛИС. Конечно там начинается сложность с тем что развертка по строке движется линейно по экрану, переходя с одного спрайта на другой и поэтому часть адреса постоянна, а часть обновляется счетчиком — но не об этом сейчас. И так у нас три канала по которым летят данные из области ОЗУ с битмапом спрайта, с его непосредственным содержимым. Эти три канала D0,D1,D2 мы можем перемешать по принципу встреченного байта прозрачности (для простоты просто нуля). Если в D0 в данном такте появился 0 то значит нужно выдать ARGB из канала D1, но если и в канале D1 у нас тоже прозрачный пиксель (ноль), то берем ARGB из канала D2. Это все довольно просто описывается в HDL, не в ЯВУ.
Имея такую аппаратную примочку мы освобождаем процессор только от маскирования потока из областей спрайтов и необходимости постоянно лить данные спрайтов, когда они меняются на экране, в фрейм-буфер. Для Спрайтового движка понятия фрейм-буфер я считаю не применимо. Это по сути текстовый экран только буква не 8х8 а 32х32 и летит не с знакогенератора (который может быть и в ПЗУ), а из области ОЗУ со спрайтом. Мы меняем код спрайта в этом слое и в течении 12 тактов, без каких либо затрат на пересылку байт битмапа 32х32 (а это прилично!) получаем смену изображения. Все что нам нужно это просто загодя (за 12 тактов раньше) начать формировать растр по строке. Так как данные будут сыпаться в этой конвейер друг за другом, перерывов между ними не будет и работать можно с дичайшим FPS меняя спрайты всего экрана чуть ли не 100 раз за кадр. Заменить 768 байт на новые — это не вопрос даже для 6502 с 1,75МГц клока.
Но… грустно становится когда нам надо произвести скролинг внутри знакоместа, ну кого к примеру устроит резкий прыжок танчика на 32 точки вперед? И вот тут бывает так что танчик то не 32х32, а нечто побольше. Мало того хотелось бы двигать его не только прямо, а скажем по диагонали или вообще под произвольным углом. И мы начинаем копировать байты внутри спрайтовой области ОЗУ. А поскольку плана у нас три, и мы хотели бы что то еще двигать на задних планах, траву, кроны деревьев, какие либо еще произвольные объекты. То увы даже загнав имеющуюся корку 6502 на 50МГц (выше не получалось) удается разве что 20FPS обеспечить. Разговор конечно не про один объект, а про кучу различных. И даже 6809 выдавал до 30FPS на сценах, как ниже. Не из за одного «танчика», конечно это все затевалось. У меня к сожалению ничего продемонстрировать не осталось из того проекта, так как уже на симулях стало понятно — что это все не то. Но можно показать к чему пришли.
В итоге это все вылилось в фрейм-буфер 1024х768х8бит с двойной буферизацией, и то что там применено в качестве процессора :) отдельная песня — вывозило только 75% заполнения экрана. Но вот даже такая демка, которую для этой аппаратуры писал мой товарищ — 8 битные корки и даже 16 битные корки процессоров ложило наглухо. А это наверное 10%-20% имеющейся пропускной.
Demo clear render
avatar
Ясно. Классические аркадно-консольные архитектуры решали все такие проблемы методично ровно по линии меньшего сопротивления:
а) обновлять VRAM можно только во время VBlank
б) слой(и) фона аппаратно скроллируется «с проворотом» так что скроллерам надо лишь раз на 8 пикселей прокрутки обновлять одну полоску тайлов фона(ов)
в) спрайты это отдельный фон из независимых объектов (отчего были всегда лимиты на количество спрайтов отображаемых в одной строке)
г) цвета палетризированы в два этапа — в плитке фона или спрайте есть селектор одной из нескольких разных палитр отчего информация о цвете еще сжимается.
Таково как бы классическое противостояние между Tile/Sprite Engine и Framebuffer render.
avatar
Да все отлично описали! Особенно пункт б) автоскролинг реализованный на счетчике адреса чтения данных спрайта со смещением — классика, там по сути делать ничего не надо, пиши смещение и весь экран горизонтально или вертикально скролируется. Но :) хотелось чего то очень продвинутого и универсального, без заморочек, настолько быстрого что бы можно было писать в ЯВУ, даже без оптимизации — и в итоге для нас это оказался clear render для frame buffer. Рискну предположить почему так, потому что читать про преодолевание трудностей это одно — здорово, очень интересно, испытываешь кураж прошедшего головоломное приключение человека и гордишься им. Однако читая ты не можешь оставить без анализа те трудности которые пришлось преодолевать другому и заранее закладываешь себе дорожку без них. С одной стороны — красивый хак, с другой — универсальность и избежание хака. Ну это так мысли в слух, возможно я не прав :)
avatar
Хех, «интереснее» — это в смысле преодоления собственноручно созданных себе трудностей? И зачем на полноценную корку вешать функции очень примитивного блиттера, какой в чистом виде получился бы быстрее и проще?

Что касается аппаратных тайлоспрайтов, это же древнее решение для древних же проблем, которые давно отпали сами собой (то есть тормознутости и малых объёмов видеопамяти). Никаких разумных причин применять это решение сейчас при создании нового псевдоретро железа не вижу. Смысл есть только в ускорении относительно старого готового железа новой прошивкой (дендиконфа)
avatar
«интереснее» в том что от стандартных корок, которые не тянули, зато имели свои инструменты вроде ЯВУ и трансляторов ассемблера, пришлось перейти к собственной архитектуре которая обеспечивала приличную пропускную, но потребовала написать и трансляторы и компилятор и отладчики. Это все очень интересно, любительство оно такое в чем то беспощадное и требовательное, азартное :)

Суть же телодвижений с спрайт-тайтлами, ну в какой то момент показалось что имея аппаратное наложение слоев друг на друга, можно наслаждаться отрисовкой в квази-фрейм буфере (можно так группировать тайтлы/спрайты что в области графического представления, в области спрайтов — будете работать с массивом небольшой размерности, скажем 256х128 или 256х256. И это казалось что ли убер экономией видеопамяти, казалось конечно… Ну аппаратное наложение слоев конечно сильно экономило время и такты, но вот отрисовка в области спрайтов какой либо анимации… все равно вылилось в полноценный фрейм-буфер, который из блочного ОЗУ ПЛИС само собой переполз в SDRAM.
avatar
Таки нашел, глубоко в историю ушло :) Если мне опять же память не изменяет то тут 6809, и конечно с одним единственным танком он справляется. Танк состоит из 4 спрайтов. Все в одном слое, видео с наложением слоев не нашел, только фото монитора. Видно что на один такой примитив да успевает отрабатываться.
Наложение с прозрачностью
Демо вращение гусениц в 4 спрайтах
avatar
Потрясающее комьюнити. Отличные знания.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.