Страница Ивана Рощина > Статьи >

© Иван Рощин, Москва

ZXNet: 500:95/462.53
E-mail: bestview@mtu-net.ru
WWW: http://www.ivr.da.ru

Секреты текстового вывода

Радиолюбитель. Ваш компьютер» 2,3/2001, под псевдонимом BV_Creator)
Дата последнего редактирования: 15.12.2002.

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

Сначала несколько общих слов. Как известно, в ZX Spectrum реализован единственный графический режим с разрешением 256x192. Цвета в нём задаются не для каждой точки, а сразу для целого квадрата 8x8 — то есть фактически мы имеем не настоящее цветное изображение, а раскрашенное чёрно-белое.

В других компьютерах (например, PC) наряду с графическими режимами обычно имеются и текстовые. ZX Spectrum лишён этой возможности, и на нём приходится выводить текст в графическом режиме. С одной стороны, это медленнее и отнимает больше памяти, но с другой стороны — размеры, форма и местоположение символов могут быть любыми. В графическом режиме также становится возможным плавный скроллинг при просмотре текста, что несомненно повышает удобство чтения.

Я буду рассматривать наиболее часто встречающийся случай, когда шрифт задан в растровом виде (бывают ещё векторные шрифты), и все его символы имеют одинаковую ширину и высоту.

При печати может быть использован как шрифт 8x8, находящийся в ПЗУ по адресам #3D00—#3FFF (там хранятся лишь изображения символов с кодами #20—#7F), так и шрифт, загружаемый в ОЗУ (размеры и количество символов в котором, само собой, могут быть произвольными).

Процедура печати символа обычно имеет дело с такими параметрами: код символа (1 байт), координаты (x,y) и цвет. Координаты чаще всего задаются не в пикселах, а в знакоместах (под знакоместами я имею в виду области размером в один символ). Скажем, при использовании шрифта 8x8 на экране помещается 32 символа по горизонтали и 24 по вертикали; соответственно, координата x может изменяться от 0 до 31, а y — от 0 до 23. Начало координат (0,0) традиционно располагается в верхнем левом углу экрана.

Как видим, происходит некоторая эмуляция текстового режима с помощью графического. В 99% случаев при этом используется один из следующих трёх «текстовых» режимов: 32x24 (шрифт 8x8), 42x24 (шрифт 6x8) и 64x24 (шрифт 4x8). (Как вы можете заметить, в режиме 42x24 четыре пиксела по горизонтали остаются неиспользованными — обычно их либо равномерно распределяют справа и слева, либо оставляют на какой-то одной стороне.)

В режиме 32x24 каждый символ может иметь свой цвет (что непосредственно вытекает из структуры спектрумовского экрана). В режимах 42x24 и 64x24 это невозможно, но там каждое слово может быть окрашено в свой цвет (если считать, что слова разделены пробелами). Другие режимы (скажем, 51x24 — при использовании шрифта 5x8) лишены и этого.

О теории я сказал, кажется, уже достаточно. А теперь начнём оптимизировать! :)

Обычно в шрифте под изображение каждого символа отводится восемь байтов. Но довольно часто такое представление оказывается избыточным, так как некоторые биты не используются. К примеру, для шрифта 6x8 (рис. 1) два крайних столбца не задействованы и всегда равны нулю.

Рис. 1

Когда шрифт расположен в ОЗУ, такое его представление вполне оправданно, как обеспечивающее достаточную скорость печати. (Между прочим, когда печать символов 6x8 хотят сделать особенно быстрой, используют целых четыре набора символов, каждый из которых сдвинут относительно другого на два пиксела по горизонтали — чтобы не тратить время на сдвиг при печати каждого символа.) Но для экономии дискового пространства, занимаемого вашей программой, имеет смысл удалять из шрифта всю избыточную информацию.

Экономия при этом может быть весьма значительной: так, шрифт из 256 символов 6x8, изначально занимавший #800 байтов, уменьшится на четверть. А если в таком шрифте изображение символов реально занимает только 5x6 пикселов (т.е. между символами предусмотрен обязательный пробел в один пиксел по горизонтали и два по вертикали), то он сократится более чем вдвое!

Ниже приведена процедура, удаляющая из шрифта избыточную информацию. Смысл используемых в ней констант font_sx, font_x, font_sy и font_y пояснён на рис. 2.

Рис. 2

SOURCE     EQU  #8000 ;Здесь расположен исходный шрифт,
DESTINY    EQU  #C000 ;а сюда поместим упакованный...
KOLVO      EQU  #100  ;Количество символов (1-256).

font_sx    EQU  1 ;Эти параметры определяют используемую часть
font_sy    EQU  1 ;матрицы 8x8 для преобразуемого шрифта.
font_x     EQU  5
font_y     EQU  6

DEST_LEN   EQU  ((font_x*font_y*KOLVO)+7)/8 ;Длина упакованного
                                            ;шрифта в байтах.

           ORG  #6000

           LD   HL,SOURCE
           LD   IX,DESTINY
           LD   E,0
           LD   B,KOLVO

M1         PUSH BC
           PUSH HL

           LD   BC,font_sy
           ADD  HL,BC

           LD   B,font_y
M7         LD   A,(HL)

           LD   C,font_sx
           INC  C
M2         DEC  C
           JR   Z,M3
           ADD  A,A
           JR   M2

M3         LD   C,font_x
           INC  C
M4         DEC  C
           JR   Z,M5
           ADD  A,A
           RL   (IX)
           INC  E
           BIT  3,E
           JR   Z,M4
           INC  IX
           LD   E,0
           JR   M4

M5         INC  HL
           DJNZ M7

           POP  HL
           LD   BC,8
           ADD  HL,BC

           POP  BC
           DJNZ M1

           DEC  E
M6         INC  E
           RET  Z

           BIT  3,E
           RET  NZ

           SLA  (IX)
           JR   M6

После загрузки программы с диска, естественно, такой шрифт нужно будет преобразовать к его первоначальному виду. Вот соответствующая процедура (если заранее известно, какими будут значения констант, то её можно оптимизировать):

SOURCE     EQU  #8000 ;Здесь расположен упакованный шрифт,
DESTINY    EQU  #C000 ;а сюда будем распаковывать...
KOLVO      EQU  #100  ;Количество символов (1-256).

font_sx    EQU  1 ;См. предыдущую процедуру.
font_sy    EQU  1
font_x     EQU  5
font_y     EQU  6

           ORG  #6000

           LD   HL,DESTINY
           PUSH HL
           LD   DE,DESTINY+1
           LD   BC,KOLVO*8-1
           LD   (HL),0
           LDIR

           LD   IX,SOURCE
           POP  HL
           LD   E,8
           LD   D,(IX)
           LD   B,KOLVO

M1         PUSH BC
           PUSH HL

           LD   BC,font_sy
           ADD  HL,BC

           LD   B,font_y

M7         LD   C,font_x
           XOR  A
M3         SLA  D
           ADC  A,A
           DEC  E
           JR   NZ,M2
           LD   E,8
           INC  IX
           LD   D,(IX)
M2         DEC  C
           JR   NZ,M3

           LD   C,8-(font_sx+font_x)
           INC  C
M4         DEC  C
           JR   Z,M5
           ADD  A,A
           JR   M4

M5         LD   (HL),A
           INC  HL
           DJNZ M7

           POP  HL
           LD   BC,8
           ADD  HL,BC

           POP  BC
           DJNZ M1

           RET

Впрочем, если вам необходимо экономить оперативную память, можно оставить шрифт и в сжатом виде, пожертвовав скоростью печати. Ну а чтобы скорость всё-таки не очень пострадала, можно использовать ещё несколько приёмов оптимизации. Например, если печатается пробел, то быстрее будет просто очистить соответствующее знакоместо на экране. При печати символа имеет смысл сначала восстановить его изображение в специальном буфере и уже оттуда выводить на экран, учитывая при этом, что если печатается символ с тем же кодом, что и предыдущий, то его изображение уже сформировано в буфере (своеобразное кэширование). Проверка совпадения кодов символов может быть реализована примерно так:

        ...............    ;Печатаемый символ в регистре A,
        CP      0          ;сравниваем его с кодом предыдущего
LAST_S  EQU     $-1        ;символа (хранится в самой команде).
        JR      Z,GO_PRN   ;Если совпали, выводим содержимое
                           ;буфера,
        LD      (LAST_S),A ;иначе запоминаем код символа и
        ...............    ;формируем его изображение в буфере.
GO_PRN  ...............    ;Вывод на экран содержимого буфера.

Ещё один способ уменьшения размера шрифта, который может использоваться совместно с предыдущим, — удаление из шрифта неиспользуемых символов. Для этого можно воспользоваться такой процедурой:

SOURCE  EQU     #8000 ;Адрес исходного шрифта (256 символов).
DESTINY EQU     #C000 ;Адрес преобразованного шрифта.
HIGH    EQU     8     ;Сколько байтов занимает один символ.

        ORG     #6000

        LD      IX,TAB_DEL
        LD      HL,SOURCE
        LD      DE,DESTINY
        XOR     A          ;Текущий символ.

NEXT_S  PUSH    AF
        CALL    CHECK
        LD      BC,HIGH
        JR      NZ,NO_LDIR
        LDIR               ;Обнуляет BC.
NO_LDIR ADD     HL,BC
        POP     AF
        INC     A
        JR      NZ,NEXT_S

        RET

;Таблица удаляемых символов представлена
;в виде битового массива размером в 256
;битов (32 байта). Если элемент массива
;равен единице, соответствующий символ
;будет удалён из шрифта.

TAB_DEL DB %00000000,%00000000
        DB %00000000,%00000000
        DB %00000000,%00000000
        DB %00000000,%00000000
        DB %00000000,%00000000
        DB %00000000,%00000000
        DB %00000000,%00000000
        DB %00000000,%00000000
        DB %00000000,%00000000
        DB %00000000,%00000000
        DB %00000000,%00000000
        DB %00000000,%00000000
        DB %00000000,%00000000
        DB %00000000,%00000000
        DB %00000000,%00000000
        DB %00000000,%00000000

;Процедура CHECK предназначена для
;работы с битовыми массивами длиной
;до 256 элементов.
;
;Вход:  IX - адрес массива,
;        A - номер элемента.
;
;Выход: если соотв. элемент массива
;       равен 0, флаг Z установлен.
;       Значение A изменено.
;
;Если заменить команду BIT на SET или
;RES, можно не только проверять значения
;элементов массива, но и изменять их.

CHECK   PUSH    AF ;Сохранили номер элемента.

; Работа с элементом массива происходит
;с помощью команды BIT N,(IX+S), которая
;перед этим формируется в памяти.
;Значения N и S вычисляются по формуле:
;S = старшие 5 битов номера элемента
;(номер байта в массиве, где находится
; нужный элемент);
;N = 8 - младшие 3 бита номера элемента
;(номер бита в байте массива; элементы
; располагаются в байте слева направо,
; а биты нумеруются наоборот).
;
;Команда занимает в памяти 4 байта и
;выглядит так:
;
;#DD #CB S %01NNN110
; |   |  |  \|\|/\|/
; |   |  |   | |  `-- общая часть для BIT, SET, RES
; |   |  |   | `----- номер бита
; |   |  |   `------- %01 для BIT, %11 для SET, %10 для RES
; |   |  `----------- смещение
; `---+-------------- префиксы

        AND     7
        RLCA
        RLCA
        RLCA
        XOR     %01111110 ;%11111110 для SET, %10111110 для RES.
        LD      (CHECK_1+3),A
        POP     AF
        RRCA
        RRCA
        RRCA
        AND     %00011111
        LD      (CHECK_1+2),A
CHECK_1 BIT     0,(IX)

        RET

Может быть и так, что вы сами не знаете, печать каких символов производится в вашей программе, а каких — нет. В этом случае вставьте в процедуру печати символа приведённый ниже фрагмент и запустите вашу программу. После окончания её работы в битовом массиве TAB_DEL будут сведения о том, какие символы можно удалить из шрифта (в таком же формате, как и в предыдущем примере). Логика работы такова: при каждом вызове процедуры печати символа соответствующий бит в массиве обнуляется, и в результате равными единице остаются лишь те элементы массива, которые соответствуют символам, ни разу не печатавшимся за всё время работы программы.

;Начало процедуры печати: в аккумуляторе -
;код печатаемого символа.

        PUSH    AF
        PUSH    IX
        LD      IX,TAB_DEL
        CALL    RES_BIT
        POP     IX
        POP     AF
        ............  ;Продолжение процедуры печати.
        RET

;Вспомогательная процедура, аналогичная
;рассмотренной выше процедуре CHECK:

RES_BIT PUSH    AF
        AND     7
        RLCA
        RLCA
        RLCA
        XOR     %10111110
        LD      (CHECK_1+3),A
        POP     AF
        RRCA
        RRCA
        RRCA
        AND     %00011111
        LD      (CHECK_1+2),A
CHECK_1 RES     0,(IX)
        RET

TAB_DEL DB #FF,#FF,#FF,#FF
        DB #FF,#FF,#FF,#FF
        DB #FF,#FF,#FF,#FF
        DB #FF,#FF,#FF,#FF
        DB #FF,#FF,#FF,#FF
        DB #FF,#FF,#FF,#FF
        DB #FF,#FF,#FF,#FF
        DB #FF,#FF,#FF,#FF

При использовании такого шрифта возникают некоторые проблемы с печатью символа, а именно — с вычислением смещения от начала шрифта, по которому находится изображение данного символа. Тут можно либо использовать таблицу удалённых символов, либо перекодировать все текстовые сообщения, выводимые в программе.

Чтобы ещё уменьшить количество используемых символов, можно заменить в выводимом тексте русские буквы на похожие по начертанию латинские. Вот соответствующая процедура:

TEXT       EQU  #8000 ;Адрес начала текста.
LENGTH     EQU  #1234 ;Длина текста.

           ORG  #6000

           LD   HL,TEXT
           LD   BC,LENGTH

M1         LD   DE,TABLE-1
M2         INC  DE
           LD   A,(DE)
           INC  DE
           AND  A
           JR   Z,M3
           CP   (HL)
           JR   NZ,M2
           LD   A,(DE)
           LD   (HL),A
M3         INC  HL
           DEC  BC
           LD   A,B
           OR   C
           JR   NZ,M1

           RET

;Пары символов - что на что заменять:

TABLE      DB   "А","A"
           DB   "В","B"
           DB   "С","C"
           DB   "Е","E"
           DB   "Н","H"
           DB   "К","K"
           DB   "М","M"
           DB   "О","O"
           DB   "Р","P"
           DB   "Т","T"
           DB   "Х","X"
           DB   "а","a"
           DB   "с","c"
           DB   "е","e"
           DB   "к","k"
           DB   "п","n"
           DB   "о","o"
           DB   "р","p"
           DB   "х","x"
           DB   "у","y"
           DB   0       ;Признак конца таблицы.

Нужно только следить, чтобы изображения букв в используемом шрифте были действительно похожи. Если же, например, латинские буквы в шрифте выполнены толще русских, то результат будет, как на рис. 3.

Рис. 3

Если в вашей программе используется шрифт 6x8, для экономии памяти можно формировать образы символов с кодами 32—127 непосредственно во время печати, используя шрифт ПЗУ. Вот пример небольшой программы, которая формирует таким способом шрифт и распечатывает на экране все получившиеся символы.

        ORG     #6000

        LD      HL,#3D00 ;Адрес шрифта ПЗУ.
        LD      DE,#8000 ;Здесь будет шрифт 6x8.
        LD      BC,#300  ;Длина шрифта.

NEXT_B  LD      A,D      ;  Все символы разделяются на
        CP      #82      ;две группы:
        JR      Z,NO_IZM ;1) #2F-#5F
        CP      #80      ;2) #20-#2E,#60-#7F
        JR      NZ,IZM_1 ;  Преобразование символов
        LD      A,E      ;осуществляется по-разному,
        CP      15*8     ;в зависимости от того, к
        JR      C,NO_IZM ;какой группе они принадлежат.

IZM_1   LD      A,(HL)   ;Преобразование символов
        PUSH    BC       ;первой группы.
        PUSH    AF
        AND     %00001111
        RLCA
        LD      B,A
        POP     AF
        AND     %11110000
        OR      B
        POP     BC
        JR      BYTE_OK

NO_IZM  LD      A,(HL)   ;Преобразование символов
BYTE_OK RLCA             ;второй группы.
        AND     %11111100
        LD      (DE),A
        INC     HL
        INC     DE
        DEC     BC
        LD      A,B
        OR      C
        JR      NZ,NEXT_B

;Шрифт 6x8 подготовлен, теперь печатаем
;все полученные символы:

        LD      HL,#8000-#100
        LD      (#5C36),HL
        CALL    3435
        LD      A,2
        CALL    5633

        LD      A," "
PRINT_S PUSH    AF
        RST     16
        POP     AF
        INC     A
        CP      128
        JR      NZ,PRINT_S
        RET
Рис. 4

Между прочим, можно было бы применить этот способ в мониторе-отладчике STS — там как раз используется шрифт 6x8. А за счёт освободившегося места можно было бы реализовать в этом отладчике какие-либо новые возможности…

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

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

При печати символа необходимо сначала (зная его код и адрес расположения в памяти шрифта) вычислить адрес, по которому находится первый байт изображения символа, а затем по очереди читать байт изображения и записывать его в видеопамять.

Пусть код символа задан в аккумуляторе, адрес в видеопамяти (вычисляемый по координатам печати) задан в регистровой паре DE и известно, что шрифт расположен с адреса FONT. Тогда процедура печати будет выглядеть примерно так:

        LD   H,0     ;Вычисление адреса
        LD   L,A     ;образа символа
        ADD  HL,HL   ;(10 байтов/65 тактов).
        ADD  HL,HL
        ADD  HL,HL
        LD   BC,FONT
        ADD  HL,BC

        LD   B,8     ;Вывод на экран (для повышения скорости
M1      LD   A,(HL)  ;цикл можно раскрыть).
        LD   (DE),A
        INC  HL      ;Если шрифт расположен с адреса,
        INC  D       ;кратного 8, - можно просто INC L.
        DJNZ M1
        RET

«Да это всё давно известно» — скажет кто-то. Не буду спорить. Обратите только внимание на то, сколько ресурсов тратится на вычисление адреса образа символа. А если высота символов будет не восемь пикселов, а, скажем, семь? Тогда программа ещё более усложнится, ведь уже не обойдёшься тремя сдвигами, как при умножении на восемь…

Между тем, существует гораздо более эффективный способ хранения шрифта в памяти: шрифт начинается с адреса, кратного 256, в первых 256 байтах последовательно расположены верхние строки всех символов, в следующих 256 байтах — вторые сверху строки, и так далее. В этом случае, независимо от высоты символов, вычисление адреса образа символа при печати практически не требует ресурсов. Вот пример процедуры печати символа:

        LD   H,FONT/256  ;Вычисление адреса
        LD   L,A         ;образа символа
                         ;(3 байта/11 тактов).

        LD   B,8     ;Вывод на экран (для повышения скорости
M1      LD   A,(HL)  ;цикл можно раскрыть).
        LD   (DE),A
        INC  H
        INC  D
        DJNZ M1
        RET

Не правда ли, гораздо эффективнее? Правда, есть один недостаток: если в шрифте хранятся образы не всех 256 символов, то после преобразования он будет занимать больше места в памяти. (В общем случае размер будет равен H*256 байтов, где H — высота символа.)

Вот процедура, перекодирующая шрифт из обычного представления в более эффективное:

SOURCE     EQU  #8000 ;Адрес расположения исходного шрифта.
DESTINY    EQU  #C000 ;Здесь разместится преобразованный шрифт.
HIGH       EQU  8     ;Высота символов.

           LD   HL,SOURCE
           LD   DE,DESTINY

M1         PUSH DE
           LD   B,HIGH

M2         LD   A,(HL)
           LD   (DE),A
           INC  HL
           INC  D
           DJNZ M2

           POP  DE
           INC  E
           JR   NZ,M1

           RET

А вот процедура, выполняющая обратное преобразование:

SOURCE     EQU  #8000 ;Откуда.
DESTINY    EQU  #C000 ;Куда.
HIGH       EQU  8     ;Высота символов.

           LD   HL,SOURCE
           LD   DE,DESTINY

M1         PUSH HL
           LD   B,HIGH

M2         LD   A,(HL)
           LD   (DE),A
           INC  H
           INC  DE
           DJNZ M2

           POP  HL
           INC  L
           JR   NZ,M1

           RET

Осталось только сказать, что, хотя такой способ хранения шрифта считается довольно известным, я до недавнего времени не предполагал о его существовании. А рассказал мне о нём GoBLiN/BMZ — спасибо!

Страница Ивана Рощина > Статьи >