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

This article is the fourth 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 this intro by Supplex (so beautiful, so vintage):
Sine scroll in an intro by Miracle
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 the third article, we learned how to draw and animate the sine scroll, first with the CPU then with the Blitter.
In this article, we shall add some bells and whistles to the sine scroll, namely a shadow and a mirror – this doesn’t cost much CPU time cycles because the Copper makes this happen. Also, we shall try to give control back to the OS as cleanly as we can.
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.

Add a shadow and a mirror with the Copper

What is a shadow on the south-east, if not a bitplane that is displayed under himself, slighty scrolled to right and to the bottom? And what is a mirror, if not a bitplane that keeps being displayed after a given line, but in reverse order, that is by decrementing its address rather then incrementing it?
It looks like those effects may easily be implemented. Indeed, they are, thanks to the Copper. At the beginning of each line in the screen, the coprocessor allows to modify the address at which the hardware reads the data of the line to be displayed, and to set a delay before the hardware starts to display this line.
As usual, we shall first define the parameters for the effects:
  • SHADOW_DX and SHADOW_DY are the width and the height of the shadow projected toward the right and the bottom, respectively, and SHADOW_COLOR is the color of this shadow;
  • MIRROR_Y is the ordinate at which the mirror starts, and MIRROR_COLOR and MIRROR_SCROLL_COLOR are the background color and the sine scroll color in the mirror, respectively.
SHADOW_DX=2	;Between 0 and 15
SHADOW_DY=2
SHADOW_COLOR=$0777
MIRROR_Y=SCROLL_Y+SCROLL_DY
MIRROR_COLOR=$000A
MIRROR_SCROLL_COLOR=$000F
Let’s sketch what the Copper list should look like. Experience tells us that instead of rushing headlong into the coding, it’s better to sketch how things happens at the beginning of each line – the Copper allows MOVE as a line is being displayed, but we shall not use this feature. To avoid overloading the schema, brackets mean that DISPLAY_Y must be added to the enclosed constant.
WAIT and MOVE for the shadow and the mirror
First, the shadow. The hardware reads the data for the line of the bitplane to be displayed at a 32 bits address that is stored in 16 bits registers BLT1PTH and BPL1PTL. At the end of the line, the hardware updates those registers by adding BPL1MOD to them, which gives the address where it has to read data for the next line of the bitplane.
Until now, one bitplane was displayed: the bitplane 1. We add a bitplane 2, and we tell the hardware that the data for this bitplane are the same as those of bitplane 1:
	move.l bitplaneA,d0
	move.w #BPL1PTL,(a0)+
	move.w d0,(a0)+
	move.w #BPL2PTL,(a0)+
	move.w d0,(a0)+
	swap d0
	move.w #BPL1PTH,(a0)+
	move.w d0,(a0)+
	move.w #BPL2PTH,(a0)+
	move.w d0,(a0)+
Displaying another bitplane requires some other changes to the Copper list:
  • set the modulo for even bitplanes, and not only for odd ones:
    	move.w #BPL2MOD,(a0)+
    	move.w #0,(a0)+
    
  • set two more colors because the palette now contains 4 colors:
    	move.w #COLOR02,(a0)+
    	move.w #SCROLL_COLOR,(a0)+
    	move.w #COLOR03,(a0)+
    	move.w #SCROLL_COLOR,(a0)+
    
By setting DISPLAY_DEPTH to 2, we change the value that the Copper stores in BPLCON0 to tell the hardware how many bitplanes have to be displayed. For this reason, no more action is required.
The two bitplanes match precisely until line [SCROLL_Y+SHADOW_DY-1] is reached. This means that the sine scroll is displayed using color 3. That’s why COLOR03 contains SCROLL_COLOR.
Starting from line [SCROLL_Y+SHADOW_DY], the bitplanes do not match anymore because one of them is scrolled:
  • Horizontally, by setting the scroll value for even bitplanes in BPLCON1 to SHADOW_DX. The Copper must be told to set this value at the beginning of line [SCROLL_Y+SHADOW_DY]. Note that SHADOW_DX can’t be bigger than 15; to scroll the bitplane more than 15 pixels to the right, one must use BPL2PTH, BPL2PTL and BPLCON1 simultaneously.
  • Vertically, by setting the modulo for the even bitplanes in BPL2MOD to [-SHADOW_DY*(DISPLAY_DX>>3)]. As already explained, the Copper must be told to set this value at the beginning of line [SCROLL_Y+SHADOW_DY -1] so that it affects the next line.
Shifting the bitplanes to create a shadow
When line [SCROLL_Y+SHADOW_DY] is reached, the address of the line in bitplane 2 becomes that of the line in bitplane 1 minus SHADOW_DY lines. After this, bitplane 2 has to be displayed as usual. That’s why BPL2MOD must be reset to 0 at the beginning of line [SCROLL_Y+SHADOW_DY]. If not, the line would be repeated indefinitely.
Next, the mirror. As seen before, BPLxMOD makes it possible to tell the hardware to read the bitplanes from bottom to top so that it keeps displaying one line starting from some vertical position in the screen. If the width of the bitplanes is DISPLAY_DX, then:
  • at the beginning of line [MIRROR_Y-1], setting the modulo to -(DISPLAY_DX>>3) tells the hardware to repeat (at line [MIRROR_Y]) the line [MIRROR_Y-1] that is going to be displayed;
  • at the beginning of line [MIRROR_Y], setting the modulo to -2*(DISPLAY_DX>>3) tells the hardware to repeat (at line [MIRROR_Y+1]) the line [MIRROR_Y-2] that has been displayed;
  • as long as this modulo is set to this value, the line displayed at 2*MIRROR_Y-1-y is repeated at y, which keeps the mirror going.
The beginning of the mirror must match the end of the shadow, or it would look weird. This means that we have to change colors 0 and 3 via COLOR00 and COLOR03 at the beginning of line [MIRROR_Y]. We choose colors so that the sine scroll seems to be reflected in something like water.
Repeating drawn lines in reverse order to create the mirror
The Copper has to execute all the MOVE that produce the described effects at the very beginning of given lines. Therefore, those instructions must come after WAIT instructions that tell the Copper to wait for the electron beam to reach or pass those lines.
Remember that a WAIT instruction related to position (x, y) in the screen is coded with two words: one word (y<<8)!((x>>2)<<1)!$0001 followed by another word that is a mask. The mask tells the Copper which bits of the position must be compared to the matching bits of the electron beam position.
In this case, we shall tell the Copper to compare ordinates only. For this reason, our WAIT will be coded with a word (y<<8)!$0001, followed by a word $FF00.
Let’s start with the vertical shift of the shadow…:
	move.w #((DISPLAY_Y+SCROLL_Y+SHADOW_DY-1)<<8)!$0001,(a0)+
	move.w #$FF00,(a0)+
	move.w #BPL2MOD,(a0)+
	move.w #-SHADOW_DY*(DISPLAY_DX>>3),(a0)+
…then the horizontal shift…:
	move.w #((DISPLAY_Y+SCROLL_Y+SHADOW_DY)<<8)!$0001,(a0)+
	move.w #$FF00,(a0)+
	move.w #BPL2MOD,(a0)+
	move.w #0,(a0)+
	move.w #BPLCON1,(a0)+
	move.w #SHADOW_DX<<4,(a0)+
...then the end of the vertical shift of the shadow and the beginning of the mirror...:
	move.w #((DISPLAY_Y+MIRROR_Y-1)<<8)!$0001,(a0)+
	move.w #$FF00,(a0)+
	move.w #BPL1MOD,(a0)+
	move.w #-(DISPLAY_DX>>3),(a0)+
	move.w #BPL2MOD,(a0)+
	move.w #(SHADOW_DY-1)*(DISPLAY_DX>>3),(a0)+
...then the end of the horizontal shift of the shadow, the remaining part of the mirror and its palette:
	move.w #((DISPLAY_Y+MIRROR_Y)<<8)!$0001,(a0)+
	move.w #$FF00,(a0)+
	move.w #BPLCON1,(a0)+
	move.w #$0000,(a0)+
	move.w #BPL1MOD,(a0)+
	move.w #-(DISPLAY_DX>>2),(a0)+
	move.w #BPL2MOD,(a0)+
	move.w #-(DISPLAY_DX>>2),(a0)+
	move.w #COLOR00,(a0)+
	move.w #MIRROR_COLOR,(a0)+
	move.w #COLOR03,(a0)+
	move.w #MIRROR_SCROLL_COLOR,(a0)+

Give the control back to the OS

When the user clicks on the left mouse button, the show must end and the control be given back to the OS as cleanly as possible - there is no assurance that it will recover from having been shut down in such a way.
First, we wait for the Blitter:
	WAITBLIT
Next, we shut down the interrupts and the DMA channels...:
	move.w #$7FFF,INTENA(a5)
	move.w #$7FFF,INTREQ(a5)
	move.w #$07FF,DMACON(a5)
...and we reactivate them as they were in the first place:
	move.w dmacon,d0
	bset #15,d0
	move.w d0,DMACON(a5)
	move.w intreq,d0
	bset #15,d0
	move.w d0,INTREQ(a5)
	move.w intena,d0
	bset #15,d0
	move.w d0,INTENA(a5)
Then, we restore the Copper list of the Workbench. Its address lies at a given offset of the Graphics library base address. To retrieve it, we open this library by calling the Exec OldOpenLib() function. Once the Copper list address has been retrieved, we tell the Copper to use it:
	lea graphicslibrary,a1
	movea.l $4,a6
	jsr -408(a6)
	move.l d0,a1
	move.l 38(a1),COP1LCH(a5)
	clr.w COPJMP1(a5)
	jsr -414(a6)
We restore the usual behavior of the system by calling the Exec Permit() function:
	movea.l $4,a6
	jsr -138(a6)
We free the memory blocks that we allocated, by calling the Exec FreeMem() function:
	movea.l font16,a1
	move.l #256<<5,d0
	movea.l $4,a6
	jsr -210(a6)
	movea.l bitplaneA,a1
	move.l #(DISPLAY_DX*DISPLAY_DY)>>3,d0
	movea.l $4,a6
	jsr -210(a6)
	movea.l bitplaneB,a1
	move.l #(DISPLAY_DX*DISPLAY_DY)>>3,d0
	movea.l $4,a6
	jsr -210(a6)
	movea.l bitplaneC,a1
	move.l #(DISPLAY_DX*DISPLAY_DY)>>3,d0
	movea.l $4,a6
	jsr -210(a6)
	movea.l copperlist,a1
	move.l #COPSIZE,d0
	movea.l $4,a6
	jsr -210(a6)
Last, we unstack the registers and end the progam:
	movem.l (sp)+,d0-d7/a0-a6
	rts
It is time to check if the main loop runs at the frame rate of 1/50th of a second, and to optimize the code if it's too slow...
How to code a sine scroll on Amiga (4/5)