Использование VGA графики на ассемблере x86

flat assembler

В этой заметке я покажу, как инициализируется графический режим VGA с разрешением 320×200 точек с палитрой в 256 цветов и создам программу, отображающую эту палитру.

Предыдущая заметка: Настройка DOSBox в Ubuntu для программирования на ассемблере x86

Сразу хочу отметить свой недочёт в предыдущей заметке про настройку DOSBox и flat assembler. Оказывается, в составе flat assembler есть прекрасный редактор fasmd (точнее IDE), который больше подойдёт для работы, нежели редактор от Dos Navigator. Например, чтобы скомпилировать и запустить программу, достаточно нажать F9. Также можно работать сразу с несколькими файлами. Подробная информация по редактору и его горячим клавишам доступна в файле fasmd.txt.

Окно редактора fasmd

Кроме того, можно редактировать исходники непосредственно на хосте в любом удобном редакторе, например в Visual Studio Code. Конечно, в этом случае всё равно придётся компилировать и запускать код непосредственно в окне DOSBox.

Итак, для начала возьмём созданный ранее файл START.ASM.

1
2
3
4
5
; START.ASM

        org     100h

        int     20h

Скопируем его содержимое в файл VGA.ASM и добавим следующие строки:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
; VGA.ASM

        org     100h

        mov     ax,0013h    ; Выбираем режим 320x200 256 цветов
        int     10h         ; Видеосервис: Установка видеорежима

        xor     ah,ah       ; Выбираем режим ожидания ввода AH = 00h
        int     16h         ; Сервис клавиатуры: ждём нажатия на клавишу

        mov     ax,0003h    ; Выбираем текстовый режим 80x25 16 цветов
        int     10h         ; Видеосервис: Установка видеорежима

        int     20h

Ах, эти старые добрые времена, когда графический режим можно было установить всего несколькими байтами!

Эта небольшая программа переключает экран в графический режим, затем ожидает нажатия на любую клавишу, переключает экран в текстовый режим и завершает свою работу. Теперь подробнее:

Строки 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 точек), а по вертикали 25632 = 8 строк (блоками высотой по 25 точек).

Рисовать будем в один проход, слева направо и сверху вниз по одной точке. Нам потребуется 4 вложенных цикла:

8 строк блоков × по 25 точек высотой каждый × 32 столбца × 10 точек шириной = 64 000 точек = 320×200

Начнём с цикла для рисования подряд 10 точек. Потом повторим этот цикл 32 раза, каждый раз меняя цвет, и получим линию из 32 цветов шириной во весь экран.

Вывести на экран точку можно при помощи вызова прерывания с аргументом 10h. Но это будет очень медленно. Поэтому будем писать напрямую в видеопамять! 😎

Копируем содержимое файла VGA.ASM в файл PALETTE.ASM и добавим следующие строки:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
; PALETTE.ASM

        org     100h

        mov     ax,0013h    ; Выбираем режим 320x200 256 цветов
        int     10h         ; Видеосервис: Установка видеорежима

        push    0A000h      ; Отправляем в стэк адрес сегмента видеопамяти
        pop     es          ; Извлекаем адрес сегмента видеопамяти в ES
        xor     di,di       ; Обнуляем смещение от начала сегмента видеопамяти

        xor     ax,ax       ; Обнуляем индекс цвета палитры

        mov     cx,10       ; Цикл для 10 точек
plot:   stosb               ; Записываем байт в память
        loop    plot        ; Повторяем

        xor     ah,ah       ; Выбираем режим ожидания ввода AH = 00h
        int     16h         ; Сервис клавиатуры: ждём нажатия на клавишу

        mov     ax,0003h    ; Выбираем текстовый режим 80x25 16 цветов
        int     10h         ; Видеосервис: Установка видеорежима

        int     20h

Строки 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 точек подряд чёрным цветом на чёрном фоне. Давайте её немного улучшим, добавив второй цикл.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
; PALETTE.ASM

        org     100h

        mov     ax,0013h    ; Выбираем режим 320x200 256 цветов
        int     10h         ; Видеосервис: Установка видеорежима

        push    0A000h      ; Отправляем в стэк адрес сегмента видеопамяти
        pop     es          ; Извлекаем адрес сегмента видеопамяти в ES
        xor     di,di       ; Обнуляем смещение от начала сегмента видеопамяти

        xor     ax,ax       ; Обнуляем индекс цвета палитры

        mov     cx, 32      ; Цикл для 32 цветов
col:    push    cx          ; Отправляем значение счётчика в стэк

        mov     cx,10       ; Цикл для 10 точек
plot:   stosb               ; Записываем байт в память
        loop    plot        ; Повторяем

        pop     cx          ; Восстанавливаем значение счётчика из стэка
        inc     al          ; Увеличиваем индекс палитры на один
        loop    col         ; Повторяем

        xor     ah,ah       ; Выбираем режим ожидания ввода AH = 00h
        int     16h         ; Сервис клавиатуры: ждём нажатия на клавишу

        mov     ax,0003h    ; Выбираем текстовый режим 80x25 16 цветов
        int     10h         ; Видеосервис: Установка видеорежима

        int     20h

Ура! Наконец хоть какой-то результат! Происходящее, думаю, будет понятно из комментариев к добавленным строкам. Здесь используется такой-же цикл, только он сохраняет свой счетчик в стэке, а после отработки внутреннего цикла восстанавливает свой счётчик и увеличивает текущий индекс цвета.

VGA линия из первых 32 цветов

Добавим третий цикл и придадим нашим цветным линиям толщины, нарисовав 25 раз подряд тоже самое. Здесь добавляется регистр DX, который будет хранить начальное значение цвета для каждой строки палитры. После отрисовки линии в 32 цвета будем сбрасывать индекс цвета на начальное значение для этой линии.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
; PALETTE.ASM

        org     100h

        mov     ax,0013h    ; Выбираем режим 320x200 256 цветов
        int     10h         ; Видеосервис: Установка видеорежима

        push    0A000h      ; Отправляем в стэк адрес сегмента видеопамяти
        pop     es          ; Извлекаем адрес сегмента видеопамяти в ES
        xor     di,di       ; Обнуляем смещение от начала сегмента видеопамяти

        xor     ax,ax       ; Обнуляем текущий индекс цвета палитры
        xor     dx,dx       ; Обнуляем индекс цвета палитры для строки

        mov     cx,25       ; Цикл для 25 линий
block:  push    cx          ; Отправляем значение счётчика в стэк

        mov     cx, 32      ; Цикл для 32 цветов
col:    push    cx          ; Отправляем значение счётчика в стэк

        mov     cx,10       ; Цикл для 10 точек
plot:   stosb               ; Записываем байт в память
        loop    plot        ; Повторяем 10 раз

        pop     cx          ; Восстанавливаем значение счётчика из стэка
        inc     al          ; Увеличиваем индекс цвета на один
        loop    col         ; Повторяем 32 раза

        pop     cx          ; Восстанавливаем значение счётчика из стэка
        mov     ax,dx       ; Устанавливаем текущее значение индекса цвета для строки
        loop    block       ; Повторяем 25 раз

        xor     ah,ah       ; Выбираем режим ожидания ввода AH = 00h
        int     16h         ; Сервис клавиатуры: ждём нажатия на клавишу

        mov     ax,0003h    ; Выбираем текстовый режим 80x25 16 цветов
        int     10h         ; Видеосервис: Установка видеорежима

        int     20h

Теперь у наших цветных линий появилась высота в 25 точек.

VGA линия из первых 32 цветов толщиной в 25 точек

Последний цикл повторит отрисовку строки блоков цветов ещё 8 раз. Каждый раз прибавляя к начальному индексу цвета строки по 32.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
; PALETTE.ASM

        org     100h

        mov     ax,0013h    ; Выбираем режим 320x200 256 цветов
        int     10h         ; Видеосервис: Установка видеорежима

        push    0A000h      ; Отправляем в стэк адрес сегмента видеопамяти
        pop     es          ; Извлекаем адрес сегмента видеопамяти в ES
        xor     di,di       ; Обнуляем смещение от начала сегмента видеопамяти

        xor     ax,ax       ; Обнуляем текущий индекс цвета палитры
        xor     dx,dx       ; Обнуляем индекс цвета палитры для строки

        mov     cx,8        ; Цикл для 8 строк
row:    push    cx          ; Отправляем значение счётчика row в стэк

        mov     cx,25       ; Цикл для 25 линий
block:  push    cx          ; Отправляем значение счётчика block в стэк

        mov     cx, 32      ; Цикл для 32 цветов
col:    push    cx          ; Отправляем значение счётчика col в стэк

        mov     cx,10       ; Цикл для 10 точек
plot:   stosb               ; Записываем байт в память
        loop    plot        ; Повторяем 10 раз

        pop     cx          ; Восстанавливаем значение счётчика col из стэка
        inc     al          ; Увеличиваем индекс цвета на один
        loop    col         ; Повторяем 32 раза

        pop     cx          ; Восстанавливаем значение счётчика block из стэка
        mov     ax,dx       ; Устанавливаем текущее значение индекса цвета для строки
        loop    block       ; Повторяем 25 раз

        pop     cx          ; Восстанавливаем значение счётчика row из стэка
        add     al,32       ; Увеличиваем текущий индекс цвета на 32
        mov     dx,ax       ; Сохраняем начальный индекс цвета палитры для строки
        loop    row         ; Повторяем 8 раз

        xor     ah,ah       ; Выбираем режим ожидания ввода AH = 00h
        int     16h         ; Сервис клавиатуры: ждём нажатия на клавишу

        mov     ax,0003h    ; Выбираем текстовый режим 80x25 16 цветов
        int     10h         ; Видеосервис: Установка видеорежима

        int     20h

Получилась наша палитра!

VGA палитра 256 цветов

В следующей заметке буду жечь: создам эффект огня.

Ссылки