Продолжение статьи «Назад в машинный зал: как собрать эмулятор PDP-11/70 с телетайпом, блэкджеком и без Rust» (первая часть).
Для тех, кто пропустил первую часть: посмотреть живой эмулятор PDP-11 в браузере можно здесь — amesk.github.io/yaPDP/pdp11.html.
Первая часть была про магию: как оно вообще заработало. Эта — про обычную инженерную прозу: нетерпение, компромиссы и их последствия.

Вживую оно ещё лучше (под спойлером — видео)
Если интересно — добро пожаловать под cut.
Статья длинная, поэтому приведу оглавление:
Почему я не взял SIMH
«Зачем тебе свой эмулятор, если есть SIMH?» — спрашивали после первой статьи. Вопрос по делу.
SIMH Боба Супника — де-факто стандарт ретро-эмуляции: точность до такта, десятки машин под одним флагом, тысячи часов коллективной отладки. Если хочется копаться во внутренностях PDP-11, гонять XXDP-диагностику или писать свою ОС — SIMH вне конкуренции, тут спорить глупо. Лучшее, что тут может сделать одиночка, — перестать играть в ковбоя и присоединиться к сообществу.
После запуска SIMH мы получаем консоль, в точности эмулирующую работу того самого оборудования: можно грузить ОС и работать. Но погружения, которого я искал, эта консоль не даёт.
Инструменты есть, но для меня магия не сработала — долго, трудно, и всё равно не то
Утверждать, что в SIMH мы ограничены голым терминалом, — неправда.
Для SIMH сообщество сделало немало инструментов, которые сильно меняют ощущения от работы. Я попробовал вот эти:
BlinkenBone (проект Йорга Хоппе): это, пожалуй, самый известный и развитый проект. Он позволяет запускать реалистичные Java-панели для многих классических машин, включая PDP-11/40, PDP-11/70 и PDP-10.

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

Консоли и аркады меня не заинтересовали, но там есть визуально очень точная эмуляция VT52, VT240 и, наверное, десятка других терминалов.
Я воодушевился, полез ставить и настраивать, но упёрся лбом в проблемы.
Если коротко — стабильной работы интересных мне терминалов MAME совместно с SIMH добиться не удалось. ROM для них пришлось искать по всему интернету, и нашёлся только для похожего терминала — более современного, зато входящего в MAME. К установленной у меня версии он всё равно не подошёл, пришлось брать ещё менее привлекательный терминал, с ним завелось (кстати, очень красивое), а вся связка с SIMH стартовала с очень большим скрипом и требовала многошаговой подготовки, которая к моменту начала работы уже убивала желание идти дальше.
Самое печальное — не было готовых эмуляторов того, что нужно — хотелось нырнуть ещё лет на 20 глубже, в эпоху телетайпов и перфолент, когда рождались Unix и те паттерны, которые мы используем почти каждый день, часто даже не осознавая этого.
Моя цель — вернуть атмосферу того самого машинного зала и машин, которые там работали.
Посмотрите на живой пример работы yaPDP:

На видео под спойлером — загрузка системы, работа терминала и запуск классической игры 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% решает задачу погружения: параллельно осваивая плохо знакомый мне стек, я всё-таки добился нужного эффекта

Хочется продолжить работу над рефакторингом ядра, выпилить оригинал и добиться прохождения бОльшего количества XXDP-диагностик, которые позволят увеличить количество гарантированно работающего с yaPDP DEC-софта
Первые шаги уже видны в репозитории: src/core/bus.js, src/core/device.js и src/core/machine.js.
А что посоветуете вы?

