A little while back I put up a small tutorial on making a 1×1 text scroller. This tutorial builds on that foundation and becomes a dynamic 4×4 scroller. Why dynamic? I use this term since it builds each 4×4 character as needed, based on a character from a 1×1 font set. I achieve this by using a matrix of 16 character “patterns” that make up all possible 4×4 bit combinations. This is handy in situations where you’re pressed for memory in your intro and can achieve a 4×4 version of the 1×1 font for only an extra 128 bytes in font data.
The first step is to source a 1×1 character set to use. You can either rip one out of an intro (or game) using a Vice snapshot and then import it to a tool like Charpad. Or you can download one of the many from here: http://kofler.dot.at/c64/. Either way, the goal is to have the data ready to include into our assembly file and will look something like this:
font_data .byte $00, $00, $00, $00, $00, $00, $00, $00 .byte $fc, $c6, $dc, $c6, $c6, $c6, $fc, $00 .byte $fc, $c6, $c6, $c6, $c6, $c6, $fc, $00 .byte $7c, $c6, $f8, $c0, $c0, $c0, $c0, $c0 .. and so on
In the source code provided with this tutorial, I’m assuming the character set contains 64 individual 1×1 characters. I have a script file (written in LUA) which exports the raw font data into the formatted “.byte” statements shown above. Depending on your choice in compiler, the format will vary.
Now we need to create a set of 16 character patterns that can be used to form any of the characters in the set. This is made easy by using a tool like Charpad. Basically we want to create a series of characters that cover every possible combination of 4×4 bits in a character. This totals 16 possible combinations and for this example, I’ve simply used small squares.
The order of these is important and they will be used to create a matrix which the scroller code to look up to display the correct character.
These 16 characters will also need to be output for inclusion in our assembly file and will be added to the end of our 64 1×1 characters that were exported above.
Now for some source code. Many of my intros & effects run under interrupts. The code will begin by setting up an interrupt to initialise the part and then run through an interrupt chain to handle the hardware scrolling, scrolling reset and scrolling update. The chain looks like this:
Something to note is the source code here is not optimised – there are some assumptions made to help simplify, but there is plenty of room for you to improve upon in your own implementations.
At the top of the source file, I have a few helper sub routines for preparing the screen for the scroller. These routines will be called from the initialise interrupt.
The first is to clear the screen ram:
clear_screen_ramroutine lda #032 ldx #000 clear_screen_ram_loop sta C_SCREEN_RAM, x sta C_SCREEN_RAM + $100, x sta C_SCREEN_RAM + $200, x sta C_SCREEN_RAM + $2e8, x inx bne clear_screen_ram_loop rts
The second sub routine is use to clear the colour ram:
clear_colour_ramroutine lda #000 ldx #000 clear_colour_ram_loop sta C_COLOUR_RAM, x sta C_COLOUR_RAM + $100, x sta C_COLOUR_RAM + $200, x sta C_COLOUR_RAM + $2e8, x inx bne clear_colour_ram_loop rts
Another block of code at the top of the source file is for apply_interrupt. This takes care of setting up the next interrupt in the chain. It assumes information about the next interrupt is stored in the 3 registers. It’s also responsible for acknowledging the current interrupt. The apply_interrupt_repeat label is for cases where we want to acknolowedge the current interrupt, and have it repeat again before advancing to a different inteerrupt in the chain. It’s not used in this example.
apply_interrupt sta REG_RASTERLINE stx REG_INTSERVICE_LOW sty REG_INTSERVICE_HIGH apply_interrupt_repeat inc REG_INTFLAG jmp $ea81
Now let’s initialise the program. First up we call the two sub routines defined above to clear the colour and screen ram. Then we want to relocate the character data to a spot in memory where it can be used. For this program, I’m going to store the character set at $3000 (C_CHARSET). Next I want to fill the colour ram where the scroller will appear with white. I also want to print a short message to screen using the 1×1 font. This is just to show a comparison of the 1×1 and 4×4 fonts at run time. Now it’s time to tell the C64 where to source its character data from & set the screen and border colours to black. That’s the set up done.
sync_routine jsr clear_screen_ramroutine jsr clear_colour_ramroutine ldx #000 ; relocate character data relocate_font_data lda font_data, x sta C_CHARSET, x lda font_data + $100, x sta C_CHARSET + $100, x lda font_data + $180, x sta C_CHARSET + $180, x inx bne relocate_font_data ldx #160 ; init colour ram for scroller lda #001 set_scroller_colour sta C_COLOUR_RAM + 759, x dex bne set_scroller_colour ldx #019 ; render short message to screen init_short_message lda short_message, x sta C_SCREEN_RAM + 610, x lda #014 sta C_COLOUR_RAM + 610, x dex bpl init_short_message lda #029 ; switch to character set sta REG_MEMSETUP lda #000 ; init screen and border colours sta REG_BORCOLOUR sta REG_BGCOLOUR jmp hook_update_scroller
The next bit of source I want to cover are the two small interrupts which handle the setting and resetting of the hardware scroll register. The first interrupt handler is in charge of applying the hardware scroll value. The interrupt triggers just before the scan line the scroller starts on. We set up the accumulator ready to apply the hardware scroll (and 38 column mode), wait for the scroller start line and then apply the value to the register. Something to note here is the “scroller_amount” label. During the scroller update handler, this label will be written to with new values based on the scroller progress. To start with though, it will be 7 (the maximum scroll amount) and contain values in the range of 0 – 7.
hook_apply_hw_scroll lda #201 ldx #<apply_hardware_scroll ldy #>apply_hardware_scroll jmp apply_interrupt apply_hardware_scroll lda #$c0 ; 38 column scroller_amount ora #007 ; + hardware scroll ldy #202 ; wait first scroller scanline wait_scroller_start cpy REG_RASTERLINE bne wait_scroller_start sta REG_SCREENCTL_2 ; apply hardware scroll value jmp hook_reset_hw_scroll
The second interrupt handler is responsible for resetting the hardware scroller register and returning to 40 column mode. The accumulator is again set up, we then wait for the final scroller scan line to complete and then apply the value to the register. Both of these handlers are very light weight for this example.
hook_reset_hw_scroll lda #234 ldx #<reset_hardware_scroll ldy #>reset_hardware_scroll jmp apply_interrupt reset_hardware_scroll lda #$c8 ; 40 column mode + no scroll ldy #235 ; wait for final scanline done wait_scroller_end cpy REG_RASTERLINE bne wait_scroller_end sta REG_SCREENCTL_2 ; reset scroll & column mode jmp hook_update_scroller
The final interrupt handler to look at is the update scroller. This handler is where the magic in the scroller happens. Thankfully it’s not much more complicated than a 1×1 scroller. The same sort of logic applies. Decrease the hardware scroll until it’s negative. At that point we shift the screen ram contents to the left by 1 character and render a new character in the far right column. Then finally, we reset the the hardware scroll value and repeat. The difference between a 1×1 and 4×4 (apart from scrolling 4 lines of data) is instead of grabbing a new character from the message each time we scroll the data, we only grab the next character every 4 shifts. What needs to happen now is some bit masking, to work out which characters from our specially defined set of 16 to use. Do this for each of the 4 rows each time. Once we have rendered the 4×4 character, we then advance the message pointer.
First up, we advance the hardware scroll value. Here I’m advancing by 3 pixels every update. You can make the scroller slower by removing some of the dex instructions. Or you can increase the speed by adding more. We then detect if it’s time to physically shift the screen ram to the left. If not, the updated scroll value is written into a label in the apply_hardware_scroll interrupt handler and the update is complete.
update_scroller ldx scroller_amount + 1 dex ; advance hardware scroll dex dex bmi shift_scroller_data ; time to shift scroller? stx scroller_amount + 1 ; not time, so update scroll size jmp update_scroller_done ; we're done for now
The next part of the update is in charge of scrolling the screen ram to the left. At this point we’ve detected that it’s time to advance the characters to the left. We do this for all 4 rows of the scroller.
shift_scroller_data ldy #000 ; shift screen ram left scroller_shift_loop lda C_SCREEN_RAM + $2f9, y ; shift all 4 rows sta C_SCREEN_RAM + $2f8, y lda C_SCREEN_RAM + $321, y sta C_SCREEN_RAM + $320, y lda C_SCREEN_RAM + $349, y sta C_SCREEN_RAM + $348, y lda C_SCREEN_RAM + $371, y sta C_SCREEN_RAM + $370, y iny cpy #039 bne scroller_shift_loop
The next two lines are where we detect at what stage of rendering we are at into the current character. When the example starts, the value in scroller_char_step will be 255 – a negative value – which means we need to get the next character in the message string. A little bit later in the routine, this value will then be set to 3 since there are 4 steps to render for each characters (3, 2, 1, 0). If the value is a positive (0 to 3), then we jump forward in the route to render the next column of data for the current character.
ldx scroller_char_step + 1 ; grab step into rendering bpl render_next_scroll_colm ; time to render a new character?
If the next block of code is reached, it means we’re grabbing the next character from the message string and preparing to render it. The label scroller_message_index holds the next index into the message string we will use. It uses this index to look up the byte in scroller_message. In this example, if a negative value is read from the message string, that means we have reached the end of the message and need to loop back to the start. If we have to loop, then the first character from the scroller message is read into the accumulator and the index set 1 for the next character to read. We also want to grab the high byte of the scroller_message label and reset where we are reading the message from. Why do this? Well the x register can only contain values from 0 to 255. For small scroller messages, this would be fine. What happens when we have longer messages? Once we advance to offset 256, the index will be reset to 0, so we advance the source high byte by 1 and continue on. This allows us to have very long scroller messages and once complete, just reset the high byte and our message will loop. You can also use all sorts of control characters to perform different actions in your scroller (for example – adjust speed, flash colours, etc).
scroller_message_index ldx #000 read_next_scroller_char lda scroller_message, x ; grab next character to render bpl advance_scroller_index ; detect end of message lda scroller_message ldx #001 ; reset index ldy #>scroller_message ; reset message ptr sty read_next_scroller_char + 2 jmp save_scroller_index
If the above block of code was not required, then we can safely advance the message index for the next time through this code. Here it detects if the x register goes past 255 back to 0 and if so, advance the high byte of the scroller message pointer (as mentioned above). This will let us have long scroller text as the above block will reset the pointer (and offset) once it’s time to loop the text again.
advance_scroller_index inx ; advance scroller message index bne save_scroller_index ; detect if reached 256 offset inc read_next_scroller_char + 2 save_scroller_index stx scroller_message_index + 1
The final part of set up for the next character now happens. I’m making an assumption the character set will contain 64 characters. That means the character set is split into 2 x 256 byte blocks (32 chars * 8 bytes = 256). Therefore, when rendering, we need to know where the font data is so we can use a bitmask against it and render the correct characters. That means the high byte will either be #$30 or #$31. We also need to know the low byte of where to read the character data from. Once we know the high byte, we AND the character code so the value is between 0 and 31 before multiplying by 8 to give us the low byte. The values are then stored into the rendering code as they will be required for each of the 4 steps (remembering 4 characters – steps – for each 1×1 character). The bit mask is reset to 192 (binary – 11000000 – so we start with the two left most bits) and the step counter set to 3.
ldy #>C_CHARSET ; char in which charset half? cmp #031 bcc calc_scrollchar_src_low ldy #>C_CHARSET_HIGH calc_scrollchar_src_low and #031 ; calc offset into char data asl asl asl sty render_scroller_column + 2 ; store pointers for render sty render_scroller_column2 + 2 sta render_scroller_column + 1 sta render_scroller_column2 + 1 lda #192 ; reset scroller char mask sta scroller_character_mask + 1 lda #003 ; reset step into char mask sta scroller_char_step + 1
Remember, the above code blocks are only done each time we move to the next character in the message string. At this point, our setup for the next character is done – time to do some rendering.
First up we reset the hardware scroll value. We simply add 8 onto the current value (which will be a negative value as that’s what will trigger this code to run) and will leave us with the next scroll value. If we were only scrolling by 1 pixel each time, then this could just simply set the value to 8. But because this example scrolls by 3, there are multiple possible values it would have to reset to for the next frame. So simply adding 8 means it will always move to the hardware scroll value to the next correct value.
render_next_scroll_colm clc lda REG_ZERO_FE ; reset hardware scroll adc #008 tax stx scroller_amount + 1 ; save index
We’re almost there now – all the set up work is done and it’s time to actually render something to those 4 characters on the far right of the scroller. First up we set up the screen offset and character source byte index values to be 0. The screen offset value is stored on the zero page in $fd. This value will have 40 added to it each time through the loop as we render each of the 4 characters on the right side of the screen. Because each character to render represents a 2×2 bit segment of the character, we need to look at two bytes of character data for each character we render to screen. The current bit mask is applied and the result shifted down until the 2 bits are in bits 0 and 1. This leaves us with a value between 0 and 3. This is the first part of the look up value and is multiplied by 4. Why 4? Remember earlier I said the order of the 16 characters we created was important? Here is why. Have a look at the 4×4 character table earlier in the post. Notice that the top two blocks on the first 4 characters are blank (this is 00). Then in the next row it’s 01, then 10 and then 11. This is 0, 1, 2 and 3. That’s why the order is important. This first component of the look up tells us which row of our custom characters to use. The next byte of character data is read, the same bit mask applied and value again shifted down to bits 0 and 1. The second calculated value is now added to the first, which gives us our completed index into the 4×4 character matrix. We add 64 since our 16 custom characters start at screen code 64. The value is then rendered to the screen. This is repeated another 3 times until the column has been rendered.
ldx #000 stx REG_ZERO_FD ; reset screen offset render_scroller_column lda C_CHARSET, x ; load source byte scroller_character_mask and #192 ; apply mask scroller_char_step ldy #255 beq skip_shift_1 ; no shift if masking bits 0 and 1 shift_scroll_mask_loop1 lsr ; shift down to bits 0 and 1 lsr dey bne shift_scroll_mask_loop1 skip_shift_1 asl ; mul by 4 - look up char matrix asl sta REG_ZERO_FE ; cache to recall shortly inx ; advance to next byte in char ram render_scroller_column2 lda C_CHARSET, x and scroller_character_mask + 1 ; apply current mask ldy scroller_char_step + 1 beq skip_shift_2 ; no shift if masking bits 0 and 1 shift_scroll_mask_loop2 lsr ; shift down to bits 0 and 1 lsr dey bne shift_scroll_mask_loop2 skip_shift_2 clc ; calc char code to use in 2x2 block adc REG_ZERO_FE ; grab offset calculated earlier adc #064 ; add offset to the character matrix scroller_render_offset ldy REG_ZERO_FD sta C_SCREEN_RAM + $31f, y ; render character to screen tya adc #040 ; advance offset for next loop pass sta REG_ZERO_FD inx ; advance next byte in char ram cpx #008 ; entire column rendered? bne render_scroller_column
All the hard work is now done. The final part of this routine advances the scroller step value and also shifts the bit mask to the right by 2 bits, ready to render the next column of the character. Then we’re done. We now have a routine that will generate a 4×4 scroller of the 1×1 font, rendering each character on the fly.
dec scroller_char_step + 1 ; advance step lda scroller_character_mask + 1 ; advance mask lsr lsr sta scroller_character_mask + 1
Hopefully I’ve explained this well enough for you to follow along. There are quite a few comments through out the source code too (more than shown in snippets above) which should help with further understanding of what’s going on.
You can view the source on GitHub here: https://github.com/0xc64/tutorials/tree/master/scroll4x4
The source and compiled .prg file can be downloaded here: scroller_4x4.zip
This is my first tutorial where I’ve used a code plug in to help colour the syntax (instead of posting images of the source). Hopefully this is more useful for people too.
That brings an end to the tutorial. Hopefully it’s been helpful 🙂 There are a number of things you can easily do to pretty this scroller up. Why not add an FLD (Flexible Line Distance) effect just before the scroller start to make it bounce? Or add colour cycling to the colour ram behind the scroller? Or use an inverse set of characters and render some raster bars behind it? Those are just a few ideas – there are plenty more. You can expand this example to render an 8×8 font easily enough too and animate the characters (see my intros “We Are” and “Ursa Minor” for examples of this).
If you want to contact me, there is a 0xc64 Facebook page here: 0xc64 Facebook and I’m also on Twitter here: 0xc64 Twitter
I’m hoping 2017 will be a more productive year for me on the C64 front – so hope to have more tutorials and content coming soon 🙂