How should DOSBox-aware DOS program implement delay function? It should be non-CPU-consuming and non-frequency-dependent.
I have heard that there is a HLT instruction that halts CPU until interrupt occurs. Maybe one could set IRQ 0 handler and then HLT. Interrupt handler should wake up CPU when sleep is over.
The first one is working in NTVDM and also said to be working fine in DOSEmu (decreasing CPU usage from 90% to 0.1%). However, it seems to be not working in DOSBox.
The second one is working fine in DOSBox. I observe that this interrupt is implemented internally, so I can count on performance increase. I can't say for sure about performance, but at least delay times do not drift depending on CPU usage by other programs. The second one doesn't work outside of DOSBox 🙁
if (!CPU_CycleAutoAdjust && CPU_Cycles>0)
CPU_Cycles=0;
These lines might probably be the optimisation, but I didn't noticed CPU usage decrease (maybe it wasn't implemented in 0.72 yet? After all, 0.72 is two years old)
My Internet investigations shows that those who patch RTLs, patch it with INT 2F/AX=1680 method. So will I.
Another alternative to tweaking CPU cycles would be to tweak priority instead. Putting DOSBox.exe into idle priority serves the same aim: it prevents DOSBox from disturbing other programs.
Well the easiest thing to do is really lower the cycles (from a dos app you can
use the z:\config.exe and stuff to do that), but of course it depends on what
you want to do.
1uses CRT, DOS; 2 3{ INT 2F/AX=1680 } 4 5{$Q-} 6{ Disable overflow checks 7 Support timer wraparounds, 8 though didn't checked if it actually helps } 9 10const 11 TimerRemainder : LongInt = 0; 12 { Accumulates the remainders } 13 { When ms is being converted to ticks, 14 the remainder is usually being thrown out; 15 This variable accumulates remainders. 16 The intention is to make some delays 17 one tick longer, so that rounded delay 18 interval will be close to the specified 19 amount of ms. 20 } 21 22procedure YieldDelay (ms: Word); 23var 24 Timer : LongInt absolute $0040:$006C; 25 TimerSave, 26 TimerWait : LongInt; { amount of ticks to wait } 27begin 28 if ms = 0 then Exit; 29 30 TimerSave := Timer; 31 { 32 Timer increments about every 55ms (18.2Hz) 33 More precisely, it increments 1573040 ticks per day (24hours) 34 TimerWait is thus (ms * 1573040) / 24 * 60 * 60. 35 This values are big, they might overflow LongInt. 36 I have reduced them: 37 1573040 ticks per 86400000 milliseconds (24:00:00) 38 157304 ticks per 8640000 milliseconds (02:24:00) 39 78652 ticks per 4320000 milliseconds (01:12:00) 40 39326 ticks per 2160000 milliseconds (00:36:00) 41 19663 ticks per 1080000 milliseconds (00:18:00) 42 The values are rather precise, they should scale good. 43 It is better then TimerWait := ms div 55; 44 } 45 TimerWait := (LongInt(ms) * LongInt(19663)); 46 TimerWait := TimerWait + TimerRemainder; 47 TimerRemainder := TimerWait mod LongInt(1080000); 48 TimerWait := TimerWait div LongInt(1080000); 49 { 50 0 <= ms <= 2^16; 0 <= 19663 <= 2^15; 0 <= ms * 19663 <= 2^31 51 All the values should be fitting boundaries under any 52 circumstances (though I didn't checked it) 53 54 Given the fact that max delay is 65.5 seconds, this might be not 55 worth the trouble 56 } 57 while (Timer - TimerSave) < TimerWait do 58 asm 59 push ax; mov ax, $1680; int $2f; pop ax 60 end;
…Show last 16 lines
61 { Returns the timeslice to the multitasker } 62 63 {Timer - TimerSave -- there might be wraparound here, 64 that's why I used $Q- } 65end; 66 67 68var Hour, Minute, Second, Sec100 : Word; 69 70begin 71 repeat 72 GetTime(Hour, Minute, Second, Sec100); 73 WriteLn(Hour : 2, ':', Minute : 2, ':', Second : 2, '.', Sec100 : 2); 74 YieldDelay(1000); 75 until KeyPressed; 76end.
1uses CRT, DOS; 2 3{ HLT } 4 5{$Q-} 6{ Disable overflow checks 7 Support timer wraparounds, 8 though didn't checked if it actually helps } 9 10const 11 TimerRemainder : LongInt = 0; 12 { Accumulates the remainders } 13 { When ms is being converted to ticks, 14 the remainder is usually being thrown out; 15 This variable accumulates remainders. 16 The intention is to make some delays 17 one tick longer, so that rounded delay 18 interval will be close to the specified 19 amount of ms. 20 } 21 22procedure HLTDelay (ms: Word); 23var 24 Timer : LongInt absolute $0040:$006C; 25 TimerSave, 26 TimerWait : LongInt; { amount of ticks to wait } 27begin 28 if ms = 0 then Exit; 29 30 TimerSave := Timer; 31 { 32 Timer increments about every 55ms (18.2Hz) 33 More precisely, it increments 1573040 ticks per day (24hours) 34 TimerWait is thus (ms * 1573040) / 24 * 60 * 60. 35 This values are big, they might overflow LongInt. 36 I have reduced them: 37 1573040 ticks per 86400000 milliseconds (24:00:00) 38 157304 ticks per 8640000 milliseconds (02:24:00) 39 78652 ticks per 4320000 milliseconds (01:12:00) 40 39326 ticks per 2160000 milliseconds (00:36:00) 41 19663 ticks per 1080000 milliseconds (00:18:00) 42 The values are rather precise, they should scale good. 43 It is better then TimerWait := ms div 55; 44 } 45 TimerWait := (LongInt(ms) * LongInt(19663)); 46 TimerWait := TimerWait + TimerRemainder; 47 TimerRemainder := TimerWait mod LongInt(1080000); 48 TimerWait := TimerWait div LongInt(1080000); 49 { 50 0 <= ms <= 2^16; 0 <= 19663 <= 2^15; 0 <= ms * 19663 <= 2^31 51 All the values should be fitting boundaries under any 52 circumstances (though I didn't checked it) 53 54 Given the fact that max delay is 65.5 seconds, this might be not 55 worth the trouble 56 } 57 while (Timer - TimerSave) < TimerWait do 58 asm 59 hlt 60 end;
…Show last 16 lines
61 { Powers down the CPU for the moment } 62 63 {Timer - TimerSave -- there might be wraparound here, 64 that's why I used $Q- } 65end; 66 67 68var Hour, Minute, Second, Sec100 : Word; 69 70begin 71 repeat 72 GetTime(Hour, Minute, Second, Sec100); 73 WriteLn(Hour : 2, ':', Minute : 2, ':', Second : 2, '.', Sec100 : 2); 74 HLTDelay(1000); 75 until KeyPressed; 76end.
So far, we have:
CRTDelay -- hogs, drifts very much
YieldDelay -- hogs
BIOSDelay -- hogs, drifts a bit
HLTDelay -- works
This is how everything works in DOSBox.
Outside of DOSBox (summary of facts):
BIOSDelay -- should not be used, not portable
HLTDelay -- mostly works; is reported to crash on Windows 2000
YieldDelay -- mostly works; is reported to result in long delays (8 seconds) on Windows 3.1
The basic scheme I use (not counting my recent measurements) would be to try INT 2F/AX=1680. According to interrupt list, it should return 00 in AL if supported and 80h if not.
1TimerSave := Timer; 2 3if EnvType = NotDetectedYet then 4 EnvType := try_int_2f_ax; 5 6if EnvType = MultiTasker then 7 while Timer .... do 8 int 2f/ax=1680 9else 10 while Timer ... do 11 hlt;
An expanded version is in the bottom of my post.
This scheme should properly work on Windows 2000 and true DOS systems with CPU that can be powered down (e. g. DOS Laptops)
DOSBox, however, doesn't fit into this scheme. It reports "supported" on int 2f/ax=1680, but hlt is indeed better on dosbox.
So far, I also need to detect DOSBox (and HLT in this case), detect Win 3.1 (and I don't know what to do in this case).
1{$Q-} 2{ Disable overflow checks 3 Support timer wraparounds, 4 though didn't checked if it actually helps } 5 6const 7 TimerRemainder : LongInt = 0; 8 { Accumulates the remainders } 9 { When ms is being converted to ticks, 10 the remainder is usually being thrown out; 11 This variable accumulates remainders. 12 The intention is to make some delays 13 one tick longer, so that rounded delay 14 interval will be close to the specified 15 amount of ms. 16 } 17 TestYield : Byte = 0; 18 { 0 -- didn't tested yet 19 1 -- no multitasker detected, using HLT 20 2 -- multitasker detected, using INT 2F 21 22 This intetion is to do our best on every 23 known platform. 24 25 HLT lowers CPU usage on pure DOS systems. 26 INT 2F/AX=1680 yields a timeslice in a 27 multitasking environment 28 29 HLT can also yield a timeslice, but it is 30 reported that HLT crashes NTVDM on Windows 31 2000. The hybrid method implemented here 32 won't use HLT in presence of multitasker. On 33 Windows 2000, it will use INT 2F/AX=1680, and 34 won't crash. 35 } 36 37procedure Delay (ms: Word); 38var 39 Timer : LongInt absolute $0040:$006C; 40 TimerSave, 41 TimerWait : LongInt; { amount of ticks to wait } 42begin 43 if ms = 0 then Exit; 44 45 TimerSave := Timer; 46 { 47 Timer increments about every 55ms (18.2Hz) 48 More precisely, it increments 1573040 ticks per day (24hours) 49 TimerWait is thus (ms * 1573040) / 24 * 60 * 60. 50 This values are big, they might overflow LongInt. 51 I have reduced them: 52 1573040 ticks per 86400000 milliseconds (24:00:00) 53 157304 ticks per 8640000 milliseconds (02:24:00) 54 78652 ticks per 4320000 milliseconds (01:12:00) 55 39326 ticks per 2160000 milliseconds (00:36:00) 56 19663 ticks per 1080000 milliseconds (00:18:00) 57 The values are rather precise, they should scale good. 58 It is better then TimerWait := ms div 55; 59 } 60 TimerWait := (LongInt(ms) * LongInt(19663));
…Show last 41 lines
61 TimerWait := TimerWait + TimerRemainder; 62 TimerRemainder := TimerWait mod LongInt(1080000); 63 TimerWait := TimerWait div LongInt(1080000); 64 { 65 0 <= ms <= 2^16; 0 <= 19663 <= 2^15; 0 <= ms * 19663 <= 2^31 66 All the values should be fitting boundaries under any 67 circumstances (though I didn't checked it) 68 69 Given the fact that max delay is 65.5 seconds, this might be not 70 worth the trouble 71 } 72 if TestYield = 0 then 73 asm 74 push ax; mov ax, $1680; int $2f 75 or al,al 76 jz @1 77 mov TestYield,1 78 jmp @2 79 @1: 80 mov TestYield,2 81 @2: 82 pop ax 83 end; 84 85 if TestYield = 1 then 86 while (Timer - TimerSave) < TimerWait do 87 asm 88 hlt 89 end 90 { Powers down the CPU for the moment } 91 else 92 while (Timer - TimerSave) < TimerWait do 93 asm 94 push ax; mov ax, $1680; int $2f; pop ax 95 end; 96 { Returns the timeslice to the multitasker } 97 98 {Timer - TimerSave -- there might be wraparound here, 99 that's why I used $Q- } 100end;
If they start dosbox i assume they play a game, and those rarely idle around,
but feel free to provide full-fledged code that has no side effects for that "issue".
According to sources (dosbox/src/ints/bios_keyboard.cpp), when no key is pressed yet, DOSBox starts executing NOPs and eventually goes into the same callback. I have replaced (I observe that BIOS is writable) the last NOP(opcode 90h) with HLT(opcode 0f4h), and it indeed caused CPU usage decrease when I run int 16h with ah=00h. However, I can see no difference when int 16h is being run with ah=10h (this is how it is invoked mostly).