Introduction

Lorsque j'ai commencé à développer Puzzle Bobble, je m'étais fixé un objectif: il devait tourner sur la GX4000. (et cela sera le cas pour tous les jeux que nous ferons par la suite sur Amstrad).

A la différence de l'Oncle Ced, je n'ai pas un affect particulier pour cette console (j'étais passé au PC lors de sa sortie), mais l'expérience utilisateur d'un jeu console est toujours meilleure. Et la GX4000 étant rétrocompatible avec le CPC, cela en fait un fabuleux petit engin. Niveau jeu: 2 manettes de 4 flèches, 2 boutons. Difficile de faire plus simple. Rien de plus frustrant que devoir aller regarder dans un mode d'emploi pour chercher les règles du jeu.

Donc, pour revenir à Puzzle Bobble, il devait tourner sur la GX4000 et faire avec ces restrictions: 64k et pas d'accès disque. Avec no$cpc, le convertisseur d'image disque vers GX4000, l'accès disque peut être simulé, mais ce n'est pas ce que je voulais.

Si je veux que le programme tourne aussi sur 6128 (ou sur 464 avec extension mémoire), je ne peux bénéficier que d'une banque en lecture seule de 64k en supplément des mes 64k de base. Lecture seule pour qu'il tourne aussi sur la GX4000 (avec un tout petit peu de modifications pour le changement des banques, mais nous y reviendrons)

Cela fait quelques contraintes, mais beaucoup d'avantages...

Puzzle Bobble fonctionne aussi sur Dandanator avec un peu de changement de code, mais je n'en parlerais pas dans cet article.

Programme de chargement

Mémoire haute

Sur CPC6128, mon programme de chargement se fait en basic, je dois donc lui réserver l'espace mémoire entre 368 (0x170) et 1024 (0x400). Soit 656 octets tokenizés en Basic. Ce qui est suffisant pour un loader. Le basic se réserve l'espace entre 42619 (0xA67B) jusque 49152 (0xC000). Ce 42619 correspond a un print himem sur un CPC6128. Il varie selon le CPC. À partir de 49152 (0xC000), nous avons la mémoire écran.

c11a0267917c41baa1a9d6dac12a8fce

Mon application principale ne peut donc faire que 42619-1024 soit 41595 bytes maximum.

La première chose que je fais dans mon application principale, c'est de désactiver le firmware. Il prend du cpu et de l'espace mémoire pour rien. Je pourrais ainsi récupérer les 6533 octets (49152-42619) réservés par le basic. Ce n'est pas négligeable.

Sur GX4000, je n'ai pas cette limitation, mais pour avoir un code plus simple, je me l'impose aussi.

Mémoire haute

Pour la mémoire haute, c'est plus simple. Je dispose de banque de 4x16kb que je peux facilement charger en basic.

10 FOR i=1 TO 4
20 OUT &7F00,&C3+i
30 LOAD"!slot"+CHR$(48+i)+".bin",&4000
40 OUT &7F00,&C0
50 NEXT

C3: on déplace la banque RAM_4 (donc les 16 premiers ko des 64 ko supérieur) en 0x4000. Même chose pour C4,C5 et C6 (voir tableau ci-dessous) C0 étant la configuration par défaut des banques mémoires:

Switch RAM:

7 6 5 4 3 2 1 0
1 1 x x x y y y
  • 2 premiers bits: 11b pour appeler la fonction 3 du Gate Array
  • 3 bits suivants: la banque 64k sélectionnée: 0 sur un CPC6128 standard
  • 3 derniers bits: la configuration de la RAM (voir plus haut)

Configuration de la RAM:

Adresse mémoire 0 1 2 3 4 5 6 7
0000-3FFF RAM_0 RAM_0 RAM_4 RAM_0 RAM_0 RAM_0 RAM_0 RAM_0
4000-7FFF RAM_1 RAM_1 RAM_5 RAM_3 RAM_4 RAM_5 RAM_6 RAM_7
8000-BFFF RAM_2 RAM_2 RAM_6 RAM_2 RAM_2 RAM_2 RAM_2 RAM_2
C000-FFFF RAM_3 RAM_7 RAM_7 RAM_7 RAM_3 RAM_3 RAM_3 RAM_3

C'est gros Puzzle Bobble ?

Maintenant que tout est chargé, je devrais disposer d'un espace suffisant pour faire tourner le jeu.

Sauf que, pour rappel, Puzzle Bobble, c'est:

  • 7 fonds d'écran représentant les jeux Taito de 16ko
  • 7 musiques pour ces écrans + 8 musiques annexes (crédit, continue, perdu, gagné, intro, hurry, pause, level100) pour une moyenne de 3ko
  • 1 sample de 4ko (Ready Go !)
  • plein d'autres graphismes pour les sprites
  • 100 niveaux de 8x10 octets (donc 8ko)
  • et beaucoup de codes Ce qui fait un total de près de 250ko.

Est-ce que cela ne ferait pas trop pour un CPC6128 ? On fait comment alors ?

On compresse !

Compression LZSA2

J'en avais déjà parlé dans un article sur Brixen dans lequel j'utilisais LZSA2. Pour Puzzle Bobble, j'ai gardé cette compression, mais suis passé en ZX0 pour nos futurs jeux. Sa compression est bien meilleure tout en étant à peine plus lente en décompression.

7f863cb132874d3f85753d9fbbc8b154

Pour les écrans, on décompresse directement en mémoire vidéo, c'est assez simple.

Pour les sprites, c'est plus compliqué... Sauf que l'on dispose de 6533 octets non utilisés par l'application (les 6533 octets utilisés par le Basic). Cette zone mémoire sera donc utilisée comme cache. Une partie pour la musique décompressée, une autre pour les sprites et le code. Le code ? Oui, j'ai compressé une certaine partie du code rarement utilisé dans la mémoire haute et la décompresse à la demande dans cette zone cache. Laquelle me demanderez-vous ? Le QR Code ! 2 ko de code inutile 99% du temps. Cela passe en mémoire haute. Chaque octet du programme principal est précieux.

Compression des 100 niveaux

Pas de LZSA2 ici... Il faudrait compresser 100 fois 80 octets. Le résultat n'est pas intéressant. Mais vu que je n'utilise que 12 bulles différentes, je peux mettre 2 bulles sur un octet (2x4 bits) et tout mettre sur 4000 octets.

Ca, c'est en théorie et ce dont je viens de me rendre compte en écrivant cet article.

En réalité, j'ai créé un outil de compression de type RLE. Les premiers 4 bits contenant les taux de répétition de la bulle et les 4 suivants contenant l'ID de la bulle. Via cette méthode, j'arrive à 4175 octets... Donc un peu plus que via la méthode simple. Bien vu...

Ma méthode me semblait tellement logique que je n'ai pas regardé s’il y avait plus simple. Cela me servira de leçon pour l'avenir.

Switch de banques

J'en avais parlé au début de cet article, pour décompresser les objets en mémoire haute, il faut faire du switch de banques. Et cela ne se fait pas de la même manière sur le CPC que sur la GX4000.

Sur CPC

En basic, c'est expliqué plus haut. En assembleur, c'est à peine plus compliqué:

bankTo4000:
    di
    ld h,#0
    ld bc,#0x7FC4   ; switch RAM to 0x4000
    add hl,bc
    push hl
    pop bc
    out (c),c
    ei
    ret

On charge le slot dans le registre L et appelons la "fonction" bankTo4000. Pour le retour dans le mapping par défaut, cela se passe via la fonction restoreBank.

restoreBank:
    di
    ld bc,#0x7FC0   ; switch RAM to default
    out (c),c
    ei
    ret

Sur GX4000

Sur GX4000, les fonctions sont un peu différentes: les slots étant en ROM, les ports à attaquer ne sont donc pas les mêmes.

bankTo4000:
    di
    ld h,#0
    ld bc,#0x7FA8+#4   ; switch ROM to 0x4000
    add hl,bc
    push hl
    pop bc
    out (c),c
    ld bc,#0x7F88      ; activation de la ROM basse
    out (c),c
    ei
    ret
restoreBank:
    di
    ld bc,#0x7F8C      ; désactivation de la ROM basse
    out (c),c
    ei
    ret

Adressage de la ROM inférieure sur ASIC:

7 6 5 4 3 2 1 0
1 0 1 1 1 x x x
  • 3 premiers bits: 101b pour sélectionner la ROM inférieure
  • 2 bits suivants: 11b connexion de la ROM inférieure en 0x4000
  • 3 derniers bits: la ROM que l'on veut connecter

Conclusion

Nous y sommes arrivés.. 250k sur une machine de 112k utilisable.

Ma méthode de compression n'est pas l'unique façon de gagner de l'espace. Il y en a d'autres et sans doute des plus optimales.

Je n'ai repris le développement CPC qu'il y a un peu plus de 2 ans et j'apprends de nouvelles choses à chaque développement. Certaines de mes optimisations peuvent paraitre naïves, mais elles étaient suffisantes dans ce cas-ci.