Мой виртуальный 16-битный "компьютер мечты" - SimpX

Исходники: github.com/aa-dav/SimpX
Онлайн-версия: alxhost.tk/SimpX/SimpX.html (первая загрузка будет долгой, но потом закешируется)
В веб-версии рекомендую сразу нажать меню View->Set 400% чтобы выправить соотношение сторон.
Выбираем в левом списке редактора файлы test0x.asm и нажимаем меню Emulator->Compile and run чтобы увидеть результат.
Если активирована не английская раскладка клавиатуры — ввод с кнопок может не работать (это важно для последних тестов).
Так же еще замечу, что в веб-версии в коде могут некорректно отображаться табуляции — это некритично и вызвано разным отношениям к пикселям в среде Qt в stand-alone и wasm вариантах. В stand-alone всё визуально корректно.
Описание процессора — Simpleton (4) и его ассемблера уже было.

Подначивание ютубера 8bit-guy про «8-битный компьютер мечты» который в его варианте превращается уже давно в Commander X16 (базирован на потомке MOS 6502) довело меня уже до этой точки — SimpX задышал и оброс мясом эмуляции так, что его уже можно в альфа-версии щупать наглядно и с откликом.

Прежде всего всё крутится вокруг виртуальной процессорной архитектуры Simpleton 4. Цифру 4 вообще уже можно откинуть, т.к. все предыдущие итерации я считаю неудачными. Simpleton 4 же стал имхо идеалом.

Главное идеологическое кредо Simpleton — это простота как системы команд так и программирования в этой системе.
Идейный (но не архитектурный) вдохновитель — Gigatron TTL обладает крайне минималистичной системой команд, но программирование на нём превращается в геморрой — вместо элементарных операций иногда используются выборки из таблицы и вот такое вот всё.
В пику монструозному названию «Gigatron» я назвал Simpleton именно simpleton ибо в английском языке это слово значит «туповатый» или «одноклеточный» в смысле «недалёкости».
Однако программировать в системе команд Simpleton в разы проще, чем в системе команд Gigatron! Несмотря на минималистичность и туповатость.
Все инструкции процессора Simpleton делают всегда только одну вещь: берут аргументы X и Y, записывают их в арифметико-логическое устройство (АЛУ) и полученный результат записывают в назначение R.
В синтаксисе языка Си это выглядит как:

R = Y op X

где op — код операции.
Главное упрощение которое сделало Simpleton 4 таким красивым как он есть — это отказ от байта.
Интересно, что в Gigatron любая 8-битная инструкция в памяти программ всегда сопровождается 8-битным immediate из памяти данных программы которое в большинстве инструкций просто не используется — т.е. по сути программное слово уже есть 16-битное слово, которое потрачено в большинстве случаев зря.
Осознание этого и натолкнуло меня на простую идею — сделать слово основой всего. Не байт, а слово.

И поэтому в архитектуре SimpX всё есть 16-битные слова — и регистры и ячейки памяти.
И это просто сделало прорыв — стало достаточно десятка команд чтобы описывать то, что в 8-битных архитектурах выражается гораздо более коряво и многословно.
Пример: берём три глобальных переменных var1, var2 и var3 и прокручиваем с ними на Си операцию:

var1 = var2 + var3

В синтаксисе ассемблера Simpleton эта операция будет выглядеть как:

[ var1 ] = [ var2 ] + [ var3 ]

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

LD HL, ( var2 ) ; 3 байта
LD BC, ( var3 ) ; 4 байта, т.к. используется расширенный набор инструкций с префиксом $ED
ADD HL, BC ; 1 байт
LD ( var1 ), HL ; 3 байта

Т.е. 11 байт или пять с половиной слов, что больше! Наглядный пример как широкое командное слово приводит напротив к оптимизации по коду. А учитывая, что в Simpleton слова есть битность всего, включая ячейки памяти и считывание слова происходит за такт, то скорость тоже возрастает прилично — четыре считывания из памяти, одна запись и готово. А у Z80 — 11 считываний и две записи.

Таким образом виртуальный ПК базированный на Simpleton — SimpX (тут тоже игра слов, ибо словом simp в английском кого только не обзывают :D ) заимствует главное — все 65536 ячеек памяти как и все восемь регистров есть 16-битные слова. Т.е. без MMU эта архитектура изначально в адресном пространстве содержит 128Кб.

Но SimpX использует маппер памяти — MMU который дробит адресное пространство на восемь страниц каждую из которых можно перемаппить через запись в порты ввода-вывода (mmuPage0-mmuPage7).
Но в тестовой пока среде это не используется и пока ассемблер даже не может дотянуться до MMU. Инструкция а-ля page x y его еще только ждёт.
Но уже в 64Kw базового адресного пространства можно развлекаться.

; Раскладка памяти:
; $0000 - крохотный раскрутчик прыгающий в итоге на пользовательскую 
;         метку start.
; $0010 - обработчик прерываний (по VBlank). Если irqExtHandler не 0, то
;         передаёт управление по этому адресу. Иначе инкрементирует 
;         irqTimer и выходит.
; Далее идёт код simple_lib, библиотек и в конце - пользовательский код.
; Расклад
; $7FFF - начало стека (растёт вниз)
; $8000-BFFF - 16Kw видеобитмапа (растровые данные экрана или символов тайлов/текста)
; $C000-C3FF - 1024 слов чармапа (32*32, видимая область 32*24, возможен скроллинг)
; $C400-FFDF - свободная зона
; $FFE0-FFFF - порты ввода-вывода


Список реализованных процедур:

simple_lib.inc

; loadPalette  - загрузить палитру
; simpleInit  - инициализировать раскладку памяти и текстовый видеорежим
; initTextMode  - инициализировать текстовый видеорежим
; printChar  - вывести один символ без учёта управляющих символов (быстро)
; cursorBack  - отодвинуть курсор назад, но не далее верхнего-левого края экрана
; printSpace  - напечатать пробел
; printCr    - перенести строку
; printTab  - вывести табуляцию
; printBkSp  - стереть символ слева и вернуть курсор назад
; printSpChar  - вывести символ с учётом управляющих символов
; printHex  - вывести число в 16-ричной форме
; printNum  - вывести беззнаковое число
; printInt  - вывести число со знаком
; readKey    - определить первую зажатую кнопку клавиатуры (обновляет LastKey)
; readKeyOnce  - как readKey, но возвращает 0 если кнопку не отжали
; decodeLastKey  - переводит код кнопки в LastKey в код ASCII-символа
; inputChar  - ждёт нажатия кнопки (блокирует) и возвращает код ASCII-символа

zstr.inc

; zstrPrint  - печать строки
; zstrLength  - получить длину и конец строки
; zstrCopy  - скопировать строку в буфер
; zstrFromHex  - сконвертировать число в строку (16-ричное представление)
; zstrFromNum  - сконвертировать беззнаковое число в строку
; zstrFromInt  - сконвертировать число со знаком в строку
; zstrInput  - ввод строки с клавиатуры

math.inc

; r0_mul_r1  - умножить r0 на r1 (числа без знака)
; r0_div_r1  - поделить r0 на r1 (числа без знака)


Соглашение о вызовах:
1. параметры передаются в регистрах начиная с r0, r1…
2. результаты возвращаются так же в регистрах r0, r1…
3. процедуры сохраняет неизменными все регистры кроме результатов и psw (флаги)

Главные отличия ассемблера Simpleton от мейнстрима:
1. все идентификаторы должны быть отделены пробельными символами (послабление только для скобок круглых и квадратных)
даже символ комментария; должен быть отделён пробелом слева и справа чтобы парсер его понял, а не воспринял
как часть сложного идентификатора
2. после инструкции ассемблера «mode new» он ожидает инструкции в «арифметическом» формате R = Y op X
3. для разделения параметров используются пробелы. никаких запятых. поэтому для того чтобы обозначить, что нужно
вычислить compile-time выражение типа label + 1 надо окружить выражение в скобки: ( label + 1 )
например: r0 = r1 + ( label + 10 )

ОПИСАНИЕ ВИДЕО-РЕЖИМА:

Базисом изображения служит плоский 16-цветный (4bpp) битмап размером 256x256 пикселей. На экран при этом влазит область 256x192 (формат 4:3) которая может скроллироваться мгновенно «с проворотом» как это было принято регистрами скроллинга.

В одном слове (ячейке памяти) Simpleton помещается 4 смежных по горизонтали пикселя.
Т.е. 256/4=64 слова в строке, умножить на 256 строк = 16384 слова занимает память графических данных.
Она линейна, 16-цветна и проста — замапив её в адресное пространство процессора с помощью MMU можно менять вручную.

Однако видеочип отталкивается при выводе изображения не от этой памяти, а от карты символов/тайлов.
Суть тут в том, что память графических данных как квадратное изображение 256x256 поделено на квадратные же блоки 8x8 пикселей обычной квадратной гнездовой сетки.
И эти квадратики уже пронумерованы от 0 до 1023 (256/8=32 тайла по вертикали и по горизонтали дают 32*32=1024 тайла в графических данных). Тут важное отличие от консольных техник, что видеоданные тайлов располагаются в памяти не линейно, а как квадраты в битмапе линейных данных пронумерованных слева-направо сверху-вниз.

Допустим графические данные располагаются по адресу 0 адресного пространства видеоконтроллера, тогда нулевой символ/тайл будет базироваться по адресу 0 где подряд будут лежать 2 слова с первыми его 8 пикселями, далее идут первые пиксели первых 32 тайлов и только через 64 слова начинается вторая строка тайла номер 0. И так далее. Через 512 слов начнётся полоска пикселей описывающая графические данных следующий 32 тайлов. Надеюсь понятно будет без рисунка.
Битовую маску номера тайла в 16-битной форме можно представить так:
000000HHHHHLLLLL (10 бит числа)
Тогда адрес где лежит первый пиксель левого-верхнего угла тайла в графических данных можно изобразить как:
00HHHHH000LLLLL0
Т.е., в пикселях это pixmap[ 2*L, 512*H ], где L и H это соответственно нижние и верхние 5 бит номера тайла.
Вычислять всё это для видеоконтроллера никакого труда не составит — происходит прямой маппинг линейного пространства тайлов на квадратно-гнездовую пиксельную битовую карту линейного же формата.

Так вот еще в одном месте видеопамяти лежит прямоугольная же область — назовём её символьной или тайловой картой — из 32*32=1024 слова которая и описывает какой именно тайл (символ) выводится в данном квадратике.
Если эту область заполнить возрастающими числами от 0 до 1023, то область графических данных будет отображаться на экране совершенно прямолинейно как есть.
Если же начать мутить с символьной картой, то можно перепутать все квадратики в точности как в тайловых движках!

В SimpX сейчас битмап заполнен файлом font-00.asm который содержит исходник полученный через меню Tools->Image to bitmap.asm

P.S.

Работа над сабжем ведётся, он еще далёк от завершения.

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

avatar
Хотелось бы увидеть пример с демо-эффектом. Плазма какая-нить или огонь.
  • tsl
  • +2
avatar
это уже возможно, просто не хватает времени.
но да, если залить в чармап (1Rw) последовательно возрастающие числа, то битмап станет прямым отображением пикселей на экране.
это уже сейчас работает, но руки не доходят…
avatar
Rw->Kw
была опечатка
Имелось ввиду под Rw — K2 — килослова.
avatar
Вот тут есть старое видео графического режима: youtu.be/ESg7SWPMpE8
Тут видно, что выделяются знакоместа 8x8 пикселей.
И конечно это не просто так.
SimpX использует 16-цветный битмап 256x256 пикселей (видимая зона — 256x192), но каждое знакоместо 8x8 имеет собственный 4-битный атрибут палитры. Таким образом это эдакое мощное расширение спектрумных идей — в пределах знакоместа 16 цветов одной из 16 палитр (палитра содержит 15-битные RGB — 0RRRRRGGGGGBBBBB) так что всего на экране цветов может быть 256.
Более того зона атрибутов (32*32 слова) SimpX содержит так же индирекции какое именно знакоместо в текущем знакоместе выводится (0-1023).
Т.е. формат атрибута знакоместа в битах следующий:
PPPP00NNNNNNNNNN, где P — это номер палитры, а N — номер знакоместа которое в данном знакоместе выводится.
Если всю область чармапа залить нулями, то каждое знакоместо на экране будет выводить первый (нулевой) квадратик 8x8 из битмапа 256x256 — левый верхний уголочек. Если залить числом 1023 — то на экране 32*24 раз выведено будет нижнее левое знакоместо битмапа.
В любом случае манипулируя верхними четырьмя битами этой заливки можно окрашивать знакоместа в одну из 16 субпалитр.

Сейчас в simple_lib.inc эти характеристики используются чтобы организовать простейший текстовый видеорежим — битмап (16 килослов) заливается (частично) вот этой картинкой:

И чармап используется как буфер символов, т.к. они просто маппятся на эту картинку.
Если же залить чармап возрастающими от 0 до 1023 числами — то битмап будет отображён на экран прямолинейно как есть и даст графический режим с 16 цветами.
Который можно обогащать 16 субпалитрами добавляя к чарам в чармапе верхние 4 бита субпалитры.
avatar
Коррекция: «нижнее левое знакоместо» => «нижнее правое знакоместо»
avatar
P.S.
В этом SimpX тоже пытается быть максимально простым и схемотехнически и программистски, но не экономией на спичках!
Текстовый видеорежим в нём является частным случаем графического режима. Идеи эти базированы очевидно на тайловых видеочипах консолей, но сделан тот ход, что номера тайлов в битмапе не линейно возрастают, а образуют двумерную картинку «as is» битмапа.
Но в статье описано, что перетусовки бит в этом случае настолько просты и прямолинейны, что не несут никаких накладных расходов по сравнению с линейной организацией тайловой карты.
В SimpX же это позволяет на одной и той же физической схеме реализовать и быстрый текстовый режим и линейный графический.
А при желании — демосценить с атрибутами знакомест! :D
avatar
Пример: берём три глобальных переменных var1, var2 и var3 и прокручиваем с ними на Си операцию:
всё же чаще в прикладных полезных программах оперируют локальными переменными
а глобальные — компиляторы стараются группировать с одной базой и адресовать потом по смещению
так вот другой пример, на локальные — сложить два числа с верхушки стека, что будет здесь? ;)
avatar
всё же чаще в прикладных полезных программах оперируют локальными переменными
Это когда пошли процессоры заточенные под стековую адресацию.
На MOS6502 адресовать стек легко невозможно. На Z80 даже вроде бы при наличии адресации через IX/IY+offset такие инструкции дают приличный пенальти на работу со словами. Поэтому максимально быстрый код писался на глобальных переменных. Для меня это составляет некоторую романтику той эпохи поэтому инструкции адресации стека я вводить не стал.

так вот другой пример, на локальные — сложить два числа с верхушки стека, что будет здесь? ;)
Семь слов если на стеке (и загрязнение трёх регистров). Против четырёх слов без загрязнения регистров у глобальных переменных.

Я это обдумывал по следующей ссылке и там же придумал апгрейд архитектуры до лёгкой адресации стека: gamedev.ru/flame/forum/?id=249067&page=23&m=5439258#m333
Там как раз сперва рассуждения о том насколько просядет производительность с переменными в стеке.
А потом идея превратить один из пяти регистров без особых функций в аналог BP из i86: косвенные чтения/записи по регистру r4 всегда считывают слово из [ pc++ ], прибавляют его к r4 и полученное значение используется в качестве адреса для косвенного чтения/записи.
Тогда всё еще большую симметричность приобретёт: r0-r3 — регистры без специальных функций и r4-r7 — регистры особого назначения.
Но я не хочу так делать чтобы не ломать дух эпохи.
Хочешь Си — просади производительность. :)
avatar
P.S.
Правда еще надо заметить (по ссылке выше это описано) — если смещения от вершины стека переменных укладываются в -8..+7 слов, то размер примера [ a ] = [ b ] + [ c ] снова можно вернуть к четырём словам, но загрязнение регистров адресами локальных переменных останется. Причём если дальше нужно поработать с переменными отстоящими от уже полученных указателей опять же не далее чем на 4-битное знаковое смещение — опять можно обойтись в инструкции перенастройки одним словом. Короче inplace immediate может дать возможность к оптимизации. Но это так, полумеры.
avatar
Это когда пошли процессоры заточенные под стековую адресацию.
локальные переменные — необязательно стековые, могут быть ведь и статические, где можно

На MOS6502 адресовать стек легко невозможно. На Z80 даже вроде бы при наличии адресации через IX/IY+offset такие инструкции дают приличный пенальти на работу со словами. Поэтому максимально быстрый код писался на глобальных переменных.
максимально быстрый код для восьмибиток пишется с удобным размещением данных
собс-но, в идеале что для локальных, что для глобальных адресов часто только младший байт изменяется

Семь слов если на стеке (и загрязнение трёх регистров). Против четырёх слов без загрязнения регистров у глобальных переменных.
ну вот, а у z80 — три байтика)))

вообще выигрыш у тебя больше за счёт 16-битности там, где от z80 ты тоже требуешь 16-битных операций
а на байтовых (например, при обработке текста) уже не всё так однозначно с твоим отказом от байта
avatar
локальные переменные — необязательно стековые, могут быть ведь и статические, где можно
Так с машинной точки зрения между статическими и глобальными разницы нет. Если статические — значит верно всё то что написано выше про глобальные.
ну вот, а у z80 — три байтика)))
С фига ли? Семь байт на _сложение двух слов из стека с записью результата в стек_. По любому смещению.
Три байта у Z80 тут даже близко нет.

вообще выигрыш у тебя больше за счёт 16-битности там, где от z80 ты тоже требуешь 16-битных операций
У Z80 есть 16-битные операции. И загрузка и сохранение и арифметика. Но вот…
При этом Simpleton субоптимальная архитектура — я писал про это в статье про сам Simpleton 4 в первых же параграфах. Её главная цель не вложить максимум команд в минимум байт, а обеспечить максимально чтобы были простыми одновременно и машинная и программистская стороны вопроса. Оптимальностью команд при этом пришлось пожертвовать, но что интересно — широкое слово даже после этого чаще всего обходит 8-битки по плотности команд когда действительно надо работать со словами. Даже если это такая мощная 8-битка как Z80 где обработка слов есть. Причём обходит заметно, причём разбазаривая биты и не делая сложных режимов адресации. Это забавно.
avatar
P.S. «Семь байт» читать как «Семь слов»
avatar
Так с машинной точки зрения между статическими и глобальными разницы нет.
разница может быть даже между глобальными и глобальными))

С фига ли? Семь байт на _сложение двух слов из стека с записью результата в стек_. По любому смещению.
Три байта у Z80 тут даже близко нет.
а я говорил — «с верхушки» и не говорил про запись — так будет три

У Z80 есть 16-битные операции. И загрузка и сохранение и арифметика. Но вот…
но вот не всегда они оправданы и нужны
avatar
Можно бесконечно спорить на частных примерах где получается больше, а где меньше. Но это не самоцель же на самом деле. Я просто показал в статье, что меня позабавило, что несмотря на очевидное разбазаривание плотности команд (у того же дедушки PDP-11 с наследниками в виде БК-шек она гораздо выше) весьма базовые действия такие как сложение слов с занесением сразу же результата в целевую ячейку памяти получаются заметно короче нежели на классических 8-битках. Многое конечно будет наоборот заметно больше — например нет ничего похожего даже на LDIR или инкремент регистра всегда осуществляется за слово, а не за байт (хотя инкремент может быть любым числом в диапазоне -8..+7 и может записать результат не в тот регистр из которого бралось первое слагаемое. собственно в архитектуре нет выделенной операции MOV потому что это инкремент на 0 какого-то регистра или ячейки памяти с занесением в другой регистр/ячейку памяти). Ну и тому подобное и так далее.
В 80-х такая архитектура не могла бы появится, потому что там более бережно отнеслись бы к расходу памяти и сделали бы что-нибудь типа PDP-11 или MSP-430. С их довольно ветвистыми системами команд и режимами адресаций.
Я же преследовал простоту всего — отказ от байта это в эту же копилку. Я прекрасно понимаю, что у Simpleton поэтому немало слабых сторон.
Но практикум программирования в эмуляторе показал лично для меня, что да, программировать просто — очень небольшой, буквально с час, период привыкания и всё, ты просто пишешь код состоящий из очень простых операций вида R = X op Y и многое о чём болела постоянно голова в Z80 или 6502 вообще отсутствует как класс.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.