How to code a sine scroll on Amiga (3/5)

This article is the third of a serie of five about how to code a one-pixel sine scroll on Amiga, an effect commonly used by coders of demos and other cracktros. For example, in this cracktro by Supplex:
Sine scroll in a cracktro by Supplex
In the first article, we learned how to install a development environment on an Amiga emulated with WinUAE, and how to code a basic Copper list to display something on the screen. In the second article, we learned how to set up a 16×16 font to display the columns of pixels of its characters, and to use triple buffering to display the pictures on the screen without any flickering.
In this third article, we shall go to the heart of the matter by learning how to draw and animate the sine scroll, first with the CPU, then with the Blitter.
Click here to download the archive of the source and data of the program hereby explained.
If you’re using Notepad++, click here to download and enhanced version of the UDL 68K Assembly (v3).
NB : This article may be best read while listening to the great module composed by Nuke / Anarchy for the diskmag part of Stolen Data #7, but this is just a matter of personal taste…
Cliquez ici pour lire cet article en français.

Scroll and animate the sine scroll

The main loop may now be described. It accomplishes the following tasks:
  • wait for the electron beam to finish drawing the picture;
  • roll the three bitplanes to display the newly drawn picture;
  • wait for the Blitter and tell it to erase bitplane C that contains the penultimage picture;
  • draw the text in bitplane B that contains the antepenultimate picture;
  • animate the index of the first column of the first character of text to draw;
  • animate the sine of this first column;
  • test if the left mouse button is pressed.
The first three tasks have already been described. Let’s describe the others.
The sine scroll is drawn by a loop that draws SCROLL_DX columns of consecutive characters, starting at column SCROLL_X in the bitplane. The index of the first column to draw and the index of first character it belongs to are stored in the variables scrollColumn and scrollChar, respectively. The offset of the sinus of the first column of the sine scroll is stored in the variable angle.
Let’s animate the sine scroll in the main loop.
Having the text moving along a sine curve would be of no interest if this curve were not to be animated: it would look like as if the text was moving along a roller coaster. For this reason, we decrement the offset of the sine of the first column of the sine scroll on each frame, and doing this, we watch for the underflow:
	move.w angle,d0
	sub.w #(SINE_SPEED_FRAME<<1),d0
	bge _angleFrameNoLoop
	add.w #(360<<1),d0
_angleFrameNoLoop:
	move.w d0,angle
Moreover, the text must scroll from the left to the right. To this end, we must increment the index of the first column in the text by SCROLL_SPEED. We must then watch for two overflows: when the last column of a character is reached, we must jump to the first column of the next character; when the last character of the text is reached, we must jump to the first character of the text:
	move.w scrollColumn,d0
	addq.w #SCROLL_SPEED,d0
	cmp.b #15,d0				;Is the next column after the last column of the current character?
	ble _scrollNextColumn		;If not, keep this column
	sub.b #15,d0				;If yes, change for a column in the next character...
	move.w scrollChar,d1
	addq.w #1,d1				;...and move to the next character
	lea text,a0
	move.b (a0,d1.w),d2
	bne _scrollNextChar			;...and check if this next character lies beyond the end of the text
	clr.w d1					;...and if yes, loop on the first character of the text
_scrollNextChar:
	move.w d1,scrollChar
_scrollNextColumn:
	move.w d0,scrollColumn
We may now draw the sine scroll.
Drawing the sine scroll is accomplished by the latter loop that lies in the main loop. Before we code this main loop, we must initialize a number of things to maximize the usage of the CPU registers. This way, we avoid reading data from the memory, which saves CPU time cycles.
First, we compute the offset (D6) of the word in the bitplane where the bit that matches the first column to be drawn lies, and we remember this bit (D7):
	;Compute the offset of the word in the bitplane where lies the first column to be drawn

	moveq #SCROLL_X,d6
	lsr.w #3,d6		;Offset of the byte where the column lies
	bclr #0,d6		;Offset of the word where the column lies (same thing as lsr.w #4 then lsl.w #1)

	;Compute the bit in this word that matches this column

	moveq #SCROLL_X,d4
	and.w #$000F,d4
	moveq #15,d7
	sub.b d4,d7	;Bit dans le mot
Next, we compute the address (A0) of the next character and the address (A1) of the word in the 16x16 font that contains its current column (D4). This column has to be drawn in the current bitplane column that was just computed:
	move.w scrollChar,d0
	lea text,a0
	lea (a0,d0.w),a0
	move.w scrollPixel,d4
	clr.w d1
	move.b (a0)+,d1
	subi.b #$20,d1
	lsl.w #5,d1				;32 bytes per character in the 16x16 font
	move.w d4,d2			;Column of the character to be drawn
	lsl.w #1,d2				;2 bytes per line in the 16x16 font
	add.w d2,d1
	move.l font16,a1
	lea (a1,d1.w),a1		;Address of the column to be drawn
Note that in the previous code, the offset of the first column of a character is computed by substracting $20 to the ASCII code of this character - the characters in the 8x8 font are sorted in ascending ASCII order, which makes this it possible.
Next, we initialize various registers that are intensively used in the loop, starting with the offset of the sine for the current column (D0) and the number of the columns we still have to draw (D1):
	move.w angle,d0
	move.w #SCROLL_DX-1,d1
	move.l bitplaneB,a2
Here is what the registers look like at the begining of the loop:
RegisterContents
D0Offset for the sine value of the current column in the bitplane
D1Current column in the bitplane
D4Current column of the character
D6Offset of the word in the bitplane that contains the current column
D7Bit in this word that matches the current column
A0Address of the current character in the text
A1Address of the word in the font matching the current column of this character
A2Address where to draw in the bitplane
The loop that draw the SCROLL_DX colmuns of the sine scroll accomplishes the following tasks:
  • compute the address of the word that contains the first pixel of the column in the bitplane;
  • draw the current column of the current character;
  • move to the next column of the the current character, or to the first column of the next character, or the first column of the first character;
  • decrement the angle for the current column.
Computing the address (A4) of the word that contains the first pixel of the column in the bitplane requires a multiplication with a precomputed sine value. This sine value is a power of 2:
	lea sinus,a6
	move.w (a6,d0.w),d1
	muls #(SCROLL_AMPLITUDE>>1),d1
	swap d1
	rol.l #2,d1
	add.w #SCROLL_Y+(SCROLL_AMPLITUDE>>1),d1
	move.w d1,d2
	lsl.w #5,d1
	lsl.w #3,d2
	add.w d2,d1	;D1=(DISPLAY_DX>>3)*D1=40*D1=(32*D1)+(8*D1)=(2^5*D1)+(2^3*D1)
	add.w d6,d1
	lea (a2,d1.w),a4
Yes! Although D1 has been initialized to store a counter for the loop, we use it as a temporary variable. That's because we are short of registers. So, the loop has to begin and end with some data exchange with the stack:
_writeLoop:
	move.w d1,-(sp)
	;...
	move.w (sp)+,d1
	dbf d1,_writeLoop
We may now display the current column of the current character in the current column of the bitplane. It's quite simple because the 16x16 font has been rotated. We just have to test the successive bits in the word that matches the current column to be drawn, rather than to test the same bit in successive words:
Drawing a column of a character pixel after pixel
The following code displays the current column (word at A1) of the current character in the current column of the bitplane (bit D7 in the word at A4):
	move.w (a1),d1
	clr.w d2
	moveq #LINE_DX,d5
_columnLoop:
	move.w (a4),d3
	btst d2,d1
	beq _pixelEmpty
	bset d7,d3
	bra _pixelFilled
_pixelEmpty:
	bclr d7,d3
_pixelFilled:
	move.w d3,(a4)
	lea DISPLAY_DX>>3(a4),a4
	addq.b #1,d2
	dbf d5,_columnLoop
The column having been drawn, we may display the next column of text, that is the next column of the current character or the first column of the next character - if the current character is the last one, we have to loop on the first character of the text to repeat the text of the sine scroll indefinitely:
	addq.b #1,d4
	btst #4,d4
	beq _writeKeepChar
	bclr #4,d4
	clr.w d1
	move.b (a0)+,d1
	bne _writeNoTextLoop
	lea text,a0
	move.b (a0)+,d1
_writeNoTextLoop
	subi.b #$20,d1
	lsl.w #5,d1
	move.l font16,a1
	lea (a1,d1.w),a1
	bra _writeKeepColumn
_writeKeepChar:
	lea 2(a1),a1
_writeKeepColumn:
This next column is displayed at another ordinate that is computed by decrementing the offset of the current sine...:
	subq.w #(SINE_SPEED_PIXEL<<1),d0
	bge _anglePixelNoLoop
	add.w #(360<<1),d0
_anglePixelNoLoop:
...and it is displayed in the next column of the bitplane. This column may lie in the word that comes after current one:
	subq.b #1,d7
	bge _pixelKeepWord
	addq.w #2,d6
	moveq #15,d7
_pixelKeepWord:

Draw much faster with the Blitter

Displaying the columns is an intensive task for the CPU. Its workload may be lighten by using the Blitter.
In a previous article, we learned that the Blitter can copy blocks and draw lines. This latter functionality is of utmost interest here, because the Blitter can draw a line that repeats a 16 pixels pattern. May this pattern be the column of a character to be drawn ? Certainly. We shall use the Blitter to draw as many 16 pixels lines, vertical and textured, that there are columns to draw to create the sine scroll.
We draw a column with the CPU from the top to the bottom, but if we use the Blitter, we must draw it the other way. The reason is that the pattern of the column is arranged so that its bit 15 does not match the first pixel of the column, but its last.
Drawing a column of a character with the Blitter
Setting up the Blitter to draw lines is a bit tedious because it requires storing a lot of values in registers. A good part of this initialization may be done once for all. Indeed, the contents of those registers is not modified, and has not to be modified, if lines of the same kind are drawn.
Let's start by defining some parameters:
LINE_DX=15	;# of lines of the line - 1 : LINE_DX=max (abs(15-0),abs(0,0))
LINE_DY=0	;# of columns of the line - 1 : LINE_DY=min (abs(15-0),abs(0,0))
LINE_OCTANT=1
Next, let's code the part of the initialization of the Blitter that is required just once for all:
	move.w #4*(LINE_DY-LINE_DX),BLTAMOD(a5)
	move.w #4*LINE_DY,BLTBMOD(a5)
	move.w #DISPLAY_DX>>3,BLTCMOD(a5)
	move.w #DISPLAY_DX>>3,BLTDMOD(a5)
	move.w #(4*LINE_DY)-(2*LINE_DX),BLTAPTL(a5)
	move.w #$FFFF,BLTAFWM(a5)
	move.w #$FFFF,BLTALWM(a5)
	move.w #$8000,BLTADAT(a5)
	move.w #(LINE_OCTANT<<2)!$F041,BLTCON1(a5)		;BSH3-0=15, SIGN=1, OVF=0, SUD/SUL/AUL=octant, SING=0, LINE=1
For each column to be drawn, we must tell the Blitter the address of the word that contains the starting pixel of the line via BPLCPTH / BLTCPTL and BLTDPTH / BLTDPTL, the number of this pixel via BLTCON0 and the pattern of the line via BLTADAT.
	WAITBLIT
	lea LINE_DX*(DISPLAY_DX>>3)(a4),a4
	move.l a4,BLTCPTH(a5)
	move.l a4,BLTDPTH(a5)
	move.w (a1),BLTBDAT(a5)
	move.w d7,d2
	ror.w #4,d2
	or.w #$0B4A,d2
	move.w d2,BLTCON0(a5)		;ASH3-0=pixel, USEA=1, USEB=0, USEC=1, USED=1, LF7-0=AB+AC=$4A
To tell the Blitter it must draw the 16 pixels line, we have to write in BLTSIZE:
	move.w #((LINE_DX+1)<<6)!$0002,BLTSIZE(a5)
This code may almost replace the code that draws a column with the CPU. The only noticable difference is the way this column is identified. When the CPU is used, this is the number of the bit of the current word. When the Blitter is used, this is the number of the pixel in this word. One is the opposite of the other: pixel 15 matches with bit 0; pixel 14 matches with bit 1; and so on.
The source contains both the code for the CPU version and the Blitter version. A constant BLITTER allows to activate one or to the other (0 for drawing the sine scroll with the CPU, 1 for drawing it with the Blitter):
BLITTER=1
The compilation of various parts of the code depends on the value of this constant. For example:
	IFNE BLITTER

	moveq #SCROLL_X,d7
	and.w #$000F,d7

	ELSE
	
	moveq #SCROLL_X,d4
	and.w #$000F,d4
	moveq #15,d7
	sub.b d4,d7

	ENDC
It's done! The sine scroll runs. We have to make it prettier by adding some effects that require just a few CPU time cycles, and we certainly have to optimize its code...