Tutorial 8: Full-screen smooth scrolling. Know your timing & banking!

In this section, we will be covering a couple of important things in order to achieve full screen smooth scrolling with colors:

  • Bank switching & double buffering
  • Raster interrupt timing to handle color copying
  • Avoiding flickering and stops
  • Screen copying routines

In the end, this is what it all comes down to:

The idea is simple, but the implementation somewhat tedious.

  • For each frame, use the vertical scroll register and scroll y between 0 and 7
  • When the scroll register is zero, copy all image & color data one row down
  • Render a new line of text & color at the top of the screen

Sounds easy? Yeah but man this is C64 and nothing is that simple. As mentioned in this blogpost, there are a number of things we need to address before being able to achieve god-like smooth scrolling (as opposed to hellish flickering):

  • Copying of screen data is slow, and cannot be performed in a single pass. Therefore, we have to split the copying into two parts: copy upper and lower screen data separately at different values for the scroll register.
  • In addition, to prevent flickering, we implement double buffering. This means that we are displaying one screen while writing to another, before switching the screen memory pointer when the scroll register hits 0. This is done via bank switching – we  continuously switch between bank 0 and bank 1 in order to prevent screen artifacts.
  • As the C64’s color data located at $D800 is completely static, it is impossible to double buffer colors. This means that the color copying must be “live”, and requires that we perform the copying synchronized with the current raste line. Again, this needs to be performed in two passes.

The good news

Turbo Rascal SE contains a lot of built-in functions, which are hard-coded drunkenly composed assembler functions that can be called directly from TRSE. In addition, we will now encounter the first use of the Turbo Rascal SE Ras library, or RasLib for short. RasLib is a collection of include files that contain bunch of useful implementations that is written in Rascal, so you don’t have to delve into the technical parts. However, we will be covering the more advanced implementations further down in this tutorial, just because.


program Tutorial8;
var  
   index, time, a, val, color, colorShift,i,j,k : byte; 
   fade : array [16] of byte = (11,6,6,12,4,14,15,1,1,15,14,4,12,6,6,11); 
   mainChar: IncBin("test.bin","$27FF");

// Include methods for banking
@include "../RasLib/verticalbanking.ras"

// include vscroll methods
@include "../RasLib/verticalscrolling.ras"

In the variable declaration, we have our old fade function and a custom character set loaded at $2800 (yeah there is an extra rogue byte there somewhere). For the case of scrolling & banking, we only need to include two RasLib files in order to get the vertical scrolling up and running : “verticalbanking.ras“, which contains banking & copying methods, and “verticalscrolling.ras” which contains exactly what it sounds like. These RasLib files are located in the main project folder and can be edited just like regular files.

The main routine looks as follows:


procedure Setup();
begin
	copyfullscreen(^$27FF,^$27FF+^$4000);

	poke(SCREEN_BG_COL, 0, 0);
	poke(SCREEN_FG_COL, 0, 0);

	hideBorderY(0);
end;

begin
	time:=0;	
	Setup();

	disableinterrupts();
	RasterIRQ(Update(), 1);
	enableinterrupts();

end.

Note here some new functions:

  • CopyFullScreen is a built-in method that will copy exactly 1000 bytes from location A to B. In this case, we are copying 1000 bytes from the custom caracter set from bank 0 to bank 1 (+$4000 bytes)
  • HideBorderY extends the border (bad naming perhaps) by hiding line 0 and 24 on the screen. This enables the illusion of continuous smooth scrolling by preventing the user from seeing the actual writing on line 0.

Again, the main method is hooked up to a raster IRQ:


// Raster update
interrupt Update();
begin
	time:=time+1;
	a:=(time*4)&1;

	if a=0 then begin
		VerticalScroll();
		
		// Only print a new color line when scrolling index is 0
		if g_vscroll=0 then 
			printColor();
		
		// Only print a new line when scrolling index is 7
		if g_vscroll=7 then 
			PrintLine();

	end;
	kernalinterrupt();
end;

The Update() interrupt increases a counter, and slows it down by letting the vertical scroll method only be called on every 4th frame. The VerticalScroll() method is implemented in the RasLib file “verticalscrolling.ras”, and performs all the copying, banking and raster timing you need. For now. The only left needing implementation is what to actually display during scrolling. The PrintLine outputs characters when the (global) scroll register index i 7, while color data is added when the scroll register is at 0. The implementations are as follows:


// Print a single line at the top of the screen on the current bank
procedure PrintLine();
begin
	j:=sine[time*1]/8;
	k:=sine[j*time/64]/2;
	for i:=0 to 40 do begin
		val:=val/32+64 - 4;
		// Chose the center of a nice sine functions so that the background looks
		// like a canal		
		val:=sine[(i*4 + j+k)+30]/16 + 64 - 4;

		if val<64 then
			val:=$20;

		if g_currentBank=1 then
			poke(^$0400, i, val)
		else
			poke(^$4400, i, val);
	
	end;

end;

// Print a line of colors at $D800
procedure printColor();
begin
	colorShift:=0;
	color:=fade[(time/16 + colorShift)&15];
	for i:=0 to 40 do begin
		poke(^$D800,i,color);
	end;

end;

Again, these two methods are quite arbitrary : printColor just prints a indexed color from the fade function, while PrintLine outputs characters 64-80 as the shifted center of a sine function. This particular character set has graded characters from empty to filled on this particular location (64-80), hand-drawn by me, and was also used in the Plasma Effect tutorial.

The gritty parts

If you just want to get vertical scrolling up and running, you can stop reading here. However, if you are going to use this method in a full-scale project, you had better understand how this routine really works, because you will probably have to modify it.

The deal with VIC banks:

The C64 has 64K of memory, but the VIC graphics chip can only address 12 bits, meaning that

  • The C64 can "see" all memory simultaneously
  • The VIC chip han only "see" 16K, or $4000 bytes of memory
  • Color data is different, and is located globally at $D800, no matter what

When double-buffering, we are copying data from the current bank to the next bank in line, which is fine and well. However, since we loaded the custom character set at $2800, this memory address is invisible to the VIC when we switch from bank 0 to bank 1. We therefore have to copy the entire page of character sets to the "mirrored" position that bank 1 can access - meaning that we copy data from $2800 to $2800 + $4000. For future tutorials, this will also have to be performed when displaying sprites while bank switching.

Anyway, here's the Vertical Scroll method included from RasLib:


procedure VerticalScroll();
var 
	g_vscroll : byte;
begin
		g_vscroll:=(g_vscroll+1)&7;

		if g_vscroll=0 then begin
			WaitForRaster(150);
			CopyColor(0);
		end;

		waitForRaster(251);
		if g_vscroll=0 then SwitchBank();
		if g_vscroll=1 then CopyScreen(1);
		if g_vscroll=4 then CopyScreen(0);

		if g_vscroll=0 then begin
			CopyColor(1);
			//printColor();
		end;	
		scrolly(g_vscroll);

		//if scroll=7 then PrintLine();

end;

Whenever this method is called, a global scroll register "g_vscroll" is increased and kept between values 0-7. Breakdown:

  • If a bank switching (displaying the next buffer) is going to happen (at g_vscroll=0), we need to copy color data as well. However, color data is fixed at $D800, so the trick here is to start copying *after* the raster line has passed the current position that we are copying from. In addition, we only have time enough to copy half of the color data - or the whole process ends up with severe flickering. We therefore time the copying routime to wait for rasterline 150 before starting the copying of the upper half of the color data.
  • We then wait for the raster to near the end of the screen, so flickering is avoided (about at position 250)

Next, to spread the workload and decrease chances of flickering, we perform various tasks at various stages of the scroll register:

On g_vscroll = 0, we perform the actual bank switching, presenting the next buffer and switching the current bank index

On g_vscroll =1, we copy the lower half of character data from the current bank to the next bank in line

On g_vscroll=4, the upper half of character data is copied to the next bank in line

If we are at g_vscroll=0, when color copying should happen, we are now guaranteed to be at a rasterline position near the bottom, so we can proceeed by copying the lower half of the color data without getting flickering.

Finally, at rasterlines>250, we shift the actual hardware vertical scroll register. If we did this at <250, it would result in severe flickering.

Finally, we turn our attention to the bank switching methods. Here are all definitions from the verticalbanking.ras RasLib include file:


procedure Banking_Vars();
var
	g_currentBank:byte;
    temp_colorCopy : array[40] of byte;

begin
	g_currentBank:=0;
end;


procedure CopyScreen(ul_:byte);
begin

	if g_currentBank=0 then begin
		if ul_=0 then
			copyhalfscreen(^$0400 + ^520, ^$4400 + ^40 + ^520,12, 1)
		else
			copyhalfscreen(^$0400, ^$4400 + ^40, 13, 1);

	end;
	if g_currentBank=1 then begin
		if ul_=0 then 
			copyhalfscreen(^$4400+^520, ^$0400 + ^40 + ^520, 12, 1)
		else
			copyhalfscreen(^$4400, ^$0400 + ^40,13, 1);
	end;

end;




procedure CopyColor( ul2_copycolor:byte );

begin
	 if ul2_copycolor=0 then begin
		// First, copy the missing line
		memcpy(^$DA08, 0, temp_colorCopy, 40);
		copyhalfscreen(^$D800, ^$D800+^40,13, 1);
	end

	else begin 
		copyhalfscreen(^$D800+^14*^40 , ^$D800+^15*^40,10,1);
		memcpy(temp_colorCopy, 0, ^$D800 + 14*40, 40);

	end;


end;

procedure SwitchBank();
begin
	if g_currentBank=0 then 
		SetBank(VIC_BANK1)
	else 
		SetBank(VIC_BANK0);

	poke(VIC_DATA_LOC, 0, $1A);

	g_currentBank:=(g_currentBank+1)&1;
end;

These methods should be quite self-explanatory, but anyway here's a small breakdown:

  • These methods assume that we are switching between bank 0 and 1. If you need different banks, you need different methods (fo now).
  • The CopyScreenVerticalShift method copies either the upper or lower half of the screen to the next bank in line with a vertical shift
  • The CopyColorVerticalShift copies eiher the upper or lower half of the screen to the next bank in line with a vertical shift
  • SwitchBank alternates g_currentBank between 0 and 1, while making sure that the current character location is set properly to $2800 ($A * $400 = $2800) on the local VIC bank.