Как перестать кодить и начать патчить

Итак, я продолжу рассказы на тему портирования игр с других платформ на систему ZX Enhanced (TS Config).
Цель cейчас прежняя – Sega Master System. Будем уточнять понимание системы путём патча игрового кода некой игры. Не Соником единым :)

Давай верно настроимся. Здесь будет код, здесь будут байты. Давай drum ' n ' byte.


Итак, на этот раз я взял цветастую игруху с Сеги – Ottifants. Обьем – 256К. Исходников – нет, работаем с бинарём. Цветная – ураган. Платформер. Смешная :)
Буду юзать наработки Сонника.


Итак, для старта подобной игры на твоей любимой платформе необходимо сделать следующее:
1. Организовать менеджер памяти для управления нашими страницами
2. Пропатчить вывод в видео процессор, VDP
3. Организация вывода тайлов, организовать вывод графики в проц. Это основная сложность.
4. Патч вывода в звуковой чип.

И вот сейчас у нас — микрохирургия. Правим байты. Бережно.
Нужно учесть – все производимые тобою изменения кода повлияют на работу всей программы. Используемые регистры – нужно сохранить и восстановить. Изменение размера стека не имеет каких-то глобальных последствий в большинстве случаев. Но нужно уложиться в размер исходного кода!

Берём Emulicious, смотрим как работает исходный код. Из отладчика эмуля можно сохранить декомпиленную версию, которая нам пригодится для постоянных уточнений — что и где было до момента, когда мы патчим их красивый код своими этими руками. Естественно, с эмулем придётся дружить – нужно уточнять, как программа работает в обоих эмулях – соответствие данных в регистрах при вызове, их изменение в процессе.

1. Система управления памятью работает через запись в три ячейки — $fffd, $fffe, $ffff, которые относятся к окнам 0, 1, 2 — $0000, $4000, $8000.
Игруха, как принято на этой сеге, работает с адреса 0. Код данной игры – 32к. В данном случае переключение страниц происходит только для 2го окна — $8000 — $bfff.
Ок! Я делаю просто – пробегаем всю память кода и сравниваем на предмет записи в $ffff.
Три байта ld ($ffff),a отлично подменяются на call set_page2, в которой не только меняется пага (ld bc,PAGE2: out (c),a ), но и сохраняется состояние ячейки $ffff – игра этим пользуется.

Интересно, что у системы есть всего 8 кб памяти. На всё.
Эта память начинается с адреса #e000, всё выше для системы является «шедоу». Тут мы и расположимся со всем необходимым. Практически весь набор процедур будет лежать именно здесь.

Итак, оригинальный файл загружен в память, нужные паги установлены в окна, патч управлением памяти пройден.
Давай запустим :)

Итак, стартуем. 0, начало.
Первое что видим – IM 1. Парни, это хорошее прерывание. На RST 38. Удобно!
Вторая штучка – RST xx. Отличная команда, незаслуженно обойдённая вниманием сценеров под 128к. По понятным причинам, на самом деле :)

Для оценки работы программы стоит использовать DEBUG VIEW эмулятора тс-конф. Он нам покажет жизненность программы – смотрим как работает процессор, как выполняются тайные циклы игры, наблюдаем большие разноцветные полосы на экране, связанные с отправкой граф. данных в порт vdp, цикл выполнения как-то изменяется (судя по занятости процесора), игра работает сама в себе – но ты видишь, что нет CPU HALTED и прочих неприятностей, связанных с неаккуратностью первого, большого патча кода – менеджер памяти.
Это – радует.

2. Приступим ко второму, самому интересному пункту – работа с VDP.
Видеочип – неплох. Симпатичен даже. По своему удобен, по своему – нет. Но, тем не менее, он довольно увесист, и довольно могуч!
Для полноты понимания – стоит обратиться к докам, которые полностью описывают работу с видеопроцом. Я же расскажу об общих паттернах работы с ним.

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

С самого начала работы с видео, в систему отправляется 2 байта. Первый из которых всегда — данные, второй – всегда команда и данные, как две тетрады. Значимыми являются два старших бита, которые описывают КОМАНДУ для видеопроцессора.

Выглядит это примерно так:
ld a, $E0
	out (Port_VDPAddress), a	;     6: Enable display; no picture is shown when this bit is 0
					;     5: Enable VSync interrupt generation (IRQ)
	ld a, $81
	out (Port_VDPAddress), a	; reg 1

В данном случае можно сказать следующие важные вещи:
$81 – разложим число на тетрады, из которых верхняя 8 значит – отправка номера регистра видеопроца, вторая тетрада – номер регистра.
Отлично, здесь программируется состояние. Признак — $8x
Восьмёрка говорит о том, что идёт запись в реги видео.

Ок, что есть ещё?

ld a, $00
	out (Port_VDPAddress), a
	ld a, $7F
	out (Port_VDPAddress), a

Тут второй основной паттерн.
Первая тетрада второго байта начинается с 7. Итак – это бит записи в память.
Куда именно? Включен 7ой бит, который говорит о ЗАПИСИ в память, всё остальное – АДРЕС записи. Итак, здесь мы имеем установку адреса для отправки данных в видеопамять, по адресу #3f00. в доке эта память предназначена для описания дескрипторов спрайтов, в формате – 64 байта Y, после этого с #3f80 — X, I – координата и номер тайла для спрайта.
Ок, важно не это. Важен сам способ записи — #7x – будет запись в видеопамять.
Что после этого? Пачки out-ов:
-	nop
	nop
	ld a, (de)
	inc de
	out (Port_VDPData), a
	djnz –

Проц не выгребает по скорости, нужны минимум 4 такта на его работу. Используют и 8, как в данном случае. Нам это удобно – больше пространства.
Отлично, учитываем.

Обратная ситуация:
ld a, $00
	out (Port_VDPAddress), a
	ld a, $38
	out (Port_VDPAddress), a

Отмечаем что два старших бита второго байты выключены. Это говорит о том, что будет происходить ЧТЕНИЕ.
И последняя обобщённая команда:
xor a
	out (Port_VDPAddress), a
	ld a, $C0
	out (Port_VDPAddress), a

оба бита включены – пишем в палитру.
Палитра – 64 цвета, 2 бита на цвет. Ровно как у Евы.
Следующие отправки – это запись в палитровую память, с адреса 0 (первый байт отправки в порт)

Итог. Что мы здесь имеем? Управление их видеопроцом разделено всего на 4 части, две из которых – это запись и чтение, основные функции.

Итак, после отправки старшего байта адреса в духе
ld a, $00
	out (Port_VDPAddress), a
ld a, $78
	out (Port_VDPAddress), a

и рассмотрения карты видеопамяти мы можем сказать – сейчас будет происходить запись в тайловую память.

Обращу твоё внимание теперь на их карту видеопамяти:
#0000 — #37ff – память графики,
#3800 — #3eff – описание тайлов – графического слоя у системы НЕТ.
#3f00 — #3fff – описание спрайтов, 64 штуки всего * 3 байта.

Карта тайлов выглядит как 32х24х2 байта, где первый байт – номер тайла, второй байт – номер тайла + биты инверсии вывода, номер палитры из двух доступных, и — «приоритет над спрайтами».
Всё это неплохо уклаывается в расклад TS Config.
Да, биты не те.
Да, графика по другому организована.
Да, у нас памяти на тайлы, извините – 8 паг по 16кб а не одна.
У нас 85 спрайтов а не 64, и размер их может быть больше чем, прости, 8х16 (максимальный). До 64х64.
Да и скорость cpu выше ;)

Но вот тут как раз начинаются особенности их системы.
Прикинь – 16ц, 4 бита на цвет в тайле, как у нас?
Хрена.
Графика устроена своим особенным способом. Для того что бы сформировать изображение тайла, необходимо отправить 4 байта, каждый из которых связан с точкой :)
Каждый бит 4х байт – это цвет точки. Почему в системе сделано так я не понимаю. Но с этим нам нужно работать.
Итак, берём 4 байта, сдвигаем каждый по очерёдности в регистр – он наполняется настоящим содержимым, подходящим для ссылки на цвет в палитре. Неудобно. Но нужно, организация системы хранения графических данных именно такая.
Ок, всё сдвинуто правильно, всё уложено даже по нужным адресам.

Давайте займёмся выводом.
Особенностью системы является то, что вывод на экран позиционируется двумя командами видеопроца- #86 / #87, отвечающие за точку отображения в видеопамяти.
Кроме того, радость в том что видеопроц врапит вывод по Х и по Y своё видео. Укажи точку откуда выводить – он оттуда и отрисовывает. Круто, довольно.
Что сделать.
Моё решение состоит в том, что я эмулирую его работу. У нас есть 32 тайла по горизонтали, у нас есть 24 строки по вертикали для вывода. Итак – зацикли. Врапь.
Как это сделать? Мне кажется, что удобным для меня является вариант, когда я организовываю свою область для их тайловой видеопамяти. С которой я позже буду работать кодом по выводу, именно их образом- с учётом заворотов «wrap».

Куда всё это пристроить? Явно — в прерывание. Ищем место, заменяем, патч. Направляй вывод офсетов координат vdp…

Ну, на этом пока – всё. Остальное – в другой части. Но я жду вопросов!

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

avatar
Вова, спасибо за разбор, очень интересно. Удивил цикл:
-	nop
	nop
	ld a, (de)
	inc de
	out (Port_VDPData), a
	djnz –
В официальной доке Сеги пишут: «The VDP chip cannot process data any faster than the following rates:
16 Z80A T-States during VBLANK
29 Z80A T-States during active video.
This means that you should never issue two consecutive OUT or IN instructions to the VDP; they should be separated by at least a NOP instruction.» Похоже что эти твои кодеры не умеют (или не любят) считать такты :)
avatar
они упростили себе жизнь, зная что видеопроц тоже требует тактов на обработку. отсюда — любые команды, лишь бы подождать.
особенность микросхемы, вижу — нужно ожидание 8 тактов перед отправкой новых данных.
avatar
Вова, так вон оно сказано: нужно в худшем случае 29 тактов. А у них даже без нопов 37 тактов в цикле.
avatar
Им нужно не добавить 8 тактов, а убрать!
avatar
Вот обсуждение той же темы: www.smspower.org/forums/16298-VDPTimingConstraints?highlight=states

Если вкратце, во время VBLANK можно писать как угодно, а во время работы видеопроцессора реальный потолок — 26 тактов.
avatar
Слушай, Вова. Но раз ты как бы подразумеваешь статическую видеопамять, любые трюки со счётом строк и вообще со строчными прерываниями на Сеге видимо тебя полностью торпедируют. Я верно понимаю?
avatar
Да, видеопроц имеет счётчик строк и может вызвать прерывание на нужной линии экрана.
Это у нас тоже есть :)
В сонике такой подход был, назван был RasterSplit. Но я его тогда не реализовал, каюсь.
avatar
А как оно у тебя работает, если ты не реализовал часть рендера?
avatar
там это используется только для перегрузки палитры, на нужной строке экрана.
сверху всё жёлтое, под водой — всё голубое, по линии воды.
avatar
А почему не реализовал? По идее у тебя же есть всё для этого.
avatar
Было гораздо больше приоритетных задач, да и вообще — общих проблем: расшифровка работы с вдп, со звуком, поиски хардкода адресов на данные, прочее.
эту штуку я отключил в процессе поиска бага — казалось что она вообще не используется.
а она на первых уровнях не использовалась соврешенно, и только в конце стало понятно — где используется, и как протестировать.
это можно будет доработать, конечно
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.