logo

Архивы в играх PSX

Основное отличие PSX от приставок предыдущего поколения, разумеется, является наличие CDROM, которое, в общих чертах, имеет преимущество по сравнению с другими носителями при чтении длинных потоков данных на постоянной скорости. И проигрывает с технической точки зрения во всём остальном (например, в скорости поиска, разгона/остановки).
Изначально компакт-диск PSX имеет одновременно треки цифрового аудио и данных. Он имеет файловую систему ISO 9660 для треков данных, описание которой нас мало интересует, кроме нескольких важных фактов, влияющие в дальнейшем на методики чтения секторов данных.

Таким образом, и всего сказанного следует, что процессы, связанных с поиском/чтением на диске являются наиболее критичными с точки зрения скорости загрузки, а, кроме того, не стоит забывать, что перемещение головки – это не только лишнее время, но и лишний шум/износ механических частей CDROM.

Эти процессы – первые кандидаты на оптимизацию, для которой есть два пути:

  1. Снижать количество поисков на диске. С этой точки зрения, желательно не вызывать функцию CdSearchFile() вообще. Она возвращает позицию файла (минуты, секунды, сектора) и общую длину указанного файла на диске. Естественное желание, узнать эти параметры для каждого файла заранее до сборки образа, сохранить их где-нибудь, а потом непосредственно передавать их в CdRead(), качественно повысив скорость загрузки в RAM с диска.
    Узнают эту информацию самыми разными способами (во времена коммерческой разработки игр под PSX, из утилиты разработчика CDGen или Buildcd, в наше время iml карту образа легко сделать CDVDGEN).
    А хранят в двух местах:
    • HardCoded метод: в теле исполняемого файла (сам файл располагают последним в дорожках данных, чтобы сектор его начала уже не изменялся, подготавливают заголовочный файл с нужными данными и компилируют с этим файлом, а затем создают образ).
    • Отдельный файл со структурой диска, который находится первым вызовом CdSearchFile(), а дальше для чтения используется прочитанная карта диска.
      В любом случае, необходимо стремиться, чтобы часто используемые файлы физически хранились рядом с целью, чтобы поиск осуществлялся поворотом линзы, а не линейным перемещением головки, или, если файлы большие, как можно меньшим линейным перемещением головки. Это справедливо и для случая, если мы не вызываем функцию CdSearchFile(), ведь для CdRead() тоже необходимо позиционировать головку над нужной дорожкой
  2. Снижать объём чтения с диска и количество читаемых файлов.

Наконец, мы подбираемся к двум основным особенностям, присущим дискам PSX в частности, дамми файлы и архивы.

  1. Дамми файлы
    иногда маленькие, иногда гигантские файлы, заполненные нулями, названные dummy.dat или маловразумительными именами, которые кладут в начало или в конец диска.
    • В начале, чтобы отодвинуть файлы к краю диска, потому что, указанные в официальной документации, ±128 дорожек на внешнем и внутреннем диаметрах диска содержат разное количество секторов (если все помнят, длина окружности равна Pi*d). Поэтому при равной плотности файлов, головка будет меньше перемещаться в радиальном направлении, если все файлы будут расположены как можно ближе к краю диска. Кроме того, чтение данных с диска происходит при одинаковой линейной скорости, поэтому частота оборотов шпинделя изменяется в зависимости от положения головки: 500 об/мин (8.3 об/c) при чтении с внутренних областей и 200 об/мин (3.3 об/c) при чтении с наружных дорожек. А значит, допускаемое количество секторов, на котором могут находиться файлы, чтобы не перемещать головку линейно на внутреннем диаметре равно 128*75/8.3 = ±1156 секторов, а на наружном: 128*75/3.3 = ±2909 секторов. Таким образом, с целью снижения времени чтения с диска, файлы лучше располагать на внешней стороне диска.
    • В конце диска располагают дамми файл на 3 минуты, чтобы избежать проблем в результате промаха головки в процессе поиска файла:
      Overshoot
      Чтение из пустого места неуправляемо и его стараются всеми способами избежать.
      В подавляющем большинстве случаев, дамми файлы встречаются именно в конце диска (например, в Ghost in the shell файл назван ‘3MINDUMY’, а в Tales of destiny II – ‘3.DA’)
  2. Архивы
    Cобственно, то, ради чего и затевался документ.
    Как правило, когда заходишь в образ доброй половины игр обнаруживаешь не привычное множество папок и файлов, а один исполняемый файл и один гигантский файл, который и занимает весь диск.
    Это архив игры. Формат его может быть самым разнообразным в зависимости от фантазии разработчиков. Чаще всего, он совмещает в себе описание файловой системы (смещение (относительно начала архива или в абсолютном отношении), размер и, часто, имя файла.) и непосредственно массив данных файлов, следующих сектор за сектором.
    Смысл архива в том, что, во-первых, он содержит в себе ту самую пресловутую карту файлов, необходимую для оптимизации процесса работы с диском.
    А, во-вторых, в заголовке архива можно указать смещение от начала архива, которое потом можно перевести в номер сектора от начала архива, найти одним вызовом CdSearchFile() сектор архива и, сложив, высчитать сектор начала файла на диске. Это даёт возможность избежать высчитывания каждый раз при пересборке диска LBA каждого файла (что само по себе проблематичнее, чем посчитать его позицию в архиве) и вписывать их в EXE или в заголовок архива, так как при любом изменении файла удобно просто пересобрать архив без пересборки и корректировки самого образа.

Итак, на сегодня теории достаточно. Пора пощупать какой-нибудь простенький архив отладчиком. Я взял архив в игре Spider Man, который состоит из двух файлов CD.HED (расширение намекает на то, что это файл заголовка) и CD.WAD – сам архив. Формат архива настолько прост, что можно сразу открыть CD.HED в хексредакторе и раскопать его сходу, но наша задача найти код и понять, как архив обрабатывается. План действий, как всегда, прост до безобразия – отыскать код, который читает данные из файла CD.HED, понять, как эти данные используются, рассмотрев код в отладчике, либо в дизассемблере. Искать место, где читаются данные, мы будем из отладчика. Для PSX их не так уж и много, я пользовался PCSX 1.5 от zHAOsILi, модифицированным Zidane и Horror’ом. У отладчика есть возможность установки точек останова на чтение определенного сектора. Открыв образ игры, например в UltraISO легко отыскать LBA файла. Для CD.HED это 25 или 0х19. Запускаем игру в отладчике, сразу после начала жмём F11 и в окне ‘CD-ROM Read on sector’ вбиваем 19. Жмём Set и Run и вываливаемся на команде по адресу 0x8008d8d8 в памяти.
CD break
В код вокруг этой команды можно особо не вглядываться. Это библиотечная функция CD_getsector, вызываемая CdRead(). Главное, что мы получили здесь, это адрес RAM, куда копируется содержимое считанного сектора. Снимаем галки с останова при чтении сектора и ставим останов на чтение из RAM по адресу 0х800B9E68. Опять жмём Set и Run и на сей раз вываливаемся прямо в функции чтения содержимого заголовочного файла:

80064BA8 lbu $v1, 0($s0)
Теперь можно исследовать код прямо в окне отладчика, пробегая по нему, а можно проанализировать в дизассемблере. Я воспользуюсь вторым вариантом и загружу исполняемый файл ‘SLUS_008.75’ в IDA. Этот дизассемблер сам разберет файл, проанализирует и даже переименует библиотечные функции при наличии PsyQ FLIRT сигнатур (с версии 3.85 IDA они входят в стандартную поставку). Перейдём по нашему адресу 0x80064BA8 и натолкнемся на вполне заурядную процедуру анализа содержимого заголовочного файла:
<script src=“http://zoom.it/I0FR.js?width=auto&height=400px”>

В общих чертах, эта процедура изначально подсказала мне, что в структуре заголовочного файла есть некая нуль-терминированная строка, длина которой выравнена на четыре, и есть два 32-хбитных значения, которые читаются, если строка в заголовочном файле и, находящаяся в памяти равны. Очевидно эта строка – имя файла, а два значения смещение и размер файла в архиве CD.WAD, которые обрабатываются уже вне этой процедуры. Всё. Я же сказал, что архив будет простеньким. Чтобы полноценно работать с архивом, можно написать программку, распаковывающую все файлы из архива и собирающие его обратно из списка файлов. Небольшой нюанс заключается в необходимости соблюдения исходного порядка файлов, так как, очевидно, разработчики этой игры помнили о необходимости хранения часто используемых файлов как можно ближе друг к другу.
Или можно воспользоваться возможностями Total Commander и собственноручно написать архиваторный плагин под такой тип архива, что добавит удобства и простоты в обращении с архивом. Я пойду более элегантным вторым путём, так как, если вы поняли всё, что я написал до этого, о том, как накрапать простенькую утилиту в 100 строк вам рассказывать не нужно.
Итак, архиваторный плагин под Total Commander имеет расширение .wcx и является ничем иным, как dll с несколькими функциями, вызываемыми самим TCmd. Близким нашему случаю примером такого плагина служит ISO.wcx, который позволяет работать с файлами .iso образов как с обычной директорией TCmd. Плагины пишутся на Delphi или C++, я буду проще и возьму в руки Delphi.
Вообще говоря, в сети существует несколько документов для написания архиваторных плагинов, в том числе и довольно подробные от Motorocker. Основной официальный документ – ‘WCX Writer’s Reference’. В рамках этой статьи я собираюсь кратко описать, что я делаю в каждой из функций, реализованных плагином, потому что конкретную реализацию любой желающий может увидеть в исходнике. Файл wcxhead.pas – заголовочный файл, поставляемый с WCX Writer’s Reference, содержащий коды ошибок и другие числовые константы, которые можно использовать в коде. wad.dpr – код самого плагина. Как и в любой dll исходный код в конце имеет секцию

exports
OpenArchive,
ReadHeader,
ProcessFile,
SetChangeVolProc,
SetProcessDataProc,
GetPackerCaps,
CloseArchive,
PackFiles;

Плагин умеет открывать архив, распаковывать любой переданный в TCmd файлы, то есть работать с файлами прямо в архиве и умеет собирать файлы, переданные в TCmd в архив. Можно дополнительно добавить возможность добавлять и удалять файлы из архива, но это требует введения какого-нибудь связанного списка, чего мне, для плагина, которым я воспользуюсь пару раз, было лень.

OpenArchive(Var ArchiveData: TOpenArchiveData): HANDLE; stdcall; 

Открывает архив и должна, как минимум, вернуть уникальный хэндл открытого архива. Я дополнительно проверяю здесь все ли файлы архива в наличии и если всё успешно, создаю файловые потоки файла заголовка и файла данных, которые в дальнейшем освобождаются в функции CloseArchive(hArcData: HANDLE): Integer

ReadHeader(hArcData: HANDLE; Var HeaderData: THeaderData): Integer; stdcall;

Заполняет массив записей HeaderData и возвращает ноль в случае успеха. В моем случае заполняется имя файла и его размер. Дополнительно ставятся даты создания файлов с тем, чтобы каждый следующий файл имел большую дату создания. Это необходимо для сохранения порядка файлов при последующей пересборке архива. Достаточно лишь отсортировать файлы по дате создания. В архиве игры дат файлов в любом случае нет.

Function ProcessFile(hArcData: HANDLE; Operation: Integer;
DestPath: PChar; DestName: PChar): Integer; stdcall;

Копируем файл из архива по следующему смещению в файл по переданному имени. Здесь же обрабатываем нажатие кнопки «Отмена» в процессе распаковки.

Function PackFiles(PackedFile: PChar; SubPath: PChar; SrcPath: PChar;
AddList: PChar; Flags: Integer): Integer; stdcall;

Упаковываем переданный список файлов в один архив. Никаких хитростей, разве что нужно обратить внимание, что каждый новый файл начинается с нового сектора, поэтому предусмотрено выравнивание адреса начала каждого файла на 0х800 и дополнительно предусмотрена передача имени упаковываемого в данный момент файла в TCmd для отображения в окошке Progress Bar’а.
Отладка такая же, как и любого dll - прописываем в Delphi в качестве host application исполняемый файл Total Commander, закрываем запущенный TCmd, если вы им пользуетесь и запускаем его из-под Delphi. Ставим точки останова для отладки и производим действия с архивом, которые должны вызвать исполнение нужного нам кода плагина.
Похоже, что всё. Для автоматической установки плагина в TCmd добавляем в архив с откомпилированным .wcx файлом ini файл pluginst и спокойно работаем с архивом.