ACNews #59
02 ноября 2005

Увеличение памяти под код в EvoSDK - как расширить область памяти кода до очень больших объёмов.

               Увеличение памяти под код в EvoSDK
                         by Hippiman

В своей предыдущей статье (Info Guide #11) я упоминал о том, что
под код и переменные в EvoSDK доступно примерно 32K, а также о
том, что этого объёма вполне хватает для небольших программ, но
для более крупных приходится прибегать к различным ухищрениям
(например, выносить часть данных в расширенную память). Но этого
всё равно недостаточно. Код на C компилируется довольно
размашисто, а это значит, что при написании большой игры в
любом случае придётся как-то ужиматься.

Я разработал способ, как можно расширить область памяти кода до
очень больших объёмов. Кратко суть метода:

1. Часть кода, который не вмещается в основную память мы
компилируем отдельно;
2. Преобразуем в бинарный файл;
3. Забрасываем на дискету;
4. В нужный момент загружаем этот файл в расширенную память;
5. Оттуда монтируем во 2-й слот (0x8000-0xbfff);
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 "..Родительский проектresources.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, #0xbff7
    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, #0xbff7
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 0xBC00  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 чистой выгоды




Темы: Игры, Программное обеспечение, Пресса, Аппаратное обеспечение, Сеть, Демосцена, Люди, Программирование

Похожие статьи:
Пишите письма - На деревню дедушке...
Новости от DITRONIK - Раздел не пущен в номер.
Мнения - жив ли Спектрум в твоём городе?

В этот день...   17 ноября