Psykotrope
1990-1996
CTO
Pousser les performances des ordinateurs Amiga des années 90 à fond, défier les limites d’un microprocesseur 680x0
Réalisation de démos avec des effets graphiques codés en Assembler 680x0 et de la musique tonitruante réalisée sur des trackers. Elles ont été présentées à différentes Party en France, au Danemark, …
À l’époque, seuls les militaires et les universitaires avaient internet. Le HTML n’existait pas encore ! Pour trouver de l’info, en France on était super bien logés, on allait sur le minitel sur 3615 RTEL. C’était le point de rendez-vous de tous les geeks. Les codeurs des pays nordique payaient une fortune pour avoir le minitel chez eux et pour se connecter en France sur les BBS dédiés à l’informatique. ET mes parents hurlaient pour que je lâche le minitel qui représentait un coût de communication exorbitant à leurs yeux 😀
Quelques heures avant la deadline de la compétition à laquelle nous avions présenté Eternity et Shortcut to Heaven, deux intros de 64ko (voir les vidéos ci-dessous).
Voici quelques réalisations récupérées chez des Allemands nostalgiques
C’était une intro de moins de 40Ko avec les dessins, font, musique, … Elle utilisait le nouveau mode AGA des Amiga qui permettait d’afficher jusqu’à 16777216 couleurs, wouah !
Elle a été présentée à la party Sun is a Pac Man et a fini en 2ème place.
C’était une intro de moins de 4Ko avec les dessins, font, musique, … Oui, Quatre Kilo Octets ! Gros défis de compression, économie de chaque octet. Pour faire tourner le cube par exemple, on a besoin de faire des calculs de rotation et donc on a besoin des fonctions de sinus et de cosinus. Les CPU 680x0 n’avaient pas de calcul à virgule flottante. Au lancement de la démo, on générait les 90 premiers degrés de la fonction sinus avec un calcul de série entière et on recopiant ensuite dans la RAM par symétrie les 90 degrés suivant et encore par symétrie, de 180 à 360 degrés. Pourquoi ? Parce que la taille du code de ma série entière prenait 88 octets, soit 2 de moins les 90 octets des 90 premiers degrés de la fonction sinus.
Ce cube ne fait pas grand-chose mais à l’époque, c’était une révolution car il a 3 effets :
Concernant la musique, c’est dommage, l’allemand qui a ripé la vidéo de cette intro ne l’a pas mise. Mais le concept était le même, le sample qui composait la musique était calculé au lancement ! C’était la première 4ko intro avec de la musique à l’époque.
Encore une intro de moins de 40Ko présentée au Danemark à la Party 4. Elle a été disqualifiée parce qu’elle faisait planter le video-projecteur de l’organisation. Pour accélérer le nombre d’images par seconde de mon labyrinthe mappé, je passais l’affichage du mode PAL au mode NTSC, car j’avais découvert que le CPU s’overclockait en faisant ça et ça me permettait d’avoir le premier labyrinthe mappé à la VBL, c’est à dire au top du top à l’époque : 25 images par seconde !
VBL, ça veut dire Vertical Blank Line, c’est le temps que met l’électron dans le tube cathodique pour parcourir tout l’écran, d’en haut à gauche jusqu’à en bas à droite. Et oui pas de LCD à l’époque ! Si le temps de calcul de la prochaine image à afficher est inférieur à la VBL, il suffit d’attendre le temps restant pour switcher l’image (double buffer) et on obtient 25 images par seconde.
Si le temps de calcul de la prochaine image est supérieur à la VBL par contre et qu’on switch immédiatement le double buffer, on obtient alors une animation légèrement plus lente mais avec de gros artefacts qui représentent le mélange de l’image précédente avec la nouvelle. Beurk 🤮 A l’époque, on appelait les codeurs qui osaient faire ça des Lamers !
Sur cette démo, je n’ai rien codé, c’est Psy qui s’en est chargé.
24h avant la deadline de présentation des démos, il me dit qu’il n’a pas de musique et me demande d’en composer une. Sympa le challenge. Je lui ai donc composé la musique pendant la Saturne Party 2 à Paris après 48h de débogage de ma démo Eternity sans dormir. On était dans une salle de 2000 geeks venus de toute l’Europe, dans un brouhaha pas possible. Ce n’est qu’après la Party, au calme, que je me suis rendu compte qu’il y a un passage dans la musique qui fait un peu “fausse note” 😬
Petite explication technique des démos de l’époque : vous voyez la photo du lapin tout seul ? Pourquoi le codeur s’amuse-t-il à nous montrer une image ? Juste pour faire beau ? Non évidemment :-) Pendant que vous regardez l’image, il est en train de précalculer toutes les rotations de l’image en RAM pour pouvoir s’en servir ensuite pendant l’effet zoomrotat où il n’a plus qu’à calculer le zoom. Malin n’est-ce pas ?
Cette démo s’appelle Eternity parce que sont des cubes en rotation qui défilent pendant… une éternité !
L’allemand qui a rippé la vidéo a eu un petit problème, dans la vraie vie, il n’y avait pas de noir et blanc… Techniquement, j’avais travaillé sur l’effet d’apparition en cercle qu’on voit au début à l’intérieur des petits carrés et tout à la fin pendant les crédits.
Pendant cet effet au début sur le texte, je pré-calcule tous les cubes comme un malade en RAM pour pouvoir les afficher tranquillement ensuite dans la démo.
Toute la scène avait l’habitude dans ses démos de raconter des choses sans intérêt et de remercier d’autres personnes d’autres groupes. Si quelqu’un en Europe ne le faisait pas dans une démo, il était sifflé pendant sa présentation par plusieurs milliers de personnes.
Nouveau détail technique aussi : pour synchroniser la musique avec les effets, on mettait des codes dans la musique. Quand on lit la musique dans la routine principale, dès qu’on détecte le code caché, on déclenche la fin d’un effet, le début d’un nouveau, …
C’est quoi un Pack ? C’était une disquette avec plein de démos dessus que les groupes s’échangeaient. Zone (le graphiste du groupe) s’occupait de spreader nos démos partout en Europe pour qu’on soit connus. Dans chaque groupe, un membre était spreader, c’était le marketing de l’époque avec les moyens qu’on avait.
Zone a pas mal gagné en notoriété et par la force des choses, on a créé nos packs qui ont cartonnés : les Spayce Pack. La vidéo que vous voyez au dessus est donc un lanceur de démo. Ça peut paraître simple comme ça, mais toute la difficulté consiste à lancer une démo et quand celle-ci est finie, récupérer l’ordinateur pour retourner dans le Spayce Pack. Une démo utilise tout dans l’ordinateur :
A y penser aujourd’hui, à l’heure de VMWare, de Docker, des machines virtuelles Erlang, … j’avais pragmatiquement utilisé les mêmes concepts sans que le CPU 680x0 ne m’aide en quoique ce soit.
Dans toutes les démos (même la 4ko Intro), le spectateur peut déclencher une partie secrète en faisant un Konami Code like. Avis aux possesseurs d’Amiga :-)
Voici le code Assembler 680x0 que j’ai écrit à l’époque pour mapper une texture sur un mur composé de 2 triangles dans le labyrinthe de Today is sunday. La base d’un moteur 3D, c’est le triangle !
Routine3D.24anim.s
incdir
include --sources:bases/base.s
Planewidth = 320/8
Planelong = 256
Planesize = Planelong*Planewidth
Plan = 4
;***********************************************************************
A_Start: lea DemoGo(pc),a0
move.l a0,$80
trap #0
rts
;---------------OUVRE LA GFXLIBRARY
;------------------------------------------------------------------------
DemoGo: move.l execbase,a6
lea gfxname,a1
jsr OpenLibrary(a6)
move.l d0,gfxbase
;---------------ALLOUE LA MEMOIRE
;------------------------------------------------------------------------
move.l Execbase,a6
move.l #planesize*plan,d0
move.l #Clear,d1
jsr AllocMem(a6)
move.l d0,Planeadr
beq.w fin
;---------------INITIALISE LA COPPERLIST
;-----------------------------------------------------------------------
lea Copperlist+2(pc),a0
move.l Planeadr,d0
moveq #plan-1,d1
copperloop:
swap d0
move.w d0,(a0)
swap d0
move.w d0,4(a0)
addq #8,a0
add.l #planesize,d0
dbf d1,copperloop
;---------------PAS DE MULTITACHE
;-----------------------------------------------------------------------
lea $dff000,a5
move.l Execbase,a6
jsr Forbid(a6)
;---------------INSTALLE LA COPPERLIST
;-----------------------------------------------------------------------
move #$03e0,DMAcon(a5)
move.l #Copperlist,Cop1lch(a5)
clr Copjmp1(a5)
;---------------DONNEES DE L'ECRAN
;-----------------------------------------------------------------------
lea $dff000,a5
move.w intenar(a5),old_intena
move.w #$2871,Diwstrt(a5)
move.w #$28d1,Diwstop(a5)
move.w #$0038,Ddfstrt(a5)
move.w #$00a0,Ddfstop(a5)
move.w #%0100000000000000,bplcon0(a5)
move.w #$0,Bplcon1(a5)
move.w #$0,Bplcon2(a5)
move.w #$0,Bpl1mod(a5)
move.w #$0,Bpl2mod(a5)
;---------------INSTALLE LA VBL ET LES INTERRUPTIONS
;-----------------------------------------------------------------------
DMASET= %1000010111001111
move.w #dmaset!$8200,dmacon(a5)
lea $dff000,a5
move.l $6c,Old_vbl
lea new_vbl,a0
move.w #$7fff,intena(a5)
move.l a0,$6c
move.w #$c020,intena(a5)
move.w #3,$dff1fc
move.w #0,$dff106
move.l #$0123088f,$dff180
moveq #0,d0
moveq #0,d1
moveq #0,d2
moveq #0,d3
moveq #0,d4
moveq #0,d5
moveq #0,d6
moveq #0,d7
move.w #256-1,d0
moveq #0,d1
lea YTable,a0
Loopm1:
move.w d1,(a0)+
addi #40,d1
dbf d0,loopm1
;***********************************************************************
move.l planeadr(pc),a0
lea pts,a2
moveq #2-1,d0
EveryObject:
move.l a2,a1
bsr.w FillTri
lea 12(a2),a2
dbf d0,EveryObject
mousy: btst #6,$bfe001
bne.b mousy
;***********************************************************************
fin: btst #14,$dff002
bne.b fin
lea $dff000,a5
move.l execbase,a6
move.l Old_vbl,$6c
move.w old_intena,d0
or.w #$8000,d0
move.w d0,intena(a5)
move.l gfxbase,a4
move.l Startlist(a4),Cop1lch(a5)
clr Copjmp1(a5)
move.w #$8020,DMAcon(a5)
jsr Permit(a6)
move.l gfxbase,a1
jsr CloseLibrary(a6)
move.l execbase,a6
move.l planeadr,a1
move.l #planesize*plan,d0
jsr FreeMem(a6)
moveq #0,d0
rte
;************************************************************************
cop: dc.l 0
MyCopper: dc.l 0
Planeadr: dc.l 0
Gfxbase: dc.l 0
old_intena: dc.l 0
old_vbl: dc.l 0
Irq_flag: dc.l 0
cladr: dc.l 0
Gfxname: dc.b "graphics.library",0
even
;---------------COPPER LIST DATA
;------------------------------------------------------------------------
Copperlist: dc.w $e0,$0
dc.w $e2,$0
dc.w $e4,$0
dc.w $e6,$0
dc.w $e8,$0
dc.w $ea,$0
dc.w $ec,$0
dc.w $ee,$0
dc.w $ffff,$fffe
FinCopperlist:
;---------------WAIT VERTICAL BLANKER
;------------------------------------------------------------------------
New_vbl: move.b #$ff,Irq_flag
move.w #$20,$dff09c
rte
WaitVbl: clr.b Irq_flag
loop: tst.b Irq_flag
beq.s loop
rts
;------------------------------------------------------------------------
; ROUTINE DE REMPLISSAGE DE TRIANGLE ! CODED BY POTSKY
; Entree: a0:destination a1:ptr sur les points
;------------------------------------------------------------------------
FillTri: movem.l d0-d7/a2-a6,-(sp)
;----------------------------------------
; TRI DES 3 POINTS SELON Y
;----------------------------------------
lea ptorg,a2
movem.w (a1)+,d0-d5
Tri1: cmp d3,d1 ; Tri Par Y des pt du triangle!
ble.b Tri2 ;
exg d3,d1 ; Y
exg d2,d0 ; X
tri2: cmp d5,d1
ble.b Tri3
exg d5,d1 ; Y
exg d4,d0 ; X
tri3: cmp d5,d3
ble.b tri4
exg d5,d3 ; Y
exg d4,d2 ; X
tri4: move.w d2,(a2) ; on save nur X2 pour la suite
;----------------------------------------
; CALCUL DES COEFFS DIRECTEUR
;----------------------------------------
move.w d0,a5 ; save x,y
move.w d1,a6
sub d2,d0 ; DX1 petite 1ere
sub d3,d1 ; DY1
sub d4,d2 ; DX2 petite 2eme
sub d5,d3 ; DY2
sub a5,d4 ; DX3 grande
sub a6,d5 ; DY3
ext.l d0
ext.l d1
beq.b d1nul
swap d0 ; 1er petit coef
divs.l d1,d0
move.l d0,a2
bra.b Cont.1
D1nul:
moveq #0,d0
move.l d0,a2
Cont.1:
ext.l d2
ext.l d3
beq.b d3nul
swap d2 ; 2eme petit coef
divs.l d3,d2
move.l d2,a3
bra.b Cont.2
D3nul:
moveq #0,d2
move.l d2,a3
Cont.2:
ext.l d4
ext.l d5
beq.b D5Nul
swap d4 ; 3eme big coef
divs.l d5,d4
move.l d4,a4
bra.b cont.3
D5nul:
moveq #0,d4
move.l d4,a4
Cont.3:
;----------------------------------------
; FILL DE LA 1ERE PARTIE DU TRIANGLE
;----------------------------------------
lea Ytable,a1 ; Avant le test du scan 2 !!!
move.w (a1,a6.w*2),a6 ; offset y de d�part
tst.l d1
beq.b scan2init
bpl.b Scan_Y1
neg.l d1
Scan_Y1: movem.l d2/d3,-(sp)
subq #1,d1 ; dbf rulez !
moveq #0,d0
moveq #0,d2 ; virflo.1
moveq #0,d3
moveq #0,d6 ; virflo.2
moveq #0,d7
Scan_Y2: swap d2 ; 1ere line
move.w d2,d3
add.w a5,d3
swap d2
swap d6 ; 3eme line
move.w d6,d7
add.w a5,d7
swap d6
move.w d7,d0 ; save d7 !!!
sub d3,d0 ; difference !
beq.b NoFill
bpl.b Scan_OK1 ; Ok !!!!!!
neg d0 ; valeur abs !
exg d3,d7 ; scan de d3 vers d7
Scan_OK1: divu #31,d0 ; 108
beq.b onlyFewPix
subq #1,d0
ScanLoop: bfset (a0,a6){d3:31}
addi #31,d3
dbf d0,ScanLoop
OnlyFewPix: clr.w d0
swap d0
beq.b NoFill
bfset (a0,a6){d3:d0}
Nofill: addi #40,a6
add.l a2,d2
add.l a4,d6
dbf d1,scan_Y2
movem.l (sp)+,d2/d3
bra.b fin1erscan
scan2init: moveq #0,d6
;----------------------------------------
; FILL DE LA 2EME PARTIE DU TRIANGLE
;----------------------------------------
Fin1erscan: lea ptorg,a1
move.w (a1),a1 ; perpendiculaire du new pt
tst.l d3
beq.b Fin2emescan
bpl.b Scan_Y1b
neg.l d3
Scan_Y1b: subq #1,d3 ; dbf rulez !
moveq #0,d0 ; virflo.1
moveq #0,d1
moveq #0,d2 ; sinon c crade !
Scan_Y2b: swap d0 ; 2ere line
move.w d0,d1
add.w a1,d1
swap d0
swap d6 ; 3eme line
move.w d6,d7
add.w a5,d7
swap d6
move.w d7,d2 ; save d7 !!!
sub d1,d2 ; difference !
beq.b NoFillb
bpl.b Scan_OK1b ; Ok !!!!!!
neg d2 ; valeur abs !
exg d1,d7 ; scan de d1 vers d7
Scan_OK1b:
divu #31,d2 ; 108
beq.b onlyFewPixb
subq #1,d2
ScanLoopb:
bfset (a0,a6){d1:31}
addi #31,d1
dbf d2,ScanLoopb
OnlyFewPixb:
clr.w d2
swap d2
beq.b NoFillb
bfset (a0,a6){d1:d2}
Nofillb: addi #40,a6
add.l a3,d0
add.l a4,d6
dbf d3,scan_Y2b
Fin2emescan:
movem.l (sp)+,d0-d7/a2-a6
rts
;************************************************************************
Pts:
dc.w 10,10
dc.w 10,100
dc.w 20,100
dc.w 10,10
dc.w 20,10
dc.w 20,100
PtOrg:
dc.w 0
YTAble:
dcb.w 256