User:Omarrx024/VESA Tutorial

From OSDev Wiki
Jump to navigation Jump to search

VESA Tutorial

Hello. Today, I'm posting specifically to teach people who want graphics all about VESA. That means that in less than 30 minutes, you'll be able to have a high-color graphics world in your OS! So anyway, VESA is the Video Electronics Standard Association. They are the people who made the VBE, or the VESA BIOS Extensions Standard. Like the name implies, it is an optional BIOS extension. But I think it's safe to say that 99% of PCs built since the late 1990's have VESA support. Note that UEFI GOP has succeeded VESA BIOS Extensions, but UEFI implementations also have a legacy VESA-compaitble BIOS for backwards-compatibility.

Anyway let's have a little history lesson... In 1991, VESA released VBE version 1.2, which was mostly compatible with the previous 1.0 and 1.1 but had more popularity. Here, VESA defined some "video mode numbers" which are mostly the same idea as VGA mode numbers (e.g 0x03 is text 80x25, 0x13 is graphics 320x200x8bpp, ...) and they also defined "bank switching." The idea worked like this: you call the VESA BIOS and it sets a video mode for you. Then, because 1991 was still considered "DOS days," you write to 0xA000:0x0000 (linear 0xA0000) to draw to the screen. But that buffer only had a maximum of 64 KB because it is a 16-bit segment... So they invented bank switching; in which the BIOS divided the video memory into smaller chunks called banks, and you'll switch banks as you need to while drawing. For example, in a 640x480x8bpp VESA mode, we need 300 KB of video memory. But unfortunately, we can only access the video memory as 64 KB segments... So if we want to draw to the entire screen, first we switch to bank 0, which is the first 64 KB of the screen, then we draw; then we switch to bank 1, which is the second 64 KB of the screen, then we draw to the same address; then we switch to bank 2, and so on... This had a severe performance penalty, and bank switching is deprecated today, and has been succeeded by linear framebuffers, which are discussed later in this tutorial.

Here are some modes the VESA defined with VBE 1.x:

MODE    RESOLUTION  BITS PER PIXEL  MAXIMUM COLORS
0x0100  640x400     8               256
0x0101  640x480     8               256
0x0102  800x600     4               16
0x0103  800x600     8               256
0x010D  320x200     15              32k
0x010E  320x200     16              64k
0x010F  320x200     24/32*          16m
0x0110  640x480     15              32k
0x0111  640x480     16              64k
0x0112  640x480     24/32*          16m
0x0113  800x600     15              32k
0x0114  800x600     16              64k
0x0115  800x600     24/32*          16m
0x0116  1024x768    15              32k
0x0117  1024x768    16              64k
0x0118  1024x768    24/32*          16m

Notes:

  • Some BIOSes support 24-bit color, in which each color is an RGB value, in which each color component is 8 bits; giving a total of 16 million available colors. Other BIOSes support 32-bit color, in which color is also an RGB value and each color component is still 8 bits, but there is an empty 8 bits at the top, which are known as the alpha channel; so 32-bit color is often called RGBA color while 24-bit color is often called RGB color. This is done for two reasons: as a stub for software implementing alpha blending, as to speed up memory operations by 32-bit alignment. Note that the values are in little endian.

Then in 1994, VESA defined VBE 2.0 and this was a major improvement, although "most" VBE 2.0+ BIOSes are compatible with VBE 1.x. Anyway, in VBE 2.0, VESA defined the "linear framebuffer" which was a place in high memory (3 to 4 GB) that had a totally contiguous framebuffer; bank switching was deprecated although is still supported in the BIOS for backwards-compatibility. VESA also stated that all the modes that they defined in VBE 1.x are also deprecated, and that hardware manufacturers didn't need to support them, and that anyone can make up any modes they feel like. Most hardware vendors still support the standard VBE 1.x modes for backward-compatibility, but you should NEVER depend on that, because one day you'll find a PC on which your code just won't work. But wait a minute... How can we set VESA modes without knowing the mode numbers? Well, VESA gave us two things: a function that returns an array of all available mode numbers, and another function that gets the details of a specified mode number (width, height, bpp, linear framebuffer address, etc...) The basic idea is that we query the BIOS for the information of every available mode, and when we find a mode that fits our needs, we can use it. Since mode numbers are not standard, you should NEVER assume mode numbers, width of a mode, height of a mode, or bpp of the mode. For example, my laptop has VESA mode 0x0118 as 1024x768x32 while older software assumed it to be a 24-bit mode. Newer software may assume it to be a 32-bit mode; while Bochs and QEMU emulate it as a 24-bit mode. Anyway, you should NEVER assume VESA modes and should always query the BIOS for what it supports.

That's enough theory for today, I guess. Let's take a look on how to actually use the VESA BIOS Extensions! VESA put its VBE functions at function 0x4F of BIOS interrupt 0x10. You put the function number 0x4F in AH register, the subfunction number in AL register, parameters in other registers, and call INT 0x10. All VESA calls return 0x004F in AX on success. Any other return code should be taken as an error.

Here are some useful functions that can be used with VBE 2.0+:

FUNCTION: Get VESA BIOS information

Function code: 0x4F00

Description: Returns the VESA BIOS information, including manufacturer, supported modes, available video memory, etc... Input: AX = 0x4F00

Input: ES:DI = Segment:Offset pointer to where to store VESA BIOS information structure.

Output: AX = 0x004F on success, other values indicate that VESA BIOS is not supported.

Anyway, the above function returns the following structure and stores it in ES:DI as they were on entry. On entry, ES:DI should contain a pointer to the following structure:

vbe_info_structure:
	.signature		db "VBE2"	; indicate support for VBE 2.0+
	.table_data:		resb 512-4	; reserve space for the table below

After the BIOS call, if it succeeded (AX is 0x004F), then the same structure above now contains the following:

struct vbe_info_structure {
	char[4] signature = "VESA";	// must be "VESA" to indicate valid VBE support
	uint16_t version;			// VBE version; high byte is major version, low byte is minor version
	uint32_t oem;			// segment:offset pointer to OEM
	uint32_t capabilities;		// bitfield that describes card capabilities
	uint32_t video_modes;		// segment:offset pointer to list of supported video modes
	uint16_t video_memory;		// amount of video memory in 64KB blocks
	uint16_t software_rev;		// software revision
	uint32_t vendor;			// segment:offset to card vendor string
	uint32_t product_name;		// segment:offset to card model name
	uint32_t product_rev;		// segment:offset pointer to product revision
	char reserved[222];		// reserved for future expansion
	char oem_data[256];		// OEM BIOSes store their strings in this area
} __attribute__ ((packed));

Notice that all segment:offset fields are in little-endian, which means the low word is the offset, and the high word is the segment. Things that might be of interest to you from the above structure: "signature" will be changed from "VBE2" to "VESA". It must be "VBE2" on entry to indicate software support for VBE 2.0. If it contains "VBE2", the BIOS will return the 512 bytes of data for VBE 2.0+. If it contains "VESA", the BIOS will return 256 bytes of data for VBE 1.x. If it is not "VESA" after the call, you should assume that VESA BIOS Extensions are not available. "version" tells you the version of VBE; 0x100 is 1.0, 0x101 is 1.1, 0x102 is 1.2, 0x200 is 2.0, and 0x300 is 3.0 (the latest version). VBE 1.x returns 256 bytes of data in the above structure, VBE 2.0 and 3.0 return 512 bytes of data if the "signature" field contained "VBE2" on entry. "video_modes" is a segment:offset pointer to the list of supported video modes. Each entry in the array is a 16-bit word, and is terminated by a 0xFFFF. If while searching for your mode, you find a 0xFFFF, then the mode is not supported. "video_memory" contains how much VGA RAM the PC has in 64 KB chunks. So, to have it in KB, multiply the value in "video_memory" by 64. Anyway, about the supported modes array, if the PC supports modes 0x0103, 0x0115, and 0x0118, the array would look like this in a hexdump:

03 01 15 01 18 01 FF FF

Notice how it is terminated by a 0xFFFF and all values are in little-endian.

FUNCTION: Get VESA mode information

Function code: 0x4F01

Description: This function returns the mode information structure for a specified mode. The mode number should be gotten from the supported modes array.

Input: AX = 0x4F01

Input: CX = VESA mode number from the video modes array

Input: ES:DI = Segment:Offset pointer of where to store the VESA Mode Information Structure shown below.

Output: AX = 0x004F on success, other values indicate a BIOS error or a mode-not-supported error.

Here's the structure returned by this function in ES:DI:

struct vbe_mode_info_structure {
	uint16_t attributes;		// deprecated, only bit 7 should be of interest to you, and it indicates the mode supports a linear frame buffer.
	uint8_t window_a;			// deprecated
	uint8_t window_b;			// deprecated
	uint16_t granularity;		// deprecated; used while calculating bank numbers
	uint16_t window_size;
	uint16_t segment_a;
	uint16_t segment_b;
	uint32_t win_func_ptr;		// deprecated; used to switch banks from protected mode without returning to real mode
	uint16_t pitch;			// number of bytes per horizontal line
	uint16_t width;			// width in pixels
	uint16_t height;			// height in pixels
	uint8_t w_char;			// unused...
	uint8_t y_char;			// ...
	uint8_t planes;
	uint8_t bpp;			// bits per pixel in this mode
	uint8_t banks;			// deprecated; total number of banks in this mode
	uint8_t memory_model;
	uint8_t bank_size;		// deprecated; size of a bank, almost always 64 KB but may be 16 KB...
	uint8_t image_pages;
	uint8_t reserved0;

	uint8_t red_mask;
	uint8_t red_position;
	uint8_t green_mask;
	uint8_t green_position;
	uint8_t blue_mask;
	uint8_t blue_position;
	uint8_t reserved_mask;
	uint8_t reserved_position;
	uint8_t direct_color_attributes;

	uint32_t framebuffer;		// physical address of the linear frame buffer; write here to draw to the screen
	uint32_t off_screen_mem_off;
	uint16_t off_screen_mem_size;	// size of memory in the framebuffer but not being displayed on the screen
	uint8_t reserved1[206];
} __attribute__ ((packed));

Lots of useless sh*t, I know. The only things that interest us: "attributes" bit 7 (value 0x80) indicates the mode supports a linear frame buffer. "width", "height" are "bpp" are used while searching for the mode we want to use. "framebuffer" is the 32-bit physical pointer to the linear framebuffer. The linear framebuffer must be enabled while setting the VBE mode, which is discussed in the next function. If you are using paging, be sure to map the framebuffer somewhere known in the virtual address space!

FUNCTION: Set VBE mode

Function code: 0x4F02

Description: This function sets a VBE mode.

Input: AX = 0x4F02

Input: BX = Bits 0-13 mode number; bit 14 is the LFB bit: when set, it enables the linear framebuffer, when clear, software must use bank switching. Bit 15 is the DM bit: when set, the BIOS doesn't clear the screen. Bit 15 is usually ignored and should always be cleared.

Output: AX = 0x004F on success, other values indicate errors; such as BIOS error, too little video memory, unsupported VBE mode, mode doesn't support linear frame buffer, or any other error.

So that means, if VBE mode 0x0118 is 1024x768x32bpp, and we wanted to set this mode and ask the BIOS to clear the screen for us, we can do this:

mov ax, 0x4F02	; set VBE mode
mov bx, 0x4118	; VBE mode number; notice that bits 0-13 contain the mode number and bit 14 (LFB) is set and bit 15 (DM) is clear.
int 0x10			; call VBE BIOS
cmp ax, 0x004F	; test for error
jne error

after:
	; ...

Anyway, like I mentioned at least a hundred times, you should first get the mode number from the video modes array. Those mode numbers only have the plain mode numbers (0x0118, 0x0103, etc...) and you should set bit 14 when you set the VBE mode.

FUNCTION: Get current VBE mode

Function code: 0x4F03

Description: This function returns the current VBE mode.

Input: AX = 0x4F03

Output: AX = 0x004F on success, other values indicate errors; maybe the system is not in a VBE mode?

Output: BX = Bits 0-13 mode number; bit 14 is the LFB bit: when set, the system is using the linear framebuffer, when clear, the system is using bank switching. Bit 15 is the DM bit: when clear, video memory was cleared when the VBE mode was set.

FUNCTION: Display Window Control (deprecated)

Function code: 0x4F05

Description: Sets/Gets the current bank (deprecated)

Input: AX = 0x4F05

Input: BH = 0x00 to set bank, 0x01 to get bank

Input: BL = 0x00 for window A, 0x01 for window B

Input: DX = Bank number in window granularity units

Output: AX = 0x004F on success, other values indicate errors; such as BIOS errors, unusable bank, unpresent bank, not using banked mode...

Because bank switching is deprecated, I will not go into how to calculate bank numbers.

FUNCTION: Return protected mode interface

Function code: 0x4F0A

Description: Returns the VBE 2.0 protected mode interface

Input: AX = 0x4F0A

Input: BL = 0x00

Output: AX = 0x004F on success, other values indicate errors; such as BIOS errors, protected mode interface not supported, or VBE version is less than 2.0

Output: ES:DI = Segment:Offset pointer to protected mode table

Output: CX = Length of table including code in bytes for copying purposes

This function allows software to switch banks (function 0x4F05), set display start (function 0x4F07) and set primary pallette data (function 0x4F09) from protected mode, without needing to return to real mode. This increases performance somewhat, but is overall deprecated and may not be supported in some BIOSes. Anyway, the table returned by ES:DI looks like this:

struct vbe2_pmi_table {
	uint16_t set_window;		// offset in table for protected mode code for function 0x4F05
	uint16_t set_display_start;	// offset in table for protected mode code for function 0x4F07
	uint16_t set_pallette;		// offset in table for protected mode code for function 0x4F09
} __attribute__ ((packed));

That means, to set the bank from protected mode, one should first find the linear address of the set_window function, by doing ES * 0x10 + DI + [ES:DI] and then doing a near CALL, because all protected mode functions end with a near RET. Useless, I know, but perhaps some people want to use bank switching as a fallback when VBE 2.0 is not available, or when the specific mode doesn't support a linear frame buffer.

By now, your understanding of VESA should be quite clear and your implementation of VESA shouldn't be that difficult. Note, that if you use v8086 to call VESA BIOS, many video cards have memory-mapped register in high memory (3-4 GB) and the BIOS will have to switch to protected mode transparently to you during VBE execution, which goes beyond limits of v8086. For this problem, it is recommended to configure VESA in plain 16-bit real mode, before 32-bit/64-bit mode.

Need code? Here's a function to set a VESA mode directly from a specified width/height/bpp, copied and pasted from my OS code. Notice that it runs in 16-bit real mode with DS = ES = FS = GS = 0.

; vbe_set_mode:
; Sets a VESA mode
; In\	AX = Width
; In\	BX = Height
; In\	CL = Bits per pixel
; Out\	FLAGS = Carry clear on success
; Out\	Width, height, bpp, physical buffer, all set in vbe_screen structure

vbe_set_mode:
	mov [.width], ax
	mov [.height], bx
	mov [.bpp], cl

	sti

	push es					; some VESA BIOSes destroy ES, or so I read
	mov ax, 0x4F00				; get VBE BIOS info
	mov di, vbe_info_block
	int 0x10
	pop es

	cmp ax, 0x4F				; BIOS doesn't support VBE?
	jne .error

	mov ax, word[vbe_info_block.video_modes]
	mov [.offset], ax
	mov ax, word[vbe_info_block.video_modes+2]
	mov [.segment], ax

	mov ax, [.segment]
	mov fs, ax
	mov si, [.offset]

.find_mode:
	mov dx, [fs:si]
	add si, 2
	mov [.offset], si
	mov [.mode], dx
	mov ax, 0
	mov fs, ax

	cmp [.mode], 0xFFFF			; end of list?
	je .error

	push es
	mov ax, 0x4F01				; get VBE mode info
	mov cx, [.mode]
	mov di, mode_info_block
	int 0x10
	pop es

	cmp ax, 0x4F
	jne .error

	mov ax, [.width]
	cmp ax, [mode_info_block.width]
	jne .next_mode

	mov ax, [.height]
	cmp ax, [mode_info_block.height]
	jne .next_mode

	mov al, [.bpp]
	cmp al, [mode_info_block.bpp]
	jne .next_mode

	; If we make it here, we've found the correct mode!
	mov ax, [.width]
	mov word[vbe_screen.width], ax
	mov ax, [.height]
	mov word[vbe_screen.height], ax
	mov eax, [mode_info_block.framebuffer]
	mov dword[vbe_screen.physical_buffer], eax
	mov ax, [mode_info_block.pitch]
	mov word[vbe_screen.bytes_per_line], ax
	mov eax, 0
	mov al, [.bpp]
	mov byte[vbe_screen.bpp], al
	shr eax, 3
	mov dword[vbe_screen.bytes_per_pixel], eax

	mov ax, [.width]
	shr ax, 3
	dec ax
	mov word[vbe_screen.x_cur_max], ax

	mov ax, [.height]
	shr ax, 4
	dec ax
	mov word[vbe_screen.y_cur_max], ax

	; Set the mode
	push es
	mov ax, 0x4F02
	mov bx, [.mode]
	or bx, 0x4000			; enable LFB
	mov di, 0			; not sure if some BIOSes need this... anyway it doesn't hurt
	int 0x10
	pop es

	cmp ax, 0x4F
	jne .error

	clc
	ret

.next_mode:
	mov ax, [.segment]
	mov fs, ax
	mov si, [.offset]
	jmp .find_mode

.error:
	stc
	ret

.width				dw 0
.height				dw 0
.bpp				db 0
.segment			dw 0
.offset				dw 0
.mode				dw 0

What's next? Well, that's entirely up to you! Just kidding, here's some tips on implementing a graphics library. Calculating pixel offset:

uint32 pixel_offset = y * pitch + (x * (bpp/8)) + framebuffer;

Where "pitch", "bpp" and "framebuffer" are gotten from the structure returned by function 0x4F01. Then, you can plot a pixel by writing a value there. For 32-bit modes, each pixel value is 0x00RRGGBB in little endian, so to plot a red pixel, we would write 0x00FF0000. In 24-bit mode, each pixel is 0xRRGGBB in little endian. Due to the bad memory alignment, performance is relatively low with 24-bit modes. In 16-bit modes, we have a total of 64K colors, and the color has 5 bits of red, 6 bits of green, and 5 bits of blue. There is more green because according to Wikipedia, the human eye is more sensitive to green than red and blue... In 15-bit modes, we have a total of 32K colors, 5 bits of red, 5 bits of green and 5 bits of blue. The highest bit (bit 15, value 0x8000) is unused. Because my OS supports 32-bit/24-bit modes and 16-bit as a fallback, I will focus on these. Anyway if you want something less than 16-bit your limiting yourself to the obsolete technology... Just by getting the pixel's offset and writing there, you've implemented a put_pixel() routine! Now, we want some more things: lines, squares, alpha blending, and some hot graphics! It may seem easy to implement a fill_rect() routine: just put_pixel() everything. That's going to work, but it's not going to work fast. For a 32*32 rectangle, you'll be calculating the pixel offset 1024 times! Instead, a better approach is to calculate the pixel offset one time, and then (REP STOSD/REP STOSW) for one line. Then you can do (offset = offset + pitch) just to skip one line down. This way, you can fill a 512x512 rectangle yet calculate the pixel offset only one time. By calling put_pixel() for every pixel, you'd be calculating the pixel offset 262144 times! Using the method I just said, you can make performance 262144 times better. For drawing lines, you'll probably implement an existing algorithm. Wikipedia has a good article on drawing lines. As for alpha blending, Wikipedia also has good information on that. Now that you have a linear framebuffer, a basic put_pixel() routine and a fill_rect() routine, you can implement a basic graphics library. Programming graphics now is just the same as non-OS graphics programming. Enjoy!

Keep reading!

VBE 2.0 Specification

Drawing Text

Double Buffering - a way of increasing graphics performance

Alpha blending