Разработка процедур с фиксированным временем выполнения (fixed-tacts) для ZX Spectrum

Subliminal Extacy #07

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

Именно поэтому я переписал эффекты (ничего особенного — просто вывод спрайтов на экран) как процедуры с фиксированным числом тактов (fixed-tacts).

Базовая процедура для рисования на экране — DOWNDE, которая вычисляет адрес следующей строки пикселей в видеопамяти. Типичная процедура выглядит примерно так:

DOWNDE  inc  d           ;4T
        ld   a,d         ;4T
        and  7           ;7T
        jr   nz,DOWNDE1  ;12/7T
        ld   a,e         ;4T
        add  a,32        ;7T
        ld   e,a         ;4T
        jr   c,DOWNDE1   ;12/7T
        ld   a,d         ;4T
        sub  8           ;7T
        ld   d,a         ;4T
DOWNDE1
                         ;min: 27T, max: 59T

Два условных перехода. Возможно ли вообще переписать это как процедуру с постоянной временной сложностью? Давайте попробуем:

DOWNDE_T
         inc  d               ;4T
         ld   a,d             ;4T
         and  7               ;7T
         sub  1               ;7T
         sbc  a,a             ;4T
         and  -8              ;7T
         ld   (DOWNDE_D+1),a  ;13T
         and  32              ;7T
         add  a,e             ;4T
         ld   e,a             ;4T
         ccf                  ;4T
         sbc  a,a             ;4T
DOWNDE_D and  -8              ;7T
         add  a,d             ;4T
         ld   d,a             ;4T
                              ;=84T

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

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

;вход: hl - данные спрайта
;      de - адрес в видеопамяти
;      bc - адрес маски
;      c' - ширина спрайта
        exx
        ld   b,c
S_LOOP1 exx           ;4T
        ld   a,(bc)   ;7T
        and  (hl)     ;7T
        ld   (de),a   ;7T
        inc  hl       ;6T
        inc  c        ;4T
        inc  e        ;4T
        exx           ;4T
        djnz S_LOOP1
                      ;=43T

Чтобы гарантировать, что процедура занимает одинаковое количество тактов для спрайтов любой ширины, мы добавим еще один цикл, который расширяет время выполнения до времени отрисовки самого широкого спрайта. Один проход через новый цикл должен занимать 43 такта. Однако проблема здесь в том, что инструкция ld (de),a обращается к медленной памяти и не всегда занимает одинаковое количество тактов. Но инструкция ld a,(de) делает то же самое! Итак, давайте попробуем расширить нашу процедуру:

        exx
        ld   b,c
S_LOOP1 exx            ;4T
        ld   a,(bc)    ;7T
        and  (hl)      ;7T
        ld   (de),a    ;7T
        inc  hl        ;6T
        inc  c         ;4T
        inc  e         ;4T
        exx            ;4T
        djnz S_LOOP1
        ld   a,13+1    ;нам не нравится 0 как счетчик цикла
        sub  c
        ld   b,a
S_LOOP2 exx            ;4T
        ld   a,(bc)    ;7T
        and  (hl)      ;7T
        ld   a,(de)    ;7T
        ld   iy,23610  ;14T
        exx            ;4T
        djnz S_LOOP2

Занимает ли эта процедура каждый раз ровно столько же времени? Сомневаюсь. Но этого достаточно, чтобы синхронизировать стабильный мультиколор в бордюре. Инструкция ld a,(de) здесь важна; без нее у меня ничего не работало.

Желаю вам побольше процедур с фиксированным временем выполнения!
mike/zeroteam

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