【问题标题】:delay in assembly 8086组装延迟 8086
【发布时间】:2017-05-29 15:51:43
【问题描述】:

我正在尝试在我正在创建的游戏中延迟我的动作。 我的问题是,每次我使用 int 1Ah 时,运动突然变成 “生锈”(不是在一条恒定的线上移动),而我使用“nop”的延迟程序会产生稳固的运动而不是生锈。我的老师说我不能使用带有“nop”的循环,而且我不知道如何做一个使用 1Ah 并且看起来很好(不生锈)的延迟 我正在使用图形模式和汇编 x86 谢谢。它工作得很好,但我的老师可能出于某种原因不喜欢它。

我的延迟过程:

proc delay   

delRep:
    push    cx  
    mov     cx, 0FFFFH 
delDec:
    dec     cx 
    jnz     delDec
    pop     cx
    dec     cx
    jnz     delRep
    ret
endp delay

【问题讨论】:

  • 不确定什么归类为“nop”...对于自旋锁/等待循环,我们通常使用暂停指令来降低功耗。如果您不能使用它,则使用 hlt 指令,但这会停止 CPU 直到下一个中​​断(通常是计时器),从 CPU 的角度来看,这可能需要很长时间。
  • 我的代码太长了,你不希望我在这里发布。它会炸毁这个网站... :(
  • 问题是我的老师,她说效率不高,但我不知道为什么。 + 我无法解释这段代码......它所做的只是循环很多次。
  • 如果您使用 INT 1A,滴答频率约为 18.2hz 或 55 ms 每滴答。假设您想每 3 个滴答声推进一次对象。与其延迟 3 个滴答声,不如执行初始 INT 1A 并保存该值。然后对于每一个延迟,将值加3,然后使用INT 1A,直到你读取的值大于或等于保存的值。然后再次将该值加 3 以进行下一次延迟。作为替代方案,您可以读取计时器 0(I/O 端口 040h),它以 1.19318 mhz 或 838.0965 nsecs / 周期运行。在每个端口读取之间您需要一些“jmp $+2”。
  • @JoseManuelAbarcaRodríguez 延迟过程是“nop”(无操作)。问题是,老师向他提出了不同的解决方案。我希望得到一些暗示的支持,这是预期的。如果提示是默认的 BIOS/DOS 计时器,18.2/s 滴答作响,那么动画将不稳定且不稳定,因为显示刷新率不同。

标签: assembly delay


【解决方案1】:

这是另一个使用int 15hah=86h 的“延迟”,在你的游戏中测试它:

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

另一个使用系统时间的“延迟”,它应该每秒重复大约 5 次:

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  

接下来是第三次延迟,它需要一个变量:

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      

【讨论】:

  • 感谢您的回答!但是这个过程仍然使运动看起来生锈而且不平滑:(
  • @OUR,刚刚编辑了我的答案以添加另一个“延迟”,试一试。
  • 谢谢!现在的移动没有​​生锈但是太快了你几乎看不到玩家移动,我需要改变什么才能在这个过程中做出不同的延迟?我试过 bl 但延迟还是太快了:(
  • @OUR,将“20”替换为“30”或“40”。如果它仍然太快,我们可以使用秒而不是百分之一,我将在第三次延迟时编辑我的答案。
  • 谢谢!我真的很感谢你愿意帮助我,但我保持我的延迟,因为我需要一个超短的延迟而不是几秒钟,但我相信有一天我会来这里并利用你的延迟! (我已经发送了我的项目,让我们期待一个好成绩:D)我真的不能感谢你们,以及所有发布答案的人! !
【解决方案2】:

要在屏幕上平滑移动,需要与显示模式的“垂直同步”同步。

屏幕上的图像是随着时间构建的,它不是即时的,例如当您的显示模式为 60Hz 时,这意味着大约 1000/60 = 16.666ms 生成一个图像,该时间段的一部分正在模拟“将光线返回到左上角”期间,称为“垂直回扫”,现代 LCD 并不真正需要它,但 CRT 管显示器需要它并且仍在使用。

垂直回溯通常需要 5% 的时间(0.83 毫秒),这是更新视频 RAM 内容的最佳时间,因为它可以防止任何“撕裂”发生。但是只有这么短的数据时间段通常是不够的(除非你使用一些双/三缓冲方案),所以在某些情况下,屏幕上的绘制也确实发生在垂直回扫之外,精确地定时在之前发生或在显示光线后面(从左上角逐行从上到下,每一行从左到右逐个像素,当光线返回左侧并移动一条线时,水平回扫周期非常短down ...这让我现在真的想知道,为什么旧​​的CRT显示器没有使用从左到右+从右到左的奇/偶线信号编码,从而节省了水平回扫时间。

如果您不这样做,并且您只是简单地覆盖显示数据任何时间,您可能会在旧图像已经部分显示时将新的动画阶段放在光线之前,因此该点上方的显示将显示旧阶段,之后将显示新阶段。这确实会产生“撕裂”,在一些垂直线横向移动时尤其明显。

因此,如果您想要平滑的运动动画,并且我猜您正在为 x86 DOS 编码(来自int 1Ah),您应该与 VSYNC 同步(搜索 VGA“等待回溯”示例)。

但这会引发另一波蠕虫,因为现代 PC 显示器可能以不同的刷新率运行,因此设计为在 60Hz 下具有正确速度的游戏将在 120Hz 显示器上运行两倍的速度(如果它足够快以完成所有大约 8 毫秒的游戏代码和绘图)。

由于这是学校作业,你可能在 dosbox 中运行它,我认为假设 60Hz 显示模式和 VSYNC 同步是合理的。

(当然,dosbox 窗口中的输出可能仍会被“撕裂”,因为模拟显示可能不会与窗口内容更新与实际显示刷新率同步,但是当您将 dosbox 切换到全屏时,它通常会使用真正的 VGA 模式,例如流行的 320x200 256 色“13h 模式”有 60Hz,如果你给它足够的 CPU 周期,大多数 LCD 都会正确显示,并使模拟的 DOS 应用程序运行“流畅”)


关于int 1Ah ... 那是 DOS 系统时钟,默认情况下每秒大约滴答 18.2 次,即使对于缓慢的 60Hz 刷新率也不够快,更不用说现代 100+ Hz 显示模式.


为了让这堵文字墙更“实用”,以及旧时代游戏主循环的示例(以防重绘速度足够快,如俄罗斯方块/等):

  1. 做其他事情(在接下来的步骤中没有提到),特别是如果它需要很长时间
  2. 等待回溯
  3. 扫描输入设备以获取玩家输入
  4. 根据输入更新世界(必须超快)
  5. 绘制屏幕(希望显示器仍处于垂直回扫状态,或在您写入 VRAM 之后,因此通常从上到下绘制变化)

或者,如果游戏速度不足以在光线前/后绘制,但在 60Hz 显示(全屏重绘)上足够快以适应 16.6 毫秒,您可以使用一些允许双/三缓冲方案的视频模式:

  1. 扫描输入设备
  2. 根据输入更新世界(可能需要一些时间)
  3. 等待回溯和翻转缓冲区(因此在屏幕外缓冲区中准备的旧图像现在将在屏幕上,并且您有新的未使用的屏幕外缓冲区)
  4. 将世界绘制到屏幕外缓冲区(可能需要一些时间,并且可以按任意顺序绘制)
  5. 做其他事情

如果游戏的速度甚至不足以适应 60Hz,在 DOS 中,您将不得不重新编程计时器芯片以使其滴答速度快于 18.2 倍,并使用它来计算前一个循环中错过的垂直回扫周期,所以您知道您的世界更新何时应该提前更新 2 帧或更多帧(跳过一些帧),因为之前的绘制太慢了。


关于“效率不高”:

在现代多任务操作系统中,您可以调用某种delay(ms) OS 方法,这可以保证您的代码在至少指定的时间内“关闭”,同时操作系统可以运行其他线程/进程或只是在特定平台“空闲”(针对特定芯片组/BIOS 进行优化以尽可能节省电力)。

这在游戏中很少使用,因为不能保证延迟,如果操作系统让你的线程睡眠时间过长,你会错过显示回溯并且动画会不稳定,但在性能非常好的游戏的情况下,这是一个选项(特别是在双/三缓冲方案中,图形驱动程序允许在实际回溯之前对屏幕翻转进行编程,因此当游戏意外休眠时不会造成灾难)。

在 DOS 中,CPU 一直在执行指令(除非您为了支持特定的平台/芯片组节能方法而费尽心思,否则实际上使用 NOP 循环通常非常接近它,因为许多 x86 CPU 会检测到这种模式并降低功耗)。无论您是循环直到某些寄存器减为零,还是直到 VGA 芯片组报告 VSYNC 位打开,或者直到计时器计数器“滴答”,您都不能在任何通常可用的 DOS 中“关闭”CPU方式。

什么是动画的延迟时间有点低效;由于它们的频率不同,它不会在不同的 CPU 上保持相同的速度(就像 VSYNC 等待在不同的显示刷新率下不会有相同的速度)。考虑到这一点,使用int 1Ah定时器有其优点,虽然在VSYNCed平滑度方面有所欠缺,但它会在各种机器上保持速度恒定。

此外,在进行“nop”延迟时,在某些情况下,您可以做一些更有意义的事情,比如为游戏计算一些东西,但是一旦你完成了你的框架,就没有太多意义去生产另一个框架,例如运行FPS 射击游戏在 300FPS 下开启 VSYNC 几乎是一样的,因为它在显示模式的频率下运行,因为 VSYNC 会使所有高于显示速率的图像都被丢弃并且永远不会显示给用户,这就是为什么这类游戏在VSYNC ON 选项通常将 FPS 限制为显示速率。 capped run 之间的微小差异可能在于读取用户输入的周期,以及物理模拟的频率,如果物理不够稳定以产生相同的结果,300 FPS 的体验可能会有所不同。实际上有一些旧的 DOS 游戏在快速机器上无法运行,因为一旦频率太高,它们的“物理”就会停止移动任何东西,因为它们确实使用了动态时间增量“步长”,它太小而无法真正移动对象(想象每个物理步骤的对象速度为 0.5 像素,计算为 mov ax,time_tick_delta shr ax,1 -> 当在 time_tick_delta 仅为 1 的机器上运行时,运动完全停止......如果原始程序员在他的最好的 PC 一直在 8+ 以上,很容易在未来 5 年内无法预见到这样的问题。

因此,通过这种方式,您的延迟“效率不高”,因为您本可以做一些更有用的事情,但是如果您的游戏已经以全显示速率刷新率运行,那么没有什么更有意义的事情可做,至少我会一点头绪都没有。

专业的 DOS 游戏通常需要不止一个显示帧来更新所有内容(因为游戏太复杂了),所以他们不得不重新编程计时器来计算他们错过了多少帧,并跳过这么多帧来捕捉与下一个。这可能最接近int 1Ah 方法。但它通常仍然涉及额外的二分法,即等待垂直回扫,以避免撕裂。因此,您结束了这两项工作,并拥有更复杂的游戏循环逻辑来评估必须跳过多少帧才能保持正确的游戏速度。

【讨论】:

  • 这也是我对类似问题的回答(但 OP 以 OS-dev 为重点),这可能会为您提供更多我所说的同步显示的示例:stackoverflow.com/a/42946600/4271923
  • 非常感谢您的回答!
  • 请注意,CRT 显示器以这些常见的刷新率运行:60、72、75、85、100、120、160、200。某些 PC 游戏(包括 1980 年代和 1990 年代制作的游戏)是多线程(使用他们自己的 DOS 内核),并以独立于视频刷新率的固定频率运行物理线程。在某些情况下,双重或三重缓冲是一种选择(有时是用户指定的),以防止撕裂。
【解决方案3】:

跟进 Jose Manuel Abarca Rodríguez 的回答:

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

如果在你运行游戏的环境中实现了 timer0,你可以使用类似这样的方式来读取它:

;       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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-06-09
    • 1970-01-01
    • 2023-03-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多