We are used to inserting small routines at the beginning of the default interrupt handler as an IRQ wedge. This has an advantage of being easy and not requiring any modifications to the part of memory occupied by the ROM.
But the default handler is responsible for cursor blinking, handling the screen editor and few other things. As we don't usually need them in our sprite animation, the default handler's code is just a waste of cycles and memory for us.
We have a simple routine wedged into the interrupt handler. It changes the border and the background color, executes a hundred nops and changes the colors back.
This allows us to see how much raster time it takes to run this routine.
If we change colors in the main loop, we will be able to inspect the time needed by the default handler to run until it returns control to the main routine.
:mov #GREEN; border_color
:mov #LIGHT_GREEN; background_color
It seems that it takes slightly more time than our routine, which means it wastes a bit more than 200 hundred cycles.
But as soon as we press a key on the keyboard, the default handler executes an additional routine which wastes even more time.
That's a quite a lot of cycles to save if we get rid of that additional code.
But what about the part that runs before our handler starts. To figure it out, we need to remind ourselves how the interrupt process works.
When the IRQ signal occurs, the CPU reads the address at the last two bytes of the memory and jumps to it.
By default, the address points to a routine somewhere in the KERNAL code, which stores register values on the stack, checks whether the IRQ or BRK signal caused the interrupt and then jumps indirectly to the appropriate handler through the jump-table in the RAM.
The last part is what allows inserting IRQ wedges into the handler, but it also wastes few cycles. Let's see how many exactly.
We'll be modifying the address in the last two bytes of the memory. This area is occupied by KERNAL ROM, so we'll need to copy and disable ROM's entirely.
:copy_rom_pages($a000, $bfff)
:copy_rom_pages($e000, $ffff)
lda $01
and #%11111101
sta $01
Now we need to create a small routine that will be executed before the IRQ handler. It will just change border and background colors and jump to the actual handler.
before_irq_handler:
:mov #LIGHT_GRAY; border_color
:mov #GRAY; background_color
jmp $0000
As we don't know the jump's address yet, we'll mark it with a label.
.label jump_destination = * + 1
Now, we will copy the address from the end of the memory, and replace it with the address of our routine while interrupts are disabled.
:mov16 $fffe; jump_destination
:mov16 #before_irq_handler; $fffe
With that in place, we can see that the initialization code takes almost one whole raster line. The gap in the background color change at the beginning is caused by a few cycles it took to execute lda and sta instructions.
Getting rid of the interrupt handler code is now trivial.
Instead of injecting the address of our routine indirectly into the irq_vector, we can put it at the last two bytes of the memory.
:mov16 #irq1; $fffe
We can also remove unnecessary timing related code.
:mov16 $fffe; jump_destination
:mov16 #before_irq_handler; $fffe
before_irq_handler:
:mov #LIGHT_GRAY; border_color
:mov #GRAY; background_color
.label jump_destination = * + 1
jmp $0000
And instead of jumping into the default handler, we will execute the rti
instruction to return from the interrupt.
rti
It works, and we saved a lot of cycles. Sadly, in most cases, we will need to add a bit more code to the beginning and the end of an interrupt.
As we remember, only the program counter and the status register are saved and recovered automatically.
Accumulator, X, and Y registers are not. If we don't save and restore them in an interrupt, we are going to overwrite them and cause unexpected bugs in the main code.
Consider this situation. In the main code, we will load index registers with colors.
ldx #GREEN
ldy #LIGHT_GREEN
Then in the loop, we will store their values into the border and background color registers.
stx border_color
sty background_color
If we change their values in the interrupt handler and run the program, we can see that they have been overwritten.
ldx #GRAY
ldy #LIGHT_GRAY
In general, we need to make sure that our interrupt code saves and restores values of any registers that are changed within.
One way to do that is to save them on the stack. First, we save the accumulator, then both X and Y in any order.
pha
tya
pha
txa
pha
Now at the end of the routine, we need to restore them starting from the one that was saved last.
pla
tax
pla
tay
pla
But this is not the only way to save the state.
We can also save all values in temporary addresses in the memory.
sta atemp
stx xtemp
sty ytemp
Now we can load them in any order.
lda atemp
ldy ytemp
ldx xtemp
rti
atemp: .byte $00
xtemp: .byte $00
ytemp: .byte $00
This solution is slightly slower unless our temporary addresses lie in zero page.
But if we can make the code self-modifying we can make the restoring part even faster by changing lda instructions into an immediate mode and pointing temporary addresses right into their arguments.
.label atemp = * + 1
lda #$00
.label xtemp = * + 1
ldx #$00
.label ytemp = * + 1
ldy #$00
When we have multiple interrupt routines, we need to inject their addresses right at the end of the address space instead of the irq_vector address used when inserting it as a wedge.
:mov16 #irq2; $fffe
:set_raster(180)
irq2:
sta atemp
stx xtemp
sty ytemp
:mov #BROWN; border_color
:mov #ORANGE; background_color
ldx #GRAY
ldy #LIGHT_GRAY
.for (var i = 0; i < 100; i++) {
nop
}
:mov #LIGHT_BLUE; border_color
:mov #BLUE; background_color
:mov16 #irq1; $fffe
:set_raster(140)
lda #%00000001
sta vic2_interrupt_status_register
.label atemp = * + 1
lda #$00
.label xtemp = * + 1
ldx #$00
.label ytemp = * + 1
ldy #$00
rti
When dealing with multiple interrupt handlers, it might be useful to extract the last part of the code into a subroutine and just jump to it.
This way we will add three cycles to every call, but we will also save four bytes on each routine.
Those numbers will add up and can potentially make a huge difference. So it's pretty useful to know these tricks.
See you soon!