Кстати, как ни странно, но такие игры как, например Warcraft 1, еще на DOS, на машинах своего времени нередко тоже "чиркали" по грани возможного в плане вывода графики, хотя казалось бы - чего такого, ложи в софтваре спрайты в буфер и делов то. А вот таки да - в то время машины едва с этим справлялись с приемлемым фпс.
Даже много лет спустя написав софтверный блиттер в досе для Mode 13 (320x200) на уже на порядки более быстродейственном Pentium я сильно удивился, что филлрейт то не радовал, не улетал в космос даже на пентиуме, который рвал 386-ые как бог черепаху, то есть на 386-ых варик вообще на грани возможного работал.
#496 (Правка: 27 дек. 2019, 5:19)
7:37, 13 янв. 2019
Гы, сделал по схеме LDPUSH из комментов про Old Tower и GULF выше вертикальный скроллинг двух верхних третей экрана на спектруме.
То есть заводится вспомогательный теневой буфер размером чуть больше 8 Кб описываемый в ассемблере следующим образом:
back_buf dup 128 ; всё до последнего edup будет повторятся 128 раз
ld sp, xxxx
dup 16 ; всё до следующего edup будет повторятся 16 раз
ld de, $ ; $ - это текущий адрес памяти для теста
push de
edup
edup
jp back_buf
Т.е. массив из подряд идущих 128 строк где первые три байта заняты инструкцией загрузки в SP константы - байта следующего за концом текущей строки пикселей на экране спектрума, а далее 16 раз повторяется загрузка констант в DE с помещением его в стек (суммарная длина последовательности из этих двух инструкций - четыре байта, что в два раза больше полезных данных помещаемых при этом на экран). В конце всего этого безобразия прыжок на начало для закольцовывания.
Изначально загрузки sp заполнены так, чтобы первая линия инструкций закрашивала первую строку пикселей на экране, вторая - вторую и так далее. Те кто помнит какая нелинейная у спектрума раскладка знают почему sp надо на каждой строке загружать новой базой.
Но инструкции закольцованы (и не просто так), поэтому код вызывающий их производит модификацию кода и заменяет код первой инструкции LD, SP... на инструкцию JP (HL), а сам, заполнив SP из непосредственных данных только что пропатченной инструкции, прыгает на эффективный кусок LD DE..., который уже дальше как с горки катится по строкам заполняя экран и в конце закольцовываясь через JP back_buf оказывается как раз на инструкции JP (HL) и возвращается в вызывающий код, т.к. HL бережно заполнен адресом возврата.
А по адресу возврата располагается код, который возвращает SP на настоящий кадр стека, и откатывает патчинг возвращая на место код инструкции LD SP...
Зачем так сложно? Потому что далее мы можем сделать следующее - сдвинуть все непосредственные данные в инструкциях LD SP... в кольце (128 раз) так, что строки поменяют циклично свою привязку к строкам пикселей на экране и текущей строкой (которая патчится и куда происходит переход) делать не первую, а вторую.
Таким образом переписав в памяти всего 128*2=256 байт (затравочные значения SP) и один код инструкции мы осуществляем скроллинг целых двух третей экрана.
Конечно не совсем, т.к. эффективную проброску делает код в теневом буфере, но эта проброска имеет быстродействие грани возможного и быстрее уже некуда. Скроллинг же получается практически даром. И при реализации бесконечного скроллинга обновлять в кадре надо только одну текущую полоску с инструкциями.
Сказано - сделано, написано - затестировано. Код просто бесконечно скроллящий вертикально первые 2 трети экрана работает с FPS чуть больше 60 герц! :D
Правда в игре с вертикальным скроллингом надо делать наоборот - вытягивать теневой буфер в высоту и потом уже тратить освободившееся время на спрайты и прочее.
+ если интересен код...
− Скрыть
device ZXSPECTRUM48
org $8000
entry jp start
;include "screen.inc"
;include "math.inc"
;include "zstr.inc"
;include "keys.inc"
back_buf dup 128 ; 128 строк
ld sp, 0 ; SP=конец строки
dup 16 ; 16 раз LDPUSH
ld de, $
push de
edup
edup
jp back_buf
line_size = 64+3
back_buf_size = line_size * 128
cur_line dw back_buf ; указатель на текущую (первую на экране) строку
; в теневом буфере
cur_line_num db 0 ; номер текущей строки в теневом буфере
; locate_scr_byte - найти адрес полоски пикселей на экране
; in: d=x (0-31), e=y (0-191)
; out: hl=адрес полоски пикселей
; pattern: 010YYYYY YYYXXXXX
locate_scr_byte ld a, e
rlca
rlca
and $E0
or d
ld l, a ; в l нижний байт адреса
ld a, e
and 7
or $40
ld h, a
ld a, e
rrca
rrca
rrca
and $18
or h
ld h, a ; в h верхний байт адреса
ret
sp_backup dw 0 ; времянка для SP
; raster - нарисовать текущий теневой буфер начиная со строки cur_line
raster push hl
push de
ld hl, 0
add hl, sp ; hl = sp
ld (sp_backup), hl ; сохранили sp
ld hl, (cur_line) ; патчим текущую строку в бэкбуфере
ld (hl), $E9 ; прописываем в ней опкод возврата из бэкбуфера jp (hl)
inc hl
ld e, (hl)
inc hl
ld d, (hl) ; dl = затравочное sp для текущей строки
inc hl ; hl = адрес активной части текущей строки
ex de, hl ; de <-> hl
di ; запрещаем прерывания
ld sp, hl ; sp = затравочное для текущей строки
ex de, hl ; de <-> hl (восстанавливаем)
ld (.jumper+1), hl ; устанавливаем распрыжку на текущую строку
ld hl, .raster_end ; запоминаем в hl адрес возврата из бэкбуфера
.jumper jp 0 ; адрес в распрыжке обновляется кодом выше
.raster_end ld sp, (sp_backup) ; первым делом восстанавливаем sp
ei ; разрешаем прерывания
ld hl, (cur_line) ; патчим обратно текущую линию
ld (hl), $31 ; восстанавливаем опкод ld sp, imm16
pop de
pop hl
ret
scroll_temp dw 0 ; временная переменная для scroll
; scroll - "скроллим" теневой буфер вверх на один пиксель/строку
scroll push hl
push de
push bc
; сохраним затравку SP в последней строке в temp
ld hl, (back_buf + back_buf_size - line_size + 1)
ld (scroll_temp), hl
; начинаем цикл переносящий затравки SP вверх по кольцу
; HL указывает на затравку SP предпоследней строки
ld hl, back_buf + back_buf_size - 2 * line_size + 1
ld a, 127 ; в цикле 128 раз
ld de, line_size ; размер строки для инкремента/декремента
.loop ld c, (hl)
inc hl
ld b, (hl)
dec hl ; загрузили в BC текущую затравку SP
push hl ; запомнили текущий hl
add hl, de ; перенацелили hl на следующую строку
ld (hl), c
inc hl
ld (hl), b ; сохранили BC в затравку SP следующей строки
pop hl ; восстановили hl на текущую строку
and a ; занулили CF
sbc hl, de ; переходим на следующую строку (выше)
dec a ; цикл по a...
jr nz, .loop ; ...127 раз
ld hl, (scroll_temp)
ld (back_buf + 1), hl ; первая строка обновляется из temp
; инкремент cur_line
ld hl, (cur_line)
ld de, line_size ; инкремент на одну строку
add hl, de
ld (cur_line), hl
; инкремент cur_line_num
ld a, (cur_line_num)
inc a
ld (cur_line_num), a
; проверим на выход за границу...
cp 128
jr nz, .exit ; если не превысили 127, то на выход
xor a ; зануляем a
ld (cur_line_num), a ; зануляем cur_line_num
ld hl, back_buf
ld (cur_line), hl ; cur_line указываем на первую строку
.exit pop bc
pop de
pop hl
ret
start ; инициализируем затравочные значения SP
ld hl, back_buf + 1 ; позиционируемся на адрес SP в инструкции первой строки
ld d, 31 ; 31 - последняя строка полосок экрана
ld e, 0 ; отсчитываем с нулевой строки на экране
.loop push hl
call locate_scr_byte ; вычисляем в hl адрес последней полоски в строке e на экране
inc hl ; и нам нужен следующей за ней адрес в SP
ld bc, hl ; запоминаем в bc затравочное SP
pop hl ; восстанавливаем в HL указатель на данное в инструкции
ld (hl), c
inc hl
ld (hl), b ; сохраняем в инструкцию затравочное SP из bc
ld bc, line_size-1 ; в bc инкремент (-1, т.к. не делали inc hl)...
add hl, bc ; и переходим к следующей строке
inc e ; в e - счётчик цикла
ld a, 128
cp e
jr nz, .loop ; повторяем цикл
; скроллим в бесконечном цикле
.lp1 call raster
call scroll
jp .lp1
quit ret
pend
savesna "test48.sna", entry
savebin "test48.bin", entry, pend - entry
А зачем патчить код? Рочему бы не делать вызов по указателю? Или спектрумовский процессор такого не может?
Panzerschrek[CN]
> Рочему бы не делать вызов по указателю?
Дело в том, что это инструкция JP (HL), а HL уже занят под возврат в вызывающий код как раз для краткости этой же однобайтовой инструкцией, поэтому одно другому начинает мешать и проще патчить JP START_ADDR.
Это как раз вот этот фрагмент кода:
...
inc hl ; hl = адрес куда надо перейти в буфер
ex de, hl ; de <-> hl
di ; запрещаем прерывания
ld sp, hl ; sp = затравочное для текущей строки
ex de, hl ; de <-> hl (восстанавливаем в hl адрес перехода)
ld (.jumper+1), hl ; устанавливаем распрыжку на текущую строку
ld hl, .raster_end ; запоминаем в hl адрес возврата из бэкбуфера
.jumper jp 0 ; адрес в распрыжке обновляется кодом выше
#499 (Правка: 14:32)
14:31, 13 янв. 2019
Кстати, никогда не мог понять почему в Zilog решили в мнемонике JP (HL) записывать HL в круглых скобках.
Везде в их синтаксисе круглые скобки были признаком индирекции.
Метки всегда были только числовыми значениями адресов к которым метки были прикреплены.
Так ld hl, label помещало в hl именно адрес за которым закреплена метка, её числовое значение. Т.е. просто помещение в регистр константы. В терминах ассемблера Intel это было бы mov eax, offset label, что, как по мне, запутывает.
ld hl, (label) же у Z80 загружает в hl значение из памяти по адресу label (в терминах Intel это mov eax, label). И всегда когда нужно загрузить из адреса - адрес окружается скобками. ld a, (hl) - то же самое, в аккумулятор грузится байт по адресу hl.
Но какого чёрта JP (HL) записывается как через индирекцию, хотя совершаемое действие в точности соответствует ld pc, hl - вот тут мне всегда было непонятно. Это полностью выбивается из схемы, ведь никогда не пишется JP (label) - нет такой команды вообще.
Какие то консоли, даже в чём то опередили своё время.
История Virtual Boy
KPG
Я когда покопался в вопросе, то даже ошалел.
3D-очки и виртуальность (только настоящие, а не как у виртуал боя) ныне преподносятся, как что-то только-только вылупившееся из яйца для потребительского рынка.
Но на самом деле еще во времена DOS были виртуальные решения для розничных покупателей, работающие и для Doom и Quake и так далее.
Более того - всевозможных фирм и их шлемов пытающихся окучить розничный рынок виртуальности с начала 90-х годов было не меньше десятка!
Не у всех правда было позиционирование, так что некоторые решения сводились к простой стереоскопии, но даже вон в том раннем шлеме под Doom можно было головой вертеть.
Проблем конечно было много - цена от штуки долларов и низкое разрешение, но они пытались.
Поэтому когда Oculus ворвался с обещаниями современного шлема за $300, то только в цене и качестве была новизна.
А Virtual Boy - это очень странная маркетинговая отрыжка.
Упс, перепроверил на Spectaculator - на самом деле скроллер, который я описал выше даёт не ~128 фпс, а где то в два раза ниже - почему то Unreal Speccy на котором я проверял как бы разогнан в два раза. Ума не приложу почему.
0iStalker
> А FUSE что показывает?
То же что и Spectaculator - примерно в 2 раза меньший фпс, чем было у UnrealSpeccy. Думаю, что в последнем просто включен режим турбо у Scorpion, это как раз ровно в 2 раза большая тактовая частота. Но в конфиге так вот сходу не могу найти этого, всё-таки там изрядная доля красноглазия в подходе к настройкам и GUI.
Переделал код код так чтобы он проматывал в скроллируемой области содержимое памяти спектрума и чтобы можно было в коде настраивать ширину и высоту скроллируемого участка.
А так же по совету Дениса Грачёва - того самого автора Old Tower (и многих других игр на спектруме) - применил технику измерения временных промежутков с помощью синхронизации с прерыванием и покраской бордюра, так что по ходу луча прямо видно по высоте закрашенных зон бордюра сколько времени занимает тот или иной фрагмент кода и где в момент его выполнения находился луч электронно-лучевой трубки кинескопа. При этом код получается синхронизирован с VDraw (VSync=on) и пашет ровно 50 кадров в секунду, скроллинг абсолютно при этом плавный, без вертикальных разрывов - просто идеальный.
+ код
− Скрыть
device ZXSPECTRUM48
org $8000
entry jp start
;include "screen.inc"
;include "math.inc"
;include "zstr.inc"
;include "keys.inc"
line_count = 128
push_count = 10
line_size = 4 * push_count + 3
back_buf_size = line_size * line_count
last_row = 31 - (32 - 2 * push_count) / 2
first_line = (192 - line_count) / 2
back_buf dup line_count ; 128 строк
ld sp, 0 ; SP=конец строки
dup push_count ; 16 раз LDPUSH
ld de, $
push de
edup
edup
jp back_buf
cur_line dw back_buf ; указатель на текущую (первую на экране) строку
; в теневом буфере
cur_line_num db 0 ; номер текущей строки в теневом буфере
; locate_scr_byte - найти адрес полоски пикселей на экране
; in: d=x (0-31), e=y (0-191)
; out: hl=адрес полоски пикселей
; pattern: 010YYYYY YYYXXXXX
locate_scr_byte ld a, e
rlca
rlca
and $E0
or d
ld l, a ; в l нижний байт адреса
ld a, e
and 7
or $40
ld h, a
ld a, e
rrca
rrca
rrca
and $18
or h
ld h, a ; в h верхний байт адреса
ret
sp_backup dw 0 ; времянка для SP
; raster - нарисовать текущий теневой буфер начиная со строки cur_line
raster push hl
push de
ld hl, 0
add hl, sp ; hl = sp
ld (sp_backup), hl ; сохранили sp
ld hl, (cur_line) ; патчим текущую строку в бэкбуфере
ld (hl), $E9 ; прописываем в ней опкод возврата из бэкбуфера jp (hl)
inc hl
ld e, (hl)
inc hl
ld d, (hl) ; dl = затравочное sp для текущей строки
inc hl ; hl = адрес активной части текущей строки
ex de, hl ; de <-> hl
di ; запрещаем прерывания
ld sp, hl ; sp = затравочное для текущей строки
ex de, hl ; de <-> hl (восстанавливаем)
ld (.jumper+1), hl ; устанавливаем распрыжку на текущую строку
ld hl, .raster_end ; запоминаем в hl адрес возврата из бэкбуфера
.jumper jp 0 ; адрес в распрыжке обновляется кодом выше
.raster_end ld sp, (sp_backup) ; первым делом восстанавливаем sp
ei ; разрешаем прерывания
ld hl, (cur_line) ; патчим обратно текущую линию
ld (hl), $31 ; восстанавливаем опкод ld sp, imm16
pop de
pop hl
ret
scroll_temp dw 0 ; временная переменная для scroll
; scroll_down - "скроллим" теневой буфер вниз на один пиксель/строку
scroll_down push hl
push de
push bc
; сохраним затравку SP в первой строке в temp
ld hl, (back_buf + 1)
ld (scroll_temp), hl
; начинаем цикл переносящий затравки SP вниз по кольцу
; HL указывает на затравку SP второй строки
ld hl, back_buf + line_size + 1
ld a, line_count - 1 ; в цикле line_count - 1 раз
ld de, back_buf + 1
.loop ldi
ldi
ld bc, line_size - 2
add hl, bc
ex de, hl
add hl, bc
ex de, hl
dec a ; цикл по a...
jp nz, .loop ; ...line_count - 1 раз
ld hl, (scroll_temp)
ld (back_buf + back_buf_size - line_size + 1), hl ; последняя строка обновляется из temp
; декремент cur_line
ld hl, (cur_line)
ld de, line_size ; декремент на одну строку
or a
sbc hl, de
ld (cur_line), hl
; декремент cur_line_num
ld a, (cur_line_num)
dec a
ld (cur_line_num), a
; проверим на выход за границу...
cp 255
jp nz, .exit ; если не ушли ниже нуля, то на выход
ld a, line_count - 1 ; помещаем в a верхнюю границу
ld (cur_line_num), a ; зануляем cur_line_num
ld hl, back_buf + back_buf_size - line_size
ld (cur_line), hl ; cur_line указываем на последнюю строку
.exit pop bc
pop de
pop hl
ret
start ; инициализируем затравочные значения SP
ld hl, back_buf + 1 ; позиционируемся на адрес SP в инструкции первой строки
ld d, last_row ; last_row - последняя строка полосок экрана
ld e, 0 ; отсчитываем с нулевой строки на экране
.loop push hl
call locate_scr_byte ; вычисляем в hl адрес последней полоски в строке e на экране
inc hl ; и нам нужен следующей за ней адрес в SP
ld bc, hl ; запоминаем в bc затравочное SP
pop hl ; восстанавливаем в HL указатель на данное в инструкции
ld (hl), c
inc hl
ld (hl), b ; сохраняем в инструкцию затравочное SP из bc
ld bc, line_size-1 ; в bc инкремент (-1, т.к. не делали inc hl)...
add hl, bc ; и переходим к следующей строке
inc e ; в e - счётчик цикла
ld a, line_count
cp e
jp nz, .loop ; повторяем цикл
; скроллим в бесконечном цикле
ld hl, 0
.lp1
halt ; ждём наступления прерывания
ld a,1 : out (254),a ; бордюр в синий цвет
call raster
ld a,7 : out (254),a ; бордюр в белый цвет
call scroll_down
ld a,1 : out (254),a ; бордюр в синий цвет
;jp .lp1
;обновляем верхнюю строку теневого буфера новыми данными
ld de, (cur_line)
inc de
inc de
inc de
inc de
ld a, push_count
.lp2 ldi
ldi
inc de
inc de
dec a
jp nz, .lp2
ld a,7 : out (254),a ; бордюр в белый цвет
jp .lp1
quit ret
pend
savesna "test48.sna", entry
savebin "test48.bin", entry, pend - entry
line_count - это как раз число строк скроллируемой области, а push_count - число push de, то есть число выводимых знакомест деленное на два.
поигрался параметрами и при line_count = 128 и push_count = 10 получается такая картинка:

Всё это чинно и плавно плывёт вниз.
А вот теперь внимание на бордюр - первая сверху-вниз синяя зона по вертикали отсчитывает сколько времени в единицах отрисованных строк занимает вывод LD:PUSH-буфера на экран. Если соизмерить эту зону с зоной отрисовки, то видно, что вывод этот даже немного обгоняет ход луча.
Следующая белая зона бордюра измеряет сколько времени отнимает обновление констант в инструкциях LD SP, xxxx в LD:PUSH-буфере для следующего кадра. Весомо, хотя обновляется всего 256 (128*2) байт памяти (крутится в цикле). LD:PUSH за соизмеримое время успевает перебрасывать 2560 байт - в 10 раз больше!
Вот она - сила LD:PUSH!
Следующая крохотная синяя полоска - это фрагмент обновления верхней линии LD:PUSH-буфера - просто линейно заполняется новой порцией данных из указателя бегущего по памяти ПК.
Как видно здесь уже луч ЭЛТ находится ниже активной области прокрутки, а это значит, что в коде далее можно выводить в эту зону спрайты, не боясь создать мерцания изображения.
Более того - эти спрайты не надо стирать, ибо LD:PUSH всё перетирает каждый цикл начисто.
Т.е. в целом, можно в такой зоне сделать например top-down шутер с идеально плавной прокруткой.
#506 (Правка: 15:17)
15:17, 14 янв. 2019
P.S.
А хотя картинка немного сложнее.
Прерывание срабатывает не прямо на границе экрана, а как будто бы 64 строками выше первой строки экранного изображения (это ровно половина скроллируемой области на картинке выше), то есть над первой строкой экранной области есть еще как бы 64 строки закрашенных синим - их надо включать во время LD:PUSH.
Ну и после нижней строки экрана есть еще как бы 56 виртуальных строк "vblank" (хотя в vblank формально входят и 64 верхних "виртуальных" строки, но вот так генерируется прерывание). Имея это ввиду уже можно реально оценивать какие там есть пространства для манёвров.
=A=L=X=
> Прерывание срабатывает не прямо на границе экрана
На отечественных клонах прерывание генерируется в 0й строке телевизионного растра - это, как раз, в большистве случаев, за 64 строки до первой строки фреймбуфера. На фирменных машинах, ULA генерирует прерывание в 16й строке телевизионного растра. Если у тебя эмулируется Пентагон/Скорпион, то таки да, 64-ю строками выше.
0iStalker
> На фирменных машинах, ULA генерирует прерывание в 16й строке телевизионного растра.
Я вот тут смотрел: https://www.worldofspectrum.org/faq/reference/48kreference.htm
48K ZX Spectrum
...
After an interrupt occurs, 64 line times (14336 T states; see below for exact timings) pass before the first byte of the screen (16384) is displayed...
По идее любая машина не соблюдающая этого правила пролетает по всем ухищрениям с цветами типа того же раскрашенного бордюра или мультколора.