Сразу надо сказать, что эта дока не несёт почти никакого практического значения. А посему - все те методы о которых я говорю не стоит воспринимать как руководство к действию. Проще и, уж точно, в десятки раз быстрее найти и поинтеры и их значение простой отладкой кода, благо инструменты, которые появились в последнее время, отличнейшие. Не чета тем, что были во времена написания всех документов для начинающих.
Эта дока предназначена исключительно для лучшего понимания, что такое поинтеры на NES и просто раскрывает принципы действия всех тех методов, что описываются в литературе для начинающих. Она для тех, кто хочет по-настоящему понять что такое “указатель” (в дальнейшем, будем подразумевать, что поинтер и указатель - одно и то же).
Итак, начнём.
Так всё-таки, что такое указатель?
Сразу надо предупредить, что большая часть того, что здесь написано, успешно объяснена CaH4e3’ом в своей доке, поэтому я написал эту главу для тематической целостности документа. Еще, может, кому-то будет любопытно взглянуть на это под другим углом
Для того чтобы загрузить текст на экран, его необходимо впервую очередь скопировать из РОМа в оперативную память приставки, чтобы с ним можно было работать (прочитать, скопировать, и т.д.)
Начнём с азов: Оперативная память NES(далее будем называть RAM) имеет размер в 64Кб (0000-FFFF), причём вторая половина(8000-FFFF) зарезервирована под куски РОМа (далее ROM). Т.е. именно сюда и копируется текст прямо в том виде, как он находится в ROM’e. В далёкие времена Супер Марио этого пространства хватало, чтобы скопировать в него весь ROM и работать с ним дальше как заблагорассудится. Особенно это касалось PRG-ROMa (это весь код и неграфические данные, хранимые в ROM’e)Графические данные хранятся в CHR-ROMe. Просто рассматривайте образ, который загружается в эмулятор в виде двух файлов(CHR и PRG ROM’ов) сложенных вместе. Очень скоро разработчики столкнулись с тем, что данных становилось все больше, а память оставалась прежней - всё те же 32Кб. Поэтому были спроектированы специальные составляющие картриджей, которые на уровне железа разбивали целиковый большой PRG или/и CHR ROM’ы на части (в дальнейшем, будем называть их банками). Они же подсовывают те банки, которые нужны игре в данную наносекунду в разные части RAM’a. Что касается размеров банков, то PRG ROM’ы разбиваются на банки по 16Кб, а CHR ROM’ы - по 8Кб (по крайней мере, так дела обстоят в наиболее распространённой системе формата файлов ‘.NES’). Однако у разных мапперов возникают свои нюансы. Например, всеми любимый маппер MMC3 может оперировать двумя CHR банками по 8Кб. Итак, текст, стараниями маппера, оказывается в одном из банков RAM’a. Теперь, предположим, подошёл момент вывода этоготекста на экран. Как нам скопировать первую букву (точнее, её индекс) в область тайловых карт PPU? (что там происходит, нас не интересует - текст выведется на экран). Загрузить ячейку, содержащую эту букву. Однако адрес этой ячейки представляет собой 16 битное число (word/слово): как уже упоминалось выше, адреса могут быть 0х8000 - 0xFFFF. Которые представляются в виде двух байт: например 80 00 или FF FF. Как процессору узнать, какой адрес нужно загрузить? Для этого в системе команд процессора имеются команды типа LDA(##),Y LDA(##),X. Вместо решёток можно подставить любое восьмибитное число. Эта команда берёт значение этого байта, потом читает значение следующего сразу за ним байта, скажем##=0x0D, в котором содержится число 0x67, а в ячейке 0х0E содержится число 0x9A, в итоге имеем байты 67 и 9A, соединяя и переворачивая их, получим 0х9A67, и, в конце концов, загружаем в аккумулятор ячейку по данному адресу! (разумеется, если регистры X или Y равны нулю) Вот и выход: используем такую команду, чтобы загрузить первую букву строки: в данном случае она будет располагаться в ячейке RAM’a по адресу 0x9A67. Правда, теперь нам нужно иметь в адресах 0D и 0E два байта: $67 и $9А - непосредственно адрес начала строки в RAM’e.
Так вот, эти два байта и является указателем! А указывает он ни на что иное, как на расположение первого символа строки в RAMe, чтобы процессор знал откуда ему загружать значение.
Мифы о значениях поинтеров.
Мифы о системах вычислений поинтеров.
Судя по отечественным и зарубежным документам, различают от трёх до пяти систем вычислений поинтеров. По моему мнению, система-то вообще одна: SetOff X000 - всё остальное либо разновидности её, либо вообще от лукавого =), т.е. не являются системами вообще - просто особый случай нестандартного расположения поинтеров, коих можно найти гораздо больше, чем это описано в документах. Ещё раз повторюсь, что все системы довольно эмпирические, поэтому не несут никакой познавательной или полезной нагрузки. Однако, интересно узнать, откуда все эти системы пошли. Итак, перечислим все системы:
SetOff X000 (также будет рассмотрено)
STANDARD HEADER (SH)Как сто раз было сказано другими авторами, $10 - размер хедера ROM’a, который нужен только эмулятору - в оригинальном картридже такого безобразия нет.
Округление до тысячныхтакже было несколько раз объяснено: поинтеры двухбайтовые - максимум они могут показать на адрес $FFFF, значит, если у нас текст расположен по адресам в роме 0х2bf23 или 0х45bf23, поинтеры на него будут одинаковыми, другое дело, что мапперу будет дано указание загрузить в определённый участок RAM’a другой банк. Переворачивание было объяснено выше.
Итак, откуда же произошла система SH, иначе говоря в каких случаях она сработает? Что ж, такое будет возможно при большом количестве совпадений: скажем, если банк с текстом в ROMe 3-й или 4-й, а в RAM он загружен соответственно в 0х8000 или 0хС000, т.е. в первую или вторую область. Возможны также тонкости, связанные с мапперами и так далее. Так что твёрдо сказать при каких именно условиях работает система SH невозможно.
Примечательно, что такая система просто физически не сможет работать для ROM’ов, сидящих на нулевом маппере, или которые имеют только два PRG банка (а таких случаев очень много), так как физический адрес в ROM’е текста всегда будет меньше, чем его же адрес в RAM’e.
SetOff X000 (SO)
Порядок действий:
1)Взять адрес строки и отнять 10 2)Прибавить к тысячным значение Х000 3)Убрать все цифры до тысячных 4)Перевернуть два байта
Откуда пошла данная система?
Сразу стоит отметить, что банки могут загружаться игрой в разные участки RAM’а как ей самой заблагорассудится. Иногда по нескольку десятков (сотен) раз в секунду (скажем, загружается банк с поинтерами и текстом, потом копируется отсюда в основной RAM какая-то строка, и в ту же долю секунды на месте банка с текстом появляется другой банк (текст уже не нужен)) Исходя из всего этого, банк с текстом может оказаться где угодно в RAM’е, причём на протяжении игры возможно его будут загружать в разные участки рама. Скажем, если наш ROM разбит на банки по 8 Кб, то банк с текстом может начинаться с равной вероятностью и в 8000, и в A000, и в C000, и в E000. Что в этом случае будет с поинтерами? Их значение будет меняться! Кроме того физически в ROM’е текст может находиться в абсолютно других адресах совершенно не привязанных к RAM.
Т.е. если раньше, в простейшем случае, рассмотреном выше у нас был поинтер, скажем, DF91, то, если банк загружен в RAM вторым по порядку (ROM разбит на банки по 8кб), поинтер должен быть DFB1, третьим - DFD1, четвёртым - DFF1. Понятно, что в таком общем случае простой метод standart header здесь уже не сработает. Поэтому можно воспользоваться простым перебором всех возможных значений. К нашему счастью, вариантов не так много: минимум XX8X, максимум - XXFX. Всего восемь вариантов. Один из них обязан оказаться правильным. Кстати, этот же метод(немного под другим соусом) применяет и CaH4e3 в своей доке (подробней см ниже), когда ставит остановы на 8 разных адресов, когда ищет поинтеры.
Итак, перед нами система SetOff X000. Тот, кто с ней знаком может подметить, что именно в указанные выше числа мы и подставляем сначала единичку, потом двойку, тройку и так далее.
Минутку, но ведь я говорил, что вариантов всего 8, а не 16!Вот именно! Поэтому можно сразу отбросить 8 ненужных (“неправильных”) вариантов, иначе может получиться казус как в доке Ken’a на сайте Шедевра: Для примера перебирались числа и…
Цитата: “И вот он, наш поинтер: 0012”
Такой поинтер можно даже не искать в роме и не мучиться. Впрочем, Ken честно всех предупредил, что эта неаккуратность допущена в учебных целях. Также не может быть поинтеров 0002,0022,0032,0032,0042,0052,0062,0072. Если вы не можете понять почему, прочитайте пункт 2.2 доки. Кстати, та же ситуация и в доке Timothy R. Dennie “THE MADHACKER’S GUIDE TO NES POINTERS” 98 года. Он “нашёл” поинтер 3553. Таким образом, сократилось число попыток ровно на половину.
Лично меня долгое время интересовало, почему при вычислении поинтеров по системе SetOff X000 мы прибавляем всегда по единичке? Ведь если учесть, что CHR-ROM поделён на банки минимум 8 Кб , то получается, что вполне можно было бы прибавлять $2000, т.к. все банки (16/8 Кб) имеют наибольшее общее кратное 0x2000. А система SetOff X000 применяется только для того, чтобы сделать коррекцию на то, что банк с текстом может оказаться в разных банковых областях RAM’a. Всё это делается, опять же, в целях обучения. Просто x000 удобнее считать в уме. А фактически надо брать 13 бит нашего поинтера и прибавлять по 1 к оставшимся трём битам. Например, поинтер 0х8245 в двоичной системе 100|0001001000101b. Я выделил 3 старших бита. крайний - всегда единичка, как уже много раз отмечалось ранее. Таким образом, у нас остаётся комбинация из двух оставшихся бит, которая может быть:
00: в это время поинтер равен 0х8245
01: поинтер равен 0хA245
10: поинтер равен 0хC245
11: поинтер равен 0хE245
Ну, то есть, как я и говорил, можно прибавлять по 0х2000. Всего возможно, как мы видим четыре варианта - четыре возможных значения указателя. ВСЕ остальные кроме этих четырёх являются заведомо ложными и ни при каких обстоятельствах не смогут быть нашими указателями. Оптимизация на лицо: вместо шестнадцати чисел можно подставлять только четыре.
Если рассмотреть доку CaH4e3a, то можно увидеть, что именно он скрыл от нас с целью не перегрузить нашу нервную систему: он ставит остановы на 8 возможных адресов, при этом предварительно округлив до сотен данные указателя (это важно, я потом объясню). Хотя, как мы уже убедились, остановов достаточно четырёх (как крайний случай банков по 8 Кб). У Cah4e3a текст был по адресу 0х714B0, и 8 остановов: 0х84a0 0х94a0 0хa4a0 0хb4a0 0хc4a0 0хd4a0 0хe4a0 0хf4a0. В крайнем случае, предположим, что банк с текстом загружен первым, кстати банк начнётся на 0х70000 в роме (вы же не забыли, что отнимать Х0000 можно совершенно безнаказанно - см. 3.1). Тогда наш текст в предположительно первом банке будет по адресу 94a0 (хедер-то мы отняли). Отсюда и будем прибавлять по 0х2000: 94a0 b4a0 d4a0 f4a0. Все остальные указатели обязательно окажутся ложными. Впрочем, как и три из той четвёрки, что я оставил. Неудивительно, что настоящий поинтер будет B4A0, что подтверждает возможность выкинуть те четыре лишних останова.
Теперь о том почему он округлял до сотни, а в нашем методе обязательно округлять до тысяч: тут нужно учитывать ситуацию,когда текст расположен во второй половине банка, т.е. за тысячу (первая циферка в значении указателя нечётная: как в нашем случае 14A0). Тогда, если мы просто округлим до сотых, мы потеряем расположение текста в банке (эту тысячу), т.е. получим 4A0, и начальный адрес будет 84A0, соответственно поинтеры будут не те (мы как раз проскочим нужные нам): 84A0 A4A0 C4A0 E4A0. CaH4e3’y этого делать не надо, потому что он учел всевозможное расположение текста именно тем, что брал 8 остановов. Поэтому надёжнее округлять до тысячных.
Ну, это так - сторонний ход, вернёмся к нашим системам: