ZXNet эхоконференция «code.zx»


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



от: Ivan Roshin
кому: All
дата: 24 Mar 2001
Hello, All!

═══════════════════ text_out.1 ══════════════════

(c) Иван Рощин, Москва

Fido : 2:5020/689.53
ZXNet : 500:95/462.53
E-mail: asder_ffc@softhome.net
WWW : http://www.zx.ru/echo/roschin

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

("Радиолюбитель. Ваш компьютер" 2-3/2001)

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

Сначала несколько общих слов. Как известно, в ZX Spectrum
реализован единственный графический режим с разрешением 256*192.
Цвета в нем задаются не для каждой точки, а сразу для целого
квадрата 8*8 - то есть фактически мы имеем не настоящее цветное
изображение, а раскрашенное черно-белое.
В других компьютерах (например, PC) наряду с графическими
режимами обычно имеются и текстовые. Спектрум лишен этой
возможности, и на нем приходится выводить текст в графическом
режиме. С одной стороны, это медленнее и отнимает больше памяти,
но с другой стороны - размеры, форма и местоположение символов
могут быть любыми. В графическом режиме также становится
возможным плавный скроллинг при просмотре текста, что несомненно
повышает удобство чтения.
Я буду рассматривать наиболее часто встречающийся случай,
когда шрифт задан в растровом виде (бывают еще векторные
шрифты), и все его символы имеют одинаковую ширину и высоту.
При печати может быть использован как шрифт 8*8, находящийся
в ПЗУ по адресам #3D00-#3FFF (там хранятся лишь изображения
символов с кодами #20-#7F), так и шрифт, загружаемый в ОЗУ
(размеры и количество символов в котором, само собой, могут быть
произвольными).
Процедура печати символа обычно имеет дело с такими
параметрами: код символа (byte), координаты (x,y) и цвет.
Координаты чаще всего задаются не в пикселах, а в знакоместах
(под знакоместами я имею в виду области размером в один символ).
Скажем, при использовании шрифта 8*8 на экране помещается 32
символа по горизонтали и 24 по вертикали; соответственно,
координата x может изменяться от 0 до 31, а y - от 0 до 23.
Начало координат (0,0) традиционно располагается в верхнем левом
углу экрана.
Как видим, происходит некоторая эмуляция текстового режима
с помощью графического. В 99% случаев при этом используется один
из следующих трех "текстовых" режимов: 32*24 (шрифт 8*8), 42*24
(шрифт 6*8) и 64*24 (шрифт 4*8). (Как вы можете заметить, в
режиме 42*24 четыре пиксела по горизонтали остаются
неиспользованными - обычно их либо равномерно распределяют
справа и слева, либо оставляют на какой-то одной стороне.)
В режиме 32*24 каждый символ может иметь свой цвет (что
непосредственно вытекает из структуры спектрумовского экрана). В
режимах 42*24 и 64*24 это невозможно, но там каждое слово может
быть окрашено в свой цвет (если считать, что слова разделены
пробелами). Другие режимы (скажем, 51*24 - при использовании
шрифта 5*8) лишены и этого.

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

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

байт 1 = 00000000 ░░░░░░░░░░░░░░░░
байт 2 = 00111000 ░░░░██████░░░░░░
байт 3 = 01101100 ░░████░░████░░░░
байт 4 = 01101100 ░░████░░████░░░░
байт 5 = 01111100 ░░██████████░░░░
байт 6 = 01101100 ░░████░░████░░░░
байт 7 = 01101100 ░░████░░████░░░░
байт 8 = 00000000 ░░░░░░░░░░░░░░░░

Рис. 1

Когда шрифт расположен в ОЗУ, такое его представление вполне
оправданно, как обеспечивающее достаточную скорость печати.
(Между прочим, когда печать символов 6*8 хотят сделать особенно
быстрой, используют целых четыре набора символов, каждый из
которых сдвинут относительно другого на два пиксела по
горизонтали - чтобы не тратить время на сдвиг при печати каждого
символа.) Но для экономии дискового пространства, занимаемого
вашей программой, имеет смысл удалять из шрифта всю избыточную
информацию.
Экономия при этом может быть весьма значительной: так, шрифт
из 256 символов 6*8, изначально занимавший #800 байт, уменьшится
на четверть. А если в таком шрифте изображение символов реально
занимает только 5*6 пикселов (т.е. между символами предусмотрен
обязательный пробел в один пиксел по горизонтали и два по
вертикали), то он сократится более чем вдвое!
Ниже приведена процедура, удаляющая из шрифта избыточную
информацию. Смысл используемых в ней констант font_sx, font_x,
font_sy и font_y пояснен на рис. 2.

┌────────────────────────────┐
│ Здесь должна быть картинка │
│ из файла 'ris_2.scr' │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────┘

Рис. 2

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 24 Mar 2001
Hello, All!

═══════════════════ text_out.3 ══════════════════

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

............... ; Печатаемый символ в регистре 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

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 24 Mar 2001
Hello, All!

═══════════════════ text_out.2 ══════════════════

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

font_sx EQU 1 ;эти параметры определяют используемую часть
font_sy EQU 1 ;матрицы 8*8 для преобразуемого шрифта
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

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 24 Mar 2001
Hello, All!

═══════════════════ text_out.4 ══════════════════

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

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 24 Mar 2001
Hello, All!

═══════════════════ text_out.5 ══════════════════

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

┌────────────────────────────┐
│ Здесь должна быть картинка │
│ из файла 'ris_3.scr' │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────┘

Рис. 3

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

ORG #6000

LD HL,#3D00 ; адрес шрифта ПЗУ
LD DE,#8000 ; здесь будет шрифт 6*8
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

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

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

┌────────────────────────────┐
│ Здесь должна быть картинка │
│ из файла 'ris_4.scr' │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────┘

Рис. 4

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

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 24 Mar 2001
Hello, All!

═══════════════════ text_out.6 ══════════════════

Теперь скажу несколько слов о специальном способе хранения
шрифта в памяти, при котором процедура печати оказывается более
быстрой и короткой.
Обычно в шрифте хранится сначала восемь байт, образующих
изображение первого символа, затем восемь байт второго символа
и так далее.
При печати символа необходимо сначала (зная его код и адрес
расположения в памяти шрифта) вычислить адрес, по которому
находится первый байт изображения символа, а затем по очереди
читать байт изображения и записывать его в видеопамять.
Пусть код символа задан в аккумуляторе, адрес в видеопамяти
(вычисляемый по координатам печати) задан в регистровой паре 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 - спасибо!

════════════════════════════════════════════════

С уважением, Иван Рощин.




Темы: Игры, Программное обеспечение, Пресса, Аппаратное обеспечение, Сеть, Демосцена, Люди, Программирование

Похожие статьи:
От авторов - Hесколько слов о пушистом чуде техники.
Ньюсы - Сценеры бабам дают! Да!
Ликбез - полный дизассемблеп ПЗУ TR-DOS'a.

В этот день...   14 октября