delay in assembly 8086
Asked Answered
S

3

5

I am trying to delay my movement in a game I'm creating. My problem is that every time that I use the int 1Ah the movement suddenly becomes "rusty" (not moving in a constant line) while my delay procedure where I use "nop" makes a solid movement and not rusty. My teacher says that I can't use a loop with "nop" and I don't know how to do a delay that uses 1Ah and looks fine (not rusty) I'm using graphics mode and assembly x86 Thanks. It works just fine, but my teacher probably won't like it for some reason.

my delay proc:

proc delay   

delRep:
    push    cx  
    mov     cx, 0FFFFH 
delDec:
    dec     cx 
    jnz     delDec
    pop     cx
    dec     cx
    jnz     delRep
    ret
endp delay
Skillern answered 29/5, 2017 at 15:51 Comment(11)
not sure what classifies as "nop"... for a spinlock/wait loop we usually use the pause instruction to reduce power consumption. If you can't use that, then use the hlt instruction but that stops the CPU until the next interrupt (usually timer) which may take a long while from a CPU's view.Predigest
my code is way too long you don't want me to post it here. it will blow this site... :(Skillern
The problem is my teacher, she says it's not efficient but I don't know why. + I can't explain this code... all it does is loops a lot of times.Skillern
If you use INT 1A, the tick rate is about 18.2hz or 55 ms per tick. Say you wanted to advance an object once every 3 ticks. Rather than doing a delay for 3 ticks, do an initial INT 1A and save the value. Then for each delay, add 3 to the value, and then use INT 1A until the value you read is greater than or equal to the saved value. Then add 3 to the value again for the next delay. As an alternative, you could read timer 0 (I/O port 040h), which runs at 1.19318 mhz or 838.0965 nsecs / cycle. You'll need some 'jmp $+2' between each port read.Sungkiang
@JoseManuelAbarcaRodríguez that delay proc is "nop" (no operation). And the problem is, that the teacher is asking different solution from him. Which I would expect to be backed with some hint, what is expected. And if the hint was default BIOS/DOS timer with 18.2/s ticking, then the animation will be choppy and erratic, because display refresh rate is different.Riccardo
And overall this is hardly "not efficient", because whatever method you will pick, it will run the DOS CPU at 100% and it will idle in some waiting/syncing loop, because that's the nature of obsolete OS and platform.Riccardo
Using the proc I posted and even this proc (the comment with the green checkmark): #15202455 The animation is not choppy at all. But my teacher says there's a bug and I should try to use the 1Ah (she said something about ticks from midnight) but when I try to use it. Even if I use the shortest delay, it looks choppy. In my opinion, I prefer the graphics over "efficiency".Skillern
@OUR you can reprogram the DOS timer to tick more often.. even finetune it close to display refresh rate... but it's not completely trivial... also it's not any more efficient, to do empty delay, wait for VSYNC, or wait for ticker to tick... either way your process in "infinite" loop. Your teacher is unfortunately not thinking it through and it has false idea that using timer is better under DOS... well, then just do it that way (and search how to reprogram DOS timer).. it's lot of work, and the true benefit is minimal, but you will learn more things, so (y) ... go ahead.Riccardo
Unfortunately, I have no time, I need to hand in my project tomorrow, but can you please tell me why is the proc that I posted is NOT "no efficient" so I can explain to my teacher that it's ok to use that proc ...Skillern
The issue with using loops for a delay is that the code would be processor sensitive, as opposed to "not efficient". Using INT 1Ah provides a low rate (18.2hz). Using timer 0 is fast, but the code for this is complicated and it would be obvious you had help.Sungkiang
Your teacher has it wrong. There's nothing more efficient about polling the timer than there is about polling and decrementing a loop variable. She is probably thinking about other environments where there are threads or co-routines. There you can yield the processor while you're waiting for the next timer tick. There's nothing like that in DOS. Thought it's possible to implement it yourself, that's big project in itself.Oscilloscope
C
4

This is another "delay" using int 15h with ah=86h, test it in your game :

;DELAY 500000 (7A120h).
delay proc   
  mov cx, 7      ;HIGH WORD.
  mov dx, 0A120h ;LOW WORD.
  mov ah, 86h    ;WAIT.
  int 15h
  ret
delay endp      

Another "delay" using system time, it should repeat about 5 times per second :

delay proc  
system_time:   
;GET SYSTEM TIME.
  mov  ah, 2ch
  int  21h ;RETURN HUNDREDTHS IN DL.
;CHECK IF 20 HUNDREDTHS HAVE PASSED. 
  xor  dh, dh   ;MOVE HUNDREDTHS...
  mov  ax, dx   ;...TO AX REGISTER.
  mov  bl, 20
  div  bl       ;HUNDREDTHS / 20.
  cmp  ah, 0    ;REMAINDER.
  jnz  system_time
  ret
delay endp  

Next is the third delay in seconds, it requires one variable :

seconds db 0  ;◄■■ VARIABLE IN DATA SEGMENT.

delay proc  
delaying:   
;GET SYSTEM TIME.
  mov  ah, 2ch
  int  21h      ;◄■■ RETURN SECONDS IN DH.
;CHECK IF ONE SECOND HAS PASSED. 
  cmp  dh, seconds  ;◄■■ IF SECONDS ARE THE SAME...
  je   delaying     ;    ...WE ARE STILL IN THE SAME SECONDS.
  mov  seconds, dh  ;◄■■ SECONDS CHANGED. PRESERVE NEW SECONDS.
  ret
delay endp      
Chinachinaberry answered 29/5, 2017 at 20:56 Comment(5)
Thanks for the answer! But this proc still makes the movement look rusty and not smooth :(Skillern
@OUR, just edited my answer to add another "delay", give it a try.Elagabalus
Thanks! the movement now is not rusty but is too fast you barely see the player moving, what do I need to change to make different delays in this proc? I tried bl but the delay is still too fast :(Skillern
@OUR, replace "20" by "30" or "40". If it still too fast we can use seconds instead of hundredths, I will edit my answer with the third delay.Elagabalus
Thank you! I really appreciate your willingness to help me out, but I kept my delay as it is because I need a super short delay and not seconds, but I'm sure that one day I'll come here and use your delays! (I've already sent my project, let's hope for a good grade :D) I really can't thank you enough, and to all of you, who posted answers! !Skillern
R
2

To have smooth movement on screen, you need to synchronize with the "vertical sync" of display mode.

The image on the screen is built over time, it's not instant, for example when your display mode is at 60Hz, it means one single image is being generated roughly 1000/60 = 16.666ms, part of that period is emulating "returning the ray to top-left" period, called "vertical retrace", which is not really needed for modern LCD, but it was needed for CRT tube displays and it's still used.

The vertical retrace took usually 5% of time (0.83ms), and it is the best period to update video RAM content, as it prevents any of the "tearing" to happen. But having just such short time period for data was usually not enough (unless you use some of double/triple buffering schemes), so in some cases the drawing to the screen did happen also outside of vertical retrace, precisely timed to either happen ahead of or behind the display ray (which goes from top left line by line from top to bottom, for each line from left to right pixel by pixel, and having a very short horizontal retrace period when the ray is returning to left side and moving one line down ... which makes me actually now wonder, why the old CRT displays didn't use left-to-right + right-to-left odd/even line signal encoding, saving the horizontal retrace time).

If you don't do this, and you simply overwrite display data any time, it may happen you will put new phase of animation ahead of ray just when the old image was already partially displayed, so the display above that point will show old phase, and after it there will be new phase. This does produce the "tearing", which is especially visible with some vertical lines moving sideways.

So if you want smooth movement animation, and I'm guessing you are coding for x86 DOS (from the int 1Ah), you should synchronize against VSYNC (search for VGA "wait for retrace" examples).

But this will open another can of worms, as modern PC displays may operate on different refresh rates, so a game designed to have correct speed at 60Hz will run twice the speed on 120Hz display (if it is fast enough to finish all the game code and drawing in ~8ms).

As this is school assignment, and you are probably running it in dosbox, I think assuming 60Hz display mode and syncing by VSYNC is reasonable.

(of course the output in the dosbox window may still be "torn apart" because the emulated display may not be synced with the window content updating vs real display refresh rate, but when you switch the dosbox to fullscreen, it usually does use the real VGA modes, for example the popular 320x200 256 colour "13h mode" has 60Hz, and most of the LCDs will display that properly, and make the emulated DOS app to run "smooth", if you give it enough CPU cycles)


And about int 1Ah ... that's the DOS system clock, which by default ticks about 18.2 times per second, that's not fast enough even for the slow 60Hz refresh rate, not even talking about the modern 100+ Hz display modes.


And to make this wall of text a bit more "practical", and example of old-era game main loop (in case the redraw is fast enough, like tetris/etc):

  1. do anything else (not mentioned in next steps), especially if it takes long
  2. wait for retrace
  3. scan input devices for player input
  4. update world according to input (has to be super fast)
  5. draw the screen (hoping the display is still in vertical retrace, or behind your writes to VRAM, so usually drawing changes from top to bottom)

Or in case the game is not fast enough to draw ahead/after ray, but fast enough to fit into 16.6ms on 60Hz display (full screen redraw), you can use some video mode which allows double/triple buffering scheme:

  1. scan input devices
  2. update world according to input (can take some time)
  3. wait for retrace and flip buffers (so the old image prepared in off-screen buffer will be now on-screen, and you have new unused off-screen buffer)
  4. draw world to off-screen buffer (can take some time, and can be drawn in any order)
  5. do other stuff

And in case the game is not even fast enough to fit into 60Hz, in DOS you will have to reprogram the timer chip to tick faster than 18.2 times, and use this to count missed vertical retrace periods in previous loop, so you know when your world update should update 2 or more frames ahead (skipping some frames), because previous draw was too slow.


About "not efficient":

In modern multitasking OS you can call some kind of delay(ms) OS method, which is guaranteed to make your code be "off" for at least the specified amount, and meanwhile the OS can run other threads/processes or just "idle" in platform specific (optimized for particular chipset/bios to save power as much as possible).

This is rarely used in games, because the delay is not guaranteed and if the OS makes your thread sleep way too long, you will miss the display retrace and the animation will be erratic, but in case of very performant game it's an option (especially in double/triple buffering schemes, where the graphics driver allows to program the screen flip ahead of actual retrace, so it's not a disaster when the game sleeps a bit over by accident).

In DOS the CPU churns through instruction all the time (unless you would go through insane length to support particular platform/chipset power saving methods, actually using NOP loop is usually quite close to it, as many x86 CPUs will detect this pattern and lower the power usage). It doesn't matter whether you are looping till some register decrements to zero, or till the VGA chipset reports VSYNC bit ON, or till the timer counter "ticks", you can NOT switch the CPU "off" in DOS in any generally usable way.

What is a bit inefficient about the delay timing of animation; that it will not keep the same speed over different CPUs due to their different frequencies (just like the VSYNC wait will not have same speed over different display refresh rates). Considering this aspect, using the int 1Ah timer has its merits, while it is lacking behind the VSYNCed smoothness, it will keep the speed constant on all kind of machines.

Also while doing the "nop" delay, you can in some cases do something more meaningful, like calculating something for the game, but once you have your frame done, there's not much point to churn out another one, for example running FPS shooter at 300FPS with VSYNC ON is almost the same thing, as running it at the frequency of display mode, because the VSYNC would make all the images above the display rate to be dropped and never shown to the user, that's why such games under VSYNC ON option usually cap the FPS to the display rate. The very tiny difference between capped run can be in the period of reading user inputs, and the frequency of physic simulation, if the physic is not stable enough to produce the same result, the 300 FPS experience may differ. Actually there were old DOS games, which got unplayable on fast machines, because their "physics" stopped to move anything once the frequency got too high, as they did use dynamic time-delta "step", which was too small to actually move the object (imagine having speed of object 0.5 pixel per physics step, being calculated as mov ax,time_tick_delta shr ax,1 -> when run on machine where time_tick_delta was just 1, the movement stopped completely ... if the original programmer had on his best PCs that value above 8+ all the time, it's easy to not foresee such problem in the 5 year future).

So in this way your delay is "not efficient", because you could have done something more useful, but if your game is already running at full display rate refresh rate, there's not much more meaningful to do, at least I would have no clue.

The professional DOS games often did took more than single display frame to update everything (as the game was too complex for that), so they had to reprogram the timer to count how many frames they missed, and skip so many to catch up with the next one. This is probably closest to int 1Ah approach. But it often still involved additional dichotomy of waiting also for vertical retrace, to avoid tearing. So you ended doing both and having even more complex game loop logic to evaluate how many frames has to be skipped to maintain correct speed of game.

Riccardo answered 29/5, 2017 at 16:53 Comment(3)
Also here is my answer to similar question (but the OP was OS-dev focused), which may give you more examples of what I mean by syncing to display: https://mcmap.net/q/2034549/-16-bit-animation-getting-startedRiccardo
Thank you very much for your answer!Skillern
Note that CRT monitors run at these common refresh rates: 60, 72, 75, 85, 100, 120, 160, 200. Some PC games (including games made in the 1980's and 1990's), are multi-threaded (using their own kernel for DOS), and run the physics thread at a fixed frequency independent of the video refresh rate. In some cases, double or triple buffering was an option (sometimes user specified), to prevent tearing.Sungkiang
S
1

To follow up on Jose Manuel Abarca Rodríguez answer:

INT 15 - BIOS - WAIT (AT,PS)
    AH = 86h
    CX:DX = interval in microseconds
Return: CF clear if successful (wait interval elapsed)
    CF set on error or AH=83h wait already in progress
        AH = status (see #0400)
Note:   the resolution of the wait period is 977 microseconds on most systems
      because most BIOSes use the 1/1024 second fast interrupt from the AT
      real-time clock chip which is available on INT 70

If timer0 is implemented in the environment that you run the game in, you can use something like this to read it:

;       Timer based on 8254 channel 0 and system timer interrupt (8)    ;
;       Channel 0 runs at 1.19318 mhz or 838.0965 nsecs / cycle         ;
;       System timer interrupts every 65536 cycles = 54.925 ms          ;
;       or about 18.2 interrupts / second                               ;
;       1 ms   = 1193.18 cycles                                         ;
;       1 hour = 65536 * 65536 cycles = 3599.59 secs                    ;
;
TMR     equ     040h
;-----------------------------------------------------------------------;
;       code                                                            ;
;-----------------------------------------------------------------------;
        .code
        assume  cs:@code,ds:nothing,es:nothing,ss:nothing

;-----------------------------------------------------------------------;
;       TmrGet  Returns current timer value.                            ;
;       If Timer 0 is in mode 3, it uses an output bit and the upper 14 ;
;       bits of the timer 0 register.  Each cycle decrements timer 0    ;     
;       by 2 (the upper 14 bits are decremented by 1). The output bit   ;
;       toggles when the timer 0 register goes from a value of 2 to 0.  ;
;       If Timer 0 is in mode 2, then the count is in timer 0 register. ;
;       Timer 0 is initialized to 0 (65536 cycles per interrupt).       ;
;-----------------------------------------------------------------------;
TmrGet  proc    near uses si di bp ds
Tmrgt0: cli
        mov     al,0c2h                 ;output read channel 0 cmd
        out     TMR+3,al
        jmp     short $+2
        in      al,TMR                  ;get bit  (15  )
        test    al,2                    ;br if in mode 2
        jz      Tmrgt2
        shl     al,1                    ;put bit 15 into carry
        jmp     short $+2
        in      al,TMR                  ;get bits ( 7-0) << 1
        mov     ah,al
        jmp     short $+2
        in      al,TMR                  ;get bits (14-8) << 1
        sti
        xchg    al,ah                   ;ax = bits 15-0
        rcr     ax,1
        test    ax,07fffh               ;br if bits (14-0) != 0
        jnz     Tmrgt1
        xor     ax,08000h               ;toggle bit 15
Tmrgt1: neg     ax                      ;make count positive
        ret

Tmrgt2: in      al,TMR                  ;get bits ( 7-0)
        mov     ah,al
        jmp     short $+2
        in      al,TMR                  ;get bits (15-8)
        sti
        xchg    al,ah                   ;ax = bits 15-0
        neg     ax                      ;make count positive
        ret
TmrGet  endp
Sungkiang answered 1/6, 2017 at 2:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.