[{"data":1,"prerenderedAt":792},["ShallowReactive",2],{"content-windows-98-in-the-browser-ru":3},{"id":4,"title":5,"articleTitleSource":6,"articleValid":7,"articleWarnings":8,"body":9,"comments":7,"date":777,"date_updated":8,"description":778,"extension":779,"icon":15,"image":780,"language":781,"meta":782,"navigation":7,"path":783,"publish-to":8,"readingTime":784,"seo":786,"stem":787,"titleEasterEgg":8,"titleLines":788,"topics":789,"translationKey":790,"__hash__":791},"content/windows-98-in-the-browser-ru.md","Оно живое! Вселяем Windows 98 в браузер","h1",true,null,{"type":10,"value":11,"toc":754},"minimark",[12,19,30,33,36,39,46,52,57,66,69,78,86,89,99,102,105,108,111,114,118,121,124,130,133,136,139,142,148,151,155,158,161,164,177,183,186,189,195,198,209,220,224,227,230,238,244,247,254,264,269,273,283,286,289,293,296,302,305,315,318,325,329,332,350,357,360,363,367,370,373,377,380,383,389,392,395,432,435,439,442,445,451,461,464,470,473,477,484,491,494,501,505,508,511,514,539,542,545,552,555,558,564,572,576,579,582,585,588,594,604,607,617,620,623,626,632,635,638,641,644,650,656,661,665,668,671,674,687,690,704,714,717,728,731,737,743,748],[13,14],"external-link-card",{"icon":15,"label":16,"title":17,"url":18},"streamline-ultimate-color:microsoft-logo","Открыть","Windows 98 в браузере","https://98.kolpaque.dev/",[20,21,22,23,29],"p",{},"Откройте ",[24,25,28],"a",{"href":18,"rel":26},[27],"nofollow","98.kolpaque.dev",", и после короткого чёрного экрана перед вами появится рабочий стол Windows 98 SE.",[20,31,32],{},"Это настоящая x86-установка Windows 98, работающая внутри браузера, со своими окнами, реестром, драйверами, системными диалогами, DOS-сессиями и приложениями. Не скриншот и не воссозданная на React оболочка.",[20,34,35],{},"Quake и Half-Life используют современный GPU компьютера. Две вкладки браузера обнаруживают друг друга как машины в одной Ethernet-сети. Miranda IM получает временный восьмизначный UIN и обменивается настоящими сообщениями OSCAR с другим посетителем. Winamp открывается с сохранённым плейлистом из 35 MP3-файлов и уже включённым случайным порядком. Ни одну из этих программ не переписывали как веб-приложение; это ограничение определило весь проект.",[20,37,38],{},"Мне не хотелось строить музейную витрину, похожую на старый компьютер. Я задумывал арт-проект о компьютерной ностальгии — место, где старая машина ненадолго оживает. По ходу работы стало понятно, что одних пикселей для этого недостаточно. Внешний вид был самой простой частью. В памяти остался компьютер с весом, задержками, звуками, несовместимостями, локальными сетями, беспорядком на рабочем столе и другими людьми где-то по ту сторону модема. Чтобы вернуть это ощущение, браузеру пришлось предоставить компьютер, на котором, по мнению Windows 98, она и работала.",[20,40,41],{},[42,43],"img",{"alt":44,"src":45},"Восстановленный рабочий стол Windows 98 SE на русском языке, работающий в браузере","/windows-98-in-the-browser/desktop.png",[20,47,48],{},[49,50,51],"em",{},"Финальный рабочий стол на 98.kolpaque.dev.",[53,54,56],"h2",{"id":55},"настоящая-машина-вместо-html-ремейка","Настоящая машина вместо HTML-ремейка",[20,58,59,60,65],{},"Репозиторий ",[24,61,64],{"href":62,"rel":63},"https://github.com/1j01/98",[27],"98.js",", с которого я начинал, уже содержал сложную HTML- и JavaScript-реконструкцию рабочего стола Windows 98. Там были браузерные версии Paint, Notepad, Minesweeper, Solitaire, Pinball и Winamp. Проект был обаятельным, но упирался в жёсткое ограничение: каждую программу приходилось воссоздавать отдельно.",[20,67,68],{},"Для моей задачи это не годилось. Нужны были настоящая среда Win32, реестр, старые установщики, системные настройки, виртуальные DOS-машины, оригинальные игры, сетевые драйверы и все странные допущения, которые программы конца девяностых делали о компьютере.",[20,70,71,72,77],{},"Рабочим ориентиром стал ",[24,73,76],{"href":74,"rel":75},"https://github.com/copy/v86",[27],"v86"," — эмулятор x86-компьютера, который запускается в браузере через WebAssembly. Он уже умел загружать Windows 98, поэтому сама идея была осуществима. Основная работа заключалась в подготовке собственной машины и подключении её к браузеру там, где возможностей v86 из коробки не хватало.",[20,79,80,81,85],{},"Я закрепил v86 как зависимость, а не стал поддерживать долгоживущий форк. Проект использует версию 0.5.424 с тремя отдельными патчами: защита VGA ROM от записи, корректное поведение ",[82,83,84],"code",{},"POPFD"," в DPMI Windows 9x и исправление оси мыши VMware. При подготовке рантайма воспроизводимые преобразования также добавляют поддержку Sound Blaster и OPL. Интеграции дисплея, 3D, сети и управления гостевой системой живут за пределами исходного дерева v86.",[20,87,88],{},"Для разных задач понадобились два эмулятора:",[90,91,97],"pre",{"className":92,"code":94,"language":95,"meta":96},[93],"language-text","QEMU  → установка, драйверы, обслуживание и настройка рабочего стола\nv86   → выполнение внутри браузера\n","text","",[82,98,94],{"__ignoreMap":96},[20,100,101],{},"Оба должны были описывать один и тот же воображаемый компьютер. Windows 98 цепко запоминает железо. Стоит сетевой карте появиться в другом PCI-слоте, и система может принять её за новое устройство, запустить Plug and Play или получить конфликт с прежней конфигурацией.",[20,103,104],{},"Поэтому виртуальная машина получила намеренно старую и консервативную топологию: Pentium II, 128 MiB оперативной памяти, IDE-диск, сетевую карту NE2000, Sound Blaster 16, совместимое VGA-оборудование и отключённый ACPI. Даже PCI-слот для NE2000 был зафиксирован.",[20,106,107],{},"Я разрабатывал проект на Mac с Apple Silicon, поэтому QEMU не мог использовать аппаратную виртуализацию для гостевой x86-системы. Вместо неё работал программный транслятор TCG, переводивший инструкции x86 на ARM-хосте. Быстрым это не было, зато скорости хватало, чтобы установить Windows и драйверы, перенести файлы, корректно выключить гостевую систему и вернуть тот же сырой образ диска в v86.",[20,109,110],{},"Первым действительно важным результатом стала одна установленная копия Windows 98, которая видела один и тот же компьютер и в QEMU, и в браузере.",[20,112,113],{},"Я использовал собственный дистрибутив и лицензию Windows 98 SE. Ключ продукта и исходный установочный носитель не входят в дерево исходников проекта.",[53,115,117],{"id":116},"целый-компьютер-со-скоростью-веб-страницы","Целый компьютер со скоростью веб-страницы",[20,119,120],{},"Первый системный диск занимал 512 MiB. Позже, когда добавились игры и музыка, я расширил его до 2 GiB. Оба объёма слишком велики для начальной загрузки сайта, особенно если большая часть диска не нужна в первые секунды сеанса.",[20,122,123],{},"Решение разделяет три сущности, которые выглядят как «машина с Windows», но отвечают за разные вещи:",[90,125,128],{"className":126,"code":127,"language":95,"meta":96},[93],"сырой образ диска\n  постоянные файлы, программы, реестр и состояние файловой системы\n\nленивые фрагменты диска\n  независимо сжатые части, загружаемые только при чтении из Windows\n\nснимок машины\n  память и состояние устройств уже работающего рабочего стола\n",[82,129,127],{"__ignoreMap":96},[20,131,132],{},"Исходный диск объёмом 512 MiB был разбит на части по 256 KiB, каждая из которых независимо сжималась с помощью Zstandard. Полный набор занимал около 108 MiB, но браузер не скачивал его целиком. v86 запрашивал только те части, к которым действительно обращалась Windows, а обычный HTTP-кеш удешевлял следующие посещения.",[20,134,135],{},"Снимок решал другую задачу. Холодная загрузка Windows 98 исторически достоверна, но для сайта это плохое действие по умолчанию. Сохранённые память и устройства уже работающей машины занимали около 64 MiB без сжатия и примерно 10 MiB после него.",[20,137,138],{},"Этим объясняется почти магический на первый взгляд эффект из примера v86: Windows 98 появлялась на странице после передачи всего около 13 MiB. Сама система не ужалась до такого размера. Браузер получил сжатое состояние работающей машины и несколько фрагментов диска, понадобившихся сразу после восстановления.",[20,140,141],{},"Для посетителя важнее всего был результат:",[143,144,145],"blockquote",{},[20,146,147],{},"Windows перестала загружаться как компьютер и стала открываться как страница.",[20,149,150],{},"У машины по-прежнему был настоящий диск. Приложения читали файлы через гостевой IDE-контроллер. Windows считала, что возобновила работу на обычном PC-железе. Браузер лишь не загружал блоки, которые гостевая система ещё не запросила.",[53,152,154],{"id":153},"окно-браузера-как-настоящий-монитор","Окно браузера как настоящий монитор",[20,156,157],{},"Первая браузерная версия напоминала обычную демонстрацию эмулятора: фиксированный экран 800×600 внутри оформленной страницы, поля, скруглённые углы и постоянно видимые кнопки паузы и перезапуска. Мне хотелось, чтобы самой страницей была Windows, без панели управления виртуальной машиной. Убрать рамку оказалось легко; научить Windows понимать размер браузера — гораздо сложнее.",[20,159,160],{},"Canvas можно растянуть через CSS, но тогда растянется только готовая картинка. Windows продолжит считать, что её рабочий стол имеет размер 800×600. Приложения вычислят неверную рабочую область, текст размоется, координаты мыши разойдутся, а необычные размеры окна браузера дадут пустые поля или обрезанное изображение.",[20,162,163],{},"Режимы вроде 905×602, 1111×711 или 1470×745 должны были существовать внутри гостевой системы как настоящие видеорежимы. Рабочий стол, панель задач, развёрнутые окна и курсор обязаны были совпадать с браузером пиксель в пиксель.",[20,165,166,167,172,173,176],{},"Итоговое решение использовало модифицированный стек дисплея Windows 9x на основе ",[24,168,171],{"href":169,"rel":170},"https://github.com/JHRobotics/vmdisp9x",[27],"VMDisp9x",". При запуске страница один раз измеряет область просмотра и передаёт нужные ширину и высоту через приватные эмулируемые порты ввода-вывода. Гостевой видеодрайвер читает эти значения и предлагает 32-битный режим точно такого разрешения. Затем маленькая Win32-утилита применяет его вызовом ",[82,174,175],{},"ChangeDisplaySettingsA",".",[90,178,181],{"className":179,"code":180,"language":95,"meta":96},[93],"область просмотра браузера\n    ↓ ширина и высота через порты ввода-вывода\nмодифицированный видеодрайвер Windows 9x\n    ↓\nнастоящий видеорежим гостевой системы\n    ↓\nрабочий стол Windows точно по размеру браузера\n",[82,182,180],{"__ignoreMap":96},[20,184,185],{},"Диапазон был намеренно ограничен разрешениями от 320×240 до 2560×1600, а драйвер дополнительно проверял, помещается ли режим в доступную видеопамять. После начальной настройки изменение размера браузера уже не перестраивало Windows на лету. До следующей загрузки страницы последующие изменения обрабатывались обычным масштабированием canvas.",[20,187,188],{},"Один из проверенных сеансов выглядел так:",[90,190,193],{"className":191,"code":192,"language":95,"meta":96},[93],"область документа: 1470 × 745\nгостевой canvas:   1470 × 745\nрабочий стол:      1470 × 745\nабсолютный курсор: активен\n",[82,194,192],{"__ignoreMap":96},[20,196,197],{},"Мышь потребовала отдельной интеграции. Обычная эмулируемая PS/2-мышь передаёт относительное движение и, как правило, вынуждает браузер захватывать указатель. На компьютере это неудобно, а на телефоне почти непригодно. Я установил в гостевой системе совместимый с VMware абсолютный указатель, перевёл координаты браузера в гостевые и использовал тот же путь для сенсорного ввода. После этого курсор Windows следовал за указателем хоста без обязательного захвата.",[20,199,200,201,204,205,208],{},"Ранняя реализация применяла разрешение удивительно хрупким способом: браузер нажимал ",[82,202,203],{},"Win+R",", набирал путь к утилите сканкодами и нажимал Enter. Это работало, пока снимок не был сохранён с уже включённой английской раскладкой. Автоматизация всё равно нажимала ",[82,206,207],{},"Alt+Shift",", возвращала Windows на русский язык и печатала якобы латинский путь кириллической бессмыслицей. Windows совершенно справедливо сообщала, что такого файла нет.",[20,210,211,212,215,216,219],{},"Со временем клавиатурную автоматизацию заменила небольшая резидентная программа ",[82,213,214],{},"VMHOST.EXE"," внутри гостевой системы. Браузер передавал ей ограниченную по размеру командную строку через приватный блок ввода-вывода; программа проверяла входные данные, вызывала ",[82,217,218],{},"CreateProcessA"," и возвращала настоящий код ошибки Win32. Имитация пользователя, печатающего в диалоге «Выполнить», была хрупкой. Узкий интерфейс виртуального устройства оказался проще в тестировании и анализе.",[53,221,223],{"id":222},"старые-программы-как-тестовый-стенд-для-железа","Старые программы как тестовый стенд для железа",[20,225,226],{},"Когда Windows научилась загружаться, менять разрешение, принимать ввод и воспроизводить звук, я начал устанавливать игры. Они одновременно наполняли арт-проект содержанием и безжалостно проверяли виртуальное железо.",[20,228,229],{},"Старые программы не спрашивают вежливо у слоя абстракции, какие возможности он поддерживает. Они опрашивают таймеры в цикле без пауз, переключают VGA-режимы вызовами BIOS, запрещают прерывания, программируют DMA, рассчитывают на конкретные целочисленные тайминги и вызывают функции операционной системы из контекстов, где сегодня никто таких вызовов не ждёт. Каждая игра проверяла очередной стык между Windows 98, DOS, драйверами, v86 и браузером.",[231,232,234,235],"h3",{"id":233},"повторяющийся-сбой-c0008000","Повторяющийся сбой ",[82,236,237],{},"C000:8000",[20,239,240,241,243],{},"Doom и Quake запускались, работали и внешне не показывали проблем. После нормального выхода Windows иногда сообщала о недопустимой инструкции по адресу ",[82,242,237],{},", а повторно запустить программу уже не удавалось.",[20,245,246],{},"Адрес наводил на мысль об ошибке в DOS-расширителе или процедуре завершения игры. Проблема воспроизводилась и с JIT, и без него, даже при отключённом звуке, поэтому аудио ни при чём.",[20,248,249,250,253],{},"Настоящая причина находилась уровнем ниже. v86 копировал VGA option ROM в обычную доступную для записи гостевую память около физического адреса ",[82,251,252],{},"C0000",". Виртуальный видеодрайвер Windows 98 использовал часть этого диапазона как хранилище для виртуального текстового экрана и записывал туда байты символов и атрибутов. При следующем видеовызове BIOS управление переходило в область, которая должна была оставаться ROM, и процессор исполнял повреждённые данные.",[20,255,256,257,260,261,176],{},"Замена одной игры другой могла скрыть сбой, но не исправить его. VGA ROM пришлось сделать доступным для чтения и исполнения, но не для записи, причём во всех путях работы с памятью эмулятора: интерпретируемых операциях записи, JIT-отображениях, широких записях, операциях read-modify-write и оптимизированных путях ",[82,258,259],{},"REP MOVS"," и ",[82,262,263],{},"REP STOS",[20,265,266,267,176],{},"После этого оригинальные DOS-игры снова запускались после выхода и больше не падали на ",[82,268,237],{},[231,270,272],{"id":271},"doom-бесконечно-ждал-прерывания","Doom бесконечно ждал прерывания",[20,274,275,276,279,280,282],{},"Другая проблема Doom скрывалась в обработке прерываний защищённого режима. Windows 9x виртуализирует выполняемую из ring 3 инструкцию ",[82,277,278],{},"CLI",", позволяя DOS-программе считать, что она запретила прерывания. Соответствующая ",[82,281,84],{}," должна была восстановить флаг прерываний. Вместо этого v86 применял обычное правило привилегий и оставлял прерывания выключенными.",[20,284,285],{},"IRQ Sound Blaster оставался в ожидании, а аудиокод Doom бесконечно ждал таймера, который уже не мог сработать.",[20,287,288],{},"Исправление получилось узким: распознать именно эту виртуализированную пару инструкций DOS Protected Mode Interface (DPMI) в Windows 9x и разрешить восстановление флага. Не понадобились ни JavaScript-watchdog, ни принудительное прерывание, ни глобальная правка таймингов. Оригинальная игра снова получала прерывания DMA от Sound Blaster и выдавала настоящие ненулевые PCM-сэмплы.",[231,290,292],{"id":291},"f-19-оказалась-слишком-старой-для-быстрого-виртуального-процессора","F-19 оказалась слишком старой для быстрого виртуального процессора",[20,294,295],{},"При переходе от полёта к итоговым экранам F-19 Stealth Fighter завершалась с ошибкой:",[90,297,300],{"className":298,"code":299,"language":95,"meta":96},[93],"run-time error R6003\ninteger divide by 0\n",[82,301,299],{"__ignoreMap":96},[20,303,304],{},"Иногда игра также утверждала, что плата AdLib не найдена, поэтому сначала подозрение пало на новую реализацию OPL. Трассировка процессора показала куда более странную причину.",[20,306,307,308,311,312,176],{},"Один из звуковых оверлеев игры 16 раз читал таймер PIT, усреднял результаты и делил константу на среднее значение. На виртуальном процессоре все 16 чтений иногда укладывались в один тик PIT. Измеренная задержка получалась нулевой, после чего игра выполняла ",[82,309,310],{},"DIV BX"," при ",[82,313,314],{},"BX = 0",[20,316,317],{},"Я ненадолго попробовал глобально замедлить процессор. Одной игре стало лучше ценой ухудшения всей машины: Windows потеряла отзывчивость, другие программы изменили поведение, а DOS-сессии стали менее предсказуемыми.",[20,319,320,321,324],{},"Итоговое исправление касалось только конкретной сборки ",[82,322,323],{},"ASOUND.EXE",". Небольшой воспроизводимый патч сохранял исходное верхнее ограничение, добавлял нижнюю границу в единицу и аккуратно восстанавливал состояние регистров, на которое рассчитывал окружающий код оверлея. В первой версии я забыл о последнем условии и получил другую ошибку. Регистр, кажущийся свободным внутри 20-байтного патча, вполне может быть частью недокументированного соглашения о вызовах.",[231,326,328],{"id":327},"explorer-падал-при-прокрутке","Explorer падал при прокрутке",[20,330,331],{},"Позже Explorer начал падать при многократной прокрутке большой папки. Page Up и Page Down работали нормально, а физическое колёсико мыши — нет. Ошибка воспроизводилась и в v86, и в QEMU, поэтому браузерный рендерер оказался невиновен.",[20,333,334,335,338,339,342,343,342,346,349],{},"Старый вспомогательный драйвер ",[82,336,337],{},"VBMOUSE.DRV"," обрабатывал колёсико в обработчике прерывания и прямо оттуда вызывал ",[82,340,341],{},"GetCursorPos",", ",[82,344,345],{},"WindowFromPoint",[82,347,348],{},"EnumChildWindows"," и другие функции USER. При достаточном количестве вложенных событий он повреждал или исчерпывал стек, после чего Explorer падал.",[20,351,352,353,356],{},"Я пересобрал драйвер, отключив в нём эмуляцию колёсика. Шаги прокрутки ушли в отдельный протокол между хостом и гостевой системой и доставлялись через ",[82,354,355],{},"VMHOST"," из обычного таймера Win32 и цикла обработки сообщений. Абсолютная мышь осталась в драйвере, а прокрутка покинула контекст прерывания.",[20,358,359],{},"Затем стресс-тест отправил 512 событий колёсика, получил 512 подтверждений от гостевой системы, заметно прокрутил папку и не уронил Explorer.",[20,361,362],{},"Эти расследования изменили моё отношение к установленным программам. Doom, Quake, F-19, Half-Life и Explorer проверяли вызовы BIOS, защищённый режим, виртуализацию VGA, таймеры, DMA, доставку IRQ, звук Sound Blaster, синтез AdLib, драйверы мыши и эмулятор процессора. Их сбои описывали построенный компьютер точнее, чем конфигурация эмулятора.",[53,364,366],{"id":365},"webgl-как-видеокарта-windows-98","WebGL как видеокарта Windows 98",[20,368,369],{},"Самая крупная техническая часть проекта началась с простой проблемы: программный 3D-рендеринг внутри эмулируемой x86-машины работал слишком медленно, хотя браузеру уже был доступен мощный GPU через WebGL2.",[20,371,372],{},"Можно было портировать игру в веб или собрать современный движок вокруг её файлов данных. Игра стала бы быстрее, но это была бы уже другая работа. Оригинальный исполняемый файл Windows должен был считать, что обращается к обычному системному драйверу OpenGL.",[231,374,376],{"id":375},"сначала-паравиртуализированный-транспорт-для-3d","Сначала — паравиртуализированный транспорт для 3D",[20,378,379],{},"Первый прототип назывался VM3D. Нативная Win32-демонстрация выделяла закреплённый буфер команд в гостевой памяти, записывала в него полный кадр и уведомляла эмулятор через приватные порты ввода-вывода.",[20,381,382],{},"Браузер не доверял гостевому буферу. При каждой отправке он транслировал нужные гостевые страницы, проверял все диапазоны физической памяти, копировал пакет в новый неизменяемый JavaScript-буфер, валидировал поток команд и только после этого исполнял его через WebGL2.",[90,384,387],{"className":385,"code":386,"language":95,"meta":96},[93],"приложение Windows\n    ↓ пакет команд в гостевой памяти\nтрансляция адресов v86\n    ↓ скопированный и проверенный пакет\nрендерер WebGL2\n    ↓\nGPU хоста\n",[82,388,386],{"__ignoreMap":96},[20,390,391],{},"Гостевая система не получала ни JavaScript-указатель, ни прямой доступ к памяти WebAssembly. Хост проверял длины, количество команд, размеры текстур, шаг вершинных данных, конечность значений с плавающей точкой, сочетания состояний рендеринга, время жизни ресурсов и обязательную структуру кадра. Некорректный пакет отбрасывался до WebGL.",[20,393,394],{},"На одной и той же синтетической сцене с текстурами разница была однозначной:",[396,397,398,412],"table",{},[399,400,401],"thead",{},[402,403,404,408],"tr",{},[405,406,407],"th",{},"Рендерер",[405,409,411],{"align":410},"right","Результат",[413,414,415,424],"tbody",{},[402,416,417,421],{},[418,419,420],"td",{},"Программный рендерер в гостевой системе",[418,422,423],{"align":410},"около 1,4 FPS",[402,425,426,429],{},[418,427,428],{},"VM3D через WebGL2",[418,430,431],{"align":410},"60 FPS",[20,433,434],{},"Синтетический тест подтвердил, что транспорт достаточно быстр. Совместимость с настоящей игрой ещё предстояло проверить.",[231,436,438],{"id":437},"от-локальной-библиотеки-для-игры-к-системному-opengl-icd","От локальной библиотеки для игры к системному OpenGL ICD",[20,440,441],{},"В первых экспериментах с GLQuake рядом с игрой лежала локальная библиотека совместимости. Так было удобно выяснять, какие именно вызовы фиксированного конвейера нужны Quake, но каждая игра превращалась в особую сборку.",[20,443,444],{},"В финальной архитектуре реализация переехала в саму Windows:",[90,446,449],{"className":447,"code":448,"language":95,"meta":96},[93],"оригинальный GLQUAKE.EXE\n    ↓\nMicrosoft OPENGL32.DLL из Windows 98\n    ↓\nобнаружение OpenGL-драйвера через VMDisp9x\n    ↓\nсистемный ICD VM3DGL.DLL\n    ↓\nтранспорт команд VM3D\n    ↓\nWebGL2\n",[82,450,448],{"__ignoreMap":96},[20,452,453,454,457,458,176],{},"ICD, или Installable Client Driver, — механизм, через который слой OpenGL в Windows передаёт рендеринг графическому драйверу. VMDisp9x объявляет формат пикселей VM3D, Windows загружает ",[82,455,456],{},"VM3DGL.DLL",", а оригинальная игра продолжает импортировать обычную системную ",[82,459,460],{},"OPENGL32.DLL",[20,462,463],{},"После установки ICD Quake больше не требовалась браузерная библиотека рядом с исполняемым файлом. Windows 98 предоставляла тот же графический драйвер любому совместимому приложению.",[20,465,466,467,176],{},"ICD экспортировал исторические точки входа драйвера и таблицу диспетчеризации OpenGL 1.1 на 336 слотов. Не каждая возможность OpenGL 1.1 была реализована по смыслу, но каждому слоту требовалась безопасная для ABI заглушка, которая снимала со стека x86 строго нужное количество аргументов. Заглушки с неверной очисткой стека хватало, чтобы через несколько вызовов отправить выполнение по адресу вроде ",[82,468,469],{},"00000033",[20,471,472],{},"Ориентированный на игры профиль покрывал возможности, которыми пользовались Quake и Half-Life: стеки матриц, immediate mode и массивы вершин, создание и обновление текстур, проверки глубины и альфа-канала, обычные режимы смешивания, culling, туман, scissoring, мультитекстурирование, управление контекстом и вывод кадра.",[231,474,476],{"id":475},"glquake-учим-старый-рендерер-говорить-эффективнее","GLQuake: учим старый рендерер говорить эффективнее",[20,478,479,480,483],{},"Оригинальная GLQuake умеет использовать ",[82,481,482],{},"GL_SGIS_multitexture",". Без расширения каждый полигон мира рисуется дважды: сначала с основной текстурой, затем с мультипликативной картой освещения. Я реализовал расширение в ICD, протоколе команд, валидаторе и WebGL-шейдере, чтобы оба набора текстурных координат отправлялись за один вызов отрисовки.",[20,485,486,487,490],{},"Один и тот же прогон ",[82,488,489],{},"timedemo demo1"," при 640×480 ускорился с 42,2 до 48,1 FPS — на 14%. Центральные области кадров совпали побайтно, а телеметрия хоста зафиксировала сотни тысяч настоящих вызовов мультитекстурирования. Расширение не просто появилось в строке расширений: игра действительно им пользовалась.",[20,492,493],{},"Следующий прирост дала экономия на обмене командами, а не изменение рендеринга. Повторные команды текстур и состояний исчезли. Обновления динамических карт освещения объединились. Совместимые области собирались в один пакет. В одном измеренном кадре количество команд состояний и текстур сократилось с 2223 до 184 — на 91,7%. Более поздний прогон достиг 55,9 FPS и отрисовал 981 858 треугольников за 525 кадров.",[20,495,496,497,500],{},"На финальном рабочем столе остался один ярлык Quake. Он запускал оригинальный ",[82,498,499],{},"GLQUAKE.EXE",", использовал системный ICD, рисовал через WebGL2, воспроизводил звук через Windows и после выхода возвращал обычный рабочий стол.",[231,502,504],{"id":503},"half-life-когда-gpu-уже-не-был-узким-местом","Half-Life: когда GPU уже не был узким местом",[20,506,507],{},"Half-Life стала более тяжёлой проверкой: GoldSrc генерирует гораздо больше мелких вызовов, динамических карт освещения, смен состояний и геометрии, чем GLQuake.",[20,509,510],{},"Одна из первых тяжёлых сцен работала примерно на 9,3 FPS. WebGL2 на хосте тратил на выполнение всего около 4,8 миллисекунды, а эмулируемая гостевая система генерировала кадр примерно 92 миллисекунды. Главная проблема была не в GPU. Эмулируемый путь GoldSrc и MiniGL преобразовывал вершины на x86-процессоре, разворачивал triangle strips и fans в отдельные треугольники, упаковывал большие командные буферы и постоянно хешировал или копировал данные.",[20,512,513],{},"Во второй версии протокола больше работы перешло на хост:",[515,516,517,521,524,527,530,533,536],"ul",{},[518,519,520],"li",{},"состояния modelview, projection, viewport и depth кешировались и отправлялись только после изменения;",[518,522,523],{},"преобразование вершин выполнялось в WebGL2-шейдере;",[518,525,526],{},"triangle fans и strips сохраняли исходную топологию вместо разворачивания в гостевой системе;",[518,528,529],{},"соседние совместимые примитивы объединялись в один вызов отрисовки на хосте;",[518,531,532],{},"пакеты команд стали компактнее;",[518,534,535],{},"повторные поиски текстур, цветов и состояний использовали кеши с константным временем доступа;",[518,537,538],{},"временная память для карт освещения использовалась повторно.",[20,540,541],{},"После этих изменений лёгкая сцена могла достигать 60 FPS. Тяжёлая сцена того же типа, что раньше работала на 9,3 FPS, обычно выдавала 23–28 FPS, а пакет команд сократился примерно с 449 до 226 KiB. Во время загрузочных переходов частота всё ещё могла падать до 15–17 FPS, поэтому я не называю результат «Half-Life в 60 FPS». Основные оставшиеся затраты приходятся на эмулируемый игровой процессор и количество исторических вызовов OpenGL.",[20,543,544],{},"Звуковой тракт обнаружил ещё одну достоверную для своего времени несовместимость. Бэкенд DirectSound в Half-Life выглядел активным, DMA Sound Blaster продолжал работать, но после запуска карты кольцевой буфер игры заполнялся нулями. После выхода иногда замолкали даже обычные звуки Windows.",[20,546,547,548,551],{},"В оригинальном движке уже был другой поддерживаемый звуковой бэкенд. Параметр ",[82,549,550],{},"-wavonly"," включал Win32 WaveOut, который корректно работал через эмулируемый Sound Blaster.",[20,553,554],{},"Относительному вводу мыши тоже понадобился собственный жизненный цикл. Сначала браузер запрашивал Pointer Lock. Некоторые встроенные браузеры не подтверждали и не отклоняли промис, поэтому после короткого тайм-аута страница переходила на запасной путь: отключала абсолютный адаптер рабочего стола и передавала относительные смещения прямо в гостевую систему. После выхода из игры обычный абсолютный курсор Windows возвращался.",[20,556,557],{},"Получившийся путь замкнулся в круг. Игра 1998 года вызывала OpenGL API из Windows 1998 года; ICD Windows 98 переводил вызовы в приватный протокол виртуального устройства; браузер 2026 года проверял протокол и рисовал кадр на GPU хоста. Для игры API по-прежнему предоставляла Windows 98.",[20,559,560],{},[42,561],{"alt":562,"src":563},"Оригинальная GLQuake, отрисованная системным драйвером OpenGL Windows 98 и WebGL2 хоста","/windows-98-in-the-browser/glquake-webgl2.png",[20,565,566],{},[49,567,568,569,571],{},"Оригинальный ",[82,570,499],{},": системный ICD и реализация WebGL2 на хосте.",[53,573,575],{"id":574},"две-вкладки-браузера-на-одном-ethernet-кабеле","Две вкладки браузера на одном Ethernet-кабеле",[20,577,578],{},"Одинокий старый компьютер — лишь часть воспоминания. Следующая задача была социальной: открыть страницу в двух браузерах, запустить старую игру и дать машинам обнаружить друг друга так, словно они подключены к одной локальной сети.",[20,580,581],{},"Никаких кодов комнаты, скопированных IP-адресов, диалогов настройки или отдельного браузерного интерфейса для мультиплеера. Оригинальная игра должна была использовать собственное сетевое меню.",[20,583,584],{},"В первой рассмотренной архитектуре каждый восстановленный снимок Windows сохранял один и тот же IP-адрес, а удалённые участники преобразовывались в локальные псевдонимы. Для некоторых UDP-игр этого могло хватить, но общее решение быстро превращалось в сетевую ловушку: proxy ARP, подмена адресов источника и назначения, контрольные суммы IPv4 и транспорта, фрагментация, правила для broadcast, преобразование ICMP и, возможно, отдельная обработка игр, если адрес сериализован внутри неизвестной полезной нагрузки.",[20,586,587],{},"От этой схемы я отказался. Каждая загрузка страницы получала настоящую уникальную идентичность внутри Windows:",[90,589,592],{"className":590,"code":591,"language":95,"meta":96},[93],"peer ID: случайное 128-битное значение\nMAC:     локально администрируемый 02:xx:xx:xx:xx:xx\nIPv4:    уникальный адрес в 10.98.0.0/24\nшлюз:    10.98.0.1\n",[82,593,591],{"__ignoreMap":96},[20,595,596,597,342,600,603],{},"Небольшой сигнальный сервис атомарно отклонял коллизии IP или MAC. Игровой трафик через него не проходил. Сервис лишь объявлял временное участие и обменивался ",[82,598,599],{},"offer",[82,601,602],{},"answer"," и ICE-кандидатами WebRTC.",[20,605,606],{},"При холодной загрузке всё было просто: локальный сетевой адаптер v86 мог провести DHCP-обмен с гостевой системой. Восстановление из снимка оказалось сложнее, поскольку внутри Windows уже находился полностью инициализированный стек TCP/IP, а сохранённый адрес был разбросан по оперативной памяти.",[20,608,609,610,260,613,616],{},"Команды ",[82,611,612],{},"ipconfig /release_all",[82,614,615],{},"/renew_all"," занимали около 12 секунд, открывали видимые окна и разрушали идею мгновенно восстановленного рабочего стола. Вместо этого при подготовке снимка я нашёл точные копии сохранённого IPv4-адреса в гостевой памяти — и в сетевом порядке байтов, и в порядке хоста. До исполнения первой восстановленной инструкции процессора браузер проверял старые байты и заменял известные позиции адресом текущего сеанса.",[20,618,619],{},"В одном из состояний потребовалось изменить около 60 проверенных мест — всего несколько сотен байтов. После этого Windows, Winsock, ARP и игры видели новый адрес без перезагрузки, командной строки и NAT на уровне пакетов.",[20,621,622],{},"Драйвер NE2000 сохранял свой внутренний MAC из снимка, а v86 подменял идентичность кадра на границе адаптера, используя MAC текущего сеанса. Поля узлов источника и назначения IPX получили отдельное узкое преобразование, потому что аппаратный адрес встроен в них напрямую.",[20,624,625],{},"Когда вопрос идентичности был решён, сама сеть оказалась простой:",[90,627,630],{"className":628,"code":629,"language":95,"meta":96},[93],"NE2000 в Windows\n    ↓ Ethernet-кадры\nсетевой адаптер v86\n    ↓\nнеупорядоченный WebRTC DataChannel\n    ↓\nсетевой адаптер v86 в другом браузере\n    ↓\nNE2000 в Windows\n",[82,631,629],{"__ignoreMap":96},[20,633,634],{},"Ethernet-канал работал без упорядочивания и повторных отправок, избегал блокировки начала очереди и был больше похож на сеть с потерями, чем на надёжный поток байтов. Известный unicast отправлялся одному участнику; broadcast, multicast, IPX и исторические кадры второго уровня расходились всем подключённым участникам. Мост переносил ARP, IPv4, ICMP, UDP broadcast, Ethernet II IPX, сырой 802.3, 802.2, SNAP и другие неизвестные исторические EtherType.",[20,636,637],{},"Пока двое незнакомцев не знают, как связаться друг с другом, без координационного сервера не обойтись. После установки WebRTC-соединения Ethernet-кадры шли напрямую между браузерами. TURN-сервер использовался только как ретранслятор, когда NAT или firewall не позволяли построить прямой ICE-маршрут.",[20,639,640],{},"Трассировки пакетов подтвердили обнаружение и подключение по UDP, устойчивый игровой трафик в обе стороны и IPX-кадры с ожидаемыми преобразованными идентификаторами узлов.",[20,642,643],{},"В одном UDP-прогоне записалось 133 игровых датаграммы в одну сторону и 201 в другую. Отдельный прогон IPX дал 115 и 170 кадров. Обе игры одновременно рисовали через системные OpenGL ICD и WebGL2-рендереры хоста. О WebRTC они ничего не знали и видели две машины с Windows 98 в одной сети.",[20,645,646],{},[42,647],{"alt":648,"src":649},"Первая машина с Windows 98 в браузерной мультиплеерной сессии GLQuake","/windows-98-in-the-browser/glquake-multiplayer-a.png",[20,651,652],{},[42,653],{"alt":654,"src":655},"Вторая машина с Windows 98 в той же мультиплеерной сессии GLQuake","/windows-98-in-the-browser/glquake-multiplayer-b.png",[20,657,658],{},[49,659,660],{},"Две независимые сессии Windows 98 в одной игре GLQuake поверх Ethernet между браузерами.",[53,662,664],{"id":663},"вернуть-icq-не-выпуская-windows-в-открытый-интернет","Вернуть ICQ, не выпуская Windows в открытый интернет",[20,666,667],{},"Машина с Windows 98, играми и локальной сетью выглядела технически убедительно, но оставалась пустой. Эмоциональным центром воспоминаний была не сама операционная система, а возможность внезапно увидеть другого человека в онлайне.",[20,669,670],{},"Давать непропатченной Windows 98 неограниченный доступ в современный интернет было бы безответственно и почти бесполезно. Старые клиенты не понимают актуальный TLS, а современные сервисы давно не говорят на их протоколах. Поэтому вокруг гостевой системы появился маленький контролируемый фрагмент старого интернета.",[20,672,673],{},"Испытав несколько тогдашних клиентов, я остановился на Miranda IM 0.4.1. Она работала в Windows 98 без KernelEx, Unicows и других слоёв системной совместимости. Я установил официальный русский языковой пакет и минимальный набор оригинальных плагинов.",[20,675,676,677,682,683,686],{},"На хосте работал модифицированный ",[24,678,681],{"href":679,"rel":680},"https://github.com/mk6i/open-oscar-server",[27],"Open OSCAR Server",". Miranda подключалась к ",[82,684,685],{},"10.98.0.1:5190"," внутри гостевой сети. Браузер перехватывал только это назначение и туннелировал TCP-поток в локальный OSCAR-сервис. Общего прокси не было, и гостевая система не получала доступа к произвольным адресам.",[20,688,689],{},"Каждая вкладка браузера получала случайный восьмизначный UIN. Учётная запись жила столько же, сколько вкладка:",[515,691,692,695,698,701],{},[518,693,694],{},"после закрытия и повторного запуска Miranda в той же сессии Windows сохранялись UIN и локальная история сообщений;",[518,696,697],{},"закрытие вкладки браузера удаляло временную учётную запись с сервера;",[518,699,700],{},"в списках других посетителей UIN оставался офлайн-контактом;",[518,702,703],{},"выданные UIN никогда не использовались повторно.",[20,705,706,707,709,710,713],{},"Профиль Miranda начинался с небольшого корректного шаблона и UIN-заполнителя. До разблокировки ввода ",[82,708,355],{}," запускал ограниченную конфигурационную утилиту, которая находила запись UIN нужного типа в ",[82,711,712],{},"session.dat"," и заменяла её текущим номером вкладки. Операция должна была быть повторяемой: после первой сохранённой сессии исходного заполнителя в профиле уже не было.",[20,715,716],{},"Самая интересная ошибка OSCAR проявлялась только при одном посетителе. Мой ранний end-to-end-тест всегда открывал два браузерных контекста до запуска Miranda. Оба клиента входили в сеть, появлялись в списках друг друга, обменивались сообщениями и воспроизводили звук, поэтому тест казался полным. Одиночная сессия, однако, сразу отключалась.",[20,718,719,720,723,724,727],{},"Трассировка OSCAR показала, что Miranda 0.4.1, представлявшаяся как ",[82,721,722],{},"ICQBasic",", отправляла повреждённый запрос ",[82,725,726],{},"BuddyAddBuddies",", когда её локальный список контактов был пуст. В теле находились три неинициализированных байта. Первый случайный байт мог объявить имя контакта длиной 220 байт, хотя в пакете оставалось лишь два. Строгий декодер Open OSCAR справедливо считал это неожиданным концом ввода и закрывал основную сессию OSCAR.",[20,729,730],{},"Если второй посетитель уже был в сети, Miranda отправляла корректный восьмизначный контакт и ошибка исчезала. Моя схема тестирования сама скрыла сбой.",[20,732,733,734,736],{},"Серверный патч намеренно оставался узким: только для ",[82,735,722],{},", только когда первая заявленная длина контакта превышала остаток тела и только с трактовкой этого конкретного пакета как пустого списка. Корректные запросы и повреждённые запросы от других клиентов по-прежнему проверялись строго.",[20,738,739],{},[42,740],{"alt":741,"src":742},"Miranda IM обменивается настоящим сообщением OSCAR внутри Windows 98","/windows-98-in-the-browser/miranda-im.png",[20,744,745],{},[49,746,747],{},"Обмен настоящим сообщением OSCAR через Miranda IM во временной браузерной сети.",[20,749,750,751,176],{},"Так выглядит текущая сборка. Запустить её можно на ",[24,752,28],{"href":18,"rel":753},[27],{"title":96,"searchDepth":755,"depth":755,"links":756},4,[757,759,760,761,769,775,776],{"id":55,"depth":758,"text":56},2,{"id":116,"depth":758,"text":117},{"id":153,"depth":758,"text":154},{"id":222,"depth":758,"text":223,"children":762},[763,766,767,768],{"id":233,"depth":764,"text":765},3,"Повторяющийся сбой C000:8000",{"id":271,"depth":764,"text":272},{"id":291,"depth":764,"text":292},{"id":327,"depth":764,"text":328},{"id":365,"depth":758,"text":366,"children":770},[771,772,773,774],{"id":375,"depth":764,"text":376},{"id":437,"depth":764,"text":438},{"id":475,"depth":764,"text":476},{"id":503,"depth":764,"text":504},{"id":574,"depth":758,"text":575},{"id":663,"depth":758,"text":664},"2026-08-05","Билд-лог Windows 98 в браузере: загрузка из снимка, динамические видеорежимы, ускорение через WebGL2, сеть между вкладками и ICQ.","md","og-image.jpg","ru",{},"/windows-98-in-the-browser-ru",{"wordCount":785},4066,{"title":5,"description":778},"windows-98-in-the-browser-ru","2","build log","windows-98-in-the-browser","7kI1niL9YGQheunCXV15lhGnUMbRfv6iNvxzIbqhNtk",1786074902025]