VGA графика на ассемблере x86
В этой заметке я покажу, как инициализируется графический режим VGA с разрешением 320×200 точек с палитрой в 256 цветов и создам программу, отображающую эту палитру.
Сразу хочу отметить свой недочёт в предыдущей заметке про настройку DOSBox и flat assembler. Оказывается, в составе flat assembler есть прекрасный редактор fasmd
(точнее IDE), который больше подойдёт для работы, нежели редактор от Dos Navigator. Например, чтобы скомпилировать и запустить программу, достаточно нажать F9
. Также можно работать сразу с несколькими файлами. Подробная информация по редактору и его горячим клавишам доступна в файле fasmd.txt
.
Кроме того, можно редактировать исходники непосредственно на хосте в любом удобном редакторе, например в Visual Studio Code. Конечно, в этом случае всё равно придётся компилировать и запускать код непосредственно в окне DOSBox.
Итак, для начала возьмём созданный ранее файл START.ASM
.
|
|
Скопируем его содержимое в файл VGA.ASM
и добавим следующие строки:
|
|
Ах, эти старые добрые времена, когда графический режим можно было установить всего несколькими байтами!
Эта небольшая программа переключает экран в графический режим, затем ожидает нажатия на любую клавишу, переключает экран в текстовый режим и завершает свою работу. Теперь подробнее:
Строки 5 и 6
Для переключения видеорежима используется вызов прерывания с аргументом 10h
. При этом в старшем разряде регистра AX
передаётся AH = 00h
, а в младшем разряде AL = 13h
номер видеорежима. Режим под номером 13h
соответствует графическому режиму VGA с разрешением 320×200 точек с палитрой в 256 цветов.
Строки 8 и 9
Обработка нажатий на клавиши осуществляется при помощи вызова прерывания с аргументом 16h
. При этом в старшем разряде регистра AX
передаётся AH = 00h
.
Кстати, инструкцию xor ah,ah
можно записать как mov ah,0
. Инструкция XOR
в ассемблере выполняет операцию исключающего ИЛИ
между всеми битами двух операндов. Соответственно, если подать на входы два одинаковых значения, то результат будет равен нулю. Результат операции XOR
записывается в первый операнд.
После вызова прерывания ASCII-код нажатой клавиши будет находиться в регистре AL
. Но в данном случае это неважно. Press Any Key
.
Строки 11 и 12
Здесь вновь переключаем видеорежим, подготавливаясь к выходу из программы. Но на сей раз в младшем разряде регистра AX
находится AL = 03h
, что соответствует текстовому режиму 80×25 символов с разрешением 640×200 и палитрой в 16 цветов.
Рисуем палитру 🎨
Теперь можно попробовать нарисовать что-нибудь. Например, палитру всех доступных цветов заполнив весь экран.
Посчитаем. Разрешение экрана 320×200 точек и палитра в 256 цветов. Пусть по горизонтали будет 32 столбца (блоками шириной по 10 точек), а по вертикали 256/32 = 8 строк (блоками высотой по 25 точек).
Рисовать будем в один проход, слева направо и сверху вниз по одной точке. Нам потребуется 4 вложенных цикла:
8 строк блоков
× по 25 точек высотой каждый
× 32 столбца
× 10 точек шириной
= 64 000
точек = 320×200
Начнём с цикла для рисования подряд 10 точек. Потом повторим этот цикл 32 раза, каждый раз меняя цвет, и получим линию из 32 цветов шириной во весь экран.
Вывести на экран точку можно при помощи вызова прерывания с аргументом 10h
. Но это будет очень медленно. Поэтому будем писать напрямую в видеопамять! 😎
Копируем содержимое файла VGA.ASM
в файл PALETTE.ASM
и добавим следующие строки:
|
|
Строки 8 – 10
В режиме VGA 320×200 с 256 цветами для отображения видеопамяти на основное адресное пространство используется 64 000 байт, располагающихся с адреса A000h:0000h
. Для возможности записи данных в это адресное пространство необходимо занести его начало в регистр ES
. Для этого сначала отправляем (push
) значение адреса в стэк, затем снимаем (pop
) его со стэка сразу в регистр ES
. И обнуляем смещение от начала сегмента в индексном регистре DI
.
Почему через стэк? Потому что сделать mov es,0A000h
нельзя!
Почему push 0A000h
, а не push A000h
? Потому что A000h
начинается с буквы и ассемблер распознает его как метку, а не число. А такой метки у нас нет!
После выполнения этих команд регистр ES
содержит адрес начала сегмента видеопамяти объемом 64К. А регистр DI
смещение от начала сегмента видеопамяти равное нулю.
Строка 12
В младшем разряде AL
регистра AX
будет храниться текущее значение индекса цвета палитры, их у нас 256: от 00h
до FFh
. Рисовать палитру будем с первого индекса. Поэтому обнуляем.
Строки 14 – 16
Здесь у нас цикл. Сначала отправляем в регистр CX
значение 10
, это счётчик.
Затем выполняем команду stosb
, которая сохраняет значение регистра AL
по адресу ES:DI
и увеличивает значение индексного регистра DI
. Слева на этой строке plot
– это метка, на которую будет переходить цикл, пока не обнулится счётчик.
Инструкция loop
уменьшает значение в регистре СХ
. Если после этого значение в СХ
не равно нулю, то команда loop
выполняет переход на метку plot
.
Если сейчас запустить эту программу, то мы ничего не увидим. Хотя она честно нарисует 10 точек подряд чёрным цветом на чёрном фоне. Давайте её немного улучшим, добавив второй цикл.
|
|
Ура! Наконец хоть какой-то результат! Происходящее, думаю, будет понятно из комментариев к добавленным строкам. Здесь используется такой-же цикл, только он сохраняет свой счетчик в стэке, а после отработки внутреннего цикла восстанавливает свой счётчик и увеличивает текущий индекс цвета.
Добавим третий цикл и придадим нашим цветным линиям толщины, нарисовав 25 раз подряд тоже самое. Здесь добавляется регистр DX
, который будет хранить начальное значение цвета для каждой строки палитры. После отрисовки линии в 32 цвета будем сбрасывать индекс цвета на начальное значение для этой линии.
|
|
Теперь у наших цветных линий появилась высота в 25 точек.
Последний цикл повторит отрисовку строки блоков цветов ещё 8 раз. Каждый раз прибавляя к начальному индексу цвета строки по 32.
|
|
Получилась наша палитра!