Què és la paginació de la memòria

El Z80 només pot gestionar 64K simultàniament. Per superar-ho, l'arquitectura MSX utilitza Slots, dividint l'espai en quatre pàgines de 16K. Tot i que això ja existia al MSX1, va ser amb el MSX2 i l'aparició del Memory Mapper que es va poder gestionar fins a 4MB de RAM. Aquest mapper es controla mitjançant els ports d'entrada/sortida 0xFC-0xFF. Com que cada fabricant ubicava la RAM en slots diferents (per exemple, el Philips VG-8235 la té al slot 3-2, mentre que un Sony HB-F1XDJ la té al 3-0), i si a part tenia algun mapejador de memòria (memory mapper) addicional per tenir més RAM, la gestió era complexa. Per això, el MSX-DOS2 va introduir un gestor de memòria estàndard que permetia als programadors utilitzar la RAM sense preocupar-se de l'arquitectura física del model.

En aquest article investigarem aquesta gestió i com poder-la utilitzar en els nostres programes. A l'article Paginació de la RAM ja vam tractar aquest tema i vam explicar les funcions del DOS2 en el Fusion-C, aquí veurem les equivalents en MSXgl.

A més a més, com que hem aprofundit en el coneixement de l'entorn sdcc mirarem com podem enllaçar d'una forma menys costosa els diferents mòduls. A l'article hi havia una gestió de desenvolupament com a un mòdul a part i després integració. Aquesta estratègia també serveix per al MSXgl, però aquí presentarem una nova (igualment complexe però més eficient) que també es pot aplicar al Fusion-C.

Quines funcions usarem del MSXgl?

bool DOS_Mapper_Init()
La funció per inicialitzar el control del mapejador de memòria.
u8 DOSMapper_GetPage(u8 page)
Retorna el número de segment de la pàgina page.
u8 DOSMapper_GetPage0()
Retorna directament el número de segment de la pàgina 0. Així com en l'anterior podíem escollir una de les 4 pàgines, aquí ja va directament al 0, fent que l'execució sigui una mica més ràpida. També tenim l'equivalent per les altres 3 pàgines: DOSMapper_GetPage1, DOSMapper_GetPage2 i DOSMapper_GetPage3.
bool DOSMapper_Alloc(u8 type, u8 slot, DOS_Segment* seg)
Per sol·licitar al DOS2 que ubiqui 16K de memòria que podem utilitzar i modifiqui la informació de l'estructura de segment. El type pot ser de dos tipus:DOS_ALLOC_USER a on la memòria reservada queda alliberada quan el nostre programa acaba, i DOS_ALLOC_SYS que un cop acaba el programa segueix reservada. Aquesta última és útil per fer RAM disks o per la memòria d'algun driver. slot la pàgina/slot que volem que s'ubiqui: 0,1,2,3. En el cas de DOS2, recomano utilitza les pàgines 1 i 2, ja que a la part baixa de la 0 i a la part alta de la 3 hi ha variables del sistema i s'ha d'anar en compte si les manipulem de no malmetre el sistema i que es quedi penjat.

Exemple de paginació

Hem creat un petit exemple per treballar la paginació en DOS2. La primera part canvia els segments a la pàgina/slot 1 (adreça 0x4000) i escriu diferents valors en el mapa de memòria. La segona part crida a funcions creades en altres fitxers i que han estat carregades a la pàgina 1. El codi del programa principal es troba al fitxer paginate.c i les dues funcions que són cridades als fitxers funcio1.c i funcio2.c

També utilitzarem la llibreria Debug que ens permet escriure missatges a la consola del PC a través de l'openMSX activant el dispositiu -ext debugdevice i que també ens servirà per veure el que està fent l'MSX.

  1#include "dos_mapper.h"
  2#include "dos.h"
  3#include "print.h"
  4#include "input.h"
  5#include "vdp.h"
  6#include "bios.h"
  7#include "debug.h"
  8
  9#include "font/font_mgl_std3.h"
 10
 11#include "funcions.h"
 12
 13__at(0x4000) u8 array[0x4000];
 14__at(0x8000) u8 res_func1;
 15__at(0x8001) u8 res_func2;
 16
 17u8 startingSeg;
 18u8 newSeg;
 19u8 newSeg2;
 20DOS_Segment g_newSeg;
 21DOS_Segment g_newSeg2;
 22DOS_Segment g_func1;
 23DOS_Segment g_func2;
 24u8 g_StrBuffer[128];
 25DOS_FCB g_File;
                

Comencem posant els includes de les llibreries que utilitzarem:

  1. dos_mapper.hconté les funcions del mapeig que hem explicat més amunt.
  2. dos.h ens permetrà carregar les funcions externes a memòria, controlar la disquetera.
  3. print.hper poder escriure missatges a la pantalla de l'MSX.
  4. input.h per controlar les tecles de l'MSX.
  5. vdp.hpodrem gestionar els difrents modes de pantalla de l'MSX.
  6. bios.hconté diferents crides que utilitza el DOS i que també ens permet tornar quan acaba el programa al DOS2.
  7. debug.hhi trobem les diferents funcions per escriure a la consola del PC els missatges que l'MSX va enviant.
  8. font/font_mgl_std3.hla llibreria MSXgl inclou diferents tipologies de fonts (tipus de lletra) que poden ser carregades. En aquest cas utilitzarem la mgl_std3. Si voleu canviar la tipografia, proveu variant aquest valor per trobar una que us agradi més.
  9. funcions.hla definició de les funcions externes que es troben als fitxers funcio1.c i funcio2.c.

I passem ara a les variables, primer de tot array que és de 16K i ens reserva la memòria per tal que el linker no hi posi cap altra funció o variable a la pàgina 1 que anirem carregant amb els diferents blocs de codi de les funcions externes. Encara que hem de tenir en compte, tal i com l'Aoineko ha senyalat, que la documentació del SDCC diu que si posem contingut manual utilitzant les directives __at, no hi ha cap grantia que aquest espai no és sobreescrit per contingut acumulat del compilador. Per tant, si el nostre programa peta, una de les causes pot ser aquesta sobreescriptura. Les dues variables que guardaran el retorn de les funcions externes que cridem res_func1 i res_func2. Després guardem el número de pàgina inicial a newSeg i el retorn d'assignar les dues noves pàgines. Després tenim les estructures de DOS_Segment que guardarà la informació dels segments assignats. Finalment les variables per llegir del disquet, el buffer de lectura g_StrBuffer i la d'estructura del fitxer DOS g_File.

 27void FT_SetName(DOS_FCB *p_fcb, const char *p_name) {
 28  char i, j;
 29  Mem_Set(0, p_fcb, sizeof(DOS_FCB));
 30  for (i = 0; i < 11; i++) {
 31    p_fcb->Name[i] = ' ';
 32  }
 33  for (i = 0; (i < 8) && (p_name[i] != 0) && (p_name[i] != '.'); i++) {
 34    p_fcb->Name[i] = p_name[i];
 35  }
 36  if (p_name[i] == '.') {
 37    i++;
 38    for (j = 0; (j < 3) && (p_name[i + j] != 0) && (p_name[i + j] != '.');
 39         j++) {
 40      p_fcb->Name[8 + j] = p_name[i + j];
 41    }
 42  }
 43}
            

Aquesta és la funció que hem utilitzat moltes vegades i que s'encarrega de posar el nom a l'estructura de lectura de fitxers DOS_FCB per tal de llegir els bytes de l'anomenat fitxer

 45void main(){
 46  VDP_SetMode(VDP_MODE_TEXT2);
 47
 48  VDP_SetColor(0xF0);
 49	VDP_FillVRAM_16K(0, 0x0000, 0x4000); // Clear VRAM
 50	
 51	Print_SetTextFont(g_Font_MGL_Std3, 1);
 52	Print_SetColor(0xF, 0x0);
 53
 54  DOSMapper_Init();
 55  DEBUG_INIT();

            

Les diferents inicialitzacions de la pantalla escollint el mode de pantalla i borrant la VRAM. Tembé inicialitzem la tipografia de font que hem escollit així com el mapejador de memòria i el debug.

 57	Print_SetPosition(0, 0);
 58	Print_DrawText("We write value 1 to the starting segment");
 59  for (u16 j=0; j<0x3fff; j++) {
 60    array[j] = 1;
 61  }
 62  Print_DrawText("\nValue written.\n");
 63
 64	Print_DrawCharX('-', g_PrintData.ScreenWidth);
 65  Print_DrawText("\nNow we add a new segment and we put it on page 1");
 66  startingSeg = DOSMapper_GetPage(1);
 67  DEBUG_LOGNUM("startingSeg number, value returned by alloc: ", startingSeg);
 68
 69  newSeg = DOSMapper_Alloc(DOS_ALLOC_USER, DOS_SEGSLOT_PRIM, &g_newSeg);
 70  DEBUG_LOGNUM("newSeg number, value returned by alloc: ", newSeg);
 71  DEBUG_LOGNUM("g_newSeg number", g_newSeg.Number);
 72
 73  DOSMapper_SetPage(1, g_newSeg.Number);
 74  Print_DrawText("\nSegment changed. Now we write value 2");
 75  for (u16 j=0; j<0x3FFf; j++) {
 76    array[j] = 2;
 77  }
 78  Print_DrawText("\nDone.\n");
 79  DEBUG_LOG("Written value 2 to segment g_newSeg 2");
 80
 81	Print_DrawCharX('-', g_PrintData.ScreenWidth);
 82  Print_DrawText("\nNow we add a new segment and we put it again on page 1");
 83  newSeg2 = DOSMapper_Alloc(DOS_ALLOC_USER, DOS_SEGSLOT_PRIM, &g_newSeg2);
 84  DEBUG_LOGNUM("newSeg2 value returned by alloc", newSeg2);
 85  newSeg = DOSMapper_GetPage1(); 
 86  DEBUG_LOGNUM("newSeg value returned by getPage", newSeg);
 87  DOSMapper_SetPage(1, g_newSeg2.Number);
 88  DEBUG_LOG("set in page 1 g_newSeg2");
 89  Print_DrawText("\nSegment changed. Now we write value 3");
 90  for (u16 j=0; j<0x3FFf; j++) {
 91    array[j] = 3;
 92  }
 93  Print_DrawText("\nDone");
            

La línia 57 s'encarrega de posicionar el cursor per escriure les frases que anirem fent. Després d'explicar per pantalla el que farem, creem un bucle a on hi posem el valor 1 per totes les posicions del bloc de 16K. A la línia 66 recuperem el número de segment que hi havia a l'inicialitzar l'aplicació. A la 69 reservem 16K de la memòria d'usuari i ho guardem a l'estructura g_newSeg, el valor retornat si ha funcionat bé és el 0xFF que el guardem a newSeg. A la línia 73 posem a la pàgina 1 aquest nou segment i l'omplim del número 2. A les línies 81-93 fem el mateix per al segment g_newSeg2 però ara guardem el valor 3 en aquest nou segment. Hem anat assignant valors als segments utilitzant la variable array que hem dit que es troba a l'adreça 0x4000, que té un tamany de 16K (0x4000 bytes) que correspon a tota la pàgina 1 del Z80. Així el processador s'ha pensat que sempre omplia la pàgina 1 però nosaltres hem canviat aquesta pàgina per diferents segments que corresponen a una adreça diferent.

En aquest punt, si anem al menú Debugger de l'openMSX i escollim l'opció Add hex editor i després slotted memory veurem tota la memòria de l'ordinador i si naveguem amunt i avall, ens serà fàcil veure els blocs amb els números 1, 2 ó 3. En el meu cas aquests començaven a 0x8000, 0x14000 and 0xC4000.

A la imatge de sota veureu un moment de la simulació a on es poden veure els valors de la memòria a la pàgina 1 i a la memòria general:

 96  DOSMapper_SetPage(1, g_newSeg.Number);
 97  Print_DrawText("Changed segment g_newSeg , read value: ");
 98  Print_DrawInt(array[3]);
 99  DEBUG_LOGNUM("Changed segment g_newSeg: ", array[3]);
            

Ara fem les passes enrere, tornant als segments que havíem guardat per llegir els valors d'aquests segments quan tornen a estar a la pàgina 1. Imprimim els resultats tant en pantalla utilitzant Print_DrawInt com a la consola amb DEBUG_LOGNUM.

101  DOSMapper_Alloc(DOS_ALLOC_USER, DOS_SEGSLOT_PRIM, &g_func1);
102  DOSMapper_Alloc(DOS_ALLOC_USER, DOS_SEGSLOT_PRIM, &g_func2);
103
104  DOSMapper_SetPage(1, g_func1.Number);
105
106  Print_DrawText("\n");
107  Print_DrawCharX('-', g_PrintData.ScreenWidth);
108  Print_DrawText("\nReading file funcio1.bin. ");
109  DEBUG_LOG("Reading file funcio1.bin");
110  FT_SetName(&g_File, "funcio1.bin");
111  DOS_OpenFCB(&g_File);
112  for (u16 i=0; i<g_File.Size; i+=128) {
113    DOS_SetTransferAddr(&array[i]);
114    DOS_SequentialReadFCB(&g_File);
115  }
116  res_func1 = 4;
117  DEBUG_LOGNUM("Value before func1: ", res_func1);
118  Print_DrawText("\nBefore func1: ");
119  Print_DrawInt(res_func1);
120  res_func1 = funcio1();
121  DEBUG_LOGNUM("Value after calling func1: ", res_func1);
122  Print_DrawText("\nAfter func1: ");
123  Print_DrawInt(res_func1);
124
125  Print_DrawText("\nReading file funcio2.bin. ");
126  DEBUG_LOG("Reading file funcio2.bin");
127  DOSMapper_SetPage(2, g_func2.Number);
128  FT_SetName(&g_File, "funcio2.bin");
129  DOS_OpenFCB(&g_File);
130  for (u16 i=0; i<g_File.Size; i+=128) {
131    DOS_SetTransferAddr(&array[i]); // Els he de guardar a array, llegir directament a allà
132    DOS_SequentialReadFCB(&g_File);
133  }
134
135  res_func2 = 16;
136  DEBUG_LOGNUM("\nBefore func2: ", res_func2);
137  Print_DrawText("Before func2: ");
138  Print_DrawInt(res_func2);
139  res_func2 = funcio2();
140  DEBUG_LOGNUM("After func2: ", res_func2);
141  Print_DrawText("\nAfter func2: ");
142  Print_DrawInt(res_func2);
143
144  Print_DrawText("\nWe have run the two function in different pages");
145  Print_DrawText("\nYou can debug the memory page in openmsx debugger");
146
147  Print_DrawText("\nPress space to go back to DOS");
148  while (!Keyboard_IsKeyPressed(KEY_SPACE));
149
150  Bios_Exit(0);
151}
            

Ja hem acabat la primera part del test, la que canviem bancs de memòria. A la segona part fem el mateix, però ara a cada nou segment hi guardarem una funció. Recordem que aquests mòduls amb les funcions addicionals no poden ser superiors a 16K.

Comencem reservant aquests segments a les línies 101 i 102, per després posar el primer a la pàgina 1. Mostrem diferent informació per pantalla i per al log de l'estat de l'experiment. Carreguem el binari de la funció funcio1 que es troba en el fitxer funcio1.bin en el segment recent assignat g_func1 entre les línies 110 i 115. Assignem el valor 4 a la variable res_func1, llegim i presentem aquest valor, cridem a la funció funcio1 i tornem a llegir i presentar el valor de la variable res_func1.

Entre les línies 125 i 145 fen el mateix que abans però ara per la funció funcio2.

Ja només queda escriure el text que es premi l'espai i esperar que apretin aquesta tecla per acabar.

1#pragma codeseg funcio1
2
3extern void DEBUG_LOG(const char* msg);
4
5unsigned char funcio1(){
6  DEBUG_LOG("funcio1 has been called");
7  return 0x5;
8}

1#pragma codeseg funcio2
2
3extern void DEBUG_LOG(const char* msg);
4
5unsigned char funcio2(){
6  DEBUG_LOG("funcio2 has been called");
7  return 0xde;
8}

Aquí tenim les dues funcions que fan el mateix, retornar un valor, però aquest valor és diferent. Les dues utilitzen la funció DEBUG_LOG que es trobarà en els 16K principals inicials, en la funció principal que aquest cas és la de paginate.c i és el linker (enllaçador) que s'encarregarà de buscar la seva adreça quan faci la unió de tots els binaris.

A diferència de l'anterior article, Paginació de la RAM, aquí no carreguem una altra vegada la funció DEBUG_LOG que ja està en la memòria de la funció principal. D'aquesta manera els mòduls queden més petits però augmentem la complexitat per fer bé el link.

La directriu del compilador #pragma codeseg funcio1 serveix per dir-li al compilador que tot aquest codi ha d'anar a una adreça concreta de la memòria. Així quan creï els fitxers .map i tradueixi el codi a binari, usarà l'adreça indicada a l'hora de compilar, quan li passem el paràmetre --code-loc 0x4000 al sdcc

A la següent imatge podeu veure l'execució de l'script tant en la pantalla emulada del MSX com a la sortida de la consola. Podem veure com fa els diferents passos i com els valors llegits corresponen als valors esperats segons el canvi de pàgina que s'havia fet.

Quins són els passos per compilar aquests mòduls de 16K?

Per tal d'obtenir els binaris de les funcions de 16K ben dirigits a les funcions que ja es troben a la part del programa principal, s'han de fer els següents passos:

  1. 1- Compilar el fitxer paginate.c
    
    ~/MSXgl/tools/sdcc/bin/sdcc -c -mz80 -DTARGET=TARGET_DOS2 -DMSX_VERSION=MSX_TR 
    -I~/MSXgl/projects/learning-msxgl/DOS2_pagination/ 
    -I~/MSXgl/engine/src 
    -I~/MSXgl/engine/content 
    -I~/MSXgl/tools/  
    --opt-code-speed --debug -DAPPSIGN ./paginate.c 
    -o ~/MSXgl/projects/learning-msxgl/DOS2_pagination/out/
            

    Hem utilitzat la mateixa comanda que fèiem servir per compilar el fitxer principal del mòdul en el MSXgl. En aquest pas, el compilador crea les etiquetes per després el linker pugui ubicar les funcions a l'espai de la memòria, creant el fitxer .rel. Aquest fitxer conté el codi màquina, les definicions dels símbols, les referències externes i permet ser recol·locat a la memòria (l'extensió .rel ve del relocate anglès que vol dir recol·locar).

  2. 2- Compilar les funcions
    
    ~/MSXgl/tools/sdcc/bin/sdcc -mz80 -c --codeseg funcio1 funcio1.c -o funcio1.rel
    ~/MSXgl/tools/sdcc/bin/sdcc -mz80 -c --codeseg funcio2 funcio2.c -o funcio2.rel
                

    Utilitzem el paràmetre --codeseg per indicar que creï un segment a la memòria anomenat funcio1 i que després podrem ubicar a la posició que volguem a l'hora de linkar-lo amb el paràmetre del sdcc -Wl-g_funcio1.

  3. 3- Linkar el paginate.c
    
    ~/MSXgl/tools/sdcc/bin/sdcc -mz80 --vc --no-std-crt0 -L~/MSXgl/tools/sdcc/lib/z80 
    --code-loc 0x0100 --data-loc 0x0000  --opt-code-speed --debug  
    ~/MSXgl/projects/learning-msxgl/DOS2_pagination/out/crt0_dos.rel 
    ~/MSXgl/projects/learning-msxgl/DOS2_pagination/out/msxgl.lib 
    ~/MSXgl/projects/learning-msxgl/DOS2_pagination/out/paginate.rel 
    -o ~/MSXgl/projects/learning-msxgl/DOS2_pagination/out/paginate.ihx 
    funcio1.rel funcio2.rel
                

    Un cop tenim els fitxers .rel s'ha d'utilitzar el linkador del sdcc per col·locar-lo definitivament a la memòria i resoldre tots els símbols i referències. Per axiò també li hem de donar els binaris de les funcions funcio1.rel i funcio2.rel que hem compilat abans i que són cridades pel paginate.c.

  4. 4- Buscar l’adreça de DEBUG_LOG i posar-la en el linker
    
    ~/MSXgl/tools/sdcc/bin/sdcc -mz80 --no-std-crt0 --code-loc 0x4000 funcio1.rel -o funcio1.ihx -Wl-g_DEBUG_LOG=0x1a87 -L~/MSXgl/tools/sdcc/lib/z80;
    
    ~/MSX/MSXgl/tools/sdcc/bin/sdcc -mz80 --no-std-crt0 --code-loc 0x4000 funcio2.rel -o funcio2.ihx -Wl-g_DEBUG_LOG=0x1a87 -L~/MSXgl/tools/sdcc/lib/z80;
            

    Ara hem de buscar l'adreça que té el DEBUG_LOG en el programa principal paginate.c per indicar-li als mòduls de 16K que quan faci la crida a aquesta fucnió que salti a l'adreça del programa principal. Un cop localitzada en el fitxer paginate.map li hem de dir al linker que l'utilitzi. Això ho fem amb el paràmetre -Wl-g i li indiquem el nom de la funció que apareix en el .map, en aquest cas _DEBUG_LOG i l'adreça que hem obtingut.

  5. 5- Generar els binaris i el fitxer .com
    
    ~/MSXgl/tools/MSXtk/bin/MSXhex ~/MSXgl/projects/learning-msxgl/DOS2_pagination/out/paginate.ihx -e com -s 0x0100 -l 0;
    
    ~/MSXgl/tools/MSXtk/bin/MSXhex ~/MSXgl/projects/learning-msxgl/DOS2_pagination/funcio1.ihx -e bin;
    
    ~/MSXgl/tools/MSXtk/bin/MSXhex ~/MSXgl/projects/learning-msxgl/DOS2_pagination/funcio2.ihx -e bin;
            

    Per tradui els binaris al format del MSX, utilitzem l'eina del MSXgl MSXhex. Fixem-nos-hi que el cridem de forma diferent per fer el programa principal que per fer les funcions, ja que el primer ha de seguir el format .com del MSXDOS2 mentre que els binaris és només la traducció.

  6. 6- Copiar el .com i els binaris al directori emulat
    
    cp funcio1.bin emul/dos2/;
    cp funcio2.bin emul/dos2/;
    cp out/paginate.com emul/dos2/;
                
  7. 7- Executar l’emulador
    
    ~/openMSX/derived/openmsx 
    -machine Panasonic_FS-A1ST -ext moonsound -ext debugdevice
    -ext msxdos2 -diska ~/MSXgl/projects/learning-msxgl/DOS2_pagination/emul/dos2
             

    Amb això ja tindrem l'emulador corrent i podrem veure el resultat de les nostres simulacions tal i com es pot veure a la imatge de dalt.

Podem automatitzar aquest procés?

La MSXgl ja permet fer projectes de més de 64K, però han de ser per ROM i t'ajuda a compilar-ho. Però nosaltres podem crear un script en JavaScript i que la MSXgl l'utilitzi.

Com funciona la compilació en la MSXgl?

El fitxer build.sh que es troba en el directori del nostre projecte crida a node que és la comanda del projecte Node.js que permet executar javascript fora del navegador. L'script que s'executa és el que li indiquem en el build.sh que en aquest cas és el que es troba a .../engine/script/js/build.js que s'encarrega d'orquestrar tota la compilació. El primer que busca és ./project_config.js i si no el troba, busca .../projects/default_config.js i si no .../engine/script/js/default_config.js. L'estructura d'aquests fitxers és tota la mateixa, i conté els diferents passos per compilar el projecte. Finalment, a part dels anteriors que només carrega un dels tres segons les prioritats anteriors, també carrega, en cas que existeixi,[project_name].js

El project_config.js permet carregar scripts abans de la compilació principal i scripts després de la compilació. Aquesta característica és la que utilitzarem per compilar els nostres drivers abans de la compilació principal i després en el post, localitzar les funcions del bloc principal que s'utilitzen als mòduls. Per fer axiò només s'han d'omplir les variables PreBuildScripts i PostBuildScripts amb les comandes a executar. En el nostre cas, hem creat un script en javascript, build_pagination.js que s'encarrega de fer això. Amb el que el nostre codi quedaria:

            
231//-- Command lines to be executed before the build process (array)
232PreBuildScripts = [	"node build_pagination.js pre" ];
233
234//-- Command lines to be executed after the build process (array)
235PostBuildScripts = [ "node build_pagination.js post" ];
            
        

Script per automatizar la compilació paginada

Un cop tenim clars els passos, anem a mirar com els podem posar en un script de JavaScript per continuar amb el llenguatge que s'utilitza a la MSXgl. Aquesta implementació la trobem al fitxer build_pagination.js.

  1#!/usr/bin/env node
  2// ____________________________
  3// ██▀▀█▀▀██▀▀▀▀▀▀▀█▀▀█        │   ▄▄▄                ▄▄
  4// ██  ▀  █▄  ▀██▄ ▀ ▄█ ▄▀▀ █  │  ▀█▄  ▄▀██ ▄█▄█ ██▀▄ ██  ▄███
  5// █  █ █  ▀▀  ▄█  █  █ ▀▄█ █▄ │  ▄▄█▀ ▀▄██ ██ █ ██▀  ▀█▄ ▀█▄▄
  6// ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀────────┘
  7// DOS2 Pagination Build Handler - Pre & Post Build Steps
  8
  9const fs = require('fs');
 10const path = require('path');
 11const { execSync } = require('child_process');
 12
 13const PROJECT_DIR = __dirname;
 14const OUT_DIR = path.join(PROJECT_DIR, 'out');
 15const EMUL_DIR = path.join(PROJECT_DIR, 'emul', 'dos2');
 16
 17// Load PaginatedFunctions from project_config.js
 18let PaginatedFunctions = [];
 19try {
 20    // Create a context object to capture the PaginatedFunctions variable
 21    const configCode = fs.readFileSync(path.join(PROJECT_DIR, 'project_config.js'), 'utf-8');
 22    const configModule = {};
 23    // Execute the config file in a controlled context
 24    const vm = require('vm');
 25    const context = vm.createContext({ PaginatedFunctions: [] });
 26    vm.runInContext(configCode, context);
 27    PaginatedFunctions = context.PaginatedFunctions;
 28
 29    if (!PaginatedFunctions || PaginatedFunctions.length === 0) {
 30        console.error('PaginatedFunctions not found or empty in project_config.js');
 31        process.exit(1);
 32    }
 33} catch (err) {
 34    console.error(`Error loading PaginatedFunctions from project_config.js: ${err.message}`);
 35    process.exit(1);
 36}
 37
 38const SDCC_PATH = '/home/jepsuse/MSX/MSXgl/tools/sdcc/bin';
 39const MSXHEX = '/home/jepsuse/MSX/MSXgl/tools/MSXtk/bin/MSXhex';
 40
 41const colors = {
 42    reset: '\x1b[0m',
 43    green: '\x1b[32m',
 44    yellow: '\x1b[33m',
 45    red: '\x1b[31m',
 46    blue: '\x1b[36m'
 47};
 48
 49function log(msg, color = 'reset') {
 50    console.log(`${colors[color]}${msg}${colors.reset}`);
 51}
 52
 53function exec(command, description) {
 54    log(`  ${description}...`, 'yellow');
 55    try {
 56        execSync(command, {
 57            cwd: PROJECT_DIR,
 58            stdio: 'pipe'
 59        });
 60        log(`  ✓ Done`, 'green');
 61    } catch (error) {
 62        log(`  ✗ Failed: ${error.message}`, 'red');
 63        throw error;
 64    }
 65}

Comencem definint variables que utilitzarem al llarg del procés de compilació. Destacar la variable PaginatedFunctions que contindrà el nom dels fitxers que volem compilar com a funcions a ser cridades posteriorment. Extreu aquest valor de la variable amb el mateix nom PaginatedFunctions del fitxer project_config.js.

 69function preBuild() {
 70    log('\n╔════════════════════════════════════════════════╗', 'blue');
 71    log('║   DOS2 Pagination - PRE-BUILD                  ║', 'blue');
 72    log('║   Step 1: Compile paginated functions          ║', 'blue');
 73    log('╚════════════════════════════════════════════════╝\n', 'blue');
 74
 75    if (!PaginatedFunctions || PaginatedFunctions.length === 0) {
 76        log('No paginated functions configured. Skipping pagination pre-build.\n', 'yellow');
 77        return;
 78    }
 79
 80    log('Compiling paginated functions:', 'yellow');
 81
 82    // Create out directory if it doesn't exist
 83    if (!fs.existsSync(OUT_DIR)) {
 84        fs.mkdirSync(OUT_DIR, { recursive: true });
 85    }
 86
 87    for (const func of PaginatedFunctions) {
 88        const file = `${func.name}.c`;
 89        if (!fs.existsSync(path.join(PROJECT_DIR, file))) {
 90            log(`  ✗ ${file} not found`, 'red');
 91            continue;
 92        }
 93        const cmd = `"${SDCC_PATH}/sdcc" -mz80 -c "${file}" -o "${OUT_DIR}/${func.name}.rel"`;
 94        exec(cmd, `  ${func.name}.c → out/${func.name}.rel`);
 95    }
 96
 97    log('\n✓ Pre-build complete. Main build will now process paginated functions.\n', 'green');
 98}

Aquesta és la primera funció que s'executa abans que la compilació estàndard de la MSXgl i que s'encarrega de crear els binaris dels fitxers de màxim 16K que utilitzarem en el nostre programa.

100/**
101 * Extract extern function declarations from a C source file and its includes.
102 * Looks for patterns like: extern void FUNCTION_NAME(...);
103 */
104function extractExternFunctions(filePath) {
105    const externFuncs = [];
106    const processedFiles = new Set();
107
108    function processFile(currentPath) {
109        // Avoid processing the same file twice
110        if (processedFiles.has(currentPath)) {
111            return;
112        }
113        processedFiles.add(currentPath);
114
115        if (!fs.existsSync(currentPath)) {
116            return;
117        }
118
119        const content = fs.readFileSync(currentPath, 'utf-8');
120
121        // Extract extern declarations: extern <return_type> FUNCTION_NAME(...)
122        const externRegex = /extern\s+\w+[\s\*]+(\w+)\s*\(/g;
123        let match;
124
125        while ((match = externRegex.exec(content)) !== null) {
126            const funcName = match[1];
127            if (!externFuncs.includes(funcName)) {
128                externFuncs.push(funcName);
129            }
130        }
131
132        // Extract #include directives and process them
133        const includeRegex = /#include\s+["<]([^"<>]+)[">]/g;
134        while ((match = includeRegex.exec(content)) !== null) {
135            const includePath = match[1];
136            // Try to find the included file in the project directory
137            const fullPath = path.join(PROJECT_DIR, includePath);
138            processFile(fullPath);
139        }
140    }
141
142    processFile(filePath);
143    return externFuncs;
144}
145
146/**
147 * Find symbol address in MAP file by function name.
148 * Looks for patterns like:
149 *   00001AA5  _DEBUG_LOG                         debug
150 */
151function findSymbolInMap(mapFile, funcName) {
152    const content = fs.readFileSync(mapFile, 'utf-8');
153    const lines = content.split('\n');
154
155    // Look for the symbol with underscore prefix: _FUNCNAME
156    const symbolName = '_' + funcName;
157
158    for (const line of lines) {
159        // Match: ADDRESS  _SYMBOL_NAME  module
160        const match = line.match(/^\s*([0-9A-Fa-f]+)\s+_[\w]+\s+/);
161        if (match && line.includes(symbolName)) {
162            const addr = '0x' + match[1];
163            return addr;
164        }
165    }
166
167    return null;
168}
169
170function postBuild() {
171    log('\n╔════════════════════════════════════════════════╗', 'blue');
172    log('║   DOS2 Pagination - POST-BUILD                 ║', 'blue');
173    log('║   Step 2: Relink & convert paginated functions ║', 'blue');
174    log('╚════════════════════════════════════════════════╝\n', 'blue');
175
176    if (!PaginatedFunctions || PaginatedFunctions.length === 0) {
177        log('No paginated functions configured. Skipping pagination post-build.\n', 'yellow');
178        return;
179    }
180
181    // Step 2a: Verify MAP file exists
182    log('Step 2a: Checking for main program MAP file...', 'yellow');
183    const mapFile = path.join(OUT_DIR, 'paginate.map');
184
185    if (!fs.existsSync(mapFile)) {
186        log(`   MAP file not found at ${mapFile}`, 'red');
187        log('     The main program must be linked first to generate the MAP file.\n', 'red');
188        return;
189    }
190    log('   MAP file found\n', 'green');
191
192    // Step 2b: Relink each paginated function at specified address with required symbols
193    log('Step 2b: Relinking paginated functions at specified addresses...', 'yellow');
194
195    for (const func of PaginatedFunctions) {
196        const name = func.name;
197        const pageAddr = typeof func.page === 'string' ? parseInt(func.page, 16) : func.page;
198        const codeLoc = `0x${pageAddr.toString(16).toUpperCase()}`;
199        const relFile = path.join(OUT_DIR, `${name}.rel`);
200
201        if (!fs.existsSync(relFile)) {
202            log(`   ${relFile} not found`, 'red');
203            continue;
204        }
205
206        // Extract extern function declarations from this source file
207        const sourceFile = path.join(PROJECT_DIR, `${name}.c`);
208        const externFuncs = extractExternFunctions(sourceFile);
209
210        if (externFuncs.length === 0) {
211            log(`  Relinking ${name} at ${codeLoc} (no external dependencies)...`, 'yellow');
212        } else {
213            log(`  Relinking ${name} at ${codeLoc} with ${externFuncs.length} external function(s):`, 'yellow');
214        }
215
216        // Find each extern function's address in the MAP file
217        let linkerFlags = '';
218        for (const funcName of externFuncs) {
219            const addr = findSymbolInMap(mapFile, funcName);
220            if (addr) {
221                linkerFlags += ` -Wl-g_${funcName}=${addr}`;
222                log(`    ${funcName} = ${addr}`, 'green');
223            } else {
224                log(`     ${funcName} NOT FOUND in MAP file!`, 'red');
225            }
226        }
227
228        const cmd = `"${SDCC_PATH}/sdcc" -mz80 --no-std-crt0 --code-loc ${codeLoc} ` +
229            `"${relFile}"${linkerFlags} ` +
230            `-L"${SDCC_PATH}/../lib/z80" -o "${OUT_DIR}/${name}.ihx"`;
231
232        exec(cmd, `  Relinking ${name} at ${codeLoc}`);
233    }
234
235    // Step 2c: Convert IHX to binary format
236    log('\nStep 2c: Converting IHX files to binary format...', 'yellow');
237
238    for (const func of PaginatedFunctions) {
239        const name = func.name;
240        const ihxFile = path.join(OUT_DIR, `${name}.ihx`);
241
242        if (fs.existsSync(ihxFile)) {
243            const cmd = `"${MSXHEX}" "${ihxFile}"`;
244            exec(cmd, `  Converting ${name}.ihx  ${name}.bin`);
245        }
246    }
247
248    // Step 2d: Copy files to emulator directory
249    log('\nStep 2d: Copying binaries to emulator directory...', 'yellow');
250
251    if (!fs.existsSync(EMUL_DIR)) {
252        fs.mkdirSync(EMUL_DIR, { recursive: true });
253    }
254
255    for (const func of PaginatedFunctions) {
256        const name = func.name;
257        const binFile = path.join(OUT_DIR, `${name}.bin`);
258
259        if (fs.existsSync(binFile)) {
260            fs.copyFileSync(binFile, path.join(EMUL_DIR, `${name}.bin`));
261            log(`   Copied ${name}.bin`, 'green');
262        }
263    }
264
265    log('\n Post-build complete!\n', 'green');
266}
267
        

Aquesta és la part a executar un cop la compilació estàndard de la MSXgl ha acabat. La primera funció extractExternFunctions s'encarrega de buscar el nom de les funcions que estan definides al fitxer C com extern, aquests noms són els que després la funció findSybolInMap haurà de localitzar en el fitxer map per trobar l'adreça de memòria que el link li ha assignat. Aquestes dues funcions són les que s'utilitzen en el postBuild per relinkar els nostres programes que aniran a les pàgines de 16K i saltin a l'adreça corresponent.

268// Determine which phase to run
269const args = process.argv.slice(2);
270const phase = args[0] || 'pre';
271
272try {
273    if (phase === 'pre') {
274        preBuild();
275    } else if (phase === 'post') {
276        postBuild();
277    } else {
278        log(`Unknown phase: ${phase}`, 'red');
279        process.exit(1);
280    }
281} catch (error) {
282    log(`\n✗ Build failed!\n`, 'red');
283    process.exit(1);
284}
        

Finalment només queda controlar si quan es crida l'script es fa en la fase de prebuild o en la de postbuild i segons d'on vingui la crida executar un bloc o un altre.

Funció driver de la MSXgl

Dins el llarg exemple de MSXDOS2 que es pot trobar al directori samples, s_dos2.c, hi ha una part que s'anomena driver que permet carregar binaris en memòria i cridar-los. Això és més semblant al que havíem fet a Paginació de la RAM a on el binari és autocontingut i no utilitza funcions comunes amb el bloc principal. Però la forma de compilar-ho és més senzilla.

Quins passos hem de fer per utilitzar la funcionalitat de driver?

  1. Crear el binari i compilar-lo

    Primer de tot s'ha de tenir el programa funcionant, en el nostre exemple senzill, driv1.c només retornem un valor quan es crida la funció. Per compilar-ho podem utilitzar la mateixa plantilla que s'utilitza per compilar els fitxers en C de la MSXgl, en el nostre cas l'hem copiada a driv1.js i s'ha de canviar la línia:

    
    //-- Overwrite code starting address (number). For example. 0xE0000 for a driver in RAM
    ForceCodeAddr = 0xE000;
                    
    indicant l'adreça de memòria a on serà guardada. D'aquesta manera el linker redirecciona totes les adreces de memòria a partir d'aquesta base.

  2. Cridar-lo en el prgorama principal

    Un cop ja l'hem creat s'ha de carregar en memòria i cridar-lo des del programa principal. En el nostre cas aquest és el pag_driver.c i per cridar-lo ho fem:

    
    res_func1 = CallDriver(0xE000, 5);
                    
    Per carrega-lo en memòria hem usat les comandes de disc ja utilitzades prèviament.

  3. Compilar el programa principal

    Ja només queda compilar el programa principal i executar-lo amb l'emulador.