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


тема: Оптимизация на примере intro 'Start'



от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_1 .W ══════════════════

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

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

Оптимизация на примере intro "Start"
════════════════════════════════════

("Радиомир. Ваш компьютер" 7-10/2001)

Hаверняка вы слышали о крупнейшем российском demoparty 2000
года - Chaos Constructions 000. Там были представлены и мои
работы, одна из которых - 512-байтное intro "Start".
Что такое intro? Это небольшая программа, сочетающая
графические эффекты и (обычно) музыкальное сопровождение.
Создание intro без всякого преувеличения можно назвать настоящим
искусством. Зачастую программу, время работы которой -
несколько минут, пишут в течение многих месяцев. А потом, на
demoparty, зрители выбирают лучшее intro, в котором реализованы
самые красивые эффекты при заданном ограничении на размер.
Соревнования (intro compo) проходят в различных номинациях:
512 b, 1 Kb, 4 Kb и так далее. Эти цифры определяют максимальную
длину кодового блока intro.
После загрузки с диска, как правило, intro может
использовать всю доступную оперативную память. Поэтому авторы не
особенно стараются оптимизировать intro по длине, предпочитая
писать "рыхлый" код и затем сжимать его специальными
программами-упаковщиками (которых насчитывается более десятка).
Это вполне оправданно: в большинстве случаев автоматическая
упаковка дает лучшие результаты по сравнению с ручной
оптимизацией, не говоря уже о затратах времени и сил.
Hо для 512-байтных intro этот способ не годится. Думаю, вы
уже догадались, почему: программа-распаковщик, даже самый
примитивный LZ, займет относительно много места по сравнению с
самой intro. Так что приходится оптимизировать код вручную,
используя при этом многие известные способы, придумывая новые
и выигрывая в результате байт за байтом. Для этого недостаточно
просто хорошо знать ассемблер - необходимо ясно представлять
себе, как будет выглядеть каждая команда в машинных кодах и за
сколько тактов она выполнится; необходимо в совершенстве знать
архитектуру ZX Spectrum, включая множество недокументированных
особенностей... В общем, оптимизация intro - задача не для
"чайников". :-)

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

Сначала скажу несколько слов о том, что же мы увидим,
запустив intro. Эта информация понадобится вам, чтобы понять -
а что, собственно, делает программа.
Intro может работать на любом ZX Spectrum 48/128, но
рекомендуется "Пентагон" и более быстрые модели - во-первых,
чтобы предварительные вычисления выполнялись быстрее, и
во-вторых - чтобы очисткa экрана в начале и в конце выполнялась
"мгновенно" (см. раздел "Увеличение быстродействия за счет
раскрытия циклов").
После запуска около 12 секунд уходит на предварительные
вычисления - в это время вам остается только ждать, глядя на
черный экран. Затем появляется эффект "атрибутная плазма".
(Вся красота этого эффекта в движении, а неподвижный рисунок,
увы, не в состоянии ее передать.)

┌─────────────────────────────┐
│pic_1.scr │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────┘

Рис. 1

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

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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_1 .W ══════════════════

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

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

Оптимизация на примере intro "Start"
════════════════════════════════════

("Радиомир. Ваш компьютер" 7-10/2001)

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

┌─────────────────────────────┐
│pic_1.scr │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────┘

Рис. 1

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

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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_2 .W ══════════════════

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

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

_Таблицы_,_массивы_ - обыкновенные одномерные массивы.
Именно их я буду иметь в виду, говоря о структурах данных.
Таблицами я для удобства называю такие массивы, которые не
изменяются в процессе работы программы.
Если это не оговорено особо, длина каждого элемента
предполагается равной одному байту.
_Сегмент_ - участок памяти длиной 256 байт, начинающийся
с адреса, кратного 256. Hапомню, что 256 - это 100 в
шестнадцатеричной системе счисления. Следовательно, младший
байт адреса начала сегмента равен нулю. Сегмент однозначно
определяется старшим байтом адреса своего начала. Старшие байты
адресов всех ячеек сегмента одинаковы.
Hазовем два сегмента _смежными_, если один из них
непосредственно следует за другим. Свойство смежных сегментов -
если взять любую ячейку памяти из одного сегмента и любую из
другого, то старшие байты их адресов будут различаться на
единицу.
Hазовем структуру данных _выровненной на границу сегмента_,
если она начинается с начала этого сегмента. Очевидно, младший
байт адреса первого элемента такой структуры равен нулю.
Hазовем структуру данных _односегментной_, если она
полностью лежит в каком-либо одном сегменте. У всех элементов
такой структуры старший байт адреса один и тот же.

Таблица 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 байтам. Hо ведь, согласно условиям, длина intro не должна
превосходить 512 байт! Как же было разрешено это противоречие?
Значительного сокращения размера (а вдобавок - и времени
выполнения процедуры проигрывания!) удалось добиться за счет
представления данных, необходимых для проигрывания музыки, в
наиболее выгодной форме. Выбор такой формы во многом был
обусловлен индивидуальными особенностями мелодии. Рассмотрим это
на примерах.

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_2 .W ══════════════════

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

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

_Таблицы_,_массивы_ - обыкновенные одномерные массивы.
Именно их я буду иметь в виду, говоря о структурах данных.
Таблицами я для удобства называю такие массивы, которые не
изменяются в процессе работы программы.
Если это не оговорено особо, длина каждого элемента
предполагается равной одному байту.
_Сегмент_ - участок памяти длиной 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 байт! Как же было разрешено это противоречие?
Значительного сокращения размера (а вдобавок - и времени
выполнения процедуры проигрывания!) удалось добиться за счет
представления данных, необходимых для проигрывания музыки, в
наиболее выгодной форме. Выбор такой формы во многом был
обусловлен индивидуальными особенностями мелодии. Рассмотрим это
на примерах.

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_3 .W ══════════════════

Hачнем с того, что для проигрывания музыки нужна частотная
таблица с коэффициентами деления для каждой ноты. В
универсальном плеере эта таблица содержит коэффициенты деления
(числа в диапазоне 0-4095) для всех 96 нот. Соответственно,
занимает такая таблица 96*2=192 байта. Hо, зная, какие ноты
задействованы в мелодии, можно хранить в таблице только их
коэффициенты. В мелодии, написанной для 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), и когда требуется узнать
соответствующий данной ноте коэффициент деления, этот номер
служит индексом в частотной таблице. Очевидно, что в принципе
совершенно безразлично, каким способом будет установлено
соответствие между нотами и их номерами - лишь бы оно было
взаимно однозначным. Hеобязательно самой низкой ноте должен быть
присвоен номер 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'е:

┌─────────────────────────────┐
│pic_2.scr │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────┘

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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_3 .W ══════════════════

Начнем с того, что для проигрывания музыки нужна частотная
таблица с коэффициентами деления для каждой ноты. В
универсальном плеере эта таблица содержит коэффициенты деления
(числа в диапазоне 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'е:

┌─────────────────────────────┐
│pic_2.scr │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────┘

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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_4 .W ══════════════════

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


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

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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_4 .W ══════════════════

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


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

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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_5 .W ══════════════════

В строке 116 необходимо поместить 0 по адресу, заданному в
регистровой паре HL. Можно было бы сделать это командой LD (HL),
0. Hо известно, что к этому моменту регистр B обнулился после
DJNZ. Таким образом, достаточно применить команду LD (HL),B.
Экономия составляет 1 байт/3 такта.
Перед заполнением экрана текстурой (строки 121-140)
необходимо поместить в HL адрес первого обрабатываемого байта
видеопамяти - #57FF. Hо к этому моменту в HL уже содержится это
значение: оно оказалось там после процедуры CLS, выполняющей
очистку экрана. Следовательно, команда LD HL,#57FF не нужна.
Экономия составляет 3 байта/10 тактов.
В строке 147 необходимо загрузить в DE значение #AA00 (адрес
таблицы TABLE_SIN). Hо мы знаем, что 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. Hо известно, что
старший байт значения, находящегося на вершине стека, равен нулю
(строка 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].

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_5 .W ══════════════════

В строке 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].

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_6 .W ══════════════════

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

Часто можно извлечь выгоду из замены цикла с заранее
известным количеством повторений ("for") на цикл с выходом по
условию ("repeat-until").
Рассмотрим, например, заполнение экрана текстурой (строки
129-140). Длина заполняемой области известна заранее - #1800
байт. Если бы мы использовали цикл "for", то пришлось бы завести
отдельный счетчик (и выделить для него регистровую пару),
занести в него количество повторов (#1800), а затем в каждом
проходе цикла уменьшать значение счетчика и проверять его на
равенство нулю.
А как сделано в intro? Hикакого специального счетчика там
нет, а вместо этого после каждого прохода цикла производится
проверка: не стал ли шестой бит регистра 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
секунды, вычисление синуса должно быть как можно более быстрым.
Hаиболее подходит для этой цели табличный способ. Сущность его
заключается в следующем: имеется таблица, содержащая значения
функции при всех возможных значениях аргумента, а когда в
программе надо вычислить значение функции, из таблицы просто
берется готовый результат.
Применение этого способа, конечно, увеличивает размер
программы, но вместе с тем значительно повышает быстродействие.
Взять число из таблицы можно за несколько тактов, а вычисление
синуса с помощью калькулятора ПЗУ (фактически - вычисление суммы
ряда) займет на несколько порядков больше времени. В этом
нетрудно убедиться, если заметить, что формирование таблицы при
запуске intro занимает целых 12 секунд, а вычисляются при этом
всего-навсего 256 значений синуса.
Тем не менее, будьте внимательны при использовании
табличного задания функции на процессорах, более быстрых по
сравнению с Z80. Убедитесь сначала, что выигрыш действительно
будет. Дело в том, что обычно процессор работает на большей
частоте, чем память, и вычисление функции может занять меньше
времени, чем сравнительно медленное чтение из памяти.

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_6 .W ══════════════════

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

Часто можно извлечь выгоду из замены цикла с заранее
известным количеством повторений ("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. Убедитесь сначала, что выигрыш действительно
будет. Дело в том, что обычно процессор работает на большей
частоте, чем память, и вычисление функции может занять меньше
времени, чем сравнительно медленное чтение из памяти.

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_7 .W ══════════════════

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

Когда надо оптимизировать программу прежде всего по длине,
этот способ применяют редко - ведь при раскрытии циклов длина
программы возрастает. Тем не менее, в одном месте 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 тактов. Hо в данном случае такое повышение
быстродействия оказывается недостаточным.

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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_7 .W ══════════════════

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

Когда надо оптимизировать программу прежде всего по длине,
этот способ применяют редко - ведь при раскрытии циклов длина
программы возрастает. Тем не менее, в одном месте 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,
и в результате мы услышим финальный аккорд.

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_8 .W ══════════════════

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

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

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

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

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

╔═══════════════════════╤══════════════════════════╤═══════════╗
║ Hепосредственная │ Загрузка из памяти │ Экономия ║
║ загрузка │ │ ╟
╟───────────┬─────┬─────┼──────────────┬─────┬─────┼─────┬─────╢
║ команда │ L │ T │ команда │ L │ T │ L │ T ║
╠═══════════╪═════╪═════╪══════════════╪═════╪═════╪═════╪═════╣
║ LD A,N │ 2 │ 7 │ LD A,(addr) │3(+1)│ 13 │ 2 │ 6 ║
╟───────────┼─────┼─────┼──────────────┴─────┴─────┼─────┼─────╢
║ LD B,N │ │ │ │ │ ║
║ LD C,N │ │ │ │ │ ║
║ LD D,N │ 2 │ 7 │ нет │ >2 │ >6 ║
║ LD E,N │ │ │ │ │ ║
║ LD H,N │ │ │ │ │ ║
║ LD L,N │ │ │ │ │ ║
╟───────────┼─────┼─────┼──────────────────────────┼─────┼─────╢
║ LD XH,N │ │ │ │ │ ║
║ LD XL,N │ 3 │ 11 │ нет │ >2 │ >6 ║
║ LD YH,N │ │ │ │ │ ║
║ LD YL,N │ │ │ │ │ ║
╟───────────┼─────┼─────┼──────────────┬─────┬─────┼─────┼─────╢
║ LD HL,NN │ 3 │ 10 │ LD HL,(addr) │3(+2)│ 16 │ 2 │ 6 ║
╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢
║ LD DE,NN │ 3 │ 10 │ LD DE,(addr) │4(+2)│ 20 │ 3 │ 10 ║
║ LD BC,NN │ │ │ LD BC,(addr) │ │ │ │ ║
╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢
║ LD IX,NN │ 4 │ 14 │ LD IX,(addr) │4(+2)│ 20 │ 2 │ 6 ║
║ LD IY,NN │ │ │ LD IY,(addr) │ │ │ │ ║
╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢
║ LD SP,NN │ 3 │ 10 │ LD SP,(addr) │4(+2)│ 20 │ 3 │ 10 ║
╚═══════════╧═════╧═════╧══════════════╧═════╧═════╧═════╧═════╝

Таблица 1. Сравнение команд загрузки.

L - длина (в байтах), T - время (в тактах). В столбце "Загрузка
из памяти" длина указывается в виде "X(+Y)", где X - длина
команды, а Y - длина загружаемых данных. Экономия указывается с
учетом длины данных.

Примеры применения этого метода в 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 тактов).

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_8 .W ══════════════════

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

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

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

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

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

╔═══════════════════════╤══════════════════════════╤═══════════╗
║ Непосредственная │ Загрузка из памяти │ Экономия ║
║ загрузка │ │ ╟
╟───────────┬─────┬─────┼──────────────┬─────┬─────┼─────┬─────╢
║ команда │ L │ T │ команда │ L │ T │ L │ T ║
╠═══════════╪═════╪═════╪══════════════╪═════╪═════╪═════╪═════╣
║ LD A,N │ 2 │ 7 │ LD A,(addr) │3(+1)│ 13 │ 2 │ 6 ║
╟───────────┼─────┼─────┼──────────────┴─────┴─────┼─────┼─────╢
║ LD B,N │ │ │ │ │ ║
║ LD C,N │ │ │ │ │ ║
║ LD D,N │ 2 │ 7 │ нет │ >2 │ >6 ║
║ LD E,N │ │ │ │ │ ║
║ LD H,N │ │ │ │ │ ║
║ LD L,N │ │ │ │ │ ║
╟───────────┼─────┼─────┼──────────────────────────┼─────┼─────╢
║ LD XH,N │ │ │ │ │ ║
║ LD XL,N │ 3 │ 11 │ нет │ >2 │ >6 ║
║ LD YH,N │ │ │ │ │ ║
║ LD YL,N │ │ │ │ │ ║
╟───────────┼─────┼─────┼──────────────┬─────┬─────┼─────┼─────╢
║ LD HL,NN │ 3 │ 10 │ LD HL,(addr) │3(+2)│ 16 │ 2 │ 6 ║
╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢
║ LD DE,NN │ 3 │ 10 │ LD DE,(addr) │4(+2)│ 20 │ 3 │ 10 ║
║ LD BC,NN │ │ │ LD BC,(addr) │ │ │ │ ║
╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢
║ LD IX,NN │ 4 │ 14 │ LD IX,(addr) │4(+2)│ 20 │ 2 │ 6 ║
║ LD IY,NN │ │ │ LD IY,(addr) │ │ │ │ ║
╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢
║ LD SP,NN │ 3 │ 10 │ LD SP,(addr) │4(+2)│ 20 │ 3 │ 10 ║
╚═══════════╧═════╧═════╧══════════════╧═════╧═════╧═════╧═════╝

Таблица 1. Сравнение команд загрузки.

L - длина (в байтах), T - время (в тактах). В столбце "Загрузка
из памяти" длина указывается в виде "X(+Y)", где X - длина
команды, а Y - длина загружаемых данных. Экономия указывается с
учетом длины данных.

Примеры применения этого метода в 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 тактов).

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_9 .W ══════════════════

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

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

При использовании недокументированных возможностей возникает
вопрос - как это все будет работать при выполнении программы под
эмулятором. Команды, работающие с половинками индексных
регистров, поддерживаются во всех известных мне эмуляторах, а
вот насчет правильного выставления флагов при выполнении команды
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. Вот так и удалось
сократить программу еще на байт.
Hачальное значение t (находящееся в регистре XL) оказывает
влияние только на точку, с которой начнется движение, а вид
траектории от него не зависит. Поэтому я не задаю это значение и
тем самым не трачу лишние байты. Так что, если вы будете
запускать intro при различных значениях XL, то всякий раз
увидите его немного другим. :-)

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_9 .W ══════════════════

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

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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_10.W ══════════════════

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

Посмотрите, как происходит формирование 256-байтной таблицы
PALETTE. Она должна состоять из 12 равных частей, заполненных
значениями, взятыми из 12-байтной таблицы COLORS. Hо 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.
Казалось бы, к чему такие сложности - ведь можно просто
добавить проверку достижения конца таблицы во внутреннем цикле.
Да, можно - но это займет еще два байта памяти.
Между прочим, подобные таблицы применяются при реализации
многих графических эффектов, так что, надеюсь, изложенный
принцип их оптимального формирования кому-то пригодится.

* * *

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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_10.W ══════════════════

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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_11.W ══════════════════

* * *

Hесколько слов о приведенном ниже исходном тексте intro
"Start".
Текст снабжен подробными комментариями. Строки программы
пронумерованы лишь для того, чтобы на них было удобно ссылаться.
Если вам нужно набрать программу, чтобы откомпилировать ее и
запустить, то номера строк указывать не надо.
Программа предназначена для компиляции в ассемблере ZXASM.
Если вы используете какой-либо другой ассемблер, обратите
внимание на характерные для ZXASM особенности синтаксиса.
В строках 221, 223, 230, 245 находятся команды, работающие с
половинками регистровой пары IX. В ZXASM для них приняты
обозначения XH и XL. В других ассемблерах эти половинки могут
обозначаться как HX и LX, или же вообще не поддерживаться
(все-таки это недокументированная возможность Z80) - в таком
случае придется исправить в указанных командах XH на H, XL - на
L, и перед каждой такой командой поставить байт-префикс #DD.
Hапример, команда INC XL в строке 221 будет выглядеть как две
команды: DB #DD и INC L.
В строках 402-430 и 484 вы можете увидеть оператор "" -
вычисление остатка от деления. Он применяется там, чтобы
получить младший байт двухбайтной величины. Если в используемом
вами ассемблере этот оператор не поддерживается, то, возможно,
предусмотрен специальный оператор вычисления младшего байта
(например, в ALASM'е это "."), или же ассемблер сам отбрасывает
старший байт, когда результат должен быть однобайтным (кстати, и
ZXASM обладает этой способностью). В крайнем случае можно
воспользоваться равенством A256=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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_11.W ══════════════════

* * *

Несколько слов о приведенном ниже исходном тексте intro
"Start".
Текст снабжен подробными комментариями. Строки программы
пронумерованы лишь для того, чтобы на них было удобно ссылаться.
Если вам нужно набрать программу, чтобы откомпилировать ее и
запустить, то номера строк указывать не надо.
Программа предназначена для компиляции в ассемблере ZXASM.
Если вы используете какой-либо другой ассемблер, обратите
внимание на характерные для ZXASM особенности синтаксиса.
В строках 221, 223, 230, 245 находятся команды, работающие с
половинками регистровой пары IX. В ZXASM для них приняты
обозначения XH и XL. В других ассемблерах эти половинки могут
обозначаться как HX и LX, или же вообще не поддерживаться
(все-таки это недокументированная возможность Z80) - в таком
случае придется исправить в указанных командах XH на H, XL - на
L, и перед каждой такой командой поставить байт-префикс #DD.
Например, команда INC XL в строке 221 будет выглядеть как две
команды: DB #DD и INC L.
В строках 402-430 и 484 вы можете увидеть оператор "" -
вычисление остатка от деления. Он применяется там, чтобы
получить младший байт двухбайтной величины. Если в используемом
вами ассемблере этот оператор не поддерживается, то, возможно,
предусмотрен специальный оператор вычисления младшего байта
(например, в ALASM'е это "."), или же ассемблер сам отбрасывает
старший байт, когда результат должен быть однобайтным (кстати, и
ZXASM обладает этой способностью). В крайнем случае можно
воспользоваться равенством A256=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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_13.W ══════════════════

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 ;Hабор цветов для плазмы:
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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_13.W ══════════════════

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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_14.W ══════════════════

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_0256
403 DB PATTERN_1256
404 DB PATTERN_2256
405 DB PATTERN_3256
406
407 DB PATTERN_0256
408 DB PATTERN_1256
409 DB PATTERN_2256
410 DB PATTERN_4256
411
412 DB PATTERN_5256
413 DB PATTERN_6256
414 DB PATTERN_2256
415 DB PATTERN_3256
416
417 DB PATTERN_5256
418 DB PATTERN_7256
419 DB PATTERN_8256
420 DB PATTERN_9256
421
422 DB PATTERN_10256
423 DB PATTERN_11256
424 DB PATTERN_12256
425 DB PATTERN_12256
426
427 DB PATTERN_10256
428 DB PATTERN_11256
429 DB PATTERN_12256
430 DB PATTERN_12256
431

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_15.W ══════════════════

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 PLAY256
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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_16.W ══════════════════

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_TABLE256
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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_17.W ══════════════════

650 ADD A,ornament_0 ;N тек.орн
651 ORNAMENT EQU $-1
652 ADD A,ORNAMENTS256
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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_18.W ══════════════════

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-3/2001.
3. И.Рощин. "Влияние команды OUTD на флаг переноса".
"Радиолюбитель. Ваш компьютер" 5/2001 (под псевдонимом
BV_Creator).

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_12.W ══════════════════

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

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

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

от: Ivan Roshin
кому: All
дата: 26 Jan 2002
Hello, All!

═══════════════════ start_12.W ══════════════════

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

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

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




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

Похожие статьи:
BBS - список станций BBS ZXNet.
Паралельные измерения - демосцена на БК0011м.
Личность - Приключения Василия Ивановича в тылу врага и на фронте любви (часть 2).

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