User:Omarrx024/VESA Tutorial
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!