Продолжение статьи «Назад в машинный зал: как собрать эмулятор PDP-11/70 с телетайпом, блэкджеком и без Rust» (первая часть).

Для тех, кто пропустил первую часть: посмотреть живой эмулятор PDP-11 в браузере можно здесь — amesk.github.io/yaPDP/pdp11.html.

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

Работа с BASIC-11 на Teletype Model 33 ASR
Выглядит оно примерно вот так
Вживую оно ещё лучше (под спойлером — видео)

Если интересно — добро пожаловать под cut.

Статья длинная, поэтому приведу оглавление:

Почему я не взял SIMH

«Зачем тебе свой эмулятор, если есть SIMH?» — спрашивали после первой статьи. Вопрос по делу.

SIMH Боба Супника — де-факто стандарт ретро-эмуляции: точность до такта, десятки машин под одним флагом, тысячи часов коллективной отладки. Если хочется копаться во внутренностях PDP-11, гонять XXDP-диагностику или писать свою ОС — SIMH вне конкуренции, тут спорить глупо. Лучшее, что тут может сделать одиночка, — перестать играть в ковбоя и присоединиться к сообществу.

После запуска SIMH мы получаем консоль, в точности эмулирующую работу того самого оборудования: можно грузить ОС и работать. Но погружения, которого я искал, эта консоль не даёт.

Инструменты есть, но для меня магия не сработала — долго, трудно, и всё равно не то

Утверждать, что в SIMH мы ограничены голым терминалом, — неправда.

Для SIMH сообщество сделало немало инструментов, которые сильно меняют ощущения от работы. Я попробовал вот эти:

BlinkenBone (проект Йорга Хоппе): это, пожалуй, самый известный и развитый проект. Он позволяет запускать реалистичные Java-панели для многих классических машин, включая PDP-11/40, PDP-11/70 и PDP-10.

Работа с PDP-11/70 в BlinkenBone
Работа с PDP-11/70 в BlinkenBone

Штука прекрасная, но решает только часть задачи с пультом, а не всей периферией.

MAME: мощная программа, название которой расшифровывается как Multiple Arcade Machine Emulator (эмулятор множества аркадных машин). Главная цель проекта — сохранить историю видеоигр, точно воссоздавая «железо» аркадных автоматов и домашних консолей на современных компьютерах.

Всё богатство MAME в плане выбора устройств
Всё богатство MAME в плане выбора устройств

Консоли и аркады меня не заинтересовали, но там есть визуально очень точная эмуляция VT52, VT240 и, наверное, десятка других терминалов.

Я воодушевился, полез ставить и настраивать, но упёрся лбом в проблемы.
Если коротко — стабильной работы интересных мне терминалов MAME совместно с SIMH добиться не удалось. ROM для них пришлось искать по всему интернету, и нашёлся только для похожего терминала — более современного, зато входящего в MAME. К установленной у меня версии он всё равно не подошёл, пришлось брать ещё менее привлекательный терминал, с ним завелось (кстати, очень красивое), а вся связка с SIMH стартовала с очень большим скрипом и требовала многошаговой подготовки, которая к моменту начала работы уже убивала желание идти дальше.

Самое печальное — не было готовых эмуляторов того, что нужно — хотелось нырнуть ещё лет на 20 глубже, в эпоху телетайпов и перфолент, когда рождались Unix и те паттерны, которые мы используем почти каждый день, часто даже не осознавая этого.

Моя цель — вернуть атмосферу того самого машинного зала и машин, которые там работали.

Посмотрите на живой пример работы yaPDP:

Самое цепляющее применение дисплея VT11, которое я смог получить
Самое цепляющее применение дисплея VT11, которое я смог получить

На видео под спойлером — загрузка системы, работа терминала и запуск классической игры Lunar Lander. Слышно телетайп, видно задержку возврата каретки, а на векторном дисплее VT11 идёт та самая посадка.

Вживую оно ещё больше цепляет

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

Но цепляет очень сильно, почти так же, как посадка в детстве лунного модуля на «Электронике МК-61».

Кстати, забавный факт — в игре почти через 60 лет нашли ошибку.

В итоге получилось то, что получилось.

Хотелось, чтобы задержки были видны, звуки слышны, а перфоратор, световое перо и принтер вели себя как настоящие.

В этом смысле мне есть с чем сравнивать — пусть и на исходе эпохи, но я застал эти машины работающими, когда в 14 лет бегал к родителям на работу, так что воспоминания есть свои, не из YouTube.

Добиться запуска «в один клик», без многочасовой медитации с настроечными файлами, для меня было принципиально важно. Пусть оборудования меньше, зато оно заработает сразу.

Так и появился yaPDP — со всеми его возможностями и ограничениями.

Что меня подтолкнуло к созданию собственного эмулятора

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

Лет десять у меня в закладках лежал google60 — проект Masswerk, где работа в машинном зале ощущается прямо в браузере. Там я впервые увидел, что так вообще можно, а позже его движок стал основой моей консоли.

Проект Алексея Морозова по сборке «Микро-80» впечатлил другим: отладкой прошивки и адаптацией CP/M под его компьютер прямо в браузере, на JavaScript. И тем, что после такой виртуальной подготовки программа почти без изменений заработала на реальном железе.

Последняя точка — отпуск в Москве и поход в Яндекс.Музей. Разговор со смотрителем о виртуальных экспонатах рядом с настоящим работающим железом окончательно убедил: все инструменты уже под рукой, пора делать.

Инженерные компромиссы

На протяжении всей моей карьеры я не помню ни одного проекта, который не был бы набором компромиссов, выбор которых определялся бы исходом войны хотелок и технических/организационных возможностей, а в худшем случае — особенностями внутренних нейронных сетей Заказчика и Исполнителя (чтобы не писать неприличное).

Надеюсь, что в таком небольшом проекте мне удастся остаться в рамках «хорошего случая».

Главный компромисс был всё тот же: между зудом сделать всё «здесь и сейчас» и трезвой оценкой, что на такой проект может не хватить ни сил, ни квалификации.

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

Роковая ошибка?

Нельзя сказать, что я не видел [PCjs](https://github.com/jeffpar/pcjs) в части PDP-11, но реализация на тот момент показалась мне сильно более сложной… а у меня зудело.

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

После этого костяк эмулятора прямо нарисовался перед глазами:

  • одностраничное веб-приложение, легко портируемое на десктоп (привет Яндекс.Музею!)

  • чистый JavaScript-эмулятор PDP-11/70 (заимствуем у Пола, PDP-11/45 выкидываем)

  • машинный пульт (заимствуем у Пола)

  • консоль (перелицовываем принтер Masswerk и добавляем ввод от перфоратора)

  • универсальный слой доступа к образам дисков (заимствуем у Пола)

  • страница управления устройствами ввода-вывода

  • страница конфигурации

  • использование Tauri как средства лёгкого создания standalone-приложения (до этого был опыт работы с Electron, технология была знакомая)

Первая сборка, имея такой богатый арсенал, была разработана буквально за две недели по вечерам — она успешно грузила имеющиеся операционные системы, позволила поиграть в Adventure, поработать в Unix V5 и 2.11BSD.

Но нужно понимать — за быстрый старт, serverless-архитектуру и богатую экосистему для веба приходится дорого платить, некоторых возможностей в таком приложении не будет никогда.

Ограничения платформы

  • Отдельного окна не будет. Оторвать терминал или принтер в своё окно и поставить их бок о бок с основным нельзя: весь backend эмулятора живёт в той же вкладке, что и интерфейс. В десктопной версии такое окно сделать можно, для этого и появится серверная часть на Rust, но тогда веб- и десктоп-версии начнут расходиться, а расхождение версий дороже выигранного окна.

  • Идею с множеством взаимодействующих машин тоже придётся «положить под сукно». Сетевой мост на Rust пытаться делать можно. Но Rust возьмёт на себя только разбор сетевых пакетов: поднимать интерфейсы TUN/TAP придётся руками, причём для каждого поддерживаемого окружения. Это пока без меня)

  • Я выбрал работу на чистом JavaScript + CSS, при разрастании кодовой базы сверх определённого предела это станет проблемой (React, Vue и остальное придумали умные люди не зря). Но это цена быстрого старта и простоты в начале разработки.

Дальше уже не «нельзя», а «может быть плохо». Первое: тайминги могут поплыть, если увлечься аудио- и видеоэффектами, вплоть до полной поломки. Второе я уже поймал руками — приложение живёт либо в браузере, либо в системном WebView, который использует Tauri, и на разных платформах интерфейс ведёт себя по-разному: разрабатывал я на Windows 11 + Chrome, а проверял на Windows 10 + Edge в VirtualBox. На Ubuntu 24.04 с Firefox проблем при этом не было.

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

Сильные стороны

  • Установки нет вообще: открыл ссылку в браузере — эмулятор уже работает. Ни настроечных файлов, ни предварительной подготовки образов. Для школы, доклада или самостоятельного изучения — этого достаточно.

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

  • Внутри есть своеобразный «Быстрый старт» — тот, кто запустил эмулятор впервые, не остаётся один на один с чёрным экраном: начать работу можно в два клика мышкой.

  • Десктопная версия собирается из той же кодовой базы и с теми же образами. Отдельного продукта «для standalone» не существует, пользовательский опыт общий, код общий (пока что) — никаких сюрпризов.

Дальше — плюсы, которые вижу уже я как разработчик:

  • Готовая база под подходящими лицензиями, которая делает то, что мне нужно: систему команд, прерывания и диспетчер памяти с нуля писать не пришлось.

  • Выразительных средств для UI и звука хватает с запасом, и примеров в сети много (со своим Qt и QML на основной работе я тихо завидую).

  • Инсталлятор условно бесплатный, причём сразу нескольких видов. Обновления, судя по всему, тоже есть, но с ними я пока не разбирался — в Electron они были, и я даже пилотно их использовал.

  • Кросс-платформенность для десктопа достаётся почти бесплатно: Tauri делает 99% работы за нас. От меня требуется только тщательно проверить, что артефакты на заявленных платформах запускаются и ведут себя так же, как веб-версия. Для этого у меня есть VirtualBox.

  • Единый стек для основного кода, инструментальных утилит и сборки: всё на JavaScript, отдельного тулчейна для инструментов не понадобилось.

  • Возможности отладки эмулятора практически не ограничены (мне бы такие на основной работе). Об этом напишу ниже.

Будни разработчика

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

Поэтому я использую целую пачку npm-скриптов из package.json: они закрывают всё, от сборки и валидации до служебной зачистки рабочего пространства. Для оркестрации задач я нашёл Wireit от Google — для моих целей он заменил привычные мне GNU make и CMake.

Если «это работает на твоей машине» — это совсем не значит, что процесс будет повторяемым (завтра может перестать работать и на твоей, плавали, знаем). Поэтому любой артефакт у меня собирается только в CI/CD-конвейере: так версии не перемешиваются и файлы не теряются. Очень выручает то, что я перенёс проект на GitHub — изначально я хостил его на GitVerse.ru, но именно на этапе налаживания CI/CD начались сбои, разбираться с которыми не было сил. На GitHub достаточно большие квоты на артефакты для open-source-проектов, да и раннеры есть для всех интересных мне платформ. Забот с кастомными раннерами мне и по основной работе хватает, тащить их поддержку сюда не хочу.

В итоге забесплатно получается:

  • Issue tracker

  • Хранилище релизов

  • Лендинг на GitHub Pages (вот здесь как раз стреляет serverless-природа эмулятора, иначе пришлось бы заморачиваться с хостингом)

  • GitHub workflows, которые позволяют мне автоматизировать проектные задачи. Сейчас задействованы вот такие:

    • ci.yml — проверка каждого PR

    • pages.yml — публикация лендинга и самого эмулятора на GitHub Pages (оказалось очень удобно)

    • release.yml — в одно движение делает draft релиза и собирает нужный набор артефактов

    • clean-history.yml — вместе с retention policy от GitHub держит историю сборок в обозримом состоянии (иначе эту бесконечную простыню от автоматических workflow смотреть невозможно).

Ещё один workflow — про видео

Если честно, есть ещё один workflow — [videos.yml](https://github.com/amesk/yaPDP/blob/master/.github/workflows/videos.yml). Он автоматизирует производство видеороликов о работе yaPDP, а тема эта настолько большая, что тянет на отдельную статью.

Откровенно поразило то, что для моего игрушечного проекта мне же потребовались формальные документы — я реально написал RELEASING.md раньше, чем отладил сам workflow, и им же пользовался, чтобы этот workflow собрать. Как-то незаметно хобби превращается во вторую работу))

Отладка

При таком стеке возможности для отладки и инструментирования практически неограниченные.

Самое главное — весь интерпретатор написан на JavaScript, поэтому его можно править прямо в процессе отладки, как того требует задача. Начиная от банального «напечатать в консоль» какое-то значение (например, при записи и чтении определённого набора регистров) и заканчивая хуком на вызов своего кода: он в нужный момент получает управление, что-то протоколирует и фиксирует, ПРАВИТ память и регистры эмулятора и возвращает управление обратно.

Вторая киллер-фича — возможность пользоваться инструментами для JavaScript. Например, Puppeteer — библиотека Node.js от команды Google, которая управляет браузерами Chrome и Chromium по протоколу DevTools. Работает она по умолчанию в headless-режиме, без графического интерфейса.

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

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

И этот же движок работает не только на отладку: им я снимаю скриншоты областей экрана для документации и записываю демонстрационные ролики. Подробнее об этом — в следующем подразделе.

Документирование

Поскольку в проекте я делаю упор на визуал, в пользовательской документации на лендинге всегда должны быть актуальные картинки. Обновлять их руками — не вариант, поэтому скриншоты загруженных ОС, интерфейса и иконок делаются прямо в Puppeteer и сразу вставляются в документацию.

Всё автоматизировано (набор скриншотов и поддерживаемых ОС мутирует медленно, а вот пользовательский интерфейс эмулятора меняется непрерывно):

npm run screenshots:os
npm run screenshots:manual

Запускаются сценарии tools/screenshots-os.js и tools/screenshots-manual.js, которые выполняют всю черновую работу. Можно сделать гранулярно, задав параметрами только то, что требуется, а можно и пакетно. В TODO остаётся нечёткое сравнение новых скриншотов с уже имеющимися, чтобы не плодить в git замены двоичных артефактов там, где картинка не изменилась. Пока этого нет, но текущему процессу отсутствие оптимизации не мешает.

Тем же Puppeteer снимается и видео: tools/record-video.js записывает прогон эмулятора в браузере, а дальше ролик собирается отдельным конвейером — тем самым videos.yml из спойлера выше. То есть документация и ролики делаются одной обвязкой.

Модификация эмулятора и архитектурные долги

Эмулятор Пола Нанкервиса я взял именно за его понятность и простоту. Быстренько набросал прототип, который заводится и делает что-то, напоминающее то, что мне нужно.

И попутно его испоганил, внеся в src/iopage.js знание о DOM-модели UI, какие-то хуки по месту и прочие предосудительные вещи. Почему так вышло? Отчасти из-за спешки, отчасти из-за отсутствия в эмуляторе понятия шины — там всё «пришито» по месту. Этим, кстати, он и отличается от PCjs, где этот вопрос решён.

И теперь уже моя очередь заниматься рефакторингом и исправлением ситуации. Без этого дальше двигаться никак нельзя.

Но опять меня спасает Puppeteer — ценой скорости выполнения я могу создать любой нужный мне e2e-тест, который будет смотреть на продукт «в целом». После получения достаточного количества таких e2e-тестов я могу рискнуть начать выполнять рефакторинг, не опасаясь слишком сильно, что на этом проект и закончится. У меня всегда есть работающий вариант, от которого можно «танцевать».

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

И здесь у меня есть ещё один бонус от эпохи — это XXDP, специализированная диагностическая ОС от DEC. Запустив эмулятор и смонтировав диски, я настраиваю доступ к консоли для ввода-вывода (привет, SIMH!) и прогоняю родные диагностики DEC: сначала вручную, потом автоматизированно.

Первый камень преткновения

Первая диагностика прошла прекрасно, а вот на второй (`EKBBF0`) я получил сообщение:

LOOK AT CONSOLE LIGHTS: DATA LIGHTS SHOULD READ 166667, ADDRESS LIGHTS SHOULD READ 032236, CHANGE SWITCH 7 TO CONTINUE

В переводе: «Посмотрите на лампы пульта: на лампах данных должно быть 166667, на лампах адреса — 032236, для продолжения переключите тумблер 7».

Учитывая, сколько уже сделано, такое требование кодируется почти не задумываясь (для PDP-11 это всего лишь чтение и запись регистров по определённому адресу). Так что работа продолжится.

Для удобства я сделал утилиту tools/headless-term.js (наследника tools/rt11-term.js, который гонялся с включённым Puppeteer), как раз и позволяющую делать ввод-вывод, монтировать образы и ленты.

Так что задел тут хороший. Работа продолжается — диагностику теперь можно прогнать одной командой: tests/e2e-xxdp-ekbbf0.js.

Параллельно висел ещё один долг — персистентность дисков, она была не совсем честной: модифицированные блоки жили только в памяти JavaScript. Процессор можно было остановить и перезагрузить — изменения это переживали, а перезагрузка страницы их теряла. Сделать это, кстати, удалось быстро, ещё до окончания работ над рефакторингом: использовал локальное хранилище браузера.

А вот рефакторинг дал мне возможность сделать другую важную вещь — снапшоты. Это как в игровых ретро-консолях: нажал кнопку «Save», потом через неделю вернулся — а игра стартует с той точки, на которой завершилась. Вот так и yaPDP — загруженная месяц назад 2.11BSD продолжит играть в Adventure или с характерным грохотом печатать вторую половину документа.

Что дальше

Здесь у меня развилка.

  • Очень хочется получить MVP, который уже на 100% решает задачу погружения: параллельно осваивая плохо знакомый мне стек, я всё-таки добился нужного эффекта

Работа с BASIC-11 на Teletype Model 33 ASR
Ощущение уже другое, хотя и не полностью исторично: почти все кнопки нажимаются, локальный режим работает, перфоленты вводятся и выводятся, а иногда и дублируются (в 60-х так и делали)
  • Хочется продолжить работу над рефакторингом ядра, выпилить оригинал и добиться прохождения бОльшего количества XXDP-диагностик, которые позволят увеличить количество гарантированно работающего с yaPDP DEC-софта

Первые шаги уже видны в репозитории: src/core/bus.js, src/core/device.js и src/core/machine.js.

А что посоветуете вы?