В это заметке я опишу процесс чтения джойстика, изменение позиции спрайта, горизонтальный флип, эмулятора Sega Genesis и потенциально самой приставки.
Чтение нажатий, обработка “событий” джойстика сеги происходит по следующей схеме:
- Запрос комбинации битов нажатых кнопок
- Считывание битов нажатых кнопок
- Обработка на уровне игровой логики
Для перемещение спрайта скелета нам необходимо хранить переменные текущей позиции.
RAM
Переменные игровой логики хранятся в RAM, до сих пор люди не придумали ничего лучше. Объявим адреса переменных, изменим код отрисовки:
skeletonYpos = $FF0002
frameCounter = $FF0004
skeletonHorizontalFlip = $FF0006
move.w #$0100,skeletonXpos
move.w #$0100,skeletonYpos
move.w #$0001,skeletonHorizontalFlip
FillSpriteTable:
move.l #$70000003,vdp_control_port
move.w skeletonYpos,vdp_data_port
move.w #$0F00,vdp_data_port
move.w skeletonHorizontalFlip,vdp_data_port
move.w skeletonXpos,vdp_data_port
Как можно заметить, адрес доступный для работы начинается с 0xFF0000, а заканчивается в 0xFFFFFF, итого нам доступно 64 кбайта памяти. Позиции скелета объявлены по адресам skeletonXpos, skeletonYpos, горизонтальный флип по адресу skeletonHorizontalFlip.
Joypad
По аналогии с VDP, работа с джойпадами происходит через два порта по отдельности – порт контроля и порт данных, для первого этого 0xA10009 и 0xA10003 со-но. При работе с джойпадом есть одна интересная особенность – сначала нужно запросить комбинацию кнопок для поллинга, а затем, подождав обновления по шине, прочитать нужные нажатия. Для кнопок C/B и крестовины это 0x40, пример далее:
move.b #$40,joypad_one_control_port; C/B/Dpad
nop ; bus sync
nop ; bus sync
move.b joypad_one_data_port,d2
rts
В регистре d2 останется состояние нажатых кнопок, либо не нажатых, в общем что просили через дата порт, то и останется. После этого идем в просмотрщик регистров Motorola 68000 вашего любимого эмулятора, смотрим чему равен регистр d2 в зависимости от нажатий. По-умному это можно узнать в мануале, но мы не верим наслово. Далее обработка нажатых кнопок в регистре d2
cmp #$FFFFFF7B,d2; handle left
beq MoveLeft
cmp #$FFFFFF77,d2; handle right
beq MoveRight
cmp #$FFFFFF7E,d2; handle up
beq MoveUp
cmp #$FFFFFF7D,d2; handle down
beq MoveDown
rts
Проверять нужно конечно отдельные биты, а не целыми словами, но пока и так сойдет. Теперь осталось самое простое – написать обработчики всех событий перемещения по 4-м направлениям. Для этого меняем переменные в RAM, и запускаем процедуру перерисовки.
Пример для перемещения влево + изменение горизонтального флипа:
move.w skeletonXpos,d0
sub.w #1,d0
move.w d0,skeletonXpos
move.w #$0801,skeletonHorizontalFlip
jmp FillSpriteTable
После добавления всех обработчиков и сборки, вы увидите как скелет перемещается и поворачивается по экрану, но слишком быстро, быстрее самого ежа Соника.
Не так быстро!
Чтобы замедлить скорость игрового цикла, существуют несколько техник, я выбрал самую простую и не затрагивающую работу с внешними портами – подсчет цифры через регистр пока она не станет равна нулю.
Пример замедляющего цикла и игрового цикла:
move.w #512,frameCounter
WaitFrame:
move.w frameCounter,d0
sub.w #1,d0
move.w d0,frameCounter
dbra d0,WaitFrame
GameLoop:
jsr ReadJoypad
jsr HandleJoypad
jmp GameLoop
После этого скелет забегает медленее, что и требовалось. Как мне известно, наиболее распространенный вариант “замедления” это подсчет флага вертикальной синхронизации, можно подсчитывать сколько раз экран был отрисован, таким образом привязаться к конкретному fps.
Ссылки
https://gitlab.com/demensdeum/segagenesissamples/-/blob/main/8Joypad/vasm/main.asm
Источники
https://www.chibiakumas.com/68000/platform2.php
https://huguesjohnson.com/programming/genesis/tiles-sprites/
Пишем на Ассемблере для Sega Genesis #4
В этой заметке я опишу как рисовать спрайты с помощью VDP эмулятора приставки Sega Genesis.
Процесс отрисовки спрайтов очень схож с рендерингом тайлов:
- Загрузка цветов в CRAM
- Выгрузка частей спрайтов 8×8 в VRAM
- Заполнение Sprite Table в VRAM
Для примера возьмем спрайт скелета с мечом 32×32 пикселя![]()
Skeleton Guy [Animated] by Disthorn
CRAM
С помощью ImaGenesis сконвертируем его в цвета CRAM и паттерны VRAM для ассемблера. После этого получим два файла а формате asm, далее переписываем цвета на размер word, а тайлы нужно положить в корректном порядке для отрисовки.
Интересная информация: можно переключить автоинкремент VDP через регистр 0xF на размер word, это позволит убрать инкремент адреса из кода заливки цветов CRAM.
VRAM
В мануале сеги есть корректный порядок тайлов для больших спрайтов, но мы умнее, поэтому возьмем индексы из блога ChibiAkumas, начнем подсчет с индекса 0:
0 4 8 12
1 5 9 13
2 6 10 14
3 7 11 15
Почему все кверх ногами? А что вы хотите, ведь приставка японская! Могло быть вообще справа налево!
Поменяем вручную порядок в asm файле спрайта:
dc.l $11111111 ; Tile #0
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111 ; Tile #4
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111 ; Tile #8
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111122
dc.l $11111122
dc.l $11111166
dc.l $11111166 ; Tile #12
dc.l $11111166
dc.l $11111166
и т.д.
Прогрузим спрайт как обычные тайлы/паттерны:
lea Sprite,a0
move.l #$40200000,vdp_control_port; write to VRAM command
move.w #128,d0 ; (16*8 rows of sprite) counter
SpriteVRAMLoop:
move.l (a0)+,vdp_data_port;
dbra d0,SpriteVRAMLoop
Для отрисовки спрайта осталось заполнить таблицу спрайтов (Sprite Table)
Sprite Table
Таблица спрайтов заполняется в VRAM, адрес ее нахождения проставляется в VDP регистре 0x05, адрес опять хитрый, посмотреть можно в мануале, пример для адреса F000:
Ок, теперь запишем наш спрайт в таблицу. Для этого нужно заполнить “структуру” данных состоящую из четырех word. Бинарное описание структуры вы можете найти в мануале. Лично я сделал проще, таблицу спрайтов можно редактировать вручную в эмуляторе Exodus.![]()
Параметры структуры очевидны из названия, например XPos, YPos – координаты, Tiles – номер стартового тайла для отрисовки, HSize, VSize – размеры спрайта путем сложения частей 8×8, HFlip, VFlip – аппаратные повороты спрайта по горизонтали и вертикали.![]()
Очень важно помнить что спрайты могут находиться вне экрана, это корректное поведение, т.к. выгружать из памяти спрайты вне экрана – достаточно ресурсоемкое занятие.
После заполнения данных в эмуляторе, их нужно скопировать из VRAM по адресу 0xF000, Exodus также поддерживает эту возможность.
По аналогии с отрисовкой тайлов, сначала обращаемся в порт контроля VDP для начала записи по адресу 0xF000, затем в порт данных записываем структуру.
Напомню что описание адресации VRAM можно почитать в мануале, либо в блоге Nameless Algorithm.
Вкратце адресация VDP работает так:
[..DC BA98 7654 3210 …. …. …. ..FE]
Где hex это позиция бита в желаемом адресе. Первые два бита это тип запрашиваемой команды, например 01 – запись в VRAM. Тогда для адреса 0XF000 получается:
0111 0000 0000 0000 0000 0000 0000 0011 (70000003)
В итоге получаем код:
move.l #$70000003,vdp_control_port
move.w #$0100,vdp_data_port
move.w #$0F00,vdp_data_port
move.w #$0001,vdp_data_port
move.w #$0100,vdp_data_port
После этого спрайт скелета отобразится в координатах 256, 256. Круто да?
Ссылки
https://gitlab.com/demensdeum/segagenesissamples/-/tree/main/7Sprite/vasm
https://opengameart.org/content/skeleton-guy-animated
Источники
https://namelessalgorithm.com/genesis/blog/vdp/
https://www.chibiakumas.com/68000/platform3.php#LessonP27
https://plutiedev.com/sprites
Пишем на Ассемблере для Sega Genesis #3
В этой заметке я опишу как выводить изображение из тайлов на эмуляторе Sega Genesis с помощью ассемблера.
Картинка сплэша Demens Deum в эмуляторе Exodus будет выглядеть так:

Процесс вывода PNG картинки с помощью тайлов делается по пунктам:
- Уменьшение изображения до размеров экрана Сеги
- Конвертация PNG в ассемблерный дата-код, с разделением на цвета и тайлы
- Загрузка палитры цветов в CRAM
- Загрузка тайлов/паттернов в VRAM
- Загрузка индексов тайлов по адресам Plane A/B в VRAM
- Уменьшить изображение до размеров экрана Сеги можно с помощью любимого графического редактора, например Blender.
Конвертация PNG
Для конвертации изображений можно использовать тул ImaGenesis, для работы под wine требуются библиотеки Visual Basic 6, их можно установить с помощью winetricks (winetricks vb6run), либу RICHTX32.OCX можно скачать в интернете и положить в папку приложения для корректной работы.
В ImaGenesis нужно выбрать 4 битную цветность, экспортировать цвета и тайлы в два файла формата ассемблера. Далее в файле с цветами нужно каждый цвет положить в слово (2 байта), для этого используется опкод dc.w.
Для примера CRAM сплэш скрина:
dc.w $0000
dc.w $0000
dc.w $0222
dc.w $000A
dc.w $0226
dc.w $000C
dc.w $0220
dc.w $08AA
dc.w $0446
dc.w $0EEE
dc.w $0244
dc.w $0668
dc.w $0688
dc.w $08AC
dc.w $0200
dc.w $0000
Файл тайлов оставить как есть, он и так содержит корректный формат для загрузки. Пример части файла тайлов:
dc.l $11111111 ; Tile #0
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111 ; Tile #1
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
dc.l $11111111
Как можно увидеть из примера выше, тайлы представляют из себя сетку 8×8, состоящую из индексов цветовой палитры CRAM.
Цвета в CRAM
Загрузка в CRAM производится с помощью выставления команды загрузки цвета по конкретному адресу CRAM в порт контроля (vdp control). Формат команды описан в Sega Genesis Software Manual (1989), добавлю лишь что достаточно прибавлять к адресу 0x20000 для перехода к следующему цвету.
Далее нужно загрузить цвет в порт данных (vdp data); Проще всего понять загрузку на примере ниже:
lea Colors,a0 ; pointer to Colors label
move.l #15,d7; colors counter
VDPCRAMFillLoopStep:
move.l d0,vdp_control_port ;
move.w (a0)+,d1;
move.w d1,(vdp_data_port);
add.l #$20000,d0 ; increment CRAM address
dbra d7,VDPCRAMFillLoopStep
Тайлы в VRAM
Далее следует загрузка тайлов/паттернов в видеопамять VRAM. Для этого выберем адрес в VRAM, например 0x00000000. По аналогии с CRAM, обращаемся в порт контроля VDP с командой на запись в VRAM и стартовым адресом.
После этого можно заливать лонгворды в VRAM, по сравнению с CRAM не нужно указывать адрес для каждого лонгворда, так как есть режим автоинкремента VRAM. Включить его можно с помощью флага регистра VDP 0x0F (dc.b $02)
lea Tiles,a0
move.l #$40200000,vdp_control_port; write to VRAM command
move.w #6136,d0 ; (767 tiles * 8 rows) counter
TilesVRAMLoop:
move.l (a0)+,vdp_data_port;
dbra d0,TilesVRAMLoop
Индексы тайлов в Plane A/B
Теперь предстоит заполнение экрана тайлами по их индексу. Для этого заполняется VRAM по адресу Plane A/B который проставляется в регистрах VDP (0x02, 0x04). Подробнее об хитрой адресации есть в мануале Сеги, в моем примере проставлен адрес VRAM 0xC000, выгрузим индексы туда.
Ваша картинка в любом случае заполнит за-экранное пространство VRAM, поэтому отрисовав экранное пространство, ваш рендер должен остановить отрисовку и продолжить заново когда курсор перейдет на новую строку. Вариантов как реализовать это множество, я использовал простейший вариант подсчета на двух регистрах счетчика ширины изображения, счетчика позиции курсора.
Пример кода:
move.w #0,d0 ; column index
move.w #1,d1 ; tile index
move.l #$40000003,(vdp_control_port) ; initial drawing location
move.l #2500,d7 ; how many tiles to draw (entire screen ~2500)
imageWidth = 31
screenWidth = 64
FillBackgroundStep:
cmp.w #imageWidth,d0
ble.w FillBackgroundStepFill
FillBackgroundStep2:
cmp.w #imageWidth,d0
bgt.w FillBackgroundStepSkip
FillBackgroundStep3:
add #1,d0
cmp.w #screenWidth,d0
bge.w FillBackgroundStepNewRow
FillBackgroundStep4:
dbra d7,FillBackgroundStep ; loop to next tile
Stuck:
nop
jmp Stuck
FillBackgroundStepNewRow:
move.w #0,d0
jmp FillBackgroundStep4
FillBackgroundStepFill:
move.w d1,(vdp_data_port) ; copy the pattern to VPD
add #1,d1
jmp FillBackgroundStep2
FillBackgroundStepSkip:
move.w #0,(vdp_data_port) ; copy the pattern to VPD
jmp FillBackgroundStep3
После этого остается только собрать ром с помощью vasm, запустив симулятор, увидеть картинку.
Отладка
Не все получится сразу, поэтому хочу посоветовать следующие инструменты эмулятора Exodus:
- Дебаггер процессора m68k
- Изменение количества тактов процессора m68k (для slow-mo режима в дебаггере)
- Вьюверы CRAM, VRAM, Plane A/B
- Внимательно читать документацию к m68k, используемым опкодам (не все так очевидно, как кажется на первый взгляд)
- Смотреть примеры кода/дизассемблинга игр на github
- Реализовать сабрутины эксепшенов процессора, обрабатывать их
Указатели на сабрутины эксепшенов процессора проставляются в заголовке рома, также на GitHub есть проект с интерактивным рантайм дебаггером для Сеги, под названием genesis-debugger.
Используйте все доступные инструменты, приятного олдскул-кодинга и да прибудет с вами Blast Processing!
Ссылки
https://gitlab.com/demensdeum/segagenesissamples/-/tree/main/6Image/vasm
http://devster.monkeeh.com/sega/imagenesis/
https://github.com/flamewing/genesis-debugger
Источники
https://www.chibiakumas.com/68000/helloworld.php#LessonH5
https://huguesjohnson.com/programming/genesis/tiles-sprites/
Пишем на Ассемблере для Sega Genesis #2
В этой заметке я опишу как загружать цвета в палитру Сеги на ассемблере.
Итоговый результат в эмуляторе Exodus будет выглядеть так:
Чтобы процесс происходил проще, найдите в интернете pdf под названием Genesis Software Manual (1989), в нем описывается весь процесс в мельчайших деталях, по сути, эта заметка является комментариями к оригинальному мануалу.
Для того чтобы записывать цвета в VDP чип эмулятора Сеги, нужно сделать следующие вещи:
- Отключить систему защиты TMSS
- Записать правильные параметры в регистры VDP
- Записать нужные цвета в CRAM
Для сборки будем использовать vasmm68k_mot и любимый текстовый редактор, например echo. Сборка осуществляется командой:
Порты VDP
VDP чип общается с M68K через два порта в оперативной памяти – порт контроля и порт данных.
По сути:
- Через порт контроля можно выставлять значения регистрам VDP.
- Также порт контроля является указателем на ту часть VDP (VRAM, CRAM, VSRAM etc.) через которую передаются данные через порт данных
Интересная информация: Сега сохранила совместимость с играми Master System, на что указывает MODE 4 из мануала разработчика, в нем VDP переключается в режим Master System.
Объявим порты контроля и данных:
vdp_data_port = $C00000
Отключить систему защиты TMSS
Защита от нелицензионных игр TMSS имеет несколько вариантов разблокировки, например требуется чтобы до обращения к VDP в адресном регистре A1 лежала строка “SEGA”.
MOVE.B A1,D0; Получаем версию хардвары цифрой из A1 в регистр D0
ANDI.B 0x0F,D0; По маске берем последние биты, чтобы ничего не сломать
BEQ.B SkipTmss; Если версия равна 0, скорее всего это японка или эмулятор без включенного TMSS, тогда идем в сабрутину SkipTmss
MOVE.L "SEGA",A1; Или записываем строку SEGA в A1
Записать правильные параметры в регистры VDP
Зачем вообще выставлять правильные параметры в регистры VDP? Идея в том, что VDP может многое, поэтому перед отрисовкой нужно проинициализировать его с нужными фичами, иначе он просто не поймет, что от него хотят.
Каждый регистр отвечает за определенную настройку/режим работы. В Сеговском мануале указаны все биты/флажки для каждого из 24 регистров, описание самих регистров.
Возьмем готовые параметры с комментариями из блога bigevilcorporation:
VDPReg0: dc.b $14 ; 0: H interrupt on, palettes on
VDPReg1: dc.b $74 ; 1: V interrupt on, display on, DMA on, Genesis mode on
VDPReg2: dc.b $30 ; 2: Pattern table for Scroll Plane A at VRAM $C000
; (bits 3-5 = bits 13-15)
VDPReg3: dc.b $00 ; 3: Pattern table for Window Plane at VRAM $0000
; (disabled) (bits 1-5 = bits 11-15)
VDPReg4: dc.b $07 ; 4: Pattern table for Scroll Plane B at VRAM $E000
; (bits 0-2 = bits 11-15)
VDPReg5: dc.b $78 ; 5: Sprite table at VRAM $F000 (bits 0-6 = bits 9-15)
VDPReg6: dc.b $00 ; 6: Unused
VDPReg7: dc.b $00 ; 7: Background colour - bits 0-3 = colour,
; bits 4-5 = palette
VDPReg8: dc.b $00 ; 8: Unused
VDPReg9: dc.b $00 ; 9: Unused
VDPRegA: dc.b $FF ; 10: Frequency of Horiz. interrupt in Rasters
; (number of lines travelled by the beam)
VDPRegB: dc.b $00 ; 11: External interrupts off, V scroll fullscreen,
; H scroll fullscreen
VDPRegC: dc.b $81 ; 12: Shadows and highlights off, interlace off,
; H40 mode (320 x 224 screen res)
VDPRegD: dc.b $3F ; 13: Horiz. scroll table at VRAM $FC00 (bits 0-5)
VDPRegE: dc.b $00 ; 14: Unused
VDPRegF: dc.b $02 ; 15: Autoincrement 2 bytes
VDPReg10: dc.b $01 ; 16: Vert. scroll 32, Horiz. scroll 64
VDPReg11: dc.b $00 ; 17: Window Plane X pos 0 left
; (pos in bits 0-4, left/right in bit 7)
VDPReg12: dc.b $00 ; 18: Window Plane Y pos 0 up
; (pos in bits 0-4, up/down in bit 7)
VDPReg13: dc.b $FF ; 19: DMA length lo byte
VDPReg14: dc.b $FF ; 20: DMA length hi byte
VDPReg15: dc.b $00 ; 21: DMA source address lo byte
VDPReg16: dc.b $00 ; 22: DMA source address mid byte
VDPReg17: dc.b $80 ; 23: DMA source address hi byte,
; memory-to-VRAM mode (bits 6-7)
Ок, теперь пойдем в порт контроля и запишем все флажки в регистры VDP:
move.l #VDPRegisters,a0 ; Пишем адрес таблицы параметров в A1
move.l #$18,d0 ; Счетчик цикла - 24 = 18 (HEX) в D0
move.l #$00008000,d1 ; Готовим команду на запись в регистр VDP по индексу 0, по мануалу - 1000 0000 0000 0000 (BIN) = 8000 (HEX)
FillInitialStateForVDPRegistersLoop:
move.b (a0)+,d1 ; Записываем в D1 итоговое значение регистра VDP из таблицы параметров, на отправку в порт контроля VDP
move.w d1,vdp_control_port ; Отправляем итоговую команду + значение из D1 в порт контроля VDP
add.w #$0100,d1 ; Поднимаем индекс регистра VDP на 1 (бинарное сложение +1 к индексу по мануалу Сеги)
dbra d0,FillInitialStateForVDPRegistersLoop ; Уменьшаем счетчик регистров, продолжаем цикл если необходимо
Самое сложное это прочитать мануал и понять в каком формате подаются данные на порт контроля, опытные разработчики разберутся сразу, а вот неопытные… Немного подумают и поймут, что синтаксис для записи регистров такой:
0B100(5 бит – индекс регистра)(8 бит/байт – значение)
0B1000001001000101 – записать в регистр VDP 2 (00010), значение флажков 01000101.
Записать нужные цвета в CRAM
Далее идем писать два цвета в память цветов CRAM (Color RAM). Для этого пишем в порт контроля команду на доступ к цвету по индексу 0 в CRAM и отправляем по дата порту цвет. Все!
Пример:
move.l #$C0000000,vdp_control_port ; Доступ к цвету по индексу 0 в CRAM через порт контроля
move.w #228,d0; Цвет в D0
move.w d0,vdp_data_port; Отправляем цвет в порт данных
После сборки и запуска в эмуляторе в Exodus, у вас должен быть залит экран цветом 228.
Давайте зальем еще вторым цветом, по последнему байту 127.
move.l #$C07f0000,vdp_control_port ; Доступ к цвету по байту 127 в CRAM через порт контроля
move.w #69,d0; Цвет в D0
move.w d0,vdp_data_port; Отправляем цвет в порт данных
Ссылки
https://gitlab.com/demensdeum/segagenesissamples
https://www.exodusemulator.com/
http://sun.hasenbraten.de/vasm/
https://tomeko.net/online_tools/bin_to_32bit_hex.php?lang=en
Источники
https://namelessalgorithm.com/genesis/blog/genesis/
https://plutiedev.com/vdp-commands
https://huguesjohnson.com/programming/genesis/palettes/
https://www.chibiakumas.com/68000/helloworld.php#LessonH5
https://blog.bigevilcorporation.co.uk/2012/03/09/sega-megadrive-3-awaking-the-beast/