From f02c767257b0f7b67cc5f087d219524c9918f8b3 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Tue, 11 Jul 2017 07:29:47 +0100 Subject: [PATCH] Extend vesa driver to support 24 and 32 bpp --- src/gopheros/device/video/console/vesa_fb.go | 197 ++++++- .../device/video/console/vesa_fb_test.go | 483 +++++++++++++++++- 2 files changed, 646 insertions(+), 34 deletions(-) diff --git a/src/gopheros/device/video/console/vesa_fb.go b/src/gopheros/device/video/console/vesa_fb.go index 8a6cb42..9648d13 100644 --- a/src/gopheros/device/video/console/vesa_fb.go +++ b/src/gopheros/device/video/console/vesa_fb.go @@ -16,9 +16,11 @@ import ( ) type VesaFbConsole struct { - bpp uint32 - fbPhysAddr uintptr - fb []uint8 + bpp uint32 + bytesPerPixel uint32 + fbPhysAddr uintptr + fb []uint8 + colorInfo *multiboot.FramebufferRGBColorInfo // Console dimensions in pixels width uint32 @@ -43,13 +45,15 @@ type VesaFbConsole struct { clearChar uint16 } -func NewVesaFbConsole(width, height uint32, bpp uint8, pitch uint32, fbPhysAddr uintptr) *VesaFbConsole { +func NewVesaFbConsole(width, height uint32, bpp uint8, pitch uint32, colorInfo *multiboot.FramebufferRGBColorInfo, fbPhysAddr uintptr) *VesaFbConsole { return &VesaFbConsole{ - bpp: uint32(bpp), - fbPhysAddr: fbPhysAddr, - width: width, - height: height, - pitch: pitch, + bpp: uint32(bpp), + bytesPerPixel: uint32(bpp+1) >> 3, + fbPhysAddr: fbPhysAddr, + colorInfo: colorInfo, + width: width, + height: height, + pitch: pitch, // light gray text on black background defaultFg: 7, defaultBg: 0, @@ -119,6 +123,8 @@ func (cons *VesaFbConsole) Fill(x, y, width, height uint32, _, bg uint8) { switch cons.bpp { case 8: cons.fill8(pX, pY, pW, pH, bg) + case 24, 32: + cons.fill24(pX, pY, pW, pH, bg) } } @@ -132,6 +138,19 @@ func (cons *VesaFbConsole) fill8(pX, pY, pW, pH uint32, bg uint8) { } } +// fill24 implements a fill operation using a 24/32bpp framebuffer. +func (cons *VesaFbConsole) fill24(pX, pY, pW, pH uint32, bg uint8) { + comp := cons.packColor24(bg) + fbRowOffset := cons.fbOffset(pX, pY) + for ; pH > 0; pH, fbRowOffset = pH-1, fbRowOffset+cons.pitch { + for fbOffset := fbRowOffset; fbOffset < fbRowOffset+pW*cons.bytesPerPixel; fbOffset += cons.bytesPerPixel { + cons.fb[fbOffset] = comp[0] + cons.fb[fbOffset+1] = comp[1] + cons.fb[fbOffset+2] = comp[2] + } + } +} + // 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. @@ -171,10 +190,12 @@ func (cons *VesaFbConsole) Write(ch byte, fg, bg uint8, x, y uint32) { switch cons.bpp { case 8: cons.write8(ch, fg, bg, pX, pY) + case 24, 32: + cons.write24(ch, fg, bg, pX, pY) } } -// write8 writes a charactero using an 8bpp framebuffer. +// write8 writes a character 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 @@ -207,10 +228,104 @@ func (cons *VesaFbConsole) write8(glyphIndex, fg, bg uint8, pX, pY uint32) { } } +// write16 writes a character using a 15/162bpp framebuffer. +func (cons *VesaFbConsole) write16(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 + fgComp = cons.packColor16(fg) + bgComp = cons.packColor16(bg) + ) + + 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+cons.bytesPerPixel, 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] = fgComp[0] + cons.fb[fbOffset+1] = fgComp[1] + } else { + cons.fb[fbOffset] = bgComp[0] + cons.fb[fbOffset+1] = bgComp[1] + } + } + } +} + +// write24 writes a character using a 24/32bpp framebuffer. +func (cons *VesaFbConsole) write24(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 + fgComp = cons.packColor24(fg) + bgComp = cons.packColor24(bg) + ) + + 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+cons.bytesPerPixel, 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] = fgComp[0] + cons.fb[fbOffset+1] = fgComp[1] + cons.fb[fbOffset+2] = fgComp[2] + } else { + cons.fb[fbOffset] = bgComp[0] + cons.fb[fbOffset+1] = bgComp[1] + cons.fb[fbOffset+2] = bgComp[2] + } + } + } +} + // 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) + return ((y + cons.offsetY) * cons.pitch) + (x * cons.bytesPerPixel) +} + +// packColor24 encodes a palette color into the pixel format required by a +// 24/32 bpp framebuffer. +func (cons *VesaFbConsole) packColor24(colorIndex uint8) [3]uint8 { + var ( + c = cons.palette[colorIndex].(color.RGBA) + packed uint32 = 0 | + (uint32(c.R>>(8-cons.colorInfo.RedMaskSize)) << cons.colorInfo.RedPosition) | + (uint32(c.G>>(8-cons.colorInfo.GreenMaskSize)) << cons.colorInfo.GreenPosition) | + (uint32(c.B>>(8-cons.colorInfo.BlueMaskSize)) << cons.colorInfo.BluePosition) + ) + + return [3]uint8{ + uint8(packed), + uint8(packed >> 8), + uint8(packed >> 16), + } } // Palette returns the active color palette for this console. @@ -222,19 +337,49 @@ func (cons *VesaFbConsole) Palette() color.Palette { // 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 + oldColor := cons.palette[index] - // Only program the DAC when we are in indexed (8bpp) mode - if cons.bpp > 8 { + if oldColor != nil && oldColor.(color.RGBA) == rgba { 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) + cons.palette[index] = rgba + + switch cons.bpp { + case 8: + // 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) + case 24, 32: + if oldColor == nil { + return + } + + cons.replace24(oldColor.(color.RGBA), rgba) + } +} + +// replace24 replaces all srcColor values with dstColor using a 24/32bpp +// framebuffer. +func (cons *VesaFbConsole) replace24(src, dst color.RGBA) { + tmp := cons.palette[0] + cons.palette[0] = src + srcComp := cons.packColor24(0) + cons.palette[0] = dst + dstComp := cons.packColor24(0) + cons.palette[0] = tmp + for fbOffset := uint32(0); fbOffset < uint32(len(cons.fb)); fbOffset += cons.bytesPerPixel { + if cons.fb[fbOffset] == srcComp[0] && + cons.fb[fbOffset+1] == srcComp[1] && + cons.fb[fbOffset+2] == srcComp[2] { + cons.fb[fbOffset] = dstComp[0] + cons.fb[fbOffset+1] = dstComp[1] + cons.fb[fbOffset+2] = dstComp[2] + } + } } // loadDefaultPalette is called during driver initialization to setup the @@ -262,7 +407,7 @@ func (cons *VesaFbConsole) loadDefaultPalette() { color.RGBA{R: 255, G: 255, B: 255}, /* white */ } - // Load default EFA palette for colors 0-16 + // Load default EGA palette for colors 0-16 var index int for ; index < len(egaPalette); index++ { cons.SetPaletteColor(uint8(index), egaPalette[index]) @@ -305,6 +450,7 @@ func (cons *VesaFbConsole) DriverInit(w io.Writer) *kernel.Error { })) kfmt.Fprintf(w, "mapped framebuffer to 0x%x\n", fbPage.Address()) + kfmt.Fprintf(w, "framebuffer dimensions: %dx%dx%d\n", cons.width, cons.height, cons.bpp) cons.loadDefaultPalette() @@ -316,8 +462,13 @@ 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)) + if fbInfo.Type == multiboot.FramebufferTypeIndexed || fbInfo.Type == multiboot.FramebufferTypeRGB { + drv = NewVesaFbConsole( + fbInfo.Width, fbInfo.Height, + fbInfo.Bpp, fbInfo.Pitch, + fbInfo.RGBColorInfo(), + uintptr(fbInfo.PhysAddr), + ) } return drv diff --git a/src/gopheros/device/video/console/vesa_fb_test.go b/src/gopheros/device/video/console/vesa_fb_test.go index ca958fb..ebbd8ba 100644 --- a/src/gopheros/device/video/console/vesa_fb_test.go +++ b/src/gopheros/device/video/console/vesa_fb_test.go @@ -18,7 +18,7 @@ import ( ) func TestVesaFbTextDimensions(t *testing.T) { - var cons Device = NewVesaFbConsole(16, 32, 8, 16, 0) + var cons Device = NewVesaFbConsole(16, 32, 8, 16, nil, 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) @@ -54,7 +54,7 @@ func TestVesaFbTextDimensions(t *testing.T) { } func TestVesaFbDefaultColors(t *testing.T) { - var cons Device = NewVesaFbConsole(16, 32, 8, 16, 0) + var cons Device = NewVesaFbConsole(16, 32, 8, 16, nil, 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) } @@ -124,7 +124,7 @@ func TestVesaFbWrite8bpp(t *testing.T) { for specIndex, spec := range specs { fb := make([]uint8, spec.consW*spec.consH) - cons := NewVesaFbConsole(spec.consW, spec.consH, 8, spec.consW, 0) + cons := NewVesaFbConsole(spec.consW, spec.consH, 8, spec.consW, nil, 0) cons.fb = fb cons.offsetY = spec.offsetY cons.SetFont(spec.font) @@ -148,7 +148,107 @@ func TestVesaFbWrite8bpp(t *testing.T) { } } -func TestVesaFbScroll8bpp(t *testing.T) { +func TestVesaFbWrite24bpp(t *testing.T) { + specs := []struct { + consW, consH, offsetY uint32 + font *font.Font + expFb []byte + }{ + { + 16, 16, 6, + mockFont8x10, + []byte("" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000321000000000000" + + "000000000000000000000000000000321321321000000000" + + "000000000000000000000000000321321000321321000000" + + "000000000000000000000000321321000000000321321000" + + "000000000000000000000000321321000000000321321000" + + "000000000000000000000000321321321321321321321000" + + "000000000000000000000000321321000000000321321000" + + "000000000000000000000000321321000000000321321000" + + "000000000000000000000000321321000000000321321000" + + "000000000000000000000000321321000000000321321000", + ), + }, + { + 20, 20, 3, + mockFont10x14, + []byte("" + + "000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000321000000000000" + + "000000000000000000000000000000000000000000000321000000000000" + + "000000000000000000000000000000000000000000321321321000000000" + + "000000000000000000000000000000000000000000321321321000000000" + + "000000000000000000000000000000000000000321321000321321000000" + + "000000000000000000000000000000000000000321321000321321000000" + + "000000000000000000000000000000000000000321321000000321321000" + + "000000000000000000000000000000000000321321000000000321321000" + + "000000000000000000000000000000000000321321321321321321321000" + + "000000000000000000000000000000000000321321000000000321321000" + + "000000000000000000000000000000000321321000000000000321321000" + + "000000000000000000000000000000000321321000000000000000321321" + + "000000000000000000000000000000000321321000000000000000321321" + + "000000000000000000000000000000321321321321000000000321321321" + + "000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000", + ), + }, + } + + var ( + // BGR + colorInfo = &multiboot.FramebufferRGBColorInfo{ + RedPosition: 16, + RedMaskSize: 8, + GreenPosition: 8, + GreenMaskSize: 8, + BluePosition: 0, + BlueMaskSize: 8, + } + fg = uint8(1) + fgColor = color.RGBA{R: 1, G: 2, B: 3} + bg = uint8(0) + ) + + for specIndex, spec := range specs { + fb := make([]uint8, spec.consW*spec.consH*3) + + cons := NewVesaFbConsole(spec.consW, spec.consH, 24, spec.consW*3, colorInfo, 0) + cons.fb = fb + cons.offsetY = spec.offsetY + cons.SetFont(spec.font) + cons.loadDefaultPalette() + cons.SetPaletteColor(fg, fgColor) + + // 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*3, spec.expFb, fb), + ) + } + } +} + +func TestVesaFbScroll(t *testing.T) { var ( consW, consH uint32 = 16, 16 offsetY uint32 = 3 @@ -325,7 +425,7 @@ func TestVesaFbScroll8bpp(t *testing.T) { fb := make([]uint8, consW*consH) copy(fb, origFb) - cons := NewVesaFbConsole(consW, consH, 8, consW, 0) + cons := NewVesaFbConsole(consW, consH, 8, consW, nil, 0) cons.fb = fb cons.offsetY = offsetY @@ -355,7 +455,7 @@ func TestVesaFbScroll8bpp(t *testing.T) { } } -func TestVesFbFill8(t *testing.T) { +func TestVesaFbFill8(t *testing.T) { var ( consW, consH uint32 = 16, 26 bg uint8 = 1 @@ -538,7 +638,7 @@ func TestVesFbFill8(t *testing.T) { fb := make([]uint8, consW*consH) copy(fb, origFb) - cons := NewVesaFbConsole(consW, consH, 8, consW, 0) + cons := NewVesaFbConsole(consW, consH, 8, consW, nil, 0) cons.fb = fb cons.offsetY = spec.offsetY @@ -564,6 +664,227 @@ func TestVesFbFill8(t *testing.T) { } } +func TestVesaFbFill24(t *testing.T) { + var ( + consW, consH uint32 = 16, 26 + // BGR + colorInfo = &multiboot.FramebufferRGBColorInfo{ + RedPosition: 16, + RedMaskSize: 8, + GreenPosition: 8, + GreenMaskSize: 8, + BluePosition: 0, + BlueMaskSize: 8, + } + bg uint8 = 1 + bgColor = color.RGBA{R: 1, G: 2, B: 3} + origFb = []byte("" + + "666666666666666666666666666666666666666666666666" + // } + "777777777777777777777777777777777777777777777777" + // }- reserved rows + "888888888888888888888888888888888888888888888888" + // } + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000", + ) + ) + specs := []struct { + // Input rect in characters + x, y, w, h uint32 + offsetY uint32 + expFb []byte + }{ + { + 0, 0, 1, 1, + 0, + []byte("" + + "321321321321321321321321666666666666666666666666" + // } + "321321321321321321321321777777777777777777777777" + // }- reserved rows + "321321321321321321321321888888888888888888888888" + // } + "321321321321321321321321000000000000000000000000" + + "321321321321321321321321000000000000000000000000" + + "321321321321321321321321000000000000000000000000" + + "321321321321321321321321000000000000000000000000" + + "321321321321321321321321000000000000000000000000" + + "321321321321321321321321000000000000000000000000" + + "321321321321321321321321000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000", + ), + }, + { + 2, 0, 10, 1, + 3, + []byte("" + + "666666666666666666666666666666666666666666666666" + // } + "777777777777777777777777777777777777777777777777" + // }- reserved rows + "888888888888888888888888888888888888888888888888" + // } + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000", + ), + }, + { + 0, 0, 100, 100, + 3, + []byte("" + + "666666666666666666666666666666666666666666666666" + // } + "777777777777777777777777777777777777777777777777" + // }- reserved rows + "888888888888888888888888888888888888888888888888" + // } + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "321321321321321321321321321321321321321321321321" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000", + ), + }, + { + 100, 100, 1, 1, + 6, + []byte("" + + "666666666666666666666666666666666666666666666666" + // } + "777777777777777777777777777777777777777777777777" + // }- reserved rows + "888888888888888888888888888888888888888888888888" + // } + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321" + + "000000000000000000000000321321321321321321321321", + ), + }, + } + + // 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*3) + copy(fb, origFb) + + cons := NewVesaFbConsole(consW, consH, 24, consW*3, colorInfo, 0) + cons.fb = fb + cons.offsetY = spec.offsetY + cons.loadDefaultPalette() + cons.SetPaletteColor(bg, bgColor) + + // 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*3, 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*3, spec.expFb, fb), + ) + } + } +} + func TestVesaFbPalette(t *testing.T) { defer func() { portWriteByteFn = cpu.PortWriteByte @@ -594,10 +915,12 @@ func TestVesaFbPalette(t *testing.T) { } var ( - dacIndex uint8 - compIndex uint8 + dacIndex uint8 + compIndex uint8 + portWriteCount int ) portWriteByteFn = func(port uint16, val uint8) { + portWriteCount++ switch port { case 0x3c8: dacIndex = val @@ -623,13 +946,20 @@ func TestVesaFbPalette(t *testing.T) { } } - cons := NewVesaFbConsole(0, 0, 8, 0, 0) + cons := NewVesaFbConsole(0, 0, 8, 0, nil, 0) cons.loadDefaultPalette() customColor := color.RGBA{R: 251, G: 252, B: 253} expPal[255] = customColor cons.SetPaletteColor(255, customColor) + // Setting the same RGB value should be a no-op + cons.SetPaletteColor(255, customColor) + + if exp := 257 * 4; portWriteCount != exp { + t.Errorf("expected %d calls to cpu.portWriteByte; got %d", exp, portWriteCount) + } + got := cons.Palette() for index, exp := range expPal { if got[index] != exp { @@ -638,12 +968,96 @@ func TestVesaFbPalette(t *testing.T) { } } +func TestVesaFbReplace24(t *testing.T) { + var ( + consW, consH uint32 = 4, 4 + // BGR + colorInfo = &multiboot.FramebufferRGBColorInfo{ + RedPosition: 16, + RedMaskSize: 8, + GreenPosition: 8, + GreenMaskSize: 8, + BluePosition: 0, + BlueMaskSize: 8, + } + ) + + specs := []struct { + bpp uint8 + inpFb []byte + expFb []byte + }{ + { + 24, + []byte("" + + "000100010002" + + "000100010002" + + "000100010002" + + "000100010002", + ), + []byte("" + + "765100010002" + + "765100010002" + + "765100010002" + + "765100010002", + ), + }, + { + 32, + []byte("" + + "0000100001000020" + + "0000100001000020" + + "0000100001000020" + + "0000100001000020", + ), + []byte("" + + "7650100001000020" + + "7650100001000020" + + "7650100001000020" + + "7650100001000020", + ), + }, + } + + for specIndex, spec := range specs { + // Convert spec fb contents from ASCII to byte + for i := 0; i < len(spec.expFb); i++ { + spec.inpFb[i] -= '0' + spec.expFb[i] -= '0' + } + fb := make([]uint8, consW*consH*uint32(spec.bpp)>>3) + copy(fb, spec.inpFb) + + cons := NewVesaFbConsole(consW, consH, spec.bpp, consW*uint32(spec.bpp)>>3, colorInfo, 0) + cons.fb = fb + cons.palette = make(color.Palette, 1) + + // First color update should not trigger a replace as the color is not used yet + cons.SetPaletteColor(0, color.RGBA{}) + if !reflect.DeepEqual(spec.inpFb, fb) { + t.Errorf("[spec %d] unexpected frame buffer contents:\n%s", + specIndex, + diffFrameBuffer(consW, consH, cons.pitch, spec.expFb, fb), + ) + } + + // Second color update should replace existing pixels with the new RGB value + cons.SetPaletteColor(0, color.RGBA{R: 5, G: 6, B: 7}) + if !reflect.DeepEqual(spec.expFb, fb) { + t.Errorf("[spec %d] unexpected frame buffer contents:\n%s", + specIndex, + diffFrameBuffer(consW, consH, cons.pitch, spec.expFb, fb), + ) + } + } +} + func TestVesaFbDriverInterface(t *testing.T) { defer func() { mapRegionFn = vmm.MapRegion portWriteByteFn = cpu.PortWriteByte }() - var dev device.Driver = NewVesaFbConsole(320, 200, 8, 320, uintptr(0xa0000)) + var dev device.Driver = NewVesaFbConsole(320, 200, 8, 320, nil, uintptr(0xa0000)) if dev.DriverName() == "" { t.Fatal("DriverName() returned an empty string") @@ -698,6 +1112,53 @@ func TestVesaFbProbe(t *testing.T) { } } +func TestVesaFbPackColor24(t *testing.T) { + specs := []struct { + colorInfo *multiboot.FramebufferRGBColorInfo + input color.RGBA + exp [3]uint8 + }{ + { + // RGB + &multiboot.FramebufferRGBColorInfo{ + RedPosition: 0, + RedMaskSize: 8, + GreenPosition: 8, + GreenMaskSize: 8, + BluePosition: 16, + BlueMaskSize: 8, + }, + color.RGBA{R: 100, G: 200, B: 255}, + [3]uint8{100, 200, 255}, + }, + { + // BGR + &multiboot.FramebufferRGBColorInfo{ + RedPosition: 16, + RedMaskSize: 8, + GreenPosition: 8, + GreenMaskSize: 8, + BluePosition: 0, + BlueMaskSize: 8, + }, + color.RGBA{R: 250, G: 200, B: 120}, + [3]uint8{120, 200, 250}, + }, + } + + cons := NewVesaFbConsole(0, 0, 32, 0, nil, 0) + cons.palette = make(color.Palette, 256) + + for specIndex, spec := range specs { + cons.colorInfo = spec.colorInfo + cons.palette[0] = spec.input + + if got := cons.packColor24(0); got != spec.exp { + t.Errorf("[spec %d] expected: %v; got %v", specIndex, spec.exp, got) + } + } +} + func dumpFramebuffer(consW, consH, consPitch uint32, fb []byte) string { var buf bytes.Buffer