Search code examples
assemblydosx86-16biosvga

How can I add a blinking cursor to the graphics video modes?


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 )


Solution

  • 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.

    Design choices for this cursor driver (TSR)

    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
    ; --------------------------------------
    

    How to use

    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" EnableGraphicsCursor
    • AX=0F02h EBX="CURS" DisableGraphicsCursor

    At 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!

    DOSBox is special

    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:

    • Because, unlike normal DOS, DOSBox does never invoke the int28h interrupt while the input is in progress, the user that wants a blinking cursor at the command prompt will have to enable the cursor manually. Just issue the command "CURSOR 1".
    • Although DOSBox updates bit 7 of the BIOS.KeyboardFlags located at linear address 0417h when the Ins key is pressed, it exclusively operates in Insertion mode. Therefore the cursor driver will change the appearance of the cursor but that will remain a purely cosmetic change.
    • DOSBox 0.74 does not support the monochrome screen 15: 640x350x2

    An unaware application

    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.