Simple8

Les ressources

Un jeu, ce n'est pas que du code : ce sont aussi des images, des polices, des cartes, des sons, des tables de valeurs. Ce chapitre montre comment Simple8 les fait entrer dans le programme, les convertit au format de chaque machine, et les range en mémoire.

Qu'est-ce qu'une ressource ?

Une ressource est une donnée du programme qui n'est pas du code : une police de caractères, des tuiles, une palette, une carte de niveau, un son, une table calculée d'avance. Elle vient d'un fichier du projet, ou elle est fabriquée au moment de la compilation.

Nous en utilisons une depuis le chapitre 2 : la police de la PC Engine.

const int POLICE = addResource("font.bin");

addResource fait trois choses :

  1. à la compilation, il lit le fichier (ou fabrique les données) ;
  2. il range les données dans la cartouche, avec le programme ;
  3. il donne un identifiant, ici POLICE, grâce auquel le programme retrouvera ces données pendant qu'il tourne.

Pour transformer les données au passage, on passe par une ResourceFactory, une « fabrique » de ressources : elle reçoit un fichier ou des paramètres, et produit les octets prêts à l'emploi.

const int VAGUE = addResource(ResourceFactory("TrigoMaker",
    "function=sin&count=32&amplitude=10&offset=12"));

Le premier argument est le nom de la fabrique, le second ses paramètres, séparés par des &, comme dans une adresse web.

Trois genres de ressources

Les ressources se distinguent par leur rapport à la machine : certaines sont les mêmes partout, d'autres s'adaptent à chaque machine, d'autres encore n'existent que pour une seule.

1. Les ressources génériques

Ce sont des données pures, qui ne dépendent d'aucun matériel : toutes les machines peuvent s'en servir telles quelles. Une table de sinus en est l'exemple type : 32 nombres entre 2 et 22, que ce soit pour un C64 ou une PC Engine.

FabriqueCe qu'elle produit
RawBinaryun fichier, tel quel. C'est ce que fait addResource("fichier") sans fabrique.
TrigoMakerune table de sinus ou de cosinus, calculée à la compilation
TrackerSongune chanson écrite dans un fichier texte, à la manière d'un tracker

Les paramètres de TrigoMaker décrivent la table :

ParamètreRôleIci
functionsin ou cossin
countle nombre de valeurs, pour un tour complet32
amplitudel'écart maximal autour du centre10
offsetle centre12 : les valeurs vont de 2 à 22

2. Les ressources multi-architectures

Ici, une même source est convertie au format de chaque machine. Une image ne s'affiche pas de la même façon sur un C64 et sur une PC Engine : nombre de couleurs, codage des pixels, palettes, tout diffère. La fabrique détecte l'architecture du projet, celle de la ligne architecture, et lance la conversion qui lui correspond.

FabriqueSourceCe qu'elle produit
SquareDotun projet de l'éditeur SquareDot (.sqd)les tuiles, les palettes et les cartes, au format de la machine : 4 bits par pixel sur PC Engine, 2 plans sur NES et Game Boy, un octet par ligne sur C64…
BuildPaletteFromImageFileune imagela palette de ses couleurs principales, au format de la machine
const int TUILES = addResource(ResourceFactory("SquareDot", "niveaux.sqd&part=tiles"));
const int COULEURS = addResource(ResourceFactory("SquareDot", "niveaux.sqd&part=palettes"));
const int CARTE = addResource(ResourceFactory("SquareDot", "niveaux.sqd&part=map&map=Carte 1"));

Ces trois lignes restent les mêmes d'une machine à l'autre : seul le résultat change. Une conversion qui n'existe pas encore pour une machine est signalée :

ResourceFactory("SpriteMotionGenerator", …): not implemented for sega.megadrive yet (available: nec.pce)

3. Les ressources spécifiques

Certaines ressources n'ont de sens que pour une machine, parce qu'elles visent un circuit qu'elle seule possède :

FabriqueMachineCe qu'elle produit
PSGSamplePC Engineun son numérisé pour le circuit sonore de la console
VgmMega Driveune musique pour le lecteur du Z80
MixerSampleMega Driveun bruitage pour le pilote son du Z80
ResourceFactory("PSGSample", …): for nec.pce only, not for commodore.c64

En résumé. Générique : les mêmes octets partout. Multi-architecture : une source, un format par machine. Spécifique : une seule machine.

Toutes les machines n'ont pas addResource

addResource range ses données dans la cartouche de la PC Engine et de la Mega Drive, ou dans l'image de l'Amiga. Sur les autres machines, le compilateur s'arrête, et indique la solution :

addResource: the resources are placed in the cartridge of the PC Engine (nec.pce) and of the
Mega Drive (sega.megadrive), or the Amiga image (commodore.amiga), not on commodore.c64;
put the data in a const array (generated by a script, in a unit)

Sur C64, NES, Game Boy et Amstrad CPC, les données vont donc dans des tableaux constants, dans une unit. Les outils de la suite Simple8 les écrivent eux-mêmes : SquareDot exporte, pour ces machines, une unit (.s8u) qui contient les tuiles et les cartes, que le programme ajoute par addunit. 3DSpriteMaker fait de même pour ses sprites, sur PC Engine et Game Boy.

// Carte.s8u, exporté par SquareDot pour le C64
unit Carte {
    const int W = 80;
    const int H = 25;
    const ubyte CHARS[56] = { … };      // les caractères dessinés
    …
}

Le genre de la ressource ne change pas : une table de sinus reste générique, qu'elle soit produite par TrigoMaker ou écrite dans un tableau.

Lire une ressource

Sur PC Engine, le processeur ne voit qu'une petite partie de la cartouche à la fois. Une ressource n'est donc pas directement lisible : il faut d'abord rendre visible la banque qui la contient. La bibliothèque sys.resource s'en charge :

FonctionRôle
resource.mapToMPR2(id)rend visible la banque de la ressource, dans la fenêtre $4000-$5FFF
resource.getOffset(id)sa position dans la banque
resource.getLength(id)sa taille en octets
resource.getBank(id)le numéro de sa banque
resource.mapToMPR2(.VAGUE);                         // la banque de VAGUE à $4000
uint debut = 0x4000 + resource.getOffset(.VAGUE);   // l'adresse de ses données
ubyte premiere = readMem(debut);                    // sa première valeur : 12

Sur Mega Drive et Amiga, plus simples, l'identifiant est directement l'adresse des données sur 32 bits : on le range dans un ulong, et on lit avec readMem.

Choisir des noms clairs. L'identifiant d'une ressource devient un nom de l'assembleur. Un nom comme A, X ou Y se confond avec un registre du processeur, et l'assemblage échoue. Préférez POLICE, TUILES_NIVEAU1…

L'organisation en mémoire : la HuCard

Sur une machine à cartouche, comment ranger des centaines de kilo-octets de ressources dans une mémoire que le processeur ne voit que par petits morceaux ? Prenons la PC Engine.

Des banques de 8 Ko

La HuCard est découpée en banques de 8 Ko. Le compilateur y range le code résident, puis les contextes, puis les ressources. Une règle s'impose : une ressource de 8 Ko au plus ne chevauche jamais deux banques, pour qu'un seul mapToMPR2 suffise à la lire en entier. Une ressource plus grande commence au début d'une banque et continue dans les suivantes ; elle se lit banque par banque.

Les tables d'index

Pour retrouver chaque ressource, le compilateur écrit trois tables dans la ROM, où l'identifiant sert d'indice : la banque de chaque ressource, sa position dans la banque, et sa taille. getBank, getOffset, getLength et mapToMPR2 ne font que lire ces tables.

C'est ce qui permet d'optimiser la place : puisque le programme retrouve toujours une ressource par ses tables, le compilateur est libre de la ranger n'importe où, sans suivre l'ordre des déclarations. Il en profite pour combler les trous :

  1. les ressources de plus de 8 Ko sont rangées d'abord, chacune au début d'une banque ;
  2. les autres sont prises de la plus grande à la plus petite, et chacune va dans la première banque où il reste assez de place, y compris la fin d'une grande ressource ;
  3. une nouvelle banque n'est ouverte que si aucune n'a la place.
Cinq ressources de 5 000, 2 000, 3 000, 1 000 et 12 000 octets. Rangées dans l'ordre de déclaration, elles occupent 4 banques ; rangées en comblant les trous, 3 banques Dans l'ordre de déclaration : 4 banques A · 5 000 B · 2 000 perdu banque 2 C · 3 000 D · 1 000 perdu banque 3 BIG · 12 000 banque 4 fin de BIG perdu banque 5 En comblant les trous : 3 banques BIG · 12 000 banque 2 fin de BIG C · 3 000 D · 1 000 banque 3 A · 5 000 B · 2 000 perdu banque 4
Cinq ressources de 5 000, 2 000, 3 000, 1 000 et 12 000 octets, déclarées dans cet ordre (A, B, C, D, BIG).

Voici les tables que le compilateur écrit pour cet exemple, dans l'ordre des identifiants A, B, C, D, BIG :

_resource_banks:            // la banque de chaque ressource
    .db RESOURCE_FIRST_BANK+2, RESOURCE_FIRST_BANK+2, RESOURCE_FIRST_BANK+1,
        RESOURCE_FIRST_BANK+1, RESOURCE_FIRST_BANK+0
_resource_offsets:          // sa position dans la banque
    .dw $0000, $1388, $0EE0, $1A98, $0000

RESOURCE_FIRST_BANK est la première banque après le code résident et les contextes. Pour C, l'identifiant 2, les tables donnent la banque RESOURCE_FIRST_BANK+1 et la position $0EE0 (3 808) : juste après la fin de BIG. Le programme n'a rien à savoir de ce rangement : il appelle mapToMPR2(.RES_C), comme avant.

Ressources et contextes

Sur une machine à cartouche

Sur PC Engine, une ressource déclarée dans une unit de contexte va dans la même table que les autres, après les banques des contextes. Elle se lit de la même façon, depuis n'importe quel contexte.

unit Niveau {
    const int VAGUE = addResource(ResourceFactory("TrigoMaker", "function=sin&count=32&amplitude=10&offset=12"));

    initContext() {
        uint debut;
        resource.mapToMPR2(.VAGUE);
        debut = 0x4000 + resource.getOffset(.VAGUE);
        // copier en mémoire vidéo, lire des valeurs…
    }
    …
}

Cela fonctionne parce que les deux fenêtres sont séparées : le code du contexte est visible à partir de $6000, la banque d'une ressource à $4000. Le contexte peut donc lire ses données sans se masquer lui-même. Et rien n'est copié au chargement : les données restent dans la ROM, où on les lit au besoin.

Le bon moment pour copier une ressource en mémoire vidéo est initContext() : les interruptions y sont masquées, la copie n'est pas interrompue (chapitre 8).

Sur une machine à disquette

Sur C64 et Amstrad CPC, un contexte est un fichier chargé depuis la disquette. Ses tableaux constants font partie de ce fichier : ils arrivent en mémoire avec lui, et disparaissent quand un autre contexte prend sa place. Les données d'un niveau ont donc intérêt à être déclarées dans le contexte de ce niveau : elles n'occupent la mémoire que lorsqu'on y joue.

Cartouche (PC Engine)Disquette (C64, CPC)
Les données d'un contexterestent dans la ROM, toujours disponiblessont chargées avec le fichier du contexte
Pour les lirerendre visible leur banque (mapToMPR2)directement : elles sont en mémoire
La limitela taille de la cartouchela place du contexte en mémoire

Un exemple : une vague de sinus

Ce programme dessine une vague à l'écran : une étoile par colonne, à la hauteur donnée par une table de sinus. Sur PC Engine, la table et la police sont deux ressources génériques.

// Les ressources : une vague de sinus (PC Engine)
architecture nec.pce;

addlib sys.resource;
addlib vce.base;
addlib vdc.base;
addlib vdc.screentext;

unit {
    // deux ressources génériques : un fichier tel quel, et une table calculée à la compilation
    const int POLICE = addResource("font.bin");
    const int VAGUE = addResource(ResourceFactory("TrigoMaker",
        "function=sin&count=32&amplitude=10&offset=12"));

    main() {
        int source;
        int length;
        uint debut;

        // la police : rendue visible à $4000, puis copiée en mémoire vidéo
        vdc.setBATSize(vdc.BAT_32x32);
        vdc.initScreen();
        resource.mapToMPR2(.POLICE);
        source = 0x4000 + resource.getOffset(.POLICE);
        length = resource.getLength(.POLICE);
        vdc.writeToVRAM(source, 0x1000, length);
        vce.setPalette(0, 0, RGB(0, 0, 2));
        vce.setPalette(0, 1, RGB(7, 7, 7));
        vce.setPalette(1, 1, RGB(2, 6, 7));
        vdc.clearScreen();

        vdc.setFontColor(0);
        vdc.setCursor(7, 1);
        vdc.print("UNE VAGUE DE SINUS");

        // la table : rendue visible à son tour, puis lue case par case
        resource.mapToMPR2(.VAGUE);
        debut = 0x4000 + resource.getOffset(.VAGUE);
        vdc.setFontColor(1);
        for (ubyte x = 0; x < 32; x++) {
            vdc.setCursor(x, (ubyte)(readMem(debut + x) + 3));  // la hauteur : la case x, sous le titre
            vdc.putChar('*');
        }
    }
}
Écran de la PC Engine : UNE VAGUE DE SINUS, une période de sinus dessinée en étoiles
La vague sur PC Engine, tirée d'une table TrigoMaker.
La version Commodore 64

Le C64 n'a pas addResource : les mêmes 32 valeurs sont rangées dans un tableau constant. La ressource reste générique ; seule sa façon d'entrer dans le programme change.

// Les ressources : une vague de sinus (Commodore 64)
architecture commodore.c64;

addlib vic.base;
addlib vic.screentext;

unit {
    // pas de addResource sur C64 : les mêmes 32 valeurs, dans un tableau constant
    const ubyte VAGUE[] = {
        12, 14, 16, 18, 19, 20, 21, 22, 22, 22, 21, 20, 19, 18, 16, 14,
        12, 10,  8,  6,  5,  4,  3,  2,  2,  2,  3,  4,  5,  6,  8, 10
    };

    main() {
        vic.initScreen();
        vic.setBorderColor(vic.BLACK);
        vic.setBackgroundColor(vic.BLUE);
        vic.clearScreen();

        vic.setFontColor(vic.WHITE);
        vic.setCursor(11, 0);
        vic.print("UNE VAGUE DE SINUS");

        vic.setFontColor(vic.CYAN);
        for (ubyte x = 0; x < 32; x++) {
            vic.setCursor((ubyte)(x + 4), .VAGUE[x]);   // la hauteur : la case x du tableau
            vic.putChar('*');
        }
    }
}
Écran du Commodore 64 : UNE VAGUE DE SINUS, la même vague en étoiles
La même vague sur Commodore 64.

À retenir

À vous de jouer

  1. Doublez l'amplitude de la vague. Tient-elle encore dans l'écran ? Sinon, quelle est la plus grande vague possible, et que faut-il changer avec l'amplitude ?
    Voir la solutionMasquer la solution

    Commençons par faire l'essai : amplitude=20, sans rien changer d'autre. Les valeurs devraient aller de 12 − 20 = −8 à 12 + 20 = 32. Deux problèmes apparaissent à l'écran :

    • la table est faite d'octets sans signe, qui ne descendent pas sous 0. TrigoMaker remplace les valeurs négatives par 0 : le haut de la vague est écrasé en un long plateau (on le vérifie dans asm/resources.asm, où la table contient neuf $00 de suite) ;
    • le bas, jusqu'à la ligne 32 + 3 = 35, sort de l'écran, qui n'a que 28 lignes visibles.
    PC Engine : la vague d'amplitude 20, au sommet aplati et dont le bas sort de l'écran
    Amplitude 20 : le sommet est aplati, le bas est hors de l'écran.

    Il faut donc aussi déplacer le centre (offset) pour que la plus petite valeur reste positive. Mais cela ne suffit pas : une vague d'amplitude 20 s'étend sur 2 × 20 + 1 = 41 lignes, plus que les 28 lignes de la PC Engine et les 25 du C64. Une vague deux fois plus haute, à raison d'une ligne par unité, ne peut pas tenir dans l'écran. Ce qu'on peut faire, c'est l'agrandir autant que l'écran le permet. La règle : offset au moins égal à amplitude (pas de valeur négative), et offset + amplitude, plus le décalage sous le titre, au plus égal à la dernière ligne.

    Sur PC Engine, les lignes 3 à 27 sont libres sous le titre, soit 25 lignes : une amplitude de 12 avec un centre à 12 donne des valeurs de 0 à 24, dessinées des lignes 3 à 27.

        // amplitude 12, centre 12 : de 0 à 24, soit les lignes 3 à 27 une fois sous le titre
        const int VAGUE = addResource(ResourceFactory("TrigoMaker",
            "function=sin&count=32&amplitude=12&offset=12"));
    PC Engine : une vague d'amplitude 12 qui occupe tout l'écran sous le titre
    Amplitude 12 : la plus grande vague qui tient sous le titre.

    Sur Commodore 64, le titre occupe la ligne 0 et il reste les lignes 1 à 24 : une amplitude de 11 avec un centre à 12 (valeurs de 1 à 23) tient. Faute de TrigoMaker, il faut recalculer soi-même les 32 valeurs, 12 + 11 × sin(2π × x / 32) arrondies :

        // amplitude 11, centre 12 : de 1 à 23, toutes les lignes sous le titre
        const ubyte VAGUE[] = {
            12, 14, 16, 18, 20, 21, 22, 23, 23, 23, 22, 21, 20, 18, 16, 14,
            12, 10,  8,  6,  4,  3,  2,  1,  1,  1,  2,  3,  4,  6,  8, 10
        };

    C'est là l'avantage d'une table fabriquée à la compilation : sur PC Engine, changer un paramètre suffit ; sur C64, il faut refaire le calcul et recopier les nombres.

  2. Dessinez une deuxième vague, un cosinus, d'une autre couleur.
    Voir la solutionMasquer la solution

    Sur PC Engine, on déclare une deuxième ressource TrigoMaker, avec function=cos et les mêmes autres paramètres. Une nouvelle couleur demande une nouvelle palette : la palette 2 reçoit un orange dans sa couleur 1. Le cosinus se dessine ensuite comme le sinus, après avoir rendu sa banque visible avec mapToMPR2.

        const int VAGUE = addResource(ResourceFactory("TrigoMaker",
            "function=sin&count=32&amplitude=10&offset=12"));
        const int VAGUE2 = addResource(ResourceFactory("TrigoMaker",
            "function=cos&count=32&amplitude=10&offset=12"));     // la même, en cosinus
            vce.setPalette(1, 1, RGB(2, 6, 7));
            vce.setPalette(2, 1, RGB(7, 5, 1));             // palette 2 : orange, pour le cosinus
            // le cosinus : sa banque est rendue visible avant de le lire
            resource.mapToMPR2(.VAGUE2);
            debut = 0x4000 + resource.getOffset(.VAGUE2);
            vdc.setFontColor(2);
            for (ubyte x = 0; x < 32; x++) {
                vdc.setCursor(x, (ubyte)(readMem(debut + x) + 3));
                vdc.putChar('*');
            }
    PC Engine : une vague bleue (sinus) et une vague orange (cosinus), décalées l'une de l'autre
    Le sinus en bleu, le cosinus en orange : la même courbe, décalée d'un quart de tour.

    Ici, les deux petites tables sont rangées dans la même banque (le compilateur comble les trous), et le deuxième mapToMPR2 ne change donc rien. Mais le programme n'a pas à le savoir : appeler mapToMPR2 avant de lire une ressource est la règle sûre, quel que soit le rangement.

    Sur Commodore 64, inutile d'écrire une seconde table : un cosinus est un sinus en avance d'un quart de tour. Avec 32 valeurs par tour, il suffit de lire la même table 8 cases plus loin, en revenant au début après la fin grâce au modulo.

            for (ubyte x = 0; x < 32; x++) {
                vic.setCursor((ubyte)(x + 4), .VAGUE[x]);   // la hauteur : la case x du tableau
                vic.putChar('*');
            }
    
            // le cosinus : la même table, lue un quart de tour (8 cases) plus loin
            vic.setFontColor(vic.YELLOW);
            for (ubyte x = 0; x < 32; x++) {
                vic.setCursor((ubyte)(x + 4), .VAGUE[(x + 8) % 32]);
                vic.putChar('*');
            }
        }

    Les deux écrans se ressemblent : la table TrigoMaker du cosinus contient exactement les valeurs du sinus décalées de 8 cases (22, 22, 21, 20…). Là où les deux vagues se croisent, la seconde, dessinée après, recouvre la première.

  3. Faites défiler la vague : à chaque image, décalez d'une case l'indice lu dans la table ((x + decalage) % 32).
    Voir la solutionMasquer la solution

    Pour faire défiler la vague, on la redessine à chaque image, en lisant la table à partir d'une case de plus. Trois ingrédients :

    • une horloge : l'interruption de l'image du chapitre 8 (irq1 sur PC Engine, irq sur C64) augmente .images, et la boucle principale attend qu'elle ait changé ;
    • un decalage qui augmente de 1 à chaque image, ramené à 0 après 31 par le modulo ;
    • un effacement : avant de dessiner l'étoile d'une colonne, on efface celle de l'image précédente, en écrivant une espace à la hauteur lue avec l'ancien décalage. Sans cela, les étoiles s'accumuleraient et l'écran se remplirait.

    Chaque colonne n'a qu'une étoile, et elle n'est touchée que par son propre tour de boucle : effacer puis dessiner colonne par colonne ne risque donc pas d'effacer une étoile qui vient d'être posée.

    Sur PC Engine

    addlib event.irq1;
    
    unit {
        // deux ressources génériques : un fichier tel quel, et une table calculée à la compilation
        const int POLICE = addResource("font.bin");
        const int VAGUE = addResource(ResourceFactory("TrigoMaker",
            "function=sin&count=32&amplitude=10&offset=12"));
        ubyte images;                                       // compté par l'interruption
        ubyte decalage;                                     // le début de la lecture dans la table
            // la table : rendue visible à son tour, puis lue case par case
            resource.mapToMPR2(.VAGUE);
            debut = 0x4000 + resource.getOffset(.VAGUE);
            vdc.setFontColor(1);
            while (1) {
                .images = 0;
                while (.images == 0) {                      // attendre l'image suivante
                }
                for (ubyte x = 0; x < 32; x++) {
                    // effacer l'étoile de l'image précédente...
                    vdc.setCursor(x, (ubyte)(readMem(debut + (x + .decalage) % 32) + 3));
                    vdc.putChar(' ');
                    // ... et dessiner la nouvelle, une case plus loin dans la table
                    vdc.setCursor(x, (ubyte)(readMem(debut + (x + .decalage + 1) % 32) + 3));
                    vdc.putChar('*');
                }
                .decalage = (ubyte)((.decalage + 1) % 32);
            }
        }
    
        interrupt -> irq1 {
            .images++;
        }
    }

    La table est rendue visible une seule fois, avant la boucle : rien d'autre ne change la banque visible à $4000 ensuite.

    Sur Commodore 64

            vic.setFontColor(vic.CYAN);
            while (1) {
                .images = 0;
                while (.images == 0) {                      // attendre l'image suivante
                }
                for (ubyte x = 0; x < 32; x++) {
                    // effacer l'étoile de l'image précédente...
                    vic.setCursor((ubyte)(x + 4), .VAGUE[(x + .decalage) % 32]);
                    vic.putChar(' ');
                    // ... et dessiner la nouvelle, une case plus loin dans la table
                    vic.setCursor((ubyte)(x + 4), .VAGUE[(x + .decalage + 1) % 32]);
                    vic.putChar('*');
                }
                .decalage = (ubyte)((.decalage + 1) % 32);
            }
        }
    
        interrupt -> irq {
            .images++;
        }
    }

    Mesuré dans l'émulateur, decalage avance bien d'une case par image sur les deux machines : la vague glisse vers la gauche et fait un tour complet en 32 images, soit environ 0,5 seconde sur PC Engine (60 images par seconde) et 0,64 seconde sur C64 (50 images par seconde). Le redessin des 32 colonnes tient largement dans le temps d'une image. Pour un défilement plus lent, il suffit d'attendre 2 ou 3 images par pas, comme while (.images < 3).

  4. Ajoutez trois ressources de 3 000, 4 000 et 5 000 octets, et cherchez dans asm/main.asm les banques que le compilateur leur a données.
    Voir la solutionMasquer la solution

    On crée trois fichiers de 3 000, 4 000 et 5 000 octets (leur contenu n'a pas d'importance ici), et on les déclare dans la unit du programme de la vague :

        // deux ressources génériques : un fichier tel quel, et une table calculée à la compilation
        const int POLICE = addResource("font.bin");
        const int VAGUE = addResource(ResourceFactory("TrigoMaker",
            "function=sin&count=32&amplitude=10&offset=12"));
        // trois fichiers de 3 000, 4 000 et 5 000 octets
        const int DONNEES3000 = addResource("donnees3000.bin");
        const int DONNEES4000 = addResource("donnees4000.bin");
        const int DONNEES5000 = addResource("donnees5000.bin");

    Le programme compile sans erreur, et asm/main.asm contient les tables d'index. Elles sont dans l'ordre des identifiants : POLICE (0), VAGUE (1), DONNEES3000 (2), DONNEES4000 (3), DONNEES5000 (4).

    ; Resource Tables (5 resources)
    ; ========================================
    
    _resource_banks:
        .db RESOURCE_FIRST_BANK+0, RESOURCE_FIRST_BANK+1, RESOURCE_FIRST_BANK+1, RESOURCE_FIRST_BANK+2, RESOURCE_FIRST_BANK+1
    
    _resource_offsets:
        .dw $0000, $1F40, $1388, $0000, $0000
    
    _resource_lengths:
        .dw $2000, $0020, $0BB8, $0FA0, $1388
    
    _resource_count = 5

    Le fichier asm/resources.asm, qui contient les données elles-mêmes, le dit plus lisiblement, banque par banque :

    BanqueRessources (position)Occupé
    RESOURCE_FIRST_BANK+0POLICE ($0000)8 192 octets : pleine
    RESOURCE_FIRST_BANK+1DONNEES5000 ($0000), DONNEES3000 ($1388 = 5 000), VAGUE ($1F40 = 8 000)8 032 octets
    RESOURCE_FIRST_BANK+2DONNEES4000 ($0000)4 000 octets

    C'est exactement la règle du chapitre, de la plus grande à la plus petite :

    1. la police, 8 192 octets, remplit la première banque ;
    2. 5 000 octets : la première banque est pleine, une deuxième est ouverte ;
    3. 4 000 octets : il ne reste que 3 192 octets dans la deuxième, une troisième est ouverte ;
    4. 3 000 octets : ils tiennent dans les 3 192 octets restants de la deuxième banque, derrière les 5 000 ;
    5. la vague, 32 octets, se glisse dans les 192 octets qui restent encore dans la deuxième banque.

    Le compilateur n'a pas suivi l'ordre des déclarations : la deuxième banque commence par les 5 000 octets, déclarés en dernier. Avec ces tailles-là, remplir les banques dans l'ordre de déclaration aurait d'ailleurs aussi demandé trois banques (la vague, 3 000 et 4 000 dans la deuxième, 5 000 dans la troisième) : le gain dépend des tailles, comme dans l'exemple du chapitre où il économise une banque entière. Le programme, lui, n'a rien à savoir : mapToMPR2(.DONNEES3000) lit les tables et trouve la bonne banque.