Ringo Render 64x48

UPD(20.12.2022): Исходники оптимизированного рендера доступны на GitHub

Всем привет!

Сегодня я немного расскажу про то как устроен рендер в нашей совсем недавно вышедшей игре Ringo.
Если вдруг пропустили то посмотреть/поиграть можно здесь

К сожалению исходники настолько запутаны, уплотнены и пестрят ошибочными комментариями из прошлых итераций движка что вряд ли кому-то помогут разобраться. Да и сам я на данный момент уже плохо помню как там и что работает. Но так как уже несколько человек попросили меня рассказать про рендер то я попробую осветить хотя бы базовые моменты которые помогли достигнуть результата.



Режим 64х48

Самый лёгкий способ сделать 64х48 на спектруме это воспользоваться двумя экранами и сделать мультиколор 8х4. Пиксели двух экранов заполняются паттерном в виде столбиков, в моём случае это 11110000b, т.е. левая часть знакоместа ink правая papper. Далее мы переключаем экраны каждые 4 строки во время прохода луча в области пикселей чтобы атрибутами второго экрана менять цвета в нижней части знакоместа независимо от верхней.
Ringo Screenshot
Традиционно в моих рендерах всё происходит в два кадра. Сначала в буфер рисуются тайлы, затирая старую картинку, а потом сверху накладываются спрайты и буфер кидается на экран. Изначально я сделал буфер в манках ( munk сокращение от Multicolor Chunk). Этот термин придумал vivid и подробнее про них можно почитать здесь Если коротко то это честный буфер в точках которые потом парами схлапываются в нужный атрибут с помощью pop hl: ldi Это очень красиво и позволило легко анимировать тайлы делая картинку живой, да и скролл по горизонтали с шагом в нашу виртуальную точку 4х4 получался сам собой, но оказалось медленно для моих целей. Я уже смирился с мыслью что придется делать всё в три кадра, но результат мне совсем не понравился. Поэтому всё ушло ко второму варианту — сделать буфер в уплотненном виде, т.е. сразу в формате атрибутов.

Чтобы вырваться на свободу в двух фреймах я решил что нарисую уровень в две быстрые страницы, в одной из них он будет сдвинут и я просто буду копировать в рабочий буфер нужный сдвиг в зависимости от х координаты камеры и накладывать спрайты уже на атрибутный буфер. У меня получилось сделать это всё в два фрейма и осталось куча времени под игру. Я даже сделал физику главного героя и логику камеры, однако в какой-то момент стало ясно что картинка выглядит мёртвой т.к. задний фон статичен. К тому же чтобы его менять нужно потратить много времени, рисуя всё в два буфера фона. После непродолжительной внутренней борьбы было решено перейти к третьему варианту, который является комбинацией первых двух. А именно: перерисовывать весь буфер тайлами и накладывать поверх спрайты, но делать это всё в уплотненном(атрибутном) виде.

В общем виде вырисовывалась такая картина:

Первый кадр:
В верхнем бордюре происходит переброска рабочего буфера в атрибуты двух экранов. Так как времени в верхнем бордюре недостаточно чтобы перебросить все 768х2 байта то продолжаем дальше кидать уже по медленной памяти в нужный момент переключая экраны каждые 4 строки и учитывая задержки памяти. После того как мы закончим кидать атрибуты мы начинаем рисовать тайлы в рабочий буфер также параллельно переключая экраны каждые 4 строки в нужные моменты времени. Здесь уже легче так как рабочий буфер лежит в быстрой памяти. Как только мы проходим весь экран мы заканчиваем рисовать тайлы в буфер, а весь нижний бордер отдаём под игру, т.к. только тут у нас есть возможность использовать код с произвольным временем выполнения.

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

Итак осталось самое сложное, нарисовать максимально быстро 63 тайла в атрибутный буфер и наложит на него спрайты.

Спрайты

В процессе формирования визуального стиля и понимания моих возможностей как художника, я пришел к размеру спрайтов 12х10 точек (на самом деле чистыми 11х10, т.к. каждый спрайт ещё дублируется в сдвинутом виде на одну точку). Спрайт приблизительно 8х8 плюс чёрная окантовка. Спрайт храниться в следующем виде: байт чистого инк+64, байт чистого папера+64… итак 120 байт. 0 считается прозрачным, а +64 нужно чтобы была возможность рисовать чёрным и делать обводку.

Так как спрайты будут накладываться параллельно с переключением экранов нужен код с постоянным временем выполнения. Также нужно было прийти к какой-то минимальной единице вывода чтобы всё легче «параллелить». Спуститься на максимально адовый уровень (сгенерировать код и всунуть в нужные моменты ауты) я не решился.

Пришлось прикидывать как это всё раскидать между линиями в блокноте, это больше походило на записки сумасшедшего. Я перепробовал разные размеры спрайтов и таких записей было куча.

В итоге такой единицей вывода у меня стала печать 2-х точек спрайта или 1-го обычного атрибута.

В виде макроса это выглядело так:

        ;маски перед началом рендера в BC
        ;ld b,00111000b
        ;ld c,00000111b
        ld bc,56*256+7

    MACRO SPRITE_LINE_FAST
        pop de ;10t байт папера байт инка

	xor a ;4
	cp e  ;4
	ld a,(hl);7t берём с экрана
	jr z,1f ;7\12
	and b ;4t оставляем инк
	or e ;4t накладываем папер из спрайта	
	jp 2f ;10t
1:	
        ;13 tdelay
        ld (00000),a ;13t
2:    
        ;50t
	;в аккумуляторе либо байт с экрана либо уже со вставленным папером из выше кода
	ld e,a ; 4t сохраняем eго в е - уже ненужно е
	xor a ;4t
	cp d ;4t
	ld a,e ;4t восстанавливаем
	jr z,1f ;12/7
	and c ;4
	or d  ;4	
	jp 2f  ;10
1:
	;13t delay	
        ld (00000),a ;13t
2:
        ld (hl),a ;7t
        ;48t
    ENDM
    ;98t(!!!!!!!!!!!!!!!!!!!!)

Спрайты выводятся в цикле, что очень сильно сэкономило памяти. Вывод одного спрайта выглядит так:

        exx : out (c),e : exx
        STEP62
        exx : out (c),d : exx
        STEP44
        exx : out (c),e : exx
        STEP26
        exx : out (c),d : exx
        STEP62
        exx : out (c),e : exx
        STEP44
        exx : out (c),d : exx
        STEP26
        exx : out (c),e : exx
        STEP62
        exx : out (c),d : exx
        STEP40

С учётом обвязки вышло что за одно переключение экранов мы успеваем положить 8 точек спрайта (4 атрибута). Так как спрайт шириной 12 точек (6 атрибутов), то нам надо шагать на строку вниз каждые 12 точек. В названии макроса зашифрована эта логика. Например STEP62 — положить 6 атрибутов, шагнуть вниз, положить ещё 2 и.т.д Конечно идеально бы паралеллились спрайты 8х8, но это уже слишком мелко, даже для меня. Для примера приведу один макрос, чтобы была понятна идея:

MACRO STEP62      
        ;20t
        SPRITE_LINE_FAST  ;98t for 2 pixels
        inc hl ;6t next screen pos
        SPRITE_LINE_FAST  ;98t for 2 pixels
        inc hl ;6t next screen pos
        SPRITE_LINE_FAST  ;98t for 2 pixels
        inc hl ;6t next screen pos
        SPRITE_LINE_FAST  ;98t for 2 pixels
        inc hl ;6t next screen pos
        SPRITE_LINE_FAST  ;98t for 2 pixels
        inc hl ;6t next screen pos
        SPRITE_LINE_FAST  ;98t for 2 pixels
        ;step down
        ;               10t         11t
        ld de,attrBufferWidth-5 : add hl,de ; next line

        SPRITE_LINE_FAST  ;98t for 2 pixels
        inc hl ;6t next screen pos
        SPRITE_LINE_FAST  ;98t for 2 pixels
        inc hl;6t
        ;ok 867
        ;45t to 912
        ld (0),bc
        nop : nop : nop : nop
        ld a,r
	ENDM


Тайлы

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

В результате экспериментов мне пришлось познать дзен и абстрагироваться от формата тайлов в принципе и придумать как вообще я смогу совей головой сгенерировать самый быстрый вывод тайлика с наложением левого края на предыдущий тайл. В итоге я пришёл к странной связке pop af: or (hl) и подгонкой формата тайла под него. В итоге я занял последние две быстрые страницы под раскрянченый код вывода тайлов. В одной странице тайлы сдвинуты и накладываются ором. Фрагмент вывода выглядит так:


        pop af ; 10t papper in a
        ld hl,ADDR ;10t        
        or (hl)         ;7t
        ld (hl),a ;7t
        pop hl : ld (ADDR+1),hl ;26
        pop hl : ld (ADDR+3),hl ;26


В другой странице не сдвинуты, но код такой же, за исключением адресов. Это нужно чтобы просто подменять страницу в зависимости от сдвига карты и оставаться с теми же тактами:


        pop af ; 10t papper in a
        ld hl,ADDR ;10t        
        or (hl)         ;7t
        ld (hl),a ;7t
        pop hl : ld (ADDR),hl ;26
        pop hl : ld (ADDR+2),hl ;26


Я надеюсь вы что-нибудь поняли из моих сумбурных записок, потому что к этому моменту я уже устал писать и решил закруглиться :)
Если вдруг будут вопросы или нужны какие-то подробности то пишите в комментариях или в личку. Будет вообще здорово если вы предложите варианты ускорения или другие подходы, т.к. меня не покидает ощущение что я пошёл по странному пути и что-то упустил!

See ya \0/

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

avatar
Почему у этой статьи всё ещё 0 комментариев?
О, уже 1 :)

В общем, Денис, спасибо! По существу пока сказать нечего, надо ещё раз перечитать)
Но я почти всё понял с первого раза! А это редко быавет)
  • sq
  • +1
avatar
Денис, очень интересный пост и прикольные задачки. Немного переписал макрос для вывода пикселей спрайта, надеюсь что не напортачил на сонную голову:

MACRO SPRITE_LINE_FAST
pop de : ld a,(hl)
inc e : dec e : jp nz,1f
jr 2f
1:
and b : or e : nop
2:
; 17+18+12=47t

inc d : dec d : jp nz,1f
jr 2f
1:
and c : or d : nop
2:
ld (hl),a
; 18+12+7=37t
ENDM
; overall: 84t
avatar
К сожалению, твой вывод тайла я не понимаю. Если врублюсь, попробую немного подумать о нём тоже…
avatar
про тайлы это я просто не всё рассказал видимо или не так подробно, там я когда делал голова болела уже, а как описывать стал опять заболела ) я попробую расписать, но не обещаю, потому что смотрю в конвертер и туплю )
avatar
класс! спасибо
avatar
Можно ещё хранить байты спрайтов -1 вместо как сейчас. Тогда прозрачный пиксел будет 255, и каждую пару inc d: dec d можно заменить на просто inc d, это ещё -8 тактов.
avatar
Круто, спасибо! Правда получается что значение в регистре меняется. Но тут даже два решения, либо в конвертере уменьшить на единичку, либо просто nop ниже заменить на dec!
avatar

MACRO SPRITE_LINE_FAST
pop de : ld a,(hl)
inc e : jp nz,1f
jr 2f
1:
dec e : and b : or e
2:

;17+14+12=43t

inc d : jp nz,1f
jr 2f
1:
dec d : and c : or d
2:
ld (hl),a
;14+12+7=33t
ENDM
; overall: 76t
avatar
Вообще я думал про конвертор (всё равно 255 для прозрачности требует в него лезть). Но да, нопы тоже можно пустить в дело, прикольно! :)
avatar
Поступило ещё одно шикарное предложение от Monster^Sage, пока ещё не пробовал закодить т.к. по времени плотно, но обязательно попробую и доложусь )

зачем переключать экраны каждые 4 строки? ведь можно это делать каждые 8 строк, по «середине» атрибута ?
avatar
Проверил, работает )

avatar
Вот это вы сейчас не по правилам сыграли, этож сколько тактов освобождается! Не зря говорят что все гениальное просто :)
avatar
Спасибо, всегда интересно почитать статьи из серии how it works и history of making.

Получается ты сначала пробовал накодить движок и уже потом прикидывал стиль игры, графику, геймплей?
Под какой размер монитора/тв расчитана игра?
avatar
Да, в этот раз сначала движок, потом уже стиль под него. Хотя я примерно знаю свои возможности как игрового художника.

Игра расчитана под любой размер монитора/тв, чем больше размер тем дальше отодвигаться надо )
avatar
Наверняка уже кто-то предложил, потому что вещь совершенно очевидная, но что если битовую область экранов 0 и 1 замостить «шахматкой». Т.о. можно повысить цветовое разрешение картинки добавив цветовых полутонов. А если бы экраны переключались каждый кадр, то на экран можно 1 поместить такую же «шахматку», но инвертированную и получить, фактически, не сильно моргающий гигаскрин.
avatar
Хотя нет. Написал, а потом уже подумал. Не получится, т.к. у нас по-горизонтали только два возможных цвета. Этот трюк работает только в пределах целого знакоместа. Жаль.
avatar
да, часто тоже так хочется )тут или stellar mode или делать широкие пиксели 8х4.
avatar
avatar
Ещё одно предложение по спрайтам от Monster^Sage с последующей доводкой introspec
Если коротко то формат спрайта меняется на байт маски+уплотненный байт спрайта, в конверторе генерируем маску зная где у нас прозрачный пиксел а где нет. вывод такой:


pop bc ;берём маску в с и уплотненный байтик в b
ld a, (hl) ;берём байт
and c ;накладываем маску
or b ;орим
ld (hl), a ;кладем в экран


Итого 32т на две точки. По итогу сумасшедшее ускорение от моего первоначального варианта.

Я постараюсь всё это оформляю в отдельный опенсорсный движок-TSU, будет 8 спрайтов против 6 в Ringo, размер вырастет на две линии в высоту и плюс ещё 16 маленьких спрайтов 5х5. Также полностью доступны два нижних бордюра на код игры.
avatar
UPD(20.12.2022) Исходники оптимизированного рендера доступны на GitHub
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.