Jump to content
IGNORED

CC65 - writing a DLI tutorial


Recommended Posts

  • 2 years later...
On 6/9/2019 at 10:58 PM, Yaron Nir said:

Hi All,

As part of my contribution to the cc65 development for the atari 8bit community,
I've decided to write a short tutorial that might help you guys if you wish to learn how to setup a DLI service in your app.

 

I assume you are already familiair with terms such as Display List, WSYNC, VBLANK, NMIEN, raster beam, scanline, clock cycles, HW registers, shadow registers, DMA.

If you're not, you can refer the book 'De-Re-Atari' which has some good explanations and tutorials for these terms.

 

What is a DLI?

DLI simply stands for Display List Interrupt. When the raster beam reaches the right part of the current scanline of the display screen it is turned off. during that time, there is an oppertunity to perform some graphics changes that enahce the display. This oppertunity is implemented through the usage of a display list interrupt.

 

What can I do with a DLI?

With the DLI you can achieve some really cool graphics enhancements which includes:

- A colorful screen. You set different colors in different scanlines. for example you wish to draw a sky which is combined out of different shades of blue.

- Multiple character sets. have multiple character sets appearing together on the same screen, for example, you have a top menu bar that display the remaining lives, time to finish level and score - all which are combined out of a specific character set, and below the top menu bar, you have the leve screen itself which is combined out of a different character set.

- Player Missle Horizontal position. The PMG horizontal position can be controlled for each scanline. for example, in your game you collect objects which are made out of players. the objects can be drawn several times on screen (not on the same scanline though) so one player can be shown as multiple object on screen in different horizontal positions.

- Players width and priority. mostly used with priority masking to blend with background to get additional colors. for example, the background is drawn overlayed with player 0 and because of the priority register, the background gets the color of the player.

 

What actually happens when a DLI is executed?

A DLI execution is broken into 3 phases:

Phase 1 - Covers the period of time that passes from the begining of the DLI routine that is being invoked till the horizontal sync instruction is reached (WSYNC). during this phase, the raster beam is drawing the last scanline of the interrupt mode line. it is not recommended to do graphics changes in this phase.

Phase 2 - Covers the period of time that passes from the WSYNC till the raster beam re-appears on screen. This phase corresponds to the horizontal blank event. This is the most important phase of the DLI execution, as soon as the raster beam is shut off, all the graphics changes should be made.

Phase 3 - Covers the period of time that passes from the re-appearance of the raster beam on screen till the end of the DLI routine.

 

Note: Graphics changes must be done in phase 2 of the routine. if not, it can affect your DLI timing and you can observe some unwanted graphics appearing on screen. For example, the raster beam changes color at the middle or towards the end of the scanline rather than changing the entire color of the scanline itself. The graphics changes should be done fast before the raster beam is "waking" up again. that is why the dli routine must be short and fast.

 

What does the DLI routine code looks like?

The dli routine is a simple C routine that returns void and accept void as a parameter. For example:

void dli_routine(void)

DLI routine thumb rules

The are few thumb rule for implementing the dli routine:

1. It has to be short and fast. If you need to do several operations on screen, it is best to break the DLI service into several dli routines that are place in a vector.

2. As the OS interrupt routine saves no registers, you must save the registers by pushing them onto the stack at the beging of the routine, and restore them by pulling them from the stack at the end of the routine. Only the Processor Status Register is being pushed automatically by the OS, but you are likely not using it in your routine.

3. You must work with the graphics HW registers rather then the shadow registers. for example the background color HW register is 53274 ($D01A) where its shadow register is 712 ($2C8).

It is important to understand that on each vertical blank (VBLANK), the OS shadow process will wipes out the HW register.

6. You must return the control back by exiting the dli routine properly. This must be done in CC65 inline asm command using the "rti" instruction.

 

You will have to write your dli routine either entirely or partially using CC65 inline asm. saving and restoring registers, and return the control must be done in asm. so begining and end of routine is inline asm. the middle part can be inline asm (preferred) but can also be in C.

It is best that once your dli routine is in place, to look how the produced asm code from that looks like and to see that there are no mistakes. (this can be done by compiling individually your c file and observe the result object *.s file which is basically an asm file). 

 

DLI routine written entirely in cc65 inline asm

Here is an example of a dli routine that alters Player 0 (PMG) color:

void dli_routine(void)
{
	asm("pha");

        asm("lda #33");

        asm("sta $D40A");

        asm("sta $D012");

        asm("pla");
	asm("rti");
}

The routine above is written entirely in CC65 inline asm. it is short and fast.

Let's break the dli routine down:

- 1st line saves the accomulator register by pushing it onto the stack. why do we need to save the accomulator register only and not the other X and Y registers, well the simple answer is that we are doing so because only the accomulator register is participating in this dli routine (lda, sta instructions), had I used X and Y registers in the routine, I would have definately save and restore them as well.

- 2nd line prepares the new color of the player. It loads it into the accomulator register.

- 3rd line is the wait for the horizontal sync or what is normally called WSYNC event. this is to wait until the raster beam finishes through the entire scan line and shuts off. that is the time to do the graphics changes.

- 4th line, it is time to do the graphics changes. we already have the accomulator loaded with the new color value so now we have to store it in the player 0 HW register ($D012 = 53266). remember HW register and not the shadow one ($2C0=704).

- 5th line, is to restore the accumolator register back from the stack

- 6th line, returning from the dli routine. must be included in all dli routines.

 

Same code written in C

As said earlier, part of the code in the dli routine can be written in C. but part of it must remain in CC65 inline asm as you can't write it in C.

Here is the same code from above, but now in ?

void dli_routine(void)
{
	asm("pha");

        *(unsigned char*)0xD012 = *(unsigned char*)0xD40A  = 33;

        asm("pla");
	asm("rti");
}

Here is the produced asm file afer compiling the dli routine:

; ---------------------------------------------------------------
; void __near__ dli_routine (void)
; ---------------------------------------------------------------
.proc	_dli_routine: near
	pha
	ldx     #$00
	lda     #$21
	sta     $D40A
	sta     $D012
	pla
	rti
.endproc

If you look carefully at the produced code above you will notice that a new instruction was added: ldx #$00. why is that, why do we need it and what is it used for?

Clearly it is not used for and not need, but the compiler and mainly the optimizer decided to produce it as it has sets of rules that also depends on the params that we on the stack prior to the call to the dli routine.

So in order to remove this ldx #$00 instruction we will need to save and restore the X register, to and from the stack, as well.

 

The revised routine will now look like this:

void dli_routine(void)
{
	asm("pha");
	asm("txa");
	asm("pha");

	*(unsigned char*)0xD012 = *(unsigned char*)0xD40A  = 33;

	asm("pla");
	asm("tax");
	asm("pla");

	asm("rti");
}

And the produced asm file will now be right and look like this:

; ---------------------------------------------------------------
; void __near__ dli_routine (void)
; ---------------------------------------------------------------

.proc	_dli_routine: near
        pha
	txa
	pha
	lda     #$21
	sta     $D40A
	sta     $D012
	pla
	tax
	pla
	rti
.endproc

Now the ldx #$00 is gone.

That is why it is always important to observe the generated asm code produced from the C code and make sure everything is in the right place.

 

Now that we have the dli routine in place, we need to set the dli vector to point to it.

To point to our dli routine simply write the following code:

OS.vdslst = &dli_routine;

This is how the vdslst is defined in the file _atarios.h:

    void (*vdslst)(void);                   // = $0200/$0201    DISPLAY LIST NMI VECTOR

It is corresponding to the addresses $0200,$0201 / 512,513 where you normally points to your display list interrupt routines.

 

Next step is to set the vertical point on screen you wish your routine to work on. for example, if you wish to have a colorful sky that start in screenline number 7 then you need to set the dli flag in the screen display list.

 

DLI screenline minus 1

Remember that phase 2 of the DLI execution, is where you do all the graphics changes. This is after a WSYNC has occured. It means that it will take affect when the raster beam re-appears on screen and that is going to be happening only in the next scanline (as the current scanline is finished being drawn).

If you work in Antic mode 4 (char mode) , which every screen line is a 8 scanlines, so each DLI flag you set in your display list, will refer to 8 scanlines of the raster beam on screen.

If you work in Antic mode E (bitmap mode), which every screen line is 1 scanline, so each DLI flag you set in your display list, will correspond to 1 scanline of the raster beam on screen.

It is important for you to understand in which screen mode you are working in, and what is the relationship between the screen line and the scanline of that screen.

 

Back to the colorful sky example, if your colorful sky begins at screen line number 7, you will have to set the dli vertical position on screen to be at screen line number 6.

 

Here is an example of a display list where the DLI instruction is set in the 8th screenline, the actual affected screen line will be screenline number 9:

unsigned char DisplayList[] = 
{
	DL_BLK8,
	DL_BLK8,
	DL_BLK8,
	DL_LMS(DL_CHR40x8x4),
	0,
	0,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_DLI(DL_CHR40x8x4),
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,
	DL_CHR40x8x4,	
	DL_CHR40x8x4,	
	DL_CHR40x8x4,	
	DL_CHR40x8x4,	
	DL_JVB,
	0,
	0
};

In the above example, dli is set using the "DL_DLI" constant and once the raster beam start its work on screenline number 9, the dli routine will be called.

 

Your dli is now setup and ready to go, all you need to do is enable it.

To enable your dli you will need to set the bit D7 of the NMIEN register at the address $D40E (54286).

The NMEIN register is part of the OS NMI which is short for None-Maskable-Interrupts.

These interrupt can not be "masked" (disabled) at the 6502 level. NMI interrupts (except SYSTEM RESET) can be disabled by ANTIC.

OS NMI handler is what handles the dispaly list interrupt.

 

Here is a sample c code to enable the dli:

*(unsigned char*) 0xD40E = 192;

192 is bit D7.

Alternatively you can use the predefined NMIEN flag in _antic.h file as follows:

ANTIC.nmien = NMIEN_DLI | NMIEN_VBI; 

The NMIEN_DLI and NMIEN_VBI are defined in _antic.h as follows:

...
#define NMIEN_DLI   0x80
...
#define NMIEN_VBI   0x40 

and 0x40+0x80 = 0xC0=192 = bit D7.

 

As soon as NMIEN bit D7 is enabled, the dli routine starts to work and now whenever the raster beam will reach to screenline number 9 it will change the color of player 0.

This will go on and on until the dli will be disabled.

 

When you no longer need the dli routine to work, for example, if the game is over and you want to switch your screen from the level screen (where the dli routine works) to the game over screen (where it should be disabled), you will need to disable the dli.

To disable the dli routine you will need to remove bit D7 from the NMIEN register.

 

NMIEN is a write-only register and can't be read back. This means you can't just remove the bit with a simple bitwise operator. 

Instead, you can just set the NMIEN register to have the NMIEN_VBI bit only and by that the D7 bit will be remove by itself. 

 

Here is an example code to disable the dli:

ANTIC.nmien = NMIEN_VBI;

When do I enable/disable my DLI routine

You have to be very careful when you enable your dli. once it is enabled the routine starts to work immediately and if you didn't time it properly you will start see things on screen you did not expect as the raster beam keeps working continuesly.

Good practice would be to enable your dli routine only after an entire screen frame was processed succesfully.

To tell that a frame has been processed, you need to wait for VBLANK to occur. a VBLANK (a.k.a vertical blank) occurs after the raster beam finished scanning the entire screen from the top left to the bottom right. the vblank is normally used by the OS to do housekeeping, but it can be also used for the programmer to do some graphic calculation or set some handlers in place.

one handler is our dli routine.

To do enable the dli routine after VBLANK you either:

- Set up a VBI interrupt handler or

- Use a trick to wait on atari clock at location 20 which indicates the VBLANK has occured.

 

As this tutorial doesn't cover VBI (I will write a new tutorial dedicated to VBI in the near future), For this example I will use the second approach (address 20 clock trick).

 

Here is an example code to enable your dli routine :

void wait_for_vblank(void)
{
    asm("lda $14");
wvb:
    asm("cmp $14");
    asm("beq %g", wvb);
}

void main(void)
{
   ...
   ...
   wait_for_vblank();
   ANTIC.nmien = NMIEN_DLI| NMIEN_VBI;

   ...
   wait_for_vblank();
   ANTIC.nmien = NMIEN_VBI;   
}

The wait_for_vblank above is written entirly in inline asm. it could have been written in C with same results:

unsigned char currClockFrame = 0;

void waitForVBLANK(void)
{
    currClockFrame = OS.rtclok[2];
    while (OS.rtclok[2] == currClockFrame);
}

Multiple DLI routines

A good practices can be using multiple DLI routines. 

This is typically powerful when you have several points on screen you want to do some graphics enhacements. each DLI routine handles a differnt point of enhacement individually. 

 

These routines will be part of a display list interrupt vector. the vector will hold the sequence on which these routines will operate. 

You can control the order of the routines in the sequence. 

Let's take an example of 2 dli routines, each routine changes a graphics HW register separetly.

To process the 2 dli routines in a sequence you need the enter the proper sequence to the dli vector.

It is done by pointing to the 2nd routine inside the 1st routine, and pointing to the 1st routine inside the 2nd routine.

 

Here is a sample code that uses 2 dli routines in a sequence:

void dli_routine1(void)
{
	...
        ...
	OS.vdslst = &dli_routine2;
	...
        ...
}

void dli_routine2(void)
{
	...
        ...
	OS.vdslst = &dli_routine1;
	...
        ...
}

A working example

As a feedback I got while posting this tutorial, I have decided to work on an example that shows how powerful DLI really is and what it can be used for and how.

 

The example below is create 2 PMGs on screen (Player 0 and Player 1).

out of the 2 players (which uses a 3rd color) 3 objects are create: a star, a bird, and an ant-eater.

Each has its own horizontal position and color. 

The star is static, the bird and ant-eater are moving horizontally.

 

The example uses 3 DLI routines (each routine for each object).

Each routine controlls the HPOS and color of each object. 

 

dli.rar 3.81 kB · 121 downloads

 

Feel  free to leave any feedback or comments.

 

cheers,

Yaron

 

dli.rar 3.81 kB · 140 downloads

 

Thanks for this excellent example, it is really the first one in CC65 that I have understood. I just have one question, if I wanted to draw some characters in the background, where would I insert them in the code? For example, if it was for the layout of a platform game. I am not sure where "phase 2" corresponds to in the code.

 

Sorry for the newbie question.

 

 

Edited by atarilux
  • Thanks 1
Link to comment
Share on other sites

  • 2 weeks later...
On 12/27/2022 at 9:54 PM, atarilux said:

Thanks for this excellent example, it is really the first one in CC65 that I have understood. I just have one question, if I wanted to draw some characters in the background, where would I insert them in the code? For example, if it was for the layout of a platform game. I am not sure where "phase 2" corresponds to in the code.

the basic concept of a game is always the same.

 

you have a main loop and inside you do the same following routines (code is in assembly):

main_loop
      jsr erase
      jsr update
      jsr draw
      jmp main_loop

 

(jsr = jump subroutine, jmp = jump subroutine )

 

 

so drawing a character on the background of the screen will be done in the draw routine.

if that character moves, then you also must erase it and update its position these are done in the erase and update routine respectively 

 

Link to comment
Share on other sites

@Yaron Nir thanks again for your excellent original post and now your reply. The game loop aspect I have not real problem with, I have that working for drawing, movements and scores - although not complete. My issue was how to get a DLI working inside such a loop. I probably need to understand the process a bit better.

 

Right now I have a rather boring monochrome level display, hence the desire for a DLI. 

Edited by atarilux
Link to comment
Share on other sites

18 minutes ago, atarilux said:

My issues was how to get a DLI working inside such a loop. I probably need to understand the process a bit better.

What I've done when wanting to use DLI for something other than colour changes is do any processing for the DLI that you can

in your main loop (probably in a VBL), set a flag(s) for the DLI's to use, the DLI can reset the flag(s) when done.

 

That way it's all synchronised, pre-processed changes done before the DLI's operate.

Edited by TGB1718
Link to comment
Share on other sites

18 minutes ago, atarilux said:

@Yaron Nir thanks again for your excellent original post and now your reply. The game loop aspect I have not real problem with, I have that working for drawing, movements and scores - although not complete. My issues was how to get a DLI working inside such a loop. I probably need to understand the process a bit better.

 

Right now I have a rather boring monochrome level display, hence the desire for a DLI.

think about DLI as something that happens in parallel. that means that the DLI "interrupts" the main thread and does it job.

DLI works regardless of the main thread loop....

 

In order for the DLI to work you need to set the NMI (non-maskable interrupts) flag with the DLI flags.

remember that the DLI is based on just few steps:

1. You choose in display list what scanmode you wish to set your interrupt with 

2. You write your own routine to do whatever you want when that interrupt is invoked (change colors, move HPOS of sprites, other things..)

3. You point the DLI vector to your routine

4. you enable the DLI flag in the NMI

that is it

 

hope it clarifies better

 

let me know if you need anything else

 

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...