Пишем эмулятор приставки ч2, или немного о CHIP16

в 13:25, , рубрики: crazy, emulator, ненормальное программирование, Программирование, разработка, метки: ,

В предыдущей своей небольшой заметке я описывал принцип построения эмулятора старой игровой платформы CHIP-8 из далеких 70-х. Здесь же речь пойдет о своего рода наследнице – CHIP16. Итак, что же такое CHIP16?

Пишем эмулятор приставки ч2, или немного о CHIP16 CHIP16 – “вымышленная” игровая приставка, которой никогда не существовало в “железе”. Всю спецификацию на нее разрабатывали (-ют) энтузиасты с одного англоязычного форума. Смысл в том, чтобы максимально упростить написание эмулятора, иметь хорошую документацию и поддержку комьюнити. Тем самым позволяя даже новичкам в программировании создать полностью рабочий эмулятор с нуля на фактически любом языке программирования. Сразу оговорюсь, что здесь я не буду приводить примеры кода эмулятора, цель – просто рассказать об этой платформе. И да, конечно все Just for fun!

Предыстория

Пишем эмулятор приставки ч2, или немного о CHIP16 А все началось где-то в 2010 году, с тех пор спецификация приобрела номер версии 1.1, основные недочёты были устранены, были написаны основные инструменты для создания игр и других программ под эту платформу (ассемблер, отладчик, конвертер изображений и прочее). Основное отличие от той-же CHIP-8 это наличие большего разрешения экрана, большего количества цветов, большего количества арифметических и других инструкций, улучшенной поддержки звукового сопровождения и отсутствие недокументированных фич.

Приставка имеет 16-ти разрядный процессор, память, два устройства ввода (джойстики типа Dendy), звуковую и видео подсистему. Рассмотрим всю эту кучу подробнее.

Процессор

Процессор включает:

  • Один 16-разрядный счетчик команд (PC)
  • Один 16-разрядный указатель стека (SP)
  • Шестнадцать 16-разрядных регистра общего назначения (R0 – RF)
  • Один 8-разрядный регистр флагов

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

Память

Всего 64Kb (65536 байт). Память распределяется следующим образом:
0x0000 – Начало бинарных данных
0xFDF0 – Начало стека (512 байт).
0xFFF0 – Порты ввода/вывода (I/O, для отслеживания состояния джойстиков).

Видео

Разрешение экрана 320x240 пикселей. Одновременное количество отображаемых цветов – 16 из стандартной палитры.

Индекс в палитре Шестнадцат. значение Цвет
0x0 0x000000 Черный, прозрачный на слое фона
0x1 0x000000 Черный
0x2 0x888888 Серый
0x3 0xBF3932 Красный
0x4 0xDE7AAE Розовый
0x5 0x4C3D21 Темно-коричневый
0x6 0x905F25 Коричневый
0x7 0xE49452 Оранжевый
0x8 0xEAD979 Желтый
0x9 0x537A3B Зеленый
0xA 0xABD54A Светло-зеленый
0xB 0x252E38 Темно-синий
0xC 0x00467F Синий
0xD 0x68ABCC Светло-синий
0xE 0xBCDEE4 Sky blue
0xF 0xFFFFFF Белый

Пишем эмулятор приставки ч2, или немного о CHIP16 Есть возможность устанавливать собственную цветовую палитру. Частота обновления экрана 60 кадров в секунду, при этом каждый кадр выставляется внутренний флаг Vblank (через каждые ~16ms). Процессор имеет возможность ожидать завершения отрисовки кадра с помощью инструкции VBLNK. Изображение формируется из спрайтов, прямого доступа к видеопамяти нет. Помимо основного слоя, где рисуются спрайты, присутствует слой фона, который заполняет весь экран одним из 16-ти цветов.

Экран зеркально не отображается, таким образом все, что не влезло в физические координаты – остается за экраном и не отображается. Из этого следует, что спрайты могут иметь отрицательные координаты (либо превышающие 320x200). Если взять спрайт 4x4 пикселя и, к примеру, отдать команду нарисовать его по координатам (-2,-2), то в верхнем левом углу экрана отобразиться часть спрайта размером 2x2 пикселя.

Так, как всего возможных цветов в палитре 16, то чтобы закодировать одну точку на экране необходимо 4 бита. Один байт является блоком из двух точек. Минимальный спрайт состоит из одного байта – это спрайт размером 2x1 (две точки). Давайте для примера посмотрим как закодировать спрайт размером 8x5 пикселей, если учесть что белый цвет в стандартной палитре это цвет с индексом 0xFh, а черный — с индексом 0x1h
Пишем эмулятор приставки ч2, или немного о CHIP16
Итого, 20 байт. То есть (8 x 5) / 2 = 20

Если новый спрайт перекрывает любые уже существующие пиксели на экране (за исключением пикселей с нулевым цветом – они прозрачны), то устанавливается флаг переполнения (carry flag). Таким образом, можно отслеживать столкновения и коллизии объектов на экране в играх.
Для назначения собственной цветовой палитры вместо стандартной существует специальная команда процессора PAL. Каждый цвет палитры состоит из 3-х байт, в формате RGB. Так как используется всего 16 цветов, то команда PAL прочитает из памяти массив в 48 байт и назначит RGB компоненты цветам с индексами 0x0, 0x1,..,0xF. Палитра сменяется после получения процессором команды VBLNK.

Звук

В начальных версиях присутствовала возможность проигрывать только три фиксированных тона (500Hz, 1000Hz, 1500Hz) определенное количество миллисекунд, но затем добавилась возможность использовать звуковой генератор типа ADSR

Устройства ввода (джойстики)

Доступ к информации с джойстиков осуществляется посредством отображаемых в памяти портов ввода-вывода. Первый джойстик по адресу 0xFFF0, второй – 0xFFF2.
Bit[0] – Up (Вверх)
Bit[1] — Down (Вниз)
Bit[2] — Left (Влево)
Bit[3] — Right (Вправо)
Bit[4] — Select (Выбор)
Bit[5] — Start (Старт)
Bit[6] — A
Bit[7] — B
Bit[8 — 15] — Не используются, всегда равны нулю.

Таким образом, прочитав значение из памяти по адресу 0xFFF0 в регистр и проверяя соответствующие установленные биты можно отслеживать какие кнопки в данный момент нажаты на первом джойстике.

Формат файлов-образов (ROM-файлов)

ROM файл (стандартное расширение .c16) содержит в себе заголовок и бинарные данные сразу после заголовка. Информация из заголовка может быть использована для определения версии спецификации и пр. Заголовок имеет постоянный размер 16 байт. Его формат:

Смещение Назначение
0x00 Магическое число 'CH16'
0x04 Зарезервированно
0x05 Версия спецификации (первые 4 бита=главная версия, вторые 4 бита=подверсия, т.е 0.7 = 0x07 и 1.0 = 0x10)
0x06 Размер ROM-файла (не включая заголовок, в байтах)
0x0A Начальный адрес (Значение счетчика команд PC)
0x0C CRC32 контрольная сумма (не включая заголовок, полином = 0x04c11db7)

Сразу за ним начинаются бинарные данные, которые всегда должны быть прочитаны в память начиная с адреса 0x0000. Счетчик команд должен быть установлен в соответствии со значением в заголовке. Как правило это 0x0000.

ROM-файлы могут и не иметь заголовка вовсе, так как он был введен в спецификацию сравнительно недавно.

Регистр флагов

Bit[0] Зарезервирован
Bit[1] c — carry flag (беззнаковое переполнение)
Bit[2] z — zero flag
Bit[3] Зарезервирован
Bit[4] Зарезервирован
Bit[5] Зарезервирован
Bit[6] o — Overflow (переполнение чисел со знаком)
Bit[7] n — negative (флаг отрицательного знака)
Типы условий для команд условного перехода

Условия используются для команд условного перехода (прыжков) или команд условного вызова подпрограмм. К примеру: «jle метка» или «cno some_label2». В квадратных скобках приводится состояние флагов, когда условие срабатывает.

Z 0x0 [z==1] Equal (Zero)
NZ 0x1 [z==0] Not Equal (Non-Zero)
N 0x2 [n==1] Negative
NN 0x3 [n==0] Not-Negative (Positive or Zero)
P 0x4 [n==0 && z==0] Positive
O 0x5 [o==1] Overflow
NO 0x6 [o==0] No Overflow
A 0x7 [c==0 && z==0] Above (Unsigned Greater Than)
AE 0x8 [c==0] Above Equal (Unsigned Greater Than or Equal)
B 0x9 [c==1] Below (Unsigned Less Than)
BE 0xA [c==1 || z==1] Below Equal (Unsigned Less Than or Equal)
G 0xB [o==n && z==0] Signed Greater Than
GE 0xC [o==n] Signed Greater Than or Equal
L 0xD [o!=n] Signed Less Than
LE 0xE [o!=n || z==1] Signed Less Than or Equal

Так же можно использовать альтернативные мнемоники:

NC 0x8 [c==0] Not Carry (Same as AE)
C 0x9 [c==1] Carry (Same as B)
Команды процессора

Любой опкод CHIP16 занимает ровно 4 байта (32 бита).
HH — старший байт.
LL — младший файт.
N — nibble (4х-битное значение).
X, Y, Z — 4х-битный идентификатор регистра.

Опкод Мнемоника Использование
00 00 00 00 NOP Нет операции, просто один цикл процессора
01 00 00 00 CLS Очистка экрана (основной слой очищается, цвет фона устанавливается в цвет с индексом 0)
02 00 00 00 VBLNK Ожидать вертикальной синхронизации. Если кадр не успел отрисоваться, то PC-=4
03 00 0N 00 BGC N Установить цвет фона с индексом N. Если индекс равен 0, то цвет фона — черный
04 00 LL HH SPR HHLL Установить размер спрайта: ширину (LL) и высоту (HH)
05 YX LL HH DRW RX, RY, HHLL Нарисовать спрайт из адреса в памяти HHLL по координатам, заданным в регистрах X и Y. Результат влияет на carry flag
06 YX 0Z 00 DRW RX, RY, RZ Нарисовать спрайт из адреса в памяти, на который указывает регистр Z по координатам, заданным в регистрах X и Y. Результат влияет на carry flag
07 0X LL HH RND RX, HHLL Поместить случайное число в регистр X. Максимальное значение задается HHLL
08 00 00 00 FLIP 0, 0 Задать ориентацию отображения спрайта. Горизонтальный переворот = НЕТ, вертикальный переворот = НЕТ
08 00 00 01 FLIP 0, 1 Задать ориентацию отображения спрайта. Горизонтальный переворот = НЕТ, вертикальный переворот = ДА
08 00 00 02 FLIP 1, 0 Задать ориентацию отображения спрайта. Горизонтальный переворот = ДА, вертикальный переворот = НЕТ
08 00 00 03 FLIP 1, 1 Задать ориентацию отображения спрайта. Горизонтальный переворот = ДА, вертикальный переворот = ДА
09 00 00 00 SND0 Остановить воспроизведение звука
0A 00 LL HH SND1 HHLL Воспроизводить 500Hz тон HHLL миллисекунд
0B 00 LL HH SND2 HHLL Воспроизводить 1000Hz тон HHLL миллисекунд
0C 00 LL HH SND3 HHLL Воспроизводить 1500Hz тон HHLL миллисекунд
0D 0X LL HH SNP RX, HHLL Воспроизводить звуковой тон в течении HHLL миллисекунд, в соответствии с текущим звуковым генератором, заданным по адресу HHLL
0E AD SR VT SNG AD, VTSR Звуковой генератор ADSR

A = attack (0..15)
D = decay (0..15)
S = sustain (0..15, volume)
R = release (0..15)
V = volume (0..15)
T = type of sound:
  • 00 = triangle wave
  • 01 = sawtooth wave
  • 02 = pulse wave (is just square for now)
  • 03 = noise
  • При неправильных значениях звук не воспроизводится

10 00 LL HH JMP HHLL Перейти на указанный адрес HHLL
12 0x LL HH Jx HHLL Перейти на указанный адрес HHLL с учетом условия 'x'. (см. Типы условий)
13 YX LL HH JME RX, RY, HHLL Перейти на указанный адрес HHLL если регистр X равен регистру Y
16 0X 00 00 JMP RX Перейти на адрес, заданный в регистре X
14 00 LL HH CALL HHLL Вызвать подпрограмму по адресу HHLL. Сохраняет PC в [SP], увеличивает SP на 2
15 00 00 00 RET Возврат из подпрограммы. Уменьшает SP на 2 и восстанавливает PC из [SP]
17 0x LL HH Cx HHLL Если выполняется условие 'x', тогда вызов подпрограммы. (см. Типы условий)
18 0X 00 00 CALL RX Вызвать подпрограмму по адресу, находящемуся в регистре X. Сохраняет PC в [SP], увеличивает SP на 2
20 0X LL HH LDI RX, HHLL Поместить в регистр X непосредственное значение HHLL
21 00 LL HH LDI SP, HHLL Установить указатель стека на адрес HHLL. Не перемещает старые значения в стеке на новый адрес
22 0X LL HH LDM RX, HHLL Поместить в регистр X 16-битное значение из памяти по адресу HHLL
22 YX 00 00 LDM RX, RY Поместить в регистр X 16-битное значение из памяти по адресу, на который указывает регистр Y
24 YX 00 00 MOV RX, RY Скопировать значение регистра Y в регистр X
30 0X LL HH STM RX, HHLL Сохранить значение регистра X в памяти по адресу HHLL
31 YX 00 00 STM RX, RY Сохранить значение регистра X в памяти по адресу, находящемуся в регистре Y
40 0X LL HH ADDI RX, HHLL Добавить непосредственное значение HHLL к регистру X. Влияет на флаги [c,z,o,n]
41 YX 00 00 ADD RX, RY Добавить значение регистра Y к регистру X. Результат помещается в регистр X. Влияет на флаги [c,z,o,n]
42 YX 0Z 00 ADD RX, RY, RZ Добавить значение регистра Y к регистру X. Результат помещается в регистр Z. Влияет на флаги [c,z,o,n]
50 0X LL HH SUBI RX, HHLL Вычесть непосредственное значение HHLL из регистра X. Результат в регистре X. Влияет на флаги [c,z,o,n]
51 YX 00 00 SUB RX, RY Вычесть значение регистра Y из регистра X. Результат помещается в регистр X. Влияет на флаги [c,z,o,n]
52 YX 0Z 00 SUB RX, RY, RZ Вычесть значение регистра Y из регистра X. Результат помещается в регистр Z. Влияет на флаги [c,z,o,n]
53 0X LL HH CMPI RX, HHLL Вычесть непосредственное значение HHLL из регистра X. Результат не сохраняется. Влияет на флаги [c,z,o,n]
54 YX 00 00 CMP RX, RY Вычесть значение регистра Y из регистра X. Результат не сохраняется. Влияет на флаги [c,z,o,n]
60 0X LL HH ANDI RX, HHLL Логическая операция 'И' непосредственного значения HHLL к регистру X. Результат в регистре X. Влияет на флаги [z,n]
61 YX 00 00 AND RX, RY Логическая операция 'И' значения в регистре Y к регистру X. Результат помещается в регистр X. Влияет на флаги [z,n]
62 YX 0Z 00 AND RX, RY, RZ Логическая операция 'И' значения в регистре Y к регистру X. Результат помещается в регистр Z. Влияет на флаги [z,n]
63 0X LL HH TSTI RX, HHLL Логическая операция 'И' непосредственного значения HHLL к регистру X. Результат не сохраняется. Влияет на флаги [z,n]
64 YX 00 00 TST RX, RY Логическая операция 'И' значения в регистре Y к регистру X. Результат не сохраняется. Влияет на флаги [z,n]
70 0X LL HH ORI RX, HHLL Логическая операция 'ИЛИ' непосредственного значения HHLL к регистру X. Результат в регистре X. Влияет на флаги [z,n]
71 YX 00 00 OR RX, RY Логическая операция 'ИЛИ' значения в регистре Y к регистру X. Результат помещается в регистр X. Влияет на флаги [z,n]
72 YX 0Z 00 OR RX, RY, RZ Логическая операция 'ИЛИ' значения в регистре Y к регистру X. Результат помещается в регистр Z. Влияет на флаги [z,n]
80 0X LL HH XORI RX, HHLL Логическая операция 'XOR' непосредственного значения HHLL к регистру X. Результат в регистре X. Влияет на флаги [z,n]
81 YX 00 00 XOR RX, RY Логическая операция 'XOR' значения в регистре Y к регистру X. Результат помещается в регистр X. Влияет на флаги [z,n]
82 YX 0Z 00 XOR RX, RY, RZ Логическая операция 'XOR' значения в регистре Y к регистру X. Результат помещается в регистр Z. Влияет на флаги [z,n]
90 0X LL HH MULI RX, HHLL Умножение непосредственного значения HHLL на регистр X. Результат в регистре X. Влияет на флаги [c,z,n]
91 YX 00 00 MUL RX, RY Умножение значения в регистре Y на регистр X. Результат помещается в регистр X. Влияет на флаги [c,z,n]
92 YX 0Z 00 MUL RX, RY, RZ Умножение значения в регистре Y на регистр X. Результат помещается в регистр Z. Влияет на флаги [c,z,n]
A0 0X LL HH DIVI RX, HHLL Деление регистра X на непосредственное значение HHLL. Результат в регистре X. Влияет на флаги [c,z,n]
A1 YX 00 00 DIV RX, RY Деление регистра X на значение в регистре Y. Результат помещается в регистр X. Влияет на флаги [c,z,n]
A2 YX 0Z 00 DIV RX, RY, RZ Деление регистра X на значение в регистре Y. Результат помещается в регистр X. Влияет на флаги [c,z,n]
B0 0X 0N 00 SHL RX, N Логический сдвиг значения в регистре X влево N-раз. Влияет на флаги [z,n]
B1 0X 0N 00 SHR RX, N Логический сдвиг значения в регистре X вправо N-раз. Влияет на флаги [z,n]
B0 0X 0N 00 SAL RX, N Арифметический сдвиг значения в регистре X влево N-раз. Влияет на флаги [z,n]. Аналогична команде SHL
B2 0X 0N 00 SAR RX, N Арифметический сдвиг значения в регистре X вправо N-раз. Влияет на флаги [z,n]
B3 YX 00 00 SHL RX, RY Логический сдвиг значения в регистре X влево на значение, находящееся в регистре Y. Влияет на флаги [z,n]
B4 YX 00 00 SHR RX, RY Логический сдвиг значения в регистре X вправо на значение, находящееся в регистре Y. Влияет на флаги [z,n]
B3 YX 00 00 SAL RX, RY Арифметический сдвиг значения в регистре X влево на значение, находящееся в регистре Y. Влияет на флаги [z,n]. Аналогична команде SHL
B5 YX 00 00 SAR RX, RY Арифметический сдвиг значения в регистре X вправо на значение, находящееся в регистре Y. Влияет на флаги [z,n]
C0 0X 00 00 PUSH RX Поместить значение регистра X в стек. Увеличивает SP на 2
C1 0X 00 00 POP RX Уменьшает SP на 2. Восстановить значение регистра X из стека.
C2 00 00 00 PUSHALL Сохранить значения всех регистров общего назначения (r0-rf) в стеке. Увеличивает SP на 32
C3 00 00 00 POPALL Уменьшает SP на 32. Восстановить значения всех регистров общего назначения (r0-rf) из стека.
C4 00 00 00 PUSHF Сохранить состояние регистра флагов в стеке. Биты 0-7 основные флаги, биты 8-15 пусты (всегда ноль). Увеличивает SP на 2
C5 00 00 00 PUSHF Уменьшает SP на 2. Восстановить состояние регистра флагов из стека
D0 00 LL HH PAL HHLL Загрузить палитру находящуюся по адресу HHLL, 16*3 байт, RGB-формат; начнет действовать сразу после последнего VBlank
D1 0x 00 00 PAL Rx Загрузить палитру находящуюся по адресу в регистре X, 16*3 байт, RGB-формат; начнет действовать сразу после последнего VBlank
Где почитать подробнее, как «пощупать»?

Почитать подробнее можно, как я уже говорил, на англоязычном форуме.
Первая (и уже устаревшая и закрытая) тема с обсуждением CHIP16
Вторая тема с обсуждением (основная). Здесь вся информация собирается в первом посте темы. Там спецификация, инструменты, примеры программ. Нужна регистрация, что бы что-то скачивать с форума.

Пишем эмулятор приставки ч2, или немного о CHIP16 Хороший эмулятор RefCHIP16: code.google.com/p/refchip16/downloads/list Исходники на Си++, имеется возможность как простой интерпретации, так и AOT (Ahead Of Time) компиляции, что несомненно доставляет. Пожалуй единственный нормальный эмулятор, который корректно обрабатывает спецификацию 1.1 (в особенности ADSR звук).

Немного устаревший набор игр, демок и тестовых образов для CHIP16. Там же исходники на ассемблере для большинства программ: rghost.ru/38862474 Новые игры и программы выкладываются в теме на форуме.

Разработка

Программы и игры для CHIP16 пока в подавляющем большинстве пишутся на ассемблере. Скачать его можно здесь: code.google.com/p/tchip16/downloads/list Для примера, возьмем наш спрайт из этого топика (стрелочку) и выведем его на экран. Для этого открываем любой текстовый редактор, создаем пустой файл habr.asm и пишем туда команды:

     spr #0504       ; установим размер спрайта 8x5
     ldi r0,10       ; в регистр r0 - X координата
     ldi r1,10       ; в регистр r1 - Y координата
     drw r0,r1,arrow ; выведем спрайт на экран по координатам (10,10)

end: jmp end         ; бесконечный цикл

; Спрайт
arrow: db #f1, #11, #11, #ff
       db #f1, #1f, #ff, #ff
       db #f1, #f1, #ff, #ff
       db #f1, #ff, #1f, #ff
       db #f1, #ff, #f1, #ff

После чего компилируем программу с помощью данной команды:
tchip16.exe habr.asm -o habr.c16
Затем открываем получившийся файл habr.c16 в эмуляторе и наслаждаемся видом черной стрелочки на белом фоне :)

Для отладки сложных алгоритмов можно использовать мой отладчик — chip16debugger: code.google.com/p/chip16debugger/downloads/list
image
Пока альфа версия, говнокод, наверняка с багами, но лучше чем ничего. Иногда реально помогает словить баг.

Эмулятор… А что еще прикольного можно замутить?

Ну, например какой-нибудь компилятор (или транслятор) с языка высокого уровня. Я, например, пытался написать транслятор с паскале-подобного языка. Кое-что конечно получилось, но до полноценного языка явно не дотягивает. Вот такие программки можно было на нем писать:

var
   xPixels, yPixels, xStart, yStart, Xsize, YSize, maxiter : integer;
   xStep, yStep : integer;
   ix,iy,x,y,x0,y0,iteration,xtemp : integer;
   dist : byte;
   temp : byte;
   xx,yy : byte;

begin
  XPixels := 160;
  YPixels := 100;
  XStart := $FF9c;
  YStart := $FFce;
  XSize := 160;
  YSize := 100;
  MaxIter := 16;

  XStep := XSize div XPixels;
  YStep := YSize div YPixels;

  yy := 20;
  For iy := 0 to yPixels do
    begin
          xx := 0;
          For ix := 0 to xPixels do
            begin
               x := xStart + ix * xStep;
               y := yStart + iy * yStep;
               x0 := x;
               y0 := y;
               iteration := 0;
               Repeat
                     xtemp := ((x*x) div 48) - ((y*y) div 48) + x0;
                     y := 2*((x*y) div 48) + y0;
                     x := xtemp;
                     iteration := iteration + 1;
                     dist := ((x*x) div 48) + ((y*y) div 48);
                     If iteration = maxiter then dist := 4000;
               Until dist > 192;

               If iteration <> maxiter then
                If iteration > 1 then
                begin
                 temp := ((iteration shl 4) or iteration) shl 8;
                 temp := temp or ((iteration shl 4) or iteration);
                 DrawSprite(xx,yy,$0201,^temp);
                end;
               xx := xx + 2;
            end;
          yy := yy + 2;
    end;
end.

На выходе получалась тонна ассемблерного говнокода, который компилился вышеуказанным ассемблером tchip16. В итоге это как-то работало и давало такую картинку:
Пишем эмулятор приставки ч2, или немного о CHIP16

К сожалению забросил, не хватило скила довести все до релиза или хотя бы беты.

Пишем эмулятор приставки ч2, или немного о CHIP16Что еще… Ах да, был случай интересный — хотели замутить CHIP16 демо-компо, типа написание демок под сабж. Ну а что, олдскульно, ресурсы ограниченны, это вам не шойдеры с гигами оперативы. В теории можно вполне крутить старые эффекты, так, как хоть тут нет фрейм-буффера, зато есть 32Kb оперативы под спрайт размером с весь экран. И 32Kb на код остается. Будет даже без морганий экрана. Моя небольшая демка sinedots (такие точки крутятся в пространстве, получается как-бы трехмерные). Правда я тут ступил, и выводил точки именно точками (спрайтами в один байт) из-за чего получается моргание.

Еще можно создать всю платформу аппаратно (для любителей ПЛИС и прочих транзисторов). А я быть может напишу свой эмулятор под эту железяку:
Пишем эмулятор приставки ч2, или немного о CHIP16
И пускай не совсем аппаратно с точки зрения аппаратности (там обычный MIPS процессор), зато все равно интересно.

Всем удачи!

Автор: tronix286

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js