Архітектура та програмування комп'ютера Vectrex

— А відеовихід у нього є?
— І як ти собі це уявляєш?
(з розмови про Vectrex)


Vectrex випускався GCE в 1982 — 1983 рр. і являє собою ігровий комп'ютер (префікс) ключова особливість якої, векторний дисплей, робить його одним з самих незвичайних і цікавих 8-розрядних комп'ютерах. З певною натяжкою можна сказати, що він є спрощеною версією векторних ігрових автоматів Cinematronics, технічно більш досконалих.

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

Оскільки комп'ютер був випущений як ігрова приставка та ігри для нього продавалися на картриджах, програма розміщується в ПЗП картриджа (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-бітний цифро-аналоговий перетворювач (ЦАП, ЦАП). Перетворює код, що надходить на нього з чіпа 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, off MUX
stb <VIA_port_b; poke $81, ramp off, off MUX

; в кінці кривої буде помітний темний кінчик. З цією затримкою, принаймні, не буде дірки перед ним
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
Група Vectrex fans unite! у FacebookVide
ParaJVD
ParaJVE
Asm80 — онлайн емулятор 6809, з відладчиком
Архів, який я зібрав різну документацію, приклади та утиліти (крім емуляторів)
Мій переклад статті про розробку гри Frogger для Vectrex
Розповідь svo про Vectrex (текст і відео)
Исходники моїх робіт — Electric Force, Invitron, Rainy, EmptyScreenTro

А також в Інтернеті нескладно знайти книжки «6809 Assembly Language Programming — Leventhal.pdf», «CoCoAssemblyLang_Color.pdf»

Тут можна подивитися мої роботи під різні ретро-платформи, а тут исходники на github.

Стаття була написана у вересні 2015 року і доповнено в жовтні 2016-го.

P. S. Дякую Тіму Ташпулатову (tnt23) за Vectrex і В'ячеславу Славинскому (svo) за емулятор ПЗУ
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.