Memory Full

Initiation au zoom

Written by Targhan in April 2010

Initially written for Demoniak #8 and published on Push'n'Pop. English version here

Alors, bande de petits veinards ! On va faire une petite initiation aux zooms. Ainsi, vous aurez de quoi ridiculiser l'Ecole Buissonnière de je sais plus quel codeur suédois (Hoeger Fruzenham, si je ne m'abuse). Vous pourrez également trouver un source (compatible Winape) ici.

Quand je parle de zoom, je parle bien d'agrandissement et de réduction d'un graphisme. Ceci est possible grâce à une technique appelée «virgule fixe». Attention, dans cet article, je ne parle que d'un zoom en X dans le but de simplifier les choses, car en Y c'est exactement la même chose, sauf que ce sera à vous de le faire... Je précise également qu'il ne s'agit que d'une initiation. Aucune optimisation n'est effectuée et le code n'a aucune élégance particulière. La méthode décrite ici est en fait trop lente pour du temps-réel, mais trouvera surtout son utilité pour générer du code que vous pourrez appeler à loisir selon le degré du zoom. La technique software la plus rapide est d'avoir autant de code qu'il y a de possibilité de zoom et si la mémoire vous le permet, vous pouvez même précalculer directement tous les codes pour tous les zooms de chaque ligne de graphisme !

Mais la technique approchée ici n'est pas utile que pour les Zooms. En effet, vous pourrez l'utiliser pour faire du digitracking (jouer des samples à différentes fréquences), simuler des accélérations/décélération d'objets, et tout ce que votre esprit pourra ajouter à cette liste.

Mais entrons dans le vif du sujet, voulez-vous ?

Un zoom, qu'est-ce que c'est donc ?
Question pertinente, s'il en est. Imaginez un graphisme dans sa taille originale. Imaginez que vous le zoomiez un peu (en X). Que se passe-t-il ? Et bien des colonnes sont doublées en certains endroits. Puis quand toutes les colonnes sont doublées, on obtient un graphisme 2 fois plus grand que l'original, mais aussi 2 fois plus pixellisé. Si vous zoomez encore, ce ne sont plus deux colonnes identiques qui se suivent, mais 3, puis 4 et ainsi de suite, jusqu'à ce que l'on ne trouve plus qu'un amas immonde de pixels. Mince, il faut pas que je vous dégoûte de la relative laideur des zooms, si vous comptez vous y mettre...

Donc, lors de l'affichage de mon graph, chaque pixel est victime d'une question : suis-je le même que celui que l'on vient d'afficher, ou suis-je un nouveau ? C'est cette question qui est le principal problème. Quand on sait y répondre, il n'y a plus de problème.

En fait, la technique que je vous propose ne suit pas exactement ce raisonnement, mais plutôt celui-là : Je prends tous les X pixels de mon graph original et les balance successivement à l'écran. Si X=1, alors le graph résultant sera le même que l'original. Si X=2, on obtient un graph deux fois plus petit, car un pixel sur deux est sauté. Mais là où ça devient intéressant, c'est que X peut-être un nombre à virgule. Si X=0.5, alors j'observe un graph deux fois plus gros. Si X=1.45, si X=0.79, si X=0.1... on peut définir un coefficient de zoom plutôt fin, et si on fait évoluer X de 0.1 à 4 par exemple, on observe un dézoom fluide et magnifique (enfin, moi je trouve).

Le seul problème donc est de gérer un nombre à virgule, car nos registres sont un peu... entiers.

Mme Fixe fait son apparition (la salle applaudit)
Le principe de la virgule fixe est plutôt simple : on sépare dans deux registres la partie réelle et la partie décimale. On considère que ces deux registres sont 8 bits, ce qui est largement suffisant pour ce qu'on a à en faire. Mais qui sait, vos effets nécessiteront peut-être une partie entière de 16 bits, ce qui sera un peu plus demandeur en temps-machine.

Deux registres 8 bits donc. La partie décimale comporte des nombres allant de 0 à 255. Ça peut paraûtre étrange d'ailleurs, mais c'est comme ça : 1.42 existe, tout comme 0.193, ainsi que 456.255, mais 5.256 n'existe pas !

Voilà, c'est tout ! Mais maintenant, il faut savoir comment utiliser ça, parce que pour l'instant, vous n'avez pas appris grand chose. Il faut implanter ce fameux facteur X qui désigne le nombre de pixels qui sépare, dans la mémoire, le point que l'on vient d'afficher de celui qui suit.

Quelques précisions concernant la routine : nous travaillerons avec une précision à l'octet uniquement. Oui, ce sera moins beau qu'au pixel, mais je pense que si vous arrivez à comprendre ce que je vais raconter, vous n'aurez aucun mal à pondre une routine plus poussée, plus rapide, plus mieux, quoi.

On considère deux variables 8 bits : tout d'abord, ZOOM correspond à la partie entière de notre coefficient de zoom. ZDECI correspond à la partie décimale. Pour un affichage normal, ZOOM=1 et ZDECI=0, ainsi chaque pixel du gfx original est affiché. Petite question : si je veux un gfx deux fois plus gros ? Quelles seront les valeurs ? Et bien ce sera : ZOOM=0, ZDECI=128. N'oublions pas que ZDECI peut prendre des valeurs de 0 à 255.

Il existe un autre octet, que l'on aurait pu appeler Boulok ou Pastek, mais que l'on appellera NEXT pour des raisons de compréhension (elle n'apparaût pas dans le source), et qui possède le nombre d'octet, dans la mémoire, qui sépare le premier octet de la ligne traitée du gfx de celui qui va être affiché dans le temps suivant. Cette variable est le résultat de ZOOM et de DECI, mais aussi de toutes les itérations précédentes sur cette ligne ! Mais si NEXT est une variable à virgule, on ne prendra que sa partie entière pour désigner quel octet afficher.

Notons que dans ma routine, j'ai choisi d'afficher toujours le premier point, quoiqu'il arrive. Le reste de l'affichage se fait à partir de lui.

1ère itération : On vient d'afficher le premier point. Il faut donc passer au suivant. On veut afficher un graphisme deux fois plus large, donc : ZOOM=0 et ZDECI=128. NEXT=0.128 ! Mais bon, NEXT à beau être à virgule, le 0.5ème point n'existe pas pour un ordinateur. On ne s'occupera que de la partie entière et donc, du 0ième octet.

2ème itération : On affiche le 0ième, car NEXT= 0.128 (calcul précédent). On doit calculer une nouvelle valeur de NEXT :

	NEXT=NEXT+ZOOM
	NEXT=NEXT+0.128
	    =0.128+0.128
	    =1


3ème itération : On affiche le 1er point (calcul précédent).

	NEXT=NEXT+ZOOM
	NEXT=NEXT+0.128
	    =1+0.128
	    =1.128


4ème itération : On affiche le 1er point.

	NEXT=NEXT+ZOOM
	NEXT=NEXT+0.128
	    =1.128+0.128
	    =2


5ème itération : On affiche le 2ème point.

	NEXT=NEXT+ZOOM
	NEXT=NEXT+0.128
	    =2+0.128
	    =2.128


6ème itération : On affiche le 2ème point...

Et ainsi de suite jusqu'à ce que décrépitude s'ensuive. Nous aborderons les conditions de fin par la suite. Pour l'instant, tout le monde a compris, non ? Eh bien tant mieux, car il va falloir adapter le tout au monde cruel de l'assembleur. Une partie de plaisir.

C'est très simple : on va utiliser deux registres 8 bits, je choisis H et L, qui représenteront NEXT. H sera la partie entière, L la décimale. Ces deux registres seront initialisés à 0 à chaque début de traitement de ligne et vont évoluer à chaque itération. Et de quelle manière ? Et bien on ajoute à L le ZDECI. Si L ne déborde pas, alors on doit incrémenter H (0.128+0.128=1, n'est-ce pas ?). Puis on doit encore ajouter à H le coefficient ZOOM. Et voilà, on possède maintenant le coefficient NEXT. Il ne reste plus qu'à ajouter son contenu à l'adresse du début de la ligne du graph traitée.

Nous allons utiliser un maximum de registres, pour passer le moins possible par la mémoire. C'est pour cela que j'utiliserai les registres auxiliaires.

Petit rappel : Un EXX inverse UNIQUEMENT les registres suivants : HL/HL', DE/DE', BC/BC'. C'est tout ! Il est volontaire que AF/AF' ne swappent pas, de manière à ce qu'il puisse y avoir un lien entre les deux jeux de registre (pour faire des transferts de valeur, par exemple (ça évite de passer par la mémoire)). Pour utiliser AF', il faut taper l'instruction EX AF,AF'. Rappelons que ces deux instructions prennent un cycle et qu'il ne s'agit pas d'écraser l'ancien jeu par le nouveau, mais bel et bien de les inverser. Deux EXX à suivre n'influent en rien sur vos registres, il en est de même pour deux EX AF,AF'. Notons que IX, IY, SP sont uniques et ne peuvent pas être échangés.

Deuxième rappel : le système utilise BC' et AF', il est important de les sauvegarder si vous pensez les modifier. Notre programme coupera les interruptions système afin de ne pas se coltiner ce dernier... Fin de la parenthèse.

Le jeu principal de registres nous servira à pointer sur la mémoire écran ainsi que sur le début de la ligne du gfx original que nous désirons traiter. Le second jeu de registre contiendra ZOOM et ZDECI et servira à calculer NEXT. Notons que si NEXT est codé sur deux registres (un pour sa partie entière, l'autre pour la décimale), seule la partie entière est additionnée au début de la ligne du gfx original afin que trouver quel octet afficher. On fera passer la partie entière par A qui n'est pas modifié lors du EXX.

Donc, à chaque itération, voici ce que l'on doit faire :

1) Afficher l'octet du gfx pointé en ce moment
2) NEXT=NEXT+ZOOM,ZDECI
3) Additionner notre pointeur de gfx avec NEXT
4) Recommencer tout ça jusqu'à putréfaction

Etudions ça en profondeur, maintenant.

1) Afficher l'octet du gfx pointé en ce moment. Si c'est la première itération, alors c'est le début du gfx que l'on pointe. Pour l'instant, on va supposer que :

	HL=Pointeur de gfx
	DE=Pointe sur la mémoire écran.


On sait que l'on opère à l'octet près, donc tout de suite après la copie de l'octet pointé par HL vers DE, on peut incrémenter DE. Mais pas HL, car on ne sait pas combien d'octets on va sauter ! LDI est donc inopérant ici (à moins de décrémenter HL systématiquement par la suite, mais on y perd en temps machine) Voilà ce qu'on va faire :

	LD A,(HL)	;On lit l'octet du gfx
	LD (DE),A	;On l'affiche
	INC DE		;On passe au point suivant à l'écran


2) NEXT=NEXT+ZOOM,DECI
(attention ! ZOOM,DECI signifie par exemple 1,128) C'est d'une simplicité audacieuse, en fait, tout se fait en une opération. On passe tout d'abord du côté des registres auxiliaires. Si dans HL vous placez NEXT (H=partie entière, L=partie décimale) et que dans DE vous placez ZOOM,ZDECI (ZOOM dans D, ZDECI dans E), et bien il ne reste plus qu'à les additionner bêtement comme des nombres 16 bits. Si une des parties décimales déborde, la partie entière sera incrémentée automatiquement. Tout ce qu'il y a à faire, c'est :

	EXX		;Registres auxiliaires engagés
        ADD HL,DE	;NEXT=NEXT+ZOOM


3) Additionner notre pointeur de gfx avec NEXT
On connaût le numéro de l'octet que l'on souhaite afficher (NEXT), mais cette valeur est relative au début de la ligne traitée du gfx, il faut donc additionner NEXT à l'adresse du début de ligne. ATTENTION : seule la partie entière de NEXT nous intéresse ici. On va donc passer H dans A, puis retourner aux registres normaux. A est conservé, ne l'oublions pas.

	LD A,H
	EXX


Mais ce n'est pas fini. Si l'on regarde les registres utilisés ici, on se rend compte que ça ne peut pas marcher. HL pointe bien l'adresse du premier octet de la ligne, mais cette valeur va menacer d'évoluer si on lui additionne notre NEXT, ce qui va être gênant pour les prochaines itérations ! Ce qu'il faut, c'est garder quekpart la valeur d'origine. On pourrait passer par la mémoire, et faire un truc du genre :

	LD HL,(ADGFX)
	ADD HL,A	(qui n'existe pas)


Mais comme il nous reste un registre 16 bits (BC) et comme on est malin, on va mettre l'adresse du début de la ligne dans BC et puis l'on va additionner BC à HL... dans lequel on aura auparavant transféré NEXT ! Comme une addition 16 bits range son résultat dans HL, BC demeure intact et peut être réutilisé infiniment. Miam cool ! Ça donnera donc :

	LD L,A		;Transfert de NEXT dans HL
	LD H,0		;obligatoire, la partie de NEXT utilisée étant sur 8 bit !
	ADD HL,BC	;Début ligne + NEXT = Adresse du point à afficher !


(Un esprit un peu malin verra qu'on peut optimiser en faisant en sorte que NEXT ne soit non pas relatif au début de la ligne, mais relatif à l'octet précédent. Ne pas oublier de mettre la partie entière de NEXT à 0 à chaque itération !)

Et voilà ! Il ne nous reste plus qu'à boucler, puisque l'affichage se fera l'itération suivante.

4) Le bouclage
Une petite question : l'affichage d'une ligne se termine-t-il lorsqu'on a affiché un certain nombre défini d'octet à l'écran, ou alors lorsque on a affiché un nombre défini d'octet du graphisme original ? Ces deux approches sont un peu différentes et chacune a ses particularités. C'est la première que j'ai choisi, c'est la plus évidente et la plus répandue. De cette manière, le graph affiché est toujours de la même taille, quel que soit le niveau de zoom, le temps machine est stable. Au niveau programmation, une simple boucle suffit. J'utilise IXL, le registre de poids faible de IX (attention, Dams et Maxam ne connaissent pas ces opcodes, à l'inverse de l'éditeur de Winape. Il faudra les entrer à la main).

	DEC  IXL
	JR   NZ,XLOOP


Pour optimiser la boucle, vous pouvez recopier plusieurs fois à suivre la routine de zoom et diviser le compteur en conséquence. Vu la petite taille de ce code, c'est parfaitement utilisable et la gain en temps machine est considérable. Une autre technique est de faire une table de RET. Ça ne prend que 3 cycles (le temps d'un RET), mais un peu plus de mémoire.

La deuxième approche consiste à afficher toujours la même partie du graph original, donc quel que soit le niveau de zoom, c'est toujours la même partie du gfx que l'on verra. Le temps machine est utilisé pour afficher le strict minimum et est donc économisé. Cependant, il n'est pas stable et la taille de la fenêtre affichée est variable. Le moyen le plus efficace pour s'arrêter aux fins de lignes est de se référer non pas à un compteur de boucle, mais de comparer NEXT à un certain nombre qui correspondrait à la largeur du gfx. Voici pourquoi ça marche : imaginez un zoom de 1. Si votre graph fait 20 octets de large, NEXT atteindra 19.0 (NEXT débute à 0) à la fin de la ligne, pour 20 octets affichés. Maintenant, pour le même gfx, si votre zoom est de 0.128, NEXT atteindra toujours 19.0 à la fin de la ligne, pour 40 octets affichés. D'ailleurs il est à noter que seule la partie entière de NEXT est à comparer, puisque le graph original est obligatoirement composé d'un nombre entier de pixels. Cependant, si vous réduisez progressivement votre zoom, la partie droite du gfx se n'efface pas ! Votre graph doit donc comporter une petite bande noire à droite, de taille proportionnelle à la vitesse maximum du dézoom, mais vous pouvez aussi vous amuser à effacer cette zone vous-même.

Tout ce qu'il convient de faire est de remplacer le code de boucle que j'ai filé tout à l'heure par :

	CP 19		(NEXT commence à 0)
	JR C,XLOOP


Et pi voilà ! Si le «JR C» vous intrigue (alors que vous auriez mis JR NZ), c'est un peu normal. Le JR C signifie «plus petit que». Qwak ?

Petit rappel : Lorsque vous faites par exemple «CP B», le Z80 fait A-B. Si A est plus petit que B, alors la soustraction déborde et la Carry est positionnée.

Mais dans notre cas, pourquoi utiliser C et non pas NZ ? Tout simplement parce que rien ne nous dit que notre valeur «19» sera atteinte par NEXT. Peut-être que le coefficient de zoom va être tel que NEXT va passer de 18 à 20 (ou inversement) sans passer par 19 ! D'ailleurs, essayez de remplacer C par NZ, vous verrez bien.

Est-ce fini ?
Presque, mais pas encore. Il nous reste à parler graphisme. Vous le savez, nous travaillons à l'octet, ce qui signifie que si vous voulez utiliser un graph en MODE 1, vous devrez l'agrandir avec un facteur 4 pour profiter de tous ses points. Comme je ne suis pas un codeur trop bourrin (pas trop), le graph utilisé par le source est un petit texte écrit avec le vecteur #BB5A, que je transforme (routine «TRANSF») en graph 4 fois plus gros. La routine qui fait ça est très simple, mais il ne faut pas oublier que si le graph qui en ressort est une plâtrée d'octets, on traite le graph original au pixel, que l'on transforme en octet. Comme c'est du mode 1, j'isole chaque pixel (4 par octet) avec un AND, et je décale chacun d'eux pour qu'ils atteignent la position de droite. Puis 4 petites comparaisons (au maximum) définissent quel octet coder en conséquence.

Comme la technique de boucle pendant le zoom est celle du «comptage du nombre d'octets affichés», je transforme une zone plus grande que celle du graph réel, de manière à ce que j'affiche du vide à droite de mon zoom même lorsqu'il est assez dézoomé. Par contre, si vous dézoomez beaucoup, vous verrez le graph cycler. C'est pas bô.

Voilà, c'est la fin !
J'espère que ça vous a plu. Le source fourni est assez brut, mais fonctionne correctement. Il y a beaucoup beaucoup BEAUCOUP d'améliorations à y apporter, comme la gestion au pixel, un zoom en Y (facile), mais aussi toutes les optimisations que vous pourrez imaginer.

Voilà, j'espère vous avoir appris deux ou trois trucs grâce à ce petit article. Si vous avez des questions, n'hésitez pas à me contacter !

Targhan



How lucky you are! You're going to learn about zooms, so that you can make a fool out of Ecole Buissonnière by this unknown Swedish coder (What is Hoeger Fruzenham?). You will also find a little compatible Winape source here.

About zoom, I mean the shrinking and stretching of an image. It is possible thanks to a technique called "fixed-point arithmetic". Please note that I will only talk about X zoom to get things simple. Y zoom is exactly the same, but you will have to do it by yourself. I also emphasize on the fact that this article is just an initiation. No optimisation is performed, and the code isn't particularly elegant. The following algorithm is actually not fast enough for most real-time display. However, it will be perfect to generate code that you will call later. The fastest software technique is to have as many coded there are zoom steps and, if the memory allows it, you could even precalculate all the code for every steps of every lines of the image!

What's more, the technique discussed here is not only useful for zooms, but also to play samples at any rate, simulate acceleration and deceleration of objects, and all that your mind can imagine.

Let's get into the heart of the matter, shall we?

What the hell is a zoom?
Imagine an image in its original size. Imagine that you zoom a bit in X. What's happening? Some columns are doubled at different places, some aren't. Zooming further, when all the columns are doubled, we have an image that is twice larger than the original, but also twice pixelized. Zooming further, you have columns that are 3 pixels wide, then 4, 5, and so on till your image is just a heap of dirty pixels. Woops, I shouldn't stress on how ugly zooms can get :).

So, when displaying the image, each pixel is asked a question : Are you the same as the one I've just displayed, or are you the next one in the original image? This question is the main problem. When you know the answer, there is no more problem!

Actually, the technique I use doesn't follow the same train of thought, but this one : I take every X pixels of my image and display them one after the other on the screen. If X=1 then my image is the same as the original. If X=2, the result is twice thinner, as we've skipped half of the pixels. But it becomes interesting because X is not an integer. If X=0.5, the image is twice bigger (because we've displayed every pixels twice). X could be 1.45, 0.79, 0.1... The zoom rate can be quite accurate, and by making X go from 0.1 to 4, we could observe a nice looking un-zoom.

So the only question is how to use non-integer numbers, as the Z80 registers are quite... integer?

Here comes Miss Fixed-point (applause)
The principle of fixed-point arithmetic is simple : we separate in 2 registers the integer part, and the decimal part. We consider the registers are 8 bits, which is enough for what we want to do. But who knows, you may want to use a 16 bits register for the integer part. It will just be more demanding, resource-wise.

Two 8-bit registers. The decimal part can hold a number from 0 to 255. This can seem strange, but we have no choice. Here we will consider these numbers valids: 1.42, 0.193, 456.255, but not 5.256!

Well, that's about it! Yet you didn't learn a lot. We must now implement this X factor that represents the number of pixels separating, in the memory, the point we have displayed to the next one.

In order to simplify things a bit more, our code will work with a byte precision. It will be uglier, but faster and more understandable. If you want something better, you will have to do it yourself.

Consider two 8-bit registers : first, we have ZOOM which is the integer part of our zoom rate. ZDECI is its decimal part. To display our image in its original size, ZOOM=1 and ZDECI=0, so that each pixel is displayed. A little question : what if I want a twice bigger gfx? Simple: ZOOM=0 and ZDECI=128. Don't forget that our registers can only hold values from 0 to 255.

There is another byte which we could have called Boulok or Pastek, but that we decided to call NEXT (it doesn't appear in the source code) and that represents the number of bytes, in the memory, separating the first byte of the current line of the image to the byte that is going to be displayed. NEXT is actually the result of all the previous iterations of the line. Even though NEXT has a decimal part too, we will only use the integer part to select the byte to display.

Little note : in this code, I chose to always display the first byte of the image, whatever happens. The next calculation will depend to it.

1st iteration : We've just displayed the first byte. We have to go to next one. Let's say we want a twice larger image, so : ZOOM=0 and ZDECI=128. So NEXT=0.128! We would like to know how to display the 0.128th byte, but we can't because it doesn't exist on a computer. So we will only use the integer part and display the 0th byte (the first byte of our gfx).

2nd iteration : we display the 0th byte, because NEXT=0.128 (previous calculation). We must calculate the next value of NEXT:

	NEXT=NEXT+ZOOM
	    =0.128+0.128
	    =1


3rd iteration : we display the 1st point (previous calculation).

	NEXT=NEXT+ZOOM
	    =1+0.128
	    =1.128


4th iteration : we display the 1st point.

	NEXT=NEXT+ZOOM
	    =1.128+0.128
	    =2


5th iteration : we display the 2nd point.

	NEXT=NEXT+ZOOM
	    =2+0.128
	    =2.128


6th iteration : we display the 2nd point...

And so on until all things rot. We will talk about ending conditions later. Has everybody understood ? Well, now we have to translate it into the cruel language of assembler. Piece of cake, really.

It's very simple: we're going to use two 8-bit registers, H and L, that will represent NEXT. H is the integer part, L the decimal one. They are both initialized to 0 at the beginning of the process of each line and will increase at each iteration. How much? Well, if we add ZDECI to L, if L overflows, we have to increase H (0.128+0.128=1, isn't it?). Then add to H the ZOOM rate. NEXT has been updated. All we have to do is to add it to the address of the beginning of the current line of your gfx.

In order not to use memory for swapping information, we will use registers as much as possible, including the auxiliary registers.

Little reminder: an EXX switches ONLY the following registers : HL/HL', DE/DE', BC/BC'. That's it! AF and AF' don't swap, and it is done on purpose, so that you can pass value from one set of registers to another through A, without having to pass through memory, which would be slower. To swap AF and AF', use EX AF,AF'. Both these instructions take 1 cycle, and really swap the registers, they don't "crush" them. Two EXXs one after the other won't produce any effect, the same for EX AF,AF'. IX, IY and SP are unique and can't be swapped.

Second reminder: the firmware uses BC' and AF', so it is important to save them if you intend to modify them. Our code will stop the interruptions so that the system doesn't mess with it.

The first set of registers will point on the screen memory, as well as on the beginning of the current line of the gfx. The second set will contain ZOOM and ZDECI, and NEXT. As I said earlier, even though ZOOM needs two registers (one for its integer part, one for its decimal part), only the first is added to the address of the beginning of the current line, in order to calculate what byte to display. We will use A to pass this offset to the other set of registers.

So, for each iteration, here's what we have to do:

1) Display the current byte of the current line of the image.
2) NEXT=NEXT+ZOOM,DECI
3) Add our gfx pointer to NEXT
4) Start again till the end of time

Let's have a closer look at each step, now.

1) Display the current byte of the current line of the image. For the first iteration, we simply point at the beginning of the gfx. For now, let's suppose that:

	HL=Pointer on the gfx.
	DE=Pointer on the screen memory.


We know that we are byte-accurate, so whenever we display a byte, we can increase DE. But not HL, because we don't know how many bytes we will skip (if any!). LDI won't work here (unless you decrease HL every time needed, which shouldn't be efficient). So:

	LD A,(HL)	;Read one byte of the gfx.
	LD (DE),A	;Display it.
	INC DE		;Next byte on the screen.


2) NEXT=NEXT+ZOOM,DECI
(Please note that ZOOM,DECI means for instance 1.128). It's very very easy, one operation does the trick. First we use the auxiliary registers. In HL we have NEXT (H=integer part, L=decimal part), and in DE we have ZOOM,ZDECI. All you have to do is... add them like 16-bit numbers. If one decimal part overflows, the integer part will be automatically incremented:

	EXX		;Using auxiliary registers.
	ADD HL,DE	;NEXT=NEXT+ZOOM


3) Add our gfx pointer to NEXT
We know the byte number that we want to display (NEXT), but this value is relative to the beginning of the current line of the gfx. We have it in HL (of the first register set). Warning, only the integer part of NEXT will be useful! So we use A to transfer it in the "gfx" registers:

	LD A,H
	EXX


It isn't over. Something's wrong with our registers. HL holds the address of the first byte of the line, but if we add NEXT to it, it won't anymore, which will be problematic for the next iterations! What we need is always keep somewhere the address of the beginning of the current line. We could do something like that, using the memory:

	LD HL,(ADGFX)
	ADD HL,A	(this instruction doesn't exist)


But as we have one 16-bit register left (BC) and clever as we are, we will set BC with the address of the beginning of the line, then add BC to HL... in which we have transferred NEXT first! As every 16-bit addition gives the result in HL, BC is intact and can be used again indefinitely. Cool! This would do this:

	LD L,A     ;Transfer the integer part of NEXT into HL.
	LD H,0	   ;Mandatory, as the integer part is 8 bits only!
	ADD HL,BC  ;Beginning of the line + NEXT = address of the point to display!


(By thinking a bit more we could optimise all this by making NEXT a relative value to the previously read byte. Don't forget to reset the integer part of NEXT at each iteration!)

Here we are ! Now we just have to take care of the loop.

4) The loop
A little question: should the displaying stops when a defined number of bytes are displayed on the screen, or when only a defined number of bytes from the original bytes are read? These two approaches are a bit different, each has its particularities. I chose the first one, it's the most obvious and most used. This way, the displayed gfx has always the same size whatever zoom rate you're using, and the same machine-time is used. On the coding side, a simple loop is enough. I use IXL, which is the less significant register of IX (warning, Dams and Maxam don't know such register. You will have to enter the opcode by hand. Winape knows, though).

	DEC IXL
	JR NZ,XLOOP


To optimise the loop, you can copy and paste the code several times, and divide the counter accordingly. Thanks to its little size, it's not memory consuming, and the machine time saved is quite satisfying. Another technique is the use of a RET table. It only takes 3 cycles (the RET instruction), and a bit of memory.

The second approach consists in always displaying the same part of the original gfx, regardless of the zoom rate. The machine time used isn't stable and directly proportional to the zoom rate, and thus, the size displayed. One way to test the end of the line is to refer not to a loop counter, but to NEXT. Imagine you have a ZOOM=1 and that your gfx is 20 bytes width. NEXT will start from 0 and will rise up to 19 at the end of one line. Now, for the same gfx, if your ZOOM is 0.128, NEXT will still go from 0 to 19.0 at the end of the line, but for 40 bytes displayed this time. So comparing the integer part of NEXT to the size of your gfx works fine.

However, if you reduce the zoom, you will notice garbage on the right side of your gfx! This is normal as you've just displayed less bytes that before. To avoid this, you can either clear this area by yourself, or add a little blank column inside your gfx, with a width proportional to the maximum speed of your un-zoom.

So all you have to do is replace the loop just above with this:

	CP 19
	JR C,XLOOP


And that's it! Perhaps the "JR C" puzzles you, as you would have written "JR NZ". JR C means "inferior to". What?

Reminder: When you are writing "CP B", the Z80 does A-B. If A is smaller than B, and the subtraction overflows and the Carry is set.

But in our case, why use C and not NZ ? Simply because nothing guarantees us that 19 will be actually reached by NEXT! Perhaps the zoom rate will be too high, and NEXT will go from 18 to 20 (or the opposite) without even reaching 19! Try replace C with NZ, you will see funny things going on.

Is that it?
Almost, but not yet. We have to deal with the image. As you know, we work with a byte accuracy, which means that if our image is in MODE 1, we have to stretch it till it is four times bigger, so that we can see all its pixels. I chose to simply convert a little text written with the #BB5A vector into a four times bigger gfx (in the "TRANSF" routine). The code is very simple, treats each pixel of the original gfx and converts them into bytes. As it is in Mode 1, I isolate each pixels (four per byte) with a AND, shift them so that they are put on the right position. Four comparisons (maximum) then convert this into a mode 1 byte.

As the loop technique I use during the zoom is done by counting the displayed bytes, I convert a bigger gfx than what I actually created, so that my code displays "blank" at the right of my zoom, even when the un-zoom rate is high. But if you un-zoom too much, you will see the gfx cycle!

The end
I hope you enjoyed this little article. The source provided is quite raw, but works fine. There are a lot, lot, LOT of things to improve, like the zoom in Y, the pixel management, as well as all the optimisations you will imagine!

I hope you've learnt a thing or two. If you have any questions, don't hesitate to contact me!

Targhan