Немного про чанки на ZX Spectrum (и творчество нашей микро-команды Sage Group)

by Monster^Sage

Что такое чанки можно прочитать здесь (первое, что было найдено по теме на просторах интернета). Мало ли, вдруг эту статью будут читать те, кто не знаком с темой.

Речь пойдет полутоновые чанки и про эффект, который был реализован в “электронной газете” BornDead #05. Точнее про его историю. Как это выглядело можно посмотреть в видео-варианте этого номера газеты. Или для ЛЛ сразу на gif-ке ниже:



На тот момент (более двадцати лет назад, конец 1998 года…), мода на 4x4 чанки была в самом разгаре. Уже вышли такие шедевры, как Shit 4 Brainz, Power Up, Refresh, Forever, а впереди были еще более крутые работы, такие как Napalm, Dogma и т.п. с активным использованием таких чанков.

Конечно же хотелось сделать что-то подобное, но на полноценное демо или добротное интро катастрофически не хватало свободного времени. И одновременно хотелось чем то удивить аудиторию. А иначе какой смысл?

После просмотра эффектов в таком формате постоянно оставалось ощущение, что это слишком медленно, слишком низкий fps, не хватает динамики. 3D эффекты вообще выглядели как “пошаговая стратегия”. Приемлемый fps получался, если рисовали через линию, но выглядело это “темнее” и хуже. Можно рисовать через линию один кадр, только четные линии, а потом следующий – нечетные (интерлейсинг), чтобы получить визуально полноценную картинку. Но в таком случае всё будет “размытым”, эдакий motion blur, что тоже выглядит так себе на динамичных эффектах.

Чаще всего для вывода 4x4 чанков использовали вот такой код (а бывало, что и еще более медленный):

Вывод на экран первой пары чанков:
POP HL
LD C, (HL)

LD A, (BC)
LD (DE), A		
INC D
INC B

LD A, (BC)
LD (DE), A
INC D
INC B

LD A, (BC)
LD (DE), A
INC D
INC B

LD A, (BC)
LD (DE), A
INC E

Такты
10
7

7
7
4
4

7
7
4
4

7
7
4
4

7
7
4

И в обратную сторону, “змейкой”, второй пары чанков:
POP HL
LD C, (HL)

LD A, (BC)
LD (DE), A		
DEC D
DEC B

LD A, (BC)
LD (DE), A
DEC D
DEC B

LD A, (BC)
LD (DE), A
DEC D
DEC B

LD A, (BC)
LD (DE), A
INC E

Регистр SP указывает на чанковый буфер. BC на визуал чанков (парами), а DE на экранную область памяти.

Краткая историческая справка:
Статья на speccy.info говорит, что 4x4 чанки впервые появились на спектруме в 1997 году (Enlight ‘97), сразу в трех демах: Eye Ache 2, Power Up и Shit 4 Brains. Именно в Power Up, в финальной части, использовался такой же код, который представлен выше. В других местах этой демы, а так же в S4B и Eye Ache 2 для вывода чанков были использованы более медленные варианты, имхо. Сложно объективно оценить их быстродействие, так как многие из них “смешаны” с кодом эффектов.
А позже такой же код использовался в Refresh (1998 год), в Napalm (1999 год), в Dogma (2000 год) и т.п.

Итого 101 такт на пару чанков. И для отрисовки всего экрана требуется минимум 155136 тактов (если я нигде не ошибся, всё таки прошло более двадцати лет …). Примерно 21-23 фпс, в зависимости от машины. Вполне приемлемо. А есть еще сам эффект, который нужно просчитать и результаты положить в чанковый буфер. В итоге хоть что-то, похожее на эффект, окончательно завалит fps.

Сразу возникало недоумение. Почему именно так? Ведь копирование это слишком медленно! Тем более что именно оно занимает бОльшую часть времени.

Пока не будем забегать вперед и зададим такой вопрос. А есть альтернативные варианты на основе копирования? Ведь у нас для этого есть специализированные опкоды LDI и LDD. Которые не только копируют, но и почти бесплатно инкрементят/декрементят пары регистров HL, DE, BC.

Да, есть.

Вот такая конструкция (которая впервые была замечена мной в Goa 4k, в 1998 году) позволяет отрисовать ¼ кадра. Каждую четвертую линию для каждой пары чанков:

POP HL	;10
LDI	;16

И повторив полную процедуру отрисовки четыре раза можно получить требуемое. У такой конструкции есть свои ограничения. Но это не важно, так как она еще медленнее, чем предыдущая процедура – 104 такта на пару чанков.

А быстрее? Ведь у нас имеется неиспользуемый BC!

POP HL		;10
LDD		;16
LD A, (HL)	;7
LD (BC), A	;7

Можем отрисовать сразу 2 байта визуала чанка. 80 тактов на пару чанков.
Конечно тут поменяется формат чанка, меньше удобства (рисуем задом наперед) и ограничений еще больше. Если отрисовывать через интерлейсинг, то всё вполне норм. А вот полноценные чанки (16 градаций с равномерным паттерном) без интерлейсинга потребуют 128К машину и переключение страниц памяти.

Уже хорошо, но наша газета на тот момент была в формате 48К и главное – можно быстрее! В итоге зародился эдакий личный челлендж – сделать вывод чанков максимально быстрым. И сделать микро-интро в газету, если челлендж завершится успехом.

Первое, что пришло в голову – а можно ли отрисовать это через стек конструкцией LD HL,*: PUSH HL? Увы, слишком много комбинаций. Просто не хватит памяти, нереально. Да и звучит это слегка безумно :)

Тогда остается второй, самый логичный вариант. Однозначно надо избавляться от копирования и выводить данные чанков на экран через пред-подготовленный код. Да, это займет больше памяти, но наверняка можно найти разумный компромисс.

Так о чем конкретно речь? Конструкция для вывода пары чанков, без учета “обвязки”, должна быть такой:


LD (HL), *	;10
INC H		;4
LD (HL), *	;10
INC H		;4
LD (HL), *	;10
INC H		;4
LD (HL), *	;10
…

Где * это байт визуала пары чанков. И мы должны сгенерить код в памяти для всех 256-ти возможных вариантов. По памяти мы точно помещаемся. Без “обвязки”, которая должна быть быстрой и легковесной, это займет менее 3 Кб. И пока 52 такта на пару чанков (11 байт)…

Но самый главный вопрос – как перемещаться между такими кусками кода?
Да и переместиться к следующему знакоместу было бы неплохо. А совсем отлично было бы отрисовать следующую пару “змейкой”.

Первое, что пришло в голову, это использовать POP для извлечения пары значений чанков из буфера. Примерно так же, как это было в “классических” конструкциях вывода чанков:

EXX		;4
POP HL		;10
JP (HL)		;4
EXX		;4

22 такта (+4 байта). Итого 74 такта. Но нужно не забывать, что нам еще требуется переместиться к следующему знакоместу. И если повезет, то продолжить рисовать “змейкой” и тогда перемещение будет стоить нам всего 4 такта (итого 78 тактов — мизерный выигрыш по сравнению с 80-ю тактами, если честно).

А если вот так?

POP IX		;14
JP (IX)		;8

Увы, это тоже самое и по тактам и по размеру в байтах.

Другого решения пока не было видно…

Так… А в каком формате должны быть значения чанков в буфере, чтобы всё это работало? Одна конструкция займет 15 байт + 1 байт для INC L, если повезет со “змейкой”. Невероятно, но это ровно 16 байт и значения чанков могут выглядеть
как #X0, гдe X это “яркость” чанка… А при таком раскладе минимальный адрес для джампа будет #0000, а максимальный #F0F0… Стоп! Приехали… :)

Вроде можно выкрутиться, старший бит должен быть всегда включен, тогда формат чанка будет %1XXX0000, но это всего 7 градаций яркости (не полноценные чанки) и о “змейке” можно уже не мечтать, а тогда наша конструкция не влезет в 16 байт :(
Это тупик.

Но ведь должен же быть выход!
В итоге эта “гениальная” идея была оставлена в покое. Где то неделю она крутилась в голове. Никак не желала покидать меня. В уме перебирались разные варианты…

В какой то момент пришло озарение. Зачем вообще для перехода от процедуры к процедуре использовать POP RR: JP (RR)? Это ведь бред какой то, когда у нас есть полный аналог – RET! Делает абсолютно тоже самое, причем быстрее в два раза и компактнее в 4 раза (1 байт).

Далее пришла мысль “плясать” от возможных вариантов формата чанка, а их по сути всего два — %1XXXX000 и %10XXXX00 (или %11XXXX00, как удобнее по памяти). В первом варианте у нас есть 8 байт на некий код, а во втором 4 байта. Меньшее кол-во не позволит разместить что либо полезное. И вот этим промежуточным кодом в итоге стал джамп на процедуру отрисовки, для которого вполне достаточно 4 байт. Конечно это добавляет еще 10 тактов на каждую пару чанков, но это терпимо по сравнению со всеми другими решения, которые сделают работу с буфером чанков крайне неудобными (про это см. ниже).

А следующей проблемой стала часть кода, которая должна пересчитывать экранный адрес для отрисовки… На этом моё терпение кончилось и дедлайн выхода нового номера газеты поджимал :) Пока было решено было остановиться на варианте, описанном в 5-ом номере BornDead. Конечно это уже не 4x4 чанки, а 4x6, но слишком уж много было у него плюсов – эффект отлично попадал в стиль газеты (одно знакоместо 4x6 пикселов), визуально выглядел на весь экран, но отрисовывалось всего шесть линий из восьми в каждом знакоместе (отмазываемся, прикрываясь стилем ;), был более быстрый расчет самого эффекта (всего 768 чанков против 1536), уже имелся готовый арт для карты смещений из предыдущего номера и высокий fps в итоге (>25 fps). Это однозначно выглядело “живее и плавнее”, чем в демках того времени, да еще и на полный экран.

А можно ли было именно этот вариант сделать более быстрым?

Конечно да. Просто времени на это уже не оставалось, нужно было выпускать номер.

И вот каким образом:

  1. Не восстанавливать каждый раз регистр H и рисовать “змейкой” (минус 4 такта на пару чанков). Правда формат чанков стал бы менее удобным. Все четные чанки (младший байт) в буфере должны были бы выглядеть так %10XXXX00, а все нечетные (старший байт) так %11XXXX00 (или, как удобнее в памяти, четные %00XXXX00, а нечетные %10XXXX00). И при расчете эффекта дополнительно выставлять старший бит в единицу для всех нечетных.
  2. Вместо RET NZ использовать просто RET (минус 1 такт на пару чанков). Но буфер чанков пришлось бы “разорвать”, поместить в него два адреса на процедуру перерасчета экранного адреса при межсегментном переходе. То есть в буфере сначала будут размещены 256 значений чанков, потом 2 байта с адресом процедуры перерасчета экранного адреса, потом снова 256 чанков и 2 байта адреса процедуры, и последние 256 чанков.
  3. У нас имеются неиспользуемые регистры. А с учетом первого пункта у нас используется только HL. А это позволяет нам использовать не только LD (HL), * для отрисовки в экранную область, но и LD (HL), A / B / C / D / E. Заранее поместив в регистры A, BC, DE самые часто используемые значения визуала чанков (#55, #AA, #00, #50, #05) мы могли бы получить прирост скорости отрисовки в примерно 8-9% в зависимости от картинки (в идеале максимум ~16%).

На тот момент эта экономия в 3840+ тактов только добавляла головной боли :)

А теперь давайте вернемся к 4x4 чанкам и прикинем, как могла бы выглядеть процедура отрисовки с учетом всех оптимизаций (и неудобств), описанных выше.

Вывод на экран первой пары чанков:
LD (HL), *
INC H
LD (HL), *
INC H
LD (HL), *
INC H
LD (HL), *
INC L
RET

и JP *

Такты
10
4
10
4
10
4
10
4
10

10

И в обратную сторону, “змейкой”, второй пары чанков:
LD (HL), *
DEC H
LD (HL), *
DEC H
LD (HL), *
DEC H
LD (HL), *
INC L
RET

и JP *


Это 76 тактов на пару чанков и минимум 64, если используем LD (HL), A / B / C / D / E и выводим один из самых частых байтов визуала чанка (98304-116736 тактов на весь экран).

При этом каждые 64 байта в буфере чанков придется один раз расставить адреса на процедуры для расчета следующего экранного адреса (перемещение на 4 линии ниже). Например так: LD HL,*; RET. Это еще 0,625 такта на каждую пару чанков или 940 тактов на весь экран. Причем нам придется “перешагивать” эти два байта каждый раз в процедуре расчета эффекта.

Будет честным еще учесть то, что на нужно выставлять один старший бит в единицу для всех нечетных чанков для “змейки”, допустим через OR Reg в 4 такта. Конечно это косвенно относится к отрисовке буфера, но именно процедура отрисовки и формат чанка принуждают нас тратить на это процессорное время. Это займет еще 2 такта на каждую пару чанков или 3072 такта на весь экран.

То есть при самом плохом раскладе 78,625 на пару чанков, если нигде не ошибся. Неплохо конечно, но хочется еще быстрее :)

Возможно ли? А почему бы и нет!

Ведь финальная процедура отрисовки занимает максимум 13 байт (минимум 9). И мы можем избавиться от джампов, сэкономив 10 тактов на пару чанков в ущерб удобству формата чанка. Например если будем использовать такой формат для пары чанков:

% 111ZYYYY XXXX0000, который сразу будет являться адресом процедуры отрисовки

Где XXXX это яркость для четных чанков, YYYY для нечетных, а Z это бит для “змейки”. Исходные данные чанков для эффекта мы можем хранить в формате % XXXX 0000 и для четных оставлять как есть, без каких либо модификаций, а для нечетных преобразовывать их через таблицу в процедуре эффекта. В идеальном случае это не займет много времени.

Например:
LD L, A		;4
LD A, (HL)	;7


И с включенным битом Z для “змейки”, если у нас будет свободный регистр:
LD L, A		;4
LD A, (HL)	;7
OR Reg		;4


Если пересчитать в тактах на пару чанков, то получится максимум
(66 + (66+11) + 66 + (66+15)) / 4 = 72,5 такта и плюс 0,625 такта на пересчет экранного адреса. Итого максимум 73,125 такта на пару и минимум 61,125 при выводе самых частых байтов визуала чанков через регистры.

Возможно аналогичные табличные преобразования будут необходимой частью эффекта, если например значения яркостей складываются и результат надо поделить. И тогда потерями на преобразования можно пренебречь. Точнее не считать их недостатком нашего метода отрисовки чанков на экран :)

Если же мы рисуем статику (предрассчитанные кадры анимации или видео), то отрисовка пары займет всего лишь от 54,625 до 66,625 тактов на пару чанков или для всего экрана от 83904 до 102336 тактов. Скорее всего, в зависимости от содержимого, получится ~90000 тактов или ~40 fps. Согласитесь, что для спекки это круто!

Жаль, что этот интересный метод вывода так никто и не использовал в дальнейшем. Хотя конечно он добавляет головной боли :) Надеюсь вам понравилась хотя бы эта история.

Успехов в творчестве!

Monster^Sage, 2022

2 комментария

avatar
Статья не моя, поэтому все плюсы и слава должны достаться MixailV aka Monster^Sage :)
  • sq
  • 0
avatar
diver4d , пора писать статью про гамма-чанки, в чём была их идея (и почему мы (я) налажали, и из-за этого никто ничего не понял :)
  • sq
  • 0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.