Двойная буферизация в x86 VGA

Чтобы вернуть некоторые воспоминания, я решил сесть и написать небольшую игру на ассемблере в режиме VGA 13h - пока не понял, что визуальный вывод чертовски мерцает.

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

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

Итак, окончательное решение, о котором я знаю, выглядит примерно так:

  • делать все графические операции - очистку экрана, настройку пикселей - на отдельной области памяти
  • дождитесь вертикального отката
  • скопировать память в видеопамять

Теория, конечно, проста, но я никак не могу понять, как сделать запись в буфер и, в конечном счете, записать ее в видеопамять!

Вот урезанный, хотя и рабочий, фрагмент моего кода, написанного для TASM:

VGA256      EQU 13h
TEXTMODE    EQU 3h
VIDEOMEMORY EQU 0a000h
RETRACE     EQU 3dah
.MODEL LARGE

.STACK 100h

.DATA 
spriteColor     DW ?
spriteOffset    DW ?
spriteWidth     DW ?
spriteHeight    DW ?
enemyOneA       DB 0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,1,0,1,1,0,1,1,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,0,1,0,0,0,0,0,0,0,0,0,1,0,1,0,0,1,0,1,0,0,0,0
spriteToDraw    DW ?
buffer          DB 64000 dup (0) ; HERE'S MY BUFFER

.CODE
Main:
    MOV     AX,@DATA;
    MOV     DS,AX

    MOV     AH,0
    MOV     AL,VGA256
    INT     10h
    CLI
MainLoop:
    MOV     DX,RETRACE
Vsync1:
    IN      AL,DX
    TEST    AL,8
    JZ      Vsync1
Vsync2:
    IN      AL,DX
    TEST    AL,8
    JNZ     Vsync2
        
    CALL    clearScreen
    CALL    updateSprites
    JMP     MainLoop
    mov     AH,1
    int     21h

    mov     AH,0
    mov     AL,TEXTMODE
    int     10h 

; program end

clearScreen PROC NEAR 
    MOV     BX,VIDEOMEMORY
    MOV     ES,BX
    XOR     DI,DI
    MOV     CX,320*200/2
    MOV     AL,12
    MOV     AH,AL
    REP     STOSW
    RET
clearScreen ENDP

drawSprite PROC NEAR
    MOV     DI,0
    MOV     CX,0
ForLoopA:
    PUSH    CX
    MOV     SI,CX
    MOV     CX,0
ForLoopB:
    MOV     BX,spriteToDraw
    MOV     AL,[BX+DI]

    CMP     AL,0
    JE      DontDraw

    MOV     BX,spriteColor
    MUL     BX

    PUSH    SI
    PUSH    DI
    PUSH    AX

    MOV     AX,SI
    MOV     BX,320
    MUL     BX
    MOV     BX,AX
    
    POP     AX
    POP     DI

    ADD     BX,CX
    ADD     BX,spriteOffset
    MOV     SI,BX

    MOV     BX,VIDEOMEMORY
    MOV     ES,BX
    MOV     ES:[SI],AL
    POP     SI
DontDraw:
    INC     DI
    INC     CX
       
    CMP     CX,spriteWidth
    JNE     ForLoopB
    POP     CX
    INC     CX
    CMP     CX,spriteHeight
    JNE     ForLoopA
    RET
drawSprite ENDP

updateSprites PROC NEAR
    MOV     spriteOffset,0
    MOV     spriteColor,15
    MOV     spriteWidth,16
    MOV     spriteHeight,8     
    MOV     spriteOffset,0
    MOV     spriteToDraw, OFFSET enemyOneA
    CALL    drawSprite
    RET
updateSprites ENDP

END Main

person obscure    schedule 15.01.2021    source источник
comment
Итак... какой у вас вопрос?   -  person fuz    schedule 15.01.2021
comment
Хорошо, как записать данные в «буфер» области памяти в моем фрагменте и перенести эту память в видеопамять после вертикальной ретрассировки.   -  person obscure    schedule 15.01.2021
comment
rep movsb или w должны быть очень быстрыми на современном процессоре, даже если назначением является видеопамять (сопоставленный WC), достаточно быстрыми, чтобы работать во время vblank. (И, вероятно, закончить до сканирования первой строки, хотя фактическое требование состоит в том, чтобы быть достаточно быстрым, чтобы сканирование не успевало за копированием.)   -  person Peter Cordes    schedule 15.01.2021
comment
Также используются регистры VGA для изменения отображаемой страницы и страницы, отображаемой в физической памяти, IIRC. Но я не вижу своих справочников для этого, что, вероятно, означает, что они застряли где-то в коробке...   -  person 1201ProgramAlarm    schedule 15.01.2021


Ответы (1)


Первая проблема заключается в том, что вы находитесь в реальном режиме. Это означает, что вы работаете с сегментами по 64 КиБ. Для 320*200 с 256 цветами буфер должен быть 64000 байт; и если вы попытаетесь иметь один сегмент данных, содержащий все, у вас останется только 1535 байт для вещей, которые не являются буфером (спрайты, глобальные переменные и т. д.). Это слишком ограничивает (рано или поздно вам понадобятся анимированные спрайты, или уровень/карта/фоновый пейзаж, или...).

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

Чтобы решить обе эти проблемы; Я бы выделил память для буфера (например, используя функцию int 0x21, ah = 0x48 DOS) и выделил бы специальный сегмент буфера. В этом случае перенос буфера в видеопамять может выглядеть так:

    push es
    push ds
    mov ax,VIDEO_MEMORY_SEGMENT
    mov bx,[bufferSegment]
    mov es,ax
    mov ds,bx
    mov cx,320*200/2
    cld
    xor si,si               ;ds:si = bufferSegment:0 = address of buffer
    xor di,di               ;es:di = VIDEO_MEMORY_SEGMENT:0 = address of video memory
    rep movsw
    pop ds
    pop es
    ret

Примечание 1. Было бы лучше/быстрее использовать mov cx,320*200/4 и rep movsd для копирования по 4 байта за раз, но для этого потребуется 32-разрядный процессор (не будет работать для 80286 и более поздних версий). Если ЦП поддерживает 32-битные инструкции, они прекрасно работают в 16-битном коде (это просто префикс размера операнда для изменения размера по умолчанию, и вам не нужно переключаться на использование защищенного режима).

Примечание 2: cld (установить сброс флага направления) может быть ненужным. Обычно вы сбрасываете флаг направления один раз в начале вашей программы (или полагаетесь на то, что флаг гарантированно очищается ОС при запуске программы), так что вам не нужно убеждаться, что он очищен каждый раз, когда вы используете строковую инструкцию (например, как rep movsw).

Для записи в буфер весь ваш код останется прежним, за исключением того, что вы установите es на buffer_segment вместо установки es на VIDEO_MEMORY_SEGMENT.

Примечание 3: Вместо того, чтобы загружать es с одним и тем же значением в нескольких местах (в clearScreen, в середине цикла в drawSprite(!) и т. д.), лучше установить es один раз во время инициализации программы и сохранить/восстановить его при вам нужно использовать es для чего-то другого (в функции блитинга); так что вы можете избежать (относительно дорогих) загрузок регистров сегментов (например, mov es,bx) во всем коде рисования.

Также; если вам в конечном итоге нужно фоновое изображение (сгенерированное из данных уровня/карты или...), вы можете использовать третий фоновый буфер. Это было бы в основном то же самое - выделить еще 64000 байт для фона (и иметь background_segment), затем один раз отрисовать фон в буфер (при загрузке уровня или общей карты или ..); затем скопируйте уже нарисованные фоновые данные из фонового буфера в основной буфер вместо очистки буфера и нарисуйте на нем свои спрайты, а затем перенесите буфер на видео.

person Brendan    schedule 15.01.2021
comment
Последние процессоры (IvyBridge и более поздние версии) имеют быстрый rep movsb. И даже rep movsw не намного, если вообще медленнее, на большинстве процессоров Intel/AMD этого века. Таким образом, использование movsd, вероятно, имеет большое значение, только если вы заботитесь о старых процессорах (например, до семейства P6), что может иметь место, если вы пишете 16-битный код в стиле ретро. - person Peter Cordes; 15.01.2021
comment
Спасибо, что нашли время @Brendan. Я попробовал то, что вы предложили для простоты, хотя я оставил буфер внутри раздела .DATA (просто для тестирования) и инициализировал его как DW 32000 dup (0). После этого я попытался модифицировать функцию clearScreen MOV BX, OFFSET buffer и MOV ES, BX, но это изменение приводит к зависанию программы на инструкции REP STOSW. Что может быть причиной? - person obscure; 16.01.2021
comment
@obscureL Вы не можете (легко) использовать смещение внутри сегмента как сегмент. Если buffer находится по смещению 0x1234 в вашем сегменте данных, то загрузка 0x1234 в es и выполнение rep stosw приведет к уничтожению всего, что находится в ОЗУ, что не имеет ничего общего с вашим кодом (возможно, перезапись кода или данных DOS, возможно перезапись вашего собственного кода, ...). - person Brendan; 17.01.2021
comment
@obscure: В частности, чтобы использовать buffer в качестве сегмента, вам нужно будет выполнить расчет segment = ds + (buffer >> 4) (который будет работать, только если buffer выровнен по 16-байтовой границе и если добавление не вызывает переполнение). - person Brendan; 17.01.2021