Быстрая запись в регистры AY-3-8912 на ZX Spectrum 128

Subliminal Extacy #07

Хотя вряд ли кому-то нужно объяснять, что в ZX Spectrum 128 установлен звуковой чип General Instrument AY-3-8912, давайте кратко освежим в памяти, как осуществляется доступ к его внутренним регистрам. Чип содержит шестнадцать регистров (R0–R15), выбор и запись которых осуществляются через два порта ввода-вывода: 65533 (#FFFD) и 49149 (#BFFD). Запись значения в диапазоне 0..15 в порт #FFFD выбирает соответствующий регистр, а последующая запись в порт #BFFD загружает в него новое значение.

Поэтому стандартный способ изменения значения регистра выглядит примерно так:

        ; D = номер регистра, E = значение

        ld   bc,#FFFD
        out  (c),d
        ld   b,#BF
        out  (c),e

При воспроизведении трекерной музыки плеер обычно формирует в памяти цепочку значений регистров и через фиксированные интервалы времени выводит их в AY. Во многих обработчиках прерываний часто можно встретить подобный код:

        ld   hl,AyRegs  ; Буфер значений регистров для вывода.
        ld   de,#FFBF
        ld   c,#FD
        xor  a
Loop    ld   b,d
        out  (c),a      ; Выбираем регистр AY.
        ld   b,e
        outi            ; Выводим значение по адресу (HL).
        inc  a
        cp   13
        jr   nc,Loop

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

4       ld   b,d
12      out  (c),a
4       ld   b,e
16      outi
4       inc  a
        ...

В сумме это дает 40 тактов на один регистр AY. Можно ли сделать лучше? Как выясняется, вполне. Если выровнять буфер регистров по границе 256 байт, то младший байт его адреса окажется равен нулю, что позволяет использовать регистр L в качестве номера регистра. Поскольку команда OUTI автоматически увеличивает HL, индекс регистра увеличивается сам при каждой записи, избавляя от необходимости отдельного инкремента:

4       ld   b,d
12      out  (c),l
4       ld   b,e
16      outi
        ...

Затраты снижаются до 36 тактов на регистр. Расплачиваться приходится гибкостью: буфер обязательно должен быть выровнен по границе 256 байт, а регистры должны выводиться в строго фиксированном порядке.

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

Вспомним, что OUTI не только инкрементирует HL, но и декрементирует B.

Грех не воспользоваться таким побочным эффектом! Превратить #FF в #BF простым декрементом на первый взгляд кажется невозможным — пока мы не вспомним, как именно ZX Spectrum дешифрует адреса ввода-вывода для чипа AY: учитываются только биты 15, 14 и 1. Это значит, что нам вовсе не обязательно превращать %11111111 (#FF) строго в %10111111 (#BF); достаточно лишь преобразовать %11xxxxxx в %10xxxxxx. А это элементарно делается, если изначально в регистре B находится %11000000, поскольку выполняемый OUTI декремент превратит его в %10111111. Полученное значение идеально подходит для обращения к порту данных AY, так как младшие биты адреса игнорируются.

Бывалые Z80-кодеры на этом месте наверняка удивленно поднимут бровь: разве OUTI не уменьшает B до того, как выставить его значение на шину? Да, совершенно верно, и решение тут элементарное — заранее загрузить в B значение #C1 вместо #C0. В итоге мы получаем следующий код:

        ld   hl,AyRegs
        ld   bc,#C1FD
        ld   a,b
        outi            ; Выбираем регистр AY из (HL).
        outi            ; Выводим значение из (HL).
        ld   b,a        ; Восстанавливаем B в #C1.
        outi
        outi
        ld   b,a
        outi
        outi
        ld   b,a
        outi
        outi
        ...

Что же изменилось? По скорости мы сохранили те же 36 тактов на регистр. Однако теперь буфер больше не требуется выравнивать по границе 256 байт, а сами регистры можно выводить в абсолютно любом порядке благодаря перемежающемуся формату буфера: регистр 1, значение 1, регистр 2, значение 2, регистр 3, значение 3 и так далее. На мой взгляд, решение получилось весьма элегантным и приятным с эстетической точки зрения.

До новых встреч!

baze / 3SC

Поделитесь вашим мнением о статье