Пишем на Ассемблере для Sega Genesis #3

В этой заметке я опишу как выводить изображение из тайлов на эмуляторе Sega Genesis с помощью ассемблера.
Картинка сплэша Demens Deum в эмуляторе Exodus будет выглядеть так:

Процесс вывода PNG картинки с помощью тайлов делается по пунктам:

  1. Уменьшение изображения до размеров экрана Сеги
  2. Конвертация PNG в ассемблерный дата-код, с разделением на цвета и тайлы
  3. Загрузка палитры цветов в CRAM
  4. Загрузка тайлов/паттернов в VRAM
  5. Загрузка индексов тайлов по адресам Plane A/B в VRAM
  6. Уменьшить изображение до размеров экрана Сеги можно с помощью любимого графического редактора, например Blender.

Конвертация PNG

Для конвертации изображений можно использовать тул ImaGenesis, для работы под wine требуются библиотеки Visual Basic 6, их можно установить с помощью winetricks (winetricks vb6run), либу RICHTX32.OCX можно скачать в интернете и положить в папку приложения для корректной работы.

В ImaGenesis нужно выбрать 4 битную цветность, экспортировать цвета и тайлы в два файла формата ассемблера. Далее в файле с цветами нужно каждый цвет положить в слово (2 байта), для этого используется опкод dc.w.

Для примера CRAM сплэш скрина:

 Colors: 
  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 

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

 Tiles: 
	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); Проще всего понять загрузку на примере ниже:

VDPCRAMFillLoop: 
    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)

TilesVRAM: 
  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, поэтому отрисовав экранное пространство, ваш рендер должен остановить отрисовку и продолжить заново когда курсор перейдет на новую строку. Вариантов как реализовать это множество, я использовал простейший вариант подсчета на двух регистрах счетчика ширины изображения, счетчика позиции курсора.

Пример кода:

 FillBackground: 
  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:

  1. Дебаггер процессора m68k
  2. Изменение количества тактов процессора m68k (для slow-mo режима в дебаггере)
  3. Вьюверы CRAM, VRAM, Plane A/B
  4. Внимательно читать документацию к m68k, используемым опкодам (не все так очевидно, как кажется на первый взгляд)
  5. Смотреть примеры кода/дизассемблинга игр на github
  6. Реализовать сабрутины эксепшенов процессора, обрабатывать их

Указатели на сабрутины эксепшенов процессора проставляются в заголовке рома, также на 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/