In the VGA graphics modes the cursor is not displayed but BIOS does keep track of its position. For every available display page, BIOS records the cursor's column and row coordinates (certainly not the X and Y coordinates) in the Cursor Save Area, 16 bytes starting at linear address 0450h. Fun fact: BIOS also updates unnecessarily the CRT Controller registers Cursor Location High and Cursor Location Low.
Since the beginning of time applications that run on a graphics screen therefore have had to create their own cursor, and so I fully realize that I too will have to provide a cursor of my own.
There is one glitch though. Apparently DOS expects users to be able to edit the command line without the help of any cursor when operating on a graphics screen! The same applies to the DOS.BufferedInput function 0Ah invoked from an application.
Then how can I add a cursor to the graphics video modes both within my application and at the command prompt?
(This is a self-answer)
To get cursor functionality both in the user application and on the DOS command line, writing a Terminate and Stay Resident (TSR) program is the solution. And if you add it to AUTOEXEC.BAT you won't have to keep thinking about installing it!
A preliminary concern though. It will harm to force a cursor on an application that was probably written many years ago and was build upon the premise that no cursor exists. Said application will have provided its own cursor.
A cursor available while inputting. It is not desirable to display a cursor in permanence. There's little point in seeing the underline shoot across the screen when characters get outputted. A cursor is useful and necessary when a program expects input from the user at the keyboard.
The cursor driver mainly focusses on the Keyboard BIOS, more specifically functions 00h (10h,20h) GetKeystroke and 01h (11h,21h) CheckForKeystroke. There is no need to also look at any relevant DOS input functions since ultimately those functions will call upon the underlying Keyboard BIOS. This is also true in DOSBox.
The phrase "cursor available while inputting" is tantamount to saying that the cursor should be disabled most of the time (...). Then in order to have the cursor show up automatically while at the command prompt the driver hooks the int28h interrupt that DOS continually invokes while the input is in progress. The int28h signal is used as a temporary enabling of the cursor. Very soon after, the int08h signal will disable the cursor again.
A faithful imitation of the text video cursor. That means an underbar for overwrite mode and an half-cell for insertion mode. The cursor must blink at approximately 2 Hz. To obtain the blinking effect the driver looks at the BIOS 18.2 Hz timer. Every fourth tick the cursor changes phase (ON/OFF). This rate is very close to what we get in a text video mode. The cursor will correctly toggle shape between underbar (for overwriting) and half-cell (for insertion), in accordance with bit 7 of the BIOS.KeyboardFlags located at linear address 0417h. The cursor driver supports the following screen modes: 13: 320x200x4, 14: 640x200x4, 15: 640x350x2, 16: 640x350x4, 17: 640x480x1, 18: 640x480x4, 19: 320x200x8.
A small footprint. My willingness to have any (useful) TSR installed is inversely proportional to the size of the TSR program. I'm happy to say that this TSR is very compact at 624 bytes, including the Memory Control Block (MCB)! Of course to arrive at this small footprint, concessions had to be made resulting in some imperfections (an occasional visual remnant). Going for the perfect cursor could have demanded an inordinate amount of memory.
Reclaim the PSP This is a 3-step process. Firstly the installation program moves the resident code down in memory overwriting most of the Program Segment Prefix (PSP) but guarding to not destroy the important data in front, secondly the DOS.TerminateAndStayResident function 31h is invoked, and thirdly the remaining bytes in the old PSP are used as the driver's background buffer. Reclaiming the entire PSP is possible because this driver never has to invoke any DOS functions!
Limit the api The necessary api was added to the BIOS.GetModeInfo function 0Fh that normally reports about: the video mode number, the number of columns and the active display page. Those items are returned in the AX
and BH
registers. The 2 subfunctions that were added use those same registers. The subfunctions AX=0F01h EBX="CURS"
EnableGraphicsCursor and AX=0F02h EBX="CURS"
DisableGraphicsCursor expect a signature in the EBX
register so as to differentiate old from new. On return the unmodified contents of AX
constitutes prove that the driver is installed, because the normal Video BIOS function 0Fh can never produce these values!
Forget about speed This was one of those ocassions where speed just couldn't be an issue. If you scrutinize the source, you can find many speedwise inefficiencies such as the use of Self Modifying Code and the redundant preservation of a number of VGA registers, but because my cursor object is so terribly small and hardly has to change over time, it does not matter.
; ***************************************
; * GraphicsCursor v1.00 01/10/2020 *
; ***************************************
; Memory map:
; 0000h PixelBuffer ; b7 Graphics mode 1=Yes 0=No
; 0040h CursorCtrl ---> ; b6 Cursor enable 1=Yes 0=No
; 0041h Code (first) ; b5 Int28 1=Yes 0=No
; 025Dh Code (last) ; b4 Cursor shown 1=Yes 0=No
CC=0040h
ZZ=0103h-0041h ; Relocation factor
ORG 256
jmp Start
; --------------------------------------
Modes db 00'0'01000b, 01'0'01000b, 01'1'01110b
db 01'1'01110b, 01'1'10000b, 01'1'10000b, 11'0'01000b
; --------------------------------------
New08: and byte [cs:CC], 11011111b ; Clearing Int28
Old08: jmp 08h*4:New08-ZZ
; --------------------------------------
New28: or byte [cs:CC], 00100000b ; Setting Int28
Old28: jmp 28h*4:New28-ZZ
; --------------------------------------
New10: test ah, ah
jz .SetVideoMode
cmp ebx, "CURS"
jne Old10
cmp ax, 0F01h
jb Old10
je .EnableGraphicsCursor
cmp ax, 0F02h
ja Old10
; - - - - - - - - - - - - - - - - - - -
.DisableGraphicsCursor:
call HideC
and byte [cs:CC], 10011111b
iret
.EnableGraphicsCursor:
or byte [cs:CC], 01000000b
iret
; - - - - - - - - - - - - - - - - - - -
.SetVideoMode:
pushf
Old10_: call 0:0
TestG: pusha
push ds
xor ax, ax
mov ds, ax
mov al, [0449h] ; BIOS.CurrentDisplayMode
push cs
pop ds
and al, 127
sub al, 13 ; Modes [0,12] are unsupported modes
cmp al, 7
jnb .NOK ; Unsupported mode, AH=0
mov bx, Modes-ZZ
xlatb ; -> AL is ModeInfo xx'y'zzzzzb
aam 64
mov [Prep-ZZ+44], ah ; {0=Mode13, 1=Modes[14,18], 3=Mode19}
mov [ShowC-ZZ+10], al ; ModeInfo 00'y'zzzzzb
.OK: mov ah, 10000000b
.NOK: mov [CC], ah ; AH={0,128}
pop ds
popa
iret
; - - - - - - - - - - - - - - - - - - -
Old10: jmp 10h*4:New10-ZZ
; --------------------------------------
New16: cmp byte [cs:CC], 10100000b ; Gfx AND (Cursor enabled OR Int28) ?
jb Old16 ; No
push ax ; (1)
and ah, 11001111b ; Function number
cmp ah, 1
ja .Other ; Not in {00h,01h,10h,11h,20h,21h}
pushf ; (2)
push ds ; (3)
sti
push 0
pop ds
je .CheckForKeystroke
.GetKeystroke:
.Loop: test byte [cs:CC], 01100000b ; (Cursor enabled OR Int28) ?
jz .HideC ; No, Int28 fell off!
test byte [046Ch], 00000100b ; BIOS.Timer CursorPhase
jz .OFF
.ON: call ShowC
mov ax, [041Ah] ; BIOS.KeyboardBufferHead
cmp ax, [041Ch] ; BIOS.KeyboardBufferTail
je .Loop ; No key waiting
.OFF: call HideC
mov ax, [041Ah] ; BIOS.KeyboardBufferHead
cmp ax, [041Ch] ; BIOS.KeyboardBufferTail
je .Loop ; No key waiting
jmp .Done ; Key is available
.CheckForKeystroke:
test byte [046Ch], 00000100b ; BIOS.Timer CursorPhase
jz .HideC
call ShowC
jmp .Done
.HideC: call HideC
.Done: pop ds ; (3)
popf ; (2)
.Other: pop ax ; (1)
Old16: jmp 16h*4:New16-ZZ
; --------------------------------------
; IN (ds=0) OUT ()
ShowC: test byte [cs:CC], 00010000b
jnz .RET ; Already shown
pusha
mov al, 0 ; SMC, ModeInfo 00'y'zzzzzb
aam 32
movzx si, ah ; [0,1]
inc si ; Thickness {1,2,2}
cbw ; CellHeight {8,14,16}
cwd
movzx bx, byte [0462h] ; BIOS.CurrentDisplayPage
shl bx, 1
mov cx, [0450h+bx] ; BIOS.CursorColumn
xchg dl, ch ; BIOS.CursorRow
shl cx, 3 ; -> X
inc dx
imul dx, ax ; -> Y (Below the matrix)
test byte [0417h], 128 ; BIOS.InsertMode ?
jz .a ; No
shr ax, 1 ; Half-cell
mov si, ax
.a: cmp al, 16
jb .b
dec dx
.b: push ds ; (1)
push cs
pop ds
xor di, di ; PixelBuffer
mov [HideC-ZZ+12], si ; Thickness
mov [HideC-ZZ+15], dx ; Y
mov [HideC-ZZ+18], cx ; X
mov bl, 7 ; White
.c: dec dx
.d: call ReadPixel ; -> AL
mov [di], al
inc di
call WritePixel
inc cx ; Next X
test cl, bl ; BL=7
jnz .d
sub cx, 8
dec si
jnz .c
call ReadPixel ; -> AL
mov [HideC-ZZ+26], al ; CursorColor
or byte [CC], 00010000b
pop ds ; (1)
popa
.RET: ret
; --------------------------------------
; IN () OUT ()
HideC: test byte [cs:CC], 00010000b
jz .RET ; Currently not shown
pusha
xor di, di ; PixelBuffer
mov si, 0 ; SMC, Thickness
mov dx, 0 ; SMC, Y
mov cx, 0 ; SMC, X
; First see if our cursor is still there
pusha ; (1)
.a: dec dx ; Next Y
.b: call ReadPixel ; -> AL
cmp al, 0 ; SMC, CursorColor
jne .c ; Not white
inc cx ; Next X
test cl, 7
jnz .b
sub cx, 8
dec si
jnz .a
.c: popa ; (1)
jnz .f ; Impaired cursor: abandon restoration
; Restore background ; and consider it is hidden
.d: dec dx ; Next Y
.e: mov bl, [cs:di]
inc di
call WritePixel
inc cx ; Next X
test cl, 7
jnz .e
sub cx, 8
dec si
jnz .d
.f: and byte [cs:CC], 11101111b
popa
.RET: ret
; --------------------------------------
; IN (cx,dx) OUT (cx,dx=03CEh,ds:si) MOD (al,di)
Prep: mov si, cx ; X
mov di, dx ; Y
push cs
pop ds
mov dx, 03CEh ; -> DX is Graphics Controller
in al, dx ; Read Address register
mov [Rest-ZZ+13], al
mov al, 8
out dx, al
inc dx
in al, dx ; Read BitMask register
dec dx
mov [Rest-ZZ+10], al
mov al, 4
out dx, al
inc dx
in al, dx ; Read ReadMapSelect register
dec dx
mov [Rest-ZZ+6], al
mov al, 5
out dx, al
inc dx
in al, dx ; Read Mode register
mov [Rest-ZZ+2], al
imul di, 40 ; Y
shl di, 2 ; SMC {0 is x40, 1 is x80, 3 is x320}
mov al, 2
cmp [$-ZZ-3], al
pushf ; (1) CF=0 mode 19, CF=1 other modes
jnb @f
out dx, al ; -> Mode register (mode 2)
shr si, 3 ; X
@@: add si, di
push 0
pop ds
add si, [044Eh] ; BIOS.StartCurrentPage
push 0A000h
pop ds ; -> DS:SI is PixelAddress
and cx, 7 ; X Mod 8
dec dx ; -> DX=03CEh
popf ; (1)
ret
; --------------------------------------
; IN (cx,dx) OUT (al)
ReadPixel:
pusha
push ds
call Prep ; -> CX DX=03CEh DS:SI CF (AL DI)
jnc .Is19
.Other: xor cx, 7 ; -> CX is PixelBitNumber
mov bl, 0
mov ax, 0304h ; Plane 3
@@: out dx, ax ; -> Read Map Select register
bt [si], cx
rcl bl, 1
dec ah ; Plane 2 then 1 then 0
jns @b
jmp .Done
.Is19: mov bl, [si]
.Done: mov bp, sp
mov [bp+16], bl ; pusha.AL
; --- --- --- --- --- --- --
Rest: mov ax, 0005h ; SMC, Original Mode register
out dx, ax
mov ax, 0004h ; SMC, Original ReadMapSelect register
out dx, ax
mov ax, 0008h ; SMC, Original BitMask register
out dx, ax
mov al, 00h ; SMC, Original Address register
out dx, al
pop ds
popa
ret
; --------------------------------------
; IN (bl,cx,dx) OUT ()
WritePixel:
pusha
push ds
call Prep ; -> CX DX=03CEh DS:SI CF (AL DI)
jnc .Is19
.Other: mov ax, 8008h
shr ah, cl ; -> AH is PixelMask
out dx, ax ; -> BitMask register
mov cl, [si] ; Dummy read
.Is19: mov [si], bl ; Write color
jmp Rest
; --------------------------------------
db 15 dup 0
; --------------------------------------
Start: cld
; Showing copyright
mov dx, .Logo
mov ah, 09h ; DOS.PrintString
int 21h
; Searching installed copy of this program
mov dx, es ; Scanning memory below this program
mov bx, 0051h ; and above the BIOS vars
.Scan: mov ds, bx ; using a 14-byte signature
mov di, 0103h
mov si, 0041h
mov cx, 14
repe cmpsb
je .Found ; CF=0 means installed
inc bx
cmp bx, dx
jb .Scan
stc ; CF=1 means not installed
.Found: mov ds, dx
pushf ; (1)
; Checking commandline
mov ecx, [0080h]
cmp cx, 0D00h ; C:\>CURSOR
je .Naked
.Text: mov dx, .Self
mov ah, 09h ; DOS.PrintString
int 21h
mov dx, .No
popf ; (1a)
jc .Go ; Not installed
cmp ecx, 0D3F2002h ; C:\>CURSOR ?
je .Is
mov dx, .YesDo1
mov ax, 0F01h
cmp ecx, 0D312002h ; C:\>CURSOR 1
je .Do
mov dx, .Help
cmp ecx, 0D302002h ; C:\>CURSOR 0
jne .Go
mov dx, .YesDo0
mov ax, 0F02h
.Do: mov ebx, "CURS"
int 10h ; -> AX=[0F01h,0F02h]
jmp .Go
.Is: mov es, bx ; -> ES=Segment TSR
mov dx, .YesIs0
test byte [es:CC], 01000000b ; Cursor enabled ?
jz .Go
mov dx, .YesIs1
.Go: jmp .Quit_
; - - - - - - - - - - - - - - - - - - -
; Testing installed
.Naked: popf ; (1b)
jnc .Exist ; Already installed
; Hooking system timer, video BIOS, keyboard, and DOSOK
.New: cli
mov bx, Old08+1
call ChangeIntVect ; -> EAX
mov bx, Old10+1
call ChangeIntVect ; -> EAX
mov [Old10_+1], eax
mov bx, Old16+1
call ChangeIntVect ; -> EAX
mov bx, Old28+1
call ChangeIntVect ; -> EAX
; Reclaiming space from the PSP
mov si, 0103h
mov di, 0041h
@@: movsb
cmp si, Start
jb @b ; (*)
; Setting up some vars depending on current video mode
mov [$+8], cs
pushf ; TestG ends with an 'iret'
call 0:TestG-ZZ
sti
; Freeing the environment
mov es, [002Ch]
mov ah, 49h ; DOS.ReleaseMemory
int 21h
; Ending program but keeping its TSR portion
mov dx, .OK_
mov ah, 09h ; DOS.PrintString
int 21h
mov dx, di ; (*)
shr dx, 4
mov ax, 3100h ; DOS.TerminateAndStayResident
int 21h
; - - - - - - - - - - - - - - - - - - -
; A subsequent invocation w/o parameter removes the TSR from memory
.Exist: mov es, bx ; -> ES=Segment TSR
; Checking ownership interrupt vectors
xor ax, ax
mov ds, ax ; -> DS=Segment IVT
mov dx, .NOK
mov al, 5 ; 'Access denied'
shl ebx, 16
mov bx, New08-ZZ
cmp [08h*4], ebx
jne .Quit
mov bx, New10-ZZ
cmp [10h*4], ebx
jne .Quit
mov bx, New16-ZZ
cmp [16h*4], ebx
jne .Quit
mov bx, New28-ZZ
cmp [28h*4], ebx
jne .Quit
; Unhooking interrupt vectors
mov eax, [es:Old08-ZZ+1]
mov [08h*4], eax
mov eax, [es:Old10-ZZ+1]
mov [10h*4], eax
mov eax, [es:Old16-ZZ+1]
mov [16h*4], eax
mov eax, [es:Old28-ZZ+1]
mov [28h*4], eax
; Taking ownership of the TSR memory
mov ax, es
dec ax
mov ds, ax
mov [0001h], cs ; DOS.MCB.Owner
; Releasing the TSR memory
mov ah, 49h ; DOS.ReleaseMemory
int 21h ; -> AX CF
jc .Quit ; AL={7,9}
; Ending program
mov dx, .OK
.Quit_: mov al, 0 ; 'OK'
.Quit: push cs
pop ds
push ax
mov ah, 09h ; DOS.PrintString
int 21h
pop ax
mov ah, 4Ch ; DOS.Terminate AL={0,5,7,9}
int 21h
; - - - - - - - - - - - - - - - - - - -
.Logo db 'GraphicsCursor v1.00 (c) 2020 Sep Roland', 13, 10, '$'
.Self db 'CURSOR is $'
.Help db 'a driver that adds an input cursor to the', 13, 10
db 'graphics modes: 13: 320x200x4, 14: 640x200x4, 15: 640x350x2', 13, 10
db ' 16: 640x350x4, 17: 640x480x1, 18: 640x480x4, 19: 320x200x8', 13, 10
db 'Use: CURSOR (un)install driver', 13, 10
db ' CURSOR ? report status', 13, 10
db ' CURSOR 1 enable cursor', 13, 10
db ' CURSOR 0 disable cursor', 13, 10, '$'
.No db 'currently not installed', 13, 10, '$'
.YesIs0 db 'installed and currently disabled', 13, 10, '$'
.YesIs1 db 'installed and currently enabled', 13, 10, '$'
.YesDo0 db 'installed and now disabled', 13, 10, '$'
.YesDo1 db 'installed and now enabled', 13, 10, '$'
.OK_ db 'CURSOR loaded', 13, 10, '$'
.OK db 'CURSOR unloaded', 13, 10, '$'
.NOK db 'Failed to unload CURSOR', 13, 10, '$'
; --------------------------------------
; IN (bx) OUT (eax)
ChangeIntVect:
push si
mov si, cs
xchg si, [bx+2] ; -> SI is offset in IVT
push ds ; (1)
xor ax, ax
mov ds, ax
mov eax, [cs:bx]
xchg eax, [si]
pop ds ; (1)
mov [bx], eax
pop si
ret
; --------------------------------------
To install the driver just run the naked CURSOR.COM program.
To uninstall the driver just run the naked CURSOR.COM program again.
When installed you can communicate with the driver.
From within an application you use the new Video BIOS subfunctions:
AX=0F01h EBX="CURS"
EnableGraphicsCursorAX=0F02h EBX="CURS"
DisableGraphicsCursorAt the command prompt you run CURSOR.COM with a command tail:
CURSOR ?
Reports whether the cursor is currently enabled or disabled.CURSOR 1
Enables the cursor now. (Added for DOSBox)CURSOR 0
Disables the cursor now. (Added for DOSBox)CURSOR *
Shows help text. (* is any text)In order to minimize the impact on non-aware applications, the driver is installed with the cursor disabled by default. An aware application that operates in the graphics mode needs to enable the cursor explicitly. It is recommended to enable and also disable the cursor closely around any input procedure. Remember the driver was not designed to provide an omnipresent cursor!
Just like normal MS-DOS (6.20), DOSBox (0.74) does not show any cursor while in a graphics mode. Installing the driver will provide one!
However:
Some time ago I posted the Rich Edit Form Input program on CodeReview. It is an application that is all about inputting. Although the program does not target the graphics screens specifically, there's nothing in the program to prevent it from running on a graphics screen. Just the lack of a cursor would then be really annoying.
Well... No longer if today's CURSOR driver is installed. And because all inputs in this application use DOS input functions, the cursor will appear automatically if running on a true DOS. If on DOSBox you will have to enable the cursor manually from a command prompt.