Jump to content
IGNORED

Tracking Elapsed Time


pixelperfect

Recommended Posts

I'm trying to figure out how to track elapsed time (in seconds preferably). For example, in MonoGame when Update is called every frame by the engine you get a float elapsedTime that gives you the seconds since the last Update. Using that you can easily track elapsed time for different things (say wait 1 sec before spawning something, or a timer endlessly counting upwards). How can I do this in C on Lynx? From what I understand the VBL needs to be used?

Edited by pixelperfect
Link to comment
Share on other sites

Actually you cannot track the time from C.

 

But you can do it very easily by creating an interruptor.

 

The idea behind an interruptor is that you can add any number of routines that all will be called whenever the Lynx receives an interrupt. Every interruptor should return with Carry flag cleared if the interrupt was not handled for good. All the interrupts in Lynx tend to leave the carry cleared. This means that you can write on interruptor that counts real time by checking for VBL interrupts. There is also sound interrupts and ComLynx interrupts.

 

Here is my time counter from Shaken, not stirred
https://bitbucket.org/karri/shaken/src

 

Look at resident/gametime.s

 

It will count time based on interrupts 75 times/second.

Link to comment
Share on other sites

Thanks! Of course now I realize there was a section in the beginner's guide for Interrupts :)

 

I don't know assembly, but I'm trying to figure out what this is doing here. Would you mind explaining line by line?

inc	_interruptOccurred
lda	_playtime
ina
sta	_playtime
cmp	#75
bne	@goon
stz	_playtime
inc	_playtime+1
bne	@goon
inc	_playtime+2

I'm assuming you're increasing interruptOccured every time one occurs, then only increasing playTime every 75 times (per second)? I'm not sure what some of those lines are doing exactly though..

Edited by pixelperfect
Link to comment
Share on other sites

I looks like he is just counting up to 75 in _playtime, and when that reaches 75, he increments _playtime+1, and when that rolls over to zero, he increments _playtime+2.

 

That's a pretty common method of time keeping, used in the Atari 8-bit computer OS too. You then just have to grab the value of _playtime in your mainline code, and then grab it again, subtract the values and do some division to get elapsed time. It's easier if the VBI is running at 60hz.

Link to comment
Share on other sites

Yes. Counting to 75 was exactly what I was doing.

	.export		_interruptOccurred ; Export a byte called interruptOccurred for C-code
	.interruptor	_gametime          ; Tell the linker to call _gametime on every interrupt
	.export		_playtime          ; Export the address for 3 bytes where the time is stored
	.export		_paused            ; Export a byte "paused" to tell if the game is paused
	.export		_getscore          ; int __fastcall__ getscore() returns number of seconds 16-bit
	.export		_getspeedtime      ; int __fastcall__ getspeedtime() returns number of interrupts 16-bit
	.export		_zerotime          ; void zerotime() sets the seconds to zero

.segment	"DATA" ; The stuff declared here will get initialized at startup

TIMER2_INTERRUPT = $04 ; Timer 2 is wired to VBL (Vertical blank)
VBL_INTERRUPT = TIMER2_INTERRUPT
INTSET      = $FD81    ; An address for dealing with interrupt hardware

_interruptOccurred:
	.byte	$00
_playtime:
	.byte	$00
	.byte	$00
	.byte	$00
_speedtime:
	.byte	$00
	.byte	$00
_paused:
	.byte	$00

.segment    "CODE" ; Declare the stuff read-only executable things

; Like getscore() but tells you how many interrupts have passed (for adjusting speed of the game)
.proc	_getspeedtime: near
        lda	_speedtime
	ldx	_speedtime+1
	rts

.endproc

; ---------------------------------------------------------------
; void __near__ getscore (void)
; Read two bytes into registers A and X and return the value to the caller
; The value tells how many seconds has passed since the start 0-65535
; ---------------------------------------------------------------
.proc	_getscore: near
        lda	_playtime+1
	ldx	_playtime+2
	rts

.endproc

; Set the seconds to zero
.proc	_zerotime: near
	lda	#0
        sta	_playtime+1
	sta	_playtime+2
	rts

.endproc

; _gametime is automaticalle called during every interrupt 
.proc	_gametime: near
	lda	INTSET               ; Who interrupted?
	and	#VBL_INTERRUPT       ; was it VBL?
	beq	@goon2               ; if no, jump to goon2
	lda	_paused              ; is the game paused?
	bne	@goon2               ; if it was paused jump to goon2
	inc	_interruptOccurred   ; increment interruptOccurred
	lda	_playtime            ; increment 75 counter
	ina
	sta	_playtime
	cmp	#75                  ; was it 75?
	bne	@goon                ; if not jump to goon
	stz	_playtime            ; zero 75 counter
	inc	_playtime+1          ; increment seconds
	bne	@goon                ; if seconds did not wrap (255->0) jump to goon
	inc	_playtime+2          ; increment high byte of seconds
@goon:
	inc	_speedtime           ; increment the speedtime counter
	bne	@goon2               ; if speedtime did not wrap over jomp to goon2
	inc	_speedtime+1         ; increment high byte of speedtime
@goon2:
	clc                          ; mark the interrupt "not handled"
	rts

.endproc


The code itself is poorly written. I wrote it during a 7DRL competition so I just tried to get everything together - did not care about efficiency or saving bytes.

 

Nobody uses "interruptOccurred" for anything. It could be deleted. I used speedtime for timing events in the game. The playtime was needed to count down a bomb. You had 10 minutes time to save the world before the big badaboom in this game.

 

The name "getscore()" is misleading. It should really be called "getseconds()".

 

It is funny that I have declared "paused" but there is no code for setting it ;)

Of course I may have written some C-code elsewhere to set the paused byte. Talk about bad programming.

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

  • 3 weeks later...

Hi,

 

I feel a little silly now.

 

Greg has implemented time functions to the Lynx.

#include <time.h>
typedef unsigned long int clock_t;
clock_t _clk_tck(void);
#define CLOCKS_PER_SEC _clk_tck()
clock_t clock(void);

So forget everything I wrote in this thread. The clock() works with all screen refresh rates (50, 60, 75)

 

I mean you get number of ticks (VBL interrupts). To get seconds you need to divide the clock()/CLOCKS_PER_SEC

Edited by karri
Link to comment
Share on other sites

I fix all the bugs in the atarilynx repo at bitbucket. There is actually quite a lot of these fixes there already.

 

The good thing is that all Lynx related fixes are in

libsrc/lynx - for Lynx dependent routines

src/sp65 - for Lynx specific sprite packer. I believe that shaped sprites are only available here - not in the latest cc65.org repo

include/ - we may have different content for lynx.h, _suzy.h and _mikey.h

 

The problem with this bug is that the original author relied on a segment called LOWCODE that does not exist in the Lynx cart builds. The clock() routine is written to work even if the screen is not in use.

 

So I may just delete the extra stuff and let the code go into the basic CODE segment.

 

On the other hand... Perhaps I need to educate me on what a LOWCODE segment is. Why was it created? Could anyone shed some light here. Shawn? Greg?

Link to comment
Share on other sites

I fixed the clock stuff and a working version is in the repository.

 

Here is a test program that works with it.

#include <lynx.h>
#include <tgi.h>
#include <6502.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>

void main(void) {
  tgi_install(&lynx_160_102_16);
  tgi_init();
  CLI();
  while (1) {
    clock_t now;
    clock_t hours, minutes, seconds;
    char buf[32];
    while (tgi_busy())
      ;
    tgi_clear();
    tgi_setcolor(COLOR_GREEN);
    now = clock();
    hours = now / (3600 * 75);
    now -= hours * 3600 * 75;
    minutes = now / (60 * 75);
    now -= minutes * 60 * 75;
    seconds = now / 75;
    now -= seconds * 75;
    memset(buf, 0, sizeof(buf));
    ultoa(hours, strchr(buf, 0), 10);
    buf[strlen(buf)] = ':';
    ultoa(minutes, strchr(buf, 0), 10);
    buf[strlen(buf)] = ':';
    ultoa(seconds, strchr(buf, 0), 10);
    buf[strlen(buf)] = ':';
    ultoa(now, strchr(buf, 0), 10);
    tgi_outtextxy(0, 0, buf);
    tgi_updatedisplay();
  }
}

In order to compile it type:

cl65 -t lynx main.c -o game.lnx
Link to comment
Share on other sites

You also need to pull the latest version from bitbucket.

cd lynx
git pull

or clone it to an empty machine

git clone https://bitbucket.org/atarilynx/lynx.git

If you have an old version installed, remove it

sudo dpkg -r cc65

Then build and install it

cd lynx/tools
sudo make -f Makefile.deb

I have not used Windows since XP times. So I am a bit lost in how you would create the Windows binaries.

Link to comment
Share on other sites

Of course I should have used the function _clk_tck() to return the VBL refresh rate 75, 60 or 50 instead of using 75 as a constant.

 

The clock also starts from 0 as seen in the output of the code above.

 

 

post-2099-0-95858300-1473589712_thumb.png

 

This screenshot reminds me of changes in the cc65.org version. They decided that printing a character at 0,0 puts the lower left pixel at 0,0. In the Lynx the hot spot of the character sprite is upper left. So essentially all old software breaks when you compile it with the cc65.org compiler.

Edited by karri
Link to comment
Share on other sites

I wonder if updating would break my demo, hmm, is there a way to check which version of cc65 I have on linux? (It's the retropie image). I think I'm gonna get a new sd card next week just to re-learn/remember how to install cc65 on linux/raspi, because I would need that to be able to write some beginners Lynx guide anyway, and maybe I could even update the readme in the Lynx bit-bucket. I'm just afraid it will be a waste of one free space if I join because I barely know what a repository is, let alone updating some assembler code in cc65 :/

Edited by Turbo Laser Lynx
Link to comment
Share on other sites

You have the same version. It is not really a version. It is the trunk. Changes every now and then but mainly bugfixes.

 

I just found out that the restriction of team members applies only on private repositories. The lynx repository is public so we can have any number of contributors for free.

Edited by karri
Link to comment
Share on other sites

  • 2 weeks later...

The problem with this bug is that the original author relied on a segment called LOWCODE that does not exist in the Lynx cart builds.

 

On the other hand... Perhaps I need to educate me on what a LOWCODE segment is. Why was it created? Could anyone shed some light here. Shawn? Greg?

Hi Karri,

 

LOWCODE is used on quite a few targets that have banking windows, like the Atari 8-bit. It just ensures that code that needs to be "low" in memory, is in fact in low memory. The Atari 8-bit has standard banking windows at $4000, $9000 and $A000. I believe that IRQ code in the library would be in LOWCODE so hopefully it doesn't get banked out during an interrupt.

 

On the Lynx it isn't really required since there's no concept of memory banks, but this became standard in cc65 since quite a few of the targets have the same issue. I'm not entirely sure when that happened. You could just change that to "CODE" on the lynx if that's an issue.

Edited by Shawn Jefferson
Link to comment
Share on other sites

No. The interrupts come at a steady rate. Drawing the buffer may take lots of time. Perhaps 10 interrupts. This means that if the VBL interrupts 75 times per second. But the game update rate would only be 7.5 frames per second.

 

The line tgi_busy() tells the CPU that we are still waiting for the end of the previous tgi_updatedisplay(). The updates will only occur at the VBL interrupt to reduce flicker.

while (1);
    if (!tgi_busy()) {
        tgi_setcolor(COLOR_WHITE);
        tgi_bar(0, 0, 159, 101); // Make screen white
        tgi_setcolor(rand() & 0x0f); // Choose a random colour
        tgi_bar(10, 10, 149, 91); // Draw box with random colour
        tgi_updatedisplay(); // Swap display buffer with drawing buffer
    }
}
Link to comment
Share on other sites

  • 5 months later...

Hi Karri,

 

I'm bumping this topic because I'm trying to program some kind of timer in... the old "newcc65" (yeah ok, right, I'm stubborn, I know ^^).

I'm using the exact same method that the one discussed in this topic (increasing a variable in the VBL interrupt). It works (no rocket science there) but it's a bit shaky since I have to assume that the interrupt frequency is 60hz.

 

So here are my questions:

1) how can the interrupt have different frequencies... Is it related to some hardware difference between lynx I and II? In Handy it seems to be 60hz.

2) I know that clk_tck() solves the problem in cc65 but for obvious reason I can't use it. I found the following asm code in clock.s but is there a way to make that work with newcc65?

__clk_tck:
ldx #$00
ldy PBKUP
lda #<75
cpy #$20 + 1
blt @ok
lda #<60
cpy #$29 + 1
blt @ok
lda #<50
@ok: stz sreg
stz sreg+1
rts
Thanks a lot!
Link to comment
Share on other sites

Actually the newcc65 was really amazing compiler as you could code your interrupts in C.

/*
  Using drawPending and swapping buffers in the interrupt
  creates a flicker-free display. Idea borrowed from Tom Schenks.
*/
char drawPending;
 
VBL() interrupt
{
    if (drawPending) {
        SwapBuffers();
        drawPending = 0;
    }
    update_music();
}
 
int main()
{
    uchar ret;
 
    InitIRQ();
    /*
       To save the cart connector during developement
       The next line allows you to upload the next version without
       swapping carts.
    */
    InstallUploader(_9600Bd);
    CLI;
    SetBuffers(SCREEN1, SCREEN2, 0);
    _SetRGB(pal);
    InstallIRQ(2,VBL);
    EnableIRQ(2);
    silence();
    ret = 1;
    abcmusic(0,abcmusic0);
    abcmusic(1,abcmusic1);
    abcmusic(2,abcmusic2);
    abcmusic(3,abcmusic3);
    while (ret) {
        if (!drawPending) {
            /* DrawSprite(Sbackgrnd); */
            drawPending = 1;
        }
    }
    /* Jump to cart loader */
}

So you could just add you time counter inside the VBL interrupt and look at it updates by itself.

 

No need to dig into assembly language if you don't want to do it.

 

I add a small template with double buffered display using VBL interrupts and abcmusic.

 

The compiler is a resource hog. Keep to small games ;)

 

Edit: also note that the interrupt is run at whatever the VBL refresh rate happens to be. Probably 60Hz. But there is no mutex locking or anything else. So if you change a variable in the VBL and also at your main program there is no guarantee which one wins. Parallell programming with no protection whatsoever.

abcmini.zip

Edited by karri
Link to comment
Share on other sites

Thanks, this is exactly what I'm doing actually (I was a bit confused since you wrote earlier in this discussion that it was not possible in C... but it is :)). After a couple of test, the VBL frequency happens to be 60hz, and if I understand you correctly, it will always be 60hz, right?

 

I guess the call to clk_tck to get the frequency in cc65 is because of the support of multiple systems? (just trying to close this door and make space for the next problem ahah).

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