Text Mode Cursor: Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
[unchecked revision][unchecked revision]
Content added Content deleted
(Rewrote most of the page)
m (Bot: Replace deprecated source tag with syntaxhighlight)
 
(15 intermediate revisions by 11 users not shown)
Line 1: Line 1:
In [[text mode]], the cursor does not work the same way as in high-level languages, automatically moving to one place after the last written character. Instead, it is simply a blinking area that can be resized, shown, hidden, and moved by the OS.
==Moving the Cursor==
===With the BIOS===


==With the [[BIOS]]==
To move the cursor with the [[BIOS]], use int 0x10 (the interrupt for screen functions). These are the registers used:

To manipulate the cursor with the BIOS, use int 0x10, the interrupt for screen functions.

===Enabling the Cursor===

Enabling the cursor also allows you to set the start and end scanlines, the rows where the cursor starts and ends. The highest scanline is 0 and the lowest scanline is the maximum scanline (usually 15).

* AH = 0x01
* CH = start scanline
* CL = end scanline

===Disabling the Cursor===

* AH = 0x01
* CH = 0x3F (bits 6-7 unused, bit 5 disables cursor, bits 0-4 control cursor shape)

===Moving the Cursor===


* AH = 0x02
* AH = 0x02
* BH = display page (usually, if not always 0)
* BH = display page (usually, if not always 0)
* DH = row
* DH = row
* DL = column
* DL = column

===Get Cursor Data===

* AH = 0x03
* BH = display page (usually, if not always 0)

The return values:

* CH = start scanline
* CL = end scanline
* DH = row
* DL = column

==Without the [[BIOS]]==

Without BIOS access, manipulating the cursor requires sending data directly to the hardware.


===Without the BIOS===
===Enabling the Cursor===


Enabling the cursor also allows you to set the start and end scanlines, the rows where the cursor starts and ends. The highest scanline is 0 and the lowest scanline is the maximum scanline (usually 15).
Without [[BIOS]] access, moving the cursor requires sending data directly to the hardware.


'''Source in C'''
'''Source in C'''


<source lang="c">
<syntaxhighlight lang="c">
void enable_cursor(uint8_t cursor_start, uint8_t cursor_end)
{
outb(0x3D4, 0x0A);
outb(0x3D5, (inb(0x3D5) & 0xC0) | cursor_start);

outb(0x3D4, 0x0B);
outb(0x3D5, (inb(0x3D5) & 0xE0) | cursor_end);
}
</syntaxhighlight>

===Disabling the Cursor===

'''Source in C'''

<syntaxhighlight lang="c">
void disable_cursor()
{
outb(0x3D4, 0x0A);
outb(0x3D5, 0x20);
}
</syntaxhighlight>

'''Source in Assembly'''

<syntaxhighlight lang="asm">
disable_cursor:
pushf
push eax
push edx

mov dx, 0x3D4
mov al, 0xA ; low cursor shape register
out dx, al

inc dx
mov al, 0x20 ; bits 6-7 unused, bit 5 disables the cursor, bits 0-4 control the cursor shape
out dx, al

pop edx
pop eax
popf
ret
</syntaxhighlight>

===Moving the Cursor===

Keep in mind that you don't need to update the cursor's location every time a new character is displayed. It would be faster to instead only update it after printing an entire string.

'''Source in C'''

<syntaxhighlight lang="c">
void update_cursor(int x, int y)
void update_cursor(int x, int y)
{
{
Line 25: Line 108:
outb(0x3D5, (uint8_t) ((pos >> 8) & 0xFF));
outb(0x3D5, (uint8_t) ((pos >> 8) & 0xFF));
}
}
</syntaxhighlight>
</source>

Note that the x and y positions start from 0 in the top-left corner. Keep in mind you don't need to update this every time a new character is displayed. It would be faster to instead only update it after printing an entire string or until a user prompt.


'''Source in Assembly'''
'''Source in Assembly'''


<syntaxhighlight lang="asm">
Since [[BIOS]] services cannot be accessed in [[Long Mode]], the following routine shows how to move cursor without BIOS in VGA text 80x25 (can be altered a bit to fit protected mode):
Cursor:
VGA.Width equ 80


.SetCoords:
<source lang="asm">
; input bx = x, ax = y
; Set cursor position (text mode 80x25)
; modifies ax, bx, dx
; @param BL The row on screen, starts from 0
; @param BH The column on screen, starts from 0
set_cursor:
pushfq
push rax
push rbx
push rcx
push rdx


mov dl, VGA.Width
; unsigned short position = (row*80) + col
mul dl
; AX will contain 'position'
add bx, ax
mov ax, bx
and ax, 0ffh ; set AX to 'row'
mov cl, 80
mul cl ; row * 80
mov cx, bx
shr cx, 8 ; set CX to 'col'
add ax, cx ; + col
mov cx, ax ; store 'position' in CX
; cursor LOW port to vga INDEX register
mov al, 0fh
mov dx, 3d4h ; VGA port 3D4h
out dx, al
mov ax, cx ; restore 'postion' back to AX
mov dx, 3d5h ; VGA port 3D5h
out dx, al ; send to VGA hardware
; cursor HIGH port to vga INDEX register
mov al, 0eh
mov dx, 3d4h ; VGA port 3D4h
out dx, al
mov ax, cx ; restore 'position' back to AX
shr ax, 8 ; get high byte in 'position'
mov dx, 3d5h ; VGA port 3D5h
out dx, al ; send to VGA hardware


.SetOffset:
pop rdx
; input bx = cursor offset
pop rcx
; modifies al, dx
pop rbx
pop rax
popfq
ret
</source>


mov dx, 0x03D4
==Setting the Cursor Start and End Scanlines==
mov al, 0x0F
out dx, al

inc dl
mov al, bl
out dx, al

dec dl
mov al, 0x0E
out dx, al

inc dl
mov al, bh
out dx, al
ret
</syntaxhighlight>

===Get Cursor Position===


With this code, you get: <tt>pos = y * VGA_WIDTH + x</tt>. To obtain the coordinates, just calculate: <tt>y = pos / VGA_WIDTH; x = pos % VGA_WIDTH;</tt>.
To set the cursor start and end scanlines, use the Cursor Start Register (0x0A) and the Cursor End Register (0x0B). The highest scanline is 0 and the lowest scanline is the maximum scanline (usually 15).


'''Source in C'''
'''Source in C'''


<source lang="c">
<syntaxhighlight lang="c">
uint16_t get_cursor_position(void)
void enable_cursor(uint8_t cursor_start, uint8_t cursor_end)
{
{
outb(0x3D4, 0x0A);
uint16_t pos = 0;
outb(0x3D5, (inb(0x3D5) & 0xC0) | cursor_start);
outb(0x3D4, 0x0F);
pos |= inb(0x3D5);

outb(0x3D4, 0x0B);
outb(0x3D4, 0x0E);
outb(0x3D5, (inb(0x3E0) & 0xE0) | cursor_end);
pos |= ((uint16_t)inb(0x3D5)) << 8;
return pos;
}
}
</syntaxhighlight>
</source>


==Font based "graphical" cursor==
To make the cursor be a block instead of a line, try enable_cursor(0, 15).


==Disabling The Cursor==
===Used by===
===With the BIOS===


Back in the DOS days it was quite common not to use the hardware VGA cursor at all, instead overriding the VGA fonts to create an arrow pointer just like in graphical modes. This technique was used in [https://en.wikipedia.org/wiki/Norton_Utilities Norton Utilities] or the DOS version of [[Norton Diskedit]] for example.
To move the cursor with the [[BIOS]], use int 0x10 (the interrupt for screen functions). These are the registers used:


[[Image:Itmouse.png|Arrow cursor in text mode]]
* AH = 0x01
* CH = 0x3F (bits 0-7 unused, bit 5 disables cursor, bits 0-4 control cursor shape)


You can easily spot this font altering cursor by the cursor color: as only the fonts are altered and the attribute bytes are untouched, the pointer changes color as you move it around.
===Without the BIOS===


Here's another example from the Screenshots forum [https://forum.osdev.org/download/file.php?id=2264&mode=view TUI with 8x8 characters and pointing finger-shaped cursor]
Without [[BIOS]] access, disabling the cursor requires sending data directly to the hardware.


===How to implement===
'''Source in C'''


The basic principle is to store 4 bytes (2x2) from the screen, copy their [[VGA Fonts]] to some unused box drawing characters (0xC0 - 0xDF), OR mask the arrow onto then write those 2x2 box drawing characters on screen. Then when the mouse moved, the original 4 bytes are restored on screen, and the whole procedure repeated on the new position.
<source lang="c">
void disable_cursor()
{
outb(0x3D4, 0x0A);
outb(0x3D5, 0x20);
}
</source>


It is important to use box drawing characters, because normally VGA displays fonts as 9x16, adding an empty 9th column, which would cause a "gap" in the pointer. With the box drawing characters that 9th column is a copy of the 8th column, and therefore does not cut the pointer in half. If you use 8x8 characters (like 80x50 or 132x50 modes) then there are no character separator columns, and you are free to use any character you like. On the forum example above you can spot the cursor on the ASCII table at characters 0xF0 - 0xF3.
'''Source in Assembly'''


Although the arrow size is the same as one character (typically 8x16 or 8x8), as the pointer can be moved with pixel precision it can overlap with the next character both horizontally and vertically, thus giving the total 4 bytes requirement:
<source lang="asm">
<pre>
disable_cursor:
char 1 attr 1 char 2 attr 2
pushf
........|????????|........|???????? first line
push rax
........|????????|........|???????? (note attribute bytes are untouched)
push rdx
........|????????|........|????????
....x...|????????|........|????????
....xx..|????????|........|????????
....xxx.|????????|........|????????
....xxxx|????????|........|????????
....xxxx|????????|x.......|????????
--------+--------+--------+--------
....xxxx|????????|xx......|???????? second line
....xxxx|????????|xxx.....|????????
....xxxx|????????|xxxx....|????????
.......x|????????|x.......|????????
.......x|????????|x.......|????????
........|????????|xx......|????????
........|????????|........|????????
........|????????|........|????????
char 3 attr 3 char 4 attr 4
</pre>
You can read the VGA Fonts with BIOS, or if you're already in protected mode, with VGA registers. Read [[VGA Fonts]] article for more information.


The character positions are calculated by dividing mouse coordinates by font size: cx = mx / 8 and cy = my / 16. Then you calculate my % 16 to get the first byte of the font glyph to be modified, and mx % 8 to get the shift value by which you have to shift the arrow mask.
mov dx, 0x3D4 ; one of VGA's Index registers controller
mov al, 0xa ; index 0xa is the LOW cursor shape register
out dx, al


==A Note on GRUB==
inc dx ; one of VGA's data registers, port 0x3D4, allows reads and writes to VGA's registers

mov al, 0x3F ; bits 6-7 must be 0, if bit 5 set the cursor is disabled, bits 0-4 control the cursor shape
If the timeout is set to 0 in your grub.cfg, the cursor will be disabled and you will need to enable it yourself. Otherwise, [[GRUB]] will enable the cursor for you. Because of this inconsistency, it is a good idea to always enable the cursor. Even if you don't set the timeout to 0, you might want to in the future, or someone might change it on their system.
out dx, al
pop rdx
pop rax
popf
ret
</source>


==See Also==
==See Also==
* [http://wiki.osdev.org/VGA_Hardware VGA Hardware]
* [[VGA Hardware]]
* [[Text UI]]


===External Links===
===External Links===
* http://www.bookcase.com/library/dos/ints/int10.html (dead link)
* http://www.osdever.net/FreeVGA/vga/textcur.htm
* https://web.archive.org/web/20080731014051/http://www.bookcase.com:80/library/dos/ints/int10.html
* https://web.archive.org/web/20120324083032/http://www.arl.wustl.edu/~lockwood/class/cs306/books/artofasm/Chapter_13/CH13-2.html
* https://web.archive.org/web/20120324083032/http://www.arl.wustl.edu/~lockwood/class/cs306/books/artofasm/Chapter_13/CH13-2.html
* https://en.wikipedia.org/wiki/VGA-compatible_text_mode
* http://www.osdever.net/FreeVGA/vga/vga.htm


[[Category:Video]]
[[Category:Video]]

Latest revision as of 05:40, 9 June 2024

In text mode, the cursor does not work the same way as in high-level languages, automatically moving to one place after the last written character. Instead, it is simply a blinking area that can be resized, shown, hidden, and moved by the OS.

With the BIOS

To manipulate the cursor with the BIOS, use int 0x10, the interrupt for screen functions.

Enabling the Cursor

Enabling the cursor also allows you to set the start and end scanlines, the rows where the cursor starts and ends. The highest scanline is 0 and the lowest scanline is the maximum scanline (usually 15).

  • AH = 0x01
  • CH = start scanline
  • CL = end scanline

Disabling the Cursor

  • AH = 0x01
  • CH = 0x3F (bits 6-7 unused, bit 5 disables cursor, bits 0-4 control cursor shape)

Moving the Cursor

  • AH = 0x02
  • BH = display page (usually, if not always 0)
  • DH = row
  • DL = column

Get Cursor Data

  • AH = 0x03
  • BH = display page (usually, if not always 0)

The return values:

  • CH = start scanline
  • CL = end scanline
  • DH = row
  • DL = column

Without the BIOS

Without BIOS access, manipulating the cursor requires sending data directly to the hardware.

Enabling the Cursor

Enabling the cursor also allows you to set the start and end scanlines, the rows where the cursor starts and ends. The highest scanline is 0 and the lowest scanline is the maximum scanline (usually 15).

Source in C

void enable_cursor(uint8_t cursor_start, uint8_t cursor_end)
{
	outb(0x3D4, 0x0A);
	outb(0x3D5, (inb(0x3D5) & 0xC0) | cursor_start);

	outb(0x3D4, 0x0B);
	outb(0x3D5, (inb(0x3D5) & 0xE0) | cursor_end);
}

Disabling the Cursor

Source in C

void disable_cursor()
{
	outb(0x3D4, 0x0A);
	outb(0x3D5, 0x20);
}

Source in Assembly

disable_cursor:
	pushf
	push eax
	push edx

	mov dx, 0x3D4
	mov al, 0xA	; low cursor shape register
	out dx, al

	inc dx
	mov al, 0x20	; bits 6-7 unused, bit 5 disables the cursor, bits 0-4 control the cursor shape
	out dx, al

	pop edx
	pop eax
	popf
	ret

Moving the Cursor

Keep in mind that you don't need to update the cursor's location every time a new character is displayed. It would be faster to instead only update it after printing an entire string.

Source in C

void update_cursor(int x, int y)
{
	uint16_t pos = y * VGA_WIDTH + x;

	outb(0x3D4, 0x0F);
	outb(0x3D5, (uint8_t) (pos & 0xFF));
	outb(0x3D4, 0x0E);
	outb(0x3D5, (uint8_t) ((pos >> 8) & 0xFF));
}

Source in Assembly

Cursor:
VGA.Width equ 80

.SetCoords:
; input bx = x, ax = y
; modifies ax, bx, dx

	mov dl, VGA.Width
	mul dl
	add bx, ax

.SetOffset:
; input bx = cursor offset
; modifies al, dx

	mov dx, 0x03D4
	mov al, 0x0F
	out dx, al

	inc dl
	mov al, bl
	out dx, al

	dec dl
	mov al, 0x0E
	out dx, al

	inc dl
	mov al, bh
	out dx, al
	ret

Get Cursor Position

With this code, you get: pos = y * VGA_WIDTH + x. To obtain the coordinates, just calculate: y = pos / VGA_WIDTH; x = pos % VGA_WIDTH;.

Source in C

uint16_t get_cursor_position(void)
{
    uint16_t pos = 0;
    outb(0x3D4, 0x0F);
    pos |= inb(0x3D5);
    outb(0x3D4, 0x0E);
    pos |= ((uint16_t)inb(0x3D5)) << 8;
    return pos;
}

Font based "graphical" cursor

Used by

Back in the DOS days it was quite common not to use the hardware VGA cursor at all, instead overriding the VGA fonts to create an arrow pointer just like in graphical modes. This technique was used in Norton Utilities or the DOS version of Norton Diskedit for example.

Arrow cursor in text mode

You can easily spot this font altering cursor by the cursor color: as only the fonts are altered and the attribute bytes are untouched, the pointer changes color as you move it around.

Here's another example from the Screenshots forum TUI with 8x8 characters and pointing finger-shaped cursor

How to implement

The basic principle is to store 4 bytes (2x2) from the screen, copy their VGA Fonts to some unused box drawing characters (0xC0 - 0xDF), OR mask the arrow onto then write those 2x2 box drawing characters on screen. Then when the mouse moved, the original 4 bytes are restored on screen, and the whole procedure repeated on the new position.

It is important to use box drawing characters, because normally VGA displays fonts as 9x16, adding an empty 9th column, which would cause a "gap" in the pointer. With the box drawing characters that 9th column is a copy of the 8th column, and therefore does not cut the pointer in half. If you use 8x8 characters (like 80x50 or 132x50 modes) then there are no character separator columns, and you are free to use any character you like. On the forum example above you can spot the cursor on the ASCII table at characters 0xF0 - 0xF3.

Although the arrow size is the same as one character (typically 8x16 or 8x8), as the pointer can be moved with pixel precision it can overlap with the next character both horizontally and vertically, thus giving the total 4 bytes requirement:

char 1   attr 1   char 2   attr 2 
........|????????|........|????????       first line
........|????????|........|????????       (note attribute bytes are untouched)
........|????????|........|????????
....x...|????????|........|????????
....xx..|????????|........|????????
....xxx.|????????|........|????????
....xxxx|????????|........|????????
....xxxx|????????|x.......|????????
--------+--------+--------+--------
....xxxx|????????|xx......|????????       second line
....xxxx|????????|xxx.....|????????
....xxxx|????????|xxxx....|????????
.......x|????????|x.......|????????
.......x|????????|x.......|????????
........|????????|xx......|????????
........|????????|........|????????
........|????????|........|????????
char 3   attr 3   char 4   attr 4

You can read the VGA Fonts with BIOS, or if you're already in protected mode, with VGA registers. Read VGA Fonts article for more information.

The character positions are calculated by dividing mouse coordinates by font size: cx = mx / 8 and cy = my / 16. Then you calculate my % 16 to get the first byte of the font glyph to be modified, and mx % 8 to get the shift value by which you have to shift the arrow mask.

A Note on GRUB

If the timeout is set to 0 in your grub.cfg, the cursor will be disabled and you will need to enable it yourself. Otherwise, GRUB will enable the cursor for you. Because of this inconsistency, it is a good idea to always enable the cursor. Even if you don't set the timeout to 0, you might want to in the future, or someone might change it on their system.

See Also

External Links