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

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

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

Консоли и аркады меня не заинтересовали, но там есть эмуляция VT52, VT104, VT240 и ещё чего-то.
Обнаружились две проблемы - всё это хозяйство у меня заводилось с очень большим скрипом и нестабильно, и, самое главное, не было эмуляторов того, что нужно - хотелось нырнуть ещё лет на 20 глубже.
В итоге получилось то, что получилось.
Но я строил не часы. Я строил прокуренную серверную семидесятых. Запах озона, гул вентиляторов, щелчки телетайпа, от которых закладывает уши, и тумблеры на пульте, которыми машину можно было разбудить или усыпить. SIMH воспроизводит процессор — а я хотел вернуть помещение, стук и задержку возврата каретки телетайпа (около 180–200 мс), гул раскручивающегося магнитного диска. Окно терминала — это не машинный зал, это замочная скважина.
Отсюда и вся архитектура yaPDP. А любое помещение ограничено несущими конструкциями. В этой статье я честно пройду вдоль четырех балок, на которых всё сейчас держится, — разберу, какие из них я забил кувалдой, какие оставил гнить, а какие собираюсь выправить, когда дойдут руки.
Сначала — короткая карта местности
Чтобы разговор был предметным, вот проект в цифрах:
Параметр | Значение |
|---|---|
Гостевых ОС | 16 — от Unix V5 до RSTS/E 10.1 |
Дистрибутив Tauri, Windows Minimal / Full | ~3.2 МБ / ~84 МБ |
Дистрибутив Tauri, Linux | ~13.5 МБ / ~97.5 МБ |
Весь Rust-слой Tauri | 25 строк кода |
Модульных тестов | три десятка файлов, |
Периферия | Телетайп Model 33 ASR, LP11, VT52, VT11 |
Но цифры — это сухие факты. Посмотрите на живой пример работы yaPDP. На этом видео вы можете увидеть загрузку системы, работу терминала и запуск классической игры Lunar Lander. Звук телетайпа, задержка возврата каретки и работа векторного дисплея VT11 — всё это воссоздано для создания атмосферы машинного зала семидесятых.

Вживую оно ещё больше цепляет
Скажу честно, посадить его мне не удалось ни разу, тут сказывается и отсутствие привычки работать световым пером (надо мышкой на элементы управления наводиться и ждать, нажимать ничего не надо, программа сама поймёт, какой манёвр пользователь хочет делать), и отсутствие понимания правильной организации “Suicidal burn” (это когда мы, максимально экономя топливо, включаем максимальную тягу в самый последний момент с расчётом, что скорость аппарата погасится почти в точке касания поверхности).
Но цепляет очень сильно, почти так же, как посадка в детстве лунного модуля на “Электронике МК-61”))
Кстати, забавный факт - в игре почти через 60 лет нашли ошибку
Балка первая. Один метроном на весь зал: терминалы одной машины и несколько машин
Как хотелось
Настоящая PDP-11 — многопользовательская система. В машинном зале образца 1975 года на одной стойке висело несколько терминалов: консоль оператора стрекотала телетайпом, рядом мигали VT52, а у стены молотил строчный принтер. Важно разделить два сценария, которые легко перепутать:
Сценарий А: одна машина, много экранов. Это штатный режим реального зала.
Сценарий Б: две независимые PDP-11 под одним капотом.
Что получилось
Сценарий А решен «окнами внутри окна». Консоль, до двух дополнительных VT52 и страница принтера — это вкладки одного SPA. Вот как это выглядит в конфиге (src/config.js):
var DEFAULTS = Object.freeze({ consoleType: "teletype", // 'teletype' | 'vt52' userTerminals: 0, // 0 | 1 | 2 — дополнительные VT52 в сайдбаре printer: false, // LP11 строчный принтер vt11: false, // VT11 векторный дисплей ... });

А вот что при этом происходит с консолью

Т.е. всё как положено - системные сообщения идут на консоль, мешая оператору) Доставка сообщений работает.
Сценарий Б так и остался за бортом - никаких сущностей вне основной машины. Технически он решаем через SharedWorker или микро-фронтенды в sandbox-iframe-ах. Но там прячется подводный камень, о котором часто забывают.
Цена главной иллюзии
Главная проблема многомашинного режима — не сериализация сообщений между воркерами. Она решается банальными очередями. Настоящая боль — детерминизм таймингов. У реалтайм-эмулятора должен быть единый источник времени, один метроном на весь зал. Когда телетайп одной машины стрекочет в левом ухе, а принтер другой — в правом, любые расхождения в 0.5 мс моментально слышны. Аудио-семплы нельзя «подождать», они обязаны приходить вовремя, иначе машинный зал превращается в какофонию.
Я попробовал завести общий метроном для этого дела, послушал эту кашу полминуты … и забил. Вкладки покрывают сценарий А на все сто, а две независимые СМ-ки нужны уж не знаю даже и для чего. Но дверной проем я разметил: если понадобятся честные две машины, я знаю, куда бить. Можно будет сделать свой маленкий ARPANET.
Балка вторая. Платы впаяны намертво: шина без арбитра
Как хотелось
Цикл-точное ядро здесь и сейчас. Писать систему команд, логику прерываний и диспетчер памяти с нуля — это год-два работы и высокий риск бросить проект на середине. Я честно оценил силы и пошел по пути меньшего сопротивления.
Что получилось
Взял готовую кодовую базу pdp11-js Пола Нанкервиса (той самой, из которой вырос PCjs в части PDP-11). Ядро точное, отлаженное, на нём грузится полтора десятка ОС. Это была сделка сдьяволом, но выгодная.
Нюанс оказался не в «монолитности» — это слишком расплывчато. Дело в отсутствии абстракции системной шины UNIBUS. В реальном железе Bus Arbiter решает, кто владеет магистралью. Здесь же устройства привязаны к адресам I/O page напрямую. Нельзя «вставить плату в слот» — сетевой адаптер DEUNA или еще один дисковый контроллер приходится припаивать к адресам вручную, дописывая логику прерываний поверх процессора.
Показательный пример — DataLoader. Он тянет образы дисков, распаковывает их и кладет в кэш, а логика RK11/RK05 живет прямо внутри слоя I/O-страницы (src/iopage.js), потому что в архитектуре ядра просто нет другого места, куда его можно вставить.

Замыкание цикла здесь важнее, чем кажется. На реальной UNIBUS контроллер завершает DMA, кладет вектор в Vector Address Register, а процессор сам считывает его по INTACK. У нас нет ни арбитра, ни INTACK — вместо них рукописная связка «устройство → обработчик», вшитая прямо в кишки процессора.
Цена сварки наголо
Любое расширение периферии — это риск задеть точную логику процессора (что я энное количество раз и сделал). Сетевая карта, новый тип диска, магнитофон — всё ручками и по месту. Причем я был не первым, кто об это споткнулся: тот же код Пола породил PCjs, где ребята уже переработали архитектуру в сторону нормальной шины. Я знал об этом с самого начала и … сознательно выбрал более простой монолит ради скорости старта. Так и живем: каждая новая железка — это ручная приварка к адресу 777xxx.
Что за этой балкой
Стена несущая, сносить её страшно. Но я её не штукатурю. Когда дойдут руки до рефакторинга, первым кандидатом станет выделение прослойки UNIBUS и вынос периферии из сердца процессора. Пока правило простое: работает — не трогай.
Балка третья. Витая пара упирается в кирпичи Tauri: сеть
Как хотелось
Честный IP-адрес в домашней сети. Чтобы эмулируемый UNIX V5 мог пинговать твой рабочий ПК, а к эмулируемой СМ-1420 можно было подключиться из реального PuTTY. Честный TAP-мост, сырые кадры, Telnet.
Что получилось
Пока — почти ничего. Весь Rust-слой Tauri в проекте (25 строк в src-tauri/src/lib.rs) умеет только подсовывать файлы из ресурсов:
/// Read a bundled media image from the app's resource directory. #[tauri::command] fn load_bundled_image(app: tauri::AppHandle, name: String) -> Result<Vec<u8>, String> { let resource_dir = app.path().resource_dir() .map_err(|e| format!("cannot locate resource dir: {e}"))?; let path = resource_dir.join("media").join(&name); std::fs::read(&path).map_err(|e| format!("cannot read bundled image '{name}': {e}")) }
Вот и весь «бэкенд». Стена сети стоит как стояла.
Где я слукавил в прошлый раз
В первой статье я писал, что «сетевой мост на Tauri — это элегантно и стабильно». Поправка: etherparse, smoltcp или pnet решают парсинг кадров, но они не создают виртуальный интерфейс. Для него нужен FFI-вызов Wintun API на Windows или /dev/net/tun на Linux. Элегантно — это про обработку пакетов, а не про создание трубы. И да, TUN и TAP — это не одно и то же: TUN гоняет IP-пакеты третьего уровня, а TAP — целые Ethernet-кадры второго, и нам нужен именно TAP, потому что гостевая ОС хочет видеть настоящий сетевой адаптер, а не туннель.
На Windows всё ещё веселее: неподписанные драйверы OpenVPN/TAP-Ninja требуют танцев с отключением проверки подписи, а подписанный wintun нужно найти и правильно упаковать. Плюс реальная задержка IPC-перехода от WASM к Tauri-бэкенду — около 0.1–0.5 мс на пакет. Для ARP/DHCP это мелочь, но в тайминги стека это придется закладывать с первого дня.
Почему Electron здесь умер бы сразу
Native-модули. В Node.js нет низкоуровневых TUN/TAP. Пришлось бы тянуть C+±аддоны вроде
node-tuntap2, пересобирая их под каждую версию Chromium черезelectron-rebuild. Обновил Electron — сломал сеть.IPC-мост. Гонять сырые Ethernet-кадры пачками из главного процесса в рендер через
contextBridge— это убийство CPU в реалтайме.Права. TAP требует админских прав. Запускать весь огромный Electron от root — катастрофический антипаттерн.
Tauri дает более чистое место для штурма этой стены, хотя кирпичей там тоже хватает.
Что за этой стеной
Telnet-сервер на Rust внутри десктопного приложения. TAP-мост, который выпустит эмулируемый UNIX в локалку. В веб-версии этого не будет никогда — там останется заглушка, чтобы гостевая ОС не паниковала при загрузке. Осознанный раскол: богатый десктоп и кастрированный веб.
Балка четвертая. Тройное остекление WebView: кросс-движковый ад
Как хотелось
Лёгкий офлайн-дистрибутив без 200 мегабайт хромиума.
Что получилось
Tauri использует системный WebView (WebView2 на Windows, WebKit на macOS/Linux), минимальная сборка весит честные ~3 МБ. Но есть нюансы.
Linux. «Копеечный вес» — правда только для винды. На Linux .deb раздувается из-за хвоста зависимостей GTK3/WebKitGTK (libgtk-3-0, libwebkit2gtk-4.1-0 и тянущиеся за ними иксы). Легко проверить: apt depends покажет этот список, и сам движок там — лишь верхушка айсберга.
Рендеринг. Ты пишешь код один раз, а тестируешь под три разных браузера. Шрифты, канвас, CSS-анимации бумаги, люминофорные эффекты CRT — всё это плывет в WebView2, WebKit и Chrome.
Цена стекла
Каждый визуальный эффект проверяется минимум на двух движках. Это медленно и утомительно, но сделка честная: платим временем разработки за размер дистрибутива.
Что видно сквозь стекло
Если цель — атмосфера, вот три направления, которые я присматриваю:
Аудиопространство. HRTF в Web Audio API. Курсор слева — стрекот телетайпа в левом ухе. Закончилась бумага — изменился тембр удара молоточка.
Физика света. Bloom/glow вокруг символов и легкое геометрическое искажение линзы (barrel distortion). Глаз узнает аналоговую оптику быстрее, чем мозг прочитает надпись «VT52».
Инертность UI. Переключение тумблера — это 150–200 мс анимации с правильным ускорением (easing) и механическим щелчком. Мгновенное переключение CSS-классом убивает иллюзию тяжелого железа.
Что бы я сделал иначе сегодня
Сухой чек-лист архитектора вместо лирики. Если бы начинал заново:
Балку потока не ломал бы сразу. Начать с вкладок (сценарий А) и заложить
userTerminals: 0..2(этим я в итоге занялся и реализовал). Многомашинный сценарий проектировать только после того, как заработает единый метроном и аудио-детерминизм.Ядро взял бы то же — но без розовых очков.
pdp11-js/PCjs — правильный выбор для скорости. Но сразу принять: расширяемости нет, любая периферия — ручная пайка к I/O page.Платформу выбрал бы Tauri снова — но формулировки были бы честнее. Не обещать «элегантно и стабильно». Помнить: крейты парсинга — это не TAP; на Windows нужен подписанный
wintun; 0.1–0.5 мс IPC-перехода надо закладывать в тайминги с первого дня.К кросс-движковой отладке готовился бы с первого дня. Не откладывать проверку на WebKit до релиза. Сразу проектировать звук как пространственный (HRTF).
И главное, что я оставил бы без изменений: цель. Я строил не точную эмуляцию — я строил комнату с запахом озона и стрекотом телетайпа. SIMH дает машину. yaPDP дает помещение. Это разные продукты, и важно быть честным про то, какая из балок держит тебя прямо сейчас.
Дисклеймер. Я сознательно упрощаю детали работы системной шины UNIBUS, механизма прерываний (DMA, INTACK) и арбитража — ведь моя задача здесь показать ограничения фронтенд-эмулятора для браузера, а не написать учебник по архитектуре PDP-11. Пользователь @SIISII уже сделал отличную подборку материалов, которые я активно использую; его работа помогает мне глубже разобраться во многих тонкостях, хотя до сих пор чувствую себя с этим неуверенно. Если вам нужны максимально точные инженерные подробности — рекомендую обратиться к первоисточникам или материалам SIISII. Здесь же мы обсуждаем компромиссы между атмосферой машинного зала и реалиями веб-программирования в руках непрофильного специалиста.
Вопрос архитектору-самоубийце: куда бить молотком?
Машина живет. Она дышит озоном и стрекочет телетайпом. Но под капотом всё держится на соплях из localStorage и глобальных переменных. Пора наводить порядок, иначе следующий шаг вперед потребует раскопок археологического слоя полугодовой давности.
Работаю один. Ресурс времени конечен. Нужно выбрать точку приложения лома:
Путь первый: Инфраструктура разработчика (Dev-in-the-loop) Мне осточертело раз за разом вводить login и монтировать диски ради проверки одного байта в драйвере RK11. Нужен комплекс: снапшоты всего контекста процессора (включая Vector Address Register и Bus Arbiter state) плюс нормальная персистентность блочного устройства. Чтобы грязный кэш диска переживал перезагрузку страницы, а снапшот восстанавливал машину ровно в момент входа в отладчик ODT. Это база, без которой любая правка превращается в сизифов труд.
Путь второй: Инфраструктура проекта (CI-for-a-retro-computer). Если оставить всё как есть, через месяц я сам же наступлю на свои грабли и уроню тайминги DECtape или потеряю из-за гонок парочку дисковых блоков. Нужен сквозной end-to-end harness на базе Puppeteer. Робот должен уметь загрузить OS, залогиниться, выполнить команду и отдать мне бинарный дамп экрана или код ошибки. Только так я смогу безопасно отвязывать ту самую шину UNIBUS от сердца процессора.
Путь третий: Добиться историчности загрузки. Имеющийся бутлоадер унаследован от проекта Пола Нанкервиса и не отвечает целям проекта. Технически, стек для разработки своего загрузчика достаточно легко поднимается на уже имеющемся эмуляторе (какие-то начатки мыслей на эту тему я озвучивал в первой статье). Это, кстати, неочевидный поначалу плюс выбранной платформы, своего рода компенсация за её ограничения.
Путь четвёртый: Пока остановиться и критическим взором окинуть проделанную работу. Осветить моменты, связанные с программной генерацией графических и видео-материалов. Это тоже относится к плюсам выбранной платформы.
Комментируйте. Аргументируйте. Предлагайте свой пятый путь, если видите его.
Потыкать в браузере: https://amesk.github.io/yaPDP/pdp11.html
Исходный код и десктоп: https://github.com/amesk/yaPDP
Для новостей и обсуждения я завёл Telegram-канал yaPDP_news_ru — под анонсы обновлений и релизов, а также yaPDP_discussions_ru — место для вопросов, идей и технических дискуссий.
Перейти к первой части

