MSX Village forum

La Place des Développeurs Interruptions sur MSX

Metalion Membre non connecté

Conseiller Municipal

Rang

Avatar

Inscrit le : 23/12/2009 à 15h32

Messages: 1503

Le 14/01/2021 à 11h31
Je créée ici un sujet séparé, pour ne pas polluer le fil d'aoineko ...

ericb59 :
Comprendre, dans le cas de la realisation d'un jeu, comment correctement utiliser les interruptions, et aménager son code en conséquence.
Le béaba de la gestion d'interruption sur MSX ca serait déjà pas mal

Déjà il faut bien être clair sur un point : l'exécution du code par le Z80 est linéaire, c'est à dire qu'il ne poursuit qu'un seul fil d'instructions à la fois. Il n'est pas capable, comme les processeurs modernes, d'exécuter plusieurs fils en même temps. Par contre, il peut réagir à des interruptions, et aiguiller son code en fonction de celles-ci. Mais qu'est ce qu'une interruption ?

C'est quand un périphérique vient "frapper à la porte" du Z80 et lui dire : je voudrais que tu traites ma demande tout de suite ! ... Le Z80 arrête alors d'exécuter le code courant, et se branche à une adresse. Il y existe plusieurs modes de gestion des interruptions sur un Z80, mais celui qui a été choisi et paramétré sur MSX, c'est d'appeler l'adresse $0038, quel que soit le périphérique qui provoque l'interruption.

La routine en $0038 (appelée KEYINT dans le BIOS) a donc la lourde charge de gérer toutes les interruptions que le Z80 reçoit. Et elles peuvent être nombreuses (VDP, lecteur de disquettes, RS232, ...). Elle exécute un code ou appelle un hook en fonction du périphérique qui appelle.

L'interruption principale est celle qui est générée en hardware par le VDP lors du VBLANK ("Vertical BLANK" - fin de balayage de l'écran). Elle a lieu 50 fois ou 60 fois par seconde en fonction du standard vidéo du VDP (PAL: 50 fois, NTSC: 60 fois). En elle-même, elle ne sert pas à grand chose (le VDP n'a pas vraiment besoin d'informer le Z80 de la fin du balayage), mais elle a le grand avantage de donner un "coup de métronome" au système. Et le BIOS l'utilise effectivement, car le code de "base" de KEYINT permet de gérer des choses qui semblent être faites en arrière plan (c'est évidemment une illusion) : scan du clavier, incrémentation horloge, ... Sans l'interruption VBLANK du VDP, il serait impossible pour le clavier du MSX de fonctionner !

Dans le cas d'un jeu, profiter de cette interruption VBLANK est évidemment essentiel, car cela permet de profiter du "coup de métronome" pour réaliser pas mal de choses : déplacer les ennemis, faire une animation en arrière plan, jouer la musique de fond, ... Et pour profiter de cette interruption, il suffit d'utiliser le hook H.TIMI, qui est justement appelé par KEYINT à chaque interruption VBLANK, pour que KEYINT donne le contrôle à la routine que tu auras écrite.

Sur MSX2, le VDP peut générer une autre interruption, à la demande de l'utilisateur cette fois-ci. C'est une interruption qui peut avoir lieu lors du balayage à l'écran d'une ligne déterminée par le programmeur. C'est ce qu'on appelle l'interruption HBLANK ("Horizontal BLANK"). C'est comme cela que l'on réalise des "splits". Par exemple, on est en SCREEN5 des lignes 0 à 128, et en SCREEN6 des lignes 128 à 192. Pour faire ça, le programmeur a paramétré une interruption HBLANK à la ligne 128, et lors du traitement de celle-ci, il change de mode d'écran, pour le restaurer ensuite lors du VBLANK.

Voilà, ça c'est pour le B-A-BA.

;)


MSX1: Daewoo DPC-200 / Yamaha CX5M
MSX2: Sony HB-F9P
MSXVR
Vidéo: V9990 (GFX-9)
Audio: MSX-Music (FM-PAC) / MSX-Audio (Audiowave) / OPL4 (Monster Sound FM Blaster) / OPNB (Neotron)
   
ericb59 Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : compte ++ Groupe : Shoutbox

Inscrit le : 17/04/2012 à 10h25

Messages: 5566

Le 14/01/2021 à 11h36
Je comprend bien le principe, mais c'est dans la pratique que je n'arrive pas à bien faire.
Je ne sais utiliser le VDP interrupt que pour synchroniser la musique.
Comment faire d'autres choses après la musique ? Et combien de temps j'ai pour faire autre chose ?
là, j ne pige plus ... :hum

Citation :

HBLANK ("Horizontal BLANK").


Ha le HBLANK c'est pour MSX2. OK...

PS : J'aime bien la façon dont tu explique l'interruption.
Tu permets que je reprenne tes mots dans ma documentation pour Fusion-C ? ;) Edité par ericb59 Le 14/01/2021 à 11h38


banniere-ericb59e
Site web    
Metalion Membre non connecté

Conseiller Municipal

Rang

Avatar

Inscrit le : 23/12/2009 à 15h32

Messages: 1503

Le 14/01/2021 à 11h45
ericb59 :
Je comprend bien le principe, mais c'est dans la pratique que je n'arrive pas à bien faire.
Je ne sais utiliser le VDP interrupt que pour synchroniser la musique.
Comment faire d'autres choses après la musique ? Et combien de temps j'ai pour faire autre chose ?
là, j ne pige plus ... :hum

Tu fais ce que tu veux dans la routine qui est appelée par le hook.
C'est ton code qui détermine ce que tu veux faire ...

En ce qui concerne le temps, la seule contrainte est de tout boucler avant la prochaine interruption !

Et donc, dans le cas de l'interruption VBLANK, sachant que:
- 1 frame prends 70.000 cycles environ (temps entre 2 VBLANK sur un MSX PAL)
- la gestion de l'interruption par KEYINT peut prendre entre 3000 et 7000 cycles selon les modèles
tu disposes en gros de 60.000 cycles maximum.

Bien évidemment, il faut aussi que tu laisse du temps pour ton code principal ... En résumé tu disposes, pour ton code principal et pour tes interruptions, de 60.000 cycles (soit environ 7500 instructions) par frame pour faire tout ce que tu dois faire sur la frame courante.



ericb59 :
PS : J'aime bien la façon dont tu explique l'interruption.
Tu permets que je reprenne tes mots dans ma documentation pour Fusion-C ? ;)

Oui, bien sûr, pas de soucis ;) Edité par Metalion Le 14/01/2021 à 11h50


MSX1: Daewoo DPC-200 / Yamaha CX5M
MSX2: Sony HB-F9P
MSXVR
Vidéo: V9990 (GFX-9)
Audio: MSX-Music (FM-PAC) / MSX-Audio (Audiowave) / OPL4 (Monster Sound FM Blaster) / OPNB (Neotron)
   
aoineko Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : Shoutbox

Inscrit le : 02/01/2011 à 21h17

Messages: 2907

Le 14/01/2021 à 11h57
Tu peux aussi avoir une boucle de gameplay qui dure 2 frames du VDP par ex. et dans ce cas tu as beaucoup plus de temps pour ton jeu ; mais faut accepter que ton visuel ne soit mise-à-jour qu'à 25 ou 30 Hz (PAL/NTSC).

Pour tout dire, si mon jeu de foot, je suis à 1 frame gameplay pour 4 frames VDP actuellement et je vise de descendre à 3 (16/20 images-par-seconde). ^^

EDIT : ET oui, c'est une très belle explication des interruptions que tu nous as fait là Metalion :top


On est toujours ignorant avant de savoir.
Github    
Metalion Membre non connecté

Conseiller Municipal

Rang

Avatar

Inscrit le : 23/12/2009 à 15h32

Messages: 1503

Le 14/01/2021 à 12h11
aoineko :
Tu peux aussi avoir une boucle de gameplay qui dure 2 frames du VDP par ex. et dans ce cas tu as beaucoup plus de temps pour ton jeu ; mais faut accepter que ton visuel ne soit mise-à-jour qu'à 25 ou 30 Hz (PAL/NTSC).

Tout à fait.

aoineko :
Pour tout dire, si mon jeu de foot, je suis à 1 frame gameplay pour 4 frames VDP actuellement et je vise de descendre à 3 (16/20 images-par-seconde). ^^

Exactement pareil pour moi : après l'input du joueur, je génère mon écran de jeu en à peu près 3 frames VDP (donc environ 16 fps en génération continue).

Ce qui me pose un problème d'ailleurs pour la mise à jour des sprites ennemis ... Car j'ai besoin de mettre à jour leur position de façon absolument régulière (1 fois toutes les x frames, peu importe la valeur de x), sauf que pour faire cela j'ai besoin d'écrire dans la VRAM du VDP. Or mon code de génération de l'écran utilise en masse des écritures séquentielles en VRAM. Et si je protège par di/ei ces écritures séquentielles, il y a un risque réel de louper le VBLANK (ou de le décaler dans le temps), ce qui serait nuisible à la routine de lecture de musique, par exemple. Je n'ai pas encore trouvé de solution pour le moment ...
:moue

aoineko :
ET oui, c'est une très belle explication des interruptions que tu nous as fait là Metalion :top

Merci ! :oups


MSX1: Daewoo DPC-200 / Yamaha CX5M
MSX2: Sony HB-F9P
MSXVR
Vidéo: V9990 (GFX-9)
Audio: MSX-Music (FM-PAC) / MSX-Audio (Audiowave) / OPL4 (Monster Sound FM Blaster) / OPNB (Neotron)
   
ericb59 Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : compte ++ Groupe : Shoutbox

Inscrit le : 17/04/2012 à 10h25

Messages: 5566

Le 14/01/2021 à 12h37
concrètement, est-ce qu'il faut séparer les routines de "compute" et d'affichage ? (sprites, tiles...)

si oui, on fait le "compute" pendant l'interruption ou on fait l'affichage ?


banniere-ericb59e
Site web    
Metalion Membre non connecté

Conseiller Municipal

Rang

Avatar

Inscrit le : 23/12/2009 à 15h32

Messages: 1503

Le 14/01/2021 à 13h13
ericb59 :
concrètement, est-ce qu'il faut séparer les routines de "compute" et d'affichage ? (sprites, tiles...)
si oui, on fait le "compute" pendant l'interruption ou on fait l'affichage ?

Il n'y a pas de règle, c'est toi qui décide en fonction de tes besoins.
Cela dépend généralement quelle est la routine qui nécessite une mise à jour régulière ...

Une autre technique, que j'ai déjà utilisée, c'est de tout faire en dehors des interruptions, mais d'utiliser les interruptions comme "timer" (attention, cela n'est possible que si tout peut être exécuté en moins d'une frame). Concrètement, l'interruption incrémente un compteur et tu utilises ce compteur comme déclencheur.

Code :
; Boucle d'attente joueur
WAIT_PLAYER:
;-----------------------------------------------------------
; Synchro VBLANK
;-----------------------------------------------------------
        call    VBLANK_WAIT    ; attente incrémentation compteur frame [timer]
;-----------------------------------------------------------
; Cycle d'animation (64 frames)
;-----------------------------------------------------------
; Evènements déclenchés une seule fois par cycle
; frame #0 : Ascenseurs
        ld      a,[timer]                             ; 13 T
        and     a                                     ; 4 T
        call    z,ASCENSEURS                          ; 17 T (true) / 10 T (false)
; frame #16 : Laser ON
        ld      a,[timer]
        cp      16
        call    z,LASER
; frame #24 : Laser OFF
        ld      a,[timer]
        cp      24
        call    z,LASER

; Evènements déclenchés plusieurs fois par cycle
; appel variable : gravité
        ld      a,[timer]
        ld      hl,gravity
        and     [hl]
        call    z,GRAVITY
; toutes les 4 frames : Animation hélices hélicoptères
        ld      a,[timer]
        and     3
        call    z,PROPELLER
; toutes les 8 frames
(...)


Edité par Metalion Le 14/01/2021 à 13h15


MSX1: Daewoo DPC-200 / Yamaha CX5M
MSX2: Sony HB-F9P
MSXVR
Vidéo: V9990 (GFX-9)
Audio: MSX-Music (FM-PAC) / MSX-Audio (Audiowave) / OPL4 (Monster Sound FM Blaster) / OPNB (Neotron)
   
aoineko Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : Shoutbox

Inscrit le : 02/01/2011 à 21h17

Messages: 2907

Le 14/01/2021 à 13h16
ericb59 :
concrètement, est-ce qu'il faut séparer les routines de "compute" et d'affichage ? (sprites, tiles...)
si oui, on fait le "compute" pendant l'interruption ou on fait l'affichage ?


Personnellement, je recommanderai de faire le moins de chose possible dans le hook (appelé par les interruptions).
Pour le V-Blank, je fais juste des set de variables qui seront traiter dans la boucle principale (comme ça l'interruption est très rapide).
Pour le H-Blank, c'est plus compliqué vu qu'il faut que le code s'exécute tout de suite.
Donc là, pas le choix, faut mettre son code dans le hook.

Pour prendre l'exemple de mon projet GOS, voici ce que font chacune des interruptions :

V-Blank :
- Dans la fonction appelé par l'interruption H.TIMI, je set une variable g_VBlank à 1 et j'incrémente un compte g_Frame (que j'utilise comme index pour mes animations par ex.).
- A la fin de mon code gameplay, je boucle tant que g_VBlank n'est pas égale à 1 et une fois que c'est le cas (donc que l'interruption du V-Blank à eu lieu) je remets cette variable à 0 et je commence une nouvelle boucle de gameplay.

En vrai, ça c'est dans le cas ou tu as 1 frame gameplay = 1 frame VDP. Dans mon cas c'est un peu plus compliqué, mais je suis pas sûr que ça vaille la peine de rentrer dans le détail.

H-Blank :
- En fait, j'utilise un scrolling hardware au delà de la limite de 256 lignes normalement autorisé. Par défaut, ça boucle avec le début de la VRAM, mais j'utilise le H-Blank pour qu'à la ligne ou c'est censé bouclé, le VDP se mette à pointer vers la suite de la VRAM. En gros, je change la page affiché au cours de l'affichage. Ca me permet d'avoir un scrolling quasiment gratos sur 384 px de haut (la taille de mon terrain de foot).
- Du coup, lors du V-Blank (au début de ma boucle de gameplay), je set la page affichée sur 0 par ex., et je set le VDP pour déclencher une interruption H-Blank à la ligne souhaité (en gros, 384 - la valeur du registre de scrolling).
- Dans le H.KEYI (point d'entrée de toutes les interruptions coté programme), je check si une interruption H-Blank a eu lieu (via le registre S#1) et si c'est le cas, je lance ma fonction de gestion du H-Blank
- Dans cette fonction, je set juste la page affiché à 1 et voilà, le tour est joué.

Bon, encore une fois, c'est un peu plus compliqué que ça car comme ma frame gameplay dure plusieurs frame VDP, le H-Blank à lieu plusieurs fois. Sans parler du fait que j'utilise du double-buffering (j'affiche une page pendant que j'écris dans l'autre). Mais bon, l'idée y est. :)


On est toujours ignorant avant de savoir.
Github    
ericb59 Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : compte ++ Groupe : Shoutbox

Inscrit le : 17/04/2012 à 10h25

Messages: 5566

Le 14/01/2021 à 15h29
ok, merci à tous les 2, j'y vois plus clair. ^^ :top


Dans le cas d'utilisation de fusion-c, toutes mes routines graphique, ainsi que les routines qui font appel au Bios, désactivent les interruptions quand il y a besoin, par exemple pour le VDP, et finissent toutes par un EI.
Du coup, on est d'accord qu'utiliser ces fonctions une fois branché sur la procédure pointée par H.TIMI va finir pas planter ?


Question subsidiaire. Il me semble avoir lu quelque part, que quand on à fait un appel à une fonction BIOS, via un Call interslot depuis MSXDOS, ou via un Call normal depuis une ROM, il fallait rendre la main par :
ei
ret

Est-ce vraiment nécessaire de faire un EI après un Call d'une fonction Bios ?




banniere-ericb59e
Site web    
Metalion Membre non connecté

Conseiller Municipal

Rang

Avatar

Inscrit le : 23/12/2009 à 15h32

Messages: 1503

Le 14/01/2021 à 16h56
ericb59 :
Dans le cas d'utilisation de fusion-c, toutes mes routines graphique, ainsi que les routines qui font appel au Bios, désactivent les interruptions quand il y a besoin, par exemple pour le VDP, et finissent toutes par un EI.
Du coup, on est d'accord qu'utiliser ces fonctions une fois branché sur la procédure pointée par H.TIMI va finir pas planter ?

Non, pas du tout ... DI/EI sont juste là pour protéger un bout de code contre une interruption éventuelle. Lorsqu'une fonction est appelée pendant une interruption, DI/EI n'auront en pratique aucun effet. Sauf peut-être dans le cas extrême où les routines appelées dépasseraient le temps complet d'une frame, et alors elles protègent simplement d'une nouvelle interruption. Pas de plantage.

Pour info, comme bonne pratique, 'ei' peut être placé une instruction avant la fin réelle de la protection. Car le Z80, en pratique, ne ré-autorise les interruptions qu'après l'instruction suivante. Et donc :

Code :
    ei
    out    (c),a
    ret

au lieu de :
Code :
    out    (c),a
    ei
    ret


ericb59 :
Est-ce vraiment nécessaire de faire un EI après un Call d'une fonction Bios ?

Je ne peut pas te répondre pour un call interslot, mais en ce qui concerne un call 'normal' d'une fonction BIOS, le 'ei' n'est pas nécessaire.


MSX1: Daewoo DPC-200 / Yamaha CX5M
MSX2: Sony HB-F9P
MSXVR
Vidéo: V9990 (GFX-9)
Audio: MSX-Music (FM-PAC) / MSX-Audio (Audiowave) / OPL4 (Monster Sound FM Blaster) / OPNB (Neotron)
   
aoineko Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : Shoutbox

Inscrit le : 02/01/2011 à 21h17

Messages: 2907

Le 14/01/2021 à 17h08
De ce que j'ai lu, le EI est nécessaire pour certaines fonctions du Bios qui font un DI mais "oublie" de faire le EI.
CALSLT est bien l'une de ces fonctions mais il y en a d'autres il me semble.

Et tant qu'on est dans les bizarreries du Bios, il est demandé de sauver / restaurer le registre A dans la routine appelé par H.TIMI car la suite du Bios s'attends à trouver dans A la valeur du registre S#0 récupéré juste avant le H.TIMI.
Je ne sais pas dans quel cas c'est vraiment nécessaire (ça marche très bien sans), mais c'est demandé en tout cas. ^^


On est toujours ignorant avant de savoir.
Github    
Metalion Membre non connecté

Conseiller Municipal

Rang

Avatar

Inscrit le : 23/12/2009 à 15h32

Messages: 1503

Le 14/01/2021 à 18h44
aoineko :
Et tant qu'on est dans les bizarreries du Bios, il est demandé de sauver / restaurer le registre A dans la routine appelé par H.TIMI car la suite du Bios s'attends à trouver dans A la valeur du registre S#0 récupéré juste avant le H.TIMI.
Je ne sais pas dans quel cas c'est vraiment nécessaire (ça marche très bien sans), mais c'est demandé en tout cas. ^^

C'est effectivement nécessaire, car la valeur de A est stockée au retour de H.TIMI dans (STATFL), qui est le miroir de la valeur du registre de statut du VDP dans la RAM. C'est vrai qu'un PUSH AF/POP AF n'aurait pas fait de mal ...

Code :
        CALL    H.KEYI
        IN      A,(VDP.SR)
        AND     A
        JP      P,INTRET
        CALL    H.TIMI
        EI
        LD      (STATFL),A


MSX1: Daewoo DPC-200 / Yamaha CX5M
MSX2: Sony HB-F9P
MSXVR
Vidéo: V9990 (GFX-9)
Audio: MSX-Music (FM-PAC) / MSX-Audio (Audiowave) / OPL4 (Monster Sound FM Blaster) / OPNB (Neotron)
   
ericb59 Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : compte ++ Groupe : Shoutbox

Inscrit le : 17/04/2012 à 10h25

Messages: 5566

Le 14/01/2021 à 19h03
Je protège la routine cible du hook comme ceci, est-ce bien ?
Code C :
 
 
void routing(void) __critical __interrupt(0)
__preserves_regs(a,b,c,d,e,h,l,iyl,iyh)
{
  vdp_status = VDP_port2;
  if (GameOn==1)
  {
      Timer();
  }
          PT3Rout();          // Route the PT3 music to PSGPT3Rout
 
          PT3Play();      // Play/Update the music playing
 
         PT3FXRout();  
 
}
 
 


banniere-ericb59e
Site web    
aoineko Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : Shoutbox

Inscrit le : 02/01/2011 à 21h17

Messages: 2907

Le 14/01/2021 à 20h52
Je te conseil de toujours regarder l'assembleur généré ; il est souvent plus parlant que la doc. ;)

Prenons les 4 fonctions suivantes :

Code C :
void test(void) { g_ModeIndex++; }
void testC(void) __critical { g_ModeIndex++; }
void testI(void) __interrupt(0) { g_ModeIndex++; }
void testP(void) __preserves_regs(a,b,c,d,e,h,l,iyl,iyh) { g_ModeIndex++; }



Voici le code généré :

Code ASM :
; void test(void) { g_ModeIndex++; }
_test::
    ld    hl, #_g_ModeIndex+0
    inc    (hl)
    ret

Ici, rien de spécial. On notera que pour une fois, SDCC à créé un code assembleur propre. :)

Code ASM :
; void testC(void) __critical { g_ModeIndex++; }
_testC::
    ld    a,i
    di
    push    af
    ld    hl, #_g_ModeIndex+0
    inc    (hl)
    pop    af
    ret    PO
    ei
    ret

__critical permet donc de désactiver les interruptions le temps de la fonction. Ca récupère aussi la valeur du registre d'interruption I qui ne sert qu'en mode IM2 (donc pas le mode d'interruption par défaut du MSX). Je sais pas pourquoi il fait ret PO ni pourquoi il push/pop AF, mais tu as besoin de tout ça en tout cas. ^^

Code ASM :
; void testI(void) __interrupt(0) { g_ModeIndex++; }
_testI::
    ei
    push    af
    push    bc
    push    de
    push    hl
    push    iy
    ld    hl, #_g_ModeIndex+0
    inc    (hl)
    pop    iy
    pop    hl
    pop    de
    pop    bc
    pop    af
    reti

__interrupt permet 2 choses : il sauvegarde/restaure presque tous les registres (il manque IX et surtout tous les registres miroirs) et fait un RETI au lieu d'un RET. C'est donc pour gérer une fonction d'interruption comme celle en 38h, mais pas pour un hook qui lui est appelé par l'interruption et qui n'a donc ni besoin de restaurer les registres, ni faire un RETI.

Code ASM :
void testP(void) __preserves_regs(a,b,c,d,e,h,l,iyl,iyh) { g_ModeIndex++; }
_testP::
    ld    hl, #_g_ModeIndex+0
    inc    (hl)
    ret


On pourrait croire que __preserves_regs ne fait rien, mais ce n'est pas le cas. On regarde juste au mauvais endroits. Cette directive dit simplement au compilateur quels registres ne sont pas modifiés, ce qui lui permet de faire des optimisations dans le code appelant.

Pour résumé, une fonction de hook H.TIMI en C ne devrait avoir aucune de ces directives. Tu peux juste ajouter un __asm("push AF"); au début et un __asm("pop AF"); à la fin et ta fonction est prête pour être appelé par le code d'interruption.


On est toujours ignorant avant de savoir.
Github    
ericb59 Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : compte ++ Groupe : Shoutbox

Inscrit le : 17/04/2012 à 10h25

Messages: 5566

Le 15/01/2021 à 07h13
J'ai lu quelque part (je peux sans doute retrouver)
que pour un bon fonctionnement la bonne pratique pour la routine appelée par le hook était:

1 - Push tout les registres
2 - Di

3 - notre code

4 - Ei
5 - pop tout les registreS

c'est pour cela que j'avais mis ces directictives
:hum


banniere-ericb59e
Site web    
Répondre
Vous n'êtes pas autorisé à écrire dans cette catégorie