Jump to content
IGNORED

cc65 DLI Graphics 0


gozar

Recommended Posts

You could use in-line asm, but the cc65 compiler can optimize that in-line code, sometimes doing things you don't want. I usually write routines like DLIs or VBIs in a separate assembly file and link to my project. Here's something I had laying around that I modified to do a simple DLI.

 

Main C code:

#include <atari.h>
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#define SAVMSC *(unsigned int *) 88         // Screen address
#define NMIEN  *(unsigned char *) 0xD40E    // NMI enable
#define SDMCTL *(unsigned char *) 559       // Antic DMA control shadow
#define SDLSTL *(unsigned int *) 560        // Display list start shadow
#define VDSLST *(unsigned int *) 0x200      // Display list interrupt vector
char dl[] = {112,112,112,66,0,0,130,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,65,0,0};
void dli(void);

void main(void) {
  unsigned char i;

  dl[4] = (unsigned char) (SAVMSC % 256);
  dl[5] = (unsigned char) (SAVMSC / 256);
  dl[sizeof(dl)-2] = ((unsigned) &dl) % 256;
  dl[sizeof(dl)-1] = ((unsigned) &dl) / 256;
  NMIEN = 0xC0;                         // enable dli + vbi
  SDMCTL = 0;                                       // turn off antic
  SDLSTL = (unsigned int) &dl;                      // new dlist address
  VDSLST = (unsigned int) &dli;                     // set dli vector
  SDMCTL = 34;                                      // turn on antic
  for(i=0; i<24; ++i) {
    gotoxy(0, i);
    cprintf("This is a string! %d", i);
  }
  while(1);
  return;
}

Assembly DLI routine:

  .export _dli
COLPF2  =   $D018
WSYNC   =   $D40A

  .code
_dli:           pha                     ; save registers
  lda #$40
  sta WSYNC
  sta COLPF2
  pla                     ; restore registers
  rti

Command line to compile/assemble and link:

cl65 -t atari -Osir vsdli.s testvs.c -o testdli.xex

testdli.xex

Edited by Shawn Jefferson
  • Like 2
Link to comment
Share on other sites

I do as Shawn does. However, If you do decide to embed, look up the asm function.

 

ex. -

ASM("LDA #22");

 

You can use replacement variables in the string and pass in certain values, kind of like printf works. Don't have the info handy, or I'd be more precise, but if you look through the cc65 docs for the ASM keyword, you'll see it.

Edited by danwinslow
Link to comment
Share on other sites

There are some problems with this code:

 

char dl[] = {112,112,112,66,0,0,130,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,65,0,0};

 

Display lists cannot cross a 1K boundary. This will fail randomly depending on where the compiler puts this array.

 

NMIEN = 0xC0; // enable dli + vbi

 

Crash if display list currently has DLI instructions in it, especially if it's garbage. This is most prone to occur if the active display list has been overwritten with garbage (sloppy loaders). DLIs should be shut off first, then the display list and DLI vector changed, then DLIs re-enabled.

 

SDMCTL = 0; // turn off antic

 

This only turns off DMA after vertical blank, and only if there isn't an IRQ running at that time (keyboard). To do this safely, you have to wait for vertical blank and then hit SDMCTL=0, DMACTL=0, or alternatively, use one of OS timers 3-5 to wait for stage 2 VBI to run.

  • Like 1
Link to comment
Share on other sites

Hi,

 

Some comments on your code, over the comments from Avery:

 

 

#include <atari.h>
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#define SAVMSC *(unsigned int *) 88         // Screen address
#define NMIEN  *(unsigned char *) 0xD40E    // NMI enable
#define SDMCTL *(unsigned char *) 559       // Antic DMA control shadow
#define SDLSTL *(unsigned int *) 560        // Display list start shadow
#define VDSLST *(unsigned int *) 0x200      // Display list interrupt vector

 

You can use the definitions already included from the atari.h header, accessing the registers like C structs for example, ANTIC.nmien = 0xC0;

 

Sadly, most of the OS variables are not defined, only IOCB and DCB.

 

 

char dl[] = {112,112,112,66,0,0,130,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,65,0,0};

 

The DLI can be writen using the header defines, DL_*, and as Avery says, you must ensure that the data don't cross 1k boundaries. There are two ways to do that, one is copying the array to a runtime-allocated buffer already aligned and the other is defining your own aligned segment, as:

#pragma data-name (push, "DISPLAY_LIST")
char dl[] = { DL_BLK8, DL_BLK8, DL_BLK8, DL_CHR40x8x1 | DL_LMS, 0,0,
              DL_DLI | DL_CHR40x8x1, DL_CHR40x8x1, DL_CHR40x8x1, DL_CHR40x8x1,
              DL_CHR40x8x1, DL_CHR40x8x1, DL_CHR40x8x1, DL_CHR40x8x1, DL_CHR40x8x1,
              DL_CHR40x8x1, DL_CHR40x8x1, DL_CHR40x8x1, DL_CHR40x8x1, DL_CHR40x8x1,
              DL_CHR40x8x1, DL_CHR40x8x1, DL_CHR40x8x1, DL_CHR40x8x1, DL_CHR40x8x1,
              DL_CHR40x8x1, DL_CHR40x8x1, DL_JVB, 0, 0 };
#pragma data-name (pop)
Note that you will need a custom linker script with the DISPLAY_LIST segment defined.

 

Assembly DLI routine:

  .export _dli
COLPF2  =   $D018
WSYNC   =   $D40A

  .code
_dli:           pha                     ; save registers
  lda #$40
  sta WSYNC
  sta COLPF2
  pla                     ; restore registers
  rti

 

Also, here you can include "atari.inc" and use the already defined equates, all the OS and hardware equates are there.

Edited by dmsc
Link to comment
Share on other sites

There are some problems with this code:

I knew there would be. :) It was test code I dug up quickly... Maybe I shouldn't have posted it as I wouldn't want to pass on too many bad ideas!

 

Display lists cannot cross a 1K boundary. This will fail randomly depending on where the compiler puts this array.

Yes... Some projects I've written I've manually adjusted the location of the data segment to make sure it didn't cross a 1k boundary. This code doesn't I believe... you do have to keep that in mind, for sure. :) Use the linker config for that in cc65.

 

Crash if display list currently has DLI instructions in it, especially if it's garbage. This is most prone to occur if the active display list has been overwritten with garbage (sloppy loaders). DLIs should be shut off first, then the display list and DLI vector changed, then DLIs re-enabled.

 

This only turns off DMA after vertical blank, and only if there isn't an IRQ running at that time (keyboard). To do this safely, you have to wait for vertical blank and then hit SDMCTL=0, DMACTL=0, or alternatively, use one of OS timers 3-5 to wait for stage 2 VBI to run.

To safely change the display list with DLIs are you saying you pretty much have to do it in a VBI?

 

So that I know better for any future code I may write, what would the safe process look like to enable a display list with DLIs?

Edited by Shawn Jefferson
Link to comment
Share on other sites

 

To safely change the display list with DLIs are you saying you pretty much have to do it in a VBI?

 

Thinking about it some more, yeah. It doesn't need to be in the vertical blank interrupt, but it needs to be within a single vertical blank. There are too many ways for it to break if some of the changes happen mid frame or span more than one frame.

 

Let's take the first case with DLIs being enabled first. Assume that we turn off the display first with SDMCTL=DMACTL=0, wait for vertical blank to set NMIEN=0xC0, and then let the stage 2 VBI restart the new display list by copying SDLSTL/SDLSTH -> DLISTL/DLISTH and SDMCTL->DMACTL. If that doesn't happen that frame because stage 2 VBI is skipped, we can crash. Why? Because ANTIC can fire DLIs even if display list DMA is disabled, if the last display list instruction that was read had the DLI bit on. DMACTL bit 5 only disables display list fetch and not the display list. DLIs firing randomly = badness. Therefore, turning on DLIs first is not safe.

 

Okay, so what if we swap it around so the display list is set up first, and then enable DLIs? Well, the first problem here is that stage 2 VBI can be skipped, so to actually enforce this ordering you'd need to wait for it, such as by setting up and polling OS timer 3. Even then, if you don't immediately enable DLIs before VBLANK ends, you've got part or all of a frame where the display list is running without its DLIs. At a minimum that will look like an ugly glitch, and it can screw up the display list for the next frame and lead to a DLI crash. So we're not any better off this way.

 

So, what we're left with is that we definitely need to flip on DLIs and swap the display list in the same frame. The easiest way to do that is to wait for vertical blank, do the required register changes manually, and modify the shadow variables at the same time so that if/when stage 2 VBI runs it doesn't screw things up:

// Wait for vertical blank.
// This loop will end either be immediately before the VBI
// or just after it. If it stops before, the VBI will run
// a few cycles later, so everything is still good.
while(VCOUNT < 124)
    ;

// change the display list interrupt vector, now that DLIs are
// off
VDSLST = (unsigned)my_dli;

// change the display list
// - safe to do this without protection around the lo/hi byte
//   writes since we're in vertical blank, and if the VBI
//   happens to run in between, we'll re-write the hardware
//   registers afterwards with the correct values
SDLSTL = (unsigned)display_list;
DLISTL = (unsigned)display_list;

// enable DLIs
// - safe to do as we are still in vertical blank and the
//   display list hasn't started yet
NMIEN = 0xC0;

// re-enable the display
// - can't rely on stage 2 VBI to do copy to DMACTL as (a) it's
//   already run by now and (b) we *HAVE* to start the display
//   list next frame
SDMCTL = 0x22;
DMACTL = 0x22;

Most programs have low enough VBI or IRQ overhead that they can rely on this routine running atomically within VBLANK (22 scanlines minimum). In a game with a long VBI or faster timer IRQs running, it may be necessary to do this within the VBI. However, in that case you've probably already swapped out the OS VBI handler anyway.

 

It's a pain to get all this correct, but I see a lot of random failures in the wild from programs that don't robustly turn DLIs on or off, especially when loaded from bad loaders that let the active display list get trashed before running a program. You really don't want to try to debug the mess that occurs when DLIs run with the wrong timing and start nesting.

 

By the way: if you absolutely want to make the first screen switch of your program bulletproof, even this isn't quite enough -- you need to swap in a blank frame first. Hint: vertical scrolling.

 

  • Like 1
Link to comment
Share on other sites

hah! That'll teach ya to post code, Shawn!

 

Phaeron had some interesting things to say about actual runtime problems with the code. DMSC, that's a cool alignment idea, you are assuming that linker script would manually set the address for the segment to an aligned address, correct? The personal style issues that have no bearing on the correctness of the program I probably would have left out.

 

Anyway, I'm glad it was you Shawn, my code would have been much worse.

Link to comment
Share on other sites

Hi!

 

DMSC, that's a cool alignment idea, you are assuming that linker script would manually set the address for the segment to an aligned address, correct?

Yes, you add to the linker config, in the SEGMENT section, a line like:

    DISPLAY_LIST:     load = RAM,     type = rw, define = yes, align = $400;
The line must be after INIT and before BSS (if not, the C library would be confused), the best place is just after the DATA section.

 

Note that the linker script does not support multiple XEX headers in the generated file, so all the space before the section will be zero-filled in the resulting XEX. Also, if you have multiple data with address restrictions, it is best to define new RAM sections with specific sizes so that you can manually fit data to sections, with the linker verifying that the data fits.

 

Linker configs are a complicated, but adds a lot of flexibility to the compilation.

Link to comment
Share on other sites

hah! That'll teach ya to post code, Shawn!

:)

 

I look at it this way... I learned something that I wouldn't have learned if I hadn't posted that code, and I hope the OP's question was answered as a result. I did post it with some trepidation, since most of the coders here are way, way ahead of me. (Wait till I release the code from the Moria port I'm doing... that will give some folks a laugh I'm sure.)

 

 

The personal style issues that have no bearing on the correctness of the program I probably would have left out.

Those were all good points too, I always ignore those equates and header files that are there to make life easier... the technical issues around safely setting up a display list with DLIs are much more interesting to me though.

 

Anyway, I'm glad it was you Shawn, my code would have been much worse.

You've got the next one. ;)

Edited by Shawn Jefferson
Link to comment
Share on other sites

  • 4 months later...

Added a very basic DLI to the little "hello world" I've been working on (in short stints, once every 6-18 months, it seems) since August 2013 :) http://newbreedsoftware.com/atari/hiworld/

 

One thing I found was that the screen went 'wonky' if I fiddled with DMACTL and SDMCTL. If I just left them as-is, everything worked fine. (I added the "while (VCOUNT<124)" trick, to avoid being another "in the wild" case of something crashing). Thanks Phaeron!

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...