diff options
Diffstat (limited to 'mubios/drivers/vbe.c')
| -rw-r--r-- | mubios/drivers/vbe.c | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/mubios/drivers/vbe.c b/mubios/drivers/vbe.c new file mode 100644 index 0000000..c85ecf5 --- /dev/null +++ b/mubios/drivers/vbe.c @@ -0,0 +1,433 @@ +/* + RCC-Project + + Copyright (C) 2026 Richard Qin + + This file is part of RCC-Project. + + RCC-Project is free software; you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free + Software Foundation; either version 3 of the License, or (at your option) any + later version. + + RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + RCC-Project; see the file COPYING. If not see + <http://www.gnu.org/licenses/>. + */ + +#include "vbe.h" +#include "io.h" +#include "vga.h" + +#define PCI_CONFIG_ADDRESS 0x0CF8u +#define PCI_CONFIG_DATA 0x0CFCu +#define PCI_COMMAND 0x04u +#define PCI_BAR0 0x10u +#define PCI_BAR2 0x18u +#define PCI_MEMORY_MASK 0xFFFFFFF0u +#define QEMU_VGA_ID 0x11111234u +/* Allocate below the platform's high MMIO/firmware area, never in RAM. + This is an allocation ceiling, not a framebuffer physical address. */ +#define PCI_MMIO_CEILING 0xF0000000u +#define DISPI_INDEX 0x01CEu +#define DISPI_DATA 0x01CFu +#define DISPI_ID 0u +#define DISPI_MEMORY_64K 0x0Au +#define DISPI_ENABLED 0x01u +#define DISPI_GETCAPS 0x02u +#define DISPI_LINEAR 0x40u +#define DISPI_PRESERVE 0x80u + +static const struct vbe_mode modes[VBE_MODE_COUNT] + = { { 0x111u, 640u, 480u, 16u }, { 0x112u, 640u, 480u, 24u }, + { 0x142u, 640u, 480u, 32u }, { 0x114u, 800u, 600u, 16u }, + { 0x115u, 800u, 600u, 24u }, { 0x143u, 800u, 600u, 32u }, + { 0x117u, 1024u, 768u, 16u }, { 0x118u, 1024u, 768u, 24u }, + { 0x144u, 1024u, 768u, 32u }, { 0x11Au, 1280u, 1024u, 16u }, + { 0x11Bu, 1280u, 1024u, 24u }, { 0x145u, 1280u, 1024u, 32u }, + { 0x11Eu, 1600u, 1200u, 16u }, { 0x11Fu, 1600u, 1200u, 24u }, + { 0x147u, 1600u, 1200u, 32u } }; + +struct vbe_state vbe; + +static u32 +pci_read (u16 devfn, u8 offset) +{ + io_out32 (PCI_CONFIG_ADDRESS, + 0x80000000u | ((u32)devfn << 8) | (offset & 0xFCu)); + return io_in32 (PCI_CONFIG_DATA); +} + +static void +pci_write (u16 devfn, u8 offset, u32 value) +{ + io_out32 (PCI_CONFIG_ADDRESS, + 0x80000000u | ((u32)devfn << 8) | (offset & 0xFCu)); + io_out32 (PCI_CONFIG_DATA, value); +} + +static void +pci_write_command (u16 devfn, u16 command) +{ + io_out32 (PCI_CONFIG_ADDRESS, 0x80000000u | ((u32)devfn << 8) | PCI_COMMAND); + /* Do not write back the write-one-to-clear PCI status word. */ + io_out16 (PCI_CONFIG_DATA, command); +} + +u16 +vbe_read (u16 index) +{ + io_out16 (DISPI_INDEX, index); + return io_in16 (DISPI_DATA); +} + +void +vbe_write (u16 index, u16 value) +{ + io_out16 (DISPI_INDEX, index); + io_out16 (DISPI_DATA, value); +} + +static u32 +pci_ram_end (void) +{ + u16 units = (u16)cmos_read8 (0x34u) | ((u16)cmos_read8 (0x35u) << 8); + u16 kib; + + /* QEMU CMOS 34h/35h describes RAM above 16 MiB in 64 KiB units. + For smaller guests, 30h/31h describes RAM above 1 MiB in KiB. */ + if (units != 0u) + return 0x01000000u + ((u32)units << 16); + kib = (u16)cmos_read8 (0x30u) | ((u16)cmos_read8 (0x31u) << 8); + return 0x00100000u + ((u32)kib << 10); +} + +static u32 +pci_allocation_limit (u16 vga_devfn, u32 ram_end) +{ + u32 limit = PCI_MMIO_CEILING; + u32 value; + u32 base; + u16 devfn; + u8 header; + u8 offset; + + /* The default QEMU pc VGA is on root bus 0. Allocate below all existing + root-bus memory resources, leaving their full ranges untouched. */ + for (devfn = 0; devfn < 256u; ++devfn) + { + if (devfn == vga_devfn || (u16)pci_read (devfn, 0u) == 0xFFFFu) + continue; + header = (u8)(pci_read (devfn, 0x0Cu) >> 16) & 0x7Fu; + for (offset = 0x10u; offset < (header == 0u ? 0x28u : 0x18u); + offset += 4u) + { + value = pci_read (devfn, offset); + /* Bit 0 identifies I/O BARs; a 64-bit BAR has type bits 10b. */ + if ((value & 1u) != 0u) + continue; + base = value & PCI_MEMORY_MASK; + if ((value & 6u) == 4u) + { + offset += 4u; + if (pci_read (devfn, offset) != 0u) + continue; + } + if (base >= ram_end && base < limit) + limit = base; + } + /* Bridge windows and PCI expansion ROMs also occupy address space. */ + if (header == 1u) + { + base = (pci_read (devfn, 0x20u) & 0xFFF0u) << 16; + if (base >= ram_end && base < limit) + limit = base; + base = (pci_read (devfn, 0x24u) & 0xFFF0u) << 16; + if (pci_read (devfn, 0x28u) == 0u && base >= ram_end && base < limit) + limit = base; + } + value = pci_read (devfn, header == 1u ? 0x38u : 0x30u); + base = value & 0xFFFFF800u; + if (base >= ram_end && base < limit) + limit = base; + } + return limit; +} + +static u32 +pci_bar_size (u16 devfn, u8 offset) +{ + u32 saved = pci_read (devfn, offset); + u32 mask; + + /* PCI sizing is done with memory decoding disabled by the caller. */ + pci_write (devfn, offset, 0xFFFFFFFFu); + mask = pci_read (devfn, offset); + pci_write (devfn, offset, saved); + /* This driver uses QEMU standard VGA's 32-bit memory BARs only. */ + if ((mask & 7u) != 0u || (mask & PCI_MEMORY_MASK) == 0u) + return 0u; + return ~(mask & PCI_MEMORY_MASK) + 1u; +} + +static int +pci_configure_vga (u16 devfn) +{ + u16 command = (u16)pci_read (devfn, PCI_COMMAND); + u32 old_framebuffer = pci_read (devfn, PCI_BAR0); + u32 old_registers = pci_read (devfn, PCI_BAR2); + u32 ram_end = pci_ram_end (); + u32 limit = pci_allocation_limit (devfn, ram_end); + u32 framebuffer_size; + u32 registers_size; + u32 framebuffer; + u32 registers = 0u; + + pci_write_command (devfn, (u16)(command & ~3u)); + framebuffer_size = pci_bar_size (devfn, PCI_BAR0); + registers_size = pci_bar_size (devfn, PCI_BAR2); + /* PCI memory BARs must fit, aligned to their probed power-of-two size, + entirely above guest RAM and below the platform MMIO ceiling. */ + if (framebuffer_size == 0u || framebuffer_size > limit) + goto fail; + framebuffer = (limit - framebuffer_size) & ~(framebuffer_size - 1u); + if (framebuffer < ram_end) + goto fail; + if (registers_size != 0u) + { + if (registers_size > framebuffer) + goto fail; + registers = (framebuffer - registers_size) & ~(registers_size - 1u); + if (registers < ram_end) + goto fail; + } + + pci_write (devfn, PCI_BAR0, framebuffer | (old_framebuffer & 0xFu)); + if (registers_size != 0u) + pci_write (devfn, PCI_BAR2, registers | (old_registers & 0xFu)); + /* A rejected BAR assignment must not be exposed as a usable framebuffer. */ + if ((pci_read (devfn, PCI_BAR0) & PCI_MEMORY_MASK) != framebuffer + || (registers_size != 0u + && (pci_read (devfn, PCI_BAR2) & PCI_MEMORY_MASK) != registers)) + goto fail; + + pci_write_command (devfn, (u16)(command | 3u)); + vbe.framebuffer = pci_read (devfn, PCI_BAR0) & PCI_MEMORY_MASK; + vbe.memory_size = (u32)vbe_read (DISPI_MEMORY_64K) << 16; + /* The CPU mapping must cover all advertised video memory. */ + if (vbe.memory_size == 0u || vbe.memory_size > framebuffer_size) + goto fail; + return 1; + +fail: + pci_write_command (devfn, (u16)(command & ~3u)); + pci_write (devfn, PCI_BAR0, old_framebuffer); + pci_write (devfn, PCI_BAR2, old_registers); + pci_write_command (devfn, command); + vbe.framebuffer = 0u; + vbe.memory_size = 0u; + return 0; +} + +static int +mode_supported (const struct vbe_mode *mode) +{ + /* Advertise only modes within DISPI capabilities and installed VRAM. */ + return vbe.present && mode->width <= vbe.max_width + && mode->height <= vbe.max_height && mode->bpp <= vbe.max_bpp + && (u32)mode->width * mode->height * (mode->bpp / 8u) + <= vbe.memory_size; +} + +const struct vbe_mode * +vbe_find_mode (u16 number) +{ + u8 index; + + for (index = 0; index < VBE_MODE_COUNT; ++index) + if (modes[index].number == number && mode_supported (&modes[index])) + return &modes[index]; + return (const struct vbe_mode *)0; +} + +void +vbe_init (void) +{ + u16 devfn; + u16 enable; + u8 index; + u8 count = 0u; + + vbe.current_mode = 0x03u; + for (devfn = 0; devfn < 256u; ++devfn) + if (pci_read (devfn, 0u) == QEMU_VGA_ID + && (pci_read (devfn, 0x08u) >> 16) == 0x0300u) + break; + /* Absence of the supported PCI device must leave text VGA available. */ + if (devfn == 256u) + return; + vbe_write (DISPI_ID, 0xB0C5u); + if (vbe_read (DISPI_ID) != 0xB0C5u || !pci_configure_vga (devfn)) + return; + + enable = vbe_read (DISPI_ENABLE); + vbe_write (DISPI_ENABLE, (u16)(enable | DISPI_GETCAPS)); + vbe.max_width = vbe_read (DISPI_XRES); + vbe.max_height = vbe_read (DISPI_YRES); + vbe.max_bpp = vbe_read (DISPI_BPP); + vbe_write (DISPI_ENABLE, enable); + vbe.present = 1u; + for (index = 0; index < VBE_MODE_COUNT; ++index) + if (mode_supported (&modes[index])) + vbe.mode_list[count++] = modes[index].number; + vbe.mode_list[count] = 0xFFFFu; +} + +void +vbe_leave_graphics (void) +{ + if (vbe.present) + vbe_write (DISPI_ENABLE, 0u); + vbe.current_mode = 0x03u; +} + +static void +graphics_registers (void) +{ + /* DISPI sets graphics timing; enable VGA planes and attribute output too. */ + io_out8 (0x03C4u, 0x01u); + io_out8 (0x03C5u, 0x01u); + io_out8 (0x03C4u, 0x02u); + io_out8 (0x03C5u, 0x0Fu); + io_out8 (0x03C4u, 0x04u); + io_out8 (0x03C5u, 0x0Eu); + io_out8 (0x03CEu, 0x05u); + io_out8 (0x03CFu, 0x40u); + io_out8 (0x03CEu, 0x06u); + io_out8 (0x03CFu, 0x05u); + io_in8 (0x03DAu); + io_out8 (0x03C0u, 0x10u); + io_out8 (0x03C0u, 0x41u); + io_in8 (0x03DAu); + io_out8 (0x03C0u, 0x20u); +} + +u16 +vbe_set_mode (u16 request) +{ + const struct vbe_mode *mode; + volatile u8 *bda = (volatile u8 *)MUBIOS_BDA_BASE; + u16 saved[10]; + u16 index; + u16 enable; + u16 number = request & 0x3FFFu; + u8 old_video_ctl = bda[BDA_VIDEO_CTL]; + volatile u32 *pixel; + u32 words; + + if (number == 0x03u) + { + vga_text_mode_set ((u8)((request >> 8) & 0x80u)); + return VBE_OK; + } + mode = vbe_find_mode (number); + /* Only LFB access is implemented. Bit 11 requests a VBE 3.0 CRTC block; + banked modes and custom timings are not supported or advertised. */ + if (mode == (const struct vbe_mode *)0 || (request & VBE_MODE_LINEAR) == 0u) + return VBE_FAILED; + + for (index = 0; index < 10u; ++index) + saved[index] = vbe_read (index); + vbe_write (DISPI_ENABLE, 0u); + vbe_write (DISPI_BPP, mode->bpp); + vbe_write (DISPI_XRES, mode->width); + vbe_write (DISPI_YRES, mode->height); + vbe_write (DISPI_BANK, 0u); + /* Delay clearing until the actual programmed mode has been verified. */ + enable = DISPI_ENABLED | DISPI_LINEAR | DISPI_PRESERVE; + vbe_write (DISPI_ENABLE, enable); + if (vbe_read (DISPI_XRES) != mode->width + || vbe_read (DISPI_YRES) != mode->height + || vbe_read (DISPI_BPP) != mode->bpp + || vbe_read (DISPI_VIRT_WIDTH) != mode->width + || (vbe_read (DISPI_ENABLE) & (DISPI_ENABLED | DISPI_LINEAR)) + != (DISPI_ENABLED | DISPI_LINEAR)) + { + /* Restore the prior hardware state on a rejected mode request. */ + vbe_write (DISPI_ENABLE, 0u); + for (index = 1u; index <= DISPI_BPP; ++index) + vbe_write (index, saved[index]); + vbe_write (DISPI_ENABLE, saved[DISPI_ENABLE] | DISPI_PRESERVE); + vbe_write (DISPI_BANK, saved[DISPI_BANK]); + vbe_write (DISPI_VIRT_WIDTH, saved[DISPI_VIRT_WIDTH]); + vbe_write (DISPI_X_OFFSET, saved[DISPI_X_OFFSET]); + vbe_write (DISPI_Y_OFFSET, saved[DISPI_Y_OFFSET]); + vbe_write (DISPI_ENABLE, saved[DISPI_ENABLE]); + /* Disabling DISPI alone does not restore text CRTC/graphics registers. + */ + if ((saved[DISPI_ENABLE] & DISPI_ENABLED) == 0u) + { + vga_text_mode_set (0x80u); + bda[BDA_VIDEO_CTL] = old_video_ctl; + vbe.current_mode + = (u16)(0x03u | ((u16)(old_video_ctl & 0x80u) << 8)); + } + return VBE_FAILED; + } + + graphics_registers (); + if ((request & VBE_MODE_PRESERVE) == 0u) + { + pixel = (volatile u32 *)vbe.framebuffer; + words = vbe.memory_size / sizeof (*pixel); + while (words-- != 0u) + *pixel++ = 0u; + enable &= (u16)~DISPI_PRESERVE; + vbe_write (DISPI_ENABLE, enable); + } + vbe.current_mode = request; + /* FFh denotes a non-legacy video mode; no mode-03h geometry is claimed. */ + bda[BDA_VIDEO_MODE] = 0xFFu; + *(volatile u16 *)(bda + BDA_VIDEO_COLUMNS) = mode->width / 8u; + *(volatile u16 *)(bda + BDA_VIDEO_PAGE_SIZE) = 0u; + *(volatile u16 *)(bda + BDA_VIDEO_PAGE_START) = 0u; + bda[BDA_VIDEO_PAGE] = 0u; + for (index = 0u; index < VGA_PAGE_COUNT; ++index) + *(volatile u16 *)(bda + BDA_CURSOR_BASE + index * 2u) = 0u; + *(volatile u16 *)(bda + BDA_CURSOR_SHAPE) = 0u; + bda[BDA_VIDEO_ROWS] = (u8)(mode->height / VGA_FONT_HEIGHT - 1u); + *(volatile u16 *)(bda + BDA_VIDEO_HEIGHT) = VGA_FONT_HEIGHT; + bda[BDA_VIDEO_CTL] = (u8)(0x60u | ((request >> 8) & 0x80u)); + return VBE_OK; +} + +u16 +vbe_get_mode (u16 *number) +{ + u8 index; + u16 enable = vbe_read (DISPI_ENABLE); + + /* GRUB's native Bochs driver may program DISPI without calling INT 10h. + Query hardware instead of returning a stale BIOS mode number. */ + if ((enable & DISPI_ENABLED) == 0u) + { + *number = (u16)(0x03u | (vbe.current_mode & VBE_MODE_PRESERVE)); + return VBE_OK; + } + for (index = 0; index < VBE_MODE_COUNT; ++index) + if (mode_supported (&modes[index]) + && modes[index].width == vbe_read (DISPI_XRES) + && modes[index].height == vbe_read (DISPI_YRES) + && modes[index].bpp == vbe_read (DISPI_BPP)) + { + *number = modes[index].number + | ((enable & DISPI_LINEAR) != 0u ? VBE_MODE_LINEAR : 0u) + | ((enable & DISPI_PRESERVE) != 0u ? VBE_MODE_PRESERVE : 0u); + return VBE_OK; + } + return VBE_FAILED; +} |
