From 13ba4bbbed4d9f3f5773d6c203d44ec623a9d613 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Sun, 9 Jul 2017 16:39:56 +0100 Subject: [PATCH] Implement vesa console driver for 8bpp framebuffers --- src/gopheros/device/video/console/vesa_fb.go | 328 +++++++ .../device/video/console/vesa_fb_test.go | 815 ++++++++++++++++++ 2 files changed, 1143 insertions(+) create mode 100644 src/gopheros/device/video/console/vesa_fb.go create mode 100644 src/gopheros/device/video/console/vesa_fb_test.go diff --git a/src/gopheros/device/video/console/vesa_fb.go b/src/gopheros/device/video/console/vesa_fb.go new file mode 100644 index 0000000..8a6cb42 --- /dev/null +++ b/src/gopheros/device/video/console/vesa_fb.go @@ -0,0 +1,328 @@ +package console + +import ( + "gopheros/device" + "gopheros/device/video/console/font" + "gopheros/kernel" + "gopheros/kernel/hal/multiboot" + "gopheros/kernel/kfmt" + "gopheros/kernel/mem" + "gopheros/kernel/mem/pmm" + "gopheros/kernel/mem/vmm" + "image/color" + "io" + "reflect" + "unsafe" +) + +type VesaFbConsole struct { + bpp uint32 + fbPhysAddr uintptr + fb []uint8 + + // Console dimensions in pixels + width uint32 + height uint32 + + // offsetY specifies a the pixel offset for the beginning for text. + // The rows of the framebuffer between 0 and offsetY are reserved and + // cannot be used for displaying text. + offsetY uint32 + + // Size of a row in bytes + pitch uint32 + + // Console dimensions in characters + font *font.Font + widthInChars uint32 + heightInChars uint32 + + palette color.Palette + defaultFg uint8 + defaultBg uint8 + clearChar uint16 +} + +func NewVesaFbConsole(width, height uint32, bpp uint8, pitch uint32, fbPhysAddr uintptr) *VesaFbConsole { + return &VesaFbConsole{ + bpp: uint32(bpp), + fbPhysAddr: fbPhysAddr, + width: width, + height: height, + pitch: pitch, + // light gray text on black background + defaultFg: 7, + defaultBg: 0, + clearChar: uint16(' '), + } +} + +// SetFont selects a bitmap font to be used by the console. +func (cons *VesaFbConsole) SetFont(f *font.Font) { + if f == nil { + return + } + + cons.font = f + cons.widthInChars = cons.width / uint32(f.GlyphWidth) + cons.heightInChars = (cons.height - cons.offsetY) / uint32(f.GlyphHeight) +} + +// Dimensions returns the console width and height in the specified dimension. +func (cons *VesaFbConsole) Dimensions(dim Dimension) (uint32, uint32) { + switch dim { + case Characters: + return cons.widthInChars, cons.heightInChars + default: + return cons.width, cons.height + } +} + +// DefaultColors returns the default foreground and background colors +// used by this console. +func (cons *VesaFbConsole) DefaultColors() (fg uint8, bg uint8) { + return cons.defaultFg, cons.defaultBg +} + +// Fill sets the contents of the specified rectangular region to the requested +// color. Both x and y coordinates are 1-based. +func (cons *VesaFbConsole) Fill(x, y, width, height uint32, _, bg uint8) { + if cons.font == nil { + return + } + + // clip rectangle + if x == 0 { + x = 1 + } else if x >= cons.widthInChars { + x = cons.widthInChars + } + + if y == 0 { + y = 1 + } else if y >= cons.heightInChars { + y = cons.heightInChars + } + + if x+width-1 > cons.widthInChars { + width = cons.widthInChars - x + 1 + } + + if y+height-1 > cons.heightInChars { + height = cons.heightInChars - y + 1 + } + + pX := (x - 1) * cons.font.GlyphWidth + pY := (y - 1) * cons.font.GlyphHeight + pW := width * cons.font.GlyphWidth + pH := height * cons.font.GlyphHeight + switch cons.bpp { + case 8: + cons.fill8(pX, pY, pW, pH, bg) + } +} + +// fill8 implements a fill operation using an 8bpp framebuffer. +func (cons *VesaFbConsole) fill8(pX, pY, pW, pH uint32, bg uint8) { + fbRowOffset := cons.fbOffset(pX, pY) + for ; pH > 0; pH, fbRowOffset = pH-1, fbRowOffset+cons.pitch { + for fbOffset := fbRowOffset; fbOffset < fbRowOffset+pW; fbOffset++ { + cons.fb[fbOffset] = bg + } + } +} + +// Scroll the console contents to the specified direction. The caller +// is responsible for updating (e.g. clear or replace) the contents of +// the region that was scrolled. +func (cons *VesaFbConsole) Scroll(dir ScrollDir, lines uint32) { + if cons.font == nil || lines == 0 || lines > cons.heightInChars { + return + } + + offset := cons.fbOffset(0, lines*cons.font.GlyphHeight-cons.offsetY) + + switch dir { + case ScrollDirUp: + startOffset := cons.fbOffset(0, 0) + endOffset := cons.fbOffset(0, cons.height-lines*cons.font.GlyphHeight-cons.offsetY) + for i := startOffset; i < endOffset; i++ { + cons.fb[i] = cons.fb[i+offset] + } + case ScrollDirDown: + startOffset := cons.fbOffset(0, lines*cons.font.GlyphHeight) + for i := uint32(len(cons.fb) - 1); i >= startOffset; i-- { + cons.fb[i] = cons.fb[i-offset] + } + } +} + +// Write a char to the specified location. If fg or bg exceed the supported +// colors for this console, they will be set to their default value. Both x and +// y coordinates are 1-based +func (cons *VesaFbConsole) Write(ch byte, fg, bg uint8, x, y uint32) { + if x < 1 || x > cons.widthInChars || y < 1 || y > cons.heightInChars || cons.font == nil { + return + } + + pX := (x - 1) * cons.font.GlyphWidth + pY := (y - 1) * cons.font.GlyphHeight + + switch cons.bpp { + case 8: + cons.write8(ch, fg, bg, pX, pY) + } +} + +// write8 writes a charactero using an 8bpp framebuffer. +func (cons *VesaFbConsole) write8(glyphIndex, fg, bg uint8, pX, pY uint32) { + var ( + fontOffset = uint32(glyphIndex) * cons.font.BytesPerRow * cons.font.GlyphHeight + fbRowOffset = cons.fbOffset(pX, pY) + fbOffset uint32 + x, y uint32 + mask uint8 + ) + + for y = 0; y < cons.font.GlyphHeight; y, fbRowOffset, fontOffset = y+1, fbRowOffset+cons.pitch, fontOffset+1 { + fbOffset = fbRowOffset + fontRowData := cons.font.Data[fontOffset] + mask = 1 << 7 + for x = 0; x < cons.font.GlyphWidth; x, fbOffset, mask = x+1, fbOffset+1, mask>>1 { + // If mask becomes zero while we are still in this loop + // then the font uses > 1 byte per row. We need to + // fetch the next byte and reset the mask. + if mask == 0 { + fontOffset++ + fontRowData = cons.font.Data[fontOffset] + mask = 1 << 7 + } + + if (fontRowData & mask) != 0 { + cons.fb[fbOffset] = fg + } else { + cons.fb[fbOffset] = bg + } + } + } +} + +// fbOffset returns the linear offset into the framebuffer that corresponds to +// the pixel at (x,y). +func (cons *VesaFbConsole) fbOffset(x, y uint32) uint32 { + return ((y + cons.offsetY) * cons.pitch) + (x * cons.bpp >> 3) +} + +// Palette returns the active color palette for this console. +func (cons *VesaFbConsole) Palette() color.Palette { + return cons.palette +} + +// SetPaletteColor updates the color definition for the specified +// palette index. Passing a color index greated than the number of +// supported colors should be a no-op. +func (cons *VesaFbConsole) SetPaletteColor(index uint8, rgba color.RGBA) { + cons.palette[index] = rgba + + // Only program the DAC when we are in indexed (8bpp) mode + if cons.bpp > 8 { + return + } + + // Load palette entry to the DAC. Each DAC entry is a 6-bit value so + // we need to scale the RGB values in the [0-63] range. + portWriteByteFn(0x3c8, index) + portWriteByteFn(0x3c9, rgba.R>>2) + portWriteByteFn(0x3c9, rgba.G>>2) + portWriteByteFn(0x3c9, rgba.B>>2) +} + +// loadDefaultPalette is called during driver initialization to setup the +// console palette. Regardless of the framebuffer depth, the console always +// uses a 256-color palette. +func (cons *VesaFbConsole) loadDefaultPalette() { + cons.palette = make(color.Palette, 256) + + egaPalette := []color.RGBA{ + color.RGBA{R: 0, G: 0, B: 0}, /* black */ + color.RGBA{R: 0, G: 0, B: 128}, /* blue */ + color.RGBA{R: 0, G: 128, B: 1}, /* green */ + color.RGBA{R: 0, G: 128, B: 128}, /* cyan */ + color.RGBA{R: 128, G: 0, B: 1}, /* red */ + color.RGBA{R: 128, G: 0, B: 128}, /* magenta */ + color.RGBA{R: 64, G: 64, B: 1}, /* brown */ + color.RGBA{R: 128, G: 128, B: 128}, /* light gray */ + color.RGBA{R: 64, G: 64, B: 64}, /* dark gray */ + color.RGBA{R: 0, G: 0, B: 255}, /* light blue */ + color.RGBA{R: 0, G: 255, B: 1}, /* light green */ + color.RGBA{R: 0, G: 255, B: 255}, /* light cyan */ + color.RGBA{R: 255, G: 0, B: 1}, /* light red */ + color.RGBA{R: 255, G: 0, B: 255}, /* light magenta */ + color.RGBA{R: 255, G: 255, B: 1}, /* yellow */ + color.RGBA{R: 255, G: 255, B: 255}, /* white */ + } + + // Load default EFA palette for colors 0-16 + var index int + for ; index < len(egaPalette); index++ { + cons.SetPaletteColor(uint8(index), egaPalette[index]) + } + + // Set all other colors to black + for ; index < len(cons.palette); index++ { + cons.SetPaletteColor(uint8(index), egaPalette[0]) + } +} + +// DriverName returns the name of this driver. +func (cons *VesaFbConsole) DriverName() string { + return "vesa_fb_console" +} + +// DriverVersion returns the version of this driver. +func (cons *VesaFbConsole) DriverVersion() (uint16, uint16, uint16) { + return 0, 0, 1 +} + +// DriverInit initializes this driver. +func (cons *VesaFbConsole) DriverInit(w io.Writer) *kernel.Error { + // Map the framebuffer so we can write to it + fbSize := mem.Size(cons.height * cons.pitch) + fbPage, err := mapRegionFn( + pmm.Frame(cons.fbPhysAddr>>mem.PageShift), + fbSize, + vmm.FlagPresent|vmm.FlagRW, + ) + + if err != nil { + return err + } + + cons.fb = *(*[]uint8)(unsafe.Pointer(&reflect.SliceHeader{ + Len: int(fbSize), + Cap: int(fbSize), + Data: fbPage.Address(), + })) + + kfmt.Fprintf(w, "mapped framebuffer to 0x%x\n", fbPage.Address()) + + cons.loadDefaultPalette() + + return nil +} + +// probeForVesaFbConsole checks for the presence of a vga text console. +func probeForVesaFbConsole() device.Driver { + var drv device.Driver + + fbInfo := getFramebufferInfoFn() + if fbInfo.Type == multiboot.FramebufferTypeIndexed { + drv = NewVesaFbConsole(fbInfo.Width, fbInfo.Height, fbInfo.Bpp, fbInfo.Pitch, uintptr(fbInfo.PhysAddr)) + } + + return drv +} + +func init() { + ProbeFuncs = append(ProbeFuncs, probeForVesaFbConsole) +} diff --git a/src/gopheros/device/video/console/vesa_fb_test.go b/src/gopheros/device/video/console/vesa_fb_test.go new file mode 100644 index 0000000..ca958fb --- /dev/null +++ b/src/gopheros/device/video/console/vesa_fb_test.go @@ -0,0 +1,815 @@ +package console + +import ( + "bytes" + "fmt" + "gopheros/device" + "gopheros/device/video/console/font" + "gopheros/kernel" + "gopheros/kernel/cpu" + "gopheros/kernel/hal/multiboot" + "gopheros/kernel/mem" + "gopheros/kernel/mem/pmm" + "gopheros/kernel/mem/vmm" + "image/color" + "reflect" + "strings" + "testing" +) + +func TestVesaFbTextDimensions(t *testing.T) { + var cons Device = NewVesaFbConsole(16, 32, 8, 16, 0) + + if w, h := cons.Dimensions(Characters); w != 0 || h != 0 { + t.Fatalf("expected console dimensions to be 0x0 before setting a font; got %dx%d", w, h) + } + + specs := []struct { + offsetY uint32 + font *font.Font + expW, expH uint32 + }{ + {0, mockFont8x10, 2, 3}, + {6, mockFont8x10, 2, 2}, + } + + // Setting a nil font should be a no-op + cons.(FontSetter).SetFont(nil) + if w, h := cons.Dimensions(Characters); w != 0 || h != 0 { + t.Fatalf("expected console character dimensions to be 0x0; got %dx%d", w, h) + } + + for specIndex, spec := range specs { + cons.(*VesaFbConsole).offsetY = spec.offsetY + cons.(FontSetter).SetFont(spec.font) + + if w, h := cons.Dimensions(Characters); w != spec.expW || h != spec.expH { + t.Fatalf("[spec %d] expected console character dimensions to be %dx%d; got %dx%d", specIndex, spec.expW, spec.expH, w, h) + } + + if w, h := cons.Dimensions(Pixels); w != 16 || h != 32 { + t.Fatalf("[spec %d] expected console pixel dimensions to be 16x32; got %dx%d", specIndex, w, h) + } + } +} + +func TestVesaFbDefaultColors(t *testing.T) { + var cons Device = NewVesaFbConsole(16, 32, 8, 16, 0) + if fg, bg := cons.DefaultColors(); fg != 7 || bg != 0 { + t.Fatalf("expected console default colors to be fg:7, bg:0; got fg:%d, bg: %d", fg, bg) + } +} + +func TestVesaFbWrite8bpp(t *testing.T) { + specs := []struct { + consW, consH, offsetY uint32 + font *font.Font + expFb []byte + }{ + { + 16, 16, 6, + mockFont8x10, + []byte("" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000010000" + + "0000000000111000" + + "0000000001101100" + + "0000000011000110" + + "0000000011000110" + + "0000000011111110" + + "0000000011000110" + + "0000000011000110" + + "0000000011000110" + + "0000000011000110", + ), + }, + { + 20, 20, 3, + mockFont10x14, + []byte("" + + "00000000000000000000" + + "00000000000000000000" + + "00000000000000000000" + + "00000000000000010000" + + "00000000000000010000" + + "00000000000000111000" + + "00000000000000111000" + + "00000000000001101100" + + "00000000000001101100" + + "00000000000001100110" + + "00000000000011000110" + + "00000000000011111110" + + "00000000000011000110" + + "00000000000110000110" + + "00000000000110000011" + + "00000000000110000011" + + "00000000001111000111" + + "00000000000000000000" + + "00000000000000000000" + + "00000000000000000000", + ), + }, + } + + var ( + fg = uint8(1) + bg = uint8(0) + ) + + for specIndex, spec := range specs { + fb := make([]uint8, spec.consW*spec.consH) + + cons := NewVesaFbConsole(spec.consW, spec.consH, 8, spec.consW, 0) + cons.fb = fb + cons.offsetY = spec.offsetY + cons.SetFont(spec.font) + + // ASCII 0 maps to the a blank character in the mock font + // ASCII 1 maps to the letter 'A' in the mock font + cons.Write(0, fg, bg, 0, 0) + cons.Write(1, fg, bg, 2, 1) + + // Convert expected contents from ASCII to byte + for i := 0; i < len(spec.expFb); i++ { + spec.expFb[i] -= '0' + } + + if !reflect.DeepEqual(spec.expFb, fb) { + t.Errorf("[spec %d] unexpected frame buffer contents:\n%s", + specIndex, + diffFrameBuffer(spec.consW, spec.consH, spec.consW, spec.expFb, fb), + ) + } + } +} + +func TestVesaFbScroll8bpp(t *testing.T) { + var ( + consW, consH uint32 = 16, 16 + offsetY uint32 = 3 + origFb = []byte("" + + "6666666666666666" + // } + "7777777777777777" + // }- reserved rows + "8888888888888888" + // } + "0000000000001000" + + "0000000000010000" + + "0000000000100000" + + "0000000001000000" + + "0000000010000000" + + "0000000100000000" + + "0000001000000000" + + "0000010000000000" + + "0000100000000000" + + "0001000000000000" + + "0010000000000000" + + "0100000000000000" + + "1000000000000000", + ) + ) + + specs := []struct { + dir ScrollDir + lines uint32 + expFb []byte + }{ + { + ScrollDirUp, + 0, + []byte("" + + "6666666666666666" + // } + "7777777777777777" + // }- reserved rows + "8888888888888888" + // } + "0000000000001000" + + "0000000000010000" + + "0000000000100000" + + "0000000001000000" + + "0000000010000000" + + "0000000100000000" + + "0000001000000000" + + "0000010000000000" + + "0000100000000000" + + "0001000000000000" + + "0010000000000000" + + "0100000000000000" + + "1000000000000000", + ), + }, + { + ScrollDirUp, + 10000, + []byte("" + + "6666666666666666" + // } + "7777777777777777" + // }- reserved rows + "8888888888888888" + // } + "0000000000001000" + + "0000000000010000" + + "0000000000100000" + + "0000000001000000" + + "0000000010000000" + + "0000000100000000" + + "0000001000000000" + + "0000010000000000" + + "0000100000000000" + + "0001000000000000" + + "0010000000000000" + + "0100000000000000" + + "1000000000000000", + ), + }, + { + ScrollDirUp, + 1, + []byte("" + + "6666666666666666" + // } + "7777777777777777" + // }- reserved rows + "8888888888888888" + // } + "0000000000010000" + + "0000000000100000" + + "0000000001000000" + + "0000000010000000" + + "0000000100000000" + + "0000001000000000" + + "0000010000000000" + + "0000100000000000" + + "0001000000000000" + + "0010000000000000" + + "0100000000000000" + + "1000000000000000" + + "1000000000000000", + ), + }, + { + ScrollDirUp, + 2, + []byte("" + + "6666666666666666" + // } + "7777777777777777" + // }- reserved rows + "8888888888888888" + // } + "0000000000100000" + + "0000000001000000" + + "0000000010000000" + + "0000000100000000" + + "0000001000000000" + + "0000010000000000" + + "0000100000000000" + + "0001000000000000" + + "0010000000000000" + + "0100000000000000" + + "1000000000000000" + + "0100000000000000" + + "1000000000000000", + ), + }, + { + ScrollDirDown, + 1, + []byte("" + + "6666666666666666" + // } + "7777777777777777" + // }- reserved rows + "8888888888888888" + // } + "0000000000001000" + + "0000000000001000" + + "0000000000010000" + + "0000000000100000" + + "0000000001000000" + + "0000000010000000" + + "0000000100000000" + + "0000001000000000" + + "0000010000000000" + + "0000100000000000" + + "0001000000000000" + + "0010000000000000" + + "0100000000000000", + ), + }, + { + ScrollDirDown, + 2, + []byte("" + + "6666666666666666" + // } + "7777777777777777" + // }- reserved rows + "8888888888888888" + // } + "0000000000001000" + + "0000000000010000" + + "0000000000001000" + + "0000000000010000" + + "0000000000100000" + + "0000000001000000" + + "0000000010000000" + + "0000000100000000" + + "0000001000000000" + + "0000010000000000" + + "0000100000000000" + + "0001000000000000" + + "0010000000000000", + ), + }, + } + + // Convert original fb contents from ASCII to byte + for i := 0; i < len(origFb); i++ { + origFb[i] -= '0' + } + + for specIndex, spec := range specs { + // Convert expected contents from ASCII to byte + for i := 0; i < len(spec.expFb); i++ { + spec.expFb[i] -= '0' + } + + fb := make([]uint8, consW*consH) + copy(fb, origFb) + + cons := NewVesaFbConsole(consW, consH, 8, consW, 0) + cons.fb = fb + cons.offsetY = offsetY + + // calling scroll before setting the font should be a no-op + cons.Scroll(spec.dir, spec.lines) + if !reflect.DeepEqual(origFb, fb) { + t.Errorf("[spec %d] unexpected frame buffer contents:\n%s", + specIndex, + diffFrameBuffer(consW, consH, consW, origFb, fb), + ) + } + + cons.SetFont(&font.Font{ + GlyphWidth: 8, + GlyphHeight: 1, + BytesPerRow: 1, + }) + + cons.Scroll(spec.dir, spec.lines) + + if !reflect.DeepEqual(spec.expFb, fb) { + t.Errorf("[spec %d] unexpected frame buffer contents:\n%s", + specIndex, + diffFrameBuffer(consW, consH, consW, spec.expFb, fb), + ) + } + } +} + +func TestVesFbFill8(t *testing.T) { + var ( + consW, consH uint32 = 16, 26 + bg uint8 = 1 + origFb = []byte("" + + "6666666666666666" + // } + "7777777777777777" + // }- reserved rows + "8888888888888888" + // } + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000", + ) + ) + specs := []struct { + // Input rect in characters + x, y, w, h uint32 + offsetY uint32 + expFb []byte + }{ + { + 0, 0, 1, 1, + 0, + []byte("" + + "1111111166666666" + // } + "1111111177777777" + // }- reserved rows + "1111111188888888" + // } + "1111111100000000" + + "1111111100000000" + + "1111111100000000" + + "1111111100000000" + + "1111111100000000" + + "1111111100000000" + + "1111111100000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000", + ), + }, + { + 2, 0, 10, 1, + 3, + []byte("" + + "6666666666666666" + // } + "7777777777777777" + // }- reserved rows + "8888888888888888" + // } + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000", + ), + }, + { + 0, 0, 100, 100, + 3, + []byte("" + + "6666666666666666" + // } + "7777777777777777" + // }- reserved rows + "8888888888888888" + // } + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "1111111111111111" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000", + ), + }, + { + 100, 100, 1, 1, + 6, + []byte("" + + "6666666666666666" + // } + "7777777777777777" + // }- reserved rows + "8888888888888888" + // } + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000000000000" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111" + + "0000000011111111", + ), + }, + } + + // Convert original fb contents from ASCII to byte + for i := 0; i < len(origFb); i++ { + origFb[i] -= '0' + } + + for specIndex, spec := range specs { + // Convert expected contents from ASCII to byte + for i := 0; i < len(spec.expFb); i++ { + spec.expFb[i] -= '0' + } + + fb := make([]uint8, consW*consH) + copy(fb, origFb) + + cons := NewVesaFbConsole(consW, consH, 8, consW, 0) + cons.fb = fb + cons.offsetY = spec.offsetY + + // Calling fill before selecting a font should be a no-op + cons.Fill(spec.x, spec.y, spec.w, spec.h, 0, bg) + if !reflect.DeepEqual(origFb, fb) { + t.Errorf("[spec %d] unexpected frame buffer contents:\n%s", + specIndex, + diffFrameBuffer(consW, consH, consW, origFb, fb), + ) + } + + cons.SetFont(mockFont8x10) + + cons.Fill(spec.x, spec.y, spec.w, spec.h, 0, bg) + + if !reflect.DeepEqual(spec.expFb, fb) { + t.Errorf("[spec %d] unexpected frame buffer contents:\n%s", + specIndex, + diffFrameBuffer(consW, consH, consW, spec.expFb, fb), + ) + } + } +} + +func TestVesaFbPalette(t *testing.T) { + defer func() { + portWriteByteFn = cpu.PortWriteByte + }() + + expPal := make(color.Palette, 0) + expPal = append(expPal, + color.RGBA{R: 0, G: 0, B: 0}, /* black */ + color.RGBA{R: 0, G: 0, B: 128}, /* blue */ + color.RGBA{R: 0, G: 128, B: 1}, /* green */ + color.RGBA{R: 0, G: 128, B: 128}, /* cyan */ + color.RGBA{R: 128, G: 0, B: 1}, /* red */ + color.RGBA{R: 128, G: 0, B: 128}, /* magenta */ + color.RGBA{R: 64, G: 64, B: 1}, /* brown */ + color.RGBA{R: 128, G: 128, B: 128}, /* light gray */ + color.RGBA{R: 64, G: 64, B: 64}, /* dark gray */ + color.RGBA{R: 0, G: 0, B: 255}, /* light blue */ + color.RGBA{R: 0, G: 255, B: 1}, /* light green */ + color.RGBA{R: 0, G: 255, B: 255}, /* light cyan */ + color.RGBA{R: 255, G: 0, B: 1}, /* light red */ + color.RGBA{R: 255, G: 0, B: 255}, /* light magenta */ + color.RGBA{R: 255, G: 255, B: 1}, /* yellow */ + color.RGBA{R: 255, G: 255, B: 255}, /* white */ + ) + + for i := len(expPal); i < 256; i++ { + expPal = append(expPal, expPal[0]) + } + + var ( + dacIndex uint8 + compIndex uint8 + ) + portWriteByteFn = func(port uint16, val uint8) { + switch port { + case 0x3c8: + dacIndex = val + compIndex = 0 + case 0x3c9: + r, g, b, _ := expPal[dacIndex].RGBA() + + var expVal uint8 + switch compIndex { + case 0: + expVal = uint8(r) >> 2 + case 1: + expVal = uint8(g) >> 2 + case 2: + expVal = uint8(b) >> 2 + } + + if val != expVal { + t.Errorf("expected component %d for DAC entry %d to be %d; got %d", compIndex, dacIndex, expVal, val) + } + + compIndex++ + } + } + + cons := NewVesaFbConsole(0, 0, 8, 0, 0) + cons.loadDefaultPalette() + + customColor := color.RGBA{R: 251, G: 252, B: 253} + expPal[255] = customColor + cons.SetPaletteColor(255, customColor) + + got := cons.Palette() + for index, exp := range expPal { + if got[index] != exp { + t.Errorf("palette entry %d: want %v; got %v", index, exp, got[index]) + } + } +} + +func TestVesaFbDriverInterface(t *testing.T) { + defer func() { + mapRegionFn = vmm.MapRegion + portWriteByteFn = cpu.PortWriteByte + }() + var dev device.Driver = NewVesaFbConsole(320, 200, 8, 320, uintptr(0xa0000)) + + if dev.DriverName() == "" { + t.Fatal("DriverName() returned an empty string") + } + + if major, minor, patch := dev.DriverVersion(); major+minor+patch == 0 { + t.Fatal("DriverVersion() returned an invalid version number") + } + + t.Run("init success", func(t *testing.T) { + mapRegionFn = func(_ pmm.Frame, _ mem.Size, _ vmm.PageTableEntryFlag) (vmm.Page, *kernel.Error) { + return 0xa0000, nil + } + + portWriteByteFn = func(_ uint16, _ uint8) {} + + if err := dev.DriverInit(nil); err != nil { + t.Fatal(err) + } + }) + + t.Run("init fail", func(t *testing.T) { + expErr := &kernel.Error{Module: "test", Message: "something went wrong"} + mapRegionFn = func(_ pmm.Frame, _ mem.Size, _ vmm.PageTableEntryFlag) (vmm.Page, *kernel.Error) { + return 0, expErr + } + + if err := dev.DriverInit(nil); err != expErr { + t.Fatalf("expected error: %v; got %v", expErr, err) + } + }) +} + +func TestVesaFbProbe(t *testing.T) { + defer func() { + getFramebufferInfoFn = multiboot.GetFramebufferInfo + }() + + getFramebufferInfoFn = func() *multiboot.FramebufferInfo { + return &multiboot.FramebufferInfo{ + Width: 320, + Height: 20, + Pitch: 320, + Bpp: 8, + PhysAddr: 0xa0000, + Type: multiboot.FramebufferTypeIndexed, + } + } + + if drv := probeForVesaFbConsole(); drv == nil { + t.Fatal("expected probeForVesaFbConsole to return a driver") + } +} + +func dumpFramebuffer(consW, consH, consPitch uint32, fb []byte) string { + var buf bytes.Buffer + + for y := uint32(0); y < consH; y++ { + fmt.Fprintf(&buf, "%04d |", y) + index := (y * consPitch) + for x := uint32(0); x < consPitch; x++ { + fmt.Fprintf(&buf, "%d", fb[index+x]) + } + fmt.Fprintln(&buf, "|") + } + + return strings.TrimSpace(buf.String()) +} + +func diffFrameBuffer(consW, consH, consPitch uint32, exp, got []byte) string { + expDump := strings.Split(dumpFramebuffer(consW, consH, consPitch, exp), "\n") + gotDump := strings.Split(dumpFramebuffer(consW, consH, consPitch, got), "\n") + + maxLines := len(expDump) + if l := len(gotDump); l > maxLines { + maxLines = l + } + + var buf bytes.Buffer + var left, right string + + buf.WriteString("exp:") + buf.WriteString(strings.Repeat(" ", len(expDump[0])-4)) + buf.WriteString(" | got:\n") + + for line := 0; line < maxLines; line++ { + if line < len(expDump) { + left = expDump[line] + } else { + left = "" + } + + if line < len(gotDump) { + right = gotDump[line] + } else { + right = "" + } + + fmt.Fprintf(&buf, "%s | %s\n", left, right) + } + + return buf.String() +} + +var mockFont8x10 = &font.Font{ + GlyphWidth: 8, + GlyphHeight: 10, + BytesPerRow: 1, + Data: []byte{ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + // glyph 1 + 0x10, /* 00010000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + }, +} + +var mockFont10x14 = &font.Font{ + GlyphWidth: 10, + GlyphHeight: 14, + BytesPerRow: 2, + Data: []byte{ + 0x00, 0x00, /* 0000000000 */ + 0x00, 0x00, /* 0000000000 */ + 0x00, 0x00, /* 0000000000 */ + 0x00, 0x00, /* 0000000000 */ + 0x00, 0x00, /* 0000000000 */ + 0x00, 0x00, /* 0000000000 */ + 0x00, 0x00, /* 0000000000 */ + 0x00, 0x00, /* 0000000000 */ + 0x00, 0x00, /* 0000000000 */ + 0x00, 0x00, /* 0000000000 */ + 0x00, 0x00, /* 0000000000 */ + 0x00, 0x00, /* 0000000000 */ + 0x00, 0x00, /* 0000000000 */ + 0x00, 0x00, /* 0000000000 */ + // glyph 1 + 0x04, 0x00, /* 0000010000 */ + 0x04, 0x00, /* 0000010000 */ + 0x0e, 0x00, /* 0000111000 */ + 0x0e, 0x00, /* 0000111000 */ + 0x1b, 0x00, /* 0001101100 */ + 0x1b, 0x00, /* 0001101100 */ + 0x19, 0x80, /* 0001100110 */ + 0x31, 0x80, /* 0011000110 */ + 0x3f, 0x80, /* 0011111110 */ + 0x31, 0x80, /* 0011000110 */ + 0x61, 0x80, /* 0110000110 */ + 0x60, 0xc0, /* 0110000011 */ + 0x60, 0xc0, /* 0110000011 */ + 0xf1, 0xc0, /* 1111000111 */ + }, +}