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 ════════════════════════════════════════════════ С уважением, Иван Рощин.




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

Похожие статьи:
Новости и старости - местные новости.
Игры - ARKANOID 2
Учимся кодить вещи - изящнaя oчисткa экpaнa.
Новости - что примерно будет во втором номере.
Презентация - Командер нового поколения - DOS v1.9, Turbo Assembler v4.0, Rock Disk Service v3.1.

В этот день...   8 мая