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 =
===============================================================================


