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

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

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

Оптимизация на примере intro «Start»

Радиомир. Ваш компьютер» 7—10/2001)
Дата последнего редактирования: 22.03.2003.

Введение
Расположение структур данных
Выбор наиболее выгодного представления данных
Использование участков программы как в качестве кодов команд, так и в качестве данных
Оптимизация помещения констант в регистры и ячейки памяти
Оптимизация циклов
Повышение скорости за счёт табличного задания функций
Повышение скорости за счёт раскрытия циклов
Самомодифицирующийся код
Размещение переменных непосредственно в командах загрузки
Построение таблиц
Использование недокументированных возможностей процессора
Ещё несколько хитростей
Исходный текст intro
Литература

Введение

Наверняка вы слышали о крупнейшем российском demoparty 2000 года — Chaos Constructions 000. Там были представлены и мои работы, одна из которых — 512-байтное intro «Start».

Что такое intro? Это небольшая программа, сочетающая графические эффекты и (обычно) музыкальное сопровождение. Создание intro без всякого преувеличения можно назвать настоящим искусством. Зачастую программу, время работы которой — несколько минут, пишут в течение многих месяцев. А потом, на demoparty, зрители выбирают лучшее intro, в котором реализованы самые красивые эффекты при заданном ограничении на размер.

Соревнования (intro compo) проходят в различных номинациях: 512 b, 1 Kb, 4 Kb и так далее. Эти цифры определяют максимальную длину кодового блока intro.

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

Но для 512-байтных intro этот способ не годится. Думаю, вы уже догадались, почему: программа-распаковщик, даже самый примитивный LZ, займёт относительно много места по сравнению с самой intro. Так что приходится оптимизировать код вручную, используя при этом многие известные способы, придумывая новые и выигрывая в результате байт за байтом. Для этого недостаточно просто хорошо знать ассемблер — необходимо ясно представлять себе, как будет выглядеть каждая команда в машинных кодах и за сколько тактов она выполнится; необходимо в совершенстве знать архитектуру ZX Spectrum, включая множество недокументированных особенностей… В общем, оптимизация intro — задача не для «чайников». :–)

В этой статье я как раз и хочу разобрать на примере своего intro «Start» использование различных приёмов оптимизации. Возможно, среди них найдутся такие, о существовании которых вы даже не подозревали! Не все приёмы специфичны для ZX Spectrum — кое-что может оказаться полезным, даже если у вас совершенно другой компьютер.

Сначала скажу несколько слов о том, что же мы увидим, запустив intro. Эта информация понадобится вам, чтобы понять — а что, собственно, делает программа.

Intro может работать на любом ZX Spectrum 48/128, но рекомендуется «Пентагон» и более быстрые модели — во-первых, чтобы предварительные вычисления выполнялись быстрее, и во-вторых — чтобы очисткa экрана в начале и в конце выполнялась «мгновенно» (см. раздел «Повышение скорости за счёт раскрытия циклов»).

После запуска около 12 секунд уходит на предварительные вычисления — в это время вам остаётся только ждать, глядя на чёрный экран. Затем появляется эффект «атрибутная плазма». (Вся красота этого эффекта в движении, а неподвижный рисунок, увы, не в состоянии её передать.)

Рис. 1

Имеется музыкальное сопровождение (кстати, из шести выставленных на Chaos Constructions 512-байтных intro музыка была только в «Start»!). Продолжительность intro — 48 секунд, в соответствии с длиной мелодии. Вместе с 12 секундами в начале получается ровно минута — именно таким было ограничение в правилах party.

Итак, с тем, как выглядит intro для неискушённого зрителя, мы разобрались. Если интересно, можете скачать его с моей web-страницы, адрес которой указан в начале статьи, и сами всё увидеть и услышать. А теперь, как я и обещал, разберём intro изнутри. :–)

Расположение структур данных

Вначале дадим несколько определений и пояснений.

Таблицы, массивы — обыкновенные одномерные массивы. Именно их я буду иметь в виду, говоря о структурах данных. Таблицами я для удобства называю такие массивы, которые не изменяются в процессе работы программы.

Если это не оговорено особо, длина каждого элемента предполагается равной одному байту.

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

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

Назовём структуру данных выровненной на границу сегмента, если она начинается с начала этого сегмента. Очевидно, младший байт адреса первого элемента такой структуры равен нулю.

Назовём структуру данных односегментной, если она полностью лежит в каком-либо одном сегменте. У всех элементов такой структуры старший байт адреса один и тот же.

Таблица TABLE_SIN (строка 14), таблица PALETTE (строка 18) и массив с содержимым регистров AY (строки 60—61) выровнены на границу сегмента. Чем обусловлено такое расположение этих структур данных?

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

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

В-третьих, если длина структуры — 256 байтов, то появляется возможность её автоматического зацикливания, то есть перехода от последнего элемента к первому и от первого к последнему без каких-либо специальных проверок. Когда мы увеличиваем младший байт адреса последнего элемента структуры, то автоматически получаем адрес первого её элемента (например, HL=#80FF, командой INC L увеличиваем младший байт адреса и получаем HL=#8000). Точно так же, когда мы уменьшаем младший байт адреса первого элемента структуры, то автоматически получаем адрес её последнего элемента (HL=#8000, DEC L —> HL=#80FF). Это весьма удобно, если структура данных содержит значения периодической функции (TABLE_SIN) или значения, которые можно трактовать подобным образом (плавные переходы цветов в таблице PALETTE).

В-четвёртых, такое расположение структуры данных обеспечивает возможность оптимизации обработки всех её элементов в цикле (подробнее см. «Оптимизация циклов»).

Как вы можете видеть, адрес размещения таблицы TABLE_SIN равен #AA00. Почему был выбран именно этот адрес, а не какой-либо другой, кратный 256? Дело в том, что к моменту, когда в регистровую пару DE необходимо поместить адрес TABLE_SIN, в регистре D уже содержится число #AA. Также известно, что значение аккумулятора к этому времени равно нулю. И однобайтной командой LD E,A (строка 147) мы помещаем в DE нужный нам адрес. А если бы адрес TABLE_SIN был другим, пришлось бы использовать трёхбайтную команду LD DE,TABLE_SIN.

Таблица PALETTE и таблица TABLE_SIN расположены в смежных сегментах. Это сделано не просто так. Во внутреннем цикле эффекта «плазма» (строки 238—264) идёт работа то с одной таблицей, то с другой. В регистре H при этом находится старший байт адреса используемой в данный момент таблицы. Так как эти старшие байты различаются на единицу, вместо перезагрузки регистра H с помощью команды LD H,n становится возможным использование команд INC H и DEC H (строки 252, 260), благодаря чему мы получаем выигрыш как в длине, так и в быстродействии.

Обратите внимание на структуру данных, содержащую информацию о музыкальных паттернах (строки 753—810). Это односегментная структура. Что это даёт? Удобство ссылки на её элементы. Каждый элемент однозначно задаётся младшим байтом своего адреса, так как старший байт адреса у всех элементов один и тот же. Благодаря этому в таблице POSITIONS (строки 402—430), где определяется порядок проигрывания паттернов, каждый элемент (являющийся ссылкой на информацию о соответствующем паттерне) занимает один байт. А eсли бы в ссылке указывался полный адрес, она занимала бы два байта.

Выбор наиболее выгодного представления данных

Музыка для intro была написана в редакторе Pro Tracker 3.4. Длина модуля, откомпилированного с плеером, оказалась равной 4558 байтам. Но ведь, согласно условиям, длина intro не должна превосходить 512 байтов! Как же было разрешено это противоречие?

Значительного сокращения размера (а вдобавок — и времени выполнения процедуры проигрывания!) удалось добиться за счёт представления данных, необходимых для проигрывания музыки, в наиболее выгодной форме. Выбор такой формы во многом был обусловлен индивидуальными особенностями мелодии. Рассмотрим это на примерах.

Начнём с того, что для проигрывания музыки нужна частотная таблица с коэффициентами деления для каждой ноты. В универсальном плеере эта таблица содержит коэффициенты деления (числа в диапазоне 0—4095) для всех 96 нот. Соответственно, занимает такая таблица 96*2=192 байта. Но, зная, какие ноты задействованы в мелодии, можно хранить в таблице только их коэффициенты. В мелодии, написанной для intro, задействованы всего десять различных нот — а значит, для хранения таблицы потребуется лишь двадцать байтов.

А теперь обратим внимание на то, в каком диапазоне находятся значения коэффициентов для этих десяти нот (строки 360—369). Это диапазон #6E—#114. Если бы коэффициенты лежали в диапазоне #00—#FF, то для хранения каждого из них достаточно было бы всего одного байта, а не двух. Так вот, оказывается, что коэффициенты можно преобразовать к указанному диапазону, если предварительно уменьшить их значения на некоторую величину (freq_shift — см. строка 371). В результате такой оптимизации частотная таблица (FREQ_TABLE) занимает вдвое меньше — десять байтов (см. строки 742—751). А когда по ходу программы требуется поместить в регистры AY значение коэффициента деления, к взятому из таблицы числу прибавляется freq_shift (строка 701).

Кстати, если бы коэффициенты деления были чётными, можно было бы поступить ещё проще: хранить в таблице их значения, делённые на два. Тогда в строке 701 стояла бы команда ADD A,A — на байт короче, чем ADD A,freq_shift, и быстрее выполняющаяся.

Как вы, должно быть, уже поняли из вышесказанного, каждая из десяти используемых в мелодии нот обозначается некоторым номером (числом 0..9 — см. строки 349—358), и когда требуется узнать соответствующий данной ноте коэффициент деления, этот номер служит индексом в частотной таблице. Очевидно, что в принципе совершенно безразлично, каким способом будет установлено соответствие между нотами и их номерами — лишь бы оно было взаимно однозначным. Необязательно самой низкой ноте должен быть присвоен номер 0, а самой высокой — 9. Таким образом, имеется некоторая свобода выбора. Этой свободой я и воспользовался в целях дальнейшей оптимизации.

Для понимания сути дела расскажу ещё немного об используемой в intro мелодии. Собственно мелодия звучит в каналах A и B, а в канале C звучат аккорды сопровождения (точнее, аккорд — это когда ноты звучат одновременно, а в данном случае ноты звучат по очереди, но быстро сменяют друг друга). Всего используется четыре различных аккорда: C5-E5-G5, D5-F5-A5, E5-G5-B5 и A4-C5-E5. У каждого из них свой номер. Естественно, возникает нужда в специальной таблице, из которой по номеру аккорда можно было бы извлечь информацию о его нотах.

Так вот, я выбрал номера нот таким образом, чтобы частотная таблица одновременно служила и таблицей аккордов! Посмотрите на строки 389—392 — там каждому аккорду ставится в соответствие некоторое число, являющееся смещением в таблице ORNAMENTS (которая, в свою очередь, является частью таблицы FREQ_TABLE). Три следующих друг за другом байта в этой таблице и есть коэффициенты деления для трёх нот аккорда. Величина выигрыша, полученного за счёт совмещения таблиц, составила в данном случае восемь байтов. Для другой программы, может быть, это и немного, но для 512-байтного intro это больше полутора процентов!

Перейдём к рассмотрению используемого в мелодии сэмпла. Вот как он выглядит в Pro Tracker’е:

Рис. 2

Для каждого элемента этого сэмпла нужно хранить значение амплитуды (число #0—#F) и смещение частоты (–2, 0 или +2). Использованный в intro формат хранения данных позволяет уместить и то, и другое в одном байте: в младших четырёх битах — значение амплитуды, а в старших двух — код смещения частоты (см. строки 816—822). Благодаря этому удаётся сократить как размер таблицы SAMPLE, так и длину тех фрагментов программы, где осуществляются манипуляции с этой таблицей.

Теперь обратите внимание на таблицу позиций (POSITIONS, строки 402—430), в которой указывается порядок проигрывания паттернов.

Обычно при написании плеера делают так: в каждом элементе этой таблицы хранят номер паттерна (один байт), а с помощью ещё одной таблицы по номеру паттерна определяют его адрес (длины паттернов неодинаковы). Зная количество позиций в мелодии (24) и количество паттернов (13), можно подсчитать расход памяти при использовании этого способа: 24+13*2=50 байтов.

Можно в каждом элементе таблицы позиций хранить непосредственно адрес соответствующего паттерна. В этом случае расход памяти составит 2*24=48 байтов.

Однако заметим, что вся информация о паттернах (строки 758—810) лежит в сегменте #8100—#81FF. А значит, старшие байты адресов паттернов будут одинаковыми и равными #81. Так зачем тогда хранить их в таблице? Оставим там лишь младшие байты, и вся таблица займёт при этом лишь 24 байта! Так и сделано в intro. А как при этом производится определение адреса паттерна, вы можете посмотреть в строках 487—488.

Да, о паттернах: рассмотрим, как повлияли особенности их структуры на формат, в котором они хранятся.

Сначала перечислим эти особенности. Во-первых, в каждом паттерне восемь нот. Во-вторых, в каждом паттерне используется только один аккорд. В-третьих, громкость в паттерне может либо убывать (от #F до #C), либо возрастать (от #C до #F), либо оставаться постоянной (#F). В-четвёртых, многие паттерны состоят из двух чередующихся нот (например, так: C5-G4-C5-G4-C5-G4-C5-G4).

При выборе формата хранения паттернов всё это было учтено, и в результате информация об одном паттерне занимает или два, или пять байтов (см. строки 753—810). Так мало? Да! И вот как это получилось.

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

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

Итак, с помощью удачно выбранного представления данных можно значительно сократить размер программы и/или повысить её быстродействие. Учитывайте только, что самое компактное представление данных — не всегда самое выгодное. Не забывайте следить за длиной фрагментов программы, обрабатывающих эти данные, и проверять, будет ли выигрыш в итоге.

Использование участков программы как в качестве кодов команд, так и в качестве данных

Обратите внимание на участок памяти по адресам #8001—#8009. Там находится фрагмент процедуры построения таблицы SAMPLE. Построение этой таблицы происходит один раз в самом начале работы программы, а в дальнейшем указанный участок используется для хранения содержимого регистров музыкального сопроцессора (AY).

При проигрывании мелодии в регистре R7 (байт по адресу #8007) должно находиться значение %??111000 (старшие два бита могут быть любыми), а в регистре R10 (байт по адресу #800A) — %???01001 (старшие три бита могут быть любыми). Так вот, в программе по адресу #8007 находится команда LD A,B (строка 92), а код этой команды — %01111000 — в дальнейшем как раз и служит в качестве значения, записываемого в 7-й регистр AY. Аналогично, находящаяся по адресу #800A команда XOR C (строка 101) имеет код %10101001 и в дальнейшем служит в качестве значения, записываемого в 10-й регистр AY. Таким образом, сначала байты в ячейках #8007 и #800A используются как коды команд, а затем — как данные.

Такой приём позволяет сократить длину программы, в данном случае — на десять байтов (отпадает необходимость в командах LD A,%00111000: LD (#8007),A: LD A,%00001001: LD (#800A),A). Разумеется, совпадение кодов команд с требуемыми значениями регистров AY не случайно. Пришлось тщательно продумывать логику работы процедуры построения таблицы SAMPLE и выбирать адрес компиляции intro так, чтобы нужные команды оказались на нужных местах. Помогло то обстоятельство, что абсолютно точного совпадения кодов команд и значений регистров AY не требовалось: допускалось различие в старших битах, не оказывающих влияния на формирование звука.

Теперь обратите внимание на выполняющуюся один раз в самом начале работы программы процедуру заполнения экрана текстурой (строки 121—140). В строке 134 находится команда JR Z,GRID_2 (иначе говоря, JR Z,$+3), которая в машинных кодах выглядит как #28, #01. Так вот, использующаяся затем в программе переменная QUARK располагается по тому же адресу, что и второй байт этой команды. Как видим, значение #01 вначале используется как часть команды перехода, а затем — в качестве начального значения переменной, то есть в качестве данных. При этом в intro не понадобилось специально выделять место под переменную QUARK — вот и сэкономили один байт.

(Примечание: можно было бы разместить переменную QUARK в любой свободной ячейке памяти, не входящей в intro, но тогда перед первым использованием её надо было бы проинициализировать значением #01, а на это ушло бы целых пять байтов (LD A,1: LD (QUARK),A).)

Естественно, процедура заполнения экрана текстурой специально была написана так, чтобы в ней была команда JR Z,$+3.

В разделе «Выбор наиболее выгодного представления данных» упоминалось о том, что в частотной таблице хранятся значения коэффициентов деления, уменьшенные на константу freq_shift. Значение этой константы должно быть таким, чтобы уменьшенные коэфициенты деления лежали в диапазоне 0..#FF. Исходя из этого, нетрудно прийти к выводу, что значение freq_shift должно лежать в диапазоне #15..#6E, т.е. имеется определённая свобода выбора.

Воспользовавшись этой свободой, я выбрал freq_shift так, чтобы первый элемент таблицы FREQ_TABLE оказался равным #C9 (см. строка 371). Для чего это нужно? Дело в том, что непосредственно перед таблицей FREQ_TABLE находится процедура IZM_FRQ (строки 682—715), заканчивающаяся командой RET. Код этой команды как раз и есть #C9. Значит, команду RET можно убрать из программы (вот почему в строке 715 она закомментирована), а в её качестве будет выступать первый элемент таблицы FREQ_TABLE.

Оптимизация помещения констант в регистры и ячейки памяти

Загрузка констант в регистры или запись их в память выполняется в реальной программе довольно часто, и оптимизация этих действий может принести весьма ощутимые результаты. О некоторых приёмах такой оптимизации, использованных при написании intro, я и расскажу.

Когда необходимо обнулить аккумулятор, команду LD A,0 обычно заменяют на XOR A (см. строки 234, 311, 525, 647), что позволяет сэкономить 1 байт/3 такта на каждой такой замене. Правда, эта команда, в отличие от LD, влияет на флаги, так что тут надо быть внимательным. Например, такой фрагмент (строки 522—526):

           BIT  5,(HL)
           LD   A,#23
           JR   Z,SET_COD
           XOR  A
SET_COD    LD   (ADR_CODE),A

ни в коем случае нельзя записывать так:

           BIT  5,(HL)
           XOR  A
           JR   NZ,SET_COD
           LD   A,#23
SET_COD    LD   (ADR_CODE),A

потому что после команды XOR A флаг Z всегда будет установлен (он устанавливается по результату операции, а результат-то нулевой!), так что дальнейшая проверка его значения (JR NZ) теряет всякий смысл, и программа не будет правильно работать.

В строке 116 необходимо поместить 0 по адресу, заданному в регистровой паре HL. Можно было бы сделать это командой LD (HL),0. Но известно, что к этому моменту регистр B обнулился после DJNZ. Таким образом, достаточно применить команду LD (HL),B. Экономия составляет 1 байт/3 такта.

Перед заполнением экрана текстурой (строки 121—140) необходимо поместить в HL адрес первого обрабатываемого байта видеопамяти — #57FF. Но к этому моменту в HL уже содержится это значение: оно оказалось там после процедуры CLS, выполняющей очистку экрана. Следовательно, команда LD HL,#57FF не нужна. Экономия составляет 3 байта/10 тактов.

В строке 147 необходимо загрузить в DE значение #AA00 (адрес таблицы TABLE_SIN). Но мы знаем, что D=#AA и A=0 (см. строки 142—143). Тогда вместо команды LD DE,#AA00 можно использовать команду LD E,A. Экономия составляет 2 байта/6 тактов. Адрес таблицы TABLE_SIN нарочно был выбран равным #AA00 (см. раздел «Расположение структур данных»), чтобы эта оптимизация стала возможной.

В строке 189 необходимо загрузить в DE адрес таблицы PALETTE. Известно, что перед этим в DE был установлен адрес таблицы TABLE_SIN, и эти 256-байтные таблицы располагаются друг за другом. Тогда достаточно использовать команду INC D вместо LD DE,PALETTE. Экономия составляет 2 байта/6 тактов.

Тот факт, что таблицы PALETTE и TABLE_SIN расположены в смежных сегментах, используется и в дальнейшем. Так, в строке 252 команду LD H,PALETTE/256 удаётся заменить на INC H, так как известно, что в регистре H находится старший байт адреса таблицы TABLE_SIN. Аналогично, в строке 260 вместо команды LD H,TABLE_SIN/256 используется команда DEC H. И в том, и в другом случае экономия составляет 1 байт/3 такта.

Аналогичным образом используется и расположение в смежных сегментах таблицы POSITIONS и данных о паттернах (строки 478—489).

А то, что переменная A_AMP и таблица FREQ_TABLE расположены в одном сегменте, позволяет вообще обойтись без загрузки старшего байта адреса (строки 566—582).

В строках 291—294 необходимо увеличить SP на 4 (иначе говоря, снять со стека два запомненных там значения, теперь уже ненужных) и поместить 0 по адресу TRIGGER. Можно сделать это так: POP AF: POP AF: XOR A: LD (TRIGGER),A. Но известно, что старший байт значения, находящегося на вершине стека, равен нулю (строка 469). Тогда команда XOR A не нужна: достаточно записать по адресу TRIGGER значение, которое окажется в аккумуляторе после первой команды POP AF. Экономия составляет 1 байт/4 такта.

В строках 475—476 для обнуления DE используются две команды LD D,A: LD E,A, при том, что A=0. Это позволяет сэкономить 1 байт/2 такта по сравнению с применением команды LD DE,0.

Если вы хотите побольше узнать об оптимизации загрузки констант в регистры, рекомендую изучить [1].

Оптимизация циклов

Часто можно извлечь выгоду из замены цикла с заранее известным количеством повторений («for») на цикл с выходом по условию («repeat-until»).

Рассмотрим, например, заполнение экрана текстурой (строки 129—140). Длина заполняемой области известна заранее — #1800 байтов. Если бы мы использовали цикл «for», то пришлось бы завести отдельный счётчик (и выделить для него регистровую пару), занести в него количество повторов (#1800), а затем в каждом проходе цикла уменьшать значение счётчика и проверять его на равенство нулю.

А как сделано в intro? Никакого специального счётчика там нет, а вместо этого после каждого прохода цикла производится проверка: не стал ли шестой бит регистра H равен нулю (строка 139). Объясню смысл этой проверки. Заполнение видеопамяти (#4000—#57FF) происходит от старших адресов к младшим, и в регистровой паре HL хранится адрес текущей заполняемой ячейки. При каждом проходе цикла этот адрес уменьшается на единицу. Когда вся видеопамять оказывается заполненной, адрес уменьшается до #3FFF. А теперь обратите внимание на старший байт адреса: когда он лежит в диапазоне #40—#57 (%01000000—%01010111), его шестой бит установлен; но как только он становится равным #3F (%00111111), шестой бит сбрасывается. Таким образом, срабатывает условие выхода из цикла. Просто и эффективно, не правда ли?

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

Другие примеры оптимизированных подобным образом циклов вы можете увидеть в строках 149—180 (вычисление таблицы sin), 191—197 (формирование палитры), 238—264 (внутренний цикл эффекта «плазма»), 315—326 (очистка экрана), 667—677 (запись в регистры AY).

Eщё один пример — главный цикл intro (строки 213—269). Прежде всего заметим, что организован он несколько необычно: проверка условия выхода выполняется в подпрограмме PLAY, отвечающей за музыкальное сопровождение (в строках 478—485). Так сделано из-за того, что продолжительность intro определяется длиной мелодии. После такого выхода из цикла, естественно, необходимо снять со стека лишние значения (запомненное в подпрограмме содержимое AF и адрес возврата), что и выполняется в строках 291—294.

Выход из главного цикла должен произойти при достижении конца таблицы POSITIONS. Длина этой таблицы (строки 402—430) не превосходит 256 байтов. В этом случае следить за достижением конца можно очень просто — по младшему байту адреса текущего элемента. Как только он стал равен младшему байту адреса первой ячейки памяти, лежащей после таблицы, — всё, таблица обработана (строки 483—485). Как вы можете увидеть, такое сравнение с переходом занимает всего пять байтов. Если бы таблица POSITIONS была длиннее 256 байтов, то в ней оказались бы по крайней мере две ячейки с одинаковыми младшими байтами адреса. Тогда пришлось бы сравнивать полный адрес элемента с адресом первой следующей ячейки памяти:

           LD   BC,PLAY
           AND  A
           SBC  HL,BC
           JR   Z,END

А это занимает уже восемь байтов — на три байта длиннее.

Повышение скорости за счёт табличного задания функций

Используемый в intro графический эффект «плазма» требует вычисления синуса при расчёте каждого элемента изображения. Естественно, чтобы успеть рассчитать всё изображение за 1/50 секунды, вычисление синуса должно быть как можно более быстрым. Наиболее подходит для этой цели табличный способ. Сущность его заключается в следующем: имеется таблица, содержащая значения функции при всех возможных значениях аргумента, а когда в программе надо вычислить значение функции, из таблицы просто берётся готовый результат.

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

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

Повышение скорости за счёт раскрытия циклов

Когда надо оптимизировать программу прежде всего по длине, этот способ применяют редко: ведь при раскрытии циклов длина программы возрастает. Тем не менее, в одном месте intro такой приём всё-таки используется.

Обратите внимание на то, как работает процедура очистки экрана (строки 311—328). Сначала происходит ожидание импульса вертикальной синхронизации (команда HALT), затем устанавливается чёрный цвет бордюра и обнуляется буфер атрибутов (длиной 768 байтов), причем обнуление идет от старших адресов к младшим (так удобнее проверять условие выхода из цикла). Так вот, обнулить атрибуты желательно до того, как луч начнет прорисовывать первую строку основного экрана, иначе получится не очень приятный эффект: в одном кадре экран будет не полностью очищенным.

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

      CLS_1  LD   (HL),A   ;7
             DEC  HL       ;6
             BIT  3,H      ;8
             JR   NZ,CLS_1 ;12

Получается (7+6+8+12)*768=25344 такта. За это время луч прорисует более 1/3 экрана, а это нам не подходит.

Если бы ограничение на общее время работы цикла было не таким жёстким (скажем, не более 24000 тактов), можно было бы сделать так: заменить команду JR на более быструю JP (она на байт длиннее), что обеспечило бы сокращение времени работы на 2*768=1536 тактов. Но в данном случае такое повышение быстродействия оказывается недостаточным.

А вот если раскрыть цикл, как сделано в intro:

      CLS_1  LD   (HL),A   ;7
             DEC  L        ;4
             LD   (HL),A   ;7
             DEC  HL       ;6
             BIT  3,H      ;8
             JR   NZ,CLS_1 ;12

то длина возрастёт всего на два байта, а время выполнения составит (7+4+7+6+8+12)*384=16896 тактов. За это время на экране успеет прорисоваться 75 строк (при 224 тактах на строку). Это уже вполне приемлемо: на «Пентагоне» (а именно на нём демонстрировались работы на CC 000) верхняя часть бордюра составляет 80 строк, так что к тому моменту, когда луч начнёт прорисовывать первую строку основного экрана, атрибуты уже будут обнулены.

Да, кстати: заметьте, что при раскрытии цикла он не просто повторяется дважды (LD HL,A: DEC HL: LD HL,A: DEC HL). Первая команда DEC HL заменяется на более быструю DEC L. Это возможно, так как при её выполнении старший байт HL заведомо не будет изменяться.

Самомодифицирующийся код

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

В разделе «Выбор наиболее выгодного представления данных» я уже упоминал, что в каждом паттерне громкость либо увеличивается, либо уменьшается, либо остаётся постоянной, и что информация об этом находится в двух старших битах первого байта описателя паттерна. Так вот, в процедуре проигрывания музыки при переходе к очередному паттерну проверяется значение этих двух битов (см. строки 491—508), и в соответствии с одним из трёх возможных случаев по адресу CODE_I_D_N заносится либо команда INC (HL), либо DEC (HL), либо NOP (строки 513—514). Как нетрудно догадаться, именно эта команда и будет изменять громкость во время проигрывания паттерна (см. строки 564—567).

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

Далее, в том же разделе я упоминал, что паттерны, в которых повторяются две ноты, хранятся в упакованном виде, и что информация об упаковке паттерна находится в пятом бите первого байта его описателя. При переходе к очередному паттерну значение этого бита проверяется, и в соответствии с ним по адресу ADR_CODE помещается либо код команды NOP (если паттерн упакован), либо код команды INC HL (в противном случае) (см. строки 518—526). Что же происходит в дальнейшем?

В каждом байте паттерна хранится информация о двух нотах. Если паттерн упакован, то в нём находится всего один байт с информацией о нотах; если нет — то четыре байта. Когда очередные две ноты проиграны, выполняется команда, находящаяся по адресу ADR_CODE (см. строки 571—572) и изменяющая позицию в паттерне. Если там NOP, то позиция не изменится, а значит, далее будут проиграны те же самые две ноты (что и требуется). Если же там команда INC HL, то произойдёт переход к следующему байту паттерна, с информацией о следующих двух нотах. Проще и не сделать, не правда ли?

И последний пример. В строке 629 находится команда JR C_NORM — переход на обработку канала C. Когда мелодия заканчивается, в строке 292 во второй байт этой команды (где хранится смещение перехода) записывается 0. Нулевое смещение в команде JR означает просто переход к следующей команде. Таким образом, вместо того, чтобы выводить в канале C три чередующихся звука разной частоты (как это было на протяжении всей мелодии), программа будет выводить такой же сэмпл, как и в каналах A и B, и в результате мы услышим финальный аккорд.

Размещение переменных непосредственно в командах загрузки

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

    Было:   VAR   DB  15
                  ......
                  LD  A,(VAR)
                  ......
           ------------------
           4 байта, 13 тактов

    Стало:        ......
                  LD  A,15
            VAR   EQU $-1
                  ......
            -----------------
            2 байта, 7 тактов

Как видим, при этом удаётся сократить как размер программы, так и время её выполнения. В нижеприведённой таблице показано, каким получается выигрыш в зависимости от регистра или регистровой пары, куда загружаются данные.

Обратите внимание, что Z80 не имеет команд для загрузки байта из памяти в регистры B, C, D, E, H, L и в половинки индексных регистров. Из-за этого в программе приходится, например, сначала загружать байт из памяти в аккумулятор, а потом пересылать значение в нужный регистр (при этом ещё иногда приходится запоминать значение аккумулятора в стеке, а затем восстанавливать). Непосредственная загрузка в этих случаях даёт особенное преимущество.

Табл. 1. Сравнение команд загрузки.
L — длина (в байтах), T — время выполнения (в тактах). В столбце «Загрузка из памяти» длина указывается в виде «X(+Y)», где X — длина команды, а Y — длина загружаемых данных. Экономия указывается с учётом длины данных.
Непосредственная загрузка Загрузка из памяти Экономия
Команда L T Команда L T L T
LD A,n27LD A,(addr)3(+1)1326
LD B,n
LD C,n
LD D,n
LD E,n
LD H,n
LD L,n
27 нет>2>6
LD XH,n
LD XL,n
LD YH,n
LD YL,n
311 нет>2>6
LD HL,nn 310 LD HL,(addr) 3(+2)16 26
LD DE,nn
LD BC,nn
310 LD DE,(addr)
LD BC,(addr)
4(+2)20 310
LD IX,nn
LD IY,nn
414 LD IX,(addr)
LD IY,(addr)
4(+2)20 26
LD SP,nn 310 LD SP,(addr) 4(+2)20 310

Примеры применения этого метода в intro вы можете увидеть в строках 465—466, 478—479, 551—552, 598—600, 602—603, 613—614, 621—623, 641—642 и 650—651.

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

Вот пример из intro:

(1)   448            LD   HL,(FREQ_A)
      449            LD   (FREQ_B),HL
      ...............................
(2)   598            LD   DE,9*256+(freq_g4–freq_shift)
      599 FREQ_A     EQU  $–2   ;E
      600 A_AMP      EQU  $–1   ;D

Почему непосредственная загрузка использована именно в (2), а не в (1)? Потому, что в (2) загрузка выполняется в регистровую пару DE, а в (1) — в HL. А, судя по приведённым в таблице данным, выигрыш будет наибольшим именно при замене загрузки в DE (3 байта/10 тактов против 2 байтов/6 тактов).

Построение таблиц

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

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

Вот как этот приём используется в intro.

Таблица синуса (точнее, таблица значений функции y=[(1+sin(2*Pi*x/255))*127]) длиной 256 байтов создаётся с помощью 32-байтного фрагмента программы (строки 145—180). Получаем выигрыш в 224 байта — правда, на построение тратится около 12 секунд. Существуют, впрочем, и более быстрые (но и более длинные) способы построения таблицы синуса — если интересно, рекомендую ознакомиться с [2] (раздел «Тригонометрия»).

Таблица PALETTE, в которой находятся значения атрибутов, также занимает 256 байтов. А вот фрагмент программы, её строящий (строки 182—197) занимает всего 14 байтов, плюс 12 байтов на данные (строки 274—285) — итого 26 байтов. Как нетрудно подсчитать, выигрыш составляет 230 байтов.

Таблица со сведениями об используемом при проигрывании музыки сэмпле… Тут ситуация более любопытная. Если вы посмотрите на структуру сэмпла (рис. 2), то увидите, что его можно разделить на две части — начало и зацикленный участок. Так вот, начало (первые четыре элемента) так и хранится в теле программы (строки 826—829). А продолжение сэмпла достраивается в строках 75—116 (на это тратится 25 байтов). В итоге общие затраты памяти составляют 29 байтов, а получаемая в итоге таблица SAMPLE занимает 77 байтов. Как видим, 48 байтов при этом удаётся сэкономить.

Использование недокументированных возможностей процессора

В строках 221,223,230,245 вы можете увидеть недокументированные команды для работы с младшим и старшим байтами 16-битного регистра IX как с отдельными 8-битными регистрами XL и XH. Их удобно использовать, когда для хранения переменных не хватает обычных регистров. Точно так же можно работать и с половинками регистра IY, правда, в intro этому не удалось найти применения. Хотя команды, работающие с половинками индексных регистров, на байт длиннее и на 4 такта медленнее по сравнению с командами, работающими с обычными 8-битными регистрами (за счёт байта-префикса), их использование всё же более выгодно, чем хранение переменных в памяти. И, между прочим, в более поздних модификациях процессора Z80 (Z380 и т.д.) эти команды уже не являются недокументированными.

В строках 666—677 (запись значений в регистры AY) для проверки условия выхода из цикла используется недокументированная особенность команды OUTD: после её выполнения флаг переноса устанавливается, если значение регистра H изменилось, и при этом выводимое в порт значение не равно нулю; в противном случае флаг сбрасывается. Собственно, в процессе написания intro я и обнаружил эту особенность — интересующиеся могут прочесть об этом в [3].

Цикл построен так, что запись в регистры AY происходит в обратном порядке — от R10 к R0. После записи в R0 значение HL уменьшается с #8000 до #7FFF. Чтобы при этом флаг переноса оказался установлен (а это и служит условием выхода из цикла), необходимо, чтобы выводимое в R0 значение не было равно нулю. Это значение представляет собой младший байт делителя частоты в канале A. На протяжении всей мелодии этот байт никогда не становится равным нулю — это получилось, в общем-то, случайно. Если бы вдруг оказалось, что в какие-то моменты этот байт обнуляется, то пришлось бы изменить мелодию — скажем, выбрав другую тональность.

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

Ещё несколько хитростей

Изображение в эффекте «плазма» движется по траектории, описываемой уравнениями вида x=sin(t), y=sin(t+fi). Такая траектория называется фигурой Лиссажу, и её вид в данном случае зависит от значения fi — разности фаз. Если fi=0 или Pi, фигура вырождается в отрезок прямой; если fi=Pi/2 — превращается в окружность, а при других значениях представляет собой эллипс (что как раз и нужно). В программе значениям fi от 0 до 2*Pi соответствуют числа от 0 до #100. Таким образом, понятно, что разность фаз может быть любой, только не кратной #40 (т.е. Pi/2). Воспользовавшись этой свободой, я выбрал её равной #AA — это позволило в строке 227 вместо команды ADD A,n (2 байта/7 тактов) использовать команду ADD A,H (1 байт/4 такта). Дело в том, что в регистре H к этому моменту уже содержится число #AA — старший байт адреса таблицы TABLE_SIN. Вот так и удалось сократить программу ещё на байт.

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

При проигрывании музыки используется тот факт, что количество нот в паттерне (8) является степенью двойки. Это позволяет оптимизировать фрагмент программы, где производится увеличение номера ноты и проверка на достижение конца паттерна (строки 465—471). После того, как номер ноты увеличен, вместо команды сравнения (CP 8) используется команда AND 7. И та, и другая команда установит флаг Z, если после увеличения номер стал равным 8, так что пока выигрыша вроде бы не видно. Но после команды AND при этом ещё и произойдёт обнуление аккумулятора, а это нам как раз и надо, ведь после 7-й ноты одного паттерна идёт 0-я нота следующего. Вот и выигрыш: не потребовалось специально обнулять номер ноты.

Посмотрите, как происходит формирование 256-байтной таблицы PALETTE. Она должна состоять из 12 равных частей, заполненных значениями, взятыми из 12-байтной таблицы COLORS. Но 256 на 12 нацело не делится, и части получаются такими: 11 по 22 байта и одна длиной 14 байтов.

Процедура заполнения (строки 191—197) устроена очень просто. В DE помещён адрес начала таблицы PALETTE. Из (HL) берётся очередное значение и 22 раза записывается в (DE) (после записи каждого байта E увеличивается). Если после записи очередных 22 байтов достигается конец таблицы PALETTE (т.е E=0), заполнение заканчивается. Если нет, то HL увеличивается и в таблицу заносятся очередные 22 байта.

Если вы протрассируете эту процедуру, то обнаружите нечто далеко не очевидное с первого взгляда. После того, как таблица PALETTE заполнится в первый раз, работа процедуры не закончится. Таблица будет заполняться «по кругу» ещё и ещё — до тех пор, пока последний байт очередного 22-байтного участка не совпадёт с последним байтом таблицы. К этому моменту, как нетрудно подсчитать, в таблицу будет записано 2816 байтов (это наименьшее общее кратное чисел 256 и 22), а HL увеличится на 2816/22=128. Поэтому адрес, устанавливаемый в HL перед началом заполнения (см. строка 184) меньше, чем адрес начала таблицы COLORS, на 128–12=#74 байта, так что после того, как заполнение завершится, таблица PALETTE будет как раз заполнена данными из таблицы COLORS.

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

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

* * *

Ну что, не ожидали, что нужно так много знать, чтобы написать такую маленькую программу? :–) Попробуйте свои силы в оптимизации — сократите intro хотя бы на байт без потери функциональности! Сразу скажу, что это возможно, и один из способов мне известен…

Исходный текст intro

Несколько слов о приведённом ниже исходном тексте intro «Start».

Текст снабжён подробными комментариями. Строки программы пронумерованы лишь для того, чтобы на них было удобно ссылаться. Если вам нужно набрать программу, чтобы откомпилировать её и запустить, то номера строк указывать не надо.

Программа предназначена для компиляции в ассемблере ZX ASM. Если вы используете какой-либо другой ассемблер, обратите внимание на характерные для ZX ASM особенности синтаксиса.

В строках 221, 223, 230, 245 находятся команды, работающие с половинками регистровой пары IX. В ZX ASM для них приняты обозначения XH и XL. В других ассемблерах эти половинки могут обозначаться как HX и LX, или же вообще не поддерживаться (всё-таки это недокументированная возможность Z80) — в таком случае придётся исправить в указанных командах XH на H, XL — на L, и перед каждой такой командой поставить байт-префикс #DD. Например, команда INC XL в строке 221 будет выглядеть как две команды: DB #DD и INC L.

В строках 402—430 и 484 вы можете увидеть оператор «\» — вычисление остатка от деления. Он применяется там, чтобы получить младший байт двухбайтной величины. Если в используемом вами ассемблере этот оператор не поддерживается, то, возможно, предусмотрен специальный оператор вычисления младшего байта (например, в ALASM’е это «.»), или же ассемблер сам отбрасывает старший байт, когда результат должен быть однобайтным (кстати, и ZX ASM обладает этой способностью). В крайнем случае можно воспользоваться равенством A\256=A–((A/256)*256).

001 +--------------------------------------+
002 |                                      |
003 |               "START"                |
004 |           512 bytes intro            |
005 |                 for                  |
006 |       Chaos Constructions 000        |
007 |                                      |
008 |   (c) 2000 by Ivan Roshin, Moscow    |
009 |                                      |
010 +--------------------------------------+
011
012 ;Структуры данных:
013
014 TABLE_SIN  EQU  #AA00 ;Этот адрес
015                       ;выбран
016                       ;не случайно!
017
018 PALETTE    EQU  TABLE_SIN+#100 ;И этот
019                       ;тоже!
020
021 ;Аппаратные константы видеоконтроллера:
022
023 black      EQU  0   ;Номера цветов.
024 blue       EQU  1
025 red        EQU  2
026 magenta    EQU  3
027 green      EQU  4
028 cyan       EQU  5
029 yellow     EQU  6
030 white      EQU  7
031
032 paper      EQU  8   ;Коэфф. умножения.
033
034 ;Пример задания атрибута:
035 ;white*paper+black
036
037 border     EQU  254 ;Порт управления
038                     ;цветом бордюра.
039
040 ;Адреса процедур для работы
041 ;с калькулятором:
042
043 A_TO_STACK EQU  #2D28
044 FROM_STACK EQU  #2DD5
045
046 ;Используемые команды калькулятора:
047
048 multiply   EQU  #04
049 add_       EQU  #0F
050 sin        EQU  #1F
051 stk_data   EQU  #34
052 end_calc   EQU  #38
053 stk_one    EQU  #A1
054
055
056 ;Пошла сама программа:
057
058            ORG  #8001
059
060 ;С #8000 в дальнейшем первые 11 байтов
061 ;будут использоваться как дамп AY.
062 ;
063 ;Сейчас установлены следующие значения:
064 ;#8007=%00111000 (микшер AY); вообще-то,
065 ;старшие 2 бита не важны и могут быть
066 ;любыми, в зависимости от удобства.
067 ;#800A - правый канал - 9 (громкость
068 ;орнамента). Старшие 3 бита могут быть
069 ;любыми, в зависимости от удобства.
070 ;
071 ;Сейчас эти данные "встроены" в
072 ;программу и интерпретируются как
073 ;команды.
074
075 ;Строим таблицу sample с #8201 (точнее,
076 ;достраиваем ее - первые 4 значения
077 ;уже находятся с #81FD):
078
079            LD   HL,#8201
080            LD   BC,#0C40
081
082 ;B - текущая громкость (уменьшается
083 ;    каждые 6 int'ов);
084 ;C - константа #40, используемая для
085 ;    установки смещения частоты в
086 ;    соответствии с up_2, up_0, dn_2.
087
088 ;По адресу #8007 у нас %01111000 - это
089 ;значение микшера и одновременно код
090 ;нужной здесь команды LD A,B:
091
092 MAKE_SAMPL LD   A,B
093            LD   (HL),B
094            INC  L
095
096 ;По адресу #800A - #A9 (громкость
097 ;орнамента в правом канале = 9) и
098 ;одновременно код нужной здесь команды
099 ;XOR C:
100
101            XOR  C
102            LD   (HL),A
103            INC  L
104            LD   (HL),A
105            INC  L
106            ADD  A,C
107            LD   (HL),A
108            INC  L
109            SUB  C
110            LD   (HL),A
111            INC  L
112            LD   (HL),A
113            INC  L
114            DJNZ MAKE_SAMPL
115
116            LD   (HL),B ;=LD (HL),0
117
118            HALT
119            CALL CLS
120
121 ;Рисуем сетку (HL=#57FF):
122
123 ;Эта процедура специально так написана,
124 ;чтобы был фрагмент JR $+3, т.к смещение
125 ;в JR, равное 1, будет использоваться
126 ;затем в качестве данных (переменная
127 ;QUARK).
128
129 GRID_1     LD   D,%10101010
130            BIT  0,H
131            JR   Z,GRID_2
132            LD   DE,%0001000101000100
133            BIT  1,H
134            JR   Z,GRID_2
135 QUARK      EQU  $-1 ;этот байт равен 1
136            LD   D,E
137 GRID_2     LD   (HL),D
138            DEC  HL
139            BIT  6,H
140            JR   NZ,GRID_1
141
142 ;После рисования сетки HL=#3FFF, D=#AA,
143 ;A=0 (это еще после CLS)!
144
145 ;Формируем таблицу sin:
146
147            LD   E,A ;DE=#AA00=TABLE_SIN!
148
149 LOOP_SIN   PUSH DE
150
151 ;Если использовать регистровую пару BC
152 ;вместо DE, можно сэкономить байт
153 ;(вместо LD A,C: CALL A_TO_STACK ставим
154 ;CALL A_TO_STACK+1). К сожалению, в этой
155 ;программе использовать BC вместо DE
156 ;нельзя (иначе потом больше потеряем)...
157
158            LD   A,E
159            CALL A_TO_STACK
160
161 ;Вычисление
162 ;int ((1+sin((2*Pi/255)*COUNTER))*127):
163
164            RST  #28
165            DB   stk_data ;2*Pi/255
166            DB   #EB,#49,#D9,#B4,#56
167            DB   multiply
168            DB   sin
169            DB   stk_one
170            DB   add_
171            DB   stk_data ;127
172            DB   #40,#B0,#00,#7F
173            DB   multiply
174            DB   end_calc
175
176            CALL FROM_STACK
177            POP  DE
178            LD   (DE),A
179            INC  E
180            JR   NZ,LOOP_SIN
181
182 ;Формируем палитру (DE=TABLE_SIN!):
183
184            LD   HL,COLORS-#74
185
186 ;Смещение -#74 подобрано специально.
187 ;А все потому, что 256 не делится на 12.
188
189            INC  D ;DE:=PALETTE!
190
191 MAKE_PAL_1 LD   A,(HL)
192            INC  HL
193            LD   B,0+(256+11)/12
194 MAKE_PAL_2 LD   (DE),A
195            INC  E
196            DJNZ MAKE_PAL_2
197            JR   NZ,MAKE_PAL_1
198
199 ;PLASMA - распределение регистров:
200 ;
201 ;DE - адрес в файле атрибутов,
202 ; C - координата y на экране
203 ; (координату x получаем как E AND 31),
204 ; H - старший байт адреса таблицы sin
205 ;     или палитры (различаются на 1),
206 ; L - используется для доступа к таблице,
207 ; B - смещение по x,
208 ;XH - смещение по y,
209 ;XL - смещение в таблице SIN (по этому
210 ;     смещению берутся смещения по x,y),
211 ;     начальное значение XL не важно.
212
213 PLASMA     HALT
214            CALL PLAY
215
216 ;Смещения по x,y берем из таблицы sin,
217 ;тогда плазма будет двигаться по фигуре
218 ;Лиссажу (в данном случае - эллипс):
219
220            LD   H,TABLE_SIN/256
221            INC  XL
222
223            LD   A,XL
224            LD   L,A
225            LD   B,(HL)
226
227            ADD  A,H ;сдвиг фаз = #AA
228            LD   L,A
229            LD   A,(HL)
230            LD   XH,A
231
232            LD   DE,#5800
233            LD   C,24
234            XOR  A
235
236 ;sin (2*x+sm_x) + sin (2*y+sm_y) + sm_x:
237
238 LOOP       ADD  A,A      ;2*x
239            ADD  A,B      ;2*x+sm_x
240            LD   L,A
241            LD   A,(HL)   ;sin(2*x+sm_x)
242            EX   AF,AF'
243            LD   A,C      ;y
244            ADD  A,A      ;2*y
245            ADD  A,XH     ;2*y+sm_y
246            LD   L,A
247            EX   AF,AF'
248            ADD  A,(HL)   ;Сумма синусов.
249            ADD  A,B      ;+sm_x
250            LD   L,A
251
252            INC  H ;= LD H,PALETTE/256
253
254            LD   A,(HL)
255            LD   (DE),A
256            INC  E
257            LD   (DE),A
258            INC  DE
259
260            DEC  H ;Восстановили H.
261
262            LD   A,E
263            AND  31
264            JR   NZ,LOOP ;Цикл по x.
265
266            DEC  C
267            JR   NZ,LOOP ;Цикл по y.
268
269            JR   PLASMA  ;Все сначала.
270
271 ;---------------------------------------
272 ;Набор цветов для плазмы:
273
274 COLORS     DB   yellow*paper+yellow
275            DB   yellow*paper+red
276            DB   red*paper+yellow
277            DB   red*paper+red
278            DB   red*paper+magenta
279            DB   magenta*paper+red
280            DB   magenta*paper+magenta
281            DB   magenta*paper+green
282            DB   green*paper+magenta
283            DB   green*paper+green
284            DB   green*paper+yellow
285            DB   yellow*paper+green
286
287 ;---------------------------------------
288 ;Сюда передается управление, когда
289 ;мелодия заканчивается:
290
291 END        POP  AF ;После этого A=0!
292            LD   (TRIGGER),A ;Смещение
293                             ;для JR=0.
294            POP  AF ;Адрес возврата.
295
296            CALL CLS
297
298 ;Финальный аккорд:
299
300            LD   B,#4D
301 ACCORD     PUSH BC
302            LD   DE,freq_e5-freq_shift
303            CALL NE_NOTE_F
304            HALT
305            POP  BC
306            DJNZ ACCORD
307
308 ;---------------------------------------
309 ;Процедура очистки экрана:
310
311 CLS        XOR  A
312            OUT  (border),A
313
314            LD   HL,#5AFF
315 CLS_1      LD   (HL),A
316            DEC  L
317
318 ;Раскрываем цикл - код длиннее на 2
319 ;байта, зато при вызове CLS после HALT
320 ;атрибуты очистятся прежде, чем попадут
321 ;под луч.
322
323            LD   (HL),A
324            DEC  HL
325            BIT  3,H
326            JR   NZ,CLS_1
327
328            RET
329
330 END_MAIN
331
332 ;---------------------------------------
333
334            ORG  #80C6
335
336 ;   Плеер
337
338 ;   Константы
339
340 tempo      EQU  12    ;Интервал между
341                       ;нотами в 1/50 с.
342
343 length     EQU  24    ;Длина мелодии
344                       ;в паттернах.
345
346 patt_size  EQU  8     ;Количество нот
347                       ;в паттерне.
348
349 note_g4    EQU  0     ;Номера нот
350 note_b4    EQU  1     ;(не случайно ноты
351 note_a4    EQU  2     ;идут в таком
352 note_c5    EQU  3     ;порядке - это
353 note_e5    EQU  4     ;позволяет совмес-
354 note_g5    EQU  5     ;тить таблицы
355 note_b5    EQU  6     ;FREQ_TABLE и
356 note_d5    EQU  7     ;ORNAMENTS).
357 note_f5    EQU  8
358 note_a5    EQU  9
359
360 freq_g4    EQU  #114  ;Значения делителя
361 freq_a4    EQU  #F8   ;частоты для
362 freq_b4    EQU  #DD   ;каждой из
363 freq_c5    EQU  #CF   ;используемых нот
364 freq_d5    EQU  #B8   ;
365 freq_e5    EQU  #A5   ;(используется
366 freq_f5    EQU  #9B   ;чистая гамма, а
367 freq_g5    EQU  #8A   ;не темперирован-
368 freq_a5    EQU  #7C   ;ная).
369 freq_b5    EQU  #6E
370
371 freq_shift EQU  freq_g4-#C9
372                       ;Такое значение,
373                       ;чтобы для каж-
374                       ;дого freq число
375                       ;freq-freq_shift
376                       ;было 0..#FF.
377
378 code_up    EQU  #00 ;Этими значениями
379 code_dn    EQU  #40 ;кодируется, будет
380 code_max   EQU  #80 ;ли громкость в
381                     ;паттерне повышать-
382                     ;ся, понижаться или
383                     ;быть максимальной.
384
385 code_pack  EQU  #20 ;Упакованный паттерн
386                     ;(2 ноты повторяются
387                     ;4 раза).
388
389 ornament_0 EQU  1   ;Смещения использу-
390 ornament_1 EQU  5   ;емых орнаментов в
391 ornament_2 EQU  2   ;таблице ORNAMENTS.
392 ornament_3 EQU  0
393
394
395 ;   Переменные
396
397 ;Таблица positions
398 ;
399 ;Хранится младший байт начала каждого
400 ;паттерна.
401
402 POSITIONS  DB   PATTERN_0\256
403            DB   PATTERN_1\256
404            DB   PATTERN_2\256
405            DB   PATTERN_3\256
406
407            DB   PATTERN_0\256
408            DB   PATTERN_1\256
409            DB   PATTERN_2\256
410            DB   PATTERN_4\256
411
412            DB   PATTERN_5\256
413            DB   PATTERN_6\256
414            DB   PATTERN_2\256
415            DB   PATTERN_3\256
416
417            DB   PATTERN_5\256
418            DB   PATTERN_7\256
419            DB   PATTERN_8\256
420            DB   PATTERN_9\256
421
422            DB   PATTERN_10\256
423            DB   PATTERN_11\256
424            DB   PATTERN_12\256
425            DB   PATTERN_12\256
426
427            DB   PATTERN_10\256
428            DB   PATTERN_11\256
429            DB   PATTERN_12\256
430            DB   PATTERN_12\256
431
432 ;   Процедуры
433
434 PLAY
435
436 ;Проверяем: надо перейти к следующей
437 ;ноте?
438
439            LD   HL,QUARK
440            DEC  (HL)
441            JR   NZ,NE_NOTE ;нет
442
443            LD   (HL),tempo
444
445 ;При переходе - сразу копируем значения
446 ;частоты и громкости из канала A в B:
447
448            LD   HL,(FREQ_A)
449            LD   (FREQ_B),HL
450
451 ;И устанавливаем позицию в SAMPLE для
452 ;канала A:
453
454            LD   HL,SAMPLE
455            LD   (POS_IN_S_A),HL
456
457 ;А вот эта установка пригодится лишь
458 ;в конце...
459
460            LD   (POS_IN_S_B),HL
461
462 ;Проверка: надо перейти к следующей
463 ;position?
464
465            LD   A,patt_size-1
466 NOTE       EQU  $-1
467            INC  A
468            AND  7
469            PUSH AF ;Запомнили!
470            LD   (NOTE),A
471            JR   NZ,NE_POSIT
472
473 ;Переход к следующей position:
474
475            LD   D,A ;=LD DE,0
476            LD   E,A ;(потом пригодится)
477
478            LD   HL,POSITIONS-1
479 POSITION   EQU  $-2
480            INC  HL
481            LD   (POSITION),HL
482
483            LD   A,L
484            CP   PLAY\256
485            JR   Z,END
486
487            LD   L,(HL)
488            INC  H ;=LD H,PATTERN_0/256
489            LD   A,(HL)
490
491 ;В двух старших битах аккумулятора
492 ;находится значение, характеризующее
493 ;изменение громкости в паттерне:
494 ;%00 для up, %10 для dn, %01 для max.
495 ;В зависимости от этого значения
496 ;устанавливаем по адресу CODE_I_D_N
497 ;одну из команд INC (HL), DEC (HL), NOP,
498 ;а также устанавливаем текущую громкость
499 ;ALL_AMP
500
501            ADD  A,A       ;code_max=#80
502            JR   C,SET_PAR ;с DE=#0000
503
504            LD   DE,#34FF
505            ADD  A,A       ;code_dn=#40
506            JR   C,SET_PAR
507
508            LD   DE,#3505  ;code_up=#00
509
510 ;Сейчас в D - то, что надо записать
511 ;в CODE_I_D_N, в E - ALL_AMP.
512
513 SET_PAR    LD   A,D
514            LD   (CODE_I_D_N),A
515            LD   A,E
516            LD   (A_AMP),A
517
518 ;Если паттерн упакован (т.е. в паттерне
519 ;повторяются 2 ноты), помещаем код
520 ;NOP (#00), иначе код INC HL (#23):
521
522            BIT  5,(HL)
523            LD   A,#23
524            JR   Z,SET_COD
525            XOR  A
526 SET_COD    LD   (ADR_CODE),A
527
528 ;Младшие 5 битов - номер орнамента.
529
530            LD   A,(HL)
531            AND  %00011111
532            LD   (ORNAMENT),A
533
534 ;Устанавливаем адрес начала паттерна:
535
536            INC  HL
537            LD   (POS_IN_PAT),HL
538
539 ;Обработка очередной NOTE.
540 ;Помещаем новый указатель позиции
541 ;в sample для канала B (для A был
542 ;помещен раньше):
543
544 NE_POSIT   LD   HL,SAMPLE+12
545            LD   (POS_IN_S_B),HL
546
547 ;Здесь по очереди будем брать старшие
548 ;4 бита или младшие: если мл. бит NOTE
549 ;=0, то старшие, если 1, то младшие.
550
551            LD   HL,0
552 POS_IN_PAT EQU  $-2
553
554            POP  AF ;NOTE
555            RRCA
556            LD   A,(HL)
557            JR   C,NOTE_LOW
558
559            RRCA
560            RRCA
561            RRCA
562            RRCA
563
564 ;Изменяем громкость:
565
566            LD   HL,A_AMP
567 CODE_I_D_N NOP  ;Или INC (HL), DEC (HL).
568
569            JR   NOTE_TO_AY
570
571 NOTE_LOW   INC  HL ;Или NOP для PACKED.
572 ADR_CODE   EQU  $-1
573            LD   (POS_IN_PAT),HL
574
575 NOTE_TO_AY AND  %00001111
576            ADD  A,FREQ_TABLE\256
577            LD   L,A
578
579 ;В H уже установлено FREQ_TABLE/256.
580
581            LD   A,(HL)
582            LD   (FREQ_A),A
583
584 NE_NOTE
585
586 ;Обработка канала A (центрального):
587
588 ;В самом начале в канале A стоит нота g4
589 ;с громкостью -9 (см. ниже). При первом
590 ;вызове PLAY эти значения будут
591 ;скопированы для канала B, и там будет
592 ;тихо (амплитуда 2->1->0) звучать эта
593 ;нота (именно эта - т.к. и дальше она
594 ;там используется). Вообще-то на канале
595 ;B в самом начале должна быть тишина -
596 ;но так сделать проще:
597
598            LD   DE,9*256+(freq_g4-freq_shift)
599 FREQ_A     EQU  $-2   ;E
600 A_AMP      EQU  $-1   ;D
601
602 NE_NOTE_F  LD   HL,0
603 POS_IN_S_A EQU  $-2
604            LD   A,(HL)
605            INC  HL
606            LD   (POS_IN_S_A),HL
607
608            LD   L,8
609            CALL IZM_FRQ
610
611 ;Обработка канала B (левого):
612
613            LD   HL,0
614 POS_IN_S_B EQU  $-2
615            LD   A,(HL)
616            PUSH AF ;Для фин. аккорда.
617            INC  HL
618            LD   (POS_IN_S_B),HL
619
620            LD   L,9
621            LD   DE,0
622 FREQ_B     EQU  $-2   ;E
623 B_AMP      EQU  $-1   ;D
624            CALL IZM_FRQ
625            POP  AF
626
627 ;Обработка канала C (правого):
628
629            JR   C_NORM
630 TRIGGER    EQU  $-1
631
632 ;В конце мелодии (финальный аккорд)
633 ;в TRIGGER записывается 0, что означает
634 ;переход к следующей команде.
635
636            LD   L,#A
637            LD   E,freq_g5-freq_shift
638            CALL IZM_FRQ
639            JR   OUT_AY
640
641 C_NORM     LD   A,2            ;Прошлое
642 SM_IN_ORN  EQU  $-1            ;смещение
643                                ;в орн.
644            INC  A
645            CP   3
646            JR   NZ,SAVE_SM
647            XOR  A
648 SAVE_SM    LD   (SM_IN_ORN),A
649
650            ADD  A,ornament_0 ;N тек.орн.
651 ORNAMENT   EQU  $-1
652            ADD  A,ORNAMENTS\256
653
654            LD   H,ORNAMENTS/256
655            LD   L,A
656
657 ;HL указывает на значение делителя
658 ;частоты.
659
660            LD   A,(HL)
661            LD   HL,#8004
662            CALL IZM_FRQ_2
663
664 ;Выводим дамп AY:
665
666 OUT_AY     LD   L,#0A     ;H=#80
667 OUT_AY_1   LD   BC,#FFFD
668            OUT  (C),L
669            LD   B,#BF
670            OUTD
671
672 ;После OUTD флаг C=0, если значение H
673 ;не изменилось после уменьшения, и C=1,
674 ;если изменилось (и если в (HL) не 0).
675 ;А вы не знали? ;-)
676
677            JR   NC,OUT_AY_1
678            RET
679
680 ;---------------------------------------
681
682 IZM_FRQ    SUB  D
683            LD   H,#80
684            LD   (HL),A ;Громкость.
685
686 ;Сейчас в регистре L номер регистра AY
687 ;для амплитуды канала, преобразуем его
688 ;в номер регистра AY для младшего байта
689 ;частоты того же канала:
690
691            SLA  L       ;*2
692            RES  4,L     ;-16
693
694            RLCA
695            RLCA
696            RLCA
697            AND  7
698            ADD  A,E
699            SUB  2
700
701 IZM_FRQ_2  ADD  A,freq_shift
702
703 ;После прибавления freq_shift в A будет
704 ;младший байт делителя, а во флаге C -
705 ;старший байт.
706
707            LD   (HL),A
708
709            INC  L
710            LD   A,0   ;Получаем значение
711            ADC  A,A   ;старшего байта
712                       ;и выводим его.
713            LD   (HL),A
714
715 ;           RET   (см.ниже)
716
717 ;Таблица частот (каждой ноте в таблице
718 ;соответствует значение делителя частоты
719 ;минус смещение freq_shift, т.о. на одну
720 ;ноту тратится один байт, а не два).
721 ;
722 ;Значение freq_shift подобрано таким
723 ;образом, чтобы первый байт таблицы
724 ;равнялся #C9 и служил одновременно
725 ;командой RET, что сокращает общую
726 ;длину программы.
727 ;
728 ;freq_shift min = #15
729 ;freq_shift max = #6E
730 ;
731 ;1-е знач. в табл (g4): м.б. #A6-#FF.
732 ;
733 ;Совмещена с таблицей ORNAMENTS.
734 ;
735 ;Таблица ornaments (8 байтов).
736 ;
737 ;Хранятся значения freq. Для использова-
738 ;ния нужно знать смещение в этой табли-
739 ;це, тогда ornament - это 3 байта, от-
740 ;считываемые от этого смещения.
741
742 FREQ_TABLE DB   freq_g4-freq_shift
743            DB   freq_b4-freq_shift
744 ORNAMENTS  DB   freq_a4-freq_shift
745            DB   freq_c5-freq_shift
746            DB   freq_e5-freq_shift
747            DB   freq_g5-freq_shift
748            DB   freq_b5-freq_shift
749            DB   freq_d5-freq_shift
750            DB   freq_f5-freq_shift
751            DB   freq_a5-freq_shift
752
753 ;Таблица нот в каждом паттерне:
754 ;
755 ;1 байт - громкость + орнамент,
756 ;4 байта - ноты, в байте 2 ноты.
757
758 PATTERN_0  DB   code_dn+code_pack+ornament_0
759            DB   note_c5*16+note_g4
760
761 PATTERN_1  DB   code_dn+code_pack+ornament_1
762            DB   note_d5*16+note_a4
763
764 PATTERN_2  DB   code_dn+code_pack+ornament_0
765            DB   note_e5*16+note_b4
766
767 PATTERN_3  DB   code_up+code_pack+ornament_0
768            DB   note_e5*16+note_b4
769
770 PATTERN_4  DB   code_up+ornament_0
771            DB   note_e5*16+note_c5
772            DB   note_d5*16+note_e5
773            DB   note_a5*16+note_g5
774            DB   note_f5*16+note_e5
775
776 PATTERN_5  DB   code_dn+code_pack+ornament_1
777            DB   note_a5*16+note_d5
778
779 PATTERN_6  DB   code_max+code_pack+ornament_2
780            DB   note_g5*16+note_d5
781
782 PATTERN_7  DB   code_dn+code_pack+ornament_2
783            DB   note_b5*16+note_e5
784
785 PATTERN_8  DB   code_dn+code_pack+ornament_0
786            DB   note_f5*16+note_c5
787
788 PATTERN_9  DB   code_up+ornament_0
789            DB   note_g5*16+note_c5
790            DB   note_g5*16+note_c5
791            DB   note_a5*16+note_c5
792            DB   note_a5*16+note_c5
793
794 PATTERN_10 DB   code_max+ornament_1
795            DB   note_a5*16+note_d5
796            DB   note_f5*16+note_d5
797            DB   note_a5*16+note_d5
798            DB   note_f5*16+note_d5
799
800 PATTERN_11 DB   code_max+ornament_3
801            DB   note_e5*16+note_a4
802            DB   note_c5*16+note_a4
803            DB   note_e5*16+note_a4
804            DB   note_c5*16+note_a4
805
806 PATTERN_12 DB   code_max+ornament_0
807            DB   note_g5*16+note_c5
808            DB   note_e5*16+note_c5
809            DB   note_g5*16+note_c5
810            DB   note_e5*16+note_c5
811
812 END_PLAY
813
814            ORG  #81FD
815
816 ;Константы, прибавляемые к значению
817 ;амплитуды в sample и указывающие на
818 ;смещение частоты:
819
820 up_2       EQU  #80
821 up_0       EQU  #40
822 dn_2       EQU  #00
823
824 ;Первые 4 байта таблицы sample:
825
826 SAMPLE     DB   #F+up_0
827            DB   #E+up_0
828            DB   #D+up_0
829            DB   #D+up_0

Литература

  1. И.Рощин. «Z80: оптимизация загрузки констант в регистры». «Радиолюбитель. Ваш компьютер» 9/2000, 2/2001 (под псевдонимом BV_Creator).
  2. И.Рощин. «Ещё о программировании арифметических операций». «Радиолюбитель. Ваш компьютер» 12/2000, 1—4/2001.
  3. И.Рощин. «Влияние команды OUTD на флаг переноса». «Радиолюбитель. Ваш компьютер» 5/2001 (под псевдонимом BV_Creator).

Другие мои статьи о программировании графических эффектов:

1. 

«Драйвер экрана для компьютера „Пентагон-128“». «ZX-Ревю» 3—4/1997.

2. 

«Два графических эффекта». «ZX-Ревю» 3—4/1997.

3. 

«Графический эффект „цветные полосы“». «ZX-Ревю» 7—10/1997.

4. 

«Несколько графических эффектов». «ZX-Ревю» 11—12/1997.

5. 

«Графический эффект „iris“». ZX Format #8, Adventurer #8.

6. 

«Улучшенный эффект из demo „Art Vision“». Adventurer #8.

Другие мои статьи о различных приёмах оптимизации при программировании на ассемблере Z80:

1. 

«По поводу релоцируемых программ». «ZX-Ревю» 5—6/1997.

2. 

«Z80: оптимизация загрузки констант в регистры». «Радиолюбитель. Ваш компьютер» 9/2000, 2/2001 (под псевдонимом BV_Creator).

3. 

«Ещё о программировании арифметических операций». «Радиолюбитель. Ваш компьютер» 12/2000, 1—4/2001.

4. 

«Влияние команды OUTD на флаг переноса». «Радиолюбитель. Ваш компьютер» 5/2001, «Радиомир. Ваш компьютер» 8/2003 (под псевдонимом BV_Creator).

5. 

«Менеджер вызова подпрограмм из различных банков памяти». «Радиомир. Ваш компьютер» 12/2001, 2/2002, 4/2002.

6. 

«Улучшение сжатия программ на ассемблере Z80». «Радиомир. Ваш компьютер» 4/2003.

7. 

«Процедура сравнения строк на ассемблере Z80». «Радиомир. Ваш компьютер» 6/2003.

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