Архитектура и программирование Vectrex

— А видеовыход у него есть?
— И как ты себе это представляешь?
(из разговора о Vectrex)


Vectrex выпускался GCE в 1982 — 1983 гг. и представляет собой игровой компьютер (приставку) ключевая особенность которой, векторный дисплей, делает его одним из самых необычных и интересных 8-разрядных компьютеров. С некоторой натяжкой можно сказать, что он является упрощённой версией векторных игровых автоматов Cinematronics, технически более совершенных.

В качестве процессора в Vectrex используется Motorola 6809 — он похож на MOS 6502/6510, но добавлены 16-битные регистры, дополнительные режимы адресации, умножение.
Тактовая частота — 1.5MHz.

Поскольку компьютер был выпущен как игровая приставка и игры для него продавались на картриджах, программа размещается в ПЗУ картриджа (32 кб), а ОЗУ — совсем крохотное (1 кб — две штуки 2114) и предназначено больше для данных.
Также есть встроенное ПЗУ с BIOS'ом (8 кб — одна 2363), который включает набор подпрограмм для рисования векторов и вывода текста, несколько примитивных мелодий и даже одну игру — Minestorm (многим известную как Asteroids).

Звук реализован на чипе AY8912 (также используется в MSX2 и поздних ZX Spectrum) однако, кроме этого существует штатная возможность проигрывания 8-битного звука через ЦАП (практическое применение этого способа, впрочем, ограничено).

Vectrex выполнен в виде моноблока (включающего ЭЛТ экран), но клавиатура не предусмотрена в принципе. Управление осуществляется двумя джойстиками (в т.ч. аналоговыми). Кроме того, может быть подключено световое перо и очки 3D Imager.

С 1982 г. по нынешнее время для Vectrex написали примерно полторы сотни игр, несколько серьёзных программ (типа редакторов графики, музыки, анимации), а также около десятка демо и интро. Интересно, что более половины игр (и все демо) выпущены после 1995 года, т.е. через десятилетие после прекращения производства и поддержки Vectrex. Возрождение платформы связано, в первую очередь, с появлением хороших эмуляторов, которые сделали разработку доступной любому желающему. Сами компьютеры пока также вполне доступны на eBay.


ВЕКТОРНЫЙ ДИСПЛЕЙ

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

Векторные дисплеи, сейчас практически не встречающиеся и мало кому известные, были распространены в 1970-е.
Основное отличие от всем известных растровых заключается в отсутствии автоматической развёртки. Луч не бегает по строчкам сам. Его перемещение управляется программой — код, который вы напишите, определяет направление, скорость, длительность и яркость, с которой будет перемещаться луч. Как следствие — у векторного дисплея нет пикселов, а, следовательно, и нет понятия «разрешение» (по крайней мере, в привычном понимании).
Фактически, такой дисплей представляет собой осциллограф, к горизонтальному (X), вертикальному (Y) и каналу яркости (Z) которого подключены цифро-аналоговые преобразователи.

Чтобы получить на экране линию, необходимо не просто переместить луч из одной точки в другую, а сделать это равномерно и с нужной скоростью. Затем луч можно погасить и переместить в другую точку, где зажечь и переместить в третью, и т.д. Таким образом получим некую фигуру.

После завершения перемещения люминофор будет какое-то время светиться и линия будет видна. Однако, это послесвечение длится недолго.
По этой причине всё, что требуется, включая выполнение кода, задержки вызванные обращением к периферии и саму отрисовку линий, нужно выполнить за ограниченное время.
Если линий будет много, изображение начнёт мерцать. Это делает невозможным создание закрашенных фигур (разве что совсем крохотных), а сколько-нибудь серьёзные вещи придётся писать на ассемблере т.к., даже если представить себе идеально оптимизирующий компилятор, рассчитать все временные задержки при перемещении луча, которые будут происходить в выдаваемом им коде, проблематично — одна лишняя загрузка в регистр в неудачном месте может совершенно исказить изображение.

Если для традиционных 8-битных платформ (типа Commodore VIC-20, C64 или ZX Spectrum) вполне можно программировать, ни разу не посмотрев на схему компьютера, в случае с Vectrex понимание того, как работает часть формирующая изображение — необходимо.
Конечно, можно пользоваться для отрисовки линий, простых фигур и вывода надписей готовыми подпрограммами BIOS, однако это не позволяет в полной мере использовать возможности устройства — рано или поздно всё равно придётся работать с железом напрямую.
По этой причине, знакомство с разработкой будет тесно переплетено с описанием устройства компьютера.

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

В данном контексте нас интересуют следующие компоненты Vectrex'a (в скобках приведены номера на блок схеме):

MOS 6522 (IC207) — универсальный адаптер интерфейсов (VIA — Versatile Interface Adapter) — этот чип отображается на область памяти $D000...$D00F. Соответственно, запись по этим адресам значений (например, командами процессора STA $D00x) приводит к изменению его регистров.
6522 содержит, в частности, порты ввода-вывода, два таймера, сдвиговый регистр (shift register).

Подробности лучше посмотреть в документации на чип (см. ссылку на архив в конце статьи), но суть состоит в том что, записывая командами микропроцессора значения в упомянутые выше ячейки памяти, мы устанавливаем торчащие из чипа ножки в 0 или 1 (а также наоборот — можем проверить, поданы ли на них 0 или 1).

MC 1408 (IC301) — 8-разрядный цифро-аналоговый преобразователь (DAC, ЦАП). Преобразует код, поступающий на него с чипа 6522 в соответствующий уровень напряжения. С точки зрения программирования диапазон напряжений соответствует цифрам -128… +127 (а не 0...255!)

LF347 (IC303) — схемы выборки-хранения s&h (store & hold) на операционных усилителях. Сохраняют на выходе напряжение (в т.ч. после того, как оно будет снято с входа). Их две — по каналам Y и Z (для X не нужна, пояснение ниже).

CD 4052 (IC302) — аналоговый мультиплексор с цифровым управлением (mux). Взависимости от кода на его цифровых входах (которые подключены всё к тому же 6522) пропускает входное напряжение с ЦАП-а на один из нескольких своих выходов.

4066 (IC305) — аналоговые ключи. Управляемые (тем же 6522) выключатели, позволяющие пропускать или не пропускать через себя напряжение.

LF247 (IC303) — интеграторы на операционных усилителях (их два — по X и по Y, соответственно). Преобразуют входной прямоугольный сигнал, амплитуда которого задана кодом на ЦАП-е, в изменяющееся напряжение, заставляющее луч плавно перемещаться из одной точки в другую, оставляя на экране светящийся след.

Далее идёт электронно-лучевая трубка с отклоняющей системой (по горизонтали и вертикали) на которую через усилители подаётся напряжение с интеграторов и, отдельно, напряжение управляющее яркостью луча.
При отсутствии напряжения на отклоняющих системах луч находится в центре экрана. При максимально допустимом напряжении на любой из них — за пределами экрана.



(для более подробного рисунка см. блок-схему и схему)

Рисование осуществляется примерно следующим образом:

Загружая в определённые регистры 6522 значения, мы можем устанавливать на выходе ЦАП нужное напряжение. Но ЦАП всего один, а нам нужно выставить три напряжения — X (направление по горизонтали -128...+127), Y (направление по вертикали -128...+127) и Z (яркость 0...$7F).
Для этого после установки каждого напряжения нужно переключить мультиплексор, чтобы напряжение было передано на нужный выход. С каналами Y и Z в этом отношении всё просто, а вот канал X идёт (явно для упрощения схемы) в обход мультиплексора.
Т.е., устанавливая Y или Z мы всегда одновременно устанавливаем и X!

Поэтому поступаем так:

1. Записываем в ЦАП яркость, переключаем мультиплексор на вывод в канал Z. Напряжение сохраняется на схеме sample & hold (s&h) канала Z.

2. Записываем в ЦАП Y, переключаем мультиплексор на вывод в канал Y. Напряжение сохраняется на схеме s&h канала Y.

3. Выключаем мультиплексор и записываем в ЦАП X (s&h тут не нужна, так как напряжение сохраняется на самом ЦАП)

Канал Z нам больше не интересен (яркость постоянна), а вот с X и Y разбираемся дальше.

Итак, напряжения по X и Y с выходов схем s&h у нас поданы на аналоговые ключи. Через 6522 (выход PB7) мы подаём на эти ключи сигнал RAMP. Ключи одновременно открываются и оба напряжения попадают на соответствующие интеграторы — по X и по Y.

На выходе интеграторов, соответственно, получаем изменяющиеся напряжения. Они меняются либо от предыдущего значения, оставшегося на конденсаторе интегратора (помните, мы куда-то там до этого поставили луч?), либо от нуля (если ранее интеграторы сбросили в ноль, подав на них через тот же 6522 сигнал ZERO — конденсатор разрядится).



Интегрирование идёт, напряжение меняется, луч движется по экрану и оставляет след за счёт послесвечения люминофора. Когда надоест, мы можем его остановить, отключив напряжение от интеграторов уже упомянутым сигналом RAMP.
Таким образом линия нарисована, а остаток напряжения на интеграторах соответствует её концу (и началу следующей, если понадобится).

Возникает вопрос — в какой момент отключать напряжение? В принципе, это ваше дело. Вы можете просто посчитать, какой длины нужен вектор и вбить задержку в нужное число тактов подходящими командами.
Однако, на практике обычно применяется другой способ — задействуется таймер 1 в 6522. В таймер заносится некое значение, не слишком удачно названное «scale» (масштаб) и начинается обратный отсчёт. Когда значение достигнет нуля, сигналом RAMP интегрирование будет автоматически остановлено. Т.е., достаточно выставить и запустить таймер, луч остановится сам. Однако, тут есть проблема — луч остановится, но как об этом узнать, чтобы начать рисовать следующий?
Для этого придётся в цикле проверять один из регистров 6522, где после завершения счёта установится флажок. По сути, получается ожидание впустую, поэтому это время в цикле иногда используют для выполнения каких-нибудь полезных вычислений.

Помимо сплошной линии есть достаточно кривая возможность рисовать пунктирную. Для этой цели используется сдвиговый регистр (shift register) в 6522. Заносим туда необходимый паттерн (к примеру, $AA = %01010101) и говорим 6522, что сдвиг должен происходить автоматически. При сдвиге каждый бит выползает на сигнал BLANK и, таким образом, луч сам включается на единицах и выключается на нулях. Проблема в том, что после 8 сдвигов в регистре остаются одни нули и весь наш замечательный пунктир обрывается. Чтобы этого не происходило, необходимо снова и снова заносить туда значение pattern. Делается это в вышеупомянутом цикле ожидания окончания интеграции. В таких условиях получить именно тот пунктир, какой хочется — весьма непросто.
Впрочем, именно этот регистр используется BIOS'ом для функции вывода текста (т.е., фактически, стандартные символы — растровые, просто рисуются прерывистыми горизонтальными векторами).

Из всего вышеописанного следует три важных момента:

1.Рисование происходит не по абсолютным координатам, а по относительным. Следующее перемещение отсчитывается от конца предыдущего и вектор имеет некую длину в некотором направлении (кстати говоря, это позволяет совершенно штатно выводить изображение за границы видимой части экрана — к примеру, для реализации скроллинга).
Из-за различных утечек с каждым перемещением быстро растёт погрешность (единицы сантиметров на десять тысяч тактов), поэтому в начале каждого «кадра» (серии рисований) луч выставляют в центр экрана.

2.Длина и направление вектора зависит от сочетания scale и напряжений по X и Y. В некотором смысле, scale задаёт длину, а X и Y направление (но при этом также влияя на длину). Можно сказать, что на рисунке с графиком scale задаёт время от A до B (или от B до C), а значение X (или Y) подаваемое на ЦАП — наклон отрезков на нижней части графика (говоря иначе — скорость изменения напряжения).

3.Поскольку scale — время перемещения луча, оно должно быть по возможности минимальным. Чем оно меньше, тем больше векторов можно успеть нарисовать, пока не начнётся мерцание.

Для немерцающего изображения в 50 кадров в секунду (50 гц для любого Vectrex) необходимо со всеми рисованиями и вычислениями уложиться в 30000 тактов.

Если рисовать горизонтальные линии во всю ширину экрана в максимальном масштабе ($FF) и перед началом рисования каждой линии устанавливать луч в центр, а затем в точку начала линии, то в упомянутые 30000 тактов уложится примерно 60-70 линий, что весьма немного. Ясно, что при уменьшении масштаба (и, соответственно, уменьшении их длины) максимальное число линий будет расти.

Рисование прямой линии

Теперь разберём, как всё вышеописанное реализуется в коде (константы VIA_* из vectrex.i от исходников BIOS).

Практически любая программа для Vectrex представляет собой бесконечный цикл. Первым делом в нём всегда вызывается подпрограмма BIOS Wait_Recal, а затем уже всё остальное — необходимые вычисления, отрисовка всех векторов для данного «кадра», проигрывание музыки, опрос джойстиков и пр.
В отличие от традиционных растровых дисплеев, где луч обегает все строчки одинаковое для каждого кадра время, здесь всё иначе — ведь в одном кадре может выводится 10 линий, а в следующем только 5.
Для обеспечения равного времени выполнения всех итераций цикла («кадров») используется следующий метод:

Таймер 2 VIA 6522 программируется в режим one shot и в него заносится значение Vec_Rfrsh = 30000 ($7530). С момента занесения идёт обратный отсчёт. В начале Wait_Recal ожидаем, когда отсчёт закончится. Как только он закончился, осуществляем рекалибровку схем (а в таймер опять заносим то же значение). Смысл этого ожидания в том, чтобы итерация цикла гарантированно занимала не менее 30000 тактов. Если рисование и вычисления заняли меньше, время всё равно будет «дополнено» до 30000.
А вот если они заняли больше, это приведёт к негативным эффектам — во-первых, начнут гаснуть векторы нарисованные первыми, во-вторых, из-за поздней рекалибровки, могут возникнуть различные искажения.

Почему именно 30000? Значение выбрано из расчёта 50 Гц (т.е. на один «кадр» отводится 1/50 секунды), исходя из частоты процессора 1.5 MHz. Именно 50 Гц, по-видимому, выбрано чисто по традиции.

Wait_Recal состоит из нескольких последовательных процедур:
— Увеличивается счётчик Vec_Loop_Count (это просто переменная word)
— Ожидается когда закончит считать таймер 2 (и если закончил, запускается заново)
— Устанавливается максимальный масштаб (scale) = $FF
— Выключение обнуления интеграторов, выключение луча, луч перемещается в позицию x = $7F, y = $7F (левый нижний угол) из предыдущей своей точки (которая может быть какой угодно)
— Включается луч, очищается сдвиговый регистр (т.е. пустой паттерн для линий), включение «обнуления» интегратора (луч оказывается в центре)
— Выключение обнуления интеграторов, выключение луча, луч перемещается в позицию x = $80, y = $80 (правый верхний угол)
— Ещё раз включение обнуления интеграторов (луч снова оказывается в центре)
— Устанавливается ноль относительно нуля ЦАПа (см. ниже примечание от svo)

Каким образом помогает калибровке перемещение луча в углы экрана — неясно.

С практической точки зрения важно, что в результате этих действий, после вызова Wait_Recal луч у нас выключен и принудительно находится в центре (обнуление интеграторов включено — они не могут работать, даже если начать интегрирование), scale установлен в $FF.

Итак, исходник:
loop:
	jsr     Wait_Recal              ; ожидание таймера 2 и рекалибровка CRT

; Включаем луч и выключаем "обнуление" интеграторов (иначе луч не будет перемещаться)
	lda	#$CE 	                ; BLANK low, ZERO high		
	sta	<VIA_cntl            ; записываем в управляющий регистр 6522

;Устанавливаем нужный масштаб (scale):

	lda     #$ff			; $ff -- максимальный масштаб
	sta     <VIA_t1_cnt_lo	; записываем в младший байт таймера 1

;Устанавливаем яркость луча (Z):

	lda     #$7f                    ; $7f - максимальная яркость
	sta     <VIA_port_a          ; записываем значение в ЦАП

	ldb	#$04
	stb     <VIA_port_b          ; (4 в portb) включаем мультиплексор и переключаем его на канал 2 (sel0=0, sel1=1), чтобы напряжение попало в s&h по Z

	stb     <VIA_port_b          ; задержка

	ldb     #$05
	stb     <VIA_port_b          ; (5 в port b) выключаем мультиплексор (сигнал S/H с 6522)

	ldb     #100                    ; куда рисуем линию - X
	lda     #100                    ; куда рисуем линию - Y

	sta     <VIA_port_a          ; записываем значение Y в ЦАП
	clr     <VIA_port_b          ; (0 в port b) включаем мультиплексор и переключаем его на канал 0, чтобы напряжение попало в s&h для Y 


	inc     <VIA_port_b          ; (1 в port b) выключаем мультиплексор
	stb     <VIA_port_a          ; Записываем значение X в ЦАП и там оставляем (оно идёт дальше мимо мультиплексора, в отличие от Y и Z).

	lda     #$aa         	 	; паттерн для линии ($ff - сплошная, $aa - пунктир)

	ldb     #$40		        ; бит прерывания таймера 1

	sta     <VIA_shift_reg  	; записываем паттерн в регистр сдвига

	clr     <VIA_t1_cnt_hi  	; запускаем таймер, что автоматически начинает интегрирование (сигналом RAMP)

wait_timer:
	sta     <VIA_shift_reg  	; обновляем паттерн в регистре сдвига

	bitb    <VIA_int_flags	; проверяем бит прерывания таймера 1 ($40) - не вышло ли время соответствующее заданному scale
	beq     wait_timer	        ; если нет, обновляем паттерн снова. Если вышло, линия готова

	clr     <VIA_shift_reg       ; очищаем паттерн для линии (без этого в конце линии появится яркая точка)

; при желании рисуем ещё линии, играем музыку, выполняем вычисления и прочее

; ......

	bra 	loop		        ; и всё сначала

Обратите внимание, что паттерн записывается в регистр дважды — перед началом интегрирования и потом в каждой итерации цикла. Второе необходимо потому, что при сдвиге (автоматическом) регистр становится пустым и если значение не обновлять — линия станет невидимой. При обновлении же очень важно правильное соответствие сдвига и тактов, которые занимают команды в цикле. Если туда вставить даже один nop, видимый паттерн линии изменится.

Если рисуется несколько линий (к примеру, квадрат) то, конечно, «clr <VIA_shift_reg» имеет смысл ставить не после каждой линии, а один в самом конце.
В подпрограмме BIOS Draw_VL (вызывающей Drawline_d) этот момент обыгрывается довольно хитро: есть переменная Vec_Misc_Count, в которую заносится число линий, которые планируется нарисовать. И когда дорисована последняя, там не просто очищается паттерн, но ещё выполняется сброс интеграторов (т.е. частично выполняется код из Wait_Recal).

Если нужно быстро вернуть луч в центр экрана до завершения цикла и очередного вызова Wait_Recal, можно пользоваться «обнулением» интеграторов, не забывая его отключать:


; обнуляем интеграторы
	        lda     #$CC
                sta     <VIA_cntl       ; /BLANK low and /ZERO low

; ставим луч в центр, записывая нули в ЦАП
	        ldd     #$0302
                clr     <VIA_port_a     ; clear D/A register
                sta     <VIA_port_b     ; mux=1, disable mux
                stb     <VIA_port_b     ; mux=1, enable mux
                stb     <VIA_port_b     ; do it again
                ldb     #$01
                stb     <VIA_port_b     ; disable mux

; выключаем обнуление интеграторов
	        lda	#$CE 		   ; /Blank low, /ZERO high
        	sta	<VIA_cntl

Тоже самое делает подпрограмма BIOS Reset0Ref (плюс, там ещё очищается shift register), которая вызывается из Wait_Recal.
Существенно, что необходимо выполнять И обнуление интеграторов И запись нулей в ЦАП. Для эмулятора это без разницы, а вот на реальном Vectrex разница очень даже будет.

ЗАМЕЧАНИЯ:

Описанный вариант отрисовки линии намеренно избыточен. К примеру, масштаб и яркость совершенно необязательно устанавливать каждый раз, сплошная линия не потребует лишнего цикла с обновлением shift register. В ряде случаев куда оптимальнее (но менее наглядно) загружать сразу A и B инструкцией LDD, использовать clr, inc/dec и т.д. В реальной ситуации это делать придётся, т.к. экономит такты.



По-поводу «бессмысленных» инструкций задержки: после записи в порты 6522, в коде BIOS'а иногда выжидают несколько тактов. Иначе следующая запись может не пройти. Когда это нужно и когда нет — вопрос достаточно туманный. Судя по экспериментам (как моим, так и других людей) — не нужно вообще. Возможно, это требовалось для ранних экземпляров Vectrex.

Нужно понимать, что если хочется рисовать длинные непрерывные вектора (к примеру, во всю ширину экрана), то это возможно лишь при максимальном масштабе (scale). Однако, если он будет максимальным, это автоматически означает снижение «разрешения» в том смысле, что шаг в единицу по вертикали будет означать расстояние примерно в ширину яркой линии (т.е. между рядом параллельными линиями будет промежуток).
Если же делать длинные линии из нескольких коротких, места стыков будут заметны.
На практике имеет смысл постоянно переключать масштаб — в одном (большом) рисуется линия, в другом (маленьком) луч перемещается к началу рисования следующей.

Если нарисовать длинную (от -128 до 127) горизонтальную линию (в масштабе $FF) так, чтобы её левый конец был у правого края экрана, а правый конец далеко за экраном, то не получится передвинуть её влево так, чтобы правый конец оказался за левым краем экрана.
На первый взгляд здесь нет проблемы, т.к. перемещения луча всегда относительны. Однако, на настоящем Vectrex линия влево за край не уйдёт (причём, если уменьшать начальную координату линии плавно, то будет видно, как её движение влево будет замедляться и прекратится, не дойдя до нужной позиции около четверти экрана. Это связано с ограничениями на максимальную амплитуду напряжений на выходах интеграторов (эмулятор, к слову, этого не понимает).

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

В этой связи стоит упомянуть про точки — они рисуются простым включением луча (без интегрирования), выжидания в цикле некоторого времени, от которого зависит яркость и выключения луча (см. подпрограмму Dot_here).

Прямая линия с интегрированием вручную

В предыдущем разделе для начала перемещения луча мы запускали таймер, что автоматически запускало интегрирование. И затем ожидали окончания, проверяя в цикле, не закончил ли таймер считать.
Существует другой способ рисования линии, при котором таймер не используется: интегрирование запускаем вручную (установкой RAMP на ножке PB7 6522), а затем любым способом ждём нужное нам время, пока луч ползёт в заданном (X и Y) направлении. Взависимости от задачи, для ожидания могут быть использованы простые nop, либо цикл (в котором, в частности, можно обновлять значение shift register, если требуется штрих-пунктирная линия).
В конце рисования интегрирование прекращается (также вручную).

Код выглядит так:




loop:
                jsr     Wait_Recal

                lda     #$CE            ; (11001110) /Blank low, /ZERO high
                sta     <VIA_cntl       ; enable beam, disable zeroing

                clr     <VIA_shift_reg

; Пеоеключаем выход PB7 6522, чтобы интегрирование начинать вручную (а не с запуском таймера, как обычно)
                lda     #$18            ; (00011000) AUX: shift mode 4 (110). PB7 not timer controlled. PB7 is ~RAMP
                ldb     #$81            ; (10000001) disable MUX (bit0=1), disable ~RAMP (bit7=1), MUX set to channel Y
                stb     <VIA_port_b
                sta     <VIA_aux_cntl

; Заносим значение в ЦАП и включаем мультиплексор, чтобы оно оказалось на интеграторе канала Y (оно же
; неизбежно окажется и в канале X, т.к. X идёт мимо мультиплексора)
                lda     #127            ; задаём Y
                sta     <VIA_port_a

                ldb     #$80            ; (10000000) enable MUX (bit0=0), disable ~RAMP (bit7=1), MUX set to channel Y
                stb     <VIA_port_b     ; enable MUX, that means put DAC to Y integrator S/H

; Теперь выключаем мультиплексор и записываем в ЦАП значение X
                ldb     #127            ; задаём X
                lda     #$81            ; (10000001) disable MUX (bit0=1), disable ~RAMP (bit7=1), MUX set to channel Y (уже неважно)
                sta     <VIA_port_b
                stb     <VIA_port_a     ; store B (X_update) to DAC


; начинаем интегрирование
                ldb     #$01            ; (00000001) Disable mux (bit0=1), enable ~RAMP (bit7=0), MUX set to channel Y (уже неважно)
                stb     <VIA_port_b

; паттерн нужно задавать именно после начала интегрирования (чтобы до него он был пустой). Иначе получим в начале загнутый хвостик
                ldb     #$ff            ; паттерн для сплошной линии
                stb     <VIA_shift_reg

; выдерживаем паузу, во время которой луч идёт по экрану в выбранном выше (X,Y) направлении
                nop
                nop
                nop
                nop
                nop
                nop

                clr     <VIA_shift_reg     ; прекращаем рисовать линию, задав пустой паттерн

; окончание интегрирования
                ldb     #$81              ; (10000001) disable MUX (bit0=1), disable ~RAMP (bit7=1), MUX set to channel Y
                stb     <VIA_port_b        

; эта задержка нужна в том случае, если следуюшая линия должна начаться с места окончания этой.
; Без задержки между ними будет неизвестный науке разрыв
                nop
                nop
                nop
                nop

; восстанавливаем обычное управление началом интеграции - по таймеру
                lda     #$98            ; (10011000) AUX: shift mode 4 (110). PB7 timer controlled (bit7=1). PB7 is ~RAMP
                sta     <VIA_aux_cntl

                bra     loop


Поскольку таймер здесь не используется, задание масштаба (scale) не имеет смысла и ни на что не влияет. Направление и длина линии полностью определяются значениями X,Y и задержкой между началом и окончанием интегрирования.

В BIOS подобный подход (без таймера) используется при выводе символов (подпрограмма Print_Str).

Рисование кривой

С рисованием прямых векторов, если разобраться, всё обстоит достаточно просто. Но что делать, если требуется нарисовать кривую? Специальных аппаратных возможностей для этого Vectrex не имеет.
Более того, вы даже не можете произвольно перемещать луч — необходимо переключать каналы мультиплексора, причём один из каналов (X) вообще идёт мимо него.



С официальной точки зрения рисовать кривые нельзя. В BIOS нет ни одной подпрограммы, которая бы делала что-то похожее. Однако, если делать всё вручную, кривые, с рядом серьёзных ограничений, нарисовать всё же удаётся.

Для этого, как и в предыдущем случае, от запуска интегрирования по таймеру и проверки его окончания придётся отказаться.

Запуск производится вручную (сигналом RAMP) и, после того как луч начал движение, мы в нужные моменты начинаем писать значения в каналы X и Y, в результате чего луч вынужден менять направление. Помимо, опять же, необходимости чётко вычислять необходимые для записи моменты, серьёзным ограничением является необходимость переключения мультиплексора между каналами. На практике это приводит к тому, что с каналом X всё хорошо, т.к. он идёт в обход мультиплексора (поэтому отклонения луча по горизонтали получаются чистые и аккуратные). А вот с каналом Y всё плохо.
Чтобы в него записать, необходимо включить мультиплексор (активировать выход S/H 6522 ведущий на DIS MUX мультиплексора), записать значение и снова выключить мультиплексор что, вероятно, приведёт к остановке интегрирования.

Рассмотрим простой пример, в котором рисуется кривая с отклонением только по горизонтали (X):


loop:
                jsr     Wait_Recal        ; калибровка

                lda     #$7f
                sta     <VIA_t1_cnt_lo ; масштаб (здесь действует только на Moveto_d)

                lda     #-120   ; Y
                ldb     #0                ; X -127
                jsr     Moveto_d          ; перемещаем луч в точку начала кривой

; режим с интегрированием вручную и выключение mux
                ldd     #$1881
                stb     <VIA_port_b    ; poke $81 to port B: disable MUX, disable ~RAMP
                sta     <VIA_aux_cntl  ; poke $18 to AUX: shift mode 4. PB7 not timer controlled. PB7 is ~RAMP

; Значение Y, к которому начнёт двигаться луч
                lda     #127              ; Y
                sta     <VIA_port_a        ; записываем в ЦАП

                decb                      ; B now $80
                stb     <VIA_port_b        ; enable MUX, that means put DAC to Y integrator S/H

; интегрирование должно начинаться когда в ЦАП уже есть какой-то X, иначе в начале будет прямой отрезок линии
                ldb     #0                ;  X start
                inc     <VIA_port_b    ; MUX off, only X on DAC now
                stb     <VIA_port_a    ; store B (X_update) to DAC

; начинаем интегрирование
                ldb     #$01              ; load poke for MUX disable, ~RAMP enable
                stb     <VIA_port_b    ; MUX disable, ~RAMP enable

; задаём паттерн (сплошная линия)
                ldb     #$ff
                stb     <VIA_shift_reg

; рисуем, собственно, кривую. Для каждого сегмента записываем в ЦАП новое значение X. На практике, конечно,
; удобнее это делать в цикле и значения брать из таблицы. Общее время выполнения записей и промежуточных
; вычислений влияет на форму и длину кривой

                lda     #$10
                sta     <VIA_port_a    ; первый сегмент

                lda     #$20
                sta     <VIA_port_a    ; второй сегмент

                lda     #$30
                sta     <VIA_port_a    ; третий сегмент

; прекращаем интегрирование
                ldb     #$81              ; load value for ramp off, MUX off
                stb     <VIA_port_b    ; poke $81, ramp off, MUX off

; в конце кривой будет заметен темный кончик. С этой задержкой, по крайней мере, не будет дырки перед ним
                nop
                nop
                nop
                nop

                clr     <VIA_shift_reg ; очищаем паттерн

; восстанавливаем обычный режим таймера (enable PB7 timer, SHIFT mode 4)
                lda     #$98
                sta     <VIA_aux_cntl

                bra     loop



Фактически, приведенный пример аналогичен примеру из предыдущего раздела (прямая линия с интегрированием вручную), просто здесь в процессе рисования мы еще и в DAC значение X пишем.



Что касается использования shift register, здесь есть сложности. Пунктирную линию сделать можно, но точность пунктира будет условной, т.к. проблематично изменять и X и значение shift register в точности тогда, когда это нужно.

Если через равные промежутки времени писать в X одинаковые значения (например 10,10,10), изменение будет линейным — т.е. получится наклонная прямая.

От задержек между записью значений зависит гладкость кривой и её длина. Если задержки большие, получится просто ломаная линия. Поэтому в конкретных ситуациях будет иметь значение любая лишняя инструкция, а циклы, вполне вероятно, придётся разворачивать. Кроме того, получившаяся кривая может зависеть от конкретного экземпляра вектрекса (из-за отличия параметров аналоговых компонентов/цепей), хотя добиться примерной схожести на разных экземплярах — возможно (правда неясно, где взять столько Vectrex'ов для тестирования).

Описанная технология использовалась на практике, хотя и редко. Наиболее известный пример — дорога в игре Pole Position. Другой — моя intro «Electric Force» для CC'2015.

Можно ли менять яркость в процессе рисования линии (прямой или кривой)? В практических целях — вряд ли. Яркость меняется через тот же ЦАП — так же, как и с каналом Y, понадобится переключение мультиплексора (с соответствующими побочными эффектами). Кроме того, на это будет уходить время, так что длина сегмента линии одинаковой яркости будет слишком большой.

Растр из векторов

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



1. За доступные 30000 тактов (чтобы изображение было стабильным) слишком много линий развёртки не нарисуешь.

Фактически, при максимальной длине линии их число (назовём это вертикальным разрешением) будет измеряться десятками. Причём, если scale будет максимальный (т.е., если мы хотим растр шириной в экран), то линий успеет нарисоваться меньше. Более того — нужно учитывать, что после рисования каждой линии мы должны как-то перейти к началу следующей. Даже если мы рисуем во время хода луча обратно (создавая себе проблемы при адресации точек), при максимальном scale не получится нарисовать несколько горизонтальных линий вплотную — между ними будут значительные промежутки. Чтобы этого избежать, можно уменьшать scale перед перемещением луча по вертикали и возвращать обратно на максимум перед рисованием линии. Это переключание также займёт время.
Можно отказаться от растра на всю ширину экрана и изначально задать scale поменьше. Тогда, во-первых, решается проблема с промежутками по вертикали, во-вторых уменьшается время рисования линий.

2. Если не рекалибровать схему после каждой линии, всё это дело съезжает в сторону.



Фатальность проблемы зависит от выбранного scale (т.е., чем дольше рисуется луч, тем сильнее уплывают параметры интеграторов) и от конкретного экземпляра Vectrex. На моём экземпляре при растре во всю ширину экрана уже вторая линия начинается на миллиметр левее, так что о стабильном растре говорить не приходится. Уменьшение scale несколько снижает остроту проблемы, но не снимает её.
Решением является рекалибровка (обнуление интеграторов и ЦАПа) после каждой линии. Это даёт стабильный растр но, разумеется, ценой лишних тактов.
На эмуляторе (ParaJVE) уплывание параметров аналоговых элементов схемы отсутствует, поэтому ориентироваться на него в этом плане нельзя.

3. Включать и выключать луч в процессе его движения нужно очень быстро и точно.

Приемлимая скорость возможна лишь при использовании shift register.
Причём, если при рисовании пунктирной линии равномерность пунктира мало кого волнует, то при выводе изображения регулярная потеря точек или лишние промежутки, особенно в совокупности с другими проблемами — недопустимы.
Подход, с таймером тут не срабатывает, поэтому нужно использовать описанный ранее запуск интегрирования вручную после чего, через точно рассчитанные промежутки времени (так, чтобы каждые 8 «пикселов» правильно стыковались друг с другом) shift register обновляется нужными значениями. Соответственно, окончание рисования линии определяется также вручную — программным счётчиком.
Именно так работает подпрограмма BIOS Print_Str, при помощи которой обычно выводятся на экран текстовые строки. На практике, однако, есть масса ньюансов — достаточно посмотреть в код Print_Str. Кстати говоря, в ней для экономии тактов после каждой строки луч не устанавливается в ноль. Из-за чего почти любая строка (длиннее нескольких символов, взависимости от масштаба) выводится искажённой, как минимум на некоторых экземплярах Vectrex.

4.Как уже было замечено ранее, переключать яркость во время рисования линии проблематично. Однако ничто (кроме необходимости уложиться в 30000 тактов) не мешает рисовать 2-3 растра разной яркости, накладывая один на другой.

Звук

Несмотря на почтенный возраст, со звуком в Vectrex дела обстоят неплохо. Есть даже два независимых способа его воспроизводить — через чип AY8912 (т.е. аналогично Yamaha MSX2 и продвинутым версиям ZX Spectrum) и через ЦАП (т.е. проигрывая сэмплы).

Применение сэмплов ограничено скромным размером адресуемой памяти и производительностью процессора — слишком много тактов будет расходоваться на звук. Я использовал оцифровку короткой фразы в своей интро Invitron. Общий смысл в том, что выбирается соответствующий канал мультиплексора (sel0=1, sel1=1) и далее значения (8 бит, со знаком) пишутся в ЦАП.

Выглядит это так:


                lda     #%10000110    
                sta     <VIA_port_b     ; enable mux, set mux to sound channel (%11)

                ldx     #sample         ; sample address
                ldy     #23570          ; sample length (bytes)

next:

                lda     ,-y
                lda     ,x+            	; получаем очередной байт
                sta     <VIA_port_a     ; пишем его в ЦАП
                cmpy    #$0000
                beq     done

                ldb     #$19            ; задержка. $19 для 8КГц
delay:
                decb
                cmpb    #$00
                bne     delay

                jmp     >next

done:

....

sample:
	        db xx, xx, xx, ...


Преобразовать сэмпл к нужному виду (в данном случае signed 8 bit, mono, 8khz) можно так:
ffmpeg -i input.wav -acodec pcm_s8 -ar 8000 -ac 1 output.au (не забыв потом отрезать заголовок)

Что касается проигрывания музыки через AY8912, то это основной способ, не слишком затратный по тактам. Даже в BIOS'е есть встроенные средства для воспроизведения примитивных мелодий (и небольшой их набор). Проиграть мелодию оттуда можно так:


                inc     Vec_Music_Flag
loop:
                jsr     DP_to_C8
                ldu     #$fef8          ; адрес данных мелодии в ROM

                jsr     Init_Music_chk  ; инициализация
                jsr     Wait_Recal      ; стандартное ожидание следующего кадра и рекалибровка
                jsr     Do_Sound	; проигрывание

                tst     Vec_Music_Flag  ; проверка доигралась ли мелодия
                beq     endmusic

                jsr     DP_to_D0

; здесь, при необходимости, вывод изображения

                bra     loop

endmusic:


Однако, если говорить о воспроизведении на 8912 нормальной музыки, самый простой способ — использование YM_VPACK.EXE из пакета VecSound от Christopher Salomon (понадобится DosBox).
YM_VPACK преобразует известный (в узких кругах :) формат YM (YM5) в .asm, содержащий сразу плейер (умеющий распаковывать музыку на лету) и саму музыку. Под Windows .ym можно получить путём экспорта из Arkos Tracker, либо преобразовав с помощью в AY_Emul (преобразование там из плейлиста осуществляется).

Файл .ym для YM_VPACK предварительно должен быть распакован (пакованный YM — это архив lha).
Остаётся натравить на него ассемблер, в результате чего получится бинарник, включающий плеер и музыку, который можно запустить на Vectrex.
Нужно учитывать, что тактов всё равно расходуется немало (там происходит RLE распаковка «на лету»). Совсем же неупакованная музыка скорее всего займёт неприемлимый объем.

Для звуковых эффектов через 8912 также есть готовое решение — AYFXEdit (PC/Win) и плеер для них в виде sfx.asm (от Richard Chadd)

Карта памяти

Память распределена следующим образом

$0000 — $7FFF — ПЗУ картриджа (собственно, ваша программа)
$8000 — $C7FF — не используемое адресное пространство
$C800 — $CBFF — ОЗУ, свободно — 874 байта
$C800 — $C87F — ОЗУ, используется Vectrex'ом
$D000 — $D7FF — регистры VIA 6522
$D800 — $DFFF — адресуются одновременно ОЗУ и 6522, не использовать
$E000 — $FFFF — ПЗУ BIOS (включая игру Minestorm)

При включении Vectrex, после инициализации системы BIOS и показа заставки, программа начинает выполняться с адреса $0000. После нажатия Reset, либо при включении с зажатой кнопкой «1» заставка пропускается.
В случае каких-либо проблем с чтением картриджа типичная реакция системы — запуск встроенной игры MineStorm.

О загадочном канале «zero ref»

Помимо каналов Y, Z и SOUND у мультиплексора есть ещё один загадочный выход, обозначенный на схеме как 'ZERO REF' и ведущий на положительные входы интеграторов. В BIOS этот канал выбирается только в подпрограмме Reset_Pen (она же ACTGND в официальном листинге — SET ACTIVE GROUND). Эта подпрограмма, в свою очередь, часть Reset0Ref (вызываемой при сбросе луча в центр экрана) и явно является частью процесса калибровки. Процитирую мнение svo на эту тему:

— svo ---

Ноль DAC-а не равен нулю железному (типа потенциал земли) ни разу. И, чтобы быть уверенным, что мы все делаем именно относительно DAC-овского логического нуля, мы выполняем такой фокус. Запоминаем на S&H, что бы он там ни выдавал, и дальше работаем относительно этого значения.

ЦАП перемножающий, выдает ток, пропорциональный произведению разности потенциалов Vref+-Vref- (ноги 14, 15) и цифрового значения на входе. Как устанавливаются эти Vref, похоже не очень важно, суть в том, что какой-то ток скармливается на преобразователь тока в напряжение + фильтр на IC304/2. И вот у него на выходе все уже совсем приблизительно.

Какому потенциалу соответствует цифра «0» в этакой схеме? Я думаю, что разработчики этой схемы очень хорошо знали, что это будет всякий раз разный потенциал, и сделали такую самокалибрующуюся конструкцию.

Интегратор интегрирует по времени значение разности потенциалов между входами ОУ. Например, если разность 0, то интеграл этого должен быть 0 даже при T=вечность. Если потенциал неинвертирующего входа не будет равен потенциалу нуля, потому что мы забили на калибровку, получится нелинейность, типа 1 — 0 != 2 — 1. Интеграл нуля не будет равен нулю и все уползет неизвестно куда. Внешне это, наверное, и должно проявляться как плохо предсказуемые искажения векторов.

— end svo ---

Средства разработки

Проблема с эмуляторами Vectrex носит иной характер, чем с эмуляторами компьютеров с растровым дисплеем (Commodore, Atari, Spectrum и пр.) Существенная часть схемы Vectrex — аналоговая. И полная её эмуляция хотя и возможна чисто теоретически, на практике вряд ли в ближайшее время кто-то будет заниматься подобным.
Соответственно, существующие эмуляторы в этом отношении скорее симуляторы — на них корректно работает лишь некоторое количество наиболее используемых в играх и демках подходов к формированию изображения. Впрочем, это покрывает почти все игры и часть демок.

Лучший на данный момент (октябрь 2016 г.) эмулятор Vectrex — Vide.



Он хорош как эмулятор (достаточно сказать, что он правильно изображает кривые в моей интро Electric Force.
Хотя и делает это не вполне честно). Помимо этого, по сути Vide является средой разработки. Кроме эмулятора в нём есть отладчик, редактор, масса различных утилит для конвертации музыки, сэмплов, создания векторных рисунков и прочее. Вещь довольно сырая и иногда ведёт себя странно. Но вполне рабочая. Когда я писал под Vectrex упомянутые здесь работы, Vide не существовало, поэтому подробностей не будет. Впрочем, там всё самоочевидно по сравнению с ParaJVE.

Другой эмулятор Vectrex — ParaJVE.



Он включает отладчик ParaJVD, из под которого запускается ParaJVE. Всё это можно получить, написав автору вежливое письмо и попросив у него ключик.

Как это работает:

Создаём где-нибудь test.asm. Например, в C:\Program Files (x86)\parajvd\data\sources\test

В качестве ассемблера можно использовать as09 v1.41 / windows (79872 bytes) или a09.exe / windows (98304 bytes). Оба нормально запускаются под Win 64 бит (большинство ассемблеров 6809 не запускаются под Win 64).

Я пользовался as09, но предупреждаю, что у него есть две проблемы 1) макросы почти неработоспособны 2) слишком длинные строки (комментариев) не допускаются. Причём, в обоих случаях возникают очень странные ошибки.

Сборка происходит двумя командами:

as09.exe -i test.asm (получаем бинарник test.bin)
as09.exe -ig -h0 -w200 -l -m test.asm (получаем отладочную информацию — test.dbg и test.lst)

В результате имеем .asm, .bin, .dbg, .lst

(.lst очень полезен если нужно посмотреть, во-первых, как именно ассемблер понял каждую мнемонику, во-вторых, сколько тактов какие команды займут)

Далее можно просто запустить .bin в эмуляторе jve:

ParaJVE.exe -game=test.bin

А можно в отладчике jvd:

Запускаем parajvd.bat, создаём проект. Там есть два варианта (source mode) — .lst либо .dbg. Разница в следующем (цитирую автора):

So the DBG mode shows the source exactly as you typed it, whereas LST does not (if you look at the content of a generated LST file, you will see that it contains lots of «garbage» text, like generated addresses, etc.)
But on the other hand, the LST option is good if your source uses a lot of macros (LST will display the expanded macro, whereas DBG will not).


В качестве «source» выбираем не .asm (как можно было бы подумать), а .dbg либо .lst

Если проект уже создан, просто выбираем после запуска jvd.bat нужный. Запускается jvd и в нём ваш test.bin. Можно отлаживать.

Нет иного способа перезагрузить dbg/lst, кроме как полным перезапуском jvd (!). Меню Debug/Reload Cartridge ROM относится к jve. Т.е. jvd не узнает о перезагрузке.
Через этот Reload можно перезагрузить перекомпилированный .bin и он запустится, не более того. Причём, нельзя делать это когда програма остановлена (например, на breakpoint'e) — всё повиснет.
Breakpoint'ы при выходе сохраняются (т.е. при следующем запуске jvd он сработает).

Ещё раз: не стоит воспринимать JVE как полноценный эмулятор железа. Он ориентирован скорее на выполнение типичного софта. К примеру, всё что происходит в вызове Wait_Recal для эмулятора без особого ущерба можно сократить до нескольких команд (вызов DP_to_D0 и сброс интеграторов). Получившийся код будет прекрасно работать в эмуляторе, но совершенно не работать на реальном железе.
Кроме того, от некоторых безобидных на вид сочетаний команд эмулятор может падать с ошибкой.



Однако, если при программировании не выходить за рамки того, что делается в самом BIOS'е, можно считать, что эмуляция достаточно хороша.

Из достойных внимания есть ещё один, более старый эмулятор — DVE. Он похуже и под DOS. Тем не менее, вполне работает под DosBox, включает отладчик с дизассемблером и может быть полезен. В числе прочего, рекомендую почитать help.dat/* от него.

Лично мне было оптимально писать код в Sublime Text 3, а затем одним нажатием кнопки ассемблировать его и тут же запускать в JVE (запуская соответствующий .bat).
Отладчик (JVD) я не использовал вообще — чем с ним возиться, быстрее самому понять, где ошибка.

Сейчас безусловно лучше и проще использовать Vide.

Разумеется, регулярно приходилось проверять код на настоящем Vectrex. Я для этого использовал эмулятор ПЗУ изготовленный svo (подключаемый по USB), но позднее купил другой.

Заготовка исходника .asm:


                include "vectrex.i"

                org     0

                db      "g GCE 2015", $80 ; Изменять можно только год. 'g' - знак копирайта
                dw      $F600            ; адрес музыки которую надо играть при показе названия программы на начальной заставке (в данном случае - никакой)
                db      $F8, $32, 33, -$36; высота, ширина, Y, X названия программы на заставке
                db      "PROGRAM TITLE", $80; название программы
                db      0                 ; признак конца заголовка

loop:

                jsr     Wait_Recal        	

	....

                bra	loop


Немного о дизассемблерах…

Их есть два, предназначенных именно для Vectrex — оба с исходниками (на C и на Pascal). Оба под DOS (т.е. требуют DosBox) и без описания.

С одним из этих дизассемблеров идут конфиги для дизассемблирования пары десятков игр и программ. Всё это можно найти на http://vectrexmuseum.com/share/coder/

Также можно попробовать IDA. Но до версии 6.7 он не понимает адресации по DP (что делает его практически бесполезным). В 6.7. вроде как это изменилось, но я не проверял.

Лично я пользовался DIS6809.EXE, вполне успешно. Для него надо создать .ctl примерно такого вида:


; FILENAME.CTL 

TITLE _просто_название_

ASM

FILE  FILENAME.BIN 000000 0000 076A
LABS  LABELS

ENTRY 0024	; начало кода
ASCII 0000 0023	; от 0 до 23 - ascii строка
BYTE  0710 0769	; от 0710 до 0769 - данные (fcb)


и т.д. (там есть исходник на Паскале, из него в общих чертах ясно, что делают те или иные ключевые слова и параметры).

Затем в DosBox пишем dis6809 filename.ctl >filename.asm

Приложение — мои работы для Vectrex

Мной были написаны несколько работ под Vectrex — Electric Force (233 байта), Invitron (32 килобайта) и две пробных — Rainy (413 байт) и Emptyscreentro (128 байт).

Electric Force (233b)
(source+bin)



В работе использована не совсем стандартная возможность Vectrex рисовать кривые (изменяя значение на ЦАП-е в канале X в процессе того, как идёт интегрирование по каналу Y). Рисование кривых — первое, что меня заинтересовало, когда я начал изучать Vectrex. Соответственно, эксперименты затем вылились в эту работу, представленную на конкурс Tiny intro фестиваля Chaos Constructions'2015.

Эмулятор (ParaJVE) плохо понимает такие вещи (хотя и понимает), поэтому в нём это смотреть не стоит — если нет Vectrex'a, лучше посмотреть на Youtube.

Надо сказать, что сколько я не пытался записать нормальное видео (на Sony NEX7, 35/F1.8) — получается лишь жалкое подобие того, что видно на ЭЛТ экране. Во-первых, катастрофически не хватает динамического диапазона — между совершенно чёрным экраном и яркой точкой на нём, где задерживается луч — огромная разница в яркости, которую камера никак не может зафиксировать. Кроме того, не передать плавность (маленькая выдержка снижает чувствительность). Короче говоря, видео — плод компромиссов, в результате которого «тёплая ламповость» неизбежно теряется, что очень обидно.

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

«Случайные» данные для кривых берутся из BIOS, конкретно — из таблицы символов (там попадаются наиболее подходящие значения ;)

Количество кривых — фактически предельное, чтобы уложиться (вместе со всеми вычислениями + изменение яркости + возврат к центру экрана для калибровки + отрисовка текста + две горизонтальные линии) в стандартные 30 тыс тактов на «кадр».

Длина горизонтальных линий — максимальная, какую можно обеспечить одной линией в максимальном масштабе. На моём экземпляре Vectrex'a получается чуть меньше ширины экрана, на другом запросто может быть иначе.

Отдельно замечу, что хотя никакого звука в коде нет, сам Vectrex из-за наводок жужжит (это типично для большинства Vectrex'ов), что в данном случае скорее плюс :)

Youtube | Pouet

Invitron (32kb)
(source+bin)



Работа была написана в качестве invitation intro (приглашения) на Chaos Constructions'2015.
Основной объём из её 32к занимает оцифрованный (7кГц) несжатый голос — 20кб. Известная (в узких кругах) фраза «здесь пахнет демосценой» была произнесена Random'ом во время «Random speech compo» на CC'2004.
Её проигрывание осуществляется через ЦАП.
После этого через чип AY8912 фоном начинает играть музыка Plaigraunt by C-Jeff. Она преобразована из pt3 в .YM через AY-Emul, затем в данные для VecSound через ym_vpack. Соответственно, в качестве плеера используется VecSound.
Как несложно услышать, плеер этот далеко не идеален, но попытки найти другой — успехом не увенчались. Так что, с одной стороны, неудобно перед C-Jeff-ом за искажение его отличной музыки, с другой — альтернативы всё равно не было (разбираться с 8912 и писать плеер самостоятельно я морально не готов :).
В процессе проигрывания VecSound плеер распаковывает YM на лету. Хотя там довольно простое сжатие, на это уходит порядочно тактов. В распакованном же виде музыка бы не влезла в стандартный 32кб объём картриджа Vectrex.

Что касается основной части интро — она была навеяна титрами из фильма «Tron Legacy». Конечно, сходство весьма поверхностное. Проблема в том, что за 30 тыс тактов (на условный «кадр») можно нарисовать очень немного векторов (ну или много, но очень коротких). Напомню, что перемещение луча к началу следующего вектора — тоже считается за вектор, как и яркие точки. При этом, после рисования почти каждого вектора, нужно рекалибровать систему, возвращая луч к центру экрана (иначе всё будет съезжать, искажаться и пр.).

Скроллинг верхней и нижней части реализован довольно просто — каждый вектор сначала рисуется за пределами экрана и затем, с каждым кадром, добавляется смещение. После того, как он скроется за другим краем, всё повторяется. Таким образом можно скроллить лишь векторы довольно небольшой длины. Это связано с тем, что напряжение на выходе интеграторов не может превысить определённое значение. Поэтому, если смещать луч в одном направлении снова и снова, в конце концов он как бы упрётся в невидимую эластичную «стену». Забавно, что в эмуляторе (ParaJVE) эта особенность железа не учитывается и луч уходит сколь угодно далеко.
Точки на концах векторов яркие не за счёт управления яркостью, а за счёт того, что луч задерживается в этом месте несколько тактов (эмулятор это также не понимает, да и в видеоролике, к сожалению, эффекта почти не видно).

Текст в центре первоначально хотелось выводить, рисуя символы векторами. Однако, это оказалось совершенно неприемлимо по тактам. В итоге, используется переделанная процедура BIOS выводящая строку «растром». Переделка заключалась, во-первых, в разворачивании подпрограмм, которые из неё вызывались (для экономии тактов) и, во-вторых, скорректирована одна задержка, из-за которой на большинстве Vectrex'ов (насколько это можно судить по разным видео на youtube), длинные строки текста начинает перекашивать в определённом направлении.

Подпрограммы рисования векторов, перемещения луча, изменения яркости — также все были вынуты из BIOS, вставлены непосредственно в код интро и, по-возможности, оптимизированы.
По ходу дела обнаружилось, что asm09win некорректно работает с макросами — когда их начинаешь использовать, при ассемблировании периодически возникают странные синтаксические ошибки, в произвольных строках. Пришлось нужные куски исходника дублировать вручную.

Youtube | Pouet

EmptyScreentro (128b)
(source+bin)



Эксперимент по выводу изображения за пределы экрана (туда, где ещё есть люминофор, но он уже скрыт корпусом).
Рисуются две вертикальные линии слева и справа, в максимальном масштабе и с максимальной яркостью. При этом рисуются по 30 раз за кадр каждая, за счёт чего яркость возрастает ещё больше и свечение из под рамки становится отчётливо видно. В принципе, таким образом можно попортить кинескоп, хотя в данном случае участок всё равно находится за пределами видимой области. Кроме того, теоретически в Vectrex есть защита от слишком долгого нахождения луча в одном положении (яркость должна автоматически уменьшиться).
Длина линий меняется взависимости от данных при проигрывании короткой мелодии через AY8912 (которая берётся из ROM).

Youtube

Rainy (413b)
(source+bin)



Имитация струй дождя с переменной интенсивностью. Используется большое количество векторов разной яркости и с разным паттерном (изменяющимся в процессе). Масштаб максимальный. Внизу, в конце линии, луч на некоторое время придерживается, для получения яркой точки.
Хотя этот код был первой попыткой написать что-то осмысленное для Vectrex, опубликована работа была последней. При большом желании код можно сократить как минимум вдвое (данные для координат линий не хранить, а вычислять, а также частично использовать подпрограммы BIOS для Draw_Line_d, Move_D, Reset0Ref, Intensity_a (в данном случае они вынесены из BIOS и модицифированы, без чего потенциально можно обойтись).

Youtube

Ссылки:

Мой семинар про Vectrex, CC'2015
http://vectrexmuseum.com/share/coder/
Форум vectorgaming
Vide
ParaJVD
ParaJVE
Asm80 — онлайн эмулятор 6809, с отладчиком
Архив, в который я собрал различную документацию, примеры и утилиты (кроме эмуляторов)
Мой перевод статьи разработчика игры Frogger для Vectrex
Рассказ svo о Vectrex (текст и видео)

А также книжки «6809 Assembly Language Programming — Leventhal.pdf», «CoCoAssemblyLang_Color.pdf»

Здесь можно посмотреть мои работы под разные ретро-платформы, а здесь их исходники на github.

Статья была написана в сентябре 2015 года и немного дополнена (абзацом про Vide и ссылкой на новый эмулятор ПЗУ, описанием моих intro) в октябре 2016-го.

Автор статьи: Пётр Соболев (frog AT enlight.ru)

P.S. Спасибо Тиму Ташпулатову (tnt23) за Vectrex и Вячеславу Славинскому (svo) за эмулятор ПЗУ

25 комментариев

avatar
блин, а cut использовать слабо?
avatar
ад вообще…
Пётр, спасибо — очень интересно!
  • bfox
  • +2
avatar
Офигенная статья! Прочитал на одном дыхании. Правда, пока пропускя некоторые технические детали. Захотелось заиметь Vectrex, и перечитать статью еще раз, более вдумчиво.
  • nyuk
  • +3
avatar
frog, прошу прощения, я добавил тег в начале
  • VBI
  • 0
avatar
тег CUT
avatar
Да, спасибо — забыл про него
avatar
я использовал ассемблер asm6809(пригодился для Dragon32)

А как реализована бегущая строка?
www.youtube.com/watch?v=QAuYcUDsaAI

точек много.
avatar
Принцип тот же, что и в биосовской Print_Str. Ну т.е. через управление BLANK через shift register. У меня сейчас Vectrex не под рукой, но я подозреваю, что эта строка вместе с музыкой не укладывается по тактам и мерцает (по видео это не понять).
Вообще, что касается вывода битмапов на Vectrex, самое впечатляющее вот это: www.youtube.com/watch?v=lG2Oh6R5RGw
Бинарник недоступен, так что посмотреть как это в реальности — невозможно (покупать — жаба душит). Ну музыки, понятно, там фоновой нет. Обращаю внимание, что насколько можно судить по видео, там спрайты имеют как минимум две градации яркости. Что означает, что они успевают рисоваться дважды. Но опять же — вопрос с мерцанием.
avatar
Хм. я думал, что замучено что-то с relative координатами. Не нашел на pouet демо, в нем скролится картинка под музыку Robcop.
avatar
Для таких статей и кат не нужен.
Если вы не собираетесь читать такую статью полностью сразу, как только она появилась — извините, но вам вряд ли здесь место.
  • sq
  • +1
avatar
кхм… а нафига портянка на главной? давайте тег cut запретим)
да, я еще не читал, но прочту позже. как минимум для общего развития.
avatar
Отличная статья, спасибо! В более поздних игровых автоматах Atari векторные дисплеи (Quadrascan) ни в чем не уступают растровым собратьям. Посмотрите, какая стильная даже по нынешним временам графика в игре Major Havoc (1983 год!).

www.youtube.com/watch?v=9n6I1KPxOfE
avatar
очень мощно
avatar
Да, впечатляет. Но и стоил он, я думаю, сильно других денег чем Vectrex. Вот схема: arcarc.xmission.com/PDF_Arcade_Atari_Kee/Major_Havoc/Major_Havoc_SP-252_2nd_Printing.pdf
Там аж два 6502, хотя конечно денег стоит скорее всё остальное.
Описание игры www.atarimuseum.com/orubin/mhavoc.html
Под такую машинку было бы интересно что-то написать, жаль железка недоступна.
avatar
Пётр, а каков «нормальный» диапазон цен на vectrex на сегодняшний день? На ebay увидел разброс от 14т.р. аж до 1к$, т.е. почти в пять раз… И как обстоят дела с переносом данных? Флеш-картридж, какой-то спец.девайс, или что?
avatar
Мы со svo и tnt23 взяли на троих за $400, насколько я помню. Причём он продавался как частично рабочий (по факту оказался полностью рабочий). Но это всё как повезёт. Такой же разброс я наблюдал на Intellivision, к примеру (раза в три). При том, что их довольно много на рынке.
С Vectrex'ом ещё в копеечку выйдет доставка. «Без монитора» его не купишь (Тим тащил на себе через несколько границ :)
Я знаю Manwe тоже себе покупал — можешь спросить у него.
Про перенос данных ссылка есть в статье — www.vectrex.hackermesh.org/index.php/en/mvbd-mvmc-3/faq
avatar
А что такое луч? Поток электронов отклоняемый магнитным полем? От чего тогда зависит толщина луча? (точки)
И второй вопрос. Использование какого-нибудь современного микроконтроллера типа stm32 насколько радикально изменило бы качество графики в играх?
avatar
Насколько я понял, тут отклонение электронов электростатическое, т.е. не магнитным, а электрическим полем, как в трубках осциллографов.
Толщина луча зависит только от системы фокусировки луча в трубке. Ещё от собственно тока, наверное, но тут вроде on-off, ток луча плавно не регулируется.
avatar
Вопрос к тому, что могли ли разработчики сделать еще и размер точки (фокуса) регулируемым.
avatar
lvd по идее правильно написал. А фокусировка как раз магнитным полем, по крайней мере я так смутно помню по последнему разобранному телевизору :)
Что касается программного управления фокусировкой — мне кажется это нецелесообразно. Т.е. чтобы её быстро менять, наверное пришлось бы что-то серьёзное дополнительно наворачивать и это было сильно увеличило цену конструкции.
Вот чего принципиально не умеют векторные дисплеи — это сплошных заливок. Вот где проблема…
avatar
Так вот в этой связи и подумало про размер.
Можно было бы рисовать хотя бы сплошные круги, для взрывов, толстые линии, большие буквы и т.п.
А то и вовсе оперировать пикселями как пикселями, пусть и круглыми.
avatar
Только ты учти, что чем больше пятно, тем меньше его яркость. Я думаю что при диаметре в сантиметр его еле разглядишь. Придётся долго светить в одно место (да и то — время послесвечения люминофора весьма ограничено), в результате быстро двигать уже не получится, кроме того из-за этой задержки останется меньше тактов на всё остальное.
avatar
нет, я определённо плюсую Андрея — тоже захотелось живую тачку и покодить под неё:) эх, был бы там ещё z80… ну или вообще более шустрый камень — это же кладезь для 3d!
жаль, конкретно в этом видео автор считерил на предгенерации координат:)
  • bfox
  • +3
avatar
> был бы там ещё z80
Это знаешь, всё равно что говорить «Эх, вот был бы ещё в Амиге 486-й проц!» :)
avatar
Напомню, что у 6809 команды по тактам вдвое короче, чем у Z80. И можно нередко слышать мнение, что в принципе его архитектура лучшая среди 8-битных процессоров того времени. Так что в общем случае он в этой системе не (сильно) медленнее, чем Z80 на ZX Spectrum.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.