Page 1 sur 3

Son Son II (maps + analyse technique)

Publié : lun. 22 mars 2010 18:54
par Tanuki
Lorsque j'ai un peu de temps, je me plonge dans le code du jeu Son Son II, car je voudrais savoir comment un jeu comme celui-ci fonctionne de A à Z.

J'ai déjà trouvé quelques trucs intéressants.

Et voici pour commencer:

Code : Tout sélectionner

===============================================================================
= Son Son II (Task Manager)                                                   =
= 16/04/10                                                                    =
= par  Tanuki                                                                 =
= pour Necstasy                                                               =
===============================================================================

- Sommaire -
------------

1)  Introduction
2)  Gestion de la pile
3)  Le statut des tâches
4)  Le gestionnaire
5)  Enregistrement d'une tâche
6)  La boucle du gestionnaire
7)  Temporiser une tâche
8)  Le rôle de IRQ1
9)  La reprise d'une tâche
10) Arrêt d'une tâche
11) Destruction d'une tâche
12) Fonctions prédéfinies
13) Schéma d'exécution d'une tâche
14) Derniers détails

1) Introduction
---------------

Lorsque le jeu Son Son II exécute un reset, ce dernier met à zéro la RAM,
initialise le hardware, et lance une espèce de gestionnaire de tâche.
C'est à partir de ce dernier que tout le jeu va s'articuler.

Il peut prendre en charge jusqu'à 4 tâches:

  t0, t1, t2 et t3 avec une priorité tel que t0 > t1 > t2 > t3.

Lorsqu'une tâche s'exécute, toutes les autres sont en attente, jusqu'à ce que
cette dernière ait rendu la main.

2) Gestion de la pile
---------------------

La pile est ségmentée en 5 zones:

  1 pour le gestionnaire
  1 pour chacune des tâches (x4)

Ce qui nous donne la disposition suivante:

   +--------------+ - $FF
   | task manager |
   |              |
   +--------------+ - $C0      la répartition est un peu près de l'odre de:
   |      t3      |
   |              |
   +--------------+ - $90      task manager area = 256 / 4        = 64 bytes
   |      t2      |            task area (tx)    = (256 - 64) / 4 = 48 bytes
   |              |
   +--------------+ - $60            
   |      t1      |
   |              |
   +--------------+ - $30
   |      t0      |
   |              |
   +--------------+

Lorsque le gestionnaire ou une tâche prend la main, le pointeur de pile est
ramené dans la zone correspondante.

3) Le statut des tâches
-----------------------

Le gestionnaire utilise 4 octets d'information par tâche, qui sont logés
au tout début de la RAM:

        +----------+            +---------+
  $2000 |    t0    |     ->     | status  | n + 0
     .  |          |            +---------+
     .  +----------+            | counter | n + 1
  $2004 |    t1    |            +---------+
     .  |          |            |    -    | n + 2
     .  +----------+            +---------+
  $2008 |    t2    |            |    -    | n + 3
     .  |          |            +---------+
     .  +----------+
  $200C |    t3    |
     .  |          |            n = task index x 4
     .  +----------+

Pour chacune des tâches:

  (n + 0) est le statut
  (n + 1) est un compteur de frames
  (n + 2) et (n + 3) ont une utilité variable

Les différents statuts sont les suivants:

   +-----+---------------------------------+
   | id  | statut                          |
   +-----+---------------------------------+
   | $00 | aucune tâche disponible         |
   | $01 | tâche en cours de temporisation |
   | $02 | tâche en cours d'exécution      |
   | $04 | tâche à reprendre               |
   | $08 | tâche à lancer                  |
   +-----+---------------------------------+

4) Le gestionnaire
------------------

Le gestionnaire est composé d'une boucle, et de quelques fonctions spéciales.
L'ensemble se trouve dans la banque $00 avec MPR7= $00.

Avant de continuer, voici quelques adresses utilisées:

  $2010: vecteur de tâche (lsb)
  $2011: vecteur de tâche (msb)
  $2013: indique que IRQ1 à parcouru les compteurs de tâche
  $2015: index de la tâche en cours d'exécution

5) Enregistrement d'une tâche
-----------------------------

Avant de lancer le gestionnaire, il faut au moins enregistrer une tâche.
Le jeu enregistre les tâches t0 et t3 pour commencer.

L'enregistrement se fait en écrivant le vecteur de la tâche à l'adresse
2010/2011 puis en plaçant sont index dans la registre A.

Ensuite on appel la fonction $E1E2 (qui produit le traitement suivant):

        n <- A x 4

        +-------+
        | n + 0 | <- $08
        +-------+
        | n + 1 |
        +-------+
        | n + 2 | <- $2010 (vector lsb)
        +-------+
        | n + 3 | <- $2011 (vector msb)
        +-------+

Une fois que l'on a au moins une tâche, on se branche sur la boucle du
gestionnaire:

        JMP $E1A5

6) La boucle du gestionnaire
----------------------------

Avant que la boucle commence, le gestionnaire place le pointeur de pile
dans sa zone:

        S <- $FF

Puis la boucle commence, en évaluant le statut des tâches dans l'odre de
priorité, de façon continue (t0, t1, t2, t3, t0, t1, t2, ...).

La boucle ne produit que des actions en rapport au statuts $08 et $04, et
ignore les autres.

Lorsque la boucle détecte notre tâche (statut = $08), cette dernière
sauvegarde l'index courant, puis se branche sur notre code.

           $2015 <- task index

           n <- task index x 4

           +-------+
           | n + 0 | <- $02
           +-------+
           | n + 1 | <- $00
           +-------+
  $2010 <- | n + 2 | (vector lsb)
           +-------+
  $2011 <- | n + 3 | (vector msb)
           +-------+

           JMP($2010) 'lancement de notre tâche'

Note:
-----

  Une tâche doit commencer en faisant 2 choses:

   - initialiser le pointeur de pile en fonction de son index:
     ($30, $60, $90, $C0)

   - faire un CLI (pour que IRQ1 ne soit pas masqué)*

  (*) Son Son II n'utilise pas TIRQ.

7) Temporiser une tâche
-----------------------

A un moment ou un autre, le code d'une tâche aura besoin d'attendre une
certaine quantité de temps avant de continuer son traitement.

Il lui suffit de placer dans le registre A le nombre de frames à attendre,
puis d'appeler la fonction $E1FE (qui produit le traitement suivant):

        push A
        push X
        push Y

        task index <- $2015 *

        n <- task index x 4

        +-------+
        | n + 0 | <- $01
        +-------+
        | n + 1 | <- frames (A)
        +-------+
        | n + 2 | <- stack pointer (S)
        +-------+
        | n + 3 |
        +-------+

        JMP $E1A5 'retour à la boucle du gestionnaire'

(*) L'index est récupèré là où le gestionnaire l'avait laissé, et permet
     de retouver la zone d'information qui lui est attribuée.

Explication:
------------

  Lors de l'appel de la fonction, PC est poussé sur la pile (notre adresse
  de retour), ensuite la fonction n'a plus qu'a empiler les registres A, X,
  et Y, ce qui à pour but de préserver le contexte d'exécution de notre tâche.
  Ensuite il suffit juste de conserver le pointeur de pile.

      +-----+ - call
      | PCH |
      +-----+
      | PCL |
      +-----+
      |  A  |
      +-----+
      |  X  |
      +-----+
      |  Y  |
      +-----+ - stack pointer

  Quand on se rebranche sur le gestionnaire, ce dernier replace le pointeur
  de pile à $FF, et ainsi va préserver la zone de pile de notre tâche.

8) Le rôle de IRQ1
------------------

Lors de l'initialisation hardware, le VDC est configuré pour produire une
interruption à chaque période de suppression d'image (VBLANK), ce qui se
produit tout les 1/60 de secondes.

Dans la routine d'interruption, il y a un petit morceau de code qui parcours
le statut des 4 tâches pour chercher celles qui ont le code $01, si il en
existe une, il décrémente le compteur qui lui est attaché, et si il est égale
à zéro, change le statut à $04 pour dire: c'est OK pour celle-ci !.

C'est comme ça que sont temporisées chacune des tâches.

Le code écrit aussi une valeur non nulle dans la variable $2013, avant de
commencer à parcourir les compteurs, pour forcer la boucle du gestionnaire à
recommencer à évaluer les tâches à partir de t0.

  $E0B9: IRQ1
  $E14B: code en question

9) La reprise d'une tâche
-------------------------

Une fois que le compteur de notre tâche est à zéro, et que le gestionnaire
est de nouveau dans sa boucle (c'est à dire qu'aucune autre tâche est en
cours), il va trouver notre statut de $04, et donc récupèrer notre pointeur
de pile, réstituer nos registres, et enfin faire un RTS.

           $2015 <- task index

           n <- task index x 4

           +-------+
           | n + 0 | <- $02
           +-------+
           | n + 1 | <- $00
           +-------+
  stack <- | n + 2 |
  pointer  +-------+
           | n + 3 |
           +-------+

           pull Y
           pull X
           pull A

           RTS 'retour à notre tâche'

10) Arrêt d'une tâche
---------------------

Une tâche ayant achevé son job peut s'auto-terminer en appelant la fonction
$E212 (qui produit le traitement suivant):

        task index <- $2015

        n <- task index x 4

        +-------+
        | n + 0 | <- $00
        +-------+
        | n + 1 |
        +-------+
        | n + 2 |
        +-------+
        | n + 3 |
        +-------+

        JMP $E1A5 'retour à la boucle du gestionnaire'

11) Destruction d'une tâche
---------------------------

Il reste une fonction spécial, celle qui détruit une tâche.

Il suffit de placer dans le registre A l'index de la tâche en question puis
d'appeler la fonction $E1F3 (qui produit le traitement suivant):

        n <- A x 4

        +-------+
        | n + 0 | <- $00
        +-------+
        | n + 1 | <- $00
        +-------+
        | n + 2 |
        +-------+
        | n + 3 |
        +-------+

        JMP $E1A5 'retour à la boucle du gestionnaire'

12) Fonctions prédéfinies
-------------------------

Dans la pratique, pour réduire la taille du code, les tâches se temporisent
en appelant une série de fonctions prédéfinies correspondant chacune à un
nombre précis de frames (pour MPR5= $04):

   +----------+--------+ +----------+--------+
   | function | frames | | function | frames |
   +----------+--------+ +----------+--------+
   |  $B9B8   |    1   | |  $B9D0   |   24   |
   |  $B9BC   |    2   | |  $B9D4   |   30   |
   |  $B9C0   |    3   | |  $B9D8   |   60   |
   |  $B9C4   |    6   | |  $B9DC   | A x  6 |
   |  $B9C8   |   12   | |  $B9E4   | A x 60 |
   |  $B9CC   |   18   | |          |        |
   +----------+--------+ +----------+--------+

13) Schéma d'exécution d'une tâche
----------------------------------

   +---------+                 +-----------+              +-------------+
   |  task   |>---[JMP/RTS]--->| task (tx) |>--->[JSR]--->| SP function |
   | manager |                 +-----------+              +-------------+
   +---------+                                                   |
        ^                                                        |
        |                                                        |
        +-----------------------<[JMP]<--------------------------+

14) Derniers détails
--------------------

Une tâche en attente peut-être écrasée par l'enregistrement d'une autre
par dessus.

Lors du reset, le pointeur de pile est placé dans l'espace de la tâche t0
avant de commencer quoi que ce soit (S= $30).

===============================================================================
= EOF                                                                         =
===============================================================================

Re: Son Son II (tech)

Publié : lun. 22 mars 2010 19:00
par touko
Sacré boulot d'analyse, félicitations ..

Re: Son Son II (tech)

Publié : lun. 22 mars 2010 19:12
par Tanuki
Merci à toi, mais fait attention, je viens juste de corriger une boulette que j'avais fait.
(maintenant c'est tout bon). :mrgreen:

Re: Son Son II (tech)

Publié : lun. 22 mars 2010 19:34
par touko
et au lieu de gestionnaire de taches, je dirai plus gestionnaire d'événements ..

Re: Son Son II (tech)

Publié : lun. 22 mars 2010 20:03
par Tanuki
touko a écrit :et au lieu de gestionnaire de taches, je dirai plus gestionnaire d'événements ..
Honnêtement, à ce stade de mon étude, je n'ai pas vraiment vu comment il était exploité.
Mais tu as peut-être raison. :wink:

Re: Son Son II (tech)

Publié : lun. 22 mars 2010 20:10
par MooZ
Eh beh! Beau boulot :)

Re: Son Son II (tech)

Publié : lun. 22 mars 2010 20:21
par Tanuki
Ah oui, juste une chose.

Si quelqu'un trouve une erreur dans la doc, qu'il regarde si je ne l'ai pas déjà corrigée avant de me mettre la honte. :wink:

Re: Son Son II (tech)

Publié : jeu. 25 mars 2010 18:22
par Tanuki
Et voilà la deuxième tournée (après il faudra attendre un peu):

Code : Tout sélectionner

===============================================================================
= Son Son II (BG tiles)                                                       =
= 27/03/10                                                                    =
= par  Tanuki                                                                 =
= pour Necstasy                                                               =
===============================================================================

- Sommaire -
-------------

1) Introduction
2) BG tile area
3) Disposition d'un bloc dans la ROM
4) Capacité de stockage d'une banque
5) Mapping pour le chargement
6) La table de chargement
7) La fonction de chargement
8) Derniers petits détails

1) Introduction
---------------

Le jeu Son Son II n'utilise que des graphismes en 3-bpp (BG tiles & sprites),
ce qui implique que toutes les palettes sont de 8 couleurs.

Ici je ne parlerai que des caractères d'arrière-plan.

La BAT est construite à partir de 2 éléments de base:

  - les tiles (8x8 pixels)
  - les blocs (groupement logique de 2x2 tiles)

2) BG tile area
---------------

Tous les tiles utilisés par la BAT sont rangés dans la VRAM, dans une zone
située à l'adresse $1800, d'une taille fixe de 20KB.

Cette zone possède une provision de 640 tiles (tous utilisés).

Comme la BAT utilise de nombreux blocs, pour faciliter le transfert en VRAM,
tous les tiles (même ceux individuels) sont regoupés en blocs:

c'est l'unité de stockage dans la ROM et de tranfert en VRAM.

Au final, pour pouvoir ranger et utiliser les blocs en VRAM correctement,
il faut gérer cette zone comme un espace 2D:

il y a ainsi 8 blocs par ligne et notre zone obtient donc une dimension
logique de 8 x 20 blocs (soit 16 x 40 tiles).

     +----+----+----+----+----+
     | 00 | 01 | .. | 06 | 07 |
     +----+----+----+----+----+
     | 08 | 09 | .. | 0E | 0F |    disposition logique des 160 blocs
     +----+----+----+----+----+              de la zone
     | 10 | .. |    | .. | 17 |
     +----+----+----+----+----+
     | .. |    |    |    | .. |
     +----+----+----+----+----+
     | 88 | .. |    | .. | 8F |
     +----+----+----+----+----+
     | 90 | 91 | .. | 96 | 97 |
     +----+----+----+----+----+
     | 98 | 99 | .. | 9E | 9F |
     +----+----+----+----+----+

Les tiles d'un bloc dans cet espace ont la relation suivante:

     +-------+-------+
     |       |       |
     |n + $00|n + $01|
     |       |       |        n = tile number
     +-------+-------+
     |       |       |
     |n + $10|n + $11|
     |       |       |
     +-------+-------+

3) Disposition d'un bloc dans la ROM
------------------------------------

D'abord, le regroupement des 4 tiles d'un bloc:

dans la ROM, les tiles composant le bloc sont rangés les uns à la suite des
autres, dans l'ordre suivant: A, B, C, D.

     +---+---+
     | A | B |
     +---+---+
     | C | D |
     +---+---+

Puis le format d'un tile:

Il y a en premier les 16 bytes des plans binaires CH0 & CH1, puis ensuite
seulement les 8 bytes du plan CH2.

            +-------+
            |       |
   16 bytes |  CG0  | (CH0/CH1)             1 tile = 24 bytes
            |       |                       1 bloc = 96 bytes
            +-------+
            |  CG1  | (CH2 seulement)
    8 bytes |       |
            +-------+

4) Capacité de stockage d'une banque
------------------------------------

Une banque peut contenir 85 blocs (donc 340 tiles), c'est le cas par exemple
de la banque $18.

Ceci vient du fait que chaque tile n'utilise que 3 bits sur 4 pour chaque
pixel, ce qui donne un gain systématique de:

   1 - 3/4 = 25%

Avec des tiles 4-bpp nous aurions:

    8192
  -------- = 64 blocs
   4 x 32

Alors que dans notre cas, nous avons:

     64
   ------ ~= 85 blocs
    0.75

5) Mapping pour le chargement
-----------------------------

  MPR5= $04 - contient la fonction $BA4C utilisée par la routine de chargement.

  MPR3= $02 - contient la fonction de chargement

  MPR2= $01 - contient la table de chargement

  MPR6= $XX - sert à mapper les banques de tiles

La fonction $BA4C remplit la simple tâche de sélectionner le registre du VDC
à utiliser, à partir de son index contenu dans le registre A.

6) La table de chargement
-------------------------

Le jeu Son Son II est réparti en 7 stages, et tous les morceaux de code qui
doivent charger des ressources spécifiques en rapport avec l'un d'eux,
utilisent un index situé à l'adresse $2069.

Les valeurs $00 à $06 représentent les stages 1 à 7 respectivement.

La table de chargement (située à l'adresse $548F), débute avec les adresses
de départ des 7 segments qui la composent (en raison d'un par stage).

Il suffit à notre fonction de chargement (qui connaît l'index), de prendre
la bonne adresse puis de pointer sur le segment en question.

          +-----------+
    $548F |   adr#0   |
          |   adr#1   | 7 words
          |    ...    |
          |   adr#6   |
          +-----------+
    adr#0 |           |
          | segment 0 |
          |           |
          +-----------+
    adr#1 |           |
          | segment 1 |
          |           |
          +-----------+
          |    ...    |
          +-----------+
    adr#6 |           |
          | segment 6 |
          |           |
          +-----------+

Chaque segment est constitué d'un certain nombre de données de séquence
dont le format est le suivant:

   +-------+----------------------------------------+
   | bytes | usage                                  |
   +-------+----------------------------------------+
   |   1   | banque à mapper avec MPR6              |
   |   1   | nombre de blocs à charger              |
   |   2   | adresse du premier bloc dans la banque |
   +-------+----------------------------------------+

Notes:

  Losque la banque d'une des séquences à la valeur $FF, ceci sert à indiquer
  que l'on a terminé la phase de chargement.

  Chaque segment charge exactement 160 blocs.

7) La fonction de chargement
----------------------------

La fonction $7882:

  - récupère l'index de stage $2069
  - crée un pointeur sur un des segments de la table
  - initialise le pointeur de la VRAM à $1800
  - charge les blocs en relation avec les données de la table.

Pseudo-code de la boucle:

  > lecture de de la banque à utiliser

  si(banque = $FF)
   > la fonction prend fin

  sinon
   [
    > mapping de la banque avec MPR6
    > lecture du nombre de blocs
    > lecture de l'adresse du premier bloc

    > écriture des n blocs dans la VRAM
   ]

Notes:

  Les blocs sont insèrés de gauche à droite, et de haut en bas
   (comme indiqué par le schéma de la section 2).

  Pour chaque tile, des zéros sont écrits pour complèter le plan binaire
  manquant (CH3).

8) Derniers petits détails
--------------------------

Il existe d'autres tiles (tels que ceux de l'écran titre), mais le principe
reste exactement le même que celui qui a été décrit précédemment.

Le tout premier tile porte le numèro $180.

Les caractères alpha-numériques sont rangés de façon que:

  char = $180 + ASCII code

Le tile 'empty' porte le numéro $1A0, ce qui correspond également au
caractère ASCII $20 (space).

===============================================================================
= EOF                                                                         =
===============================================================================

Re: Son Son II (tech)

Publié : jeu. 25 mars 2010 18:46
par Tanuki
Voici une image montrant le mapping des tiles pour chaque stage:

http://rapidshare.com/files/368488786/s ... t.gif.html (over)

(J'utilise une palette factisse de 8 gris, mais ça reste regardable). :)

Re: Son Son II (tech)

Publié : jeu. 25 mars 2010 20:04
par Tanuki
- ça y est, le lien fonctionne ! (J'ai eu quelques problèmes) :D

Re: Son Son II (tech)

Publié : jeu. 25 mars 2010 20:16
par shubibiman
Sacré boulot, merci encore :D

Re: Son Son II (tech)

Publié : jeu. 25 mars 2010 20:30
par Tanuki
shubibiman a écrit :Sacré boulot, merci encore :D
De rien, c'est juste ma grosse passion. :)

Re: Son Son II (tech)

Publié : sam. 27 mars 2010 02:19
par peperocket
Meric de la partager !!

Re: Son Son II (tech)

Publié : dim. 28 mars 2010 20:52
par Emperor Udan
Je plussois aussi.
Je le jure solennellement, si je gagne l'euromillion, tu es engagé si tu créé une boite de dev sur PCE. :mrgreen:

Re: Son Son II (tech)

Publié : mar. 13 avr. 2010 16:06
par Tanuki
J'ai généré les macro-blocs et les maps de chaque stage du jeu en pcx.

Pour les détails techniques il faudra attendre que j'ai fait un peu plus le tour de la question (ce qui va certainement me prendre du temps) pour ensuite écrire la doc. :wink:

http://rapidshare.com/files/375423820/s ... s.zip.html (over)