Процессоры

Часть 1

Многие устройства в игре будут программироваться игроком на стадии подготовки к игре. Гамер будет готовить программы, которые он должен написать, отладить, проверить и т.д. Я вот и думал, как это все будет лучше организовать, чтобы и возможностей было побольше, и гемороя поменьше, чтобы все куски программы оказались поуниверсальней, и вот что мне придумалось.

Будет некоторый объект Processor, который должен уметь выполнять программы и обращаться к внешним устройствам (двигатели робота, сенсоры, радары, передатчики и др.), чтобы реализовать свои намерения. Сразу возникает вопрос: память - это часть процессора или некоторое внешнее устройство? Я не вижу однозначного ответа, так что это стоит обсудить. Этот процессор будет иметь связи с внешними устройствами путем слотов (как и в компьютерах). В начале игры игрок будет настраивать этот процессор и говорить, к какому слоту что у него подключено.

Дальше про организацию программ. Для расширения возможностей пользователя надо дать ему возможность самому на низком уровне программировать процессор, то есть не ограничивать пользователя тем, что мы навязываем ему какой-то свой скриптовый язык. Не исключено, что найдутся люди, которые напишут свои крутые оптимизирующие компиляторы под наш процессор, их компиляторы будут генерить крутейшие программы, которые будут очень компактные и будут делать всех остальных. Так что наш скрипт-язык должен быть легко заменяем на любой другой язык. Для этого процессор должен иметь обычную для настоящих процессоров архитектуру - выборка команды->загрузка опкода->загрузка операндов->обработка-> сохранение результата->выборка команды->..... По тем же соображениям (не ограничивать игрока) надо сделать работу с внешними устройствами не с помощью функций типа move_to_cell(564, 21), а с помощью вывода команд в регистры: out robot_port, (ENGINE_LEFT & ENGINE_RIGHT & ENGINE_START & NO_POWER) Разумеется, что кайф от такого программирования получит не каждый, а только тот, кто хочет добиться ультравысокой производительности своей системы. Как и любое программирование на асме, это позволяет выжать максимум из своих юнитов. Для игроков, которым больше по душе уровень повыше, написать крутой модуль стратегического планирования, тактической обороны и т.д., у нас будет готовая библиотека, которая как бы закрывает от пользователя детали работы с портами, и предоставляет более высокоуровневый интерфейс для программиста, как раз функции типа enter_nearest_bunker(now). И эта библиотека при подключении как и любая другая программа тоже отожрет кусок памяти (ну в общем все, как и в нормальных процессорах).

Раз появляется такое многообразие возможностей, надо сделать эффективную систему, с помощью которой можно будет слинковать библиотеки, программы, данные в единое целое, готовое для загрузки в процессор, чтобы библиотеки, в какие адреса их не прилинкуй, нормально работали, чтобы либо автоматически пересчитывались адреса, либо чтобы внутренние переходы внутри библиотеки были относительными, это еще надо решить. Каждый юзверь волен сам придумать/ загрузить/использовать такую систему. То есть его модули не будут восприниматься теми тулзями, которые предложим мы, но у него может быть свой инструментарий для построения программ. В качестве примера может быть динамическая линковка модулей уже во время исполнения. Когда центральная управляющая станция принимает какое-то тактическое решение, то определенным роботам, задействованным в операции, будет заслана нужная программа. Все их хранить на роботе не получится, слишком много памяти потребуется, а вот подгружать по мере необходимости из центра с пол-пинка. Наши инструменты не будут поддерживать такие экзотические решения, их придется программировать игроку самостоятельно.

Предлагаю наш вариант сделать таким, как это сделано в линуксе, самой рулезной операционке из всех операционок (наши поезда самые поездатые поезда в мире). :)

1 стадия - компиляция программы

Каждый кусок программы, все библиотеки и т.д. компилируются с получением объектного файла. Но объектный файл не такой, как в большинстве компилеров, а с изюминкой. Библиотеки будут линковаться к программе не целиком, а только в той части, которая реально используется программой. Для этого в объектном файле будет записана программа. М-да, чтобы это нормально объяснить, приведу пример.

---------- MODULE 1 ----------
variable A1
variable B1

function A
    A1 = 5
end
--------- END MODULE ---------

---------- MODULE 2 ----------
extern variable B1
extern function A
variable C1

function B
    B1 = 5
    call A
end

function C
    C1 = 5
end

function main
restart:
    call B
    goto restart
end
--------- END MODULE ---------

Видно, что функция C и переменная C1 не используются программой, так что они вообще не должны быть прилинкованы к программе. Значит после компиляции образуются объектные файлы:

--------- OBJECT 1 ---------
Код:
0000    00000001 00000000 00000005      ; mov A1, 5
0003    00000002                        ; ret
0004    00000000                        ; место для переменной A1
0005    00000000                        ; место для переменной B1
Таблица экспортируемых символов:
0000    function        A
0004    variable        A1
0005    variable        B1
Таблица импортируемых символов:
0001    variable        A1
Таблица блоков:
        0000                            ; функция A
        0004                            ; переменная A1
        0005                            ; переменная B1
--------- END OBJECT ---------

--------- OBJECT 2 ---------
Код:
0000    00000001 00000000 00000005      ; mov B1, 5
0003    00000003 00000000               ; call A
0005    00000002                        ; ret
0006    00000001 00000000 00000005      ; mov C1, 5
0009    00000002                        ; ret
000A    00000000                        ; место для переменной C1
000B    00000003 00000000               ; call B
000D    00000004 00000000               ; goto restart
Таблица экспортируемых символов:
0000    function        B
0006    function        C
000A    variable        C1
000B    function        main
000B    label           restart
Таблица импортируемых символов:
0001    variable        B1
0004    function        A
0007    variable        C1
000C    function        B
000E    label           restart
Таблица блоков:
        0000
        0006
        000A
        000B
--------- END OBJECT ---------

Что здесь что значит:

Каждый модуль делится компилятором на блоки - функции и переменные. Особенность блока состоит в том, что он не может быть разорван на куски линкером. Поэтому первый модуль состоит из трех блоков - функция и две переменные. А у второго модуля - две функции и одна переменная. Линкер будет определять, что засунуть в готовый бинарник, выкидывая неиспользованные блоки. Теперь про особенности импортируемых и экспортируемых символов: с экспортом все понятно. Все, что объявлено в данном модуле, должно быть экспортировано, то есть ставятся в соответствие адреса в программе и идентификаторы всего того, что написано в этом модуле. Импортируемые символы: все, что я использовал в этом модуле, не зная заранее адресов.

2 стадия - линковка программы

Здесь начинается самая дискотека. Адреса в каждом модуле начинаются с нуля, а задача линкера - упихать все эти модули в единое адресное пространство процессора. Во-первых линкер строит таблицу "кто кого юзает".

main ----> B ----> A ----> A1
           |
           V
           B1

C ----> C1

Эта таблица строится путем определения, какой блок импортирует данные из каких блоков. Например, блок, содержащий функцию B импортирует данные из блоков, содержащих B1 и A.

Очевидно, что C и C1 не должны быть прилинкованы к программе. Потом линкер решает, в каком порядке складывать это дерьмо в программу. В принципе это неважно, но решить это все равно надо. Например, чтобы получше объяснить принцип действия линкера, положим сначала все переменные, а потом все функции в таком порядке:

0000    variable        A1
0001    variable        A2
0002    function        A
0006    function        B
000C    function        main

Компилятору на самом деле насрать, что это - переменные или функции. Он размещает блоки! Остальное не его забота! Да и вообще он не использует информацию о том, какой блок.

Значит все положили в память, теперь самое сложное - пофиксить адреса. Как это делается:

Компилятор строит таблицу соответствия идентификаторов и их реальных адресов. Получается таблица типа такой:

0000    A1
0001    B1
0002    A
0006    B
000C    main
000C    restart

Потом по таблицам импортируемых символов фиксятся адреса импортированных объектов. Линкер берет каждую строку таблицы импортирования и смотрит: Например "0001 variable A1" (из 1 модуля). Он думает: Так! Локальный адрес 0001 модуля 1 соответствует блоку A! Ага! Относительный адрес внутри этого блока равен 1! Реальный адрес этого блока равен 2! Значит реальный адрес места, которое надо фиксить равен 3! И туда надо положить адрес A1, т.е 0000. Аналогично делается и со всеми другими импортируемыми символами. Естественно, если определили, что локальный адрес соответствует неиспользованному блоку, то этот фикс не производится.

Соответственно получается такая программа:

0000    00000000                        ; A1
0001    00000000                        ; B1
0002    00000001 00000000 00000005      ; mov A1, 5
0005    00000002                        ; ret
0006    00000001 00000001 00000005      ; mov B1, 5
0009    00000003 00000002               ; call A
000B    00000002                        ; ret
000C    00000003 00000006               ; call B
000E    00000004 0000000C               ; goto restart

А, еще! Во всех примерах я предположил, что каждое число в программе занимает 4 байта, а адресация ведется не каждого байта в отдельности, а всего 4-байтового слова. Я думаю, что так будет даже лучше, это и обрабатываться быстрее будет, и гемороя для разработчиков программ будет меньше, им как минимум не придется экономить место в программах, утомительно подбирая нужную инструкцию.

Как будет производится загрузка и выполнение программ:

В процессоре начальные адреса памяти зарезервированы для внутренних нужд. Как и в настоящих компах, там будет что-то вроде системной области. Поясню: чтобы программы роботов не висли и не падали в General Protection Fault, там будет сделан некий механизм обработки исключений. Вот предполагаемая раскладка памяти:

0000    INIT    адрес инициализации программы (функция main или как там ее)
0001    RUN     адрес запуска программы
0002    FAIL    адрес ошибки (исключения)
0003    STACK   текущий указатель стека

Что здесь чего делает:

Как только у робота появляется питание, либо по команде RESET, процессор выполняет команды с адреса INIT до специальной команды STOP. Как только появляется STOP, процессор автоматически переходит на выполнение части RUN. Как только RUN дошел до STOP, опять вызывается RUN, и так далее. Если во время выполнения программы возникает сбой (например неизвестная инструкция), то процессор автоматически переходит в FAIL, выставляя еще в каком-нибудь адресе причину ошибки. Как только там всретится STOP, опять продолжится выполнение с RUN. STACK - указатель стека, я думаю тут комментарии излишни. Его кстати можно задавать на этапе компиляции программы. Опкод у команды STOP лучше всего сделать 0, чтобы, если программа попадет в область памяти, содержащую нули, чтобы она просто вылетела на старт, а не повисла.

Общение с периферийными устройствами. Тут еще пока темный лес, но есть какие- то идеи. Например, с передачей по радио. Есть специальное устройство, которое, будучи воткнуто в один из слотов, при поступлении пакета записывает в один из портов признак об этом. Потом программа пишет в другой порт этой "сетевухи" адрес, куда поместить пакет, и сетевуха сама копирует пакет в этот участок памяти, или может быть при инициализации компьютера задается маппирование адресов внутренней памяти сетевухи на какие-то адреса процессора. Короче, будут идеи, пишите.

Процессоры. Часть 2.

Тут я напишу, как я реализовал компилятор и процессор. Часть сказанного в первой части я оставил таким же, часть пересмотрел. Здесь я скажу, что конкретно я оставил, а что поменял.

Ну во-первых, как вообще устроен компьютер робота:

Процессор выполняет команды, считываемые из памяти, и в соответствии с ними обращается к памяти или устройствам. Регистров как таковых нет, вместо них надо просто использовать ячейки памяти. Кроме того, обращение к устройствам ничем не отличается от обращения к обычным ячейкам памяти. Просто у каждого устройства есть свой базовый адрес и начиная с этого адреса некоторое количество адресов зарезервировано за этим устройством. Память также является просто рядовым устройством, и реализована как потомок объекта Device. Еще будет устройство например "engine", который будет цепляться только к роботам и координировать его движения. А, да, еще! Все ячейки в памяти 32-битные. Это значит, что нет операций над 8-битными или 16-битными числами. Нет лишних инструкций, нет длинных команд (все вообразимые команды элементарно впихнутся в 32 бита) и все такое. М-да, но вернемся к нашим баранам. Пока я планирую на Engine отвести всего одно слово, и биты развести так:

       00000000 00000000 00000000 00ddeepp
                                          
 направление (0 - вперед) правого --/   \+-- мощность двигателя (0-3)
   направление (0 - вперед) левого --/ \-- включить левый
                                      \-- включить правый

Отдельно управляется левая гусеница робота и отдельно правая. Для каждой из них можно указать, в каком направлении она должна крутиться и должна ли вообще. Обе гусеницы работают от одного мотора, которому и надо будети задать мощность.

Вот так устроены устройства. Теперь о выполнении программ. Как нетрудно увидеть, работа собственно симуляции игры управляется таймером, т.е. игровая ситуация просчитывается строго определенное число раз в секунду. Так вот, за каждый такой такт объект Computer получает управление ровно один раз. И за этот раз он выполняет несколько инструкций. Сколько именно, определяет поле Computer::clock. Для каждой инструкции декодируется команда, берутся нужные операнды, делается нужное действие, и записывается полученный результат. Если на каком-то из этих этапов произошла недопустимая операция, например деление на 0, то команда все равно выполнится до конца, только вот ее результат будет непредсказуемым, например, в переменную-результат деления может попасть какое-то совсем левое число. Про INIT, RUN, FAIL и STACK написано в первой части.

Далее, что касается слишком низкоуровневого языка. На выбор игроку будет предоставляться туева хуча разных языков, начиная от банального ассемблера, и заканчивая супер-навороченным языком построения AI-сценариев. Чем конкретно пользоваться, игрок решит сам. Тогда получается довольно интересно: один игрок - это фанат программирования на низком уровне, он может засесть и за ночь написать код например для быстрого объезда препятствий, что в результате выльется в качественное преследование или убегание от противника, а другой - специалист в области AI, он будет неделю мучиться, писать крутое стратегическое планирование для своих роботов. Но дальше - самое интересное: второму игроку придется пользоваться стандартными низкоуровневыми библиотеками, которые могут быть не настолько качественно оптимизированными, как у первого игрока, а у первого игрока будет только какой-то его собственный, не навороченный AI, который скажем не сможет быстро приспособиться к какой-то изощренной атакующей тактике противника. То есть, у каждого из программистов появляется возможность работать над тем, что ему интересно, а не только над высокоуровневым AI. Я уже писал в первом тексте про то, как особо извращенные программисты могут сделать свои какие-то плагины, которые будут уже во время работы подгружаться в роботов. Это их воля, если они решат так оптимизировать работу своих программ. А если делать только высокоуровневые скрипты, то тут уж хрен их подгрузишь. Кроме того, так можно будет писать вирусы для роботов противника, тоже неплохая идея, и все это возможно только при низкоуровневом языке.

Теперь о том, КАК именно можно будет интегрировать крутые и оптимизированные низкоуровневые программы с высокоуровневым AI.

Например программист-системщик пишет подпрограмму moveto и кладет ее в файл move1.ro (ro означает Robots Object), другой программист пишет другую процедуру moveto и кладет ее в move2.ro. Точно так же поступают два высокоуровневых программиста - пишут подпрограммы enter_nearest_bunker и делают соответственно два файла - bunker1.ro и bunker2.ro. А потом пользователь хочет написать свой крутой AI. Для этого он пишет свою программу, которая например использует процедуру enter_nearest_bunker и компилирует ее в main.ro. А потом он начинает экспериментировать, какая комбинация работает лучше, и определяет ту, которая и будет в его окончательном варианте. Все ro потом линкуются в файл re (Robots Executable), который уже не содержит никакой символьной информации, а просто представляет собой копию памяти процессора. Например: link -o program.re bunker1.re move2.re Таким образом, если пользователь хочет иметь какой-то высокоуровневый язык для написания программ, он просто достает низкоуровневую библиотеку и линкует ее к своей высокоуровневой программе. Кстати, ее можно будет писать не только на SRL, но и на каких-то других языках, которые кстати, пользователь сможет разработать сам, если конечно приспичит :)

Как организованы компилятор и линкер, написано в первой части, я все реализовал без изменений, то есть неиспользованные процедуры и переменные вообще не линкуются к программе.



Вернуться на страницу проекта


Вернуться на главную страницу

100% MS Free Rambler's Top100 Мониторинг сервера осуществляется системой UpTime.Ru
Best viewed with Lynx