HOME INDEX LINK CONTACT
(En construction et
susceptible de contenir (beaucoup) d’erreur…)
ATTENTION, ce qui
suit sont mes notes concernant la programmation de la PLAYSTATION en très
bas-niveau : sans aucun SDK et autres librairies (seul les fonctions de la
ROM seront utilisées).
Si vous cherchez
seulement à programmer la PSX avec du C/C++ et le SDK, dirigez-vous à cette
adresse :
Vous trouverez
tous ce qu’il vous faut : SDK comprenant les bibliothèques, compilateur
C/C++/MIPS3000, documentations et tutoriels J
Si vous souhaitez
attaquer la Playstation en bas-niveau alors vous pouvez continuer à lire cette
page et comme toujours vous méfiez de ce que je raconte et plus encore pour
cette console. Certains passages sont des copier/coller de mes références,
d’autre sont des remarques diverses sans lien entre eux.
Néanmoins je
pense que cette section donne tous les « trucs » de base et vous permettra
de gagner pas mal de temps (en tout cas je l’espère).
Si vous chercher
un compilateur MIPS ou autres outils que vous avez la flemme de les développez,
vous trouverez cette adresse : http://hitmen.c02.at/html/psx_tools.html
se trouve un assembleur SPASM avec une syntaxe type MOS/M68000 et un
compilateur MIPSGNU.
Le SDK sur psxdev
contient également un assembleur (aspsx/asmpsx).
Vous pouvez
éventuellement utiliser mon assembleur écrit en JAVA: ZawaMIPSX.
Il n’est pas encore terminé, ni très évolué et la syntaxe est un petit peu
spécifique concernant les macros (ou plutôt leur absence) et directives. Il ne
gère seulement que le segment .Text et produit directement un exécutable psx.
Néanmoins, dans
ces articles ainsi que les programmes d’exemple on utilisera ZawaMIPSX.
Comme avec
l’AMIGA, je pars toujours du principe que l’on n’a rien (ni
SDK/compilateur/linker) et que l’on a envie éventuellement de programmer les
outils plus aptes à répondre à nos besoins.
C’est juste un
exercice de style pour montrer que nous n’avons pas besoin de SDK pour
développer mais seulement de connaissances.
D’ailleurs je ne
comprends absolument pas comment peut-on retirer une réelle satisfaction en
programmant cette architecture complexe avec le SDK et les bibliothèques.
Ne seront pas
aborder dans cette partie :
- Le MDCP pour le
décodage de bloc type JPEG
- Le lecteur
CDROM que nous contrôlerons par les fonctions du bios pour la lecture de
fichiers.
- La programmation
directe des contrôleurs PAD que nous contrôlerons par les fonctions du
bios.
- Les ports de
carte mémoire que nous contrôlerons par les fonctions du bios.
- Le décodeur du
lecteur CD/ROM pour le son et la reverb unit du SPU.
- Les ports externes
(PIO et SIO).
Contrairement à
l’Amiga, je documente maintenant systématiquement ce que je fais avec la PSX.
Donc des articles plus spécifiques sur le GTE, le SPU et le CD/ROM sont en
cours également.
Vous trouverez
ici les listings complets d’une série progressive de programmes en rapport avec
cet article et compilables avec ZawaMIPSX ainsi que quelques fichiers media
utilisés dans les exemples.
References :
-
Sony
Developer CD
-
Service
Manual Playstation 75xx
1-
Introduction
Console (et
particulierement les consoles de 5/6eme generation) oblige, la Playstation
possède une architecture très exotique.
La PSX est bâtit
autour d’un circuit à intégration multiple fabriqué par SONY et LSI. Ce circuit
(IC103 CXD8606 que nous appellerons R3000A) intègre un processeur MIPS R3000
(plus proche du R3051) 32 bits de type RISC cadencé à 33MHZ munit de 4Ko de
cache d’instructions. Le processeur est customisé pour les besoins de la PSX
avec une SRAM de 1Ko qui remplace le D-cache et l’amputation du COP1 pour le
calcul des flottants. Le coprocesseur système (COP0) est quasi identique au
standard en revanche, le coprocesseur de calcul vectoriel (COP2 ou GTE) est lui
spécifique. La puce contient également un décodeur de bloc DCT type JPEG (MDCP)
et, semble-t-il, un autre composant hybride contenant les compteurs internes (3
compteurs) et faisant l’interface avec les interruptions externes (provenant du
GPU, SPU, CD/ROM etc.).
Le system
travaille dans une RAM de 2MB.
La PSX possède
également deux autres circuits spécialisés :
Le GPU (IC203
CXD8561) est le coprocesseur graphique, il est très puissant et possède un
paquet de routine de dessin « Hardware ». La mémoire vidéo (1Mo de
VRAM + 1 cache texture de 2KO+ tampon de commande de 64 octets ??) y est
indépendante et inaccessible directement par le processeur.
Le SPU (Intégrée
en un seul chip IC732 depuis 75xx) est le chip audio 16 bits avec de belles
possibilités: 24 canaux programmables avec enveloppe ADSR, synthèse ADPCM,
générateur de bruit, synthèse FM et une infinité d’autre choses mais il n’est
également pas sans défaut. Le SPU possède un tampon de 512 Ko.
Le lecteur CD/ROM
double vitesse possède également un encodeur PCM->ADPCM en relation étroite
avec le SPU.
Deux port
manettes et deux port carte mémoire.
Un port Série.
La PSX intègre
une ROM de 512KO dans lequel se trouve son OS.
Voici à peu près
le bloc fonctionnel de la PlayStation
2-
Quel(s)
émulateur(s) utiliser pour la programmation ?
L’utilisation
d’un émulateur est quasi indispensable (sans link-cable) et surtout au début
pour l’apprentissage.
Contrairement aux
émulateurs pour les retro-machines comme le C64, les Amstrad CPCs et WinUAE
pour l’Amiga, les émulateurs de la PSx restent encore très loin d’une émulation
correcte. En effet de par sa complexité et son manque de documentation
bas-niveau, la PSX est une machine redoutable à émuler : presque 20 ans
d’existence et aucun émulateur vraiment convainquant.
La plupart sont
plus des émulateurs de jeux PSX que de véritable émulateur Playstation.
Actuellement le
meilleur émulateur pour moi est XEBRA. En tout cas pour tester des programmes que
vous écrirez en bas-niveau, c’est celui qui se rapprochera le plus du vrai
fonctionnement du GPU de la PSX (hormis le cache de commande). Il respecte
aussi assez bien le timing d’une vrai PSX et tous les jeux que j’ai testés
dessus ont fonctionné de façon impressionnante. Il n’est pas parfait en termes
d’émulation du SPU ou quelque problème de rendu à l’écran par exemple et
peut-être d’autres composants que je connais moins. Il n’empêche je conseille
vivement l’utilisation de cet émulateur pour tester nos programmes.
Malheureusement, XEBRA ne contient pas de debugger.
Les émulateurs
populaires à base de plugins (Psxe, Psxseven etc) dépendent surtout de la
qualité du plug-in et en termes de GPU, ils sont tous inferieurs à XEBRA. Mais
ils permettent le chargement direct d’exécutable ce qui nous évite la
génération de l’image du CD. Bien qu’ils émulent assez bien la plupart des
jeux, comme pour PSx1.13 ils pardonnent beaucoup trop les erreurs de
programmations.
PSx1.13 est une
autre alternative avec son debugger qui peut breaker lors d’un changement
d’état de mémoire mais il pardonne beaucoup trop les erreurs de programmation
et ne semble absolument pas émuler ni timing ni le cache du GPU et le son SPU
est un peu bizarre. Dommage car en termes de GUI c’est mon préféré.
C’est pourquoi je
vous conseille l’utilisation de NoCash-PSX.
Il contient un bon debugger capable de breaker sur une addresse et sur
des changement de mémoire avec en plus la possibilité de modifier le code en
direct. Il est capable d’executer directement un executable sans passer par
generation de l’image CD.
De plus l’auteur
propose même une documentation de la PSX la plus complète qui soit actuellement
trouvable sur le net. La description de certains registres, dans cet article,
sont des copie/colle de celle-ci. Même si il est loin d’etre parfait en terme
d’émulation (GPU cache commande, read delay et autres), l’auteur semble
travailler encore dessus donc on peut espérer des améliorations très
prochainement. Et pour le développement c’est certainement l’émulateur idéal.
http://problemkaputt.de/psx.htm
Mais encore une
fois ne vous fiez surtout pas à un emulateur : dans tous les cas il
faudra tester nos programmes sur une vrai PSX pour être certain de leur
fonctionnement et de nombreuses surprises (souvent extrement frustrante)
peuvent vous attendre.
Nous somme en 2013 à l’heure ou
j’ecris ce paragraphe et il est fort possible que la situation ait quelque peu changee…
3-
MIPS R3000
Avant de
commencer il faut, bien entendu, se familiariser avec l’assembleur du MIPS
R3000.
Voici un lien
pour tout savoir sur le R3000 ainsi que son jeu d’instructions:
http://cgi.cse.unsw.edu.au/~cs3231/doc/R3000.pdf
Et un excellent
lien en français :
http://cgi.cse.unsw.edu.au/~cs3231/doc/R3000.pdf
Je me contenterais de faire un petit résumé
rapide et suffisant pour comprendre tous les listings qui suivront et en
insistant sur les spécificités du R3000A de la PSX.
Processeur RISC
de 32 bits architecture MIPS I. 32 Registres de 32 bits (r0 jusqu’à r31). En
standard, il est équipé de deux coprocesseurs arithmétiques le Cop1 et Cop2.
La PlayStation
utilise une version modifiée de ce processeur avec un cache d’instructions
de 4Ko et un pseudo cache de données (ScrachPad) de 1Ko. Cadencé à 33MHZ et
amputé du cop1 et munis d’un cop2 spécifique : le GTE. Le Bus de données
est bien sur de 32 bits. Nous verrons plus tard les mécanismes du cache I
ainsi que des tampons R/W.
L’OS de la PSX et
la plupart des assembleurs utilisent les syntaxes suivantes pour les
registres :
Register number |
Name |
Usage |
R0 |
$ZR |
Constant Zero |
R1 |
$AT |
Reserved for the assembler |
R2-R3 |
$V0-$V1 |
Values
for results and expression evaluation |
R4-R7 |
$A0-$A3 |
Arguments |
R8-R15 |
$T0-$T7 |
Temporaries
(not preserved across call) |
R16-R23 |
$S0-$S7 |
Saved (preserved across call) |
R24-R25 |
$T8-$T9 |
More
temporaries (not preserved across call) |
R26-R27 |
$K0-$K1 |
Reserved for OS Kernel |
R28 |
$GP |
Global Pointer |
R29 |
$SP |
Stack Pointer |
R30 |
$FP |
Frame Pointer |
R31 |
$RA |
Return
address (set by function call) |
REMARQUES
DIVERSES :
-
Un
mot ou WORD désigne 32 bits ! Même sur les x86 actuelle, on continue
d’appeler un mot 16 bits et un double-mot (DWORD) 32 bits héritage 16 bits des
x86.
-
Et
donc un demi-mot 16 bits (HALF).
-
La
programmation du R3000 est clairement orienté registre.
-
Un
seul mode d’adressage disponible : Off(Reg) où Reg est l’adresse de base
représenté par un registre et Off est l’offset signé sur 16 bits.
-
Les
adresses doivent toujours être multiples de 4 pour être adressées exceptée pour
les instructions lb/h/w et sb/h/w qui acceptent des adresses non alignées avec
une pénalité de non alignement.
-
Pipeline
de 5 niveaux (P-E-A-M-W) mais les « Interlocks » (comme les
multiplication ou divisions) peuvent bloquer le pipeline.
-
Le
processeur est Little-Endian pour la
Playstation même si théoriquement il peut être configuré en Big-Endian.
-
Le
Registre 0 n’est accessible qu’en lecture avec toujours une valeur de 0.
-
Le
registre R31 est le registre de l’adresse de retour d’un appel par JAL.
-
Le
Registre 28 GP est utilisé par les compilateurs pour pointer au milieu d’une
zone statique de mémoire (section BSS/DATA).
-
Le
Registre 30 FP est utilisé pour stocke les variables de sous-routines par les
compilateurs (modèle STACK-FRAME).
-
Pas
de gestion de pile explicite, même les appelles avec retour se font sur les
registre (en général le R31). Le registre 29 SP est utilisé comme pile par les
compilateurs et l’OS de la PSX.
-
Une
unité de calcul de multiplication et division entier indépendant.
-
Apres un branchement il y a un délai
d’un cycle et l’instruction suivante est toujours exécutée: insérer
une instruction utile et si vous n’avez aucune instruction indépendante insérer
un NOP.
-
Apres une lecture de mémoire il y a un
délai d’un cycle à cause de la nature du pipeline, insérer une instruction
(utile) avant d’interpréter le résultat !
-
RISC
oblige, le code en assembleur devient très vite indigeste. C’est pourquoi il
est d’usage d’utiliser des pseudo-instructions (LI, LA, SUBI, NOP,MOV)
étendues pour englober plusieurs instructions de base ou bien traduire
l’instruction avec un memo plus compréhensible. Mais je n’utiliserai pas les
Pseudo-Instructions (hormis NOP)
dans cette section car je veux vous faire sentir le RISC du R3000.
Nous allons juste
passez en revue les bases pour comprendre les listings de cette première
partie.
La syntaxe des
listings qui suivront est spécifique à mon assembleur. J’utilise la
syntaxe officiel du MIPS : hexadécimale avec ‘0x’ commentaire
avec ‘;’ label se terminant toujours avec ‘:’ En revanche,
j’incorpore un system de souslabel qui commence par le caractere ‘.’. La
syntaxe des macros, commandes d’insertion et include sont différentes. Les
directives .reorder ne sont pas incluses. Les registre peuvent être
indifféremment représenter par R8,$8,$T8,T8.
Copie de valeurs immédiates dans un registre
L’instruction LUI charge dans les demi-mots de poids
fort du registre la valeur immédiate sur 16 bits en argument. Le demi-mot de
poids faible est réinitialisé à 0.
LUI R8,0xAAAA #Charge dans le Registre R8=0xAAAA 0000
Les instructions ADDI, ADDIU, ORI, ANDI et XORI utilisent toujours comme argument RT, RS, IM.
RT est le registre de destination, RS est
le registre source et IM est la valeur immédiate sur 16
bits. L’opération s’effectue toujours ainsi : RT=RS op IM
ADDI travaille en 16 bits signés, ADDIU
travaille en non signés.
En combinant
l’instruction LUI et ces dernières
nous pouvons écrire dans un registre n’importe quelle valeur.
ORI R8,R0,0x0000 ; R8=0x0000 00000
LUI R8,0xCCAA ; R8=0xCCAA 0000
ORI R8,R8,0xDDEE ; R8=0xCCAA DDEE
LUI R8,0x0001 ; R8=0x00001 0000
ADDIU R8,R8,0x0400 ; R8=0x00001 0400
ADDIU R8,R0,0x0010 ; R8=0x0000 0010
ADDI R8,R8,0xFFFF ; R8=0x0000 000F Decremente R8
ADDI R8,R0,0xFFFF ; R8=0xFFFF FFFF
Copie et opérations de registre à registre :
Les instructions AND, OR, XOR, ADD, ADDU, SUB permettent,
en outre les opérations traditionnelles, de copier les registre entre eux.
La syntaxe est OP
RD,RS,RT ou l’instruction est exécuté de la façon suivante : RD=RS
OP RT
ADD R8,R9,R10 ; R8=R9+R10
SUB R8,R9,R10 ; R8=R9-R10
ADDU R8,R9,R0 ; R8=R9
ADDU R8,R0,R9 ;
R8=R9
AND R8,R9,R10 ; R8=R9&R10
OR R8,R9,R0 ; R8=R9
Incrémentation et décrémentation :
On utilise ADDI dans les deux cas la valeur
immédiate est interprété en tant que 16 bits signée et donc :
Addi R8,R8,0x0001 ; R8++
Addi R8,R8,0xFFFF ; R8—
Addi R8,R8,4 ;R8+=4
etc
Pseudo-Instruction NOP:
L’instruction NOP qui ne fait rien et consomme 1 cycle
est en fait l’instruction SLL R0,R0,0
code par 0x0000 0000.
Adressage Mémoire
Il n’y a qu’un
seul mode d’adressage de type Off(RS) où RS est l’adresse de base
représenté par un registre et Off et l’offset signé sur 16 bits
Pour lire ou
écrire dans une case mémoire on utilise les instructions :
-
LW LH LB respectivement lecture en mémoire d’un mot, d’un
demi-mot et d’un octet avec extension de signe
-
LHU LBU respectivement lecture en mémoire d’un demi-mot
et d’un octet non signé.
-
SW SH SB respectivement écriture en mémoire d’un mot, d’un
demi-mot et d’un octet.
La syntaxe est OP
RT,IM(RS) ou RT est le registre de destination et
RS
le registre d’adresse mémoire. IM l’offset sur 16 bits signées dans
la mémoire.
L’alignement de
l’adresse est important en fonction du type lu/écrit. L’adresse pour LW/SW doit-être aligne sur 4 octets, LH/LHU/SH sur 2 octets. Sinon le processeur
part en exception.
ATTENTION, après
une lecture de mémoire il y a un délai d’un cycle. Si nous devons nous servir
du registre RT juste après, nous devons insérer un NOP avant.
Lecture en Mémoire :
lw rd,Offset(rs) ; charge dans rd la
valeur pointée par rs+offset
nop ;
délai de lecture
ATTENTION ! Apres une
lecture en mémoire, il y a 1 délai d’une instruction et rd ne peut être interpréter
que durant le prochain cycle. La plupart des émulateurs ne prennent pas en
compte ce délai (hormis Xebra)
Ecriture en Mémoire :
Sw rd,Offset(rs) ; écriture de rd dans
la mémoire pointée par rs+offset
Exemples :
lui R4,0x1001
ori R4,R4,0x0200 ; R4 pointe vers 0x1001 0200
lw R8,0(R4) ; Copie dans R8 la valeur qui se trouve à
l’adresse 0x1001 0200
nop ; Délai de lecture ici superflus
lw R9,4(R4) ; Copie dans R9 la valeur qui se trouve à
l’adresse 0x1001 0204
addi R8,R8,1 ; Increment R8
addi R9,R9,-1 ; Décrémente R9
sw R8,0(R4) ; Ecriture de la valeur de R8 à l’adresse
0x1001 0200
sw R9,4(R4) ; Ecriture de la valeur de R9 à l’adresse
0x1001 0204
Registre Global GP
L’offset de
l’adressage mémoire permet d’atteindre -32KO a +32KO. Pour cette bonne raison,
il est d’usage d’initialiser le registre R28
(GP) à l’adresse du milieu de notre code. De cette façon, une variable de
notre code est seulement définit par son offset et on peut alors
simplifier les accès en mémoire tout en économisant 2 instructions et 2
cycles:
sw R8,OFFSETGP VAR(GP)
Au lieu de
lui R9,VAR+1
ori R9,R9,VAR
sw R8,0(R9)
L’idéal est
évidement de posséder un segment DATA où seront contenue nos variables et
initialiser le GP par DATA + 32768.
Les compilateurs
utilisent de la même façon du registre R30
(FP) pour implémenter le modèle de la « stack-frame » et
l’accession aux variables locales par offset lors dans une fonction.
Opération de décalage de bits
Les instructions
décalage logique SLL et SLR ont pour syntaxes: OP
RD,RT,SH
Ou nous avons RD =
RT OP SH
ORI R8,R0,0x0180 ; Charge R8 avec la valeur 0x0180
SLL R8,R8,1 ; R8=R8<<1=0x0300
SLL R9,R8,2 ; R9=R8<<2=0x0C00
SLL R10,R9,16 ; R10=R9<<16=0x0C00 0000
etc
Attention, le jeu
d’instruction du R3000 ne contient pas de rotation de bits.
Pile
Pas de pile
explicite nous devons procéder manuellement: Par convention, le registre R29 est utilisé comme pile par les
compilateurs.
;PUSH
ADDI R29,R29,0xFFFC ; R29=R29-4
SW Rn,0(R29) ; PUSH Rn
;POP
LW Rn,0(R29) ; POP RN
ADDI R29,R29,0x0004 ; R29=R29+4
PILE PSX:
ATTENTION!!!
Certaines
fonctions du BIOS sont boguer et détruise les premiers éléments de la pile.
Pour éviter de
mauvaise surprise je fais comme ça :
;PUSH
ADDI R29,R29,-32 ; R29=R29-32
SW Rn,4(R29) ; PUSH Rn
;Etc…
;POP
LW Rn,4(R29) ; POP RN
;etc…
ADDI R29,R29,32 ; R29=R29+32
De plus si vous
voulez quitter correctement un programme il est impérative de sauver les
registre non pas dans la pile mais dans un espace statique de votre programme.
Beaucoup de
fonctions du BIOS détruisent non seulement le contenu, mais le pointeur de pile
également !!!
Branchement conditionnel
ATTENTION, L’INSTRUCTION SUIVIT D’UN BRANCHEMENT
EST TOUJOURS EXECUTE !
INSERER DONC UNE INSTRUCTION UTILE OU BIEN UN NOP.
Les instruction BEQ et BNE utilisent la syntaxe : BEQ RS,RT,OFFSET et effectuent le branchement si RS=RT
et RS !=RT
respectivement.
Ori
R8,R0,4 ; R8=4
.Boucle :
……………..
Addi R8,R8,-1 ; R8--
Bne R8,R0,.Boucle ; Si R8 !=0 branche
Nop ; Délai de branchement
.Suite :
………………….
Les autres
instructions BGEZ, BGTZ, BLEZ, BLTZ
utilisent la syntaxe BGEZ RS,OFFSET ou RS est toujours comparé à
la valeur 0.
BGEZ branche si RS est supérieur ou égale à 0
BGTZ branche si RS est strictement supérieur à 0
BLEZ branche si RS est inférieur ou égale à 0
BLTZ branche si RS est strictement
inférieur à 0
Ori R8,R0,4 ; R8=4
.Boucle :
……………..
Addi R8,R8,-1 ; R8--
Bgtz R8,.Boucle ; Si R8 >0 branche
Nop ; Délai de branchement
.Suite :
………………….
L’offset est
toujours implicitement multiplié par 4, les branchements sont donc limité à 256
KO (-128KO; +127 KO) dans le code.
Branchement inconditionnel
L’instruction J adresse
effectue un saut direct. L’adresse est codée sur 26 bits multipliée par 4. On
peut donc atteindre jusqu’à 256 Mo. Pour la Playstation c’est beaucoup plus
qu’il n’en faut.
Appel de routine
En général, une
routine s’appelle via un JAL adresse. L’instruction JAL adresse sauve
l’adresse d’appelle dans le registre R31
et effectue le branchement (toujours avec un délai d’une instruction).
La routine doit
donc contenir JR R31 pour revenir à
l’adresse d’appel.
Il est également
possible d’utiliser l’instruction JAL
Reg,Adresse pour préciser le registre dans lequel nous voulons sauvez
l’adresse d’appel. La routine devra donc contenir l’instruction JR Reg approprié.
;Appel de ma routine
JAL MaRoutine
NOP
………………………
MaRoutine:
JR R31 ; R29=R29-4
NOP
Comme nous le
voyons, l’architecture ne nous impose rien de particulier c’est aux
développeurs de l’organiser.
Mais bien sur des
conventions existent et ce sont celles qui sont données dans le tableau avant
qui sont appliquées pour la Playstation plus celle que je donnerai.
Exemple :
MaRoutine:
ADDI R29,R29,0xFFFC ; R29=R29-4
SW R31,0(R29) ; PUSH Rn
…………………………………
LW R31,0(R29) ; POP RN
JR R31 ; RET
ADDI R29,R29,0x0004 ; R29=R29+4
Multiplication et division
L’unité de calcul
est indépendante.
Les instructions MUL DIV se servent de registres
spéciaux LO et HO
multu R16,R17 ;R16*R17
mflo R18 ;=R18
Ces instructions
possèdent plusieurs cycles d’exécution et il important d’insérer plus
instruction indépendante avant d’utiliser MFLO ou MFHO pour ne pas bloquer le pipeline.
Instruction système :
SYSCALL
déclenche une interruption pour la PSX l’exception permet de passer en mode
superviseur.
Coprocesseur :
Le R3000A
contient 2 coprocesseurs le COP0(System) et le COP2 (GTE).
Chaque
coprocesseur contient en théories ses registres spécifiques 32 registres de
control et 32 registres data.
Pour la
Playstation, le COP0 ne contient qu’un certain nombre de registres de contrôle
(Le 12,13 et 14) accessible par les instructions MFC0 et MTC0.
Le COP1 n’est pas
présent. Et le COP2 est le processeur de calcul vectoriel le GTE que nous
verrons plus tard.
Pseudo-Instruction
Du code
assembleur RISC devient très vite indigeste donc il est d’usage d’utiliser des
pseudo-instructions qui peuvent englober plusieurs instructions ou bien rendre
une instruction plus compréhensible :
Par
exemple :
LI R8,0x18001F00 remplace LUI
R8,0x1800 , ORI R8,R8,0x1F00.
MOV R8,R7 remplace OR R8,R7,R0
SUBI R8,R8,1 remplace ADDI
R8,R8,-1
Mais les listings
de cette section n’utiliseront que rarement ces pseudo-instructions.
Quelques
conventions appliquées :
Les listings qui
suivront distinguent 2 types de fonction :
-
Les
fonctions minimales qui n’affectent seulement les registres R2 et R3. Les fonctions de bases InitGPU, WaitGPUReady, InitSPU etc
n’affectent que ces registres.
-
Les
autres fonctions qui affectent les autres registres temporaires également.
-
Le
registre R27 ($K0) pointera toujours
sur la base des registres hardware c’est-à-dire l’adresse 0x1F80 0000
-
On ne
se sert que de R31 ($RA) comme
registre de link et donc seule l’instruction JAL sera utilisée comme appel de routine.
-
Si
une routine se sert des registre R16-R23
($S0-$S7), elle se doit de ne pas les affecter et donc de les sauvés en
Pile pour les restaurée avant un retour.
-
Une
Routine peut utiliser le registre FP est emprunter à la Pile un espace pour les
variables locales (Stack-Frame model).
Remarque : Pour plus de clarté, les optimisations même
bateau sont évités au début, mais plus on avancera plus le code présenté sera
optimisé.
4-
Organisation Mémoire
Même si les
spécifications MIPS du R3000 prévoient la possibilité de virtualiser la
mémoire, celle-ci n’est pas implémenter dans le R3000A de la PSX.
Sur la
Playstation la mémoire est mappée de cette façon par segment:
La zone
0x00000000-0x001FFFFF correspond à la mémoire réelle accessible seulement par
les processus en mode Superviseur.
La zone
0x80000000-0x801FFFFF correspond à la même zone physique mais accessible par
les processus en mode utilisateur.
La zone I/O
(registres des chips spécialisées) est dans la zone : 0x 1F801000 –
0x1F801FFF.
D-Cache :
Une zone mérite
une attention particulière, c’est la zone 0x1f800000~0x1f8003ff.
Celle-ci pointe sur le
D-Cache de 1KO (SRAM interne du processeur) où le processeur lit et écrits des
mots en 1 cycle contre 5/6 cycles pour la RAM normal. Cette zone n’est
évidemment pas accessible par la DMA.
Memory Map
00000000 - 001FFFFF
(2mb) RAM segment for System
1F000000 - 1F7FFFFF
(up to 8mb) adaptor rom shadow (action replay)
1F800000 – 1F8003FF (1Kb) D-Cache ou ScratchPAD.
1F801000 - 1F801xxx (8kb) hardware i/o map: gpu, pads, memcard, pio port,
sio port
80000000 - 801FFFFF
(2mb) RAM segment for user
80000000 - 8000FFFF (64kb) System Area
80010000 – 801FFFFF
(2Mb-64Kb) User Area (Code, Heap et stack)
bfc00000 - bfc7ffff (512kb) System ROM (BIOS)
Pour l’instant
nous devons simplement prendre en compte que nos programmes utiliseront
l’espace 0x8001 0000-0x81FF FFFF.
L’OS de la PX
réservé les 64 premiers Ko (0x8000 0000 0x8000 FFFF). Le reste de la mémoire
est à notre disposition.
Si vous voulez
insérer vos programmes dans un CD démo officielle, charger vos exécutables à
l’adresse 0x80018000.
5-
Cache, Timing et
Optimisation
Si vous êtes presser sauter cette partie, vous y
reviendrez tranquillement plus tard.
Les éléments à
prendre en compte sont :
-
le
pipeline.
-
Le
cache I
-
Le
pseudo D cache : scratch PAD
-
Les
tampons FIFO : 4 octets pour la lecture et 4 tampons de 4 octets pour
l’écriture.
-
Les instructions
Interlock
Le R3000 est
doté (selon les spécifications) d’un pipeline de 5 niveaux (IF/RD/ALU/MEM/WB).
IF : Instruction Fetch
RD : Read argument from register and
decode instruction
ALU : Perform operation
(Arithmetics/Shift/Boolean/Adress Calculation)
MEM : Access Memory (Data cache) if sw
instruction.
WB : Write back ALU Memory and
register
L’une des forces
de l’implémentation MIPS du R3000 est le mécanisme appelé ‘Bypass’ qui fait que
la modification d’un registre par l’ALU ou bien un acces mémoire dans MEM est
prise en compte même si le résultat de l’opération n’est pas physiquement écrit
dans le registre. Le Bypass est implémentée dans le RD qui est rafraichit par
le bypass dans ALU et dans MEM.
L’autre élément à
prendre en compte et qu’en réalité RD possède également une ALU pour les
branchements. Voilà pourquoi seulement une instruction est toujours exécutée
après une instruction de branchement et non pas deux.
Le délai de
lecture s’explique par la nature du pipeline et de l’emplacement des bypass. Regardez :
LW
$8,0($4)
ADD
$8,$8,1
Cycle/Pipelines |
IF |
RD |
ALU |
MEM |
WB |
Commentaire |
1 |
LW R8,0(R4) |
|
|
|
|
|
2 |
ADD R8,R8,1 |
LW R8,0(R4) |
|
|
|
|
3 |
- |
ADD R8,R8,1 |
LW R8,0(R4) |
|
|
R8 toujours pas disponible |
4 |
- |
- |
ADD R8,R8,1 |
LW R8,0(R4) |
|
R8 disponible mais trop tard car ADD est dans
L’ALU |
LW
R8,0(R4)
NOP
ADD
R8,R8,1
Cycle/Pipelines |
IF |
RD |
ALU |
MEM |
WB |
Commentaires |
1 |
LW R8,0(R4) |
|
|
|
|
|
2 |
NOP |
LW R8,0(R4) |
|
|
|
|
3 |
ADD R8,R8,1 |
NOP |
LW R8,0(R4) |
|
|
R8 Disponible |
4 |
|
ADD R8,R8,1 |
NOP |
LW R8,0(R4) |
|
ADD incrémente
R8 correctement |
5 |
|
|
ADD R8,R8,1 |
NOP |
LW R8,0(R4) |
|
Délai de
lecture et branchement :
Rappelez-vous que
le branchement est effectué dans RD.
Supposons que
0(R4) soit égale à 0 et R2 à 1
LBU
R2,0(R4)
BEQ
R2,R0,@L
ADDI
R2,R2,1
@L:
OR
R3,R2,R0
CYCLE/PIP |
IF |
RD |
ALU |
MEM |
WB |
Commentaires |
1 |
LBU R2,0(R4) |
|
|
|
|
R2=1 |
2 |
BEQ R2,R0,@L |
LBU R2,0(R4) |
|
|
|
R2=1 |
3 |
ADDI R2,R2,1 |
BEQ R2,R0,@L |
LBU R2,0(R4) |
|
|
R2=1 Non branch |
4 |
OR R3,R2,R0 |
ADDI R2,R2,1 |
BEQ R2,R0,@L |
LBU R2,0(R4) |
|
R2=0 |
5 |
|
OR R3,R2,R0 |
ADDI R2,R2,1 |
BEQ R2,R0,@L |
LBU R2,0(R4) |
R2+=1 = 1 |
6 |
|
|
OR R3,R2,R0 |
ADDI R2,R2,1 |
BEQ R2,R0,@L |
R3=R2=1 |
LBU
R2,0(R4)
NOP
BEQ
R2,R0,@L
ADDI
R2,R2,1
ADDI
R2,R2,2
@L:
OR
R3,R2,R0
CYCLE/PIP |
IF |
RD |
ALU |
MEM |
WB |
Commentaires |
1 |
LBU R2,0(R4) |
|
|
|
|
R2=1 |
2 |
NOP |
LBU R2,0(R4) |
|
|
|
R2=1 |
3 |
BEQ R2,R0,@L |
NOP |
LBU R2,0(R4) |
|
|
R2=0 |
4 |
ADDI R2,R2,1 |
BEQ R2,R0,@L |
NOP |
LBU R2,0(R4) |
|
R2=0 Branch |
5 |
OR R3,R2,R0 |
ADDI R2,R2,1 |
BEQ R2,R0,@L |
|
|
R2=0 |
6 |
|
OR R3,R2,R0 |
ADDI R2,R2,1 |
|
|
R3=R2=1 |
7 |
|
|
OR R3,R2,R0 |
|
|
|
Mais comme
toujours nous devons nous méfier des mangeurs de cycles suivants :
-
Data
Cache non prêt (pour la PSX cette situation n’existe pas car le Cache D est
fixée dans une adresse constante)
-
Cache
I non prêt
-
Bus
mémoire occupé (Lecture/écriture/Dma)
-
Instructions
mflo et mfho
-
Lecture
mémoire qui prendra toujours 5 cycles si le buffer n’est pas prêt + pénalité si
le buffer Write n’est pas vide.
-
Ecriture
en mémoire si les buffers non prêts.
-
Ecriture
ou lecture sur des adresses non alignées (exemple Lh/lb sh/sb).
La lecture et
l’écriture en mémoire sont évidemment les points ou l’on rencontrera les pertes
de cycles.
Le R3000a de la
PSX implémente un tampon d’écriture FIFO de 4 mots et un tampon de lecture d’un
mot Buffer R.
Le bus mémoire
charge tout d’abord ce tampon lors d’une lecture ce qui prend 5 cycles.
L’écriture est
dotée de 4 tampons de 4 octets Buffer W.
Le chargement effective dans la mémoire prend alors 4 cycles mets si on s’arrange
pour aligner les écritures par 4 mots on va minimise les effets de ces pertes
de cycles car l’écriture dans le tampon ne prend que 1 cycles et le chargement
aligné de 4 word ne prendra que 10 cycles.
Le Buffer W aura toujours la priorité sur
le Buffer R ! Une lecture provoque automatiquement le vidage du Buffer W et donc la
« consommation » effective des écritures.
Un point obscur
reste l’arbitrage du bus mémoire. Par exemple lorsque les canaux DMA vont le
réquisitionner, je n’ai pour l’instant aucune idée comment cela se passe, la
documentation de Sony n’en parle pas.
D’après le « Service Manual », l’arbitre se situe dans le
R3000A et doit certainement jouer sur les différentes vitesses des composants
(Le CPU est cadencée à 33,86MHZ, le GTE à 67 MHZ et le GPU à 53MHZ le SPU à
4MHZ) afin d’éviter les inter-blocages du bus.
Un deuxième point
délicat reste le fonctionnement du Cache I. Le R3000A utilise un algorithme de
« Direct Mapping » pour la gestion du cache I.
Le cache I
contient 256 lignes de 16 octets (soit 4 instructions). Chaque ligne contient
un champ (un Tag) d’information. Le Tag indique une instruction dans la ligne a
déjà été lue et indique les 20 bits supérieur de l’adresse source de la RAM.
Lorsque une instruction est pré-chargé, les
bits 5-12 de l’adresse sont utilisé pour spécifier une ligne unique celle-ci
étant adressée en interne par 8 bits. Le CPU va vérifier le Tag de la ligne
pour vérifier si l’instruction s’y trouve. Si l’instruction se trouve dans le
cache le pré-chargement (IF) ne prend qu’un cycle. Sinon le CPU va lire la ligne
correspondante de la RAM la pénalité sera fonction de l’alignement de
l’instruction dans la ligne. Si l’instruction est la première sur la ligne il
n’y a que 4 cycles de pénalité, si c’est la deuxième 5 cycles si c’est la
troisième 6 cycles et si c’est la dernière 7 cycles. Il y aura donc de 4 à 7 cycles de pénalité
pour charger l’instruction dans le Buffer R. Enfin 1 cycle supplémentaire est
nécessaire pour charger la ligne.
Voilà pourquoi a priori nous devons
alignée notre code pour les séquences dans une boucle par exemple afin de
minimiser les ratés du cache (CACHE-MISS).
Comme nous le
voyons, l’ordre du code et son alignement des blocs de codes pour le cache,
l’utilisation intensifs des registres et du scratch-PAD sont les clefs de
voutes d’un code optimisé pour notre Playstation.
6-
PSX EXE et Premiers
programmes (enfin !)
Entrons dans le
vif du sujet. Comment faire booter un programme sur notre PSX. C’est très simple,
notre CD doit contenir un exécutable et un fichier texte appelé SYSTEM.CNF
indiquant le nom de l’exécutable que l’OS doit lancer après un boot.
Examinons tout
d’abord le format de l’exécutable.
Executable
PSX Structure :
typedef
struct _EXE_HEADER_ {
unsigned byte id[8];
unsigned long text; // 8
unsigned data; //12
unsigned long pc0; //16
unsigned long gp0; //20
unsigned long t_addr; //24
unsigned long t_size; //28
unsigned long d_addr; //32
unsigned long d_size; //36
unsigned long b_addr; //40
unsigned long b_size; //44
unsigned long s_addr; //48
unsigned long s_size; //52
unsigned long sp,fp,gp,ret,base; //56,60,64,68,72
}
EXE_HEADER;
ATTENTION
!!! LITTLE EDIAN !!!
Nous
n’utiliserons pas de segment DATA et autre BSS. Seul le segment TEXT sera
utilisé. La taille sera fonction de notre fichier BIN (c.à.d. notre code
machine) à « Linker ».
Si vous voulez
que votre code puisse se lancer du « Laucher Demo » officiel. Fait
démarrer votre code en 0x80018000 voir l’annexe.
Dans nos
exemples, nous chargerons à l’adresse 0x8001 0000 mais les autres programmes
démarreront à 0x80018000.
Exemple:
0x0000 50 53 2D 58 20 45 58 45 00 00 00 00
00 00 00 00
0x0010 yy yy yy yy gg gg gg gg zz zz zz zz xx
xx xx xx
0x0020 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00
0x0030 pp pp pp pp 00 00 00 00 00 00 00 00
00 00 00 00
0x0040 00 00 00 00 00 00 00 00 00 00 00 00
53 6F 6E 79
0x0050 20 43 6F 6D 70 75 74 65 72 20 45 6E
74 65 72 74
0x0060 61 69 6E 6D 65 6E 74 20 49 6E 63 2E
20 66 6F 72
0x0080 20 45 75 72 6F 70 65 20 61 72 65 61
00 00 00 00
............ 00
0x07F0 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00
0x0800 Début du code/données.
* zz zz zz zz Adresse du segment .text exemple
* xx xx xx xx Taille du segment .text exemple
00 48 04 00 taille = 0x0004 4800
* yy yy yy yy Register PC exemple
78 BE 02 80 correspond à l’adresse 80 02 BE 78 Le code démarre à cette adresse.
* pp pp pp pp Adresse
de la Pile exemple F0 FF
1F 80 La pile commence donc en 80 1F FF F0.
* gg gg gg gg Registre GP exemple
00 00 80 80 La valeur sera du GP sera donc de 80 80 00 00
L’adresse 0x4C
contient la chaine « Sony Computer Entertainment » etc. Cette chaine
n’est pas indispensable. Ma Playstation exécute les exécutables prive de cette
chaine sans problème.
La partie
« objet (à partir de l’adresse 0x800 notre code/donnees) est chargé en zz
zz zz zz. Le PC indique ou se situe le
Start de notre code.
Pour l’instant,
le plus important pour nous est de faire démarrer notre code pour commencer à
tester notre PSX
Et le plus simple
est de faire comme ceci :
PC0= 0x80010000
SEG=0x80010000
Sur notre
assembleur/Linker, on fera toujours :
org 0x8001 0000
Start:
Taille= toujours multiple
de 2048 sinon la PS refusera de le charger.
Pour l’instant
cette partie ignore le registre GP que j’initialise à 0. Mais lors de la
deuxième partie on s’en servira (ainsi que des pseudo-instructions) pour
alléger notre code.
PSX_001_1.asm:
Programme de 4 octets qui ne fait rien et
tourne en boucle infinie.
Org 0x80010000 ; Notre code objet sera toujours chargé à
cette adresse.
Start :
Beq $9,$9,Start ;Boucle infinie=0x1129 FFFF
nop
Résultat en
détail de l’exécutable :
ADRESSE DATA COMMENTAIRE
0x0000 50 53 2D 58 20 45 58 45
00 00 00 00 00 00 00 00 CHAINE
“PS-X EXE”
0x0010 00 00 01 80 00 00 00 00
00 00 01 80 00 20 00 00 0x10:
PC (Où démarre notre Code) 0x18 : Segment TEXT 0x1C : Taille du
Segment TEXT
0x0020 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
0x0030 F0 FF 1F 80 00 00 00 00
00 00 00 00 00 00 00 00 0x30:
Initialisation de la PILE
………. 00
0x0800 FF FF 29 11 00 00 00
00 00 00 00 00 00 00 00 00 Notre
code 0x1129FFFF => BEQ R9,R9,-1 ;boucle infinie
………. 00
0x0FF0 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
Rappelez-vous que
la PSX travaille en Little-Endian !
Sur notre PSX, il
n’y a aucun moyen de savoir si notre objet a bien été chargé et exécuter.
L’écran de présentation restant fixe.
Donc on va plutôt
tenter d’initialiser l’affichage et vérifier que quelque chose se produise.
Deuxieme Test: RESET GPU
On va
réinitialiser le GPU ce qui devrait provoquer l’effacement de l’écran de
présentation.
Nous verrons un
peu plus tard le GPU. Pour l’instant nous devons savoir que pour faire faire
des taches au GPU, nous devons lui envoyer des commandes sur 2 ports :
-
Control
Register 0x1f80 1814 dans lequel sont envoyées des commandes de configuration.
-
Data
Register 0x1f80 1810 dans lequel sont envoyées des commandes de dessins.
L’adresse de base
des registres I/O est 0x1F80 0000 et on va s’intéresser au registre de contrôle
du GPU 0x1814 (mappé en 0x1F80 1814) pour réinitialiser notre GPU.
Exemple de Commande:
* RESET GPU: CMD=0x00 PARAM=0x00
Donc pour envoyer la commande RESET GPU,
on écrit simplement un mot (4 octets) avec comme valeur 0 à l’adresse 0x1F80
1814.
PSX_001_2.asm:
Org 0x80010000
START :
LUI R27, 0x1f80 ; Le register k1 (R27) pointe sur la base I/O
(Registre hardware)
SW
R0,0x1814(R27) ; Envoi de la commande 0x0000 0000 dans GPU1
0x1F80 1814
BEQ
R0,R0,0xFFFF ; boucle infinie
NOP
Résultat en
détail de l’exécutable :
ADRESSE DATA COMMENTAIRE
0x0000 50 53 2D 58 20 45 58 45
00 00 00 00 00 00 00 00 CHAINE
“PS-X EXE”
0x0010 00 00 01 80 00 00 00 00
00 00 01 80 00 20 00 00 0x10:
PC (Où démarre notre Code) 0x18 : Segment TEXT 0x1C : Taille du
Segment TEXT
0x0020 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
0x0030 F0 FF 1F 80 00 00 00 00
00 00 00 00 00 00 00 00 0x30:
Initialisation de la PILE
………. 00
0x0800 80 1F 1B 3C 88 31 01 80
FF FF 29 11 00 00 00 00 Le
code
………. 00
0x0FF0 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
Si l’écran vire au noir après l’écran de
licence c’est que notre code a été bien exécuté !
Troisième Test: Draw red triangle
On peut encore essayer une variante plus
intéressante pour être absolument certain que nous faisons bien exécuter notre
code sur la PSX. Nous allons afficher un triangle rouge sans réinitialiser le
GPU.
Par défaut, la PSX démarre avec un GPU
configuré en 640x480. Nous verrons dans la section suivante les commandes du
GPU, pour le moment essayons la commande suivante :
*Dessiner un triangle:
MOT 1=
0x20 BB GG RR
MOT 2=
Y0 X0
MOT 3 = Y1 X1
MOT 4 = Y2 X2
Dessine un triangle de couleur RGB et de
sommet S0(X0,Y0),S1(X1,Y1),S2(Y2,X2).
Les paramètres BB GG et RR représente les
composant bleu, vert et rouge code chacun sur 8 bits
Nous envoyons cette commande dans 0x1F80
1810 le registre de données (GPU0) du GPU.
PSX_001_3.asm:
Org
0x80010000
Start:
lui R27,0x1F80 ;R27 pointe vers I/O BASE
;Dessine
un triangle rouge avec les coordonnées suivante (20,400) (320,20) (620,400)
lui R8,0x2000 ;Commande GPU DRAW TRIANGLE 0x20 BB GG RR où
RGB désigne les couleurs RVB chacune codée sur 8 bits.
ori R8,R8,0x00FF ;RED TRIANGLE
sw R8,0x1810(R27) ;SEND CMD TO GPU 0x1F80 1810
lui R8,400 ;Y0=400
ori R8,R8,20 ;X0=20
sw R8,0x1810(R27) ;SEND PARAM TO GPU 0x1F80 1810
lui R8,20 ;Y1=20
ori R8,R8,320 ;X1=320
sw R8,0x1810(R27) ;SEND PARAM TO GPU 0x1F80 1810
lui R8,400 ;Y2=400
ori R8,R8,620 ;X2=620
sw R8,0x1810(R27) ;SEND PARAM TO GPU 0x1F80 1810
;END DRAW TRIANGLE
beq r0,r0,0xFFFF ;boucle infinie
nop
Résultat en détail de l’exécutable :
ADRESSE DATA COMMENTAIRE
0x0000 50 53 2D 58 20 45 58 45
00 00 00 00 00 00 00 00 CHAINE
“PS-X EXE”
0x0010 00 00 01 80 00 00 00 00
00 00 01 80 00 08 00 00 0x10:
PC (Où démarre notre Code) ; 0x14 : GP ; 0x18 : Segment
TEXT ; 0x08: Taille du Segment TEXT ;
0x0020 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
0x0030 F0 FF 1F 80 00 00 00 00
00 00 00 00 00 00 00 00 0x30:
Initialisation de la PILE
………. 00
0x0800 80 1F 1B 3C 00 20 08 3C
FF 00 08 35 10 18 68 AF ; Notre
code
0x0810 90 01 08 3C 14 00 08 35
10 18 68 AF 14 00 08 3C ; Notre
code
0x0820 40 01 08 35 10 18 68 AF
90 01 08 3C 6C 02 08 35 ; Notre
code
0x0830 10 18 68 AF FF FF 00 10
00 00 00 00 00 00 00 00 ;
Notre code
…….. 00
0x0FF0 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
Si nous exécutons ce code et que nous
voyons apparaitre un triangle rouge (Transparent car affiche une ligne sure
deux et scintillant) c’est que nous avons réussi à faire faire exécuter du code
à notre Playstation ! J
PSX_001_3.exe sous
emulateur PSX_001_3.exe
sous Playstation SCPH-7502
Remarque importante:
Attention n’essayer pas d’envoyer d’autres
commandes pour l’instant. Le programme fonctionnera si vous n’envoyez qu’une
seule commande.
Sur ma PSX les artefacts sont
systématiques car pas de synchronisation avec le retour de balayage. Donc si
vous envoyer d’autre commande il se peut que vous n’obtiendrai pas l’effet
escompté à cause de la limite du cache de commande du GPU et de l’absence de
synchronisation verticale.
7-
CD/ROM ET BOOT
Une fois notre exécutable généré, comment
faire pour produire notre CD bootable pour notre PSX. Avant cela il s’agit de
contourner le système de protection. Et oui la Playstation est protégée de
lecture de tout CD/R. Fort heureusement cette protection est relativement
facile à contourner.
Le code de protection est composé de 4
bytes ASCI codés sur le WOBBLE du CD/ROM :
« SCEI » JAPON
« SCEE » EUROPE
« SCEA » AMERIQUE
Le code pays de la Playstation est
localisé en ROM.
Cette protection fait donc deux pierres
d’un coup.
Elle empêche la Lecture de CD/ROM dont le
code pays est diffèrent de la PSX.
Le lecteur de la Playstation lit le WOBBLE
du CD et en fonction de sa localité elle vérifie la conformité en fonction de
la chaine.
Les conséquences est qu’il est impossible
de
-
Faire
démarrer un jeu japonais sur une console européenne ou américaine et vice
versa.
-
Un
CD-R contiendra évidemment un WOBBLE non conforme : La PSX refusera de
booter avec ce CD.
Etant donné qu’il nous est impossible (du
moins pour le commun des mortels) de modifier le Wobble d’un CD-R il nous faut
utiliser d’autres alternatives :
-
Une
première solution c’est celle que j’utilise est d’installer un ModChip sur sa
PSX : celui-ci court-circuite les lectures du WOBBLE et envoie vers le CPU
la chaine conforme.
Il existe beaucoup de variante
d’installation et qui dépendent également de la version de notre PSX.
J’espère faire un petit tuto la dessus avec
un Attiny13 de chez Amtel (la plupart des modchip sont des PIC à 8 broches mais
les AVR ont incontestablement l’avantage d’une reprogrammation pratiquement
sans limite (10000 réécriture assurée ça donne de la marge) et un simple
programmateur USB-asp pour microcontrôleur AVR se trouve pour moins de 5euros).
-
Une
deuxième solution est d’acheter une cartouche Action Replay (ou se le faire)
qui se branche sur le port parallèle de la PSX et qui court-circuite la ROM et
qui permet de programmer la PSX plus facilement en chargeant des programmes via
carte-mémoire ou bien en court-circuitant la protection.
-
Une
solution de fortune est la technique de changement de CD : en gros on boot
la PSX avec un CD officiel, une fois l’écran de présentation affiché des que le
CD ralentit, on change immédiatement le CD officiel par notre CD-R. Une fois
fait, le CD se met à tourner vite et ralenti immédiatement remettre le
CD-officiel et c’est là que cela devient dure : tout de suite après que
l’écran disparaissent, on remet son CD-R. Le programme sur notre CD-R devrait
démarrer normalement.
-
Enfin
une bonne solution est d’utiliser un CD-Demo officiel que l’on dupliquera
partiellement et on remplacera les exécutables présents par nos programmes
(voir plus loin). C’est la solution pour ceux qui possède une PSX non
« modee » et qui ne veulent prendre le risque d’endommager leur
console et qui ne veulent pas passez par la technique du swap très
contraignante.
Toujours est-il, l’utilisation d’un
émulateur indispensable pour l’apprentissage mais attention car ceux-ci sont
encore très loin du fonctionnement de la vrai PSX : si globalement ils
émulent correctement la plupart des jeux ils émuleront en revanche très mal le
vrai fonctionnement de la PSX et de surcroît toutes les bêtises que nous
pouvons faire en programmant directement dessus !!!
Une fois passez cette protection, il y a parait-il
d’autre protections mineurs mais qui, semble-t-il, dépendant de la ROM:
- Licence qui consiste en 37632 octets
(les 16 premiers secteurs) (en mode 2, un secteur fait 2352 octets) se présente
ainsi qui contient la licence d’utilisation. (Il est facile d’extraire cette
licence à partir de n’importe quelle CD.ROM playstation).
- Le Volume doit s’appeler PLAYSTATION sur
le secteur de boot (0x9320 Secteur 16) et le nom en 0x9340.
J’avoue ne pas comprendre comment ces
protections agissent. Toutes les démos et autres programmes ont très bien
fonctionné sans.
Je n’ai pas le courage de vérifier sur
quelles ROMs ça marche et sur lesquelles non : j’utilise la licence et
basta.
Que doit contenir notre CD/ROM pour que la
PSX puisse booter ?
Tout simplement notre exécutable (toujours
aligné sur 2048 octets) et un fichier texte de format Unix/AMIGA/LINUX (ligne
séparé seulement par 0x0A) nommé « SYSTEM.CNF » qui est un fichier
text dans lequel nous devons indiquer à l’OS-PSX l’exécutable à booter plus
quelques infos supplémentaires :
BOOT =
cdrom:\NOTREBOOT.EXE;1
TCB = 4
EVENT = 10
STACK = 801FFF00
Je donne le fichier SYSTEM.CNF par défaut.
TCB indique le nombre max de thread.
Ici notre exécutable se nomme évidemment
« NOTREBOOT.EXE »
Attention ! Veuillez que le nom de
l’executable ne depasse pas 10 caracteres (Extention incluse) et utiliser que
des caracteres majuscule.
Deuxième chose importante : Lorsque
vous gravez vos CD, choisissez obligatoirement le MODE 2 avec pour 2352 octet
la taille d’un secteur. La PSX ne gère que ce format.
Pour créer le CD/ROM, perso je fais comme
ça :
-
J’utilise
le pack MODE2Create que vous trouverez ici
-
Le
programme BurnAtONCE
Avec MODE2CREATE je créer le BIN du CD ou
j’insère le fichier SYSTEM.CNF et l’exécutable + éventuellement d’autre
données. (Pour que le bin soit au moins égale à 768 ko)
Exemple de commande :
mode2cdmaker -f "SYSTEM.CNF" -f
"MYPSX.EXE" -f "FICHIER.DAT" -o MYPSX
Ensuite je remplace les premiers 16
secteurs du BIN par la licence avec une utilitaire qui s’écrit en 10 minutes.
Une fois le BIN convertit créer je crée le
fichier CLUE correspondant en ajoutant éventuellement des pistes musicales et ensuite
j’utilise BURNEATONE pour Graver le CD.
Le plus important et que le BIN soit
suffisamment volumineux (au moins 768 ko) c’est pourquoi il faut insérer des
fichiers superflus dans le bin.
Lancement des programmes avec une PSX sans modchip sans disc swap avec un
CD démo officiel :
Comme cite plus haut une bonne méthode
pour démarrer nos programmes est de disposer d’un CD démo officiel. On effectue
une copie complète du BIN du CD. Ensuite avec un programme spécialisé dans
l’extraction et la génération de fichier bin de CD on remplace les exécutable
par les nôtres. Ensuite on boot avec la démo officiel, une fois que le menu
s’affiche, on retire le CD officiel et on le remplace par le nôtre. Et lors du
lancement d’une démo, nos exécutables devraient se lancer.
Tout d’abord nous devons comprendre
comment le CD-ROM en mode 2/From1 est décomposées en secteurs et ces derniers
se présente de cette façon :
Le Champs Sync contient tout bêtement 12
octets : 00 FF FF FF FF FF FF FF FF FF FF FF
3 octets pour l’adresse du secteur et 1
bit pour le mode. Par exemple le secteur 16 d’un CD playstation
contiendra :
00 00 02 00 (pour l’adresse du secteur) et
02 (Pour le mode).
Ensuite viennent les données de 2336 mais
sachant qu’un secteur logique de données sera limite a 2048 octets on a en
faites :
Mode2/Form1 (CD-XA)
000h 0Ch Sync 00Ch 4 Header (Minute,Second,Sector,Mode=02h) 010h 4 Sub-Header (File, Channel, Submode AND
DFh, Codinginfo) 014h 4 Copy of Sub-Header 018h 800h Data (2048 bytes) 818h 4 EDC (checksum accross [010h..817h]) 81Ch 114h ECC (error
correction codes) |
C’est à dire que les données proprement
dites vont de 0x018 à 0x818.
Mais si on remplace un secteur par
d’autres données on devra obligatoirement générer l’EDC et l’ECC. Comme c’est
assez complexe et que le sujet de cet article est la programmation de la
playstation et non comment un CD-R est formaté en mode2/form1 vous pouvez
sauter cette partie : vous prenez l’utilitaire et basta : le plus
important pour le lecteur est de pouvoir exécuter du code-machine écrit par
lui-même sur une Playstation sans mode-chip.
La fiabilite d’un CD-ROM etant ce qu’elle
est, il est imperatif d’utiliser, pour chaque secteur, un code correcteur. Un
codage Red-Salomon est utilisee et va s’appliquer deux fois consecutivement.
Comme c’est assez technique et que le
sujet de cet article est la programmation de la playstation je ne detaillerais
pas mais je prevois un article specialement sur le sujet.
Tout d’abord il s’agit de lire le Root
directory.
Dans le secteur 16, à l’offset 158 (sur 2
octets) se trouve le numéro du secteur du Root.
Dans le root secteur chaque entrée
est identifié par :
OFFS
0 short
len
2-6 int
sector Little Endian
6-9 int
sector Big Endian
10-13 int file size Little Endian ;A
modifier ??
14-17 int File size Big Endian ;
Amodifier ??
18-24 Date information ;Do
not care
25 byte
typefile (flags) 2:dir;0
- > FILE
26 File
Unit size 0
27 Interleave
Gap Size 0
28-31 Volume Sequence Number
32 byte
namelen
33 name
file (rest of len).
Donc il suffit simplement d’identifier un
fichier exe suffisamment grand pour le remplacer par le nôtre.
PS : Pour l’instant ca ne marche pas malgre
les bon EDC/ECM test et modification du size ?
Vous trouverez ici un petit utilitaire
écrit en JAVA et qui contient une classe JAVA ZawaCDPatch qui nous permettra
entre autre de remplacer un fichier exécutable par le nôtre (sans changer le
nom du fichier). Les codes sources étant dispo vous pourrez par la suite le
compléter si cela vous chante.
8-
Introduction au GPU
La programmation directe du GPU qui semble
facile au premier abord peut s’avérer assez délicate si l’on ne fait pas
attention à quelques détails importants.
Les plus importants sont :
-
le
cache de commande de (64 octets)
-
le
retour vertical.
Pour éviter les problèmes de cache il vaut
mieux utiliser une linked list de dessin et la DMA. Cependant pour
l’apprentissage nous utiliserons au début l’envoie de commande direct nous
verrons les « linked list » plus tard. Si nous ne surveillons pas le
cache et que nous utilisons l’envoi de commande direct en I/O, il nous faudra systématiquement
utiliser la fonction WaitGPUReady (que nous verrons tout
à l’heure). Le problème avec le retour vertical nous limite dans le nombre de
dessin que peut effectuer le GPU durant un inter-frame.
Typiquement en dessin direct par I/O, nous
somme limité à un effacement de l’écran + 2 triangles texturés ou bien 6
triangles non texturée.
J’insiste sur ces points car aucun
émulateur ne simule le vrai comportement du GPU lorsque le cache est plein ou
lorsque nous dessinons durant un frame. (Même Xebra).
De plus le GPU semble déclencher des exceptions
(Je n’ai pas encore identifié les conditions ni comment contrôler ce genre
d’évènement).
Le GPU peut accéder à la mémoire via la
DMA ou nous pouvons envoyer un liste d’instruction préparée avant nous verrons
cela lors de la section sur la DMA.
De plus il semble y avoir plusieurs
versions du GPU les différences semblent être minimes mais j’avoue qu’il m’est
impossible de les étudier hormis avec la GPULIB.
Le GPU de ma Playstation me renvoi 0x0000
0002 lorsque j’utilise la commande GetInfo (0x1000 0007).
Le GPU possède une VRAM de 1024*512 16
bits. Le processeur ne peut écrire directement dessus.
Les images peuvent être organise en :
-
16 bits (1 bit de transparence + BGR sur 5 bits chacun).
-
8 bits (1 octet par pixel ou la valeur de l’octet est un index dans une table
de couleur de 256 entrées dans la VRAM)
-
4 bits (4 pixels par demi-mots ou la valeur de 4 bits est un index dans une
table de couleur 16 entrées dans la VRAM)
Les modes d’affichage pour la résolution
verticale sont de 240 ou 480. Pour cette dernière valeur, l’entrelacement est
activé.
Nous n’aborderons pas ce mode dans cet
article car l’impossibilité d’implémenter un double buffer plus les problèmes
de dessin qu’elle engendre (celui-ci affiche une ligne paire et impaire alternativement :
il faut imperativement passer par DMA) rend ce mode assez delicat (même si la
PSX démarre dans ce mode).
Pour simplifier, on se bornera ici à
configurer le GPU en 320x240.
Nous avons vu dans le deuxième et
troisième programme de test la façon dont on envoie des commandes vers le GPU
pour lui faire réaliser certaines choses.
Les commandes sont de deux types :
Envoyer dans le registre CTRLPORT 0x1814
(que nous appellerons GPU1) ou envoyer dans le registre 0x1810 DATAPORT (que
nous appellerons GPU0).
Les commandes du GPU1 sont dédiées à la
configuration du GPU, gestion de la DMA, le mode d’affichage et la zone de la
VRAM à afficher.
Les commandes du GPU0 sont dédiées au
dessin, aux transferts MEM<->VRAM et autre configurations de la zone de dessins
etc.
La lecture du GPU1 nous permet également
d’accéder aux états de celui-ci.
0-3 Texture page
X Base (N*64) ;GP0(E1h).0-3
4 Texture page Y Base (N*256) (ie. 0 or 256) ;GP0(E1h).4
5-6 Semi
Transparency (0=B/2+F/2, 1=B+F,
2=B-F, 3=B+F/4) ;GP0(E1h).5-6
7-8 Texture page colors (0=4bit, 1=8bit, 2=15bit, 3=Reserved) ;GP0(E1h).7-8
9 Dither 24bit to 15bit (0=Off/strip LSBs, 1=Dither
Enabled) ;GP0(E1h).9
10 Drawing to display area (0=Prohibited,
1=Allowed) ;GP0(E1h).10
11 Set Mask-bit when drawing pixels (0=No,
1=Yes/Mask) ;GP0(E6h).0
12 Draw Pixels (0=Always, 1=Not to Masked areas) ;GP0(E6h).1
13 "reserved" (seems to be always set?)
14 "Reverseflag" (0=Normal, 1=Distorted) ;GP1(08h).7
15 Texture Disable (0=Normal, 1=Disable Textures) ;GP0(E1h).11
16 Horizontal Resolution 2 (0=256/320/512/640, 1=368) ;GP1(08h).6
17-18
Horizontal Resolution 1 (0=256,
1=320, 2=512, 3=640) ;GP1(08h).0-1
19 Vertical Resolution (0=240, 1=480, when Bit22=1) ;GP1(08h).2
20 Video Mode (0=NTSC/60Hz,
1=PAL/50Hz) ;GP1(08h).3
21 Display Area Color Depth (0=15bit, 1=24bit) ;GP1(08h).4
22 Vertical Interlace (0=Off, 1=On) ;GP1(08h).5
23 Display Enable (0=Enabled, 1=Disabled) ;GP1(03h).0
24 Interrupt Request (IRQ1) (0=Off, 1=IRQ) ;GP0(1Fh)/GP1(02h)
25 DMA / Data Request, meaning depends on
GP1(04h) DMA Direction:
When
GP1(04h)=0 ---> Always zero (0)
When
GP1(04h)=1 ---> FIFO State (0=Full,
1=Not Full)
When
GP1(04h)=2 ---> Same as GPUSTAT.28
When
GP1(04h)=3 ---> Same as GPUSTAT.27
26 Ready to receive Cmd Word (0=No, 1=Ready) ;GP0(...)
;via GP0
27 Ready to send VRAM to CPU (0=No, 1=Ready) ;GP0(C0h)
;via GPUREAD
28 Ready to receive DMA Block (0=No, 1=Ready) ;GP0(...)
;via GP0
29-30 DMA
Direction (0=Off, 1=?, 2=CPUtoGP0, 3=GPUREADtoCPU) ;GP1(04h).0-1
Les commandes envoyées vers le GPU1 sont
en générales pour la configuration de notre GPU, des modes, de la zone à
afficher etc.
CMD 0x00 RESET GPU
Paramètres : 0x0000: Initialisation du
GPU. En fonction de la PSX, rétablit les paramètres initiaux. « Enable
Display » est FALSE ce qui explique pourquoi l’écran devient tout
noire.
CMD 0x01 RESET COMMAND BUFFER
Parametres:0x0000 Initialisation du cache de commande FIFO.
CMD 0x02 RESET IRQ
Paramètres: 0x0000 ?????
CMD 0x03 DISPLAY ENABLE/DISABLE
Paramètres : 0x0000 ENABLE 0x0001 DISABLE
CMD 0x04 DMA SETUP
Paramètres : 0x0000 DMA DISABLE 0x0002 CPU TO GPU 0x0003 GPU TO CPU
CMD 0x05 DISPLAY AREA
Paramètres bit
$00-$09 X (0-1023) bit $0A-$12
Y (0-512)
Définition des coordonnées de départ de la
zone à afficher
CMD 0x06/0x7 HORIZONTAL/VERTICAL
DISPLAY
PAL: gpu_ctrl(6,
0xC62262); // Horizontal screen range gpu_ctrl(7,
0x04B42D); // Vertical screen range
NTSC: gpu_ctrl(6, 0xC4E24E); // Horizontal screen range gpu_ctrl(7, 0x040010); // Vertical
screen range
CMD 0x08 SETDISPLAY MODE
Paramètres: 0x08 0 0 RF W1 INT CB VM H W0W0
RF: Reverse
Flag (a essayer)
W1: Voir
Width
INT: Interruption Enable/Disable
CB: Color
bits 15/24
VM: Video
Mode NTSC/PAL
H: Resolution
H 240/480
W0: Resolution
W tel que:
W0(b1-b0) W1(b6)
00 0 256 pixels
01 0 320
10 0 512
11 0 640
00 1 384
Data Register 0x1f80 1810 WRITE (GPU0)
On peut classer les commandes du GPU0 en 3
groupes, les commandes de configuration, les commandes de transfert et les
commandes de dessin de primitives.
En fonction des bits B31-B30-B29 on
a :
B31 B30
B29
0 0 0 N/A
Ce sont théoriquement des commandes à envoyer au GPU1 que nous avons vues.
0 0 1 Dessin
Polygone (3/4 sommets)
0 1 0 Dessin
Line
0 1 1 Dessin
Sprite
1 0 0 Transfer
1 1 1 Commande
de configuration
Commande de configuration :
Dans le port GPU0, les commandes de
configuration servent à définir les parties où nous pouvons dessiner, nous
verrons plus tard ces commandes lors de l’implémentation du double-buffer.
DRAW AREA START (0xE3):
Word 0: 0xE3 00 PP PP PP Paramètres
PP : X=b0-b9 Y=b10-b19
DRAW AREA END (0xE4):
Word 0: 0xE3 00 PP PP PP Paramètres
PP : X=b0-b9 Y=b10-b19
DRAW AREA OFFSET (0xE5):
Word 0: 0xE3 00 PP PP PP Paramètres
PP : X=b0-b9 Y=b10-b19
DRAW MASK SETTING (0xE6):
Word 0: 0xE3
00 00 00 0P Paramètres P b0 :
0 n’applique pas de masque 1 :
Applique un masque dans la VRAM au moment du dessin: b15=1
b1 :
0 ignore les masques 1 :
Prend en compte les masques au moment du dessin
Tout d’abord avant d’envoyer de dessiner
quoi que ce soit il nous faut correctement initialiser notre GPU et préparer
quelques fonctions pour surveiller l’état du GPU.
Fonction InitGPUSimple :
Cette fonction réinitialise le GPU passe
en mode 320*256 en 16 bits en mode PAL. Place la zone d’affichage en 0,0 dans
la VRAM.
InitStdGPU:
;R27 POINT TO I/O BASE
lui R27,0x1F80 ; POINT TO I/O BASE
lui R8,0x0000 ; CMD=RESET GPU
sw R8,0x1814(R27) ; SEND CMD
;HORIZONTAL
lui
R8,0x06C6
ori
R8,R8,0x2262
sw
R8,0x1814(R27)
;VERTICAL
lui
R9,0x0704
ori
R9,R9,0xB42D
sw
R9,0x1814(R27)
;DISPLAY AREA => WHERE TO DISPLAY
(TOP/LEFT)
lui R8,0x0500 ; CMD=0x05
ori
R8,R8,0x0000 ; 0,0
sw R8,0x1814(R27) ; SEND TO GPU1
;SET DRAW MODE => TEXTUE PAGE AND DRAW
INTO DISPLAY ENABLE
lui
R9, 0xe100
ori
R9,R9,0x0400 ; draw mode: B18=1 Draw to display area permitted
sw
R9,0x1810(R27) ; SEND TO GPU0
;SET DRAW AREA START=> FROM WHERE WE
CAN DRAW
lui R8, 0xe300
ori
R8,R8,0x0000 ; 0,0
sw R8,0x1810(R27) ; SEND TO GPU0
;SET DRAW AREA END BOTTOM/RIGHT => TO
WHERE WE CAN DRAW
lui R9, 0xe407 ; 511
ori
R9,R9,0xFFFF ; 1023
sw R9,0x1810(R27)
;SET DRAW AREA OFFSET => WHERE DRAW
lui
R8, 0xe500 ; DRAW OFFSET
ori
R8,R8,0x0000 ; OFFSET 0
sw
R8,0x1810(R27)
;MASK SETTINGS NONE
Lui R2,0xE600 ; Mask setting : Désactive les masques.
Sw r2,0x1810(R27) ; SEND TO GPU1
;MODE PAL 320*256
lui
R9,0x0800 ;R12-> CMD 8 SET MODE
ori R9,R9,0x0009 ;B3=1->PAL B4=0 =>16 BIT B0=1->W=320
sw
R9,0x1814(R27) ;SEND TO GPU1
;DMA DISABLE
lui
R8,0x0400 ;DMA DISABLE
sw R8,0x1814(R27) ;SEND TO GPU1
;ENABLE DISPLAY
lui
R9,0x0300 ;CMD=0x03
ori R9,R9,0x0000 ;B0=0 => ENABLE DISPLAY
sw
R9,0x1814(R27) ;SEND TO GPU1
;RETURN
jr
R31 ;And return
nop
Fonction WaitGPUIdle :
Cette fonction attend que le GPU soit prêt
à recevoir une liste de commande par DMA. Le GPU est dit IDLE si le bit 28 est
allumé. Cette fonction sera utilisée plus tard pour l’envoi de liste chainée de
commande par DMA.
WaitGPUIdle:
.GPUNOTIDLE:
lw
R2,0x1814(R27)
lui R3,0x1000 ;MASK TEST B27 GPU BUSY
and
R2,R2,R3
bne
R2,R3,.GPUNOTIDLE ;NO 0x0400 0000 so WAIT
nop
;RETURN
jr
R31 ;And return
nop
Fonction WaitGPUReady :
Cette fonction attend que le GPU soit prêt
à recevoir des commandes. Le GPU est prêt si le bit 26 est allumé. Si une
commande en cours n’est pas complétée ou si le cache de commande est plein, Le
bit 26 sera éteint. Cette fonction est très importante surtout lorsqu’au début
en tant que débutant on utilise souvent l’envoi de commande direct par I/O.
WaitGPUReady:
.GPUNOTREADY:
lw
R2,0x1814(R27)
lui
R3,0x0400 ;MASK TEST B26 GPU NOT READY
and
R2,R2,R3
bne
R2,R3,.GPUNOTREADY ;NO 0x0400 0000 so WAIT
nop
jr
R31 ;And return
nop
Commande de Dessin :
En fonction de la commande les paramètres varieront.
Je ne vais pas faire une liste exhaustive (elle se trouve dans les liens au
début).
On va juste voir quelques exemples de
dessins de primitives n’utilisant pas les textures.
On utilise les coordonnées de
l’écran :
X : coordonnées horizontal de gauche
à droite
Y : coordonnées vertical de haut en
bas.
Ligne Monochrome (0x40):
Word 0: 0x40
BB GG RR BB GG RR
désignent les couleurs bleu, vert et rouge respectivement codées sur 15 bits
(le bit supérieur pour la transparence).
Word 1: YY
YY XX XX Coordonnées
vertical (Y) et horizontal (X) du sommet 0 SIGNEES
Word 2: YY
YY XX XX Coordonnées
vertical (Y) et horizontal (X) du sommet 1 SIGNEES
Triangle simple (0x20):
Word 0: 0x20
BB GG RR BB GG RR
désignent les couleurs bleu, vert et rouge respectivement codées sur 15 bits
(le bit supérieur pour la transparence).
Word 1: YY
YY XX XX Coordonnées
vertical (Y) et horizontal (X) du sommet 0 SIGNEES
Word 2: YY
YY XX XX Coordonnées
vertical (Y) et horizontal (X) du sommet 1 SIGNEES
Word 3: YY
YY XX XX Coordonnées
vertical (Y) et horizontal (X) du sommet 2 SIGNEES
Triangle Gradue (Ombrage Garound)
(0x30):
Word 0: 0x30
BB GG RR BB GG RR
désignent les couleurs bleu, vert et rouge respectivement codées sur 15 bits
(le bit supérieur pour la transparence) du sommet 0.
Word 1: YY
YY XX XX Coordonnées
vertical (Y) et horizontal (X) du sommet 0 SIGNEES
Word 2: N/A
BB GG RR BB GG RR
désignent les couleurs bleu, vert et rouge respectivement codées sur 15 bits
(le bit supérieur pour la transparence) du sommet 1.
Word 3: YY
YY XX XX Coordonnées
vertical (Y) et horizontal (X) du sommet 1 SIGNEES
Word 4: N/A
BB GG RR BB GG RR
désignent les couleurs bleu, vert et rouge respectivement codées sur 15 bits
(le bit supérieur pour la transparence) du sommet 2.
Word 5: YY
YY XX XX Coordonnées
vertical (Y) et horizontal (X) du sommet 2 SIGNEES
Rectangle simple (0x60):
Word 0: 0x60
BB GG RR BB GG RR
désignent les couleurs bleu, vert et rouge respectivement codées sur 15 bits
(le bit supérieur pour la transparence).
Word 1: YY
YY XX XX Coordonnées
vertical (Y) et horizontal (X) du sommet inferieur gauche SIGNEES
Word 2: HH
HH WW WW Largeur (512 max)
et longueur (1024 max) du rectangle
Remplissage (0x02):
Cette commande est quasi identique au
rectangle simple, la différence et que celle-ci ignore les masks et les 4
premiers bits de la position horizontale et donc de surcroît est plus rapide.
Word 0: 0x02
BB GG RR BB GG RR
désignent les couleurs bleu, vert et rouge respectivement codées sur 15 bits
(le bit supérieur pour la transparence).
Word 1: YY
YY XX X0 Coordonnées
vertical (Y) et horizontal (X) du sommet inferieur gauche. 4 premiers bits
inferieurs sont ignorés.
Word 2: HH
HH WW WW Largeur (512 max)
et longueur (1024 max) du rectangle.
Pixel (0x68):
Word 0: 0x68
BB GG RR BB GG RR
désignent les couleurs bleu, vert et rouge respectivement codées sur 15 bits
(le bit supérieur pour la transparence).
Word 1: YY
YY XX XX Coordonnées
vertical (Y) et horizontal (X) du pixel SIGNEES
Fonction GPUClearDisplayVRAM:
Cette fonction efface (remplit en noire)
la VRAM en 0,0 sur 320,256.
;CLEAR SCREEN 320*256 at 0,0
;ASSUME THAT R27=0x1F80
GPUClearDispayVRAM:
lui
R2,0x0200 ; 0x02 FILL DRAW BUFFER B=0
ori R2,R2,0x0000 ; GR=0
lui
R3,0x0100 ; HEIGHT 256
ori R3,R3,0x0140 ; WIDTH 320
sw
R2,0x1810(R27) ; SEND CMD TO GPU0 FILL DRAW BUFFER WITH
BLACK COLOR
sw
R0,0x1810(R27) ; SEND PARAM TO GPU0 TOP/LEFT=0
sw
R3,0x1810(R27) ; SEND PARAM TO GPU0 WIDTH*HEIGHT=320*256
jr R31
nop
Fonction R3000Delay :
Cette fonction est une autre petite
bricole pour nos programmes de début. Elle attend un certain temps en fonction
du registre R4 passé en paramètre.
;MISC FUNCTIONS
;$A0 => HOW MANY Tick to wait
;33MHZ 33.000.000 instruction per seconde
;loop is 4 cycles
;So
$A0=0x0080 0000 to wait above 1 second
M3000Delay:
addi
$A0,$A0,0xFFFF ;Decrement $A0 1 cycle
bgtz
$A0,M3000Delay ;Repeat if $A0>0 2 cycles
nop ;branch delay 1 cycle
jr
$RA ;Return 2
cycles
nop ;Branch delay 1
cycle
Les 5 fonctions que nous venons de voir ne
seront pas répétées dans les listings afin d’économiser de la place.
PSX_02.asm:
Très, très simple programme utilisant les
fonctions vues précédemment : initialise le GPU à 320x256 en 16 bits en
PAL.
Le programme utilise les commandes 0x02
(Remplissage ecran), 0x30 (Triangle ombré) et 0x60 (Rectangle simple) vues
précédemment.
Start:
jal
InitStdGPU ;GPU en MODE PAL 320*256 écran visible en 0,0
de la VRAM
nop
Mainloop:
jal
WaitGPUReady
nop
;EFFACE L’ECRAN PRINCIPAL
Jal GPUClearDisplayVRAM
nop
;DESSINE
UN TRIANGLE GRADUEE AVEC DES SOMMET ROUGE BLEU ET VERT
;VERT
0
lui R8,0x3000 ;CMD 0x30
ori R8,R8,0x00FF ;RED
VERT 0
sw
R8,0x1810(R27) ;SEND CMD TO GPU 0x1F80 1810
lui R9,200 ;Y0=200
ori
R9,R9,20 ;X0=20
sw R9,0x1810(R27) ;SEND
CMD TO GPU 0x1F80 1810
;VERT 1
lui
R8,0x00FF ;BLUE
ori
R8,R8,0x0000 ;BLUE
sw
R8,0x1810(R27) ;SEND CMD TO GPU 0x1F80 1810
lui R9,20 ;Y1=20
ori
R9,R9,160 ;X1=160
sw R9,0x1810(R27) ;SEND CMD
TO GPU 0x1F80 1810
;VERT 2
lui
R8,0x0000 ;GREEN
ori
R8,R8,0xFF00 ;GREEN
sw
R8,0x1810(R27) ;SEND CMD TO GPU 0x1F80 1810
lui R9,200 ;Y2=200
ori
R9,R9,300 ;X2=300
sw R9,0x1810(R27) ;SEND
CMD TO GPU 0x1F80 1810
;Court
delai
jal M3000Delay
lui
$A0,0x0020
jal
WaitGPUReady
nop
;DESSINE UN RECTANGE PLEIN ROUGE 40,20 20x20
lui R8,0x6000 ;0x60 FILL DRAW BUFFER B=0
ori R8,R8,0x00FF ;RED
lui
R9,10 ;Y=60
ori
R9,R9,10 ;X=240
lui
R10,20 ;H=20
ori
R10,R10,20 ;W=20
sw
R8,0x1810(R27) ;SEND CMD TO GPU
sw
R9,0x1810(R27)
sw
R10,0x1810(R27)
;DESSINE UN RECTANGE PLEIN VERT 40,20 20x20
lui R8,0x6000 ;0x60 FILL DRAW BUFFER B=0
ori R8,R8,0xFF00 ;VERT
lui
R9,10 ;Y=20
ori
R9,R9,290 ;X=280
lui
R10,20 ;H=20
ori
R10,R10,20 ;W=20
sw R8,0x1810(R27)
sw R9,0x1810(R27)
sw R10,0x1810(R27)
;DESSINE UN RECTANGE PLEIN BLEU 40,20 20x20
lui R8,0x60FF ;0x60 Rectangle
ori
R8,R8,0x0000 ;BLUE
lui
R9,210 ;Y=200
ori
R9,R9,10 ;X=80
lui
R10,20 ;H=20
ori
R10,R10,20 ;W=20
sw R8,0x1810(R27)
sw R9,0x1810(R27)
sw R10,0x1810(R27)
;DESSINE UN RECTANGE PLEIN BLANC 40,20 20x20
lui R8,0x60FF ;0x60 Rectangle
ori
R8,R8,0xFFFF ;WITHE
lui R9,210 ;Y=200
ori
R9,R9,290 ;X=280
lui
R10,20 ;H=20
ori
R10,R10,20 ;W=20
sw
R8,0x1810(R27)
sw R9,0x1810(R27)
sw R10,0x1810(R27)
;Court delai
jal M3000Delay
lui
$A0,0x0020
;En Boucle
j Mainloop
nop
PSX_02.asm Des petits
carrés clignotant autour d’un triangle en ombrage Gouraud
ATTENTION, le programme ci-dessous
fonctionne sur une Vrai console. Mais comme je l’ai déjà dit, il faut prendre
garde lorsque l’on charge trop de commandes.
Utiliser GPUWaitReady pour être sûr que le
cache contienne au moins une entrée libre. Toutefois cela peut s’avérer
insuffisant car le GPU n’a pas toujours le temps d’exécuter toutes les
commandes avant la fin d’un retour vertical.
L’utilisation de la DMA, aborder plus
tard, résout le problème de cache pour ce qui est des commandes de dessin et le
double-buffering résoudra les problèmes pour les animations.
Conseils pour éviter les frustrations :
- Cache de commandes (64 octets).
- Une fois la commande exécutée, le cache
libère une place dans la file FIFO.
- Lorsque le cache est plein, le bit
GPUReady s’éteint.
- Lorsque l’on vide le cache, certaines
opérations en début de queue seront tous simplement annulées donc attention.
Pour éviter cela, il faut donc utiliser la fonction GPUWaitIdle avant de vider
le cache.
- Lorsque nous envoyons le premier mot
d’une commande, le bit GPUReady s’éteint et ne s’allumera que lorsque la
commande sera complétée par tous ses paramètres et que, bien sûr, le cache
contienne encore de la place pour l’envoi d’une nouvelle commande.
- Lors du mode de 480 ligne,
l’entrelacement est actif il faut donc dessiner deux fois une fois les lignes
paires et l’autre les lignes impaire en surveillant le retour horizontale
(Compteur 1).
- XEBRA semble bien mieux émuler le GPU
que tous les autres émulateurs. En tous cas tous les programmes graphiques
fonctionnant sous cet émulateur on toujours fonctionner sur ma PSX et presque
tous ceux qui ne fonctionnaient pas sous XEBRA ne fonctionnaient également pas
sur la PSX tout en fonctionnant parfaitement sur tous les autres Emulateurs.
Je n’ai pas essayé tous les jeux avec
XEBRA mais pour moi c’est actuellement le meilleur émulateur. Dommage qu’il ne possède pas de debugger et
c’est pour ce manque que j’encourage l’utilisation en complément de NoCache-PSX
ou eventuellement PSx1.13 pour la programmation.
9-
ROM et Routine Système.
La Playstation contient en ROM un OS
(Appelle PSX-OS) qui contient pas mal de routine basique d’entrée/sortie
d’initialisation des ports comme les manettes, le CD/ROM et de gestion des
évènements et interruptions. L’auteur de PS-NOCACHE critique fortement cet OS
lui reprochant ces bugs, sa gestion des events et surtout sa lenteur
(exécutions de certaines routines en section non-cache, gestion des
interruptions).
Il y a 2 types de services :
-
Les
services BIOS appelable avec l’instruction j 0xa0 j0xb0 j 0xc0
-
Les
services System (pour la Gestion des interruptions) appelable avec
l’instruction SYSCALL
Les SYSCALL Enter_Critical_Section (1) et
Exit_Critical_Section (2) inhibe et rétablit respectivement les interruptions.
Une des lib PSX Apres avoir booté,
désinstalle le driver du Lecteur CD entre un Enter et Exit critical section.
Puis réinstalle le driver. Pourquoi ?
Les services BIOS sont appelés via des
Jump J. Pour pouvoir appeler ces fonctions (j 0xA0 j 0xB0
j 0xC0) correctement il faut insérer les appelles dans une fonction avec
une instruction JAL de façon à ce que le service retourne correctement grâce
au registre R31.
Assez bizarrement, le numéro de fonction
est passe en registre R9 ($T1)
Les paramètres sont passe en en $A0-$A3
(R4-R7) et le reste dans la pile.
Les valeurs de retour sont en $V0 ($V1 si
64 bits) (R2/R3).
Exemple :
Jal ResetEntryInt
Nop
…………………
ResetEntryInt :
addi
R9,R0,0x0018
j
0xb0
nop
jr
R31
Nop
Importance de ResetEntryInt :
Un bon conseil, commencez toujours par
cette fonction avant de commencer votre programme. Cela vous évitera de
mauvaise surprise.
Le bios se sert d’une liste de tache à
effectuer lors d’une interruption, on réinitialise cette liste avec justement
la fonction ResetEntryInt.
Les exemples qui vont suivre nous
montrerons comment se servir des services du BIOS.
Service I/O :
Avant de se servir des services I/O pour
pouvoir charger et lire un fichier en CD/ROM, il nous faut correctement
initialiser certaine fonction dont la réinstallation du driver du CD/ROM et
ensuite le réinstaller.
Lorsque votre programme boot depuis le
CD/ROM un appel à ResetEntryInt (0xB0 0x18) suffit.
En revanche si vous démarrez votre
programme depuis un CD de démo officielle et que vous avez l’intention
d’utiliser les services du BIOS pour la lecture CD, il faudra utiliser la
sequence Remove96 et CDInit pour initialiser les drivers CD/ROM
Sequence d’initialisation:
; Réinstallation des services du CD/ROM
;TOUJOURS APPLER DANS NOTRE CODE BOOT
Start:
;RESET CALLBACK
Jal
ResetEntryInt ; Réinitialise la table des vecteurs
d’interruptions standard.
nop
;RESET HANDLE AND CONFIGURE CD/ROM POUR SON
UTILISATION SI VOTRE PROGRAMME NE DEMMARRE PAS DEPUIS UN BOOT
jal EnterCriticSec
nop
sh
R0,0x1074(R27) ; Disable all
interruption
sh
R0,0x1070(R27) ; IF interruption
should occur disable it
jal Remove96 ; DESINSTALLATION DES SERVICES I/O DU CD/ROM
nop
jal
ExitCriticSec
nop
jal
Init96 ; REINSTALLATION DU DRIVER POUR LE CD/ROM
nop
;$K1 pointe sur I/O hardware
Lui
R27,0x1F80
Main:
;SUITE DE NOTRE CODE
…………………………………….
;LES FONCTIONS CI-DESSOUS NE SERONT PAS
REPETER DANS LES EXEMPLE SUIVANTS !
ResetEntryInt:
addi
R9,R0,0x0018
j
0xb0
nop
jr
$RA
nop
EnterCriticSec:
ADDI
R4,R0,0x0001 ; Service 1 => ENTER_CRITICAL_SECTION
SYSCALL ; EnterCriticalSection
jr
$RA
nop
ExitCriticSec:
ADDI
R4,R0,0x0002 ; Service 2 => EXIT_CRITICAL_SECTION
SYSCALL ; ExitCriticalSection
jr
$RA
nop
Remove96:
addi R9,R0,0x0072
j
0xa0 ; DESINSTALLATION DU SERVICE ISO 96 CD/ROM
nop
jr
$ra
nop
Init96:
addi
R9,R0,0x0071
j 0xa0 ; INSTALLATION DU SERVICE ISO 96 DU CD/ROM
nop
jr
$RA
nop
Description des fonctions I/O standard :
Le numéro de fonction est passé en
registre R9
Les paramètres sont passés en en A0-A3
(R4-R7) et le reste dans la pile.
Les valeurs de retour sont en V0 (V1 si 64
bits) (R2/R3).
ATTRIBUTS
#define O_RDONLY 1
#define O_WRONLY 2
#define O_APPEND 8
#define O_RDWR (O_RDONLY
| O_WRONLY)
#define O_CREAT 512
#define O_TRUNC 1024
OUVERTURE FICHIER
Open j 0xb0
R9=0x32 unsigned
long open (char *devname, unsigned long flag)
($A0,$A1)
Devname: nom du fichier
Flag: voir flags.
La fonction retourne le fd du fichier ou -1 si
l’ouverture échoue.
FERMETURE FICHIER
Close j 0xb0
R9=0x36 long close (unsigned long
fd) ($A0)
fd: descriptor du fichier
La fonction retourne le fd du fichier ou -1 si la
fermeture échoue.
LECTURE FICHIER
Read j 0xb0
R9=0x34 long
read (unsigned long fd, void *buf, long n) ($A0,$A1,$A2)
fd File descriptor
buf Read buffer address
n Number of bytes to be read
La fonction retourne le nombre de octets lues.
DEPLACEMENT POINTEUR FICHIER
Lseek j 0xb0
R9=0x33 long
lseek (unsigned fd,long offset,long flag)
($A0,$A1,$A2)
Flags:
#define SEEK_SET 0
#define SEEK_CUR 1
#define SEEK_END 2
EXEMPLE CHARGEMENT ET LECTURE D’UN FICHIER :
;Exemple de chargement de fichier
;OPEN FILE
LUI A0,>FILENAME ;ADRESSE HAUTE DU FICHIER
ORI A0,A0,FILENAME ;ADRESSE BASSE DU FICHIER
ADDI A1,A1,0x0001 ; A1=1 => READ ONLY
;BIOS CALL : OPEN FILE
JAL OPENFILE
NOP
;TEST RETURN VALUE V0 R2
HANDLEFILE :
DW ? ; SAUVE LE HANDLE RETOURNE PAR LA FONCTION
FILENAME :
BYT "cdrom:\IMAGE.BMP;1" ; CD/ROM
FILEMEMCARD
BYT "bu01:IMAGE.BMP" ; Memory cards
OPENFILE:
J 0x0B ; FONCTION BIOS
ORI R9,R0,0x0032 ; Nr 0x32 => OPEN FILE
ATTENTION !!! cd-rom en petits caractères
sinon ça foire !!!
Lorsque on lit un fichier situe sur un
CD/ROM avec le service Read il est important que le buffer soit un multiple de
2048. Sinon ça foire !
Pareil pour l’offset qui doit être
multiple de 2048.
Exploration FILE I/O
struct
DIRENTRY {
char
name[20]; Filename
long
attr; Attributes (dependent on file system)
long
size; File size (in bytes)
struct
DIRENTRY
*next; Pointer to next file entry (for user)
char
system[8]; Reserved by system
};
Exemple:
;GET FIRST FILE OF DIR
lui $A0,> FileScan
ori
$A0,$A0, FileScan ;A0=>File
lui $A1,>DirEntry
ori
$A1,$A1,DirEntry ;A1=>DirEntry
jal
FirstFile
nop
ori
$S0,R0,0x0028
.Loop:
;NEXT FILE
lui
$A0,>DirEntry
ori
$A0,$A0,DirEntry ;A1=>DirEntry
jal
NextFile
nop
beq
R2,R0,.EndLoop
nop
;PRINT FileNAME
lui
$A0,>DirEntry
ori
$A0,$A0,DirEntry ;A0=>DirEntry
or
$A1,$S0,R0
sll
$A1,$A1,16
ori
$A1,$A1,0x0008
jal
Print4B
nop
addiu
$S0,$S0,0x0008
j
.Loop
nop
.EndLoop:
j
.End
nop
.End:
beq
R0,R0,0xFFFF ;
nop
FileScan:
asci
"cdrom:\?*",0
MemScan:
Asci
“bu00:?*”,0
DirEntry:
word
0,0,0,0
word
0,0,0,0
word 0,0,0,0
word
0,0,0,0
Attention !!! Les fonctions du BIOS
sont passablement boguer et peuvent détruire le contenu de la pile !!!
Pour éviter toute mauvais surprise
utilisée une zone statique pour la sauvegarde des registres ou pour la pile si
vous utilisez les fonctions du BIOS.
Gestion de la mémoire :
Stack (la pile) :
La pile (registre SP ou R29) est configure
par l’OS lors du boot de notre programme en fonction des paramètres dans notre
exécutable. Nous pouvons bien sûr reconfigurer l’emplacement mais franchement
je ne vois à aucun moment l’utilité. Placer la pile en 0x801FFFF0 et ne changez rien.
HEAP (le tas) :
Avant d’utiliser le TAS, nous devons
l’initialiser avec la fonction InitHeap.
Ensuite les fonctions malloc et free sont
sans surprises.
A(39h) - InitHeap(addr, size)
A(33h) - malloc(size)
A(34h) - free(buf)
Evidement vous pouvez ignorer le Tas est
gérer vous-même l’espace mémoire. Parait-il que les fonctions du Tas sont
egalement bugger et de plus Les fonctions du BIOS sont lentes donc c’est
souvent une bonne solution.
10-
Contrôleur PAD
Pour simplifier, on va utiliser les
routines du BIOS pour la lecture du PAD. Ici il y a juste un petit point
particulier et qu’avant il est nécessaire de s’occuper d’abord d’initialiser le
gestionnaire de carte mémoire avant d’initialiser le PAD lorsque on démarre à
partir d’un CD/ROM
Il semble que le BIOS soit bogué (encore)
à ce niveau. Si on essaye d’initialiser le PAD sans avoir initialiser le
gestionnaire de MEMORYCARD, le système plante.
Donc avant d’initialiser le PAD il faut
faire comme cela :
Initialisation standard :
;INIT INT VECTOR TABLE
Jal ResetEntry ;Réinitialise la file d’interruptions
nop
;INIT CARD
addi
$A0,R0,0x0001
jal
InitCard
nop
jal
StartCard
nop
jal
_Bu_Init
nop
jal
StopCard
nop
………………………………………..
;MEMORY CARD FUNCTIONS
InitCard:
addi
R9,R0,0x004A
j
0xb0
nop
jr
$RA
nop
StartCard:
addi
R9,R0,0x004B
j
0xb0
nop
jr
$RA
nop
StopCard:
addi
R9,R0,0x004C
j
0xb0
nop
jr
$RA
nop
BuInit:
addi
R9,R0,0x0070
j 0xa0
nop
jr
$RA
nop
Mais pour que ces routines system fonctionnent,
il faut impérativement d’abord appelé la fonction ResetEntry
POURQUOI? ->Car la fonction ResetEntry
initialise la file d’interruptions : La fonction d’entrée d’interruption
parcourt une liste de fonction à appeler lorsqu’une interruption se produit.
Les fonctions bios de lecture du PAD
ajoutent une entrée dans la queue des fonctions lorsqu’un VBlank apparait.
Donc c’est durant un retour vertical que
les données du PAD sont lues.
Lecture du PAD :
Les fonctions du BIOS installe dans l’interruption
du retour vertical, une routine qui lit l’état du PAD et écrit l’état à
l’adresse spécifié en paramètre. Cela nous permet également de vérifier
approximativement l’état d’un retour vertical.
Méthode 1 :
L’auteur de PS-NOCACHE déconseille fortement
l’utilisation la fonction 0x15. Mais comme c’est la première que j’ai utilisé
et qui a fonctionné (ouaaaa !!! le pad réponds !!) … J
Mais je pense que cette routine est la
source de beaucoup de probleme que j’ai rencontre… ???
On utilise la routine du BIOS PAD_INIT
0x0015 j 0xB0 à ne pas confondre avec InitPAD 0x0012 j 0xB0
Le paramètre en $A0 concerne le type de
PAD et le pad (1 ou 2) et on utilise toujours soit 0x2000 0001 ou 0x2000 0002.
Le paramètre en $A1 indique a quelle
adresse doit être stocké le résultat de la lecture du PAD. Réserver dans le
buffer 20 word en tout : je ne suis pas certain de l’utilité mais j’ai eu
le droit à de beau plantage lorsque je ne réservais que 4 word. (Je n’ai pas
depuis vérifié combien exactement on doit réserver).
;PAD_INIT AND VERTICAL SYNC
;THIS ROUTINE WORKS ON REAL PSX
PAD_INIT:
lui $A0,0x2000
ori
$A0,$A0,0x0001
lui $A1,>PadBufI
ori
$A1,$A1,PadBufI
addi R9,R0,0x0015
j
0xb0
nop
jr
$RA
nop
PadBufI:
word 0,0,0,0
PadData :
word
0,0,0,0
word
0,0,0,0
word 0,0,0,0
word 0,0,0,0
Ensuite pour lire le PAD, on va
continuellement lire le buffer, si celui-ci est vide (égale à 0) on attend
(nous ne sommes pas encore dans un retour vertical). Lorsque le buffer est
plein nous sommes dans un retour vertical et la routine transfert l’état du PAD
à l’adresse qui suit 4 word du PADBuffer.
;THIS ROUTINE WORKS ON REAL PSX
ReadPadStatus:
lui
$T1,>PadBufI
ori
$T1,$T1,PadBufI
.WaitVSync:
lw
$T0,0($T1)
nop
beq
$T0,R0,.WaitVSync
nop
lui
$T2,0xFFFF
ori
$T2,$T2,0xFFFF
xor
$T0,$T0,$T2
sw
$T0,16($T1) ;COPY TO PADDATAI
jr
$RA
nop
Ensuite on peut interpréter l’état des
boutons très simplement :
Mask des buttons du Pad :
;Lecture de l’état du PAD
lui R8,>PadDataI
ori R8,R8,PadDataI ;R8 pointe sur
PadDataI
lw R16,0(R8) ;Lecture
de PadDataI dans le Registre R16
nop
;MASK BUTTONS PAD
ori $T0,R0,0x8000 ;DROITE
ori $T0,R0,0x4000 ;BAS
ori $T0,R0,0x2000 ;GAUCHE
ori $T0,R0,0x1000 ;HAUT
ori $T0,R0,0x0080 ;MASK BUTTON CARRE
ori $T0,R0,0x0040 ;MASK BUTTON CROIX
ori $T0,R0,0x0020 ;MASK BUTTON ROND
ori $T0,R0,0x0010 ;MASK BUTTON TRIANGLE
ori $T0,R0,0x0100 ;MASK BUTTON SELECT
ori $T0,R0,0x0800 ;MASK BUTTON START
ori $T0,R0,0x0004 ;MASK BUTTON L1
ori $T0,R0,0x0001 ;MASK BUTTON L2
ori $T0,R0,0x0008 ;MASK BUTTON R1
ori $T0,R0,0x0002 ;MASK BUTTON R2
Le PADBuffer indiquant un retour vertical,
la WaitVSyncOnly attend d’être
dans un début de retour vertical pour rendre la main.
;THE PAD IS READ ON VBLANK
;SO WHEN THE PAD IS READ, PADBUF should be
0xFFFF FFFFF we are in VBLANK
;INSTEAD PADBUF is 0.
WaitVSyncOnly:
lui
$T1,>PadBufI
ori
$T1,$T1,PadBufI
;INIT
sw
R0,0($T1) ;WRITE 0 AND WAIT AS VBLANK SET THIS FIELD
.@1:
lw
$T0,0($T1) ;READ FIELD
nop
beq
$T0,R0,.@1 ;AND WAIT FOR START OF VBLANK
nop
jr
$RA
nop
Animation:
Grace à la fonction WaitVSyncOnly, on est
en mesure d’effectuer de petite (très petite) animation en simple buffer.
Exemple :
jal WaitGPUReady
nop
jal
WaitVSyncOnly
nop
;CLEAR SCREEN
jal
GPUClearVRAM
nop
;DRAW
jal
DrawTransTriTx
nop
Methode 2 :
Je n’ai toujours pas
essaye la deuxieme method avec la function InitPAD 0x0012 j 0xB0
PROGRAMME DEPUIS UN CD-DEMO OFFICIEL
Etant donné que les fonctions du BIOS sont
capables de détruire le contenu et le pointeur de pile, il faut dès le début de
notre programme sauver toutes les registres vitaux dans un espace statique dans
notre programme.
Ci-dessous un programme qui peut être
démarré d’un CD-Démo et fermer proprement après un appui sur select. Le
programme ne fait rien à part afficher un triangle bleu centrée.
ORG 0x8001 8000
Start:
;SAVE REGISTER ON STATIC SPACE (NOT ON
STACK BECAUSE BUGS OF PSX-OS)
lui
R2,>SaveRegister
ori
R2,R2,SaveRegister
sw
R31,0(R2) ;SAVE RA
sw
R29,4(R2) ;SAVE SP
sw
$K0,8(R2) ;SAVE K0
sw
$K1,12(R2) ;SAVE K1
sw
$S0,16(R2) ;SAVE S0
sw
$S1,20(R2) ;SAVE S1
sw
$S2,24(R2) ;SAVE S2
sw
$S3,28(R2) ;SAVE S3
sw
$S4,32(R2) ;SAVE S4
sw
$S5,36(R2) ;SAVE S5
sw
$S6,40(R2) ;SAVE S6
sw
$S7,44(R2) ;SAVE S7
;GPU INIT 320*240 PAL
lui R27,0x1F80 ;R27 pointe vers I/O BASE
jal InitStdGPU
nop
;SEQUENCE STANDARD : RESETENTRY
jal
ResetEntryInt
nop
;PREPARE function before using PAD_INIT
addi
$A0,R0,0x0001
jal
InitCard
nop
jal
StartCard
nop
jal
BuInit
nop
jal
StopCard
nop
;PAD
jal
PAD_INIT
nop
;******** NOTRE PROGRMME DEMMARRE VRAIMENT
ICI
;QUICK TEST DRAW A TRIANGLE
lui
R27,0x1F80 ;R27 pointe vers I/O BASE
;Dessine un triangle BLEU Centree
lui
R8,0x20FF ;Commande GPU DRAW TRIANGLE 0x20 BB GG RR où
RGB désigne les couleurs RVB chacune codée sur 8 bits.
ori R8,R8,0x0000 ;0000
sw
R8,0x1810(R27) ;SEND CMD TO GPU 0x1F80 1810
lui R8,200 ;Y0=200
ori
R8,R8,20 ;X0=20
sw R8,0x1810(R27) ;SEND PARAM TO GPU 0x1F80 1810
lui R8,20 ;Y1=20
ori
R8,R8,160 ;X1=160
sw R8,0x1810(R27) ;SEND PARAM TO GPU 0x1F80 1810
lui R8,200 ;Y2=200
ori
R8,R8,300 ;X2=300
sw R8,0x1810(R27) ;SEND PARAM TO GPU 0x1F80 1810
MainLoop:
;READ PAD DATA
jal
WaitVSync
nop
.ReadPad:
lui
$S1,>PadDataI
ori
$S1,$S1,PadDataI
lw
$S0,0($S1) ;PAD DATA ON $S0
nop
.Select:
andi
$T0,$S0,0x0100 ;Button Select
beq $T0,R0,MainLoop ;NON ON
BOUCLE
nop
;******** FIN DE NOTRE PROGRAMME
.EndProg:
;STOP PAD
jal
StopPad
nop
;RESET GPU
lui
R3,0x1F80 ; R3->IO
sw
R0,0x1814(R3) ; RESET GPU HARD
;RESTAURE REGISTER FROM STATIC SPACE
lui
R2,>SaveRegister
ori
R2,R2,SaveRegister
lw
R31,0(R2) ;SAVE RA
lw
R29,4(R2) ;SAVE SP
lw
$K0,8(R2) ;SAVE K0
lw
$K1,12(R2) ;SAVE K1
lw
$S0,16(R2) ;SAVE S0
lw
$S1,20(R2) ;SAVE S1
lw
$S2,24(R2) ;SAVE S2
lw
$S3,28(R2) ;SAVE S3
lw
$S4,32(R2) ;SAVE S4
lw
$S5,36(R2) ;SAVE S5
lw
$S6,40(R2) ;SAVE S6
lw
$S7,44(R2) ;SAVE S7
;RETURN 0 AND QUITTE PROGRAMM
or
R2,R0,R0 ;RETURN 0
jr R31 ;QUITTE PROGRAMME
or
R3,R0,R0
include "PSXSYSTEM.asm" ;POUR LES APPELLES DES FONCTION BIOS
nop
include "PSXGPU.asm" ;ROUTINES GPU DEFINIT AU DEBUT
nop
SaveRegister:
word
0,0,0,0 ;R31,R29,$K0,$K1
word 0,0,0,0,0,0,0,0,0 ;S0..S7
word
0,0 ;8 octets supplémentaires au cas ou…
Tous les programmes de démonstrations de
ce tutoriel utiliseront dorénavant le corps de ce listing.
11-
Transfert d’Image sans
DMA
Pour transférer une image de la RAM vers
la VRAM, on utilise la commande :
0xA0 Y|X H|W PIXEL(S)
Pixel contient 1 ou 2 pixels en fonction
du mode.
Tous les tests de transferts ont marché
même une image de 1024*512 sur tout la VRAM. L’auteur de NoCash-PSX affirme que
le transfert sur tout la VRAM n’est pas possible : peut-être sur une autre
version du GPU. Si vous avez un problème avec, vérifier d’abord si vous ne
faites pas d’erreurs de programmation (si par exemple vous envoyer trop de
données vous aurez droit a des effets imprévisibles).
Il semble que la fonction 0xA0 ne se sert
pas du cache de commande même si la plupart des librairies que j’ai pu
voir initialise le cache avant le transfert. J’ai l’intuition que cela est
absolument inutile mais je n’ai pas vérifié donc initialisez le cache avant un
transfert et basta.
La GPULib pour les transferts désactive les interruptions
vérifie que le cache n’est pas plein le vide éventuellement vérifie la taille
du transfert, divise par 64 octets (16 words taille max d’un bloc).
Transfert le premier bloc non égale à 16 par I/O et le
reste par DMA. (Lorsque le PAD est
initialisé ou non, les transferts I/O fonctionnent dans tous les cas).
Exemple Transfert VRAM I/O:
;$A0 =>
PTR MEM SOURCE
;$A1 =>
DESTINATION Y|X DANS LA VRAM
;$A2 =>
IMAGE PARAMS H|W
Mem2VramIO:
;PUSH R31
addi
R29,R29,0xFFFC ; R29=R29-4
sw
R31,0(R29) ; PUSH R31
;R27 POINT TO I/O BASE
lui
R27,0x1F80
;CALC NEED BLOCK
;READ
W/H
Or
R8,$A2,R0 ;W
Or
R9,$A2,R0 ;R9=H<<16|W
Srl
R9,R9,16 ;R9=H
multu
R8,R9 ;W*H
mflo R10 ;W*H=R10
;CALC COUNT OF 1 words 2 pixels
srl
R10,R10,1 ;R10/2=>W*H/2
;WAIT FOR GPU READY
jal
WaitGPUReady
nop
;SETUP DMA OFF POUR LE GPU
lui
R9,0x0400 ;DMA CMD
ori
R9,R9,0x0000 ;DMA OFF
sw R9,0x1814(R27) ;SEND CMD DMA OFF TO GPU1
;FLUSH CACHE
lui
R8,0x0100
sw
R8,0x1810(R27) ;FLUSH CACHE GPU0
lui
R10,0xE600 ;DISABLE MASK STUFF
sw R10,0x1810(R27) ;CMD DISABLE MASK STUFF
lui
R9,0xA000
sw
R9,0x1810(R27) ;SEND IMG CMD 0xA0
sw
$A1,0x1810(R27) ;SEND CMD DST Y|X
sw
$A2,0x1810(R27) ;SEND CMD H|W
;START COPY LOOP
.Loop:
lw
R8,0($A0) ;READ 2 PIXEL
addi
R10,R10,0xFFFF ;R10 CNT--
sw R8,0x1810(R27) ;SEND 2 PIXEL
bne R10,R0,.Loop ;loop if R10!=0
addi
$A0,$A0,0x0004 ;NEXT 4
;END COPY LOOP
;POP R31
lw
R31,0(R29) ; POP RN
nop ; LOAD DELAY
;RET
jr
R31 ; RETOUR
addi
R29,R29,0x0004 ; RESTAURE STACK
12-
Polygone Texturé et
Sprites
Maintenant que nous savons transférer des
images en VRAM nous pouvons essayer les sprites et les polygones texturés.
POLYGONE TEXTURES :
La grande force de la PSX est de pouvoir afficher
des polygones ombrés et texturés. Ce qui permet de faire un grand nombre de
choses sans grand effort (même si certains trouverons cela moins intéressant la
PSX contient quelques défis et problèmes à résoudre comme notamment l’absence
de Z-Buffer, le clipping en Z etc.). Pour l’instant nous allons seulement voir
comment afficher en triangle texture en ombrage plat (FLAT SHADING).
La VRAM est organisé en Texture page, 32
zones de 64x256 où le no de page est donnée par la formule TY/16+TX/64.
La page de texture est sélectionnée dans
les paramètres de commandes de dessin pour les polygones (Triangles,
rectangles-poly).
Une texture page possède 3 modes
d’affichage couleur :
0
4
bits 16 couleurs
1
8
bits 256 couleurs
2
15bits RGB un
pixel = octet (5 bit pour chaque RGB)
Pour les Sprites, le mode est
sélectionnable par la commande 0xE1 (Set Texture Page) que nous verrons plus
tard.
Sinon c’est dans la commande polygone que
nous définissons le mode couleur.
Tout d’abord il convient de mettre la
texture dans la VRAM. Pour simplifier nous allons utiliser le mode 15 bits.
Une texture ne peut avoir des dimensions
supérieures à 256x256.
Une fois la texture dans la VRAM, on peut configurer la fenêtre de Texture par
la fonction 0xe2 : c’est là que nous définissons une fenêtre dans la page
de texture.
GP0(0xE2) - Texture Window setting
0-4 Texture window Mask X (in 8 pixel steps) 5-9 Texture window Mask Y (in 8 pixel steps) 10-14 Texture window Offset X (in 8 pixel steps) 15-19 Texture window Offset Y (in 8 pixel steps) 20-23 Not used (zero) 24-31 Command
(E2h) |
;TEXTURE WINDOW SETTING OUR IMAGE IS
ON PAGE X=0 Y=0 W=64 H=64
;$13-$0F $0E-$0A $9-$5 $4-$0
;TWY TWX TWH TWW ;MULTIPLE OF 8 !
TWY TWX de façon à ce que WX=TWX*8 et
WY=TWY*8
Et TWW et TWH configure la taille de façon
à ce que WW=256-(TWW*8) et WH=256-(TWH*8)
lui R27,0x1F80
lui $T1,0xe200 ; Le CLUT ID=0
ori
$T1,$T1,0x0000 ; 0,0 256,256
ori
$T1,$T1,0x0318 ; 0,0 64 ,64
sw $T1,0x1810(R27) ; SEND CMD
ATTENTION La page de texture ne change
rien aux conséquences sur les coordonnées de textures : u v vont toujours
se limite entre 0 et 255 dans la Texture Page.
Si nous limitons notre fenêtre à 64*64 et
que les coordonne de texture d’un polygone à 4 cotes avec u et v possédant des
valeurs de 0 et 255.
La texture 64*64 sera répétée et non
affichée entièrement !
La fenêtre de texture sert donc
principalement à la répétition de texture. Par exemple un texture de 32*32 avec
la fenêtre configuré sur cette texture on peut alors répéter 8 fois la texture
sur un polygone en utilisant u et v de 0 ; 255.
Par default la fenêtre est définit sur
tout la texture. Donc pour l’affichage de notre polygone nous ne le ferons pas.
Pour afficher notre triangle texturé, nous
utilisons la commande 0x24 qui va se présenter ainsi :
24
BB GG RR
YY YY XX
XX ;COORDONNES VERTEX 0
ID CLUT V U ;IDCLUT=0 Coordonne de Texture v u vertex 0
YY YY XX
XX ;COORDONNES VERTEX 1
TP V U ;Texture page Coordonne de Texture v u vertex 1
YY YY XX
XX ;COORDONNES VERTEX 2
N/A V U ;N/A=0 v u vertex 2
Les coordonne de texture vont de 0 à 255.
En mode 15 bits on s’en tape du ID CLUT
Le paramètre clef est TP (Texture Page)
qui se présente sur 16 bits ainsi :
B8-B7 : Mode : 10 ->15
bits, 00-> 4bit, 01->8bit
B4 : YP page tel que Y=YP*256
B3-B0 : XP page tel que X=XP*64
Supposons que nous chargions une Texture
de 256*256 dans la VRAM en X=512 Y=0 de 15 bits.
TP aura donc la valeur de 0x108
Exemple Triangle texturé:
; Affiche un triangle texture dont celle ci
de 256x256 se trouve en 512,0 dans la VRAM
TriText1:
;R27 POINT TO I/O BASE
lui
R27,0x1F80
;SET TEXTURE WINDOW 0,0 256*256
lui $T1,0xe200 ;CLUT ID=0 DON’T CARE
ori
$T1,$T1,0x0000 ;PARAMS 0
sw $T1,0x1810(R27) ;SEND CMD
;DRAW A TEXTURED TRIANGLE (20,200) (140,20) (300,200)
lui
$T0,0x2480 ;TEXTURED 3 point plygone
ori
$T0,$T0,0x8080 ;GRAY 0.5
sw $T0,0x1810(R27)
;FOR EACH VERTEX
;VERTEX 0
lui
$T1,200 ;Y0=200
ori
$T1,$T1,20 ;X0=20
sw
$T1,0x1810(R27) ;SEND COORDS Y|X
lui
$T2,0x0000 ;CLUT ID =0 MODE 15 bit DON’T CARE
ori
$T2,$T2,0xFF00 ;V=255 U=0
sw $T2,0x1810(R27) ;SEND
CLUTID AND COORDTEXT
;VERTEX 1
lui
$T1,20 ;Y0=20
ori
$T1,$T1,140 ;X0=140
sw
$T1,0x1810(R27) ;SEND COORDS Y|X
lui
$T2,0x0108 ;TEXTURE PAGE : 15 bit direct(b8-b7)=2 -
X(b3-b0)=8=>8*64=512 - Y=0(b4=0) (1 0000 1000)
ori
$T2,$T2,0x0080 ;V=0 U=128
sw
$T2,0x1810(R27) ;SEND TP AND COORDS U|V
;VERTEX 2
lui
$T1,200 ;Y0=200
ori
$T1,$T1,300 ;X0=300
sw
$T1,0x1810(R27) ;SEND COORDS Y|X
lui
$T2,0x0000 ;DON'T CARE
ori
$T2,$T2,0xFFFF ;V=255 U=255
sw
$T2,0x1810(R27) ; SEND COORDS U|V
jr
$RA
nop
SPRITE :
Pour l’affichage des Sprites nous allons nous
intéresser aux tables de couleurs les CLUTs et au Texture Pages.
A noter qu’il ne s’agit pas là de vrai
Sprites câblés traditionnels mais de rectangle texturés avec la couleur noire
toujours transparente.
Toujours est-il que la Playstation possede
de telle capacitée d’affichage qu’elle rivalise sans probleme avec n’importe
quelle borne d’arcade contemporaine pour l’affichage 2D.
La VRAM est organisé en Texture page, 32
zones de 64x256 où le no de page est donnée par la formule TY/16+TX/64.
Une texture (image) de Sprite ne peut avoir des dimensions supérieures à
256x256.
La texture page est configuree par la
commande du GPU0 0xE1
GP0(E1h) - Draw Mode setting (aka
"Texpage")
0-3 Texture page X Base (N*64) (ie. in 64-halfword steps) ;GPUSTAT.0-3 4 Texture page Y Base (N*256) (ie. 0 or 256) ;GPUSTAT.4 5-6 Semi Transparency (0=B/2+F/2, 1=B+F, 2=B-F, 3=B+F/4) ;GPUSTAT.5-6 7-8 Texture page colors (0=4bit, 1=8bit, 2=15bit,
3=Reserved);GPUSTAT.7-8 9 Dither 24bit to 15bit (0=Off/strip LSBs,
1=Dither Enabled) ;GPUSTAT.9 10 Drawing to display area (0=Prohibited,
1=Allowed) ;GPUSTAT.10 11 Texture Disable (0=Normal, 1=Disable if
GP1(09h).Bit0=1) ;GPUSTAT.15 12 Unknown (usually zero) (but BIOS does set
this bit on power-up...?) 13 Unknown (usually zero) (but BIOS does set
it equal to GPUSTAT.13...?) 14-23 Not used (should be 0) |
;SET TEXTURE PAGE CMD 0xE1
OR DRAW MODE SETTING
;B10
Draw to display B09:DITTER B8-B7 TP B6-B5:ABR B4=Y B3-B0=X
;1
ENABLE 0
(DITHER OFF) 10 (16 bits) 00(Don't care) 0 1000(8->8*64=512)
;0x0508
SetTexurePage :
lui R8,0xE100 ;CMD E1 SET TEXTURE PAGE
ori R8,R8,0x0508 ;SEE
ABOVE
sw R8,0x1810(R27) ;SEND
CMD GPU0
Examinons la commande 0x64. Elle est
composée de 4 mots de cette façon :
0x64 BB GG RR
YY YY
XX XX
CLUT CLUT VV UU
HH HH WW WW
Comme pour le triangle les 24 bits suivant
définissent la couleur en BGR.
Ensuite les position du coin supérieur
gauche par Y(16 bits) et X(16 bits)
Ensuite vient la position du CLUT 10 bits
pour Y et 5 bits pour X tel que X/16.
Et enfin la taille du Sprite vient sur le
mot suivant H dans le nibble supérieur et W dans le nibble inferieur.
Avec cette commande nous pouvons réaliser
des Sprites 3D.
Pour l’instant, je laisse au lecteur le
soin de réaliser seul un programme utilisant des Sprites 3D avec la commande
0x64.
Examinons la commande 0x74. Elle affiche
un sprite de 8x8 Elle est composée de 3 mots de cette façon :
0x74 BB GG RR
YY YY
XX XX
CLUT CLUT VV UU
Comme pour le triangle les 24 bits suivant
définissent la couleur en BGR.
Ensuite les position du coin supérieur
gauche par Y(16 bits) et X(16 bits)
Ensuite vient la position du CLUT 10 bits
pour Y et 5 bits pour X tel que X/16.
Et les coordonnes de texture dans la
texture page.
Premier exemple : Fonts 16 bits en 8x8.
La fonction ci-dessous se sert d’une image
de 128*128 16 bits ou chaque caractère se trouve logiquement dans une table de
16*16 entrées.
Un caractère est affiché par un sprite 8x8
(commande 0x74).
Elle suppose que la texture page soit déjà
configurée et se trouve en 512,0 dans la VRAM (Page 8).
Les coordonnées de texture du Sprite
sélectionnent le caractère.
Afin de limiter la longueur du listing,
cette routine gère les coordonnées d’écran de manières simplificatrices :
X et Y sont limités à 511 (and 0x1FF). Chaque caractère incrémente X par 8. Si
X atteint 0 alors Y est augmenté de 8.
; Affichage d'une chaine de caractères ASCII
8*8
; Utilisation des Spirtes 8x8
; SetTexurePage suppose que l’image des fonts
se trouve en 512,0 dans la VRAM
; L’image des fonts est une table de 16*16
donc de dimension 128*128
; Configuration de la Texture Page en 512*0
; PARAMETRES :
; $A0=>PtrText
la chaine doit se terminer par 0
; $A1=>Position
a l'ecran Y|X
Print16b:
; SAVE RETURN ADRESS
addi
R29,R29,0xFFFC ; R29=R29-4
sw
R31,0(R29) ; PUSH R31
; START
jal
SetTexturePage
nop
.printChar:
;R10->Char
lb
R10,0x0000($A0) ;R10 get first char
nop
beq
R10,R0,.End ;0 means END STRING
;IMAGE represents TABLE 16x16 of 8x8
sprite chars.
;LINE=char/16->R11
or
R11,R0,R10 ;R11=R10=Char
srl
R11,R11,4 ;R11/16=>LINE NUMBER
;COLUMN=CHAR-(LINE*16)
sll
R13,R11,4 ;R13=Line*16
sub
R14,R10,R13 ;R14=R10-R13=CHAR-(LINE*16)
;AT THIS POINT R11->LINE NUMBER AND
R14=COLUMN
ori
R12,R0,0x00FF ;MAX 255
sll
R11,R11,3 ;R11*8 LINE NUMBER->V
sll
R14,R14,3 ;R14*8 COL NUMBER->U
and
R14,R14,R12 ;U MASK
and
R11,R11,R12 ;V MASK
sll
R11,R11,8 ;V UPPER NIBBLE
or
R11,R11,R14 ;R11 IS THE UV
andi
R11,R11,0xFFFF ;CLUT TO 0
; SHOW ONE SPRITE 8x8 TEST CMD 0x74
jal
GPUWaitReady ;Since we could send lot of sprite the
simpler things is
nop ; to wait systematically if GPU is ready
to receive command.
lui
R8,0x74FF
ori
R8,R8,0xFFFF ;SPRITE WHITE
sw
R8,0x1810(R27) ;SEND GPU0 CMD
sw
$A1,0x1810(R27) ;SEND GPU0 POSITION Y|X
sw
R11,0x1810(R27) ;SEND GPU0 CLUT|V|U
; ADJUST X|Y
;R8=X R9=Y
ori
R8,$A1,R0 ;R8=Y<<16|X
andi
R8,R8,0x01FF ;R8=X
ori
R9,$A1,R0 ;R9=Y<<16|X
srl
R9,R9,16 ;R9=Y
;NEXT X
addi
R8,R8,0x0008 ;NEXT X
andi
R8,R8,0x01FF ;MAX X =512
bne
R8,R0,.SetYX ;NO SO LET Y
nop
;NEXT Y
addi
R9,R9,0x0008
andi
R9,R9,0x1FF ;MAX=512
; SET $A1 Y|X
.SetYX:
sll
R9,R9,16 ;Y ON UPPER NIBBLE
or
$A1,R9,R8 ;SET $A1=Y<<16|X
j
.printChar ;PRINT NEXT CHAR
addiu
$A0,$A0,0x0001 ;NEXT CHAR
.End:
; END POP AND RET
lw R31,0(R29) ;
POP RN
nop
jr R31 ; RETOUR
addi R29,R29,0x0004
Deuxième exemple : Fonts 4 bits en 8x8. Utilisation des
tables couleurs CLUT.
En mode 4 bits. Un demi-mot représente 4
pixels en ordre inverse (les 4 premiers bits représentent le pixel le plus à
droite les derniers 4 bits le plus à gauche) où la couleur code sur 4 bits est
l’index d’une table de couleur de 16 bits (5 bits pour BGR le premier bit
pour la transparence) de 16 entrées qui se trouve en VRAM.
On sélectionne la table de couleur par le
paramètre CLUT sur 16 bits. Ou les 10 premiers bits représentent Y et les 6 autres
représentent X/16.
Pour passez en mode 4 bits, notre fonction
SetTexturePage devient :
;SET TEXTURE PAGE CMD 0xE1
OR DRAW MODE SETTING 512*0
;B10
Draw to display B09:DITTER B8-B7 TP B6-B5:ABR B4=Y B3-B0=X
;1
ENABLE 0
(DITHER OFF) 00 (4 bits) 00(Don't care) 0 1000(8->8*64=512)
;0x0408
lui R8,0xE100 ;CMD E1 SET TEXTURE PAGE
ori R8,R8,0x0408 ;SEE
ABOVE
sw R8,0x1810(R27) ;SEND
CMD GPU0
;SET TEXTURE PAGE CMD 0xE1
OR DRAW MODE SETTING EN 512*256
;B10
Draw to display B09:DITTER B8-B7 TP B6-B5:ABR B4=Y B3-B0=X
;1
ENABLE 0
(DITHER OFF) 00 (4 bits) 00(Don't care) 1(256) 1000(8->8*64=512)
;0x0408
lui R8,0xE100 ;CMD E1 SET TEXTURE PAGE
ori R8,R8,0x0418 ;SEE
ABOVE
sw R8,0x1810(R27) ;SEND
CMD GPU0
Et l’image Fonts ne fait plus que 32*128.
Cela ne modifie en rien la routine comme
si en 4 bits la page se met également en 4 bits et visualise 64*256 de la VRAM.
Nous devons seulement indiquer le bon CLUT à notre SPRITE.
or R11,R11,R14 ; R11 IS THE UV
andi
R11,R11,0xFFFF ; CLUT TO 0
lui R13,0x7FE0 ; CLUT ID->Y=511 X=32=(512/16)
or R11,R11,R13 ; R11=CLUT|Y|X
;SHOW ONE SPRITE 8x8 TEST CMD 0x74
jal
WaitGPUReady ;VERY IMPORTANT !!!
nop ;
lui
R8,0x74FF
ori
R8,R8,0xFFFF ; SPRITE WHITE
sw
R8,0x1810(R27) ; SEND GPU0 CMD
sw
$A1,0x1810(R27) ; SEND GPU0 POSITION Y|X
sw
R11,0x1810(R27) ; SEND GPU0 CLUT|V|U
Conversion hexa->Text :
Pour commencer à examiner les valeurs des
registres de notre PSX il nous faut une petite routine de conversion en
hexadécimale :
;Convert Val on $A0 to Hexadecimal Chaine
pointed by $A1
MyConvertVal:
; SAVE RETURN ADRESS ON STACK
addi
R29,R29,0xFFFC ; R29=R29-4
sw
R31,0(R29) ; PUSH R31
; START
lui
R8,>.HexVal
ori
R8,R8,.HexVal ;R8=>HEXAV ALUE
ori
R12,R0,4 ;R12 is CNT 4 Bytes
; EACH BYTE
.Loop:
or
R10,$A0,R0 ;R10 is the value
sll
$A0,$A0,8 ;Upper bytes don't care
sra
R10,R10,24 ;GET B31-b24
or
R9,R10,R0
andi
R9,R9,0x00FF ;R9 b-31-b24
;R9 contains byte
;B7-B4
or
R11,R9,R0
srl
R11,R11,4
andi
R11,R11,0x000F ;B31-B28
addu
R11,R11,R8 ;GET HEXA adress
lb
R11,0(R11) ;R11 HEX
nop
sb
R11,0($A1) ;PRINT
nop
addiu
$A1,$A1,0x0001 ;NEXT STRING
;END B31-B28
;B3-B0
or
R11,R9,R0
andi
R11,R11,0x000F ;B31-B28
addu
R11,R11,R8 ;GET HEXA adress
lb
R11,0(R11) ;R11 HEX
nop
sb
R11,0($A1) ;PRINT
;END B3-B0
addi
R12,R12,0xFFFF ;DEC CNT
bne
R12,R0,.Loop
addiu
$A1,$A1,0x0001 ;NEXT STRING
;END WRITE 0
sb
R0,0($A1) ;0 end string
;END
lw
R31,0(R29) ; POP $RA
nop
jr
R31 ; Return
addi R29,R29,0x0004 ; Restaure SP
.HexVal:
asci "0123456789ABCDEF"
Ensuite une routine inverse à partir d’une
chaine de caractères pointé par R4 et supposée représenter une valeur en hexa,
on convertit en valeur dans le registre R2.
Avec tous ces routines nous pouvons commencer
a plus efficacement analyser la PSX, ses registres I/O durant des programmes de
test par exemple.
Quad Polygone :
Les quads sont gérer par le gpu de façon
assez étrange.
V1 V3
V0 V2
Le GPU génère deux triangle v0,v1,v2 et
v1,v2,v3 pour rendre le quad.
Examinons les 2 commandes :
Commande 0x28 Quad simple
28
BB GG RR
YY YY XX
XX ;COORDONNES VERTEX 0
YY YY XX
XX ;COORDONNES VERTEX 1
YY YY XX
XX ;COORDONNES VERTEX 2
YY YY XX
XX ;COORDONNES VERTEX 3
; Draw 4 point polygon
; $A0 -> ptr mesh (X,Y,Z,0)
DrawTrRect:
;RECTANGLE SIMPLE
;GET
X1
lw
R10,0($A0) ;V0
lw
R11,8($A0) ;V1
lw
R12,16($A0) ;V2
lw R13,24($A0) ;V3
; 4 points polygone mono
lui
R8,0x2880 ; RECT 4 point polygone
ori R8,R8,0x8080 ; GRAY
sw
R8,0x1810(R27)
;VERTEX 0
sw
R10,0x1810(R27) ; SEND COORDS Y|X
;VERTEX 1
sw
R11,0x1810(R27) ; SEND COORDS Y|X
;VERTEX 2 (3)
sw
R12,0x1810(R27) ; SEND COORDS Y|X
;VERTEX 3
sw
R13,0x1810(R27) ; SEND COORDS Y|X
.End:
jr
R31
nop
Commande 0x2C Quad texturé.
2C
BB GG RR
YY YY XX
XX ;COORDONNES VERTEX 0
ID CLUT V U ;IDCLUT=0 Coordonne de Texture v u vertex 0
YY YY XX
XX ;COORDONNES VERTEX 1
TP V U ;Texture
page Coordonne de Texture v u
vertex 1
YY YY XX
XX ;COORDONNES VERTEX 2
N/A V U ;N/A=0 v u vertex 2
YY YY XX
XX ;COORDONNES VERTEX 3
N/A V U ;N/A=0 v u vertex 3
; Draw 4 point polygon texture
; Assume 256x256 texture located on 768,0
in VRAM
; $A0 -> ptr mesh (X,Y,Z,0)
DrawTrRect:
;RECTANGLE SIMPLE
;GET
X1
lw
R10,0($A0) ;V0
lw
R11,8($A0) ;V1
lw
R12,16($A0) ;V2
lw R13,24($A0) ;V3
; 4 points polygone Mono Texture
lui
R8,0x2C80 ; RECT 4 point polygone
ori R8,R8,0x8080 ; GRAY
sw
R8,0x1810(R27)
;VERTEX 0
sw
R10,0x1810(R27) ; SEND COORDS Y|X
ori R9,R0,0xFF00 ;
CLUT ID=0 TU= 0 TV=0xFF
sw R9,0x1810(R27) ;
SEND
;VERTEX 1
sw
R11,0x1810(R27) ; SEND COORDS Y|X
lui R9,0x010C ; TEXTURE PAGE X=12 (768) Y=0
; TU= 0x00 TV=0x00
sw R9,0x1810(R27) ; SEND
;VERTEX 2 (3)
sw
R12,0x1810(R27) ; SEND COORDS Y|X
ori R9,R0,0xFFFF ;
TU= 0xFF TV=0xFF
sw R9,0x1810(R27) ;
SEND
;VERTEX 3
sw
R13,0x1810(R27) ; SEND COORDS Y|X
ori R9,R0,0x00FF ;
TU= 0xFF TV=0x00
sw R9,0x1810(R27) ;
SEND
.End:
jr R31
nop
13-
DOUBLE BUFFER
Comme on en sait suffisamment pour
commencer à envoyer plusieurs centaines de polygones, il est temps
d’implémenter le double-buffer.
Vous avez sans doute remarquez lors de la
partie consacre à une configuration du GPU que celle-ci possède toutes les fonctions
nécessaire pour implémenter facilement un double buffer.
GPU1:
CMD 0x05 DISPLAY AREA
Paramètres bit
$00-$09 X (0-1023) bit $0A-$12
Y (0-512)
Définition des coordonnées de départ de la
zone à afficher
GPU0:
DRAW AREA OFFSET (0xE5):
Word 0: 0xE3 00 PP PP PP Paramètres
PP : X=b0-b9 Y=b10-b19
Grace à elle, on peut changer de surface
d’affichage sans changer les routines de dessins.
En effet supposons que l’on veuille
utiliser 2 tampons de 320*240 en (0,0) (A) et (0,256) (B). On démarre par
exemple comme écran d’affichage (0,0) et écran de dessin (0,256) en configurant
avec le Display Area. Il nous suffit dans ce cas et en fonction de changer les
paramètres DrawArea Offset.
Avant de permuter il faut juste contrôler
le statut IDLE du GPU et le retour verticale et voilà ! Implémenter un
double-tampon n’a jamais était aussi facile J
Fenêtrage ou Clipping:
GP0(E3h) - Set Drawing
Area top left (X1,Y1)
GP0(E4h) - Set Drawing Area bottom right (X2,Y2)
0-9 X-coordinate (0..1023) 10-19 Y-coordinate (0..1023) (really 10bit, not
9bit) (should be 512 max) 20-23 Not used (zero) 24-31 Command
(Exh) |
Grace aux fonctions « Drawing
area »0xE3 et 0xE4. Le programmeur peut s’affranchir des problèmes de clipping
en X et Y permettant également une implémentation facile de multifenêtres.
Config1 :
X start=0 ;Xend=319 (13F) Y Start=0;Y End=255 (0xFF)
0xE300 0000 TOP,LEFT=0,0
0xE403 FD3F BOTTOM RIGHT=255,319
Config2
X start=0 ;Xend=319 (13F) Y Start=256(0x100);Y End=511
(0x1FF)
0xE304 0000 TOP,LEFT=256,0
0xE407 FD3F BOTTOM,RIGHT=511,319
En revanche il revient au programmeur de
clipper les sommets dont la profondeur se situe derrière le plan de projection
pour la 3D. Il s’agira surtout d’exclure ou non un polygone et éventuellement
d’interpoler les coordonnées de texture.
De plus lorsque un polygone est clipper,
suivant certaine situation, il consomme des cycles GPU donc il vaut mieux dans
certaine situation clipper soit même.
;DOUBLE BUFFER
;ALWAYS CALL INIT SURFACE BEFORE USING
;REQUIERD GPUINIT
InitSurface:
lui
R2,>CurSurface
ori R2,R2,CurSurface
sw
R0,0(R2) ;Init State 0
;DISPLAY AREA 0,0 320*256
;DISPLAY
AREA => WHERE TO DISPLAY (TOP/LEFT)
lui
R3,0x0500 ; CMD=0x05 0,0
sw
R3,0x1814(R27) ; SEND TO GPU1
;SET DRAW AREA OFFSET => WHERE DRAW
lui R3, 0xe508 ; DRAW OFFSET Y 256
sw R3,0x1810(R27)
;CLIPING AREA Start 0,256
lui R2,0xE304
sw
R2,0x1810(R27)
;CLIPING AREA END 319,511
lui
R3,0xE407
ori
R3,R3,0xFD3F
sw
R3,0x1810(R27)
jr
R31
nop
;FLIP FRONT<->BACK BUFFER
FlipSurface:
lui
R3,>CurSurface
ori
R3,R3,CurSurface
LW R2,0(R3)
nop
Beq
R2,R0,.@1 ;Si State=0 state come to 1
Nop
;STATE 1->0 Front=(0,0) Back=(0,256)
;DISPLAY
AREA => WHERE TO DISPLAY (TOP/LEFT)
lui
R3,0x0500 ; CMD=0x05 0,0
sw
R3,0x1814(R27) ; SEND TO GPU1
;SET DRAW AREA OFFSET => WHERE DRAW
lui R2, 0xe508 ; DRAW OFFSET 256
sw R2,0x1810(R27)
;CLIPING AREA Start 0,256
lui R2,0xE304
sw
R2,0x1810(R27)
;CLIPING AREA END 319,511
lui
R3,0xE407
ori
R3,R3,0xFD3F
sw
R3,0x1810(R27)
J
.End
Nop
.@1:
;STATE 0->1 Front=(0,256) Back=(0,0)
;DISPLAY
AREA => WHERE TO DISPLAY (TOP/LEFT)
lui R2,0x0504 ; CMD=0x05 offset 0,256
sw R2,0x1814(R27) ; SEND TO GPU1
;SET DRAW AREA OFFSET => WHERE DRAW
lui R3, 0xe500 ; DRAW OFFSET Y0
sw R3,0x1810(R27)
;CLIPING AREA Start 0,0
lui R2,0xE300
sw
R2,0x1810(R27)
;CLIPING AREA END 319,255
lui
R3,0xE403
ori
R3,R3,0xFD3F ;B9-B0 x=0x3FF 0xF FCxx =>
sw
R3,0x1810(R27)
.End:
;lui
R2,0x0300 ;DISPLAY ENABLE
;sw
R2,0x1814(R27) ;SEND TO GPU
lui R3,>CurSurface
ori
R3,R3,CurSurface
LW
R2,0(R3)
nop
Xori
R2,R2,1 ; Inverse l’etat
jr
R31 ; RETOUR
sw
R2,0(R3) ;Mémorise l’etat
;CLEAR BACK BUFFER
ClearBackBuffer:
lui
R3,>CurSurface
ori R3,R3,CurSurface
lw
R2,0(R3)
nop
bne
R2,R0,.@1 ; STATE 0
lui R3,0x0000 ; TOP
lui
R3,0x0100 ; TOP=256
.@1:
lui
R2,0x0200 ; 0x02 FILL DRAW BUFFER B=0
ori R2,R2,0x0000 ; GR=0
lui
R4,0x0100 ; HEIGHT 256
ori R4,R4,0x0140 ; WIDTH 320
sw R2,0x1810(R27) ; SEND CMD TO GPU0
sw
R3,0x1810(R27) ; SEND PARAM TO GPU0
sw
R4,0x1810(R27) ; SEND PARAM TO GPU0
jr R31 ; RETOUR
nop
CurSurface:
word 0 ;0 ->Back=(0,256) 1->Back=(0,0)
Exemple d’utilisation:
Jal InitSurface
nop
LoopMain:
jal
WaitGPUReady
nop
jal
ClearBackBuffer
nop
; OTHER DRAWS
;
WITH I/O call WaitGPUReady
;……………………………..
jal
WaitGPUIdle
nop
jal
WaitGPUReady
nop
jal
WaitVSyncOnly
nop
jal
FlipSurface
nop
j
LoopMain
nop
14-
DMA
La DMA permet au chip spécialisé comme le
GPU ou le SPU de s’affranchir du CPU est d’aller lire ou écrire directement de
la RAM vers la VRAM (GPU) ou la SPU-RAM(SPU).
L’utilisation de la DMA est indispensable
pour le transfert de grosse quantité de données à grande vitesse dans la VRAM
ou la SRAM.
Les registres DMA sont
mappés entre 0x1f80_1080 and 0x1f80_10f4. Il y a 6 canaux DMA.
Base Address |
Channel Number |
Device |
0x1f80_1080 |
DMA channel 0 |
MDECin |
0x1f80_1090 |
DMA channel 1 |
MDECout |
0x1f80_10a0 |
DMA channel 2 |
GPU (lists + image data) |
0x1f80_10b0 |
DMA channel 3 |
CD-ROM |
0x1f80_10c0 |
DMA channel 4 |
SPU |
0x1f80_10d0 |
DMA channel 5 |
PIO |
0x1f80_10e0 |
DMA channel 6 |
GPU
OTC (reverse clear the Ordering Table) |
Chaque base est structurée de la même
façon:
DMA Memory Address Register (D_MADR) 0x1f80_10n0 MADR
Pointer to the virtual address the DMA will start reading from/writing to.
31 |
0 |
MADR |
B31-B16 BA:
NOMBRE DE BLOC A COPIER OU LIRE
B15-B0 BS:
TAILLE D’UN BLOC
ATTENTION, Le GPU et le SPU sont limités à
un bloc de 16 words.
DMA Channel Control
Register (D_CHCR) 0x1f80_10n8
0 |
TR |
0 |
LI |
CO |
0 |
DR |
7 |
1 |
13 |
1 |
1 |
8 |
1 |
TR 0 No DMA transfer busy. 1 Start
DMA transfer/DMA transfer busy.
LI 1 Transfer linked list. (GPU
only).
CO 1 Transfer continuous stream of
data.
DR 1 Direction from CPU to RAM
Componements 0 Direction from memory componements to CPU.
DMA Interrupt Control Register (DICR) 0x1f80_10f4
Utilisation inconnue, il faudra que je
vérifie moi-même sur une vrai PSX. Je suppose que c’est pour déclencher une
exception lorsque le transfert DMA est terminé ?
La structure doit être proche du registre
DPCR.
Mais avant d’utiliser la DMA nous devons
indiquer au registre de contrôle général quel canal DMA nous souhaitons
utiliser :
DMA Primary Control Register (DPCR) 0x1f80_10f0
Ou chaque canal est contrôlé par 4
bits :
B3-B0 Channel 0 0x0000
000F
B4-B7 Channel 1 0x0000
00F0
B11-B8 Channel 2 0x0000
0F00
B15-B12 Channel 3 0x0000
F000
B19-B16 Channel
4 0x000F 0000
ETC
Etat initial de DPCR=0x0000 9099 1001(CH3) 0000(CH2) 1001(CH1) 1001(CH0)
A priori, pour mettre en marche un canal
nous devons écrire dans ces registres on écrit la valeur de 1 sur le 4eme bit.
Les 3 autres bits indiquent le niveau de propriétés.
Mais les exemples pour le SPU écrivent
B3=1 B2=1 B1=0 B0=1.
Par exemple on peut mettre en marche le
canal 2 (GPU) comme ceci :
LUI R27,0x1F80 ; R27 pointe vers les registres I/O
ORI
R8,R0,0x0800 ; R8=0x0800 (CHANNEL 2)
SW
R8,0x10F0(R27) ; DMA Canal 2 actif (GPU)
LUI R8,0x0008 ; R8=0x0008
0000 (CHANNEL 4)
SW R8,0x10F0(R27) ; DMA Canal 4 actif (SPU)
NOP ; Write Delay
Exemple transfert de données son dans le SPU-BUFFER
Pour transférer un échantillon dans le
tampon du SPU on procède comme suit :
-
On
écrit dans le B5B4 du SPUCNT(0x1DAA) la valeur de 2 pour dire au SPU que l’on
va effectuer un transfert RAM->SRAM
-
On
lit dans le SPUSTAT(0x1DAE) si les les B5-B4 sont allumé-éteint respectivement
sinon on attend
-
On
écrit 4 dans le registre SPUTRANSCTRL (0x1DAC)
pour indiquer un transfert normal (bit 2 allumé).
-
On
indique dans le registre SPUADR (0x1DA6) l’adresse de destination du tampon SPU
en écrivant la valeur de l’ADRESSE divisée par 8.
-
On
allume le canal DMA 4 du SPU si ce n’est pas fait DPCR(0x10F0)
-
On
indique au canal DMA l’adresse source de la RAM dans le registre MADR du canal
4 (0x10C0)
-
On
indique dans le DMA CTRL BLOCK (0x10C4) la taille du bloque (16 words) et le
nombre de blocs
-
On démarre
le transfert par le Registre DMA CHCR du canal 4 (0x10C8) avec la valeur 0x0100
0201 (Start DMA ; TR=1 ; CO=1 ; DR=1)
-
Eventuellement,
on attend la fin du transfert en surveillant l’état du Bit TR du CHCR du canal
4 (0x10C8) TR=1 Busy TR=0 fini.
;LOAD SOUND WITH DMA
; $A0 *buffer $A1=SIZE $A2=SPU-BUFFER
ADRESS
LoadSoundDMA:
;PUSH R31
addi
R29,R29,0xFFFC ; R29=R29-4
sw
R31,0(R29) ; PUSH R31
;I/O BASE
lui
R27,0x1F80 ; R27 => I/O BASE
;SET SPU ADRESS
or
R8,R0,R6 ; R8=R6 SPU ADRESS BUFFER
srl
R8,R8,3 ; DIVIDE BY 8
sh
R8,0x1DA6(R27) ; SET ADRESS SOUND
nop
;SPU_CONTROL SET TO DMA *SPU_REG0 =
(*SPU_REG0 & 0xFFCF) | 0x0020;
lh
R8,0x1DAA(R27) ; GET SPU_CONTROL REGISTER
nop ; Load delay
andi
R8,R8,0xFFCF ; Clear B4-B5
ori
R8,R8,0x0020 ; And Set to B5=1 B4=0
sh
R8,0x1DAA(R27) ; SET DMA WRITE
nop
;Wait as DMA Write mode is effective
;SET SOURCE ADRESS *SPUDMA_MEMADDR =
src_addr; //0x1F8010C0
sw
$A0,0x10C0(R27) ; $A0=SOUND PTR
;SET SIZE BA (Block amount) BS(Block
Size=16 WORD) SPUDMA_BCR 0x1F8010C4
ori
R8,$A1,R0 ; R8=size
srl
R8,R8,4 ; Nb Block=SIZE/16
sll R8,R8,16 ; NB BLOCK ON UPPER HALF
ori
R9,R0,0x0010 ; one block=64 bytes or 16 words
or
R9,R9,R8 ; R9=R8|R9
sw
R9,0x10C4(R27) ; SET SIZE TO TRANSFERT
;START TRANSFERT *SPUDMA_CHCR =
0x01000201; //0x1F8010C8
lui R8,0x0100
ori
R8,R8,0x0201 ; R8= 0x01000201
sw R8,0x10C8(R27) ; START DMA WRITE
;while(*SPUDMA_CHCR & 0x01000000) ;
.WaitDMA1:
lw R9,0x10C8(R27)
lui
R10,0x0100 ; TEST B24
and R9,R9,R10
bne
R9,R0,.WaitDMA1 ; IF B24=1 DMA IS ON SO WAIT
nop
;POP
R31
lw
R31,0(R29) ; POP R31
nop ; LOAD DELAY
jr
R31 ; RETOUR
addi
R29,R29,0x0004 ; USE DELAY TO COMPLETE POP
Est-ce que celui la marche ?? A Tester lors
de test avec les textes aussi. Il semblerait qu’il faille impérativement lire
le registre 0x1DAA avant de transférer pour vider les tampons W et rendre
effective l’écriture.
La lecture seul de DMA write request du
registre 0x1DAE ne suffit pas. A re-tester par occasion.
Voir LOADSOUNDDMA4 qui fonctionne
correctement.
Remarque diverses sur la DMA :
-
Apparemment
un seul canal peut effectuer un transfert dans le même temps. La priorité des
canaux peuvent être configurées.
-
Si
par exemple un canal est en train de transférer, et que vous lancer un autre
canal dont la priorité est plus haute, le premier transfert en cours sera tout
bonnement interrompu.
-
Un
transfert d’image en VRAM via DMA ne fonctionne pas toujours je ne sais pas
précisément encore pourquoi.
15-
Affichage par Linked List
Lorsque nous avons plusieurs milliers de
polygones à dessiner, il vaut mieux utiliser la DMA que bloquer le processeur
par I/O. De plus l’execution par DMA est beaucoup plus rapide que par I/O.
Une linked list sera une liste chainee de
commande à envoyer au GPU.
L’utilisation de la DMA et d’une liste de
commandes nous assure une vitesse optimale et une grande flexibilité pour
l’affichage.
Un nœud d’une linked list est composé de
cette façon :
Adresse TYPE Description
0 WORD nn xx xx xx nn
est le nombre de word que comprend la commande, xx xx xx est le pointeur sur le
prochain noeud
For( i=0 i<nn ;i++) {
4*i+4 WORD cc
cc cc cc commande
GPU + parametres
}
Par exemple Supposons que l’on veuille
envoyer une commande qui efface l’écran et ensuite une autre commande qui
dessine un triangle rouge et que notre liste se trouve à l’adresse 0x80020000
ADDRESSE COMMANDE MEMOIRE COMMENTAIRES
0x80020000 03 02 00 10 10
00 02 03 ; 3 word
commandes ; next node= 0x2010
0x80020004 02 00 00 00 00
00 00 02 ; CMD 0x2 BLACK
0x80020008 00 00 00 00 00
00 00 00 ;TOP=LEFT=0
0x8002000C 01
00 01 40 40 01 00 01 ;H=256;W=320
0x80020010 04
FF FF FF FF FF FF 04 ;word 4 commandes; fin de liste
0x80020014 20 00 00 FF FF
00 00 20 ;TRIANGLE ROUGE
0x80020018 00 D0 00 20 20
00 D0 00 ;32;208
0x8002001C 00
D0 00 A0 00 20 D0 00 ;160,32
0x80020020 00
00 01 08 ;256,320
Etc.
LittleEndian : Attention dans la
mémoire on aura la représentation inverse !
LINKEDDOTLIST :
;ADD DOT ENTRY
;2
WORD COMMAND AND POINT TO NEXT
;$S4
point to current node of linked list
.Loop:
………………..
;R8
and R9 contains command and parametres,
lui
R12,0x00FF
ori
R12,R12,0xFFFF ;R12 MASK HEADER xxFFFFFF
addi
R10,$S4,12 ;R10=NEXT NODE HEADER=>WORD HEADER+2
WORD CMD
and
R10,R10,R12 ;R10=00XXXXXX where XX is the next node
adress
ori
R11,R0,2 ;2 cmd
sll
R11,R11,24 ;cmd nn in R11 ->nnXX XXXX
or
R10,R10,R11 ;R10=HEADER LINKED LIST
sw
R10,0($S4) ;WRITE HEADER
sw
R8,4($S4) ;CMD 1
sw
R9,8($S4) ;CMD 2
;sw
R8,0x1810(R27)
;sw
R9,0x1810(R27)
;NEXT LIST
addi
$S4,$S4,12 ;NEXT NODE
…………………..
.EndLoop :
addi
$S4,$S4,0xFFF4 ;-12
lw
R10,0($S4) ; Read the last note header
Lui
R12,0x00FF ;R12->MASK end list
Ori
R12,R12,0xFFFF ;R12=0x00FFFFFF
or
R10,R10,R12 ; mark header as end is with OR 0xnnFFFFFF
sw
R10,0($S4) ; WRITE THE NODE
Supposons $S4 = 0x8004 0000 c’est a dire
l’adresse de la linked List. Et trois commande dot : Bleu 0,0 Vert 1,0 et
Rouge en 1,1
Pixel (0x68):
Word 0: 0x68
BB GG RR BB GG RR désignent les couleurs bleu, vert et rouge
respectivement codées sur 15 bits (le bit supérieur pour la transparence).
Word 1: YY
YY XX XX Coordonnées
vertical (Y) et horizontal (X) du pixel SIGNEES
On aura:
ADDRESSE COMMANDE MEMOIRE COMMENTAIRE
0x8004 0000 02 04 00 0C 0C
00 04 02 2
Commande list suivant en 0x800400C + 12 (header + 2 word cmd)
0x8004 0004 68 FF 00 00 00
00 FF 68 PIXEL
BLEU
0x8004 0008 00 00 00 00 00
00 00 00 POSTION
0,0
0x8004 000C 02 04 00 18 18
04 00 02 2
Commande list suivant en 0x8004018 + 12 (header + 2 word cmd)
0x8004 0010 68 00 FF 00 00
FF 00 68 PIXEL
VERT
0x8004 0014 00 00 00 01 01
00 00 00 POSITION
1,0
0x8004 0018 02 FF FF FF FF
FF FF 02 2
Commande DERNIERE COMMANDE
0x8004 001C 68 00 00 FF FF
00 00 68 PIXEL
ROUGE
0x8004 0020 00 01 00 01 01
00 01 00 POSITION
1,1
Pour envoyer notre linked list. On procède
ainsi.
-
On
utilise la fonction WaitGPUIdle en attendant que le bit 27 soit allumé
indiquant que le GPU est prêt à recevoir une linkedlist.
-
On
allume le canal 2 (GPU) de la DMA si ce n’a pas été fait en écrivant dans le
DPCR(0x1F80 10F0)
ORI R8,R0,0x0800 ; R8=0x0800
SW R8,0x10F0(R27) ; DMA Canal 2 actif (GPU)
-
Indique
au GPU que l’on va effectuer un transfert de la RAM vers la VRAM avec la
commande GPU1 0x04 avec le paramètre 0x0002
LUI R8,0x0400 ; CMD GPU1 SET DMA
ORI R8,R8,0x0002 ; SET ENABLE DMA FOR RAM TO VRAM
SW R8,0x1814(R27) ; SEND TO GPU1
-
Indique
l’adresse de notre linked list dans le registre MADR du canal 2 (0x1F80 10A0)
LUI R8,>LinkedList
ORI R8,R8,LinkedList ; R8->LinkedList
SW R8,0x10A0(R27) ; SET ADRESS
-
Mettre
le registre BCR du canal 2 (0x1F80 10A4) à 0 étant donné qu’il s’agit d’un
transfert d’une linked list
SW R800x10A4(R27) ; LINKED LIST SO NO SIZE INDICATION
-
Démarrer
le transfert en allumant les bits 24 (Start transfert), 10(Linked List) et 0
(direction RAM->VRAM) dans le registre CHCR du canal 2 (0x1F80 10A8)
LUI R8,0x0100 ; b24=1
ORI R8,R8,0x0401 ; b10=1 ;b0=1
SW R8,0x10A8(R27) ; START DMA TRANSFERT
Le programme suivant est un exemple
complet d’utilisation d’une linkedlist en utilisant que la commande Dot.
ORG 0x8001 8000
Start:
;R27 POINT TO I/O BASE
lui R27,0x1F80
jal
InitStdGPU
nop
;Important reinitialise la table
d'interruption
jal
ResetEntryInt
nop
;PREPARE function PAD_INIT
addi
$A0,R0,0x0001
jal
InitCard
nop
jal
StartCard
nop
jal
BuInit
nop
jal
StopCard
nop
jal
PAD_INIT
nop
;R27 POINT TO I/O BASE AGAIN
lui
R27,0x1F80
;Double buffer
jal
WaitGPUReady
nop
jal
GPUClearVRAM
nop
jal
InitSurface
nop
Main:
.MainLoop:
jal
WaitVSync
nop
lui
$S1,>PadDataI
ori $S1,$S1,PadDataI
lw
$S0,0($S1)
ori
$T6,R0,0x0100 ;BUTTON SELECT
beq
$T6,$S0,.select ;END PROGRAM
nop
j
.LoopMainEnd
nop
.Select:
;QUIT
j
.End
nop
.LoopMainEnd:
.Draw:
jal
WaitGPUReady
nop
jal
ClearBackBuffer ;CLEAR BACKBUFFER
nop
jal
DrawDotSINDMA
nop
lui
$A0,>LinkedList
ori
$A0,$A0,LinkedList
jal
SendLinkedList
nop
.EndDraw:
jal
WaitGPUIdle
nop
jal
WaitGPUReady
nop
jal
WaitVSyncOnly
nop
jal
FlipSurface
nop
j
.MainLoop
nop
.End:
jr
R31
nop
include "MIPS\\PSXGPU.asm"
nop
include "MIPS\\PSXSYSTEM.asm"
nop
;DMA SINUS POINT
DrawDotSINDMA:
;PUSH
addi
R29,R29,0xFFD0 ; R29=R29-48
sw
R31,8(R29) ; PUSH R31
sw
$S0,12(R29)
sw
$S1,16(R29)
sw
$S2,20(R29)
sw
$S3,24(R29)
sw
$S4,28(R29)
sw
$S5,32(R29)
sw
$S6,36(R29)
;UPDATED TIME
lui
$S0,>.Time
ori
$S0,$S0,.Time
lw
$S6,0($S0)
nop
addi
$S6,$S6,17 ;TIME + 17
andi
$S6,$S6,0xFFFF
sw
$S6,0($S0) ;NEW TIME
;LINKED LIST POINTER
lui
$S4,>LinkedList
ori
$S4,$S4,LinkedList
lui
$S5,>.SINTAB
ori
$S5,$S5,.SINTAB
;DRAW FOR EACH
ori $S2,R0,160 ;LINES
.@Y:
ori
$S3,R0,240 ;SAMPLES
.@X:
;GET COLOR IN FUNCTION OF TIME AND Y
or
R9,$S3,R0 ;R9=curX
add
R9,R9,$S2 ;+curY
add
R9,R9,$S6 ;R9=curX+TIME
andi
R9,R9,0x00FF ;R9=&0xFF
sll
R9,R9,1 ;ALIGN half
addu
R9,R9,$S5 ;R9=idx+SINTAB
lhu
$S1,0(R9) ;$S1=color
;LINKED LIST NOD
lui
R8,0x6800
or
R11,R0,$S1 ;COLOR TIME
andi
R11,R11,0x00FF ;ONLY 8 BIT COMP IN R11
sll
R12,R11,8 ;R12=0xNN00
sll
R13,R11,16 ;R13=0xNN0000
or
R11,R11,R12 ;R11=0x00NNNN
or
R13,R13,R11 ;R13=0x00NN NNNN
or
R8,R8,R13 ;R8=0x68NN NNNN
addi
R9,$S2,40 ;R9=Y+8
sll
R9,R9,16 ;R9=Y<<16
or
R10,$S3,R0 ;R10=X
addi
R10,$S3,40 ;X+64
andi
R10,R10,0xFFFF
or
R9,R9,R10 ;Y|X
;ADD DOT ENTRY
;2
WORD COMMAND AND POINT TO NEXT
lui
R12,0x00FF
ori
R12,R12,0xFFFF ;R12 MASK HEADER xxFFFFFF
addi
R10,$S4,12 ;R10=NEXT NODE HEADER=>WORD HEADER+2
WORD CMD
and
R10,R10,R12 ;R10=00XXXXXX where XX is the next node
adress
ori
R11,R0,2 ;2 cmd
sll
R11,R11,24 ;cmd nn in R11 ->nnXX XXXX
or
R10,R10,R11 ;R10=HEADER LINKED LIST
sw
R10,0($S4) ;WRITE HEADER
sw
R8,4($S4) ;CMD 1
sw
R9,8($S4) ;CMD 2
;NEXT LIST
addi
$S4,$S4,12 ;NEXT NODE
;LOOP X
bne
$S3,R0,.@X ;FOR X>0 LOOP @X
addi
$S3,$S3,0xFFFF ;X--
;LOOP Y
bne
$S2,R0,.@Y ;FOR Y>0 LOOP @Y
addi $S2,$S2,0xFFFF ;Y--
;ADD END ENTRY ; FOR THE PREVIOUS POINTER
LIST OR 0xnnFFFFFF
addi
$S4,$S4,0xFFF4 ;-12
or
R10,R10,R12 ;R10 should contain header and R12 the
mask so mark as end is with 0xnnFFFFFF
sw
R10,0($S4)
;POP
RET
lw
R31,8(R29)
lw
$S0,12(R29)
lw
$S1,16(R29)
lw
$S2,20(R29)
lw
$S3,24(R29)
lw
$S4,28(R29)
lw
$S5,32(R29) ;
lw
$S6,36(R29)
jr
R31
addi
R29,R29,0x0030
.Time:
word
0
.SINTAB:
half
128,131,134,137,140,143,146,149,152,155,158,161,164,167,170,173
half
176,179,182,185,187,190,193,195,198,201,203,206,208,210,213,215
half
217,219,222,224,226,228,230,231,233,235,236,238,240,241,242,244
half
245,246,247,248,249,250,251,251,252,253,253,254,254,254,254,254
half
255,254,254,254,254,254,253,253,252,251,251,250,249,248,247,246
half
245,244,242,241,240,238,236,235,233,231,230,228,226,224,222,219
half
217,215,213,210,208,206,203,201,198,195,193,190,187,185,182,179
half
176,173,170,167,164,161,158,155,152,149,146,143,140,137,134,131
half
128,125,122,119,116,113,110,107,104,101,98,95,92,89,86,83
half
80,77,74,71,69,66,63,61,58,55,53,50,48,46,43,41
half
39,37,34,32,30,28,26,25,23,21,20,18,16,15,14,12
half
11,10,9,8,7,6,5,5,4,3,3,2,2,2,2,2
half
1,2,2,2,2,2,3,3,4,5,5,6,7,8,9,10
half
11,12,14,15,16,18,20,21,23,25,26,28,30,32,34,37
half
39,41,43,46,48,50,53,55,58,61,63,66,69,71,74,77
half
80,83,86,89,92,95,98,101,104,107,110,113,116,119,122,125
;Fonction sendlinkedlist
;$A0 List TO SEND
SendLinkedList:
.GPUNOTIDLE:
lw
R2,0x1814(R27)
lui
R3,0x1000 ;MASK TEST B27 GPU BUSY
and
R2,R2,R3
bne
R2,R3,.GPUNOTIDLE ;NO 0x0400 0000 so WAIT
nop
;START DMA CHANNEL 2
ori
R2,R0,0x0800 ; R8=0x0800 CHANNEL 2
sw R2,0x10F0(R27) ; DMA Canal 2 actif (GPU)
;SETUP THE GPU DMA
lui
R3,0x0400 ; CMD GPU1 SET DMA
ori
R3,R3,0x0002 ; SET ENABLE DMA FOR RAM TO VRAM
sw
R3,0x1814(R27) ; SEND TO GPU1
;SETUP THE MADR
sw
$A0,0x10A0(R27) ;MDAR_2
;SETUP THE SIZE
sw
R0,0x10A4(R27) ;BCR_2
;START THE DMA TRANSFERT
lui
R2,0x0100
ori
R2,R2,0x0401
sw
R2,0x10A8(R27) ;START TRANSFERT
nop
;WAIT DMA FINISH
.WaitDMA2:
lw
R3,0x10A8(R27) ;DMA
CHCR_2
lui R2,0x0100 ;TEST B24
and
R3,R3,R2
beq
R3,R2,.WaitDMA2 ;IF B24=1 DMA IS BUSY SO WAIT
nop
jr
R31
nop
LinkedList:
word 0
LinkedList01.asm
Les linked list sont également presque
indispensable pour la 3D dans la mesure où l’on doit traiter un certain nombre
de données: on transféra directement les données dans une commande que l’on
insèrera dans une linked list. Par exemple on considère une liste de
triangle, on traite chaque triangle en transformant ces sommets, ensuite on
applique le clipping des interpolations de textures et ensuite on envoie dans
la liste la commande(s) et on passe au triangle suivant.
La Playstation ne possédant pas de
Z-buffer, les linked list peuvent résoudre partiellement le problème :
Soit en respectant l’ordre de profondeur
durant l’insertion dans la liste (insertion minmax) ou bien une série de liste
en fonction de la profondeur : si par exemple on crée 64 pointeurs de tête
dont chacune contient une liste de polygone en fonction de leur valeur moyenne
de profondeur on obtient ainsi un pseudo-zbuffer de 6bits, la DMA va ensuite parcourir
la liste en utilisant l’algo du dessinateur (du plus éloigné au plus près) et
résoudra le problème par recouvrement.
C’est la façon la plus simple de palier le
problème de profondeur et celle utilisée par la LIB officielle et la DMA
possède également une fonction pour initialiser une table OT qui contient une
table de pointeurs de tête initialisé
16-
Son : SPU
Le SPU est un chip sonore de 16 bits avec
une mémoire de 512KO et 24 voix programmable. Son bus de données est sur 16
bits. D’après le service manual, il est cadencé à 4,19MHZ donc ATTENTION :
Le R3000 est beaucoup plus rapide que le SPU donc lorsque nous écrivons sur les
ports qui auront une influence directe sur les prochaines instructions
(lecture, transfert), il est conseillé d’attendre un petit peu.
La mémoire devra contenir des échantillons
ADPCM en gros 512 KO d’ADPCM PSX équivalent à 2Mo de PCM. Pour ma part cette
ADPCM est le principal reproche que je fais à ce chip car le résultat est un
peu bizarre. (Ce n’ai pas du tout handicapant
de façon général mais des fois on a envie de générer autre chose que du son
avec un chip son lol ).
Le SPU sera vu en détail lors d’un
chapitre spécifique. Pour l’instant le but du jeu est uniquement qu’une des
voix (sur les 24 disponibles) de la PSX nous lisent un échantillon ADPCM. Nous
avons déjà vu dans la section DMA comment transférer des échantillons ADCPM en
SPU-Ram. Pour l’instant on s’en tape : l’adresse 0x1010 devrait contenir des parasites que nous lirons
Avant de voir le SPU il faut d’abord savoir
comment celui-ci interprète les données de sa mémoire tampon de 512 KO.
Attention le SPU est un autre composant
que les émulateurs émulent justement très mal surtout le timing
d’écriture/lecture dans le registre.
Analysons les registres du SPU. Ils sont
tous de 16 bits.
1F801DAAh - SPU Control Register (SPUCNT)
15 SPU Enable (0=Off, 1=On) (Don't care for CD Audio) 14 Mute SPU (0=Mute, 1=Unmute) (Don't care for CD Audio) 13-10 Noise Frequency
Shift (0..0Fh = Low .. High
Frequency) 9-8 Noise Frequency Step (0..03h = Step "4,5,6,7") 7 Reverb Master Enable (0=Disabled, 1=Enabled) 6 IRQ9 Enable (0=Disabled/Acknowledge,
1=Enabled; only when Bit15=1) 5-4 Sound RAM Transfer Mode (0=Stop,
1=ManualWrite, 2=DMAwrite, 3=DMAread) 3 External Audio Reverb (0=Off, 1=On) 2 CD Audio Reverb (0=Off, 1=On) (for CD-DA and
XA-ADPCM) 1 External Audio Enable (0=Off, 1=On) 0 CD Audio Enable (0=Off, 1=On) (for CD-DA and
XA-ADPCM) |
Changes to bit0-5 aren't applied
immediately; after writing to SPUCNT, it'd be usually recommended to wait until
the LSBs of SPUSTAT are updated accordingly. Before setting a new Transfer
Mode, it'd be recommended first to set the "Stop" mode (and, again,
wait until Stop is applied in SPUSTAT).
1F801DAEh - SPU Status Register (SPUSTAT)
15-12 Unknown? seems to be
usually zero 11 Writing to First/Second half of Capture
Buffers (0=First, 1=Second) 10 Data Transfer Busy Flag (0=Ready, 1=Busy) 9 Data Transfer DMA Read Request (0=No, 1=Yes) 8 Data Transfer DMA Write Request (0=No, 1=Yes) 7 Data Transfer DMA Read/Write Request
;seems to be same as SPUCNT.Bit5 6 IRQ9 Flag (0=No, 1=Interrupt Request) 5-0 Current SPU Mode (same as SPUCNT.Bit5-0, but, applied a bit
delayed) |
Avant de pouvoir jouer quoique ce soit,
nous devons d’abord initialiser correctement le SPU :
Initialisation du SPU :
;INIT SPU
InitSPU:
;PUSH R31
addi
R29,R29,0xFFFC ; R29=R29-4
sw
R31,0(R29) ; PUSH R31
;R27 -> I/O BASE
lui
R27,0x1F80 ;R27 POINT TO I/O BASE
; DMA CHANEL 4 ON ;DPCR |=0x80000
lui
R8,0x0008 ;SET 1011 in 4 bytes of channel 4 in order
to start it.
lw
R9,0x10F0(R27) ;READ DPCR
nop
or
R9,R9,R8 ;DMA CHANNEL 4 ON
sw
R9,0x10F0(R27) ;SET DMA CHANEL 4 ON
nop
;OTHER SPU
ori
R8,R0,0x3FFFF ;VOLUME MAX
sh
R8,0x1D80(R27) ;SPU_MVOL_L MAIN VOLUME LEFT
sh
R8,0x1D82(R27) ;SPU_MVOL_R MAIN VOLUME RIGHT
sh
R0,0x1DAA(R27) ;SPU_CONTROL WRITE 0
;WAIT SPU READY
jal
SsWait
nop
;SPU_STATUS=4
ori
R9,R0,0x0004
sh
R9,0x1DAC(R27)
;WAIT SPU READY
.WaitSPU:
ori
R9,R0,0x07FF ; MASK FOR SPU STATUS 2 B0-B10 must be off
0111 1111 1111
lh
R8,0x1DAE(R27) ; READ SPU STATUS 2
nop ; Load Delay
and
R8,R8,R9 ; R8=(R8&R9) and R9=0x7FF all B0-B10
must be off
bne
R8,R0,.WaitSPU ; IF R8!=0 THEN BRANCH
nop ; Delay Branching
;OTHER INIT SPU
sh
R0,0x1D84(R27) ;SPU_REVERB_L
sh
R0,0x1D86(R27) ;SPU_REVERB_R
ori
R8,R0,0xFFFF ;MASK 0xFFFF KEY OFF1/2
sh
R8,0x1D8C(R27) ;SPU_KEY_OFF1
sh
R8,0x1D8E(R27) ;SPU_KEY_OFF2
sh
R0,0x1D90(R27) ;SPU_KEY_FM_MODE1
sh
R0,0x1D92(R27) ;SPU_KEY_FM_MODE2
sh
R0,0x1D94(R27) ;SPU_KEY_NOISE_MODE1
sh
R0,0x1D96(R27) ;SPU_KEY_NOISE_MODE2
sh
R0,0x1D98(R27) ;SPU_KEY_REVERB_MODE1
sh
R0,0x1D9A(R27) ;SPU_KEY_REVERB_MODE2
sh
R0,0x1DB0(R27) ;SPU_CD_MVOL_L MUTE MASTER VOLUME CD
sh
R0,0x1DB2(R27) ;SPU_CD_MVOL_R MUTE MASTER VOLUME CD
sh
R0,0x1DB4(R27) ;SPU_EXT_VOL_L
sh
R0,0x1DB6(R27) ;SPU_EXT_VOL_R
;INIT
ALL 24 VOICE
and
R9,R9,R0 ;R9=0
ori
R9,R9,23 ;24 voice to init (23-0)
lui R10,0x1F80 ;R10 ->I/O BASE 0x1F80
ori
R10,R10,0x1C00 ;R10 -> SPU_VOICE_0_BASE
.LoopVoice:
sh
R0,0x0000(R10) ;VOLUME LEFT
sh
R0,0x0002(R10) ;VOLUME RIGHT
sh
R0,0x0004(R10) ;PITCH
sh
R0,0x0008(R10) ;ENV ADS
sh
R0,0x000A(R10) ;ENV R
addi
R10,R10,0x0010 ;NEXT VOICE
addi
R9,R9,0xFFFF ;Voice count--
bne
R9,R0,.LoopVoice ;Next Voice
nop
jal
SsWait
nop
;SPU_CONTROL = 0xC000;
and R8,R0,R8
ori
R8,R0,0xC000 ;1100 0000 0000 0000 : SPU ON
sh
R8,0x1DAA(R27) ;SPU_CONTROL SET
;REVERB ADRESS
ori
R8,R0,0x1FFF ;Aucune place pour le tampon de la reverb
sh R8,0x1DA2(R27) ;SPU_REVERB_WORK_ADDR
;POP R31
lw
R31,0(R29) ; POP RN
nop ; LOAD DELAY
jr
R31 ; RETOUR
addi
R29,R29,0x0004 ; USE DELAY TO COMPLETE POP
Une fois le SPU
initialisé nous devons configurer une des voix pour que celle-ci puisse jouer
un échantillon en mémoire.
InitVoice0 and Play :
……………………..
;PREPARE VOICE 0
;SET ADRESS FOR VOICE 0
ori
R8,R0,0x1010 ;SPU BUFFER ADRESS=0x1010
srl
R8,R8,3 ;DIVIDE BY 8
sh
R8,0x1C06(R27) ;SET ADRESS SOUND
;OTHER PARAMS FOR VOICE 0
ori
R8,R0,0x3FFFF ;VOLUME MED
sh
R8,0x1C00(R27) ;VOLUME LEFT
sh
R8,0x1C02(R27) ;VOLUME RIGHT
ori
R8,R0,0x0400 ;11KHZ
sh
R8,0x1C04(R27) ;SET PITCH sample 11KHZ
;PLAY VOICE 0
ori
R8,R0,0x0001 ;VOICE 0 START
sh
R8,0x1D88(R27) ;START VOICE 0
La RAM du SPU
est de 512 KO d’où les premier 4 KO sont réservé (voir le chapitre SPU).
Les 16 bytes
suivants contiennent un échantillon ADPCM chargé par la ROM lors de l’écran de
présentation.
Donc on
démarrera nos échantillons à l’adresse 0x1010. Remarquez que les registres
(pour chaque voix) d’adresse de l’échantillon est de 16 bits. L’adresse
effective est donc un multiplie de 8 pour l’accès au 512KO.
Problème lors
des transferts :
Malheureusement je rencontre
toujours des problèmes lors de Transfer d’échantillons par DMA qui ne marche
pas à tous les coups. On dirait qu’il faut attendre une condition spécifique
pour qu’elle s’effectue. LAQUELLE ?
Il me faut
impérativement écrire un debbuger PSX avec lequel on pourra également
transférer du code par port séries depuis mon PC.
17-
Temporisation et gestion
des interruptions
Le R3000 possède en standard 6 lignes
d’interruptions matériels, 2 lignes d’interruptions logicielles et une ligne
Reset.
Un signal RESET le processeur saute à
l’adresse 0xBFC0 0000
Les autres interruptions (ainsi que les
exceptions) provoquent un branchement à l’adresse 0x0000 0080.
Les lignes d’interruption hardware sont
connectées au chip R3000A (mais passe non directement dans le CPU mais dans un
composant à part dans le chip) avec les autres chips spécialisés (GPU, SPU
CD/ROM etc.).
Ci-dessous, les ports I/0 pour la gestion
des interruptions.
1F801070h I_STAT - Interrupt status
register (R=Status, W=Acknowledge)
1F801074h I_MASK - Interrupt mask register (R/W)
Status: Read I_STAT (0=No IRQ, 1=IRQ)
Acknowledge: Write I_STAT (0=Clear Bit, 1=No change)
Mask: Read/Write I_MASK (0=Disabled, 1=Enabled)
0 IRQ0 VBLANK (PAL=50Hz, NTSC=60Hz) 1 IRQ1 GPU Can be requested via GP0(1Fh) command
(rarely used) 2 IRQ2 CDROM 3 IRQ3 DMA 4 IRQ4 TMR0 Timer 0 aka Root Counter 0 (Sysclk or
Dotclk) 5 IRQ5 TMR1 Timer 1 aka Root Counter 1 (Sysclk or
H-blank) 6 IRQ6 TMR2 Timer 2 aka Root Counter 2 (Sysclk or
Sysclk/8) 7 IRQ7 Controller and Memory Card - Byte
Received Interrupt 8 IRQ8 SIO 9 IRQ9 SPU 10 IRQ10 Controller - Lightpen Interrupt
(reportedly also PIO...?) 11-15 Not used (always zero) 16-31 Garbage |
Attribut:
Intreg_pending equ 0x1f801070 ; Interrupt occur
Intreg_mask equ 0x1f801074 ; Interrupt enable
I_Vblank equ 1 ;
Vblank is bit 0
En construction ….
Dans le CPU, le coprocesseur system COP0
contient des registres
TOT (Table of Tables) adresse 0x0100 avec
32 entrées:
L’entrée 0 contient la table des
interruptions.
Pour ajouter un structure dans la PILE
d’interruption, on utilise la fonction du BIOS SysEnqIntRP(priority,struc) j 0x0C
(0x02)
Struct HandleList {
WORD
*pNext; //Le prochain
handler
WORD
*pFunc1; //Fonction 1
WORD
*pFunc2; //Fonction 2
WORD zero; //Je
ne sais plus
};
COUNTERS
La Playstation possède 3 compteurs montés
dans le chip du R3000A pour les besoins de synchronisation et de temporisation.
Chaque compteur possède 3 registres :
un registre de mode (0x11n4), un registre qui contient la valeur actuelle du
compteur (0x11n0), et un registre qui
contient la valeur cible du compteur (la condition pour déclencher une
interruption) (0x11n8)).
Bien que les registres de compteur (0x11n0
et 0x11n8) soit de 32 bits le compte se fait sur les 16 bits inferieurs.
Il semble que chaque compteur peut-être
synchronise soit sur la fréquence du CPU soit en fonction du compteur 1/8 de la
fréquence du CPU (compteur 2) soit le retour horizontal (compteur 1) soit la
fréquence d’un pixel (compteur 0).
Le registre 0x11n4 est structure de cette
façon (NOCache Reference):
1F801104h+N*10h - Timer 0..2 Counter Mode (R/W)
0 Synchronization Enable (0=Free Run,
1=Synchronize via Bit1-2) 1-2 Synchronization Mode (0-3, see lists below) Synchronization Modes
for Counter 0: 0 = Pause counter
during Hblank(s) 1 = Reset counter to
0000h at Hblank(s) 2 = Reset counter to
0000h at Hblank(s) and pause outside of Hblank 3 = Pause until Hblank occurs once, then
switch to Free Run Synchronization Modes
for Counter 1: Same as above, but
using Vblank instead of Hblank Synchronization Modes
for Counter 2: 0 or 3 = Stop counter
at current value (forever, no h/v-blank start) 1 or 2 = Free Run
(same as when Synchronization Disabled) 3 Reset counter to 0000h (0=After Counter=FFFFh, 1=After
Counter=Target) 4 IRQ when Counter=Target (0=Disable,
1=Enable) 5 IRQ when Counter=FFFFh (0=Disable, 1=Enable) 6 IRQ Once/Repeat Mode (0=One-shot, 1=Repeatedly) 7 IRQ Pulse/Toggle Mode (0=Short Bit10=0 Pulse, 1=Toggle Bit10
on/off) 8-9 Clock Source (0-3, see list below) Counter 0: 0 or 2 = System Clock, 1 or 3 = Dotclock Counter 1: 0 or 2 = System Clock, 1 or 3 = Hblank Counter 2: 0 or 1 = System Clock, 2 or 3 = System Clock/8 10 Interrupt Request (0=Yes, 1=No) (Set after Writing) (W=1) (R) 11 Reached Target Value (0=No, 1=Yes) (Reset after Reading) (R) 12 Reached FFFFh Value (0=No, 1=Yes) (Reset after Reading) (R) 13-15 Unknown (seems to be
always zero) 16-31 Garbage (next opcode) |
Exemple:
; Lecture du Counter 2
lh $S0,0x1120(R27)
18-
Introduction au GTE
Nous allons maintenant examiner le
coprocesseur2 du R3000A qui est appelé le GTE pour « Geometry
Transformation Engine ».
Il s’agira juste d’une petite
introduction, un chapitre spécifique consacré à la 3D lui sera consacré.
Le GTE est le coprocesseur 2 du R3000A. Il
est spécialisé dans le calcul vectoriel mais, comme nous allons le voir, de
façon très ciblé.
Il est composé de 64 registres de 32
bits : les Data Register GDn et les Control Register GCn.
De façon générale, les registres GC contiennent les paramètres et GD le
résultat des calculs
On lance les opérations que doit effectuer
GTE via l’instruction COP2 gteInstr
où gteInstr désigne une instruction que peut effectuer le GTE nous en verrons
seulement quelques-unes.
GTE Command Encoding (COP2 imm25 opcodes)
31-25 Must be 0100101b for "COP2 imm25"
instructions 20-24 Fake GTE Command Number (00h..1Fh) (ignored
by hardware) 19 sf - Shift Fraction in IR registers
(0=No fraction, 1=12bit fraction) 17-18 MVMVA Multiply Matrix (0=Rotation. 1=Light, 2=Color,
3=Reserved) 15-16 MVMVA Multiply Vector (0=V0, 1=V1, 2=V2, 3=IR/long) 13-14 MVMVA Translation Vector (0=TR, 1=BK,
2=FC/Bugged, 3=None) 11-12 Always zero (ignored by hardware) 10 lm - Saturate IR1,IR2,IR3 result (0=To
-8000h..+7FFFh, 1=To 0..+7FFFh) 6-9 Always zero (ignored by hardware) 0-5 Real GTE Command Number (00h..3Fh) (used
by hardware) |
Les autres instructions du R3000 pour
interagir avec le GTE sont :
LWC2
gd, imm(base) Lecture de la memoire dans gd.stores
value at imm(base) in gte data register gd.
SWC2
gd, imm(base) Ecriture de la mémoire avec gd.
stores gte data register at imm(base).
MTC2
rt, gd Ecriture de gd avec une valeur
d’un register du MIPS3000. stores register rt in GTE data register gd.
MFC2
rt, gd Lecture de gd qui sera charger
dans un register du MIPS3000. stores GTE data register gd in register rt.
CTC2
rt, gc Ecriture d’un gc avec un
register MIPS3000. Stores register rt in GTE control register gc.
CFC2
rt, gc Lecture d’un gc dans un
registre MIPS3000. stores GTE control register in register rt.
Donc pour charger ou lire des valeurs de
registres du GTE nous pouvons nous servir des fonctions:
CTC2 CFC2 pour les registres gc
MTC2 MFC2 ou LWC2 SWC2 pour les registres
gd.
Attention, les instructions MTC2,MFC2,CTC2 et CFC2 ont des delaies (au
moins 1 cycles) pour assurer j’insere 2 autres instruction avant de démarrer
une nouvelle opération du Cop2 ou d’interpréter une lecture.
La LibGTE le fait quasi-systématiquement
en insérant deux instruction (souvent des NOP).
Par contre, il semble que les instructions
COP2 soient « interlocker » mais que certaines contiennent quand meme
2 delaies !!
En tout cas ca peux etre la cause de
probleme : c’est pourquoi tant que je ne connais pas le fond du probleme j’inserer
toujoujrs 2 nop apres un ctc,mtcmcfc et 2 nop apres un COP2.
Attention, les instruction LWC2 et SWC2 ne
sont jamais utilise par la bibliotheque officiel, et je soupconne un bug (voir
RTCUBL04.asm lors d’un resize) … affaire
a suivre.
Accéder au GTE :
Avant d’utiliser le GTE nous devons le
mettre en service en allumant le bit30 du registre 12 (registre SR) du COP0.
MFC0 R8,SR ; Recupere SR dans R8
LUI R9,0x4000 ; Bit B30 = 1
OR
R8,R8,R9 ; Allume le bit 30
MTC0 R8,SR ; Mis en service du GTE
Les 64 registres du GTE (32 gd et 32 gc) représentent
divers éléments en fonction de l’opération effectuée.
Attention !!! Les formats varient en
fonction des éléments, de plus les réels n’est pas des Flottants mais une
virgule fixe qui va dépendre du type de l’élément !
Un registre contient en général plusieurs
éléments sur 16 ou 8 bits.
Implicitement le GTE est composé de
plusieurs structures eux-mêmes composées d’éléments spécifiques. Les structures
sont par exemple, une matrice 3x3, un vecteur de translation, des vecteurs
génériques, des vecteurs de résultats etc.
Par exemple les 9 éléments de la
matrice3x3 sont en virgule fixe avec 1 bit pour le signe, 3 bits pour la partie
entière et 12 bits pour la partie fractionnelle (pas de 1/4096). En complément
a deux pour les signées.
Exemple :
1.0 => 0x1000
.5 => 0x0800
.25 => 0x0400
-1.0 => 0xF000
Etc.
Un élément (X,Y,Z) d’un vecteur générique
(V0,V1,V2) est un entier signé sur 16 bits.
Un éléments (X,Y,Z) du vecteur de
translation est un réelle de 32 bits signe 15 bits pour la partie entière et 16
bits pour la fractionnée (pas de 1/65536).
Voici une liste des registres du GTE (non
exhaustive) :
Registres GTE
No. Name Description
gc0 R12R11 Rotation
matrix éléments 11, 12
gc1 R21R13 Rotation
matrix éléments 13, 21
gc2 R23R22 Rotation
matrix éléments 22, 23
gc3 R32R31 Rotation
matrix éléments 31, 32
gc4
R33 Rotation
matrix element 33
gc5 TRX Translation
vector X
gc6 TRY Translation
vector Y
gc7 TRZ Translation
vector Z
gd0 VX0VY0 Vector
0 X and Y.
gd1 VZ0 Vector
0 Z.
gd2 VX1VY1 Vector 1 X and Y.
gd3 VZ1
Vector
1 Z.
gd4 VX2VY2 Vector 2 X and Y.
gd5 VZ2
Vector
2 Z.
gd9 IR1 Resultats 16 bits
gd10 IR2 Resultats
16 bits
gd11 IR3
Resultats
16 bits
gd12 SXY0 Resultat
Vecteur 0 (FIFO) transformé X et Y (16 bits signées chacun)
gd13 SXY1 Resultat
Vecteur 1 (FIFO) transformé X et Y (16 bits signées chacun)
gd14 SXY2 Resultat Vecteur 2 (FIFO)
transformé X et Y (16 bits signées chacun)
gd15 SXYP Resultat
Vecteur P transformé X et Y (16 bits signées chacun)
gd16 SZ0 Resultat
Vecteur 0 transformé Z (16 bits poids forts non signées)
gd24 MAC0 Resultats 32
bits
gd25 MAC1 Resultats 32
bits
gd26 MAC2 Resultats 32
bits
gd27 MAC2 Resultats 32
bits
Exemple d’instructions :
Mnémo Asm
R3000 description
rtv0
cop2
0x0486012 v0 * rotmatrix
rtv1
cop2
0x048E012 v1 * rotmatrix
rtv2
cop2
0x0496012 v2 * rotmatrix
rtv0tr
cop2
0x0480012 v0 * rotmatrix + tr vector
rtv1tr
cop2
0x0488012 v1 * rotmatrix + tr vector
rtv2tr
cop2
0x0490012 v2 * rotmatrix + tr vector
Nous allons commencer par les fonctions de
rotation et Translation du GTE en utilisant 4 structures sous-jacente :
-
Un
vecteur générique le V0.
-
La
matrice3x3 de rotation,
-
le
vecteur de translations
-
l’accumulateur
(32bits MAC 16 bits IR).
-
Les
résultats seront stocke dans MAC et IR (MAC sur 32 bits et IR sur 16 bits).
Nous utiliserons, dans cette série de
tutoriaux consacrée, la convention matrice suivante :
Convention Matrice :
XT YT ZT
X |R11 R21 R31| XT =R11.X+R12.Y+R13.Z
Y |R12 R22 R32| YT =R21.X+R21.Y+R23.Z
Z |R13 R23 R33| ZT =R31.X+R32.Y+R33.Z
Registre Gd31 FLAG
Le registre de données 31 est le registre
de drapeaux pour contrôler les erreurs de calcul. Le bit 31 de ce registre
s’allume lorsque la dernière opération a provoqué une erreur.
Premier programme :
On va utiliser le GTE de façon minimaliste
pour écrire un programme qui fait pivoter un triangle texturée sur lui-même. Ce
programme permettra de nous familiariser avec les instructions R3000 pour gérer
le GTE et quelques structures élémentaires du GTE comme la matrice 3x3 et les
vecteurs.
D’abord voici le corps du
programme qui travaillera en simple buffering avec synchronisation avec le
retour vertical grâce à la fonction WaitVSyncOnly vu auparavant. (Si vous
envoyer plus de triangles, utilisez le double-buffering).
GTETut01.asm :
Start:
;GPU INIT
jal
InitStdGPU
nop
;CLEAR MAIN SCREEN
jal
WaitGPUReady
nop
jal
GPUClearVRAM
nop
;STANDART INIT
jal
ResetEntryInt
nop
;PREPARE function PAD_INIT
addi
$A0,R0,0x0001
jal
InitCard
nop
jal
StartCard
nop
jal
BuInit
nop
jal
StopCard
nop
;PAD
jal
PAD_INIT
nop
;LOAD A IMAGE 256*256 ON 768,0
lui $A0,>Image
ori
$A0,$A0,Image ;A0=>Image
;IF IMAGE CONTAIN HEADER SKIP THEM
;Addiu
$A0,$A0,32 ;for
example Image has 32 bytes header.
lui
$A1,0x0000 ;0
-> on TOP of VRAM
ori
$A1,$A1,768 ;768 RIGHT on VRAM
lui $A2,256 ;Height of image=256
ori
$A2,$A2,256 ;Width =256
jal
Mem2VramIO
nop
;******* MAIN LOOP **********************
MainLoop:
;WAIT LITTLE
lui
$A0,0x0010
jal
M3000Delay
nop
;TRANSFORM MESH TO POLYTRANS
jal DemoGTE
nop
;NEXT ANGLE
lui
$A0,>CurRad
ori $A0,$A0,CurRad
lh
R8,0($A0)
nop
addi
R8,R8,1
sw
R8,0($A0)
;WAIT GPU READY
jal
WaitGPUReady
nop
;WAIT VSYNC
jal
WaitVSyncOnly
nop
;CLEAR SCREEN
jal
GPUClearVRAM
nop
;DRAW POLYTRANS
jal
DrawTransTriTx
nop
;GOTO LOOP
j MainLoop
nop
;FONCTIONS GTEDEMO LOADMRZ16 et
DRAWTRANSTRITX
;………………………………………………………… voir plus bas
;VARIABLES
CurRad:
word
0,0
Mesh:
half
-85,85,10,0 ;Mesh V0
half
0,-85,10,0 ;Mesh V1
half
85,85,10,0 ;Mesh V2
half
0,0,0,0
POLYTRANS:
word
0,0,0,0
word
0,0,0,0
Image:
Insert « ImageFileName »
On utilisera une texture de 256x256
chargée dans la VRAM en 768,0. La texture page sera donc 0x010C (Y=0 ;
X=768/16=12)
La variable CurRad contiendra l’index
courant de l’angle de rotation que le programme incrémente à chaque frame, la
variable Mesh pointe sur l’espace contenant 3 sommets de notre triangle centre
en 0. Comme nous n’aurons que 16 angles possibles on attendra environ 32ms
entre chaque frame.
La variable POLYTRANS contiendra le
résultat c’est-à-dire les sommets transformés par le GTE que nous utiliserons
pour dessinez notre triangle.
Voyons à présent les autres fonctions
celles qui nous intéressent.
Nous devons charger la matrice de rotation
suivante :
Rotation Z d’un polygone texture.
XT YT ZT
X |COS SIN 0.0
| XT =COS.X-SIN.Y
Y |-SIN COS 0.0
| YT =SIN.X+COS.Y
Z |0.0 0.0 1.0
| ZT =Z
Le GTE ne possède pas de fonction sinus et
cosinus c’est à nous de les générer. Dans notre exemple on utilisera une table
minimale de fonction sinus à 16 entrées.
La fonction LoadMrz16 charge la matrice RZ
en fonction du paramètre ID passe en $A0 tel que Rad=(PI/8)*ID.
;LOAD MRZ $A0=Id angle
LoadMrz16:
;PUSH
addi
R29,R29,0xFFF0 ; R29=R29-16
sw
R31,8(R29) ; PUSH R31
sw
$S0,12(R29)
;Common Value GTE
ori
R8,R0,0x1000 ;GTE 1.0 value
ctc2
R0,Gc3 ;GC3=M32|M31=0.0
ctc2
R8,Gc4 ;GC4=M33=1.0
;$A1 point to TAB SIN
lui $A1,>.SinTab
ori
$A1,$A1,.SinTab
;LOAD SIN
and
$S0,R0,R0 ;$S0=0
add
$S0,$S0,$A0 ;$S0 ADD ID
andi
$S0,$S0,0x000F ;KEEP INDEX
sll
$S0,$S0,1 ;ID*2
add
$S0,$S0,$A1 ;$S0->SinTab+id
lhu
R11,0($S0) ;R11=SIN(ID)
nop
or
R9,R11,R0 ;R9=SIN(ID)
beq R9,R0,.@1 ;IF r9=0 SKIP minus
nop
;R9=-SIN(ID)
xori
R9,R9,0xFFFF ;INVERSE COMPLEMENT
addi
R9,R9,1 ;COMPLEMENT A DEUX R9=-SIN(ID)
.@1:
sll
R11,R11,16 ;R11=SIN|0
ctc2
R11,Gc1 ;GC1=M21|M13=SIN|0
sll R9,R9,16 ;R9=-SIN|0
;LOAD COS
addi
R8,$A0,4 ;R8=ID+4
andi
R8,R8,0x000F
sll
R8,R8,1
add
R8,R8,$A1
lhu
R8,0(R8) ;R8=cos(ID)
nop
or
R9,R9,R8 ;R9=-SIN|COS
ctc2
R8,Gc2 ;GC2=M23|M22=0|COS
ctc2
R9,Gc0 ;GC0=M12|M11=-SIN|COS
;POP RET
lw
R31,8(R29) ; POP RN
lw
$S0,12(R29) ; POP S0
jr
R31 ; RETOUR
addi
R29,R29,0x0010 ; SP+=16
.SinTab: ;16 entre sin(i)=SinTab[i&0xF];
cos(i)=SinTab[(i+4)&0xF];
half 0,1568,2896,3784,4096,3784,2896,1568
half
0,63969,62640,61752,61440,61752,62640,63969
Encore une fois cette fonction charge les
valeurs que devra contenir les éléments de la matrice 3x3 représenté par gc0 à
gc4. Chaque élément de la matrice est en virgule fixe avec 1 bit pour le signe,
3 bits pour la partie entière et 12 bits pour la partie fractionnelle (pas de
1/4096) et en complément à deux.
Gardons cette fonction elle nous servira
par la suite nous l’étendrons (si vous ne l’avez pas déjà fait) à 256 valeur
possible d’angle.
DEMOGTE est notre fonction principale qui
charge la matrice avec la fonction LoadMRZ et transforme les sommets de Mesh,
un triangle de (-85,-85),(0,-85),(85,85) dans PolyTrans.
Nous utilisons seulement la fonction
rtv0tr du GTE (0x0480012) qui se charge de multiplie le vecteur v0 par la
matrice MR et additionne par le Vecteur TR avec des valeurs de 160 120 pour à
peu près replacer le triangle au centre de l’écran (320*240). Il nous faut donc
répéter l’opération pour les 3 sommets de notre polygone.
Le résultat est stocké dans les registres
Gd25-Gd27 du GTE que nous transférons ensuite dans notre espace PolyTrans.
;ROTATE POLY AROUND Z AXIS
DEMOGTE:
;PUSH
addi
R29,R29,0xFFF0 ; R29=R29-16
sw
R31,8(R29) ; PUSH R31
sw
$S0,12(R29)
;START
jal
EnableGTE
nop
;*** LOAD MROTZ element
lui
$S0,>CurRad
ori
$S0,$S0,CurRad
lh
$A0,0($S0)
jal
LoadMrz16
nop
;*** TX=160 TY=120
ori
R8,R0,160
ctc2
R8,Gc5 ;TrX=160
ori
R9,R0,120
ctc2
R9,Gc6 ;TrY=120
;*** LOAD VECTOR V0
lui
$S0,>.Mesh
ori
$S0,$S0,.Mesh ;S0=>.Mesh
lui
$S2,>PolyTrans
ori
$S2,$S2,PolyTrans ;S2->PolyTrans
ori
$S1,R0,3
.LoopTrans:
lwc2
R0,0($S0) ;VX,VY
lwc2
R1,4($S0) ;VZ
nop ;DELAY
nop ;DELAY
;*** USE RTV0TR cop2 $0486012
cop2
0x0480012 ;RTV0TR
nop ;DELAY
nop ;DELAY
;*** STORE RESULT MAC1,MAC2,MAC3
mfc2
R8,R25 ;MAC1 X
mfc2 R9,R26 ;MAC2 Y
mfc2
R10,R27 ;MAC3 Z
sh
R8,0($S2) ; Resulta Vn dans polytrans X
sh
R9,2($S2) ; Resultat Vn dans polytrans Y
sh
R10,4($S2) ; Resultat Vn dans polytrans Z
;** NEXT VERT
addi
$S1,$S1,0xFFFF ;$S1-- CNT--
addi
$S2,$S2,8 ;NEXT
VERT POLYTRANS
bne
R0,$S1,.LoopTrans
addi
$S0,$S0,8 ;NEXT VERT MESH
;POP RET
lw
R31,8(R29) ; POP RN
lw
$S0,12(R29) ; POP S0
jr R31 ; RETOUR
addi
R29,R29,0x0010 ; SP+=16
Enfin, la fonction DrawTransTriTx ne pose
pas de problème: nous envoyons simplement au GPU la commande de dessin de triangle
texture avec les sommets de PolyTrans.
Remarquez que nous n’utilisons pas la
composante Z lors du dessin. Seul Y et X sont pris en compte.
Une autre subtilité est que lors de la
lecture du sommet, le little endian inverse l’ordre X<<16|Y en
Y<<16|X lorsque le vecteur est charge dans un registre !
Exemple avec rad = 0 donc pour V0 X= X – 0
et Y = Y et après translation X =-85+160 = 75 (0x004B) et Y = 85 + 160 = 245
(0x00F5).
;*** STORE RESULT MAC1,MAC2,MAC3
mfc2
R8,R25 ; MAC1 X 0x004b
mfc2 R9,R26 ; MAC2 Y 0x00f5
…………………………………………………………..
sh R8,0($S2) ; Resultat Vn.X dans polytrans X => 0x004b
-> 0x4b00
sh
R9,2($S2) ; Resultat Vn.Y dans polytrans Y => 0x00F5
-> 0xF500
………………………………………
;GET
VERTEX
lw
R10,0($A0) ; V0 0($A0) = 0x4b00 f500 apres lw r10 =
0x00f5 004b !
C’est pourquoi on doit considérer lors de
lecture et écriture l’ordre X<16|Y et non Y<<16|X comme c’est le cas
lorsque nous envoyons les coordonnées de sommet par registre en dure.
Oui c’est assez lourd-dingue : c’est
la « magie » (pour être polie)
du little-endian mais les habitués du x86 ne devraient pas être très déroutés.
DrawTransTriTx:
;DRAW TRANSFORMED TRIANGLE BY GTE
lui $A0,>POLYTRANS
ori
$A0,$A0,POLYTRANS ;$A0->POLYTRANS
;GET VERTEX
lw
R10,0($A0) ;V0
lw
R11,8($A0) ;V1
lw
R12,16($A0) ;V2
;TRIANGLE TEXTURE
lui
R8,0x2480 ;TEXTURED triangle
ori
R8,R8,0x8080 ;GRAY TRIANGLE
sw
R8,0x1810(R27)
;VERTEX 0
sw
R10,0x1810(R27) ;SEND COORDS Y|X
ori
R8,R0,0xFF00 ;V=255 U=0
sw
R8,0x1810(R27) ;SEND CLUTID AND COORDTEXT
;VERTEX 1
sw
R11,0x1810(R27) ;SEND COORDS Y|X
lui R8,0x010C ;TEXTURE PAGE: 15 bit direct X=12*64=>768
Y=0
ori R8,R8,0x0080 ;V=0 U=128
sw
R8,0x1810(R27) ;SEND CLUTID AND COORDTEXT
;VERTEX 2
sw
R12,0x1810(R27) ;SEND COORDS Y|X
lui
R8,0x0000 ;DON'T CARE
ori
R8,R8,0xFFFF ;V=255 U=255
sw
R8,0x1810(R27) ;SEND CLUTID AND COORDTEXT
jr R31 ; RETOUR
nop
GteTut1.asm en action
J’encourage le lecteur à écrire son propre
programme et à étendre la fonction de rotation pour une meilleure fluidité. Ou
bien s’amuser a pivoté le triangle sur un autre axe que Z pour donner un effet 3D.
Deuxième programme :
Nous allons maintenant essayer d’écrire un
programme minimal 3D (sans problème de clipping ou de profondeur) en
introduisant la fonction RTPS et 3 autres structures du GTE.
On va juste mettre en rotation un petit maillage
(un petit cube texture 6 faces de 4 sommets chacune) en fonction du PAD.
On va simplifier au maximum donc on va
effectuer les opérations en coordonne local de l’objet, ensuite on se replace
dans les coordonne de l’observateur en reculant de quelque unité. Et on
appliquera une simple projection avec la fonction du GTE RTVP. On calculera la
normal de chaque face pour l’envoi l’affichage ou non.
La rotation se fera seulement en Y. Le pad
contrôlera seulement la position du cube en X et Y.
Nous sommes obligés ici d’utiliser le
double buffer car la PSX ne peut dessiner plus de 2 triangles texturés avec
effacement de l’écran dans un frame par I/O.
Le corps de notre programme se présentera
ainsi :
ORG 0x8008 0000
Start:
; SAVE R31
lui
R8,>GlobalReturn
ori
R8,R8,GlobalReturn
sw
R31,0(R8)
;R27 POINT TO I/O BASE
lui
R27,0x1F80
;GPU INIT 320*240 PAL
jal
InitStdGPU
nop
; INITIALISATION: INSTALL PAD IRQ ON
VBLANK WITH BIOS FUNCTIONS
; SEQUENCE A FAIRE SINON LE PAD NE FONCTIONNE
PAS (Les cmds CD ne sont pas obligaotire REMOVE96 et Init96).
jal ResetEntryInt
nop
;PREPARE function before using PAD_INIT
addi
$A0,R0,0x0001
jal
InitCard
nop
jal
StartCard
nop
jal
BuInit
nop
jal
StopCard
nop
;PAD
jal
PAD_INIT
nop
; AGAIN R27 POINT TO I/O BASE
lui R27,0x1F80
; INIT DOUBLE BUFFER
jal
InitSurface
nop
; LOAD TEXTURE
lui
$A0,>Texture1
ori
$A0,$A0,Texture1
ori
$A1,R0,768
jal
ZPS2VramIO
nop
;INIT GTE
jal
EnableGTE
nop
; MAIN PROGRAMM
Main:
;READ PAD DATA
jal WaitVSync
nop
lui
$T0,>CurRad
ori
$T0,$T0,CurRad
.ReadPad:
;MODE 1 READ PAD
lui
$S1,>PadDataI
ori
$S1,$S1,PadDataI
lw
$S0,0($S1) ;PAD DATA ON $S0
nop
.Right:
andi
$T0,$S0,0x8000 ;RIGHT
?
beq
$T0,R0,.Left
nop
;RIGHT
lw $T1,0($T0)
nop
addi
$T1,$T1,-1
sw $T1,0($T0)
j
.down
.Left:
andi
$T0,$S0,0x2000 ;LEFT ?
beq $T0,R0,.Down
nop
lw
$T1,0($T0)
nop
addi
$T1,$T1,1
sw
$T1,0($T0)
.Down:
andi
$T0,$S0,0x4000 ;DOWN ?
beq
$T0,R0,.Up
nop
.Up:
andi
$T0,$S0,0x1000 ;Up?
beq
$T0,R0,.Buttons
nop
.Buttons:
.Select:
andi
$T0,$S0,0x0100 ;Button Select ?
beq $T0,R0,.Process
nop
j
.EndProg ; Termine le programme
nop
; MAIN PROCESSING
.Process:
;PROCESSING HERE
jal MainGTE
nop
;DRAWING
.InitDraw:
jal
WaitGPUIdle
nop
jal
WaitGPUReady
nop
.Draw:
jal
ClearBackBuffer
nop
;DRAWING HERE
jal
DrawCube
nop
.FlushDraw:
jal
WaitGPUIdle
nop
jal
WaitGPUReady
nop
jal
WaitVSyncOnly
nop
jal
FlipSurface
nop
.EndMain:
beq
R0,R0,Main
nop
.EndProg:
; Restore R31
lui
R8,>GlobalReturn
ori
R8,R8,GlobalReturn
lw
R31,0(R8)
nop
jr
R31
nop
GlobalReturn:
word
0
; Notre Maillage :
FaceColor :
word
0xA0A0A0
word
0xA0A0A0
word 0x808080
word 0x808080
word 0x606060
word 0x606060
MeshCube:
; FRONT FACE
half
-32,32,-32,0 ;V0
half
-32,-32,-32,0 ;V1
half
32,32,-32,0 ;V2
half
32,-32,-32,0 ;V3
; BACK FACE
half
32,32,32,0 ;V6
half
32,-32,32,0 ;V7
half
-32,32,32,0 ;V4
half
-32,-32,32,0 ;V5
; FACE GAUCHE
half
-32,32,32,0 ;V4
half
-32,-32,32,0 ;V5
half
-32,32,-32,0 ;V0
half
-32,-32,-32,0 ;V1
; FACE DROITE
half
32,32,-32,0 ;V2
half
32,-32,-32,0 ;V3
half
32,32,32,0 ;V6
half
32,-32,32,0 ;V7
; FACE HAUT
half
-32,-32,-32,0 ;V1
half
-32,-32,32,0 ;V5
half
32,-32,-32,0 ;V3
half
32,-32,32,0 ;V7
; FACE BASSE
half
-32,32,32,0 ;V4
half
-32,32,-32,0 ;V0
half
32,32,32,0 ;V6
half
32,32,-32,0 ;V2
;ANGLES DE ROTATION Y
CurRad:
word
0
Voyons la fonction de transformation en
perspective RTPS :
RTPS cop2 $0180001 Perspective transformation
Dans notre exemple on ne s’intéressera
uniquement qu’à la projection proprement dites.
Les seuls paramètres d’entrée qui nous
intéresserons seront :
-
La
matrice de rotation MR Gc0-Gc4
-
Le
vecteur de translation TR Gc5,Gc6,Gc7
-
La
distance de projection H Gc26
-
Les
offsets d’écran OFFX,OFFY
Gc24,Gc25
Les paramètres de sortie qui nous
intéressent seront :
-
SPXP,
SPYP Gd15
Les rotations et translation ne change pas
par rapport à RTV0TR mais une transformation de projection est appliquée.
SPXP = OFFX + (H/SZ)*X
SPYP = OFFY + (H/SZ)*Y
Le GTE contient également une petite pile
FIFO du résultat des SP ;
SP0 = SP1 ; SP1 = SP2 ; SP2 =
SPP
Pour l’instant on va ignorer plusieurs
problèmes à résoudre que l’on rencontrera plus tard lorsqu’il s’agira d’écrire
un petit moteur 3D. En revanche il est indispensable de clipper les faces en
fonction de la valeur de leur vecteur normal. Pour cela on se servira d’une
autre fonction du GTE NCLIP 0x1400006 qui effectue un produit vectoriel :
(SV1-SV0)^(SV2-SV0).
NCLIP COP2 0x1400006 Produit vectoriel (Cross
Product)
IN: SXSY0
(GD12), SXSY1 (GD13), SXSY2 (GD14)
Calcul: (SV1-SV0)^(SV2-SV0)
OUT : MAC0
(GD24)
Tout d’abord nous avons besoin d’une
fonction pour charger la matrice de rotation sur l’axe Y. Notre table de sinus sera
cette fois ci de 256 entrées.
; LOAD MRY $A0=Id angle with 256 entries
sinus table
;
***********************************
; XT YT ZT
;
X | COS 0.0 -SIN
;
Y | 0.0 1.0 0.0
;
Z | SIN 0.0 COS
;
***********************************
;
GC0=M12|M11=0.0|COS
;
GC1=M21|M13=0.0|SIN
;
GC2=M23|M22=0.0|1.0
;
GC3=M32|M31=0.0|-SIN
;
GC4=N/A|M33=0.0|COS
LoadMry256:
;Common Value GTE
ori
R8,R0,0x1000 ;GTE 1.0 value
ctc2
R8,Gc2 ;GC2=M23|M32=0.0|1.0
;$A1
point to TAB SIN
lui
$A1,>SinTab256
ori
$A1,$A1,SinTab256
;LOAD SIN
and
R2,R0,R0 ;R2=0
add
R2,R2,$A0 ;R2 ADD ID
andi
R2,R2,0x00FF ;KEEP INDEX
sll
R2,R2,1 ;ID*2
add
R2,R2,$A1 ;R2->SinTab+id
lhu
R11,0(R2) ;R11=SIN(ID)
nop
or
R9,R11,R0 ;R9=SIN(ID)
xori R9,R9,0xFFFF ;INVERSE COMPLEMENT
addi
R9,R9,1 ;COMPLEMENT A DEUX R9=-SIN(ID)
;SUITE
ctc2
R11,Gc1 ;GC1=M21|M13=0.0|SIN
ctc2
R9,Gc3 ;GC3=M32|M31=0.0|-SIN
;LOAD COS
addi
R8,$A0,64 ;R8=ID+32
andi
R8,R8,0x00FF
sll
R8,R8,1
add
R8,R8,$A1
lhu
R8,0(R8) ;R8=cos(ID)
nop
or
R9,R9,R8 ;R9=-SIN|COS
ctc2
R8,Gc0 ;GC0=M12|M11=0.0|COS
ctc2
R8,Gc4 ;GC4=N/A|M33=0.0|COS
;RET
jr
R31
nop
;TABLE DE SINUES 256 Entree:512 octets.
SinTab256:
half
0,100,200,301,401,501,601,700,799,897,995,1092,1189,1284,1379,1474
half
1567,1659,1751,1841,1930,2018,2105,2191,2275,2358,2439,2519,2598,2675,2750,2824
half
2896,2966,3034,3101,3166,3229,3289,3348,3405,3460,3513,3563,3612,3658,3702,3744
half
3784,3821,3856,3889,3919,3947,3973,3996,4017,4035,4051,4065,4076,4084,4091,4094
half
4096,4094,4091,4084,4076,4065,4051,4035,4017,3996,3973,3947,3919,3889,3856,3821
half
3784,3744,3702,3658,3612,3563,3513,3460,3405,3348,3289,3229,3166,3101,3034,2966
half
2896,2824,2750,2675,2598,2519,2439,2358,2275,2191,2105,2018,1930,1841,1751,1659
half
1567,1474,1379,1284,1189,1092,995,897,799,700,601,501,401,301,200,100
half
0,65436,65336,65235,65135,65035,64935,64836,64737,64639,64541,64444,64347,64252,64157,64062
half
63969,63877,63785,63695,63606,63518,63431,63345,63261,63178,63097,63017,62938,62861,62786,62712
half
62640,62570,62502,62435,62370,62307,62247,62188,62131,62076,62023,61973,61924,61878,61834,61792
half
61752,61715,61680,61647,61617,61589,61563,61540,61519,61501,61485,61471,61460,61452,61445,61442
half
61440,61442,61445,61452,61460,61471,61485,61501,61519,61540,61563,61589,61617,61647,61680,61715
half
61752,61792,61834,61878,61924,61973,62023,62076,62131,62188,62247,62307,62370,62435,62502,62570
half
62640,62712,62786,62861,62938,63017,63097,63178,63261,63345,63431,63518,63606,63695,63785,63877
half
63969,64062,64157,64252,64347,64444,64541,64639,64737,64836,64935,65035,65135,65235,65336,65436
Ensuite voyons les structures qui
contiendront les données transformées. Nous aurons 2 structures:
QuadClip:
word
0,0
word
0,0
word
0,0
QuadTrans:
;FACE FRONT
Word
0,0,0,0
;FACE BACK
Word
0,0,0,0
;FACE LEFT
Word
0,0,0,0
;FACE RIGHT
Word
0,0,0,0
;FACE TOP
Word
0,0,0,0
;FACE BOTTOM
Word
0,0,0,0
QuadClip contiendra pour chaque face le résultat
de NCLIP et QuadTrans les sommets transformée en X,Y.
Voyons donc maintenant notre fonction
MainGTE qui est le cœur du programme et qui va se charger à chaque frame de
remplir QuadClip et QuadTrans.
;GTE CUBE TRANSFORM
GTE_CUBETRANS:
;PUSH
addi
R29,R29,0xFFE0 ; R29=R29-32
sw
R31,8(R29) ; PUSH R31
sw
$S0,12(R29)
sw
$S1,16(R29)
sw
$S2,20(R29)
;INIT PTR
lui
$S0,>MeshCube
ori
$S0,$S0,MeshCube ;S0=>.MeshCube
lui
$S1,>QuadTrans
ori
$S1,$S1,QuadTrans ;S1->QuadTrans
lui
$S2,>QuadClip
ori
$S2,$S2,QuadClip ;S1->QuadClip
;INIT RTPS
;PARA PROJ
;H=128.0
ori
R2,R0,0x0100 ;H=128
ctc2
R2,R26
;OFFSET SCREEN
lui R2,160
ctc2
R2,R24 ;OFFX=160
lui
R3,120
ctc2
R3,R25 ;OFFY=120
; OBJECT POSITION
ctc2
R0,Gc5 ;TRX
ori R2,R0,64
ctc2
R2,Gc6 ;TRY
ori
R3,R0,256
ctc2
R3,Gc7 ;TRZ
; LOAD ROTATION MATRIX
lui
$A0,>CurRad
ori $A0,$A0,CurRad
lw
$A0,0($A0)
jal
LoadMry256
nop
;FOR EACH FACE
ori R8,R0,6 ;6 faces
.loopface:
;FOR EACH VERT
ori
R9,R0,4 ;4 Vert
.loopVert:
lwc2
R0,0($S0) ;VX,VY dans gd0
lwc2
R1,4($S0) ;VZ
dans gd1
nop
nop
;COP
RTPS
;Xp=OFFX+(H/SZ)*X ; Yp=OFFY
+ (H/SZ)*Y
cop2 0x0180001
nop
nop
;STORE THE RESULT ON CUBETRANS
swc2
r15,0($S1) ; store on CubeTrans
addiu
R9,R9,-1 ; vert--
addiu
$S1,$S1,4 ; Next vert_trans ptr
bne
R9,R0,.loopVert
addiu
$S0,$S0,8 ; Next vert_mesh ptr
;NCLIP TO CHECK IF THE FACE IS VISIBLE
cop2
0x1400006 ; DOT PRODUCT TO MAC0
addiu
R8,R8,-1 ; face--
nop
mfc2
R3,R24 ; MAC0 dans R3
nop ; VERY IMPORTANT
nop ; BEFORE STORE THE RESULT
sw
R3,0($S2) ; STORE CLIP
bne
R8,R0,.loopface
addiu
$S2,$S2,4 ; Next ptr ClipFace
;POP RET
lw
R31,8(R29)
lw
$S0,12(R29)
lw
$S1,16(R29)
lw
$S2,20(R29)
jr
R31 ; RETOUR
addi R29,R29,0x0020 ; SP+=32
La fonction commence par initialisée les
paramètres du GTE H= 128 ; OFFX = 160 ; OFFY = 120 ; TRX =
0 ; TRY = 64 ; TRZ =256.
Le paramètres H représente la distance du
plan de projection plus il est petit plus la déformation de profondeur est
importante. En revanche, plus H est grand et plus la projection tend à être
cavalière.
Lorsque Z est inférieur à H le GTE
n’effectue plus le calcul correctement mais nous verrons cela dans un prochain
chapitre.
Les paramètres TRX,TRY et TRZ descend
l’objet et recule notre cube pour une meilleur visualisation mais je vous
encourage à modifier le programme pour gérer la position de l’objet avec le
PAD.
Ensuite nous appelons, après avoir
récupère l’angle de rotation courant, LoadMry256 pour charger la matrice de
rotation.
Ensuite pour chacune des 6 faces, nous
transformons chaque sommet et nous stockons le résultat dans QuadTrans, une
fois les sommets de la face transformés, nous utilisons NCLIP et stockons le
résultat dans QuadClip.
Remarquez qu’ici notre little-endian ne
nous dérange pas car nous stockons le registre GD15 (XPYP) directement dans les
sommets de QuadTrans avec l’instruction swc2 r15, offset(R).
La fonction DrawCubeProc se charge
simplement de vérifier le résultat de NCLIP chaque face et si la valeur est
négative utilise DrawGenQuad pour dessiner la face.
DrawCubeProc:
;PUSH
addi
R29,R29,0xFFE0
sw
R31,8(R29)
sw
$S0,12(R29)
sw
$S1,16(R29)
sw
$S2,20(R29)
sw
$S3,24(R29)
;INIT
PTR
lui
$S0,>QuadClip
ori
$S0,$S0,QuadClip ; S0=> QuadClip
lui
$S1,>QuadTrans
ori
$S1,$S1,QuadTrans ; S1->QuadTrans
ori
$S2,R0,6 ; S2 = cnt faces : 6 faces
lui
$S3,>FaceColor
ori
$S3,$S3,FaceColor ; S3->Face color
.loopface:
;CHECK CLIP
lw
R8,0($S0)
nop
bgtz
R8,.endface ; if positive skip
addiu
$S0,$S0,4 ; next clip face
; WAIT GPU CMD
jal
WaitGPUReady
nop
;SET PARAMETERS
lw
$A1,0($S3) ; Get Color face
jal
DrawGenQuad
or
$A0,$S1,R0 ; QUAD TRANS
.endface:
Addiu
$S3,$S3,4 ; Next Color Face
addiu
$S2,$S2,-1 ; face--
bne
$S2,R0,.loopface
addiu
$S1,$S1,16 ; NextQuadtrans
;POP RET
lw
R31,8(R29)
lw
$S0,12(R29)
lw
$S1,16(R29)
lw
$S2,20(R29)
lw
$S3,24(R29)
jr
R31
addi
R29,R29,0x0020
Notre fonction DrawRecGen utilisera des
quads texturés pour dessiner les faces. Nous passerons en paramètre le pointeur
de QuadTrans, la couleur de la face. La texture page et les coordonnées de
texture sont en dure.
;Draw 4 point polygon
;$A0->POLYTRANS
;$A1->COLOR
DrawGenQuad:
;RECTANGLE TEXTURE
lw
R10,0($A0) ;V0
lw
R11,4($A0) ;V1
lw
R12,8($A0) ;V2
lw
R13,12($A0) ;V3
;QUAD MONO TEXTURED
lui R8,0x2C00 ; RECT 4 point polygone
or R8,R8,$A1 ;COLOR
sw
R8,0x1810(R27)
;VERTEX 0
sw
R10,0x1810(R27) ;SEND COORDS Y|X
ori
R9,R0,0xFF00 ;V=255 U=0
sw
R9,0x1810(R27) ;SEND CLUTID AND COORDTEXT
;VERTEX 1
sw
R11,0x1810(R27) ;SEND COORDS Y|X
lui R9,0x010C ;TEXTURE PAGE: 15 bit direct X=12*64=>768
Y=0
;ori R9,R9,0x0000 ; V=0 U=0
sw
R9,0x1810(R27) ;SEND CLUTID AND COORDTEXT
;VERTEX 2 (3)
sw
R12,0x1810(R27) ;SEND COORDS Y|X
ori
R9,R0,0xFFFF ;Don't care clut id V=255 U=255
sw
R9,0x1810(R27) ;SEND CLUTID AND COORDTEXT
;VERTEX 3
sw
R13,0x1810(R27) ;SEND COORDS Y|X
ori
R9,R0,0x00FF ;V=0
U=255
sw R9,0x1810(R27) ;SEND CLUTID AND COORDTEXT
.End:
jr
R31
nop
GteTut2.asm en action
Les programmes d’exemple contiennent
GteTut3 qui rajoute la rotation sur l’axe X et les positions Y et Z de l’objet
et avec également la possibilité de changer H.
Hormis l’utilisation de RTV0TR et RTPS le
programme est fort similaire. L’ajout du control par boutons est trivial.
GTE_CUBETRANS devient :
;GTE CUBE TRANSFORM
GTE_CUBETRANS:
;PUSH
addi
R29,R29,0xFFE0 ; R29=R29-32
sw
R31,8(R29) ; PUSH R31
sw
$S0,12(R29)
sw
$S1,16(R29)
sw
$S2,20(R29)
sw
$S3,24(R29)
sw
$S4,28(R29)
;INIT PTR
lui
$S0,>MeshCube
ori
$S0,$S0,MeshCube ;S0=>.Mesh
lui
$S1,>QuadTrans
ori
$S1,$S1,QuadTrans ;S1->PolyTrans
lui
$S2,>QuadClip
ori
$S2,$S2,QuadClip ;S1->PolyTrans
;INIT RTPS
;OFFSET SCREEN
lui
R2,160
ctc2
R2,R24 ;OFFX=160
lui
R3,120
ctc2 R3,R25 ;OFFY=120
;FOR EACH FACE
ori
$S3,R0,6 ;6 faces
.loopface:
;FOR EACH VERT
ori
$S4,R0,4 ;4 Vert
.loopVert:
; LOAD ROTATION MATRIX Y
lui
$A0,>CurRadCube
ori $A0,$A0,CurRadCube ;S1->PolyTrans
lw
$A0,0($A0)
jal
LoadMry256
nop
ctc2
R0,Gc5 ;TRX = 0
ctc2
R0,Gc6 ;TRY = 0
ctc2
R0,Gc7 ;TRZ = 0
lwc2
R0,0($S0) ;VX,VY dans gd0
lwc2
R1,4($S0) ;VZ
dans gd1
nop
nop
;*** USE RTV0TR cop2 $0486012
cop2
0x0480012 ;RTV0TR
nop
nop
; LOAD ROTATION MATRIX X
lui $A0,>CurRadCube
ori
$A0,$A0,CurRadCube ;S1->PolyTrans
lw $A0,4($A0)
jal
LoadMrx256
nop
mfc2 R9,R25 ;MAC1 X
mfc2
R8,R26 ;MAC2 Y
mfc2
R10,R27 ;MAC3 Z
lui
R11,0xFFFF
sll R8,R8,16
and
R8,R8,R11
andi
R9,R9,0xFFFF
or
R8,R8,R9
mtc2
R8,Gc0
mtc2
R10,Gc1
; OBJECT POSITION
lui
$A0,>PosView
ori
$A0,$A0,PosView ;S1->PolyTrans
lw
R8,0($A0) ;CurX
lw
R9,4($A0) ;CurY
lw
R10,8($A0) ;CurZ
lw
R11,12($A0) ;CurH
ctc2
R8,Gc5 ;TRX
ctc2
R9,Gc6 ;TRY
ctc2
R10,Gc7 ;TRZ
ctc2
R11,R26 ;H
nop
nop
;COP RTPS
;Xp=OFFX+(H/SZ)*X
; Yp=OFFY + (H/SZ)
cop2
0x0180001
nop
nop
;STORE THE RESULT ON CUBETRANS
swc2
r15,0($S1) ; store on CubeTrans
addiu
$S4,$S4,-1 ; vert--
addiu
$S1,$S1,4 ; Next vert_trans ptr
bne
$S4,R0,.loopVert
addiu
$S0,$S0,8 ; Next vert_mesh ptr
;NCLIP TO CHECK IF THE FACE IS VISIBLE
cop2
0x1400006 ; DOT PRODUCT TO MAC0
addiu
$S3,$S3,-1 ; face--
nop
mfc2
R3,R24 ; MAC0 dans R3
nop ; VERY IMPORTANT
nop ; BEFORE STORE THE RESULT
sw
R3,0($S2) ; STORE CLIP
bne
$S3,R0,.loopface
addiu
$S2,$S2,4 ; Next ptr ClipFace
;POP RET
lw
R31,8(R29)
lw
$S0,12(R29)
lw
$S1,16(R29)
lw
$S2,20(R29)
jr
R31
addi
R29,R29,0x0020 ; SP+=32
;OBJECT POSITION
PosView:
word
0,8,160,256 ;x,y,z,h
;OBJECT ROTATION Y,X
CurRadCube:
word 0,0
19-
Conclusion
Voilà. Il reste encore beaucoup à
apprendre, mais on en sait suffisamment pour faire des démos, des jeux en
exploitant les merveilleuses possibilités de la Playstation et bien sûr
continuer à percer les nombreux secrets que cache encore cette console. Les
possibilités de cette machine étant assez extraordinaires il y a vraiment de
quoi faire.