Image Map Image Map
Results 1 to 9 of 9

Thread: How to use a timer to achieve uniform speed on an 8088 game?

  1. #1

    Default How to use a timer to achieve uniform speed on an 8088 game?

    Hi, friends! I'm happy to be here. First message.

    Can you help me? I'm really getting crazy with the timing of games. I made several searches on the Internet but I couldn't find a clear step by step procedure for making a game behave the same way (speed) independently of the processor type and speed. I learnt that you can use several techniques, and that every one of these has its pros and cons. Basically, I understood that you can read BIOS time or you can use interrupts (1Ch or 08h) plus reprogramming the PIT to achieve the desired frequency.

    What technique was mostly used on the 80s and 90s DOS games? Is there such a thing as a standard timing technique?

    As there exist procedures and functions to manage other aspects of gaming such as sprites (I'm thinking on XLib, by example), is there any standard procedure that could be used for define the graphics speed on a game? (for instance, forcing the framerate to 25 fps or whatever).

    I achieved to connect sound to INT 08h and programming PIT to the frequency needed but I'm just lost trying to achive the same with graphics, using INT 1Ch.

    Thank you very much for your help. Greetings!

  2. #2

    Default

    If you grab my Paku Paku program:

    https://deathshadow.com/pakuPaku

    The source code there programs the PIT to 240hz. I use this for music timing and frame rate by dividing it up into chunks. I even use it to divide up game processes into sections so that they can "release" to allow the music update. Basically, I have my own semi-cooperative realtime single-thread threading.

    The heart of which is:

    Code:
    unit timer;
    
    interface
    
    uses
    	jfunc;
    
    const
    	clock8253=1193181;
    	countExact=4971; { roughly 240hz }
    	countInterval=65536 div countExact;
    	freq=clock8253 div countExact;
    
    	PIT_Data0=$40;
    	PIT_Data1=$41;
    	PIT_Data2=$42;
    	PIT_Control=$43;
    
    	PIT_Select0=$00;
    	PIT_Select1=$40;
    	PIT_Select2=$80;
    
    	PIT_Latch=$00;
    	PIT_LSB=$10;
    	PIT_MSB=$20;
    	PIT_LSBMSB=$30;
    
    	PIT_Mode0=$00; { Interrupt on Terminal Count }
    	PIT_Mode1=$02; { Hardware Retriggerable One-Shot }
    	PIT_Mode2=$04; { Rate Generator }
    	PIT_Mode3=$06; { Square Wave }
    	PIT_Mode4=$08; { Software Triggered Strobe }
    	PIT_Mode5=$0A; { Hardware Triggered Strobe }
    
    	PIT_BCD=$01;
    
    var
    	userClockCounter:word;
    
    procedure startTimer;
    procedure killTimer;
    function getUserClockInterval(hz:word):word;
    function userTimerExpired(interval:word):boolean;
    
    implementation
    
    var
    	isrCount:word;
    	timerActive:boolean;
    	oldISR:pointer;
    	oldExitProc:pointer;
    
    function getUserClockInterval(hz:word):word; assembler;
    asm
    	xor  dx,dx
    	mov  ax,freq
    	mov  bx,hz
    	div  bx
    end;
    
    procedure timerISR; interrupt; assembler;
    asm
    	inc  userClockCounter
    	dec  isrCount
    	jnz  @done
    	mov  isrCount,countInterval
    	pushf
    	call oldISR
    @done:
    end;
    
    procedure startTimer; assembler;
    asm
    	mov  isrCount,countInterval
    	mov  dx,OFFSET timerISR
    	mov  bx,SEG timerISR
    	push ds
    	mov  ds,bx
    	mov  ax,$251C
    	int  $21
    	pop  ds
    	{ set timer 0 to desired high speed frequency }
    	mov  al,PIT_Select0 or PIT_LSBMSB or PIT_Mode2
    	out  PIT_Control,al
    	mov  ax,countExact
    	out  PIT_Data0,al
    	mov  al,ah
    	out  PIT_Data0,al
    	mov  timerActive,1
    end;
    
    procedure killTimer; assembler;
    asm
    	{ reset timer0 to normal }
    	mov  al,PIT_Select0 or PIT_LSBMSB or PIT_Mode2
    	out  PIT_Control,al
    	mov  al,$FF
    	out  PIT_Data0,al
    	out  PIT_Data0,al
    	mov  ax,$251C
    	push ds
    	lds  dx,oldISR
    	int  $21
    	pop  ds
    	mov  timerActive,0
    end;
    
    function userTimerExpired(interval:word):boolean; assembler;
    asm
    	xor  al,al
    	mov  bx,userClockCounter
    	mov  cx,interval
    	cmp  bx,cx
    	jl   @done
    	sub  bx,cx
    	mov  userClockCounter,bx
    	inc  al
    @done:
    end;
    
    procedure safeExit; far; assembler;
    asm
    	cmp  timeractive,0
    	je   @done
    	call killTimer;
    @done:
    	les  di,oldExitProc
    	mov  WORD PTR exitProc,di
    	mov  WORD PTR exitProc+2,es
    end;
    
    begin
    	asm
    		mov  ax,$351C
    		int  $21
    		mov  WORD PTR oldISR,bx
    		mov  WORD PTR oldISR+2,es
    		mov  timerActive,0
    	end;
    	oldExitProc:=exitproc;
    	exitproc:=@safeExit;
    end.
    In the main loop of the program, I just wait for:

    Code:
    		repeat
    			if userTimerExpired(timerInterval) then begin
    
    				frameThrottle := (frameThrottle + 1) mod frameSpeed;
    
    				case frameThrottle of
    					0:begin
    						{ blink dots and write sprites to backbuffer }
    					end;
    					1:begin
    						{ copy backbuffer to screen, read joystick }
    					end;
    					2:begin
    						{ erase sprites from backbuffer }
    					end;
    					3:begin
    						{ update player, update ghost, test collisions }
    					end;
    				end;
    				
    				{ sound handling here }
    
    			end;
    		until (lives <= 0) or (dots <= 0);
    This way if the machine is too slow any overflow levels out, and I don't need to have a separate ISR for audio or put it in the same ISR. It also lets me easily "move chunks around" to better divide up the tasks between music updates.

    The big trick is that I can do multiple level speeds off this one code by just changing the value used in that modulo. Since the source is 240hz, I could run it as fast as 60hz with a modulo of 4... 5 gets me 48hz, 6 gets me 40hz, 8 gets me 30hz, 10 gets me 24hz, etc, etc. The extra intervals just do nothing because, well.. what else are they gonna do?
    From time to time the accessibility of a website must be refreshed with the blood of owners and designers. It is its natural manure.
    CUTCODEDOWN.COM

  3. #3

    Default

    Thank you very much for your detailed response. I forgot to say that I am programming in C language with Turbo C++ 1.0 (no OOP) and Tasm 1.01. So I tried to convert your code to C with inline assembly:

    Code:
    #define word unsigned int
    #define bool char
    #define true 1
    #define false 0
    
    #define clock8253 1193181
    #define countExact 4971
    #define countInterval 65536 / countExact
    #define freq clock8253 / countExact
    
    #define PIT_Data0 0x40
    #define PIT_Data1 0x41
    #define PIT_Data2 0x42
    #define PIT_Control 0x43
    
    #define PIT_Select0 0x00
    #define PIT_Select1 0x40
    #define PIT_Select2 0x80
    
    #define PIT_Latc 0x00
    #define PIT_LSB 0x10
    #define PIT_MSB 0x20
    #define PIT_LSBMSB 0x30
    
    #define PIT_Mode0 0x00       // Interrupt on Terminal Count 
    #define PIT_Mode1 0x02       // Hardware Retriggerable One-Shot 
    #define PIT_Mode2 0x04       // Rate Generator 
    #define PIT_Mode3 0x06       // Square Wave 
    #define PIT_Mode4 0x08       // Software Triggered Strobe 
    #define PIT_Mode5 0x0A       // Hardware Triggered Strobe 
    
    #define PIT_BCD 01
    
    static word userClockCounter = 0;
    static word isrCount = countInterval;
    bool timerActive = false;
    
    void interrupt (*oldISR)();
    void interrupt timerISR();
    
    word timer_getUserClockInterval (word hz)
    {
        asm {
            xor     dx,dx
    	    mov     ax,freq
    	    mov     bx,hz
    	    div     bx
        }
    
        return _AX;
    }
    
    void interrupt timerISR()
    {
    	asm inc     userClockCounter
    	asm dec     isrCount
    	asm jnz     done
    	asm mov 	isrCount, countInterval
    	asm pushf
    	//(*oldISR)();  //-> Calling the oldISR on Turbo C++ just hangs on the system
     	//outport ( 0x20,0x20 );       //Ack the interrupt (?)
    
    
    done:
    
      	asm xor cx, cx   // Dummy instruction as Turbo C++ expects something here
    
    }
    
    void timer_start()
    {
    	oldISR = getvect (0x1C);
    
    	timerActive = false;
    
    
    	isrCount = countInterval;
    	
        setvect(0x1C, timerISR);
    	
        asm {
    
            // set timer 0 to desired high speed frequency
            mov  al,PIT_Select0 or PIT_LSBMSB or PIT_Mode2
            out  PIT_Control,al
            mov  ax,countExact
            out  PIT_Data0,al
            mov  al,ah
            out  PIT_Data0,al
        }
    
    	timerActive = true;
    }
    
    void timer_restore()
    {
    	asm {
    		mov  al,PIT_Select0 or PIT_LSBMSB or PIT_Mode2
    		out  PIT_Control,al
    		mov  ax,0FFFFh
    		out  PIT_Data0,al
    		mov  al,ah
    		out  PIT_Data0,al
    	}
    
    	setvect (0x1C, oldISR);
    
    	timerActive = false;
    }
    
    bool timer_userTimerExpired(word interval)
    {
    	//printf ("%d", userClockCounter);
    	asm		xor  al,al
    	_BX = userClockCounter;
    	_CX = interval;
    	asm {
    		cmp  bx,cx
    		jl   doneTimerExpired
    	}
    	asm {
    		sub  bx,cx
    	}
    	userClockCounter = _BX;
    	//printf ("%d", userClockCounter);
    	asm 	inc  al
    	return false;
    
    doneTimerExpired:
    	//puts ("OK");
    	return true;
    }
    
    
    /*
    // These ones are not still converted to C
    safeExit proc far
    
    	cmp  timerActive,0
    	je   @doneSafeExit
    	call killTimer;
    @doneSafeExit:
    	les  di,oldExitProc
    	mov  WORD PTR exitProc,di
    	mov  WORD PTR exitProc+2,es
    endp
    
    
    begin
    	asm
    		mov  ax,351Ch
    		int  21h
    		mov  WORD PTR oldISR,bx
    		mov  WORD PTR oldISR+2,es
    		mov  timerActive,0
    	end;
    	oldExitProc:=exitproc;
    	exitproc:=@safeExit;
    end.
    */
    When I call the userTimerExpired function, nothing happens. I just post the code because if it could be useful for someone. I'll keep trying to detect what's not working in my conversion, as yours in Turbo Pascal works perfectly.

  4. #4

    Default

    I extracted the timer code I used in COVOIDS to hook the timer interrupt. I think you may have had some issues in your ISR, but I didn't try debugging. There really is no reason to use assembly interspersed with C, you can do it all in C quite easily - especially with the support offered in Borland's C compilers. I compiled and ran the following using Borland C++ 2.0 (compiled as C code) as I didn't have Turbo C installed to test, but it should be easy to make any changes required by Turbo C. Note that I didn't reprogram the PIT to change the interrupt frequency, but it looks like you know how to do that. Also, TimerDelay() isn't particularly accurate because it resets the tickCount at entry and waits for it to equal the delay value, so only accurate +- 1 tick count. There are probably better ways to do this, but this was my quick and dirty implementation.

    Code:
    #include <dos.h>
    #include <string.h>
    #include <stdio.h>
    #include <conio.h>
    
    // 8253 Programmable Interval Timer ports and registers
    // The clock input is a 1.19318 MHz signal
    #define CLOCK_FREQ              1193180L
    #define PIT_COMMAND_REGISTER    0x43
    #define PIT_COUNT_REGISTER      0x42
    
    #define TIMER_INTERRUPT_VECTOR  0x1C
    
    #define TICKS_PER_SEC           18.2
    
    typedef unsigned int t_timer;
    
    void interrupt (far *chainTimerISR)() = 0L;
    t_timer timerTick           = 0;
    
    void TimerReset(void)
    {
        disable();
        timerTick = 0;
        enable();
    }
    t_timer TimerCount(void)
    {
        t_timer tick;
    
        disable();
        tick = timerTick;
        enable();
        return tick;
    }
    void TimerDelay(t_timer ticks)
    {
        if (ticks && chainTimerISR != 0L)
        {
    	disable();
    	timerTick = 0;
    	enable();
    	while (timerTick < ticks);
        }
    }
    void interrupt far TimerISR()
    {
        ++timerTick;
        //
        // Chain previous timer ISR
        //
        chainTimerISR();
    }
    void TimerInstall(void)
    {
        if (chainTimerISR == 0L)
        {
    	chainTimerISR = getvect(TIMER_INTERRUPT_VECTOR);
    	setvect(TIMER_INTERRUPT_VECTOR, TimerISR);
        }
    }
    
    void TimerUninstall(void)
    {
        if (chainTimerISR != 0L)
        {
    	setvect(TIMER_INTERRUPT_VECTOR, chainTimerISR);
    	chainTimerISR = 0L;
        }
    }
    
    int main(int argc, char **argv)
    {
        int i;
    
        TimerInstall();
        for (i = 0; i < TICKS_PER_SEC*5; i++) // Print timer count for 5 seconds
        {
    	printf("%d\r", i);
    	TimerDelay(1);
        }
        TimerUninstall();
        return 0;
    }
    

  5. #5

    Default

    I store the current time (in milliseconds) in a variable at the top of the game loop. (This doesn't have to be the system time; any millisecond-accurate timer will work. Some platforms offer a function to read the number of millis since the program started executing; this will also work fine.)

    Then I do all my stuff in the loop body.

    At the bottom, I read the time again and subtract the time from at the top of the loop, to find how long it took for the loop body to execute. Then I subtract that from a delay constant and sleep for that long (if the result is > 0). This probably isn't quite as accurate or efficient as using an interrupt, but it's much more portable.
    -- Lee
    If you get super-bored, try muh crappy YouTube channel: Old Computer Fun!
    Looking to Buy/Trade For (non-working is fine): Tandy 1000 EX/HX power supply, PS/2 Model 25-286 ISA expansion riser, Mac IIci hard drive sled and one bottom rubber foot, Multisync VGA CRTs, Decent NuBus video card, PC-era Tandy stuff, Aesthetic Old Serial Terminals (HP and Data General in particular)

  6. #6
    Join Date
    Aug 2006
    Location
    Chicagoland, Illinois, USA
    Posts
    6,881
    Blog Entries
    1

    Default

    Quote Originally Posted by carlos12 View Post
    I'm really getting crazy with the timing of games. I made several searches on the Internet but I couldn't find a clear step by step procedure for making a game behave the same way (speed) independently of the processor type and speed. I learnt that you can use several techniques, and that every one of these has its pros and cons. Basically, I understood that you can read BIOS time or you can use interrupts (1Ch or 08h) plus reprogramming the PIT to achieve the desired frequency.
    So, this is a complicated topic, and it comes down to what kind of game you're writing and what your expectations of performance will be. Because PCs are not all the same speed, it is not a good idea to assume you can write your game to a specific framerate (unless you're doing very little and targeting fast systems, or are targeting only one specific system). So a simple way to do this is to decouple the game logic and the display routines. The game logic runs on an interrupt, most usually the system timer (IRQ0/INT08), that has had its interrupt frequency changed to the internal rate you want the game logic to run at. The graphics updates then happen in the foreground, updating the display using information generated by the continuously running game logic.

    That's the overview, but you can get lost in the details. Here are some notes that can help, in no particular order:

    Sometimes the game logic is too complicated to run completely on the background interrupt (ie. it's not finished processing before the next interrupt fires). In that case, you have a few choices: Reducing the frequency of the game logic (ie. fire at 30 Hz instead of 60 Hz) is one solution, although it reduces the maximum framerate you can achieve. Another option is having the background interrupt doing something very simple, such as incrementing a counter; the game logic can then run in the foreground and 1. copy the counter, 2. zero out the original counter, 3. perform the logic, then 4. multiply the results by the value of the copied counter. Once the game logic has finished, the display routines can run based off of the logic. A third option is to make the game logic re-entrant (ie. handle being called before it is finished) by setting a "lock" when it is first entered, then remove the lock when done... and if it is entered before finished, exiting immediately if the lock is set. The drawback to this last method is that the game will run slowly if this happens, so it's not a desirable design unless it's okay for your game to run slowly.

    Implementing background music is usually called via the system timer, so choosing the frequency of your game logic can be tied to that. There is also the frequency of the display to consider; if you are targeting only 60 Hz video modes, then having a 48Hz or 72Hz (for example) game logic interrupt is not a good idea since there will be visible "beating" (frames added or skipped) to the user. One solution to that is to pick the interrupt frequency of the video mode as your game logic frequency. Another solution is to pick the least common multiple of what you need; for example, if playing a MOD (50Hz) but targeting unchained VGA (60Hz), then your interrupt frequency could be LCM(50,60)= 300 Hz, which would allow the modplayer to fire 50 times a second and the game logic to fire 60 times a second, which (assuming the display updates are fast) would match up with the screen.

    If your game logic is able to run completely in the background, you will need a way for the display routines to grab the results atomically. One simple way of doing this is to have the game logic write its results to an output buffer, and the display routine can have an input buffer. This size of the buffer should be fairly small, only containing whatever information is necessary to update the screen. When the display routine starts, it halts interrupts, copies the output buffer to its input buffer, then resumes interrupts. Since the amount of information being copied should be very small (like, 2k or less), the system won't exhibit any visible or audible "judder" as a result. If you need more than 2K of info to update your screen (ie. you're doing 3-D rendering of 1000 vertices), then you'll need to do something more complicated, such as having the game logic write to two different buffers based on the status of a flag, and the display routine sets the flag to the buffer it is NOT reading from.

    If you *must* maintain a specific display update target -- for example, your game must run at 60 Hz, fluidly, no exceptions -- then you don't use a background interrupt at all: You wait for the start of the screen refresh cycle, then do your display updates, then do your game logic for the next update (and update the music, and anything else that needs to happen every frame), then start waiting again. In DOS PC terms, you can only do this if you are supporting a known CPU speed (or faster) and a known video mode. This is not a great choice for delivering your game, since anything slower than your target can't play it effectively. However, it is sometimes acceptable to develop and/or debug your game this way, and then only when completely finished, decouple the game logic and display routines using interrupts for the final deliverable.

    You mentioned 8088 in your thread topic. One piece of advice is to develop your game with extremely fast "draft" graphics first. For example, plot a single dot, or maybe just a solid rectangle, instead of an actual masked sprite. This will let you hone and finish the gameplay first before having the entire thing bog down later when you find out how slow 8088 graphics are.

    While his advice was well-meaning, I wouldn't use pakupaku as a model for your first attempt, as pakupaku was developed specifically for a known gameplay mechanic, and a known speed target (4.77 MHz systems). Slicing your game logic into cooperative pseudo-threads is a choice that may not necessarily be appropriate for whatever game you're trying to write.

    Hope this helps.

    PS: You mention both 8088 and Xlib in your thread. Xlib is VGA only. VGA graphics are 4x the amount of video memory to manipulate than CGA. If you are writing any sort of action game, it is not practical to target VGA graphics an on 8088, as 8088 systems have very slow access to memory, and even slower access to video memory.
    Offering a bounty for:
    - A working Sanyo MBC-775 or Logabax 1600
    - Music Construction Set, IBM Music Feature edition (has red sticker on front stating IBM Music Feature)

  7. #7

    Default

    Thank you very much everyone for your advices!!! It's much more complex than I imagined...

    You gave me a lot of good stuff to study and re-read.

    Hi, Trixter. My game is been developed for several graphic modes: Hercules, CGA, PCjr/Tandy, EGA and VGA in Mode-X. The minimum platform I'm targeting would be an 8088 at 4.77 Mhz but I'm realistic here: I don't expect it will work smoothly on VGA mode, even using the Mode-X goodies such as latches and compiled sprites. I don't even think that there were many 4.77 Mhz machine owners who dared to install a VGA on their systems back in the 80s - early 90s I think it's more realistic thinking on Hercules, CGA and lo-res PCjr/Tandy for this snail speed For the sprites on these modes I'm using the invaluable code from Richard Wilton's book. The game has a horizontal scroll, which I'm moving trough a custom assembly procedure using movsw. It's quite fast (given the limitations of the platform...). Maybe things could be made even faster by using CGA compiled sprites but, sincerely, I don't even know were to start. But for the moment I'm not unhappy with the results, although the game works far better on a 7-8 Mhz 8088, just as expected. As David Murray says, "people don't usually realize how slow these machines are"

    If I'd were using only Mode-X, things would had been considerably easier as relaying on the vertical retrace makes the game work exactly the same speed regardless the system's speed (some minimum requirements given). At this moment, as I still haven't optimized the tile painting routines, removing outs, unrolling code and/or using the latches, it works ok with a PS/2 model 30-286 at 10Mhz, emulated by 86Box as I no longer own a real retro machine My wish is that it may run reasonably well on an 8086 at 8 Mhz when optimized.

    At least theoretically, CGA cards can also consult the vertical retrace. I achieved it on emulators (DosBox, PCem, 86Box) and it homogenizes the speed. The problem is that when scrolling, because of the memory organization of the CGA, it moves 4 pixels at once instead of 1 as it does with Mode-X so in relatively fast machines the dfference of speed between CGA/PCjr/Tandy (as the memory organization is very similar, I use the same routines) and VGA is huge. That's one of the reasons I need to precisely control the game speed. Another problem is that on some machines the scroll is a bit sloppy. I think that the reason could be that on some places the code is faster than on others, so relaying on a timer I think it could make things smoother or, at least, more homogeneous. Another reason I need a timer is that Hercules doesn't support the vertical retrace so it must rely on a timer.

    Thank you very much for your time and your very detailed and useful response. Greetings!

  8. #8
    Join Date
    Aug 2006
    Location
    Chicagoland, Illinois, USA
    Posts
    6,881
    Blog Entries
    1

    Default

    Quote Originally Posted by carlos12 View Post
    I don't even think that there were many 4.77 Mhz machine owners who dared to install a VGA on their systems back in the 80s - early 90s
    Actually, a lot of people did, but mostly because they used their systems for word processing or spreadsheets, and VGA allowed them to get bigger screens with clearer text.

    The game has a horizontal scroll, which I'm moving trough a custom assembly procedure using movsw.
    This is one area where EGA and VGA could help your game: They have hardware scrolling/panning at 1-pixel resolution.

    emulated by 86Box as I no longer own a real retro machine My wish is that it may run reasonably well on an 8086 at 8 Mhz when optimized.
    You can get closer to "real system speed" by using PCem, if you want to use that for testing.

    Another reason I need a timer is that Hercules doesn't support the vertical retrace
    Yes it does. It's at 3BA, just like CGA has it at 3DA. I don't know if both retrace bits are honored, but the status register is there.

    Your project sounds very ambitious; I look forward to seeing it.
    Offering a bounty for:
    - A working Sanyo MBC-775 or Logabax 1600
    - Music Construction Set, IBM Music Feature edition (has red sticker on front stating IBM Music Feature)

  9. #9

    Default

    Yes it does. It's at 3BA, just like CGA has it at 3DA. I don't know if both retrace bits are honored, but the status register is there.
    Very nice! It works like a charm.

    Code:
              if (GraphicMode != Mode_Hercules)
              {
                // Wait for vertical retrace
                  asm  mov dx,3DAh
                l1:
                  asm  in al,dx
                  asm  and al,08h
                  asm  jnz l1
                l2:
                  asm  in al,dx
                  asm  and al,08h
                  asm  jz  l2
              }
              else
              {
    	      asm  mov dx,3BAh
                l1herc:
                  asm  in al,dx
                  asm  and al,80h
                  asm  jnz l1herc
                l2herc:  
                  asm  in al,dx
                  asm  and al,80h
                  asm  jz  l2herc
      
    	  }
    Your project sounds very ambitious; I look forward to seeing it.
    Thank you for your interest. Sure, I will post something when I'll have a decent prototype.

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •