Эмулирование что это такое
Значение слова «эмуляция»
Делаем Карту слов лучше вместе
Привет! Меня зовут Лампобот, я компьютерная программа, которая помогает делать Карту слов. Я отлично умею считать, но пока плохо понимаю, как устроен ваш мир. Помоги мне разобраться!
Спасибо! Я стал чуточку лучше понимать мир эмоций.
Вопрос: нечет — это что-то нейтральное, положительное или отрицательное?
Синонимы к слову «эмуляция»
Предложения со словом «эмуляция»
Понятия со словом «эмуляция»
Отправить комментарий
Дополнительно
Предложения со словом «эмуляция»
Если эмуляция прошла успешно, можно приступать к записи диска.
Эмуляция выполняется за счёт мощности процессора, а изменённые настройки прописываются в исполняемом файле приложения, поэтому в следующий раз игру можно запускать обычным способом.
В данном контексте эмуляция – это имитация с целью сравняться или превзойти.
Синонимы к слову «эмуляция»
Правописание
Карта слов и выражений русского языка
Онлайн-тезаурус с возможностью поиска ассоциаций, синонимов, контекстных связей и примеров предложений к словам и выражениям русского языка.
Справочная информация по склонению имён существительных и прилагательных, спряжению глаголов, а также морфемному строению слов.
Сайт оснащён мощной системой поиска с поддержкой русской морфологии.
[статья] Эмулятор, в чём соль?
Сегодня в истории нашего сообщества уникальный день! Читатели не просто прислали материал для публикации, а настоящую здоровенную статью и строго по нашей теме — эмулятор и эмуляция как таковая. Пробуем читать, понимать, оставлять свои комментарии, отзывы и предложения по другим подобным «программным» материалам.
Преамбула
Согласно умным книжкам, эмуляция — это поведение одной компьютерной системы (хост), имитирующее поведение другой компьютерной системы (гость). Результатом является возможность выполнения программ для гостя на хосте (или использование хостом периферийных устройств, предназначенных для гостя) вопреки различиям между ними.
Поведение компьютера, которое мы имитируем, — это обработка информации. Компьютеры отличаются тем, какие методы они предоставляют нам для работы с информацией, что делает их пригодными для определенных целей. Однако, цели, с которыми их можно использовать, сильно ограничены, и изначально многие из них просто не способны делать то, что могут другие. Но сердцу не прикажешь, человеку сильно хочется делать больше, чем позволяет его компьютер. И самым прямолинейным решением является собственно приобретение компьютера, способного делать что-то большее. На этом построена вся индустрия компьютерного железа (аппаратного обеспечения), там люди наживаются на наших желаниях расширять свои возможности. Но не все такие богачи, чтобы все это скупать по первому зову левой пятки, а кое-кто просто жадный, и вот — человечество изобрело способ заставить один компьютер вести себя как другой.
Само слово эмуляция было придумано в IBM, когда специалисты искали наилучший способ сделать новые компьютеры обратно совместимыми с предыдущими моделями, то есть способными выполнять программы для них. На тот момент уже существовал метод симуляции поведения одного девайса другим, и это была чисто программная модель, копирующая все особенности гостя виртуально. Ребята придумали использовать для этих целей микрокод на уровне железа вместо программы, и это существенно увеличило скорость работы виртуальной модели. Именно этот метод они и назвали эмуляция. По неизвестным причинам, в таком значении это слово давно перестало использоваться. Раньше любая имитация чего-либо программно называлась симуляцией, а методами микрокода — эмуляцией. Сейчас симуляция — это программное моделирование явлений, о которых невозможно иметь исчерпывающую информацию (например, природные явления), а эмуляция — имитация поведения электронного устройства, логика которого может быть подробно изучена.
Что же представляет собой это поведение? Как именно компьютер обрабатывает информацию? Да и что вообще такое эта информация?
В физической реальности информация не существует. Она нематериальна. Однако, будучи сведениями, отражающими реальность, информация это не только то, чем могут обмениваться материальные объекты, но так же и принципы устройства и функционирования этих объектов, законы природы. Информация может усваиваться и передаваться только в закодированном виде, именно тогда она приобретает физическую форму и превращается в данные. Но данные в чистом виде ничего нам не дадут: нам надо понимать, что они описывают, уметь их расшифровывать. И если данные, используемые природой, мы можем расшифровывать лишь ценой больших усилий, и то не полностью, нечто, закодированное самим человеком, может быть легко распознано им, если известны способ шифрования и смысл, который эти данные несут.
«Информация есть запомненный выбор одного варианта из нескольких возможных и равноправных», — говорит синергетика. Минимальная единица количества информации определяется минимальным числом вариантов выбора — их может быть минимум два; и то, какой выбор из двух вариантов следует сделать, может быть описано одним битом. С битами работает двоичная система счисления, и (что отрадно) в биты может быть перекодирована любая информация, любое число. Компьютеру остается лишь совершить с этими битами нужные действия и выдать соответствующий результат.
Так как бит — вещь виртуальная, мы можем физически представлять его абсолютно в любом виде, лишь бы была возможность его распознать. Арифметически, бит обозначает варианты 0 и 1, логически — true и false (истина и ложь). В вычислительной технике и сетях передачи данных значения 0 и 1 обычно передаются различными уровнями напряжения или тока. Например, в микросхемах на основе транзисторно-транзисторной логики значение 0 представляется напряжением в диапазоне от +0 до +0.8 В, а значение 1 — напряжением в диапазоне от +2.4 до +5.0 В.
Эмулируемое
Прежде, чем данные обрабатывать, надо их как-то пересылать, а также хранить. Для начала, расскажем, как биты представлены физически на запоминающих устройствах.
Картриджи Нинтендо использовали масочные ПЗУ, то есть при производстве ненужные перемычки в них не пропаивались — они пропускались при помощи специальных масок, соединяя транзисторы (или диоды) с каналами ввода-вывода только там, где надо заказчику. Есть несколько вариантов реализации битов, для простоты представьте, что по каждому адресу памяти расположено 8 транзисторов (по разрядности шины данных, о ней ниже), от каждого из которых идет по два контакта — один на землю, другой на шину данных. Если перемычка на месте, ток идет на шину данных, если нет, в землю. Шина данных имеет 8 проводов, по которым передается ток от каждого из транзисторов. Таким образом, если перемычки сохранены у первого, второго и последнего транзисторов, от них будет идти сигнал, соответствующий единице, от остальных ноль, и в итоге мы получим такой вид битов для данного примера: 1100 0001. При необходимости, машина сложит их в байт. Какое число сохранить таким образом по какому адресу — решает автор игры.
Устройство ячеек оперативной памяти значительно сложнее. Каждый бит состоит из двух инверторов, двух битовых линий (BL), являющихся инверсиями друг друга, и линии машинного слова (WL), открывающей доступ битовых линий к ячейке. Линия слова соединяет набор из 8 (для Денди) таких ячеек-битов с шиной адреса, а битовые линии идут на шину данных. Для записи на одну битовую линию шлют сигнал, соответствующий единице, на другую — нулю. Они поступают на входы инвертирующих элементов и замыкают/размыкают соответствующие пары транзисторов в них (M5 и M1, M6 и M3). С выходов этих элементов сигнал идет обратно на входы их антиподов, повторяя цикл. То есть поступивший в ячейку сигнал бесконечно инвертируется по кругу, сохраняясь и после закрытия каналов доступа к ячейке, так как у инверторов есть свое питание (V). Для чтения по обеим битовым линиям шлют сигнал средний между 1 и 0, потом подается 1 на линию слова, и напряжение одной битовой линии падает, а другой — поднимется. По тому, на какой из битовых линий заряд больше, определяют, какое значение было в ячейке.
Системная шина — это совокупность проводников между всеми устройствами системы, обеспечивающая их сообщение. Многие компоненты приставки, к которым обращается процессор, распределены по оперативной памяти в виде последовательных регионов, обращение к каждому из них происходит посредством отправки сигнала по шине адреса до целевой ячейки памяти, и она шлет на шину данных нужную информацию, которую процессор потом получает для обработки. Шина представляет собой набор проводов, каждый из которых пересылает один бит информации. Количество этих проводов определяет разрядность (битность) шины. Например, адресная шина NES имеет 16 контактов, и способна пересылать 2 байта информации, обеспечивая доступ к 0xFFFF (65535) адресам (даже если некоторые из них не существуют физически). Шина же данных у NES 8-разрядная, то есть передаваться может только один байт данных. Максимальный объем пересылаемой одновременно информации называется машинным словом, и в случае NES он равен одному байту. Слово процессоров SNES и MegaDrive состоит из 16 бит, то есть двух байт. Именно это имеют в виду, когда говорят о битности консоли (вспоминаем массивы из транзисторов, находящиеся по каждому адресу).
Ну и наконец, вкратце о процессоре. Процессор, а в нашем случае, микропроцессор, — это микроэлектронное устройство, которое выполняет машинные инструкции, написанные программистом. Он состоит из управляющего автомата, арифметико-логического устройства и регистров. Регистры — это ячейки памяти, хранящие значения, необходимые процессору для обработки, например, временные результаты преобразований, адрес следующей инструкции, специальные флаги. Арифметико-логическое устройство — это набор элементов, обеспечивающих выполнение арифметических операций (сложение, вычитание, отрицание, увеличение и уменьшение на один), а также битовых (И, НЕ, ИЛИ и сдвиги). Управляющий автомат просто управляет всем этим.
Инструкции, которые процессор выполняет своими микроэлементами, читаются из памяти (в нашем случае, из ПЗУ) по очереди, в цикле, в соответствии с архитектурой фон Неймана:
Размер команды (опкода) в битах является еще одним способом определения длины машинного слова для данной системы, а значит и ее битности. Опкоды представляют собой числа, в которые были перекодированы понятные человеку мнемоники (краткие названия команд), которые и использовались при написании программы. В таком цифровом виде они представляют машинный код. Для первых поколений игровых консолей (и некоторых компьютеров), программы писались сразу на языке, с которым работает процессор, то есть ассемблере, тогда как позже народ стал писать на языках высокого уровня типа Си, и потом компилировать Си-код под конкретный вид ассемблера (многие игры для MegaDrive тоже писались так). Каждая команда занимает в процессоре определенное число тактов, а количество тактов, которые он способен выполнить в секунду, называется тактовой частотой. Скорость выполнения каждой инструкции фиксирована, как и сама тактовая частота.
Вот, собственно, и все! Изучив всевышеописанные характеристики нужной нам системы, мы будем способны сами реализовать ее эмулятор (при условии, что мы умеем программировать, конечно). Однако, эта статья все-таки не о написании эмулятора с нуля, а просто о его устройстве, и логично было предположить, что понять устройство эмулятора можно, только если понять устройство эмулируемого девайса. Остальное станет нюансами реализации.
Эмулирующее
Фундаментально эмулятор делится на две части, которые некоторые при разработке таки смешивают в кучу, но профи давно научились разделять: ядро и клиент.
Ядро — это собственно эмулятор консоли, полная имитация всех ее действий, нужных игрокам. Ядра отличаются точностью эмуляции, открытостью исходного кода, портируемостью. Точность выясняется на основе проходимости эмулятором специальных тестовых ROM’ов, разработанных с целью проверки реализации в эмуляторе особых приемов, которые позволяла консоль, и которые использовались в играх (или демках, луркаем «Демо-сцена»). Открытость исходников определяется лицензией, под которой написано и распространяется ядро. Существуют, например, такие варианты: платная; бесплатная с закрытым кодом; бесплатная с открытым кодом, не разрешенным к изменению; бесплатная с открытым кодом, разрешающая изменения в определенных пределах; бесплатная с открытым кодом, разрешающая любые изменения и использование кода в любых других проектах. Портируемость определяется возможностью скомпилировать ядро в обычный DLL файл и использовать в таких мультиплатформенных эмуляторах как RetroArch или BizHawk, которые являются клиентами. Mednafen, напротив, распространяется со встроенными ядрами, хотя они и могут при желании быть скомпилированы в DLL и использованы вне его. Ну и есть эмуляторы, в которых ядро и клиент по хардкоду (не путать с хардкором) слиты воедино, и использовать их в таких мультиядерных клиентах не представляется возможным без существенных модификаций (FCEUX, DeSmuMe и большинство остальных).
Ядро генерирует видео, звук, другие виды вывода, если консоль это позволяет (дрожание геймпада, светомузыка, песни и пляски с цыганами и медведями), для генерации их она использует пользовательский ввод. Для того чтобы пользователь отправил ядру свой ввод, а на выходе получил требуемые развлечения, ядру нужна прослойка, которая бы сообщала его с компьютером. Этой прослойкой является клиент. Клиенты различаются целевой платформой, драйверами, пользовательским интерфейсом. Целевая платформа — это операционная система, под которую написан клиент, например Windows, Linux, MacOS, Android. Драйвера — это специальные средства работы с графикой, звуком, вводом/выводом, которые также заточены под определенную операционную систему (хотя могут быть и кроссплатформенные), и, собственно, обеспечивают нашу связь с ядром. Пользовательский интерфейс, в первую очередь, делится на интерфейс командной строки и графический, а уж в каждом из этих видов клепают кто во что горазд.
Заглянем, наконец, в ядро!
Оно должно работать с такой скоростью, чтобы визуально было неотличимо от работы консоли. Это простое требование таит в себе много головной боли для авторов эмуляторов.
Самое, пожалуй, важное понятие, касающееся этой скорости — кадр. Как и в кинематографе, изображение, показываемое на экране, меняется (обновляется) с определенной частотой. Эта частота для подавляющего большинства старых консолей настраивалась равной частоте обновления экрана телевизора: для Америки и Японии 60 кадров в секунду (NTSC), для Европы и части Азии — 50 (PAL и SECAM).
Кадр картинки, выдаваемый консолью около полусотни раз в секунду, является основным плодом ее усилий и напоминает нам, что же такого революционного в таком жанре развлечения как видеоигра. Все начиналось с летающих квадратов, но с развитием технологий человек получил возможность сначала управлять мультфильмом (когда реалистичность графики стала соответствующей), а потом и фильмом (ну почти: тут еще не дотянули, но уже на грани). Игровая графика не только дает автору возможность выразить себя визуально, но также позволяет игроку лучше проникнуться геймплеем, сюжетом, атмосферой, получить ранее недоступные эмоции от того, что помимо лицезрения всего этого великолепия, он может им еще и управлять.
Теперь к рутине. Довольно часто производители консолей стремились использовать топовые для своего времени процессоры, чтобы умыть конкурентов и дать по-настоящему уникальный экспириенс игроку, хотя, конечно, со скидкой на гарантированную возможность их серийного штампования (Дримкасту не повезло с этим, и он стал фатальной консолью для Сеги) и на юзабельность для игроделов (Атари Ягуар провалилась как раз из-за невозможности использовать пресловутые 64 бита без лютых танцев с бубном: Игорь в итоге потонул). Тот факт, что процессоры были не абы какие, означает, что воспроизвести их работу программными методами сможет далеко не всякий комп. И ядра, всерьез заточенные на точность (соответствия изначальным параметрам консоли, соблюдения даже мелких нюансов) могут и на современных компьютерах тормозить, если эмулируемая консоль не достаточно далека от нас по времени. Так, качественная эмуляция PSX или NDS на старых машинах 60 кадров в секунду уже не выдаст, а качественная эмуляция N64 может загрузить и аппарат посовременнее. И синхронная эмуляция нескольких процессоров — это только полбеды: есть еще трехмерная графика, которую современный юзер любит подвергнуть улучшениям и заставить эмулятор рендерить картинку не 320 на 240 пикселей, как делало оригинальное железо (отсюда такая пикселявость раннего 3D), а на весь экран, да еще и с дополнительным хитрым сглаживанием.
Так или иначе, частоту эмулируемой системы придется подгонять под частоту работы компьютера. Для этого ядру требуется какое-то понятие о времени. А так как процессор работает на фиксированной (и известной) частоте, то от этой частоты нам и надо отталкиваться как от фундаментальной. Так и делают сами процессоры: каждая инструкция в них выполняется определенное количество тактов. Процессорный такт — это минимальный юнит его работы. Длительность его определяется частотой, на которую настроен тактовый генератор процессора, именно он служит точным и надежным таймером для всей системы. Генератор этот выдает волну, и все механизмы процессора синхронизируют по ней свою работу. Частота обычно выставляется равной одной операции, совершаемой процессором. Вот каковы эти операции: фетч (захавывание) инструкции, декодирование инструкции, выполнение ее (основа и цель всего происходящего, собственно вычисления), доступ к памяти и предоставление результата операции. В таком случае, можно говорить о средней частоте циклов за операцию. Если же частота выставляется меньшей, чем скорость выполнения одной операции, используют выражение «частота операций за цикл».
Как же все это соотносится с кадром? Вводим новый термин — прерывание. То есть сигнал, посылаемый процессору с целью остановить текущее действие и заняться чем-то другим на время — тем, чего потребует обработчик прерывания. Самое знаменитое из прерываний — немаскируемое прерывание, NMI. Именно оно должно происходить с той же скоростью, с которой обновляется экран телевизора.. Именно оно выдает нам с этой частотой картинку, из множества которых складывается анимация. К этому прерыванию привязаны такие компоненты игровых систем как воспроизведение звука, опрос контроллеров и многие другие. И как раз из количества совершенных тактов эмулятор определяет, когда же запустить это прерывание и выдать новую картинку, звук и все остальное.
В итоге можно, наконец, представить себе то, что делает эмулятор каждый кадр:
while (!stop_emulation) // пока эмуляция не остановлена,
<
executeCPU(cycles); // выполнять инструкции в течение
// определенного числа тактов.
generateInterrupts(); // выяснить, следует ли запустить
// какое-то прерывание.
emulateGraphics(); // сгенерировать кадр графики.
emulateSound(); // сгенерировать сэмплы звука.
emulateOtherStuff(); // сэмулировать кузькину мать.
timeSynchronization(); // синхронизировать все вышеназванное.
>
И помимо осведомленности о том, сколько циклов прошло внутри него, наш эмулируемый процессор должен еще знать, сколько времени заняло воспроизведение их всех у целевого компьютера. Если тот в одну шестидесятую секунды в итоге не уложился, мы либо делаем в коде эмулятора хаки, призванные пожертвовать точностью и добавить скорости, либо говорим его хозяину купить себе таки компьютер вместо цельного куска железа. Под цельным куском железа в данном случае понимается все то, подо что автору эмулятора лень свой код оптимизировать: есть такие кодеры, у которых эмули консолей 20-летней давности и на современных машинах неважно работают, тут как говорится, хватило бы дури. Хотя, в первую очередь, все, конечно, зависит от сложности эмулируемой системы. Если же целевая машина выполняет наш код быстрее, чем работает оригинальный процессор, надо искать способы заставить всю систему подождать с отрисовкой нового кадра (и всеми причитающимися).
Следует заметить, что хотя число тактов за кадр может быть известно и фиксировано, оно не дает гарантии абсолютной точности, так как бывают задержки в очереди выполняемых инструкций (pipeline stall), промахи кэша (cache miss) и другие пакости. Эмулятор может основываться на счетчике тактов, и неточность итоговой скорости будет незаметна невооруженным глазом, но сами консоли привязывают свои тайминги к скорости движения луча по кинескопу. Причем в консолях, работающих с тайлами (плиточной графикой), генерация картинки происходит постепенно, пиксель за пикселем, сканлайн за сканлайном (так называется одна строка изображения), пока центральный процессор выполняет свои вычисления, а в более поздних, работающих с растром, была уже область памяти (фреймбуффер), содержащая всю картинку целиком и позволяющая подвергать ее нужным эффектам, и только сам вывод ее на экран там осуществлялся все так же постепенно. Момент перехода луча в кинескопе от одного сканлайна к другому называется HBlank (horizontal blanking interval), а момент его перехода от последнего пикселя последнего сканлайна к первому пикселю нового сканлайна — VBlank (vertical blanking interval).
Рассмотрим работу основных компонентов ядра.
Центральный процессор
Основной цикл действий, выполняемых процессором, мы уже видели: прочитать память по адресу, указанному в счетчике инструкций (program counter), расшифровать содержимое в пригодную для выполнения машиной форму, произвести нужные вычисления и записать результат в регистры и/или память.
Вот что из себя представляют считываемые из игры значения. Программист из-под палки собственной кровью пишет ночами максимально запутанный код под надзором полицая. Если код получается слишком простой, полицай лупит бедолагу палкой куда попадет. Для пущей сложности код пишется сразу на ассемблере, то есть на языке, с которым нативно работает целевая консоль. Потом специальная программа перегоняет мнемоники в циферки, а специальный станок записывает эти циферки на носитель методом, уже рассмотренным ранее. Если программист долго писал недостаточно запутанный код, компилировать его в машинный его заставляют в уме. Вот почему опытные РОМ-хакеры могут читать сплошной шестнадцатеричный код игр как книгу. Не исключено, что наиболее прожженные из них даже в жизни общаются исключительно наборами чисел. Ведь недаром число бит, соответствующее ширине шины, называется словом!
Чтение, расшифровка и выполнение написанного программистом осуществляется в гигантских количествах (тактовая частота процессора NES — 1,79 МГц, а процессора MegaDrive — 7,67 МГц, то есть, имеем дело с миллионами команд в секунду), поэтому желающему научить свою программу это делать обычно требуется найти самый быстрый способ. Вот основные методы:
Интерпретатор
Самый простой метод, который потом еще и отлаживать легко, но самый медленный, так как мы просто повторяем как есть действия процессора. То есть мы берем опкоды и интерпретируем их как указатели, какую функцию нам выполнить: заготавливаем отдельную функцию под каждый опкод, и потом просто «прыгаем» по той огромной таблице функций, которая у нас получилась. Стоит заметить, что стандартная конструкция
здесь не применима, так как проверять отдельно каждое условие чрезвычайно медленно. Народ обычно использует оператор switch, который позволяет вместо проверки каждого условия по отдельности просто совершать мгновенные прыжки к нужной функции. Причем функция в данном случае — это не обязательно конкретная функция в программе, а просто некий порядок действий. Вот как будет выглядеть использование этого оператора с прыжками на основе машинного кода инструкции:
switch (machine_code)
<
case 0:
// операции для опкода 0
break;
case 1:
// операции для опкода 1
break;
.
case N:
// операции для опкода N
break;
default:
// если пришедшее значение не соответствует
// ни одному из известных опкодов
// даем юзеру понять, что это нелегальная инструкция
break;
>
Можно и по-другому. Заготавливаем собственно функции на каждый опкод, а указатели на адреса этих функций в памяти программы помещаем в большой массив (таблицу). Тогда доступ к ним можно будет осуществлять по порядковому номеру, которым тоже будет машинный код:
Вызов этих функций будет выглядеть так:
Есть еще один способ. И большинство программистов его с младенчества ненавидят, хотя визуально он будет мало отличаться от метода через switch. Это метод через goto. То есть вместо switch/case перед каждым набором операций ставим лейбл и все так же заставляем программу прыгать по этим лейблам.
Если скорости в итоге все равно не хватает, можно все это написать… тоже на ассемблере! Правда хороший компилятор языка высокого уровня (Си сотоварищи) вашу таблицу прыжков тоже скомпилирует в ассемблерный код, и получится примерно то же самое. Но для настоящего гика этого, конечно, недостаточно, и он будет сам писать на языке целевого процессора, просто потому что может (SWAG).
Рекомпилятор (двоичный транслятор)
Этот прием, в силу его сложности и возможных проблем, используется не часто, и только там, где без него совсем туго. Суть его в том, чтобы считывать кусок кода из игры и переводить (транслировать) его в двоичный код целевого процессора на лету — динамическая трансляция. Существует еще статическая трансляция, когда мы берем всю игру целиком и конвертируем перед запуском. Но это в эмуляторах практически не используется, так как:
Если мы помимо трансляции еще и измеряем частоту обращений к каждому блоку кода и оптимизируем те, что чаще вызываются — это будет динамической рекомпиляцией. Вот она-то и используется чаще всего, так как позволяет достичь существенного прироста в скорости для систем вроде N64 и более поздних.
Примеров не будет, так как это уже совсем дикие дебри. Лучше перейдем к тому, что собой представляют функции, имитирующие поведение процессора.
По сути, это будут функции работы с двумя видами данных — с внутренним состоянием процессора и с памятью, отражающей состояние всей остальной системы. Состояние процессора обычно называется контекстом, и в случае, если эмулятор имеет функцию сэйвстейтов, может быть всем скопом сохранено в файл вместе с прочей памятью. В контекст процессора могут входить как регистры, которые в нем есть физически, так и всякие служебные элементы программы, оригинальной системе неведомые, но придуманные программистом (специальные переменные, указатели на функции, векторы прерываний).
Вот как выглядит контекст центрального процессора эмулятора FCEUX:
typedef struct __X6502 < // контекст представлен структурой:
int32 tcount; // временный счетчик циклов
uint16 PC; // счетчик инструкций
uint8 A, X, Y, S, P, mooPI; // прочие регистры
uint8 jammed; // остановлен ли CPU
int32 count; // основной счетчик циклов
uint32 IRQlow; // контакт сигнала прерывания
uint8 DB; // кэш шины данных
int preexec; // неиспользованная переменная 😀
#ifdef FCEUDEF_DEBUGGER // указатели на функции отладчика
void (*CPUHook) (struct __X6502 *);
uint8 (*ReadHook) (struct __X6502 *, unsigned int);
void (*WriteHook)(struct __X6502 *, unsigned int, uint8);
#endif
> X6502;
А так выглядит функция одного из опкодов в Genesis+GX, ядре MegaDrive в составе RetroArch (размер регистров там равен 4 байтам):
// копирование содержимого одного регистра в другой
static void m68k_op_move_32_d_d(void)
<
// получить из регистров информацию об источнике (resource)
uint res = DY;
// получить из регистров информацию об адресате (destination)
uint* r_dst = &DX;
// скопировать исходное число в нужный регистр
*r_dst = res;
// установить флаги регистра состояния
FLAG_N = NFLAG_32(res);
FLAG_Z = res;
FLAG_V = VFLAG_CLEAR;
FLAG_C = CFLAG_CLEAR;
>
Все, что здесь обозначено заглавным шрифтом, представляет собой макросы препроцессора, то есть функции, созданные при помощи директивы #define. Она позволяет нужные команды вставлять прямо в код (избегая необходимости каждый раз вызывать функцию, что стоит лишнего времени), используя при этом короткие имена, отделяя тело макроса от его имени пробелом. Там, где надо максимально разогнать код, макросы используются предельно часто. Вот как определены DX и DY из выдержки выше:
#define DX (REG_D[(REG_IR >> 9) & 7])
#define DY (REG_D[REG_IR & 7])
REG_D и REG_IR — это тоже макросы, дающие доступ к элементам контекста процессора, >> и & — битовые операции, повторяющие нюансы его работы. Таким образом, можно все что угодно выстроить в последовательность макросов и обозначить коротким именем, и она при компиляции выстроится в полноценный код. Читать и отлаживать его будет невыносимо, но зато все будет летать.
Память
Обычно абсолютно все компоненты системы, к которым процессор может осуществлять доступ, имеют свои адреса в памяти. Описание того, какие адреса какому устройству соответствуют, называется картой распределения памяти (memory map). Есть случаи, когда устройства ввода/вывода мапятся не в память, а на особые порты, но консоли это редко используют, и обычно все доступно по единой карте адресов.
Эмуляция памяти имеет меньше нюансов, чем эмуляция процессора. Программе просто говорят выделить столько памяти под каждый регион, сколько реально будет нужно. Чтение и запись осуществляют разными функциями, обычно проверяющими принадлежность к региону, права на запись и еще много того, на что хватит фантазии.
// чтение из оперативной памяти
unsigned int readRAM(unsigned int address)
<
// проверка принадлежности
if (address >= 0 && address
Можно еще на лету распознавать, к какому региону обращаться, но, как мы помним, все эти if/else очень долгие, когда речь заходит о миллионах операций в секунду. Для ускорения работы следует выкинуть все возможные проверки, не пожертвовав при этом стабильностью, конечно. Вот что делает FCEUX:
typedef uint8 (*readfunc)(uint32 A);
typedef void (*writefunc)(uint32 A, uint8 V);
#define DECLFR(x) uint8 x(uint32 A)
#define DECLFW(x) void x(uint32 A, uint8 V)
static DECLFR(ARAM) <
return RAM[A];
>
// объявление «заготовщика»
void SetReadHandler(int32 start, int32 end, readfunc func) <
// все нужные проверки
.
// перебрать все ячейки региона памяти
for (x = end; x >= start; x—)
// создать фанкцию для каждой из них
ARead[x] = func;
>
// пример вызова
SetReadHandler(0, 0x7FF, ARAM);
static __inline uint8 RdMem(unsigned int A)
<
return(_DB=ARead[A](A));
>
В итоге, при чтении или записи в память в ходе работы ядра, FCEUX избегает всех проверок. И хотя ядро в нем не на ассемблере, оно крайне быстрое.
Важные особенности эмуляции памяти — размер машинного слова в оригинальном регионе и порядок байт в этом слове. Если порядок big-endian, то сначала идет старший байт (most significant byte), как в привычной нам десятичной системе. Если little-endian, то сначала младший байт (least significant byte), и хотя для микропроцессоров как раз такой подход удобнее, человеку он непривычен. Эмулятор МегаДрайва (слово равно двум байтам) Gens для всего региона ROM при загрузке игры меняет в ней каждые два байта местами (swap), чтобы уравнять его параметры с прочими регионами и обращаться к ним всем одинаково. А от размера слова будет зависеть, какие указатели использовать для каждого региона: например для Си-подобных языков, если эмулируемая машина работает только с байтами, используется указатель на char, если слово равно двум байтам — на short, если четырем — на int. То есть при обращении к адресу памяти, компьютер автоматически будет работать с тем количеством байт, которое соответствует типу указателя.
Бывают еще банки памяти. Например, когда физически доступная память не вмещается в отведенный регион. Так устроено подавляющее большинство игр для NES — они хранят больше данных, чем приставка может за раз прочитать. Тогда в картриджи встраивают специальные механизмы переключения памяти, которые подсовывают в видимую консоли область разные регионы РОМа. Конечно, образы игр сами по себе, ничего переключить не в состоянии, поэтому нужный функционал добавляется прямо в эмулятор, ведь он загружает как-раз-таки всю игру сразу, и потом уже манипулирует банками (и бутылками).
Графика
Компьютерная графика бывает двухмерной и трехмерной. Из разновидностей двухмерной графики в консолях использовалась растровая (в частности, пиксель арт), причем строилась она из плиток (tile), размер которых обычно был 8 на 8 пикселей. Из этих тайлов составлялись слои, которые могли на экране двигаться независимо друг от друга — фон и спрайты. Память и быстродействие всегда были сильно ограничены в сравнении с желаемыми. Поэтому ставился упор на то, чтобы, имея одновременно загруженным минимальное число тайлов, максимальное их число использовать повторно — отсюда знакомые всем повторяющиеся узоры в играх. Трехмерная графика в консолях обычно основана на полигонах. То есть система работает с трехмерным пространством, где каждый объект (если это не спрайт, как в Думе) состоит из вершин, ребер, граней, полигонов, поверхностей и накладываемых на эти модели текстур. И в соответствии с тем, насколько хорошо программист знает тригонометрию и законы физики, эти модели движутся в пространстве и взаимодействуют друг с другом, а мы все это видим глазами двухмерной камеры.
Само по себе рисование на экране компьютера тайлов — дело нехитрое. Задаем координаты каждому из них и двигаем по экрану. Эффект наложения создается благодаря альфа каналу, позволяющему нам задавать прозрачность, а спрайтам — иметь какие-то другие очертания, кроме прямоугольных. Цвета задаются через ARGB (в разных сочетаниях), то есть альфа канал, красный, зеленый и синий. Компьютер может работать и с цветовой моделью, которую использовали ранние консоли — YUV (канал яркости и два цветоразностных), но почему-то так никто не делает.
Проблемы же начинаются, когда мы пытаемся создать в эмуляторе изображение, идентичное оригинальному на консоли:
Эмуляция видеопроцессора обычно состоит из трех этапов.
Звук может вырабатываться двумя способами: генерация и сэмплирование. В первом случае, сам звуковой чип синтезирует тон, позволяя программисту (и, желательно, музыканту по совместительству) менять различные параметры этого синтеза (частота, громкость, скважность, фаза, форма волны). Во втором, в игру закатывается специальным способом закодированный отрезок звука нужной длительности (например, по одной ноте на каждый инструмент, либо звуковой эффект целиком), и приставка просто воспроизводит его на нужной высоте, зацикливая, если надо.
Важной особенностью звука в консолях является частота семплирования (дискретизации), то есть частота обновления амлитуды (громкости) сигнала. Вообще, слово сэмпл обозначает отрезок оцифрованного звука, и относится как к целому звуковому фрагменту (массиву), так и к каждой минимальной его части (элементу массива). Аналоговый звук непрерывен и может передать любую частоту, но при оцифровке возможно лишь выхватывать отдельные значения амплитуды с ограниченной скоростью, поэтому цифровой звук всегда дискретен (гуглим). И чтобы воспроизвести звук нужной частоты, нам надо обновлять амплитуду сигнала с частотой вдвое большей (как минимум). Например, семпл с частотой дискретизации 40 кГц может содержать звук с частотами до 20 кГц
Другой важной особенностью цифрового звука является его битность, то есть максимальное количество возможных уровней амплитуды, которые он может передавать. Если мы кодируем звук восьмью битами, он будет ограничен 256-ю уровнями громкости. И именно они будут чередоваться на нашей частоте дискретизации.
Самым простым способом эмуляции звукового чипа-синтезатора (PSG, programmable sound generator) будет запись сэмплов в буфер на той частоте дискретизации, на которую способен сам чип. Например, треугольный канал приставки NES способен выдать максимальную частоту тона 55,9 кГц, что потребует частоты дискретизации 111,8 кГц. Однако, работать с такими значениями непрактично (да и услышать такие частоты невозможно), поэтому все эмуляторы применяют разные методы передискретизации, то есть конвертируют сигнал, меняя частоту его дискретизации на традиционные значения (44,1 кГц, 48 кГц, 96 кГц). Могут применяться еще и дополнительные фильтры на усмотрение автора, так как оригинальный звук тоже всегда подвергается фильтрации внутри консоли.
Генерация квадратной волны (стандартной для 8-битных консолей) сводится к записи значения амлитуды в буфер в течение времени, соответствующего длине волны. Таймер, считающий, сколько должен длиться период волны относительно циклов CPU, в разных консолях разный. Вот его формула для NES:
timer = (cpu_freq/(16*note_freq)) — 1
Чтобы получить ноту Ля (440 Гц) от приставки NES (1.79 МГц), мы должны в таймер квадратной волны (тип волны первых двух каналов) записать 253. Следует еще учитывать скважность, чтобы знать, какую часть периода волны требуется заполнять высоким уровнем, а какую низким. В случае треугольного и пилообразного каналов, надо не повторять запись одного и того же значения, а чередовать уровни амплитуды соответствующим образом. Шумовой канал создает псевдослучайные скачки амплитуды на заданной частоте.
Более совершенные звуковые чипы были способны на частотно-модуляционный (FM) синтез. То есть берется волна определенной формы (обычно синусоидальная) и ее частота модулируется амплитудой другой волны. В итоге частота несущей волны (carrier) будет меняться в точном соответствии с изменениями амплитуды волны модулирующей. Если частота модулирующей волны низкая, мы услышим изменение высоты тона у несущей волны. Если же частота достаточно высокая, мы услышим изменение тембра.
Вот формула получения сэмпла звука на момент времени t (префикс c_ соответствует несущей волне, а префикс m_ — модулирующей, amp — амплитуда, angular_freq — угловая частота):
F = c_amp*sin(c_angular_freq*t + m_amp*sin(m_angular_freq*t))
А вот ее реализация в Genesis+GX:
inline signed int op_calc(
unsigned int phase,
unsigned int env,
unsigned int pm
) <
unsigned int p = (env > SIN_BITS) + (pm >> 1)) & SIN_MASK
];
if (p >= TL_TAB_LEN) return 0;
return tl_tab[p];
>
Так как это уже матан, ограничимся несколькими замечаниями:
Наконец, простейшая система — это семплеры. Там уже не требуется ничего генерировать на лету, все сводится к манипуляции готовыми потоками данных. Предварительно их может понадобиться переконвертировать из одного из множества форматов, в зависимости от того, с каким из них работает эмулируемая система, в тот, с которым работает целевая система.
NES может использовать два вида семплов: однобитные и семибитные. В первых изменение уровня исходного сигнала закодировано одним битом, то есть он либо понижается на одну ступень, либо повышается. Для этого сравниваются текущие уровни исходного и закодированного сигналов, и если исходная амплитуда выше — в закодированный следующим пишется 1, иначе 0. Такие семплы практически ничего не весят, но звучат относительно разборчиво. Частота дискретизации у них стандартная, так как NES способна в фиксированные интервалы времени сама воспроизводить новую порцию битов. Семибитные же семплы имеют такую точность соответствия исходному сигналу, какую захочет разработчик, так как, во-первых, все закодировано 7-ю битами (128 уровней), во-вторых, автор игры сам решает, когда воспроизвести новый отрезок семпла, хоть каждую инструкцию.
В более поздних консолях, где уже не требовалось так параноидально экономить, форматы семплов были ближе к тем, которые известны современным компьютерам.
При воспроизведении семплы иногда подвергаются изменению высоты тона (pitch) (так Наоки Кодака создал свой знаменитый семплированный бас, а SNES пыталась имитировать электрогитару), фильтрации (интерполяция звука в SNES, реверберация в PlayStation) или зацикливанию для увеличения длительности ноты.
После генерации семплов каждого инструмента их необходимо смикшировать. Это делается сложением амплитуд всех исходных каналов. Однако, это не стадион, где может одновременно кричать несколько тысяч человек, и верхней границы громкости не будет. В цифровом звуке она есть, это уже упомянутая битность. Допустим, мы работаем с 8-битным звуком. Если в результате сложения каналов получается амплитуда больше максимально допустимой (например, 400), начинаются сложности, так как приходится придумывать способы сохранить информацию и донести в максимально точной форме. Просто делить итоговую амплитуду, пока она не влезет, нельзя: каждый канал начнет звучать тише, и потеряются нюансы, на которые он тратит все доступные ему уровни громкости (256). Можно просто ограничивать амплитуду каждого семпла до максимально допустимой (clamp), но так тоже теряются характеристики звука. То же самое с отсечением всех чрезмерных амплитуд. Какое решение будет универсальным? Матан! И мы его снова пропустим.
Наконец, для выдачи всех сгенерированных и обработанных семплов человеку обычно используется кольцевой буфер. То есть, ядро пишет данные в массив, при достижении последней ячейки оно начинает записывать снова с первой, а в это время звуковой драйвер с нужным интервалом считывает из этого буфера данные, играя пользователю чиптюновую версия Бетховена. Или Ламбаду. Есть такие драйвера звука, которые позволяют вообще всю скорость работы эмулятора привязать к частоте сэмплирования. С одной стороны ядро пишет нужное количество сэмплов в буфер и ждет, когда драйвер их прочитает, и только после этого продолжает эмуляцию, с другой стороны драйвер забирает семплы и воспроизводит их со стабильной частотой, не допуская пауз. Так работают эмуляторы, использующие кроссплатформенную библиотеку SDL.
На уровне ядра ввод обычно реализуется функциями записи в память (в регистры ввода), и раз в кадр ожидается поступление от клиента информации о том, какие кнопки нажаты (каждая кнопка обычно кодируется одним битом), и каково состояние аналоговых устройств управления. Тогда игра, читая соответствующие адреса памяти, сама все сделает как надо.
Поддерживаемые консолью устройства ввода могут быть самыми экзотическими, как и поддерживаемые клиентом (при помощи соответствующего драйвера), поэтому перестанем забивать себе голову и перейдем к заключению.
Заключение
Если мама разрешит, в следующий раз я расскажу вам об устройстве клиента и инструментах эмулятора, которые выгодно отличают его от консоли. А можете сами это все изучить в качестве домашнего задания. Напоследок, вот ссылки, которые могут помочь понять описанное в этой статье (ясное дело, на английском):
Ладно, уговорили, вот на русском:
Автор статьи: feos