Увеличение памяти под код в EvoSDK
by Hippiman
В своей предыдущей статье (Info Guide #11) я упоминал о том, что
под код и переменные в EvoSDK доступно примерно 32K, а также о
том, что этого объёма вполне хватает для небольших программ, но
для более крупных приходится прибегать к различным ухищрениям
(например, выносить часть данных в расширенную память). Но этого
всё равно недостаточно. Код на C компилируется довольно
размашисто, а это значит, что при написании большой игры в
любом случае придётся как-то ужиматься.
Я разработал способ, как можно расширить область памяти кода до
очень больших объёмов. Кратко суть метода:
1. Часть кода, который не вмещается в основную память мы
компилируем отдельно;
2. Преобразуем в бинарный файл;
3. Забрасываем на дискету;
4. В нужный момент загружаем этот файл в расширенную память;
5. Оттуда монтируем во 2-й слот (0x8000-Oxbfff);
6. Вызываем этот код из основной программы.
По сути этот подход очень похож на динамически подключаемые
библиотеки (.dll), но с кое-какими ограничениями.
"Плюс" этого метода один, но очень большой: объём кода теперь не
ограничен, можно писать очень много.
"Минусов" несколько: сложность в использовании и подготовке,
небольшое замедление работы всей программы, некоторые
ограничения загружаемых модулей.
Подготовка подключаемого модуля
Первым делом нужно исправить /evosdk/_compile.bat. В этом файле
нужно убрать или закомментировать строку rd /s /q %temp%, чтобы
после компиляции проекта не удалялась директория с временными
файлами, которые нам очень нужны.
Следующим этапом будет извлечение адресов функций из файла
out.map, который находится в директории _temp_. Для этого я
использую вот такой маленький Perl скрипт:
#!/usr/bin/perl
use strict;
my $str;
my @arr;
open FIL,"_temp_\out.map";
open OUT,">map.h";
while($str=<FIL>)
{
chomp $str;
$str=~s/s*//;
if(length($str)>0)
{
@arr=split(/ /,$str);
if(substr($arr[2],0,1) eq "_")
{
print "#define " .$arr[2]." 0x". $arr[0]."n";
print OUT "#define " .$arr[2]." 0x". $arr[0]."n";
}
}
}
На выходе этого скрипта получаем файл map.h, который нужно будет
подключить к будущей библиотеке.
В конечном итоге в компилируемой библиотеке должны быть
подключены следующие хедеры:
#include "..evosdkevo.h"
#include "..evosdkstartup.h"
#include "..Родительский npoektresources.h"
+ все необходимые дополнительные хедеры из родительского проекта.
Теперь нам нужно научить дочерний проект понимать функции из
родительского.
Для этого пишем подобные конструкции для каждой функции, которую
будем использовать.
#define draw_image ((void(*)(u8,u8,u8))_draw_image)
#define select_image ((void(*)(u8))_select_image)
#define draw_tile_key ((void(*)(u8,u8,u16))_draw_tile_key)
#define put_mem ((void(*)(u8,u16,u8))_put_mem)
#define get_mem ((u8(*)(u8,u16))_get_mem)
и т. д.
После чего объявленные функции можно будет использовать в
проекте так же, как и обычные.
Далее пишем основной код подключаемого модуля. Однако чтобы не
высчитывать после каждого изменения адрес входа в функцию main,
все остальные функции лучше писать после неё (естественно,
заранее объявив).
Подготовка основной программы
Логика работы основной программы с модулем такова: файл или
файлы с дискеты загружаются в память, а потом в нужный момент
подключаются во 2-е окно и вызываются по адресу 0x800A.
В качестве примера подключения и вызова можно взять вот эту
функцию:
void manager(u8 page,u8 operation,void *a,u8 b) __naked
{
u8 c;
put_mem(trigger_text_page,exchangedate_begin,operation);
put_memw(trigger_text_page,exchangedate_begin+1,(u16)a);
put_mem(trigger_text_page,exchangedate_begin+3,b);
//------------------------------------
__asm
push ix
ld ix,#0
add ix,sp
ld a,4 (ix)
PUSH AF
xor #0x7f
LD BC, #Oxbff7
ld (_MEMSLOT2),a
OUT (C), A
POP AF
call #0x800a
pop ix
ret
__endasm;
}
page - номер страницы, в которой лежит бинарник,
operation - идентификатор операции (менеджер с другой стороны
распределяет входные параметры и вызывает нужную функцию в
зависимости от этого идентификатора),
A - бестиповый указатель - параметр,
B - ещё один параметр.
Обратите внимание на строки типа
put_mem(trigger_text_page,exchangedate_begin,operation);
Подключенная библиотека при вызове начинает вести себя как
самостоятельная программа и творит какие-то непотребства со
стеком, в результате чего вместо переданных данных функция
получает что-то совсем другое. Однако это не влияет на
последующую работоспособность всего проекта. Сильно копаться в
этих процессах не стал. Если кому-то удастся разобраться, как
красиво передавать параметры в дочернюю функцию, буду только
рад.
В конечном итоге я выбрал обходной путь. Если параметры передать
нельзя, но очень нужно, то можно воспользоваться внешним
буфером. Роль которого выполняет расширенная память.
Подготовка EVO SDK
Проблема в том, что если вы соберёте и запустите этот проект, то
ничего не заработает, так как большинство функций EvoSDK,
которые используют 2-е окно, не "чистят за собой". Не подключают
туда-обратно страницу, которая была во 2-м окне до их вызова.
Для правки, открываем put_get_mem_atm.h, lib_sprites.asm и
lib_tiles.asm.
Добавляем в начало и конец нужных фунций:
В put_get_mem_atm.h :
в начало :
push ix
ld ix,#0
add ix,sp
в конец:
pop af
LD BC, #Oxbff7
ld (_MEMSLOT2),a
out (c),a
В lib_sprites.asm и lib_tiles.asm :
в начало:
ld a,(_memSlot2)
push af
в конец:
pop af
ld (_memSlot2),a
ld bc,MEM_SLOT2
out (c),a
Это творческий процесс, т. к. не все функции переключают
страницы памяти и не все из них будут использоваться в вашей
программе.
Список функций, которые точно нужно править:
_DOS_3D13, sprites_start, sprites_stop, draw_tile,
draw_tile_key, draw_image, pal_select, swap_screen, все функции
из put_get_mem_atm.h.
Также замечу, что после внесения правок и пересборки библиотек
EvoSDK может перестать работать. Это случится из-за
превышения максимального размера библиотеки. В этом случае можно
закомментировать какие-либо "ненужные" функции, например
_sample_play в lib_sound.asm. Эта функция отвечает за covox,
который я не использую.
Сборка и компиляция
Тут всё тоже не так просто. Для компиляции дочернего проекта
нужно использовать SDCC версии 2.9. Более новые версии содержат
баг, из-за которого не получается явно задавать указатели на
функции, а следовательно библиотека получается "вещью в себе".
Мы не сможем вызывать функции родительского проекта.
Вызывать sdcc нужно с такими параметрами:
sdcc -mz80 --code-loc 0x8000 --data-loc OxBCOO main.c
-o %temp%out.ihx
Код помещаем именно с адреса 0x8000, так как если поместить его с
0x0000, то все указатели на функции будут иметь ссылки начиная с
этого адреса. А это нам не нужно, ведь с 0x0000 адреса у нас
будет код основной программы.
SDCC генерит скомпилированный код в Motorola hex формат. Для
получения бинарника я пользуюсь утилитой hex2bin.exe
(http://sourceforge.net/projects/hex2bin/ ).
Но и это ещё не всё. Код в полученном файле, как мы и указывали
при компиляции, начинается с адреса 0x8000, а остальное место
заполнено мусором.
Для обрезки бинарника я написал небольшую утилиту на java.
Для желающих - вот её код:
package cuter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author VSurjenko
*/
public class Cuter {
public static void main(String[] args)
{
try {
String filename,destfile;
int addr,a;
byte arr[];
byte arr2[];
if(args.length>0)
{
filename=args[0];
}
else
{
System.out.println("Program needs 3 parameters:"
"file name, destination file name and address of cutting.");
return;
}
if(args.length>1)
{
destfile=args[1];
}
else
{
destfile=filename.concat("_cuted");
} if(args.length>2)
{
addr= Integer.parseInt(args[2]);
}
else
{
addr=0x8000;
}
//------------------------------------
Path path = Paths.get(filename);
Path path2 = Paths.get(destfile);
arr=Files.readAllBytes(path);
arr2=new byte[arr.length-addr];
for(a=0;a<arr.length-addr;a++)
{
arr2[a]=arr[a+addr];
}
Files.write(path2, arr2);
} catch (IOException ex) {
Logger.getLogger(Cuter.class.getName()).log(Level.SEVERE, null,
ex);
System.out.println(ex.getLocalizedMessage());
}
catch(Exception ex)
{
System.out.println(ex.getLocalizedMessage());
}
}
}
Остальные могут либо придумать что-то сами, либо обратиться ко
мне, я дам jar файл.
Всё, теперь полученный бинарник готов к использованию в основной
программе.
В итоге полный процесс сборки всего проекта будет выглядеть так:
1. Компилируем основную программу, но не собираем образ;
2. Втягиваем из Out.map указатели на функции;
3. Компилируем дочернюю программу, преобразуем в бинарный файл из
hex формата, обрезаем и забрасываем в директорию к основной
программе;
4. Собираем весь проект в образ дискеты;
5. Тестируем.
Теперь кратко об ограничениях дочернего модуля.
1. Все функции, которые могут менять страницу памяти во 2-м окне,
должны находиться в родительском проекте. В противном случае они
будут портить сами себя. Это значит, что функции работы с
файлами в загружаемый модуль выносить нельзя;
2. Размер одного модуля ограничен 16К - одна страница. Это не
должно быть большой проблемой. Всегда можно сделать несколько
модулей и подключать нужный в данный момент.
3. Я так и не нашел способа "подружить" загружаемый модуль с
глобальными переменными основной программы.
Вам, скорее всего, придётся провести какое-то время с дебагером,
но результат будет того стоить. Конечно, часто вызываемый код
лучше не выносить в отдельный бинарник, но всегда найдутся
объёмные функции, которые большую часть времени не используются.
К примеру, код мануала от SpaceMerc Liberation занимает 8377
байт, я перенёс функцию вывода текста в отдельный бинарник,
и вместе с функцией вызова и разросшийся библиотекой EvoSDK код
стал занимать 6890 байт. Почти 1.5K чистой выгоды
Other articles: