Jump to content

Recommended Posts

This is quite an important sequence for the CX16 to handle interrupts 🙂.

At the start:

    asm {
        lda $9f21
        pha
        lda $9f20
        pha
        lda $9f24
        pha
        lda $9f23
        pha
        lda $9f22
        pha
        lda $9f25
        pha
    }
 

At the end:

        pla
        sta $9f25
        pla
        sta $9f22
        pla
        sta $9f23
        pla
        sta $9f24
        pla
        sta $9f20
        pla
        sta $9f21

 

Learning by doing ...

Link to comment
Share on other sites

On 5/25/2022 at 11:49 AM, SlithyMatt said:

Um... that doesn't seem like a good idea. You could mess up the display or the expected stride by writing to the data ports. Better idea is to only write to VRAM inside the interrupt.

I think that's why he recommended snapshotting it to the stack first. I made a special wrapped version of the music playback routine in Zsound because it stomps all over the VERA ports too. I recommend against advancing the music during IRQ but if you must, here's a safe function wrapper that takes a few dozen extra cycles to accomplish.... have a nice day.

😉

 

  • Like 1
Link to comment
Share on other sites

Actually it is very tricky ... 

So indeed, during interrupt i first push these vera registers to the stack, and later before interrupt exit, i pull the registers from the stack.

However, I made a mistake ... The DATA registers should NOT be pulled from the stack as the ADDRSEL in CTRL configures if ADDR0 or ADDR1 is addressed.

So when you pull DATA0 and DATA1 from the stack, it messes up the data originally in the vera.

So I have just removed the push and pull of DATA0 and DATA1.

 

image.thumb.png.0aa6f6f1c92bf27f037d38d9946bcbbf.png

 

At the start of the interrupt routine:

        lda $9f20
        pha
        lda $9f21
        pha
        lda $9f22
        pha
        lda $9f25
        pha
 

At the end of the interrupt routine:

        pla
        sta $9f25
        pla
        sta $9f22
        pla
        sta $9f21
        pla
        sta $9f20

 

  • Like 2
Link to comment
Share on other sites

Posted (edited)

One thing to note: The above code backs up the currently-selected data port.
If your code then proceeds to use the one that was not active, you'd be screwed.

If you just want to back up and use a specific data port, then return to current state:
; save the current port selection
        lda $9f25
        pha
 ; switch to the desired data port
        lda #desired_data_port_number
        sta $9f25
; back up the state of that data port
        lda $9f20
        pha
        lda $9f21
        pha
        lda $9f22
        pha
        ; Your code here
        ; Use ONLY the desired data port - be sure to leave $9f25 alone during this execution
; restore state of the used data port
        pla
        sta $9f22
        pla
        sta $9f21
        pla
        sta $9f20
; return active port selection to previous state
        pla
        sta $9f25

If you want to back up and use both ports:

; backup currently-active data port
        lda $9f20
        pha
        lda $9f21
        pha
        lda $9f22
        pha
        lda $9f25
        pha
; switch to the other port....
        eor #1
        sta $9f25
; ... and back it up as well
        lda $9f20
        pha
        lda $9f21
        pha
        lda $9f22
        sta
        lda $9f25
        pha

        ; your code goes here. Feel free to use both ports as required.

; restore both data port addresses and previous selected-port state
        pla
        sta $9f25
        pla
        sta $9f22
        pla
        sta $9f21
        pla
        sta $9f20

        pla
        sta $9f25
        pla
        sta $9f22
        pla
        sta $9f21
        pla
        sta $9f20


Note that the order of the CTRL register in these two examples is different!

The first example pushes CTRL first because this guarantees the final write of the restore will be to select the correct active port.
It then switches to the desired data port (which may be the same one that was already active, but it takes more code to test than to just blindly swap to the same port) and backs up the address of the desired data port.
When the restore begins, you'll still have the port you borrowed as the active one, which will be pointed back to wherever it was by the first 3 pops, and lastly, the selected port state is restored.

The second example puts the CTRL register on the stack after the three address selection registers because that way, when it restores, it will first select the previously-not-selected port, restore it, then swap back to the previously-selected port and restore that, leaving it in place as the active port just as it was when your routine was called.

Obviously, if you intend to use the DCSEL registers, you'd want to tuck those into your backup/restore logic.
EDIT: This is incorrect. If you write to those registers, it's because you WANT those changed, so why would you back them up and restore them?
It's completely fine to manipulate DCSEL during these two examples so long as in example 1, the ADDRSEL bit is correct before doing the restore.

Edited by ZeroByte
Fix an erroneous statemnt....
Link to comment
Share on other sites

I'd avoid doing anything with VERA or the RAM/ROM bank pointers during IRQ. Instead I have IRQ set a flag bit, and have the main program do regular checks of such flag bits. Whenever MAIN detects the flag bit set it resets it and handles whatever I need to do in VERA or the RAM banks. It might take a millisecond longer to get around to doing whatever subroutine, but it makes the custom IRQ much faster and there's no danger of messing up VERA addresses or RAM bank pointers this way.

Link to comment
Share on other sites

On 5/26/2022 at 9:32 PM, Ed Minchau said:

Instead I have IRQ set a flag bit, and have the main program do regular checks of such flag bits. Whenever MAIN detects the flag bit set it resets it and handles whatever I need to do in VERA or the RAM banks.

So ... You would put all the video painting in the main loop while in the IRQ routine, you would put the coordinates calculations and game AI logic?

Concerning the banking it really depends what memory layout you have... I mean if you have moved half of your game engine memory into a bank, then during IRQ banking is required in bram. 

 

Link to comment
Share on other sites

On 5/26/2022 at 2:46 PM, svenvandevelde said:

So ... You would put all the video painting in the main loop while in the IRQ routine, you would put the coordinates calculations and game AI logic?

Concerning the banking it really depends what memory layout you have... I mean if you have moved half of your game engine memory into a bank, then during IRQ banking is required in bram. 

 

I'd have the IRQ do as little as possible. For Asteroid Commander it just updates a couple of bytes set aside for flag bits. The main program then checks those bits a couple hundred times a second, each time a major subroutine is finished and at least once for every trip through the main loop.

Link to comment
Share on other sites

On 5/26/2022 at 2:32 PM, Ed Minchau said:

I'd avoid doing anything with VERA or the RAM/ROM bank pointers during IRQ. Instead I have IRQ set a flag bit, and have the main program do regular checks of such flag bits. Whenever MAIN detects the flag bit set it resets it and handles whatever I need to do in VERA or the RAM banks. It might take a millisecond longer to get around to doing whatever subroutine, but it makes the custom IRQ much faster and there's no danger of messing up VERA addresses or RAM bank pointers this way.

I have to disagree.

VBLANK is exactly what this kind of thing is for, and waiting for the Kernal to finish polling the keyboard before you get another crack at doing anything is very risky of bleeding out into the visible area and getting tearing artifacts and such.
The VBLANK IRQ can look for a signal value from the main loop that a frame is done, and only do no-op if that flag is not set. If the flag is set, then you know the program is sitting in an idle loop and you won't break anything by manipulating VERA.

Main loop code should always assume dirty state for the data ports anyway, (i.e. always start by configuring the port address) and since you know IRQ won't manipulate the port unless you signaled "frame done", it's safe to have a long loop for loading into VRAM, etc. because you haven't written the "update frame" flag.

I've seen the VBLANK IRQ take up to several characters' (not pixels, but characters) worth of visible raster time to complete and return to the main loop, depending on what the keyboard is doing.

Yes, it takes precious cycles away to do all this pushing VERA onto the stack, but that's why you try to avoid things like using both ports, etc. Flappy Bird is written in C, and it doesn't even push VERA onto the stack during IRQ, and it writes the sprite shadows into VERA during VBLANK and it never crashes.

I am not a super-coder or anything, and don't claim to know it all, and I'm sure there's all kinds of ways to get things done, but my understanding has been what I've just stated here.

Link to comment
Share on other sites

To clarify - I'm not saying Ed's strategy doesn't work for several types of programs as well, but for games or things with lots of movement, you really need to do your VERA writing during vblank to avoid tearing and artifacting. If you're driving a menu system or other relatively-static UI, then yeah, you can do all the VERA writing during main execution and just use the vsync IRQ as a timer that bumps a flag once per frame so your program can stop and wait for that value to change in order to begin drawing the next update.

I think the real thing to consider is that you probably need to decide whether you're doing your updates "on camera" or "off camera" and don't mix them.

  • Like 3
Link to comment
Share on other sites

On 5/26/2022 at 4:55 PM, ZeroByte said:

To clarify - I'm not saying Ed's strategy doesn't work for several types of programs as well, but for games or things with lots of movement, you really need to do your VERA writing during vblank to avoid tearing and artifacting. If you're driving a menu system or other relatively-static UI, then yeah, you can do all the VERA writing during main execution and just use the vsync IRQ as a timer that bumps a flag once per frame so your program can stop and wait for that value to change in order to begin drawing the next update.

I think the real thing to consider is that you probably need to decide whether you're doing your updates "on camera" or "off camera" and don't mix them.

If the only time you access VERA is during the VBLANK, then great, access it then. But if you're using VERA to store sequential data, like @Jeffrey did with the Wolf 3D wall images, then changing any parameters in VERA can really screw things up in a hurry. So I guess it all depends on how you want to use VERA.

Asteroid Commander uses lots of sequential data stored in VRAM for various purposes. If I try updating the screen every VSYNCH or even every third one, it'll stomp all over a whole bunch of things. But if the only time you're accessing VERA is to push data to the screen and PSG/PCM, like I was doing with the Balrog video demo, do it all in the VBLANK.

Link to comment
Share on other sites

On 5/27/2022 at 9:27 PM, Ed Minchau said:

Asteroid Commander uses lots of sequential data stored in VRAM for various purposes. If I try updating the screen every VSYNCH or even every third one, it'll stomp all over a whole bunch of things. But if the only time you're accessing VERA is to push data to the screen and PSG/PCM, like I was doing with the Balrog video demo, do it all in the VBLANK.

Ed, may I ask, does the technique to process the VBLANK in the main loop (outside of the interrupt) help with the frame rate? I mean, I notice that during my IRQ processing, that if the CPU time goes beyond the available time to process one frame, then the logic "stutters" and is noticable. When processing the frames in the main loop, this might help, isn't it? because the main loop can just take it's time to paint, while the interrupt will handle the coordinates of the objects to be painted ... hmmm... might consider.

  • Like 1
Link to comment
Share on other sites

  • 2 weeks later...
Posted (edited)

Kick C, in progress by @Jesper Gravgaard.

It is the most impressive work I've seen in years. If your code is well written, it generates FAST code that I've never seen. His methods of SSA and code optimizations is something I've never seen in a compiler for the 6502 family.

That being said ...

It has a steep learning curve. Not the language of course, which is C, but learning how to witte you code so that your xode becomes optimal. So looking at the generated assembler is how you learn how to apply the most ideal language constructs. And that is where people struggle...

The compiler works with a fragment system that contains hundreds, if not thousands, of 6502 assembler  code snippets that are used to generate the assembler.

It has taken me about a year to come to this result. I've helped Jesper to write many fragments that were missing. 

Fragments are needed as a result of missing combinations of C code constructs. For example a fragment that implements how to assign an char  element in a struct indirected by a constant pointer with variable offset from a byte in memory, assigned from a signed byte indirected by a constant offset from a variable pointer through zero page.

It takes time to learn the compiler and how to use it. It is not perfect yet. It had defects and its error messaging is good but incomplete. Sometimes the compiler will crash and it will take you about 3 weeks to find out why it is crashing. 

Of course I've also downloaded the code from github and now I'm at a level where I can debug and correct the compiler code where needed. 

But besides all that, the result is there. 

Through its users this environment will become better. Users like me who persevere. Jesper had been helping me in the background with bugs and also writing certain fragments. 

I've been debugging his code of the compiler to find crashes and intercepting the exceptions to get meaningful error messages and to know WHERE it complains in your code. 

But the core of his compiler is solid. 

If your are interested we can setup a discord session and I'll explain you my current environment. Or I'll record a video how to use it ...

I am working from a fork from his compiler and have written lots of code for the CX16 as libraries for use by others. 

Vera library, heap, banking... etc ...

Let me know if you're interested in this offer. Jesper is a bit offline now due to his personal obligations but I can help you a lot getting you started if you want. On discord. 

 

Edited by svenvandevelde
Link to comment
Share on other sites

I was wondering what it might take to make Zsound integrate with your project - I just got done with my first non-trivial hybrid C/asm project and was thinking I may go ahead and make C wrappers for the code, but it's for cc65.
I think the shortest pathway to other compilers/assemblers would be to build a "player" that you load into memory somewhere and interact via a jmp table at the head of it.

The thing I think might be the most problematic is the ZP space. Other than that, it stays in its lane and will eventually require 1 dedicated bank of working space (which you will signal at init() time)

Link to comment
Share on other sites

On 5/27/2022 at 10:55 PM, svenvandevelde said:

Ed, may I ask, does the technique to process the VBLANK in the main loop (outside of the interrupt) help with the frame rate? I mean, I notice that during my IRQ processing, that if the CPU time goes beyond the available time to process one frame, then the logic "stutters" and is noticable. When processing the frames in the main loop, this might help, isn't it? because the main loop can just take it's time to paint, while the interrupt will handle the coordinates of the objects to be painted ... hmmm... might consider.

Well, I have a couple of routines that take a really long time. The subroutine that redraws the asteroid if you move or rotate your view takes about 2 million cycles. When it's in the middle of doing its thing it's using both VERA channels and bouncing around in various RAM banks.

I have that subroutine broken up into 22 parts, though I could break it up further if necessary. Each time it's starting a new part of the asteroid to draw, it sets the VERA channels. So, in between each of these 22 (or 44, or whatever I eventually decide) parts, it checks the flag bits set by the IRQ and does whatever (very short) subroutines it needs before returning to the next part of the Draw routine. 

My IRQ just alerts the rest of the program that an event needs to take place, and regular checks of those flags in the rest of the program allows those VERA channels to be switched to different addresses without affecting other subroutines, and without the dance of addresses required if doing things within the interrupt subroutine. 

  • Like 1
Link to comment
Share on other sites

On 6/12/2022 at 8:39 PM, desertfish said:

My question was meant in reply to Ed comment ... but thanks!

On 6/9/2022 at 5:57 AM, Ed Minchau said:

My IRQ just alerts the rest of the program that an event needs to take place, and regular checks of those flags in the rest of the program allows those VERA channels to be switched to different addresses without affecting other subroutines, and without the dance of addresses required if doing things within the interrupt subroutine

So how do you manage to process interrupts at a specific scan line?

 

Link to comment
Share on other sites

On 6/12/2022 at 8:44 PM, svenvandevelde said:

My question was meant in reply to Ed comment ... but thanks!

So how do you manage to process interrupts at a specific scan line?

 

I don't. I just have things that need to happen at some multiple of 1/60 second instead happen shortly afterwards.  I break long sections of code up into multiple sections ( each should be much less than 1/60 of a second), and then in between those sections call the HandleFlagsAndEvents subroutine, a very short chunk of code that checks for things like the ClockRisingEdge flag (which is set by my IRQ).  If that flag is 1, then any events that need to use the VERA channels or RAM banks are free to do so.  These events need to be very short, such as pushing a 64 byte buffer into the PSG memory.  After events like that are handled, control goes back to the long subroutine.  Ideally HandleFlagsAndEvents is called many times every 1/60 of a second, does almost nothing most of the time and returns quickly.

Edited by Ed Minchau
  • Like 1
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.

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.

 Share

×
×
  • Create New...

Important Information

Please review our Terms of Use