В данной статье мы познакомимся с таким явлением как арты для платформы ZX Spectrum и его клонов, немного ковырнём формат файлов SCR (рассматриваем только стандартные 6144 и 6912), узнаем как можно такое запихнуть в ATARI и немножко покодим на C# и ассемблере для 6502.
Если кому-то интересно немного расширить свои знания о платформе ATARI XL/XE и не потеряться в этой статье, то предлагаю перед прочтением ознакомиться с моей предыдущей статьёй "Немного об ATARI XL/XE: 6502, графика, код".
ZXART
Итак, ZXART - один из ресурсов, откуда можно заполучить экраны спектрума (арты, сплеш-скрины)
![](https://habrastorage.org/getpro/habr/upload_files/f2b/3a7/086/f2b3a708618b738d1772ddc30eef5668.png)
Без регистрации скачивание доступно в полном объёме, включая поиск по фильтру и загрузку найденного. По желанию выбираем язык (русский есть), идём в графику и поиск по базе, в фильтре выбираем монохромный формат 6144 и ищем. Находит всего 11 картинок и только две из них могут хоть немного заинтересовать.
Не расстраиваемся, выбираем Стандартный 6912, обязательно ставим 4 в минимальном рейтинге, так список сократится на 2/3 от общего числа, ищем, а потом нажимаем на "Скачать ZIP архив". На нашем ПК после распаковки архива будет ~6000 файлов, я удалил все которые не отмечены как SCR и все что меньше 6144 байт. В итоге осталось ~5000 файлов.
Файлы размером в 6912 байт содержат атрибуты, но не обязательно являются цветными, часть из них всё равно может быть просмотрена в монохромном режиме в относительно комфортном виде (у таких картинок может совсем не быть в атрибутах ничего кроме белого и черного цветов, такое нам и нужно).
![](https://habrastorage.org/getpro/habr/upload_files/ec6/78d/8c9/ec678d8c95037304187e7d8c69b23a03.png)
![](https://habrastorage.org/getpro/habr/upload_files/324/f06/f36/324f06f367758b1f6ada09cf0f0f6a92.png)
![](https://habrastorage.org/getpro/habr/upload_files/85e/092/99a/85e09299a8545a0f5898e32dd0416752.png)
![](https://habrastorage.org/getpro/habr/upload_files/936/874/4e7/9368744e7dedd065185f11fc6f3440ed.png)
Если вы нажмёте на название картинки, то что белым цветом, то вам откроется больше информации, описание, голоса за этот арт, а так же возможность скачать её не только в оригинальном формате, но и для просмотра на ПК (весьма удобная функция например для обоев на рабочий стол).
![](https://habrastorage.org/getpro/habr/upload_files/427/fca/6e3/427fca6e3ea84d9dcfa9bef4076554c3.png)
Причем для ПК доступны версии с разным масштабом, а так же версия для печати.
SCR и ATARI
Формат прямой как лом - это последовательность байтов, которые сразу можно размещать в экранной памяти Спектрума, для формата 6144 это только монохромная часть, а для 6912 еще и атрибуты цвета, яркости и мерцания. Например полноцветная картинка выглядит так
![](https://habrastorage.org/getpro/habr/upload_files/ada/9a9/977/ada9a9977e6f78024bcc108c8aa7f09f.png)
И хотя я очень не люблю режущий глаз цвет в тех же играх на ZX, но в артах такой сочный цвет смотрится обескураживающе и притягательно (особенно если автору удалось избавиться от клешинга).
Для обработки файлов я воспользуюсь языком C#/ Задача следующая - прогнать картинку (а то и не одну) и получить на выходе текстовый файл ASM, который мы сможем подключить к программе на ассемблере и после компиляции получить один исполняемый файл, который будет отображать на экране исходное изображение.
Для ассемблера требуется соблюсти формат каждой строки как на примере ниже
.byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $FF и т.д.
Для красоты и мирового баланса мы будем использовать не десятичный, а шестнадцатеричный формат чисел. Каждое из них представляет собой 8 пикселей слева направо, где $00 - это пусто, а $FF - все восемь заполнены точками (пикселами).
Спектрумовский формат экрана 256х192 точки или 32 байта (столбца) на 192 строки. Но есть особенность - память у ZX не линейная, а расположена тремя секциями, каждая из которых разбита на 8 блоков по 8 строк, лучше посмотреть на картинке ниже
![https://retrocomputing.stackexchange.com/questions/212/what-format-is-the-timex-sinclair-zx-spectrum-screen-scr-file https://retrocomputing.stackexchange.com/questions/212/what-format-is-the-timex-sinclair-zx-spectrum-screen-scr-file](https://habrastorage.org/getpro/habr/upload_files/a8f/494/c78/a8f494c7818c940f9672ff52a52d86a0.gif)
Так что в конвертере на C# это нужно предусмотреть и все байты разместить линейно. Вот так выглядит спектрумовская картинка если не делать никаких преобразований
![](https://habrastorage.org/getpro/habr/upload_files/c61/3cc/845/c613cc845b6bd5cfb0b630bb31702e30.png)
А так - с преобразованием
![Александр Трофимчук, Владивосток https://zxart.ee/rus/avtory/s/schafft/ Александр Трофимчук, Владивосток https://zxart.ee/rus/avtory/s/schafft/](https://habrastorage.org/getpro/habr/upload_files/7c5/198/8be/7c51988bee5a138db874445bd75b446d.png)
как бы странно ни было, но ATARI аппаратно поддерживает разрешение экрана как у ZX - 256х192, базовым же является 320х192. В ATARI не стали этот режим поддерживать на уровне встроенного BASIC и базовых режимов, но функция присутствует в видеочипе и мы можем ею управлять изменяя регистры в памяти.
Конечно, я бы мог использовать и родное разрешение ATARI, но тогда мы не сможем показать картинку сразу на экране, нам пришлось бы построчно со сдвигом копировать данные. И тут мы, как я немного описывал в своей статье ранее, будем использовать списки отображения (Display Lists) - это команды видеочипу, которые показывают - что и как нужно делать с экраном. В нашем случае мы включим разрешение 256х192, укажем видеочипу на данные картинки и вуаля, она сразу же будет располагаться там, где нам и нужно и будет рисоваться видеочипом, без дополнительных манипуляций с памятью.
C# SCR->ASM
В C# для подготовки строк мы воспользуемся StringBuilder, сначала всё запишем в него, а потом просто сохраним в файл.
byte[] bytes = File.ReadAllBytes(listfiles[i]);
StringBuilder sb = new StringBuilder();
for (var pages = 0; pages < 3; pages++)
{
for(var eightlines=0; eightlines<8; eightlines++)
{
for (var eightlineblock=0; eightlineblock<8; eightlineblock++)
{
sb.Append(" .byte ");
for (var x = 0; x < 32; x++)
{
sb.Append("$" + (bytes[(pages * 192 * 32 / 3) + (eightlineblock * 8 * 32) + (eightlines * 32 + x)]).ToString("X2") + (x == 31 ? "\n" : ", "));
}
}
}
}
System.IO.File.WriteAllText(listfiles[i]+".asm", sb.ToString());
Проход по файлу из списка listfiles[i]
с картинкой, который загружаем в массив bytes[]
, далее, внутри трёх циклов формируем строки, цикл pages
переставляет указатель по трём страницам экранной памяти, eightlineblock
перебирает внутри страницы блоки по 8 строк в каждой, а eightline
- 8 строк внутри каждого блока. Так мы нелинейное преобразуем в линейное.
Сохраняем сформированные строки на диск с тем же именем только добавив в конце ".asm" - вот и вся магия.
ASM
Подготовка, настройка
Для тех, кто захочет всё потом повторить за мной, потребуется скачать MAD Assembler, я делал это с сайта проекта WUDSN.COM. Но самим проектом, который использует Eclipse я не пользуюсь, как-то мне ближе к телу Visual Studio Code и сборка через командную строку. Из архива нужен только MADS, путь к нему потом можно прописать в глобальной переменной PATH или просто писать полный путь в консоли, так как я использую FAR Manager то меня вполне устраивает вариант с полным путём.
Строка компиляции у меня выглядит так
\utils\mads\mads \work\atari\asm\zxart.asm
После компиляции мы получает исполнимый файл, который скармливаем эмулятору ATARI Altirra, о нём и его установке так же есть в моей прошлой статье
\altirra_path\altirra64 /singleinstance zxart.obx
Расширение OBX подставляет сам MADS (если не указать иное). Ключ для Altirra /singleinstance
можно не использовать, но тогда при каждом запуске у вас будут появляться новые окна с эмулятором, тут кому как удобно.
Сама программа на ассемблере будет состоять из нескольких логических блоков (звучит громко, но большая часть из них будет состоять всего из нескольких строк):
Константы. Формально можно обойтись и без них, но есть планы залить проект на github, так что уровень читабельности нужно держать.
Код инициализации. То, что будет выполняться один раз при старте.
Основной цикл программы. Пока это скучный бесконечный цикл, так как вся работа ложится на видеочип.
Пустые данные для выравнивания списка отображения (об этом ниже)
Список отображения
Данные картинки
Строки статуса, копирайт, данные о разработчике
Реализуя описанное - мы получаем такой файл
; Monochrome ZXART Viewer
; 2023 by zara6502
; ANTIC F 320x192 + Narrow Bit -> 256x192
.org $2000 ; start address
; 1. Константы
SDLSTL = $0230 ; Display List pointer
NP32chr = $21 ; Narrow playfield (32 bytes/chars)
SDMCTL = $022F ; https://atariwiki.org/wiki/Wiki.jsp?page=SDMCTL
blank1 = $00 ; 1 blank lines
blank8 = $70 ; 8 blank lines
antic2 = $02 ; ANTIC F mode
anticF = $0F ; ANTIC F mode
lms = $40 ; Load memory scan
jvb = $41 ; Jump while vertical blank
; Color registers
cr_CHR = 709 ; char color
cr_BG = 710 ; screen background
; 2. Init
mwa #dlist SDLSTL ; Load Display List
lda #0
sta cr_CHR ; set char color to WHITE
lda #15
sta cr_BG ; set screen backgroud to BLACK
lda #NP32chr
sta SDMCTL ; set 32 byte wide screen
; 3. Main
jmp *
; 5. Display List
dlist
; 4. Zero Data
zero_data
; 6. Picture
screen
icl 'file.scr.asm'
; 7. Copyright, author
copyright
.byte "ATARI ZXART VIEWER 2023 ZARA6502"
Я пока в коде не указал сам список отображения, сейчас мы поговорим о нём отдельно, а по коду выше я думаю в целом понятно. Для каждого из семи пунктов (логических блоков), я проставил соответствующие номера.
Команда icl 'file.scr.asm'
с указанием имени файла - это и есть тот файл, который мы получаем после работы конвертера. Во время работы компилятора он добавит его и соберёт в один общий файл.
Список отображения (Display List)
Так как это не просто данные, а по сути - программа для выполнения видеочипом, то есть набор инструкций и правил, которыми нужно руководствоваться при создании графического режима, мы рассмотрим те команды, которые потребуется для реализации нашей задумки. В константах уже есть набор команд
blank1 = $00 ; 1 blank lines
blank8 = $70 ; 8 blank lines
antic2 = $02 ; ANTIC F mode
anticF = $0F ; ANTIC F mode
lms = $40 ; Load memory scan
jvb = $41 ; Jump while vertical blank
здесь blank1
- это одна пустая строка сканирования экрана (условно это тонкая линия высотой в 1 пиксел, которая не содержит никаких данных), blank8
- от blank1
отличается тем, что пропускает сразу 8 строк, что эквивалентно пропуску одной строки текстового режима, antic2
и anticF
- это два видеорежима, 2 - текстовый, F - графический монохромный, lms
- бит, указывающий, что после этого байта идёт 16-битный указатель на адрес с графическими данными для отображения, jvb
- команда безусловного перехода и 16-битный указатель на список отображения, который выполнится при следующем обратном ходе луча кинескопа.
Смотрим на код ниже
; Display List
dlist
.byte blank8, blank8
.byte antic2
.byte blank1
.byte anticF + lms, <screen, >screen
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF + lms, <($3000), >($3000), anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte blank1
.byte antic2
.byte jvb, <dlist, >dlist
Так как у видеочипа 240 строк сканирования, которые он должен обрабатывать, то формально мы должны 24 строки сверху и 24 строки снизу пропускать и не использовать. Таким образом 24+192+24 и даёт нам 240 строк. Поэтому мы должны были бы в самом начале списка отображения указать три команды
.byte blank8, blank8, blank8
и затем уже программировать всё остальное. Но тогда у меня бы не получилось никак указать на экране информацию о программе, поэтому я решил сверху и снизу сделать по одной строке в стандартном текстовом режиме antic2
. Для этого верхнюю часть до основного экрана я изменяю на
.byte blank8, blank8
.byte antic2
.byte blank1
В данном случае blank1
я использую, чтобы отделить текстовое поле от самой картинки.
К такой же трюк я использую внизу, тут опять разделительная линия и текстовое поле
.byte blank1
.byte antic2
Смотрим на результат
![](https://habrastorage.org/getpro/habr/upload_files/303/fed/33b/303fed33b1f018e08cb0fe0f563bde50.png)
С последней строкой всё понятно, там идёт возврат к первой строке списка отображения,
.byte jvb, <dlist, >dlist
, а внутри списка у нас 192 элемента, которые указывают на монохромный графический режим ANTIC F. Но в начале и середине этого списка есть два элемента, к которым мы прибавляем константу lms
.
.byte anticF + lms, <screen, >screen
...
.byte anticF + lms, <($3000), >($3000) ...
Так мы видеочипу даём понять, откуда требуется взять данные для размещения в этой строке и если в самом начале такой адрес выглядит понятно, то зачем он в середине? Дело в том, что видеочип умеет работать только внутри страниц на 4 Килобайта, они фиксированы аппаратно, а наш графический режим пересекает такую границу (ведь размер картинки, как мы помним, 32*192=6144 байт), так как программу мы разместили по адресу $2000
, то следующая страница в памяти будет по адресу $3000
, её нам и нужно указать с добавлением бита lms
. Есть одна сложность, если указать этот бит не на границе страницы, то мы испортим линейность отображения, на картинке ниже я показываю что случится с экраном если я бит lms
размещу не на своём месте
![](https://habrastorage.org/getpro/habr/upload_files/a79/a7d/f4b/a79a7df4bbb011d9a47fd45d0468b46d.png)
Картинка сдвинулась, внизу появился мусор, а текстовое поле с названием программы уже не попадает точно на своё место. Так как смещение позиции на экране полностью зависит от количества байт в самой программе, то добавление или удаление чего-либо требует корректировки положения бита lms
. Можно делать это программно, но усложнять код ради этого мне не хотелось. Для тонкой корректировки в пределах 32 байт я сделал отдельный блок
zero_data
.byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00
.byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00
.byte $00
Если же добавляемый код превышал значение в 32 байта, то бит lms
нужно было сдвигать внутри самого списка отображения (ведь каждый элемент списка отображения это строка в 32 байта). Если же какие-то данные размещать после данных картинки, то корректировать ничего не требуется.
Ниже полный код программы со списком отображения и блоком пустых данных
; Monochrome ZXART Viewer
; 2023 by zara6502
; ANTIC F 320x192 + Narrow Bit -> 256x192
.org $2000 ; адрес, по которому будет размещена наша программа
; 1. Константы
SDLSTL = $0230 ; Указатель на Display List
NP32chr = $21 ; Narrow playfield (32 bytes/chars)
SDMCTL = $022F ; https://atariwiki.org/wiki/Wiki.jsp?page=SDMCTL
blank1 = $00 ; 1 blank lines
blank8 = $70 ; 8 blank lines
antic2 = $02 ; ANTIC F mode
anticF = $0F ; ANTIC F mode
lms = $40 ; Load memory scan
jvb = $41 ; Jump while vertical blank
; Color registers
cr_CHR = 709 ; char color
cr_BG = 710 ; screen background
; 2. Init
mwa #dlist SDLSTL ; Load Display List
lda #0
sta cr_CHR ; set char color to WHITE
lda #15
sta cr_BG ; set screen backgroud to BLACK
lda #NP32chr
sta SDMCTL ; set 32 byte wide screen
; 3. Main
jmp *
; 5. Display List
dlist
.byte blank8, blank8
scroll_line_dl_addr
.byte antic2 + lms, <github_line, >github__line
.byte blank1
.byte anticF + lms, <screen, >screen
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF + lms, <($3000), >($3000), anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF, anticF
.byte blank1
.byte antic2
.byte jvb, <dlist, >dlist
; 4. Zero Data
zero_data
.byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00
.byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00
.byte $00
; 6. Picture
screen
icl 'file.scr.asm'
; 7. Copyright, author
copyright
.byte "ATARI ZXART VIEWER 2023 ZARA6502"
github_line
.byte " github.com/zara6502/scr2atari "
Вы заметите, что в начале список отображения немного изменился, а в конце появилась строка
.byte antic2 + lms, <github_line, >github_line
...
github_line
.byte " github.com/zara6502/scr2atari "
Тут как раз я пользуюсь особенностью программирования видеочипа, указывая адрес, откуда нужно прочитать 32 байта - видеочип исполняя команды сначала читает 32 байта размещённые по адресу github_line
, показывает их в первой строке, а потом, увидев еще одну команду lms
- читает саму картинку. С нижней текстовой строкой это делать не требуется, так как она размещена сразу после картинки и видеочип продолжает заполнять байты экрана линейно.
Ну и теперь о том, как же получаются 32 байта на строку или заветный режим 256х192. В коде вы уже могли увидеть строки
NP32chr = $21 ; Narrow playfield (32 bytes/chars)
SDMCTL = $022F ; https://atariwiki.org/wiki/Wiki.jsp?page=SDMCTL
...
lda #NP32chr
sta SDMCTL ; set 32 byte screen
У ATARI есть регистр по адресу $022F, который позволяет установить ширину экрана в три варианта столбцов - 32, 40 и 48 байт. 40 - стандартное значение, даёт разрешение в 320 пикселей, 32 - в 256, а 48 - в 384. Собственно значение $20 включает видеочип, а +1 - устанавливает ширину экрана в 32 байта. Поэтому мы значение $21 переменной NP32chr записываем по адресу $022F.
Проблема и костыль
К этому моменту у нас есть функционирующий конвертер, программа на ассемблере и мы добились вывода картинки на экран ATARI.
Но осталась нерешённой одна проблема, часть картинок инверсна и эта инверсия реализована байтами атрибутов экрана, а в процессе конвертации мы эти данные отсекаем, поэтому мне подумалось, что было бы логично в просмотрщик встроить простой функционал - при нажатии кнопки на джойстике инвертировать цвет фона и значащих бит.
Так как это делается изменением регистров цвета, то никак не повлияет ни на сложность кода, ни на его размер и визуально будет происходить моментально, при отпускании кнопки цвет опять будет переключаться. Так как у ATARI кнопка джойстика читается простым числом в памяти, то в Main
цикле программы достаточно сделать одно сравнение и при одном варианте ответа регистр цвета делать белым, а при другом - чёрным. Как именно включить джойстик в Altirra можно прочитать в моей прошлой статье.
Ниже небольшая анимашка, где я нажимаю на кнопку джойстика и удерживая получаю инверсию цветов, когда кнопку отпускаю, цвета возвращаются обратно.
![](https://habrastorage.org/getpro/habr/upload_files/0c3/8f9/b19/0c38f9b19772bef3abedfe33d5c40ca6.gif)
Ниже пара примеров, где изображение монохромное, но атрибуты цвета всё равно используются и моя программа в текущем её виде просто ломает изображение игнорируя те самые атрибуты.
![Так выглядит оригинал на ZXART Так выглядит оригинал на ZXART](https://habrastorage.org/getpro/habr/upload_files/161/0f0/bce/1610f0bcefe1aabea6e0d98f19dd541b.png)
![Так она выглядит в моей программе Так она выглядит в моей программе](https://habrastorage.org/getpro/habr/upload_files/2f2/6f4/aa7/2f26f4aa7983f28b2f538aa2fb3d536d.png)
![А так выглядит уже с инверсией цвета. Хорошо заметны артефакты, где применялись атрибуты цветности и сама Уэнсдей из черно-нарядной превратилась в Белоснежку. А так выглядит уже с инверсией цвета. Хорошо заметны артефакты, где применялись атрибуты цветности и сама Уэнсдей из черно-нарядной превратилась в Белоснежку.](https://habrastorage.org/getpro/habr/upload_files/550/9d8/930/5509d8930b838b636266d280de36577a.png)
Понятно что этой картинке простая инверсия не поможет, но есть например картинка с Виктором Цоем и она хорошо инвертируется, так что функция должна присутствовать.
![](https://habrastorage.org/getpro/habr/upload_files/f4e/bc6/335/f4ebc63358739f3596babd629cdcc9c9.png)
![](https://habrastorage.org/getpro/habr/upload_files/301/d3d/7c8/301d3d7c85b6011562a08fa042d9239f.png)
Update1: когда статья была почти готова, я всё же решил попробовать реализовать в конвертере автоматическое изменение инверсии цвета и, например, полностью справившись с этой задачей на картинке с Уэнсдей я тут же потерпел крах на картинке с Цоем. В ходе исследований я установил, что художники пользуются атрибутами цветности в сугубо хаотическом порядке и даже в пределах одной картинки может быть использован атрибут "черные чернила белый холст" так и "белые чернила черный холст" с инверсным расположением битов (при этом визуально, для того кто смотрит на экран, вообще ничего не поменяется).
Но глаза боятся, а руки делают. Я установил опытным путём что 6 видов атрибутов приводят к изменению "обычного" порядка отображения, а именно
черные чернила, черное полотно, бит яркости 0
черные чернила, черное полотно, бит яркости 1
белые чернила, белое полотно, бит яркости 0
белые чернила, белое полотно, бит яркости 1
черные чернила, белое полотно, бит яркости 0
черные чернила, белое полотно, бит яркости 1
Для первой пары я все биты в квадрате 8х8 ставлю в 0 (на ATARI это чёрный), для второй пары в 255 (белый), а для третьей пары инвертирую биты. Я понимаю, что спектрумисты могут наверное улыбнуться над моими проблемами и решением, но платформа мне не близка, поэтому иду по саду из граблей. Буду признателен если кто-то уточнит или поправит меня, возможно я не всё учёл.
Давайте посмотрим на пример того как было изначально после конвертации без учёта атрибутов
![](https://habrastorage.org/getpro/habr/upload_files/992/9af/b05/9929afb05d625df3d320191c07cf5a8b.png)
И теперь на то, как оно стало
![отличнейший арт, за три недели написании статьи и программ этот арт, вместе с девушкой с арта выше по тексту - стали для меня буквально настольными отличнейший арт, за три недели написании статьи и программ этот арт, вместе с девушкой с арта выше по тексту - стали для меня буквально настольными](https://habrastorage.org/getpro/habr/upload_files/e4f/e8c/1b4/e4fe8c1b448dcb6c73fb09b79fdb13c5.png)
На полном наборе картинок всё равно будут варианты с артефактами, но как я убедился, это те картинки, где используется цвет либо экзотический вариант рисования. Всё что на данный момент мне придумалось в качестве одного из решений - повторять картинку в RGB, конвертировать цвета в оттенки серого, прогонять фильтр дизеринга и загонять в ATARI уже монохром. Конечно малое число пикселей может создать кашу, но пока ничего лучше для монохрома я не придумал. Пока я не приступал к такой реализации, поэтому буду рад услышать ваше мнение.
Update2: наигравшись с текущей версией программы я решил остановиться и закончить статью, но анализируя арты я пришёл к выводу что в большинстве случаев в полуручном режиме конвертировать цветной арт в монохромный всё же можно. Вероятно это станет предметом следующей статьи.
Адаптировать программу к показу цветных артов наверное можно, но нужно учитывать что:
у ATARI нет цветного режима экрана с высоким разрешением
при разрешении 160х192 стандартно мы имеем только 4 цвета
абсолютно разные несовместимые палитры
Цветное разрешение ATARI 160х192 составляет примерно 2/3 по горизонтали от разрешения ZX, что ощутимо скажется на качестве. Конечно можно хитрить и комбинировать разные видеорежимы в разных строках, например для этого изображения с применением графики PMG
![](https://habrastorage.org/getpro/habr/upload_files/8f1/cff/867/8f1cff8676c335904ca7d320297b9d6f.png)
но ожидать чуда не стоит. Часть изображений можно будет подогнать, но разницу в палитрах обойти никак не получится - у ATARI просто нет ярких и сочных цветов спектрума.
Полная автоматизация
Ну и как-то будет странно имея в руках компьютер сделать только часть автоматизаций по созданию конвертированных вариантов изображений с ZXART. Я считаю, что нужно запихнуть весь код ассемблера внутрь конвертера на C# и генерировать весь файл ASM полностью за раз, ну и конечно сразу же его и компилировать.
В итоге, в папке с файлами SCR запускаем наш конвертер и получаем еще два файла - ASM и OBX. Но нам и этого будет мало, не уверен, что кому-то захочется запускать все 12000 файлов в надежде увидеть что-то интересное, поэтому для каждого файла нужно генерировать PNG файл и тогда в Проводнике мы сможем видеть превьюшки и запускать только те файлы, которые нас заинтересуют.
Тут я думаю достаточно ссылки на Github с реализованными функциями. А визуально в Проводнике это выглядит вот так
![](https://habrastorage.org/getpro/habr/upload_files/e66/f29/bdd/e66f29bdd6a87bd65f1138490020ab29.png)
Финальный, на момент написания статьи, вариант конвертера обрёл многопоточность силами самого C# через Parallels.ForEach и при прогоне толпы картинок грузит все ядра моего Ryzen 5 3600 6/12 на 100%, диск хоть SATA хоть NVME не особо напрягаются, хоть по результату мы и имеем 5000 SCR + 5000 ASM + 5000 OBX + 5000 PNG = 20000 файлов. Выполнение длится ~2 минуты, основное время (75%) тратится на компиляцию, у MADS она многопроходная, в основном по 3 прохода на файл. На рабочей машине i5-2400/16/SATA SSD всё выполняется за 6.5 минут.
Update3: так как "Остапа понесло" и я не мог остановиться в переделках конвертера, то в финальной версии, которую я и решил выложить вместе с релизом статьи будет возможность ключами указывать некоторые опции, в том числе на создание полноцветных PNG файлов, но без адаптации их под ATARI, для 5000 файлов такая обработка длится всего 7 секунд. Этот блок конвертера я взял на Github у Александра Коваленко из Краснодара, он написан на питоне в ООП, я же, как старовер, переделал его под процедурный формат. Не знаю как там на питоне, но на C# вариант со сдвигом битов и мой вариант с циклами работают с одинаковой скоростью.
Вот несколько артов в PNG
![](https://habrastorage.org/getpro/habr/upload_files/719/0cf/7c9/7190cf7c9a30399dc0b0ce3ba94a97cb.png)
![](https://habrastorage.org/getpro/habr/upload_files/11c/8d0/adf/11c8d0adfe54d13fd6cddfcd09431ff3.png)
![](https://habrastorage.org/getpro/habr/upload_files/1c8/725/1bf/1c87251bf0158300a9ff4dcd1fab7851.png)
Думаю в рамках этой статьи информации было достаточно. Спасибо всем кто дочитал до конца.
Update4: добавил на Github исполнимый файл для Windows .NET 4.8