From 286b8d9c71858161fb34a48a019f06500d44c67e Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Tue, 4 Jul 2017 07:08:45 +0100 Subject: [PATCH] Rename Ega console to VgaTextConsole and implement the updated interface --- src/gopheros/device/video/console/device.go | 4 +- src/gopheros/device/video/console/vga_text.go | 198 +++++++++++ .../device/video/console/vga_text_test.go | 320 ++++++++++++++++++ .../kernel/driver/video/console/ega.go | 100 ------ .../kernel/driver/video/console/ega_test.go | 212 ------------ 5 files changed, 520 insertions(+), 314 deletions(-) create mode 100644 src/gopheros/device/video/console/vga_text.go create mode 100644 src/gopheros/device/video/console/vga_text_test.go delete mode 100644 src/gopheros/kernel/driver/video/console/ega.go delete mode 100644 src/gopheros/kernel/driver/video/console/ega_test.go diff --git a/src/gopheros/device/video/console/device.go b/src/gopheros/device/video/console/device.go index 8129216..1e7b7e3 100644 --- a/src/gopheros/device/video/console/device.go +++ b/src/gopheros/device/video/console/device.go @@ -7,8 +7,8 @@ type ScrollDir uint8 // The supported list of scroll directions for the console Scroll() calls. const ( - Up ScrollDir = iota - Down + ScrollDirUp ScrollDir = iota + ScrollDirDown ) // The Device interface is implemented by objects that can function as system diff --git a/src/gopheros/device/video/console/vga_text.go b/src/gopheros/device/video/console/vga_text.go new file mode 100644 index 0000000..76aecbf --- /dev/null +++ b/src/gopheros/device/video/console/vga_text.go @@ -0,0 +1,198 @@ +package console + +import ( + "gopheros/kernel" + "gopheros/kernel/cpu" + "image/color" + "reflect" + "unsafe" +) + +var portWriteByteFn = cpu.PortWriteByte + +// VgaTextConsole implements an EGA-compatible 80x25 text console using VGA +// mode 0x3. The console supports the default 16 EGA colors which can be +// overridden using the SetPaletteColor method. +// +// Each character in the console framebuffer is represented using two bytes, +// a byte for the character ASCII code and a byte that encodes the foreground +// and background colors (4 bits for each). +// +// The default settings for the console are: +// - light gray text (color 7) on black background (color 0). +// - space as the clear character +type VgaTextConsole struct { + width uint16 + height uint16 + + fb []uint16 + + palette color.Palette + defaultFg uint8 + defaultBg uint8 + clearChar uint16 +} + +// NewVgaTextConsole creates an new vga text console with its +// framebuffer mapped to fbPhysAddr. +func NewVgaTextConsole(columns, rows uint16, fbPhysAddr uintptr) *VgaTextConsole { + return &VgaTextConsole{ + width: columns, + height: rows, + clearChar: uint16(' '), + // overlay a 16bit slice over the fbPhysAddr + fb: *(*[]uint16)(unsafe.Pointer(&reflect.SliceHeader{ + Len: 80 * 25, + Cap: 80 * 25, + Data: fbPhysAddr, + })), + palette: color.Palette{ + color.RGBA{R: 0, G: 0, B: 1}, /* 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 */ + }, + // light gray text on black background + defaultFg: 7, + defaultBg: 0, + } +} + +// Dimensions returns the console width and height in characters. +func (cons *VgaTextConsole) Dimensions() (uint16, uint16) { + return cons.width, cons.height +} + +// DefaultColors returns the default foreground and background colors +// used by this console. +func (cons *VgaTextConsole) 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 *VgaTextConsole) Fill(x, y, width, height uint16, fg, bg uint8) { + var ( + clr = (((uint16(bg) << 4) | uint16(fg)) << 8) | cons.clearChar + rowOffset, colOffset uint16 + ) + + // clip rectangle + if x == 0 { + x = 1 + } else if x >= cons.width { + x = cons.width + } + + if y == 0 { + y = 1 + } else if y >= cons.height { + y = cons.height + } + + if x+width > cons.width { + width = cons.width - x + } + + if y+height > cons.height { + height = cons.height - y + } + + rowOffset = ((y - 1) * cons.width) + (x - 1) + for ; height > 0; height, rowOffset = height-1, rowOffset+cons.width { + for colOffset = rowOffset; colOffset < rowOffset+width; colOffset++ { + cons.fb[colOffset] = clr + } + } +} + +// 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 *VgaTextConsole) Scroll(dir ScrollDir, lines uint16) { + if lines == 0 || lines > cons.height { + return + } + + var i uint16 + offset := lines * cons.width + + switch dir { + case ScrollDirUp: + for ; i < (cons.height-lines)*cons.width; i++ { + cons.fb[i] = cons.fb[i+offset] + } + case ScrollDirDown: + for i = cons.height*cons.width - 1; i >= lines*cons.width; 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 *VgaTextConsole) Write(ch byte, fg, bg uint8, x, y uint16) { + if x < 1 || x > cons.width || y < 1 || y > cons.height { + return + } + + maxColorIndex := uint8(len(cons.palette) - 1) + if fg > maxColorIndex { + fg = cons.defaultFg + } + if bg >= maxColorIndex { + bg = cons.defaultBg + } + + cons.fb[((y-1)*cons.width)+(x-1)] = (((uint16(bg) << 4) | uint16(fg)) << 8) | uint16(ch) +} + +// Palette returns the active color palette for this console. +func (cons *VgaTextConsole) 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 *VgaTextConsole) SetPaletteColor(index uint8, rgba color.RGBA) { + if index >= uint8(len(cons.palette)) { + return + } + + cons.palette[index] = rgba + + // Load palette entry to the DAC. In this mode, colors are specified + // using 6-bits for each component; the RGB values need to be converted + // to the 0-63 range. + portWriteByteFn(0x3c8, index) + portWriteByteFn(0x3c9, rgba.R>>2) + portWriteByteFn(0x3c9, rgba.G>>2) + portWriteByteFn(0x3c9, rgba.B>>2) +} + +// DriverName returns the name of this driver. +func (cons *VgaTextConsole) DriverName() string { + return "vga_text_console" +} + +// DriverVersion returns the version of this driver. +func (cons *VgaTextConsole) DriverVersion() (uint16, uint16, uint16) { + return 0, 0, 1 +} + +// DriverInit initializes this driver. +func (cons *VgaTextConsole) DriverInit() *kernel.Error { return nil } diff --git a/src/gopheros/device/video/console/vga_text_test.go b/src/gopheros/device/video/console/vga_text_test.go new file mode 100644 index 0000000..9213299 --- /dev/null +++ b/src/gopheros/device/video/console/vga_text_test.go @@ -0,0 +1,320 @@ +package console + +import ( + "gopheros/device" + "gopheros/kernel/cpu" + "image/color" + "testing" + "unsafe" +) + +func TestVgaTextDimensions(t *testing.T) { + cons := NewVgaTextConsole(80, 25, 0) + if w, h := cons.Dimensions(); w != 80 || h != 25 { + t.Fatalf("expected console dimensions to be 80x25; got %dx%d", w, h) + } +} + +func TestVgaTextDefaultColors(t *testing.T) { + cons := NewVgaTextConsole(80, 25, 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 TestVgaTextFill(t *testing.T) { + specs := []struct { + // Input rect + x, y, w, h uint16 + + // Expected area to be cleared + expX, expY, expW, expH uint16 + }{ + { + 0, 0, 500, 500, + 0, 0, 80, 25, + }, + { + 10, 10, 11, 50, + 10, 10, 11, 15, + }, + { + 10, 10, 110, 1, + 10, 10, 70, 1, + }, + { + 70, 20, 20, 20, + 70, 20, 10, 5, + }, + { + 90, 25, 20, 20, + 0, 0, 0, 0, + }, + { + 12, 12, 5, 6, + 12, 12, 5, 6, + }, + } + + fb := make([]uint16, 80*25) + cons := NewVgaTextConsole(80, 25, uintptr(unsafe.Pointer(&fb[0]))) + cw, ch := cons.Dimensions() + + testPat := uint16(0xDEAD) + clearPat := uint16(cons.clearChar) + +nextSpec: + for specIndex, spec := range specs { + // Fill FB with test pattern + for i := 0; i < len(fb); i++ { + fb[i] = testPat + } + + cons.Fill(spec.x, spec.y, spec.w, spec.h, 0, 0) + + var x, y uint16 + for y = 1; y <= ch; y++ { + for x = 1; x <= cw; x++ { + fbVal := fb[((y-1)*cw)+(x-1)] + + if x < spec.expX || y < spec.expY || x >= spec.expX+spec.expW || y >= spec.expY+spec.expH { + if fbVal != testPat { + t.Errorf("[spec %d] expected char at (%d, %d) not to be cleared", specIndex, x, y) + continue nextSpec + } + } else { + if fbVal != clearPat { + t.Errorf("[spec %d] expected char at (%d, %d) to be cleared", specIndex, x, y) + continue nextSpec + } + } + } + } + } +} + +func TestVgaTextScroll(t *testing.T) { + fb := make([]uint16, 80*25) + cons := NewVgaTextConsole(80, 25, uintptr(unsafe.Pointer(&fb[0]))) + cw, ch := cons.Dimensions() + + t.Run("up", func(t *testing.T) { + specs := []uint16{ + 0, + 1, + 2, + } + nextSpec: + for specIndex, lines := range specs { + // Fill buffer with test pattern + var x, y, index uint16 + for y = 0; y < ch; y++ { + for x = 0; x < cw; x++ { + fb[index] = (y << 8) | x + index++ + } + } + + cons.Scroll(ScrollDirUp, lines) + + // Check that rows 1 to (height - lines) have been scrolled up + index = 0 + for y = 0; y < ch-lines; y++ { + for x = 0; x < cw; x++ { + expVal := ((y + lines) << 8) | x + if fb[index] != expVal { + t.Errorf("[spec %d] expected value at (%d, %d) to be %d; got %d", specIndex, x, y, expVal, fb[index]) + continue nextSpec + } + index++ + } + } + } + }) + + t.Run("down", func(t *testing.T) { + specs := []uint16{ + 0, + 1, + 2, + } + + nextSpec: + for specIndex, lines := range specs { + // Fill buffer with test pattern + var x, y, index uint16 + for y = 0; y < ch; y++ { + for x = 0; x < cw; x++ { + fb[index] = (y << 8) | x + index++ + } + } + + cons.Scroll(ScrollDirDown, lines) + + // Check that rows lines to height have been scrolled down + index = lines * cw + for y = lines; y < ch-lines; y++ { + for x = 0; x < cw; x++ { + expVal := ((y - lines) << 8) | x + if fb[index] != expVal { + t.Errorf("[spec %d] expected value at (%d, %d) to be %d; got %d", specIndex, x, y, expVal, fb[index]) + continue nextSpec + } + index++ + } + } + } + }) +} + +func TestVgaTextWrite(t *testing.T) { + fb := make([]uint16, 80*25) + cons := NewVgaTextConsole(80, 25, uintptr(unsafe.Pointer(&fb[0]))) + defaultFg, defaultBg := cons.DefaultColors() + + t.Run("off-screen", func(t *testing.T) { + specs := []struct { + x, y uint16 + }{ + {81, 26}, + {90, 24}, + {79, 30}, + {100, 100}, + } + + nextSpec: + for specIndex, spec := range specs { + for i := 0; i < len(fb); i++ { + fb[i] = 0 + } + + cons.Write('!', 1, 2, spec.x, spec.y) + + for i := 0; i < len(fb); i++ { + if got := fb[i]; got != 0 { + t.Errorf("[spec %d] expected Write() with off-screen coords to be a no-op", specIndex) + continue nextSpec + } + } + } + }) + + t.Run("success", func(t *testing.T) { + for i := 0; i < len(fb); i++ { + fb[i] = 0 + } + + fg := uint8(1) + bg := uint8(2) + expAttr := uint16((uint16(bg) << 4) | uint16(fg)) + + cons.Write('!', fg, bg, 1, 1) + + expVal := (expAttr << 8) | uint16('!') + if got := fb[0]; got != expVal { + t.Errorf("expected call to Write() to set fb[0] to %d; got %d", expVal, got) + } + }) + + t.Run("fg out of range", func(t *testing.T) { + for i := 0; i < len(fb); i++ { + fb[i] = 0 + } + + fg := uint8(128) + bg := uint8(2) + expAttr := uint16((uint16(bg) << 4) | uint16(defaultFg)) + + cons.Write('!', fg, bg, 1, 1) + + expVal := (expAttr << 8) | uint16('!') + if got := fb[0]; got != expVal { + t.Errorf("expected call to Write() to set fb[0] to %d; got %d", expVal, got) + } + }) + + t.Run("bg out of range", func(t *testing.T) { + for i := 0; i < len(fb); i++ { + fb[i] = 0 + } + + fg := uint8(8) + bg := uint8(255) + expAttr := uint16((uint16(defaultBg) << 4) | uint16(fg)) + + cons.Write('!', fg, bg, 1, 1) + + expVal := (expAttr << 8) | uint16('!') + if got := fb[0]; got != expVal { + t.Errorf("expected call to Write() to set fb[0] to %d; got %d", expVal, got) + } + }) +} + +func TestVgaTextSetPaletteColor(t *testing.T) { + defer func() { + portWriteByteFn = cpu.PortWriteByte + }() + + cons := NewVgaTextConsole(80, 25, 0) + + t.Run("success", func(t *testing.T) { + expWrites := []struct { + port uint16 + val uint8 + }{ + // Values will be normalized in the 0-31 range + {0x3c8, 1}, + {0x3c9, 63}, + {0x3c9, 31}, + {0x3c9, 0}, + } + + writeCallCount := 0 + portWriteByteFn = func(port uint16, val uint8) { + exp := expWrites[writeCallCount] + if port != exp.port || val != exp.val { + t.Errorf("[port write %d] expected port: 0x%x, val: %d; got port: 0x%x, val: %d", writeCallCount, exp.port, exp.val, port, val) + } + + writeCallCount++ + } + + rgba := color.RGBA{R: 255, G: 127, B: 0} + cons.SetPaletteColor(1, rgba) + + if got := cons.Palette()[1]; got != rgba { + t.Errorf("expected color at index 1 to be:\n%v\ngot:\n%v", rgba, got) + } + + if writeCallCount != len(expWrites) { + t.Errorf("expected cpu.portWriteByty to be called %d times; got %d", len(expWrites), writeCallCount) + } + }) + + t.Run("color index out of range", func(t *testing.T) { + portWriteByteFn = func(_ uint16, _ uint8) { + t.Error("unexpected call to cpu.PortWriteByte") + } + + rgba := color.RGBA{R: 255, G: 127, B: 0} + cons.SetPaletteColor(50, rgba) + }) +} + +func TestVgaTextDriverInterface(t *testing.T) { + var dev device.Driver = NewVgaTextConsole(80, 25, 0) + + if err := dev.DriverInit(); err != nil { + t.Fatal(err) + } + + 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") + } +} diff --git a/src/gopheros/kernel/driver/video/console/ega.go b/src/gopheros/kernel/driver/video/console/ega.go deleted file mode 100644 index 3ed939e..0000000 --- a/src/gopheros/kernel/driver/video/console/ega.go +++ /dev/null @@ -1,100 +0,0 @@ -package console - -import ( - "reflect" - "unsafe" -) - -const ( - clearColor = Black - clearChar = byte(' ') -) - -// Ega implements an EGA-compatible text console. At the moment, it uses the -// ega console physical address as its outpucons. After implementing a memory -// allocator, each console will use its own framebuffer while the active console -// will periodically sync its internal buffer with the physical screen buffer. -type Ega struct { - width uint16 - height uint16 - - fb []uint16 -} - -// Init sets up the console. -func (cons *Ega) Init(width, height uint16, fbPhysAddr uintptr) { - cons.width = width - cons.height = height - - cons.fb = *(*[]uint16)(unsafe.Pointer(&reflect.SliceHeader{ - Len: int(cons.width * cons.height), - Cap: int(cons.width * cons.height), - Data: fbPhysAddr, - })) -} - -// Clear clears the specified rectangular region -func (cons *Ega) Clear(x, y, width, height uint16) { - var ( - attr = uint16((clearColor << 4) | clearColor) - clr = attr | uint16(clearChar) - rowOffset, colOffset uint16 - ) - - // clip rectangle - if x >= cons.width { - x = cons.width - } - if y >= cons.height { - y = cons.height - } - - if x+width > cons.width { - width = cons.width - x - } - if y+height > cons.height { - height = cons.height - y - } - - rowOffset = (y * cons.width) + x - for ; height > 0; height, rowOffset = height-1, rowOffset+cons.width { - for colOffset = rowOffset; colOffset < rowOffset+width; colOffset++ { - cons.fb[colOffset] = clr - } - } -} - -// Dimensions returns the console width and height in characters. -func (cons *Ega) Dimensions() (uint16, uint16) { - return cons.width, cons.height -} - -// Scroll a particular number of lines to the specified direction. -func (cons *Ega) Scroll(dir ScrollDir, lines uint16) { - if lines == 0 || lines > cons.height { - return - } - - var i uint16 - offset := lines * cons.width - - switch dir { - case Up: - for ; i < (cons.height-lines)*cons.width; i++ { - cons.fb[i] = cons.fb[i+offset] - } - case Down: - for i = cons.height*cons.width - 1; i >= lines*cons.width; i-- { - cons.fb[i] = cons.fb[i-offset] - } - } -} - -// Write a char to the specified location. -func (cons *Ega) Write(ch byte, attr Attr, x, y uint16) { - if x >= cons.width || y >= cons.height { - return - } - - cons.fb[(y*cons.width)+x] = (uint16(attr) << 8) | uint16(ch) -} diff --git a/src/gopheros/kernel/driver/video/console/ega_test.go b/src/gopheros/kernel/driver/video/console/ega_test.go deleted file mode 100644 index 664a841..0000000 --- a/src/gopheros/kernel/driver/video/console/ega_test.go +++ /dev/null @@ -1,212 +0,0 @@ -package console - -import ( - "testing" - "unsafe" -) - -func TestEgaInit(t *testing.T) { - var cons Ega - cons.Init(80, 25, 0xB8000) - - var expWidth uint16 = 80 - var expHeight uint16 = 25 - - if w, h := cons.Dimensions(); w != expWidth || h != expHeight { - t.Fatalf("expected console dimensions after Init() to be (%d, %d); got (%d, %d)", expWidth, expHeight, w, h) - } -} - -func TestEgaClear(t *testing.T) { - specs := []struct { - // Input rect - x, y, w, h uint16 - - // Expected area to be cleared - expX, expY, expW, expH uint16 - }{ - { - 0, 0, 500, 500, - 0, 0, 80, 25, - }, - { - 10, 10, 11, 50, - 10, 10, 11, 15, - }, - { - 10, 10, 110, 1, - 10, 10, 70, 1, - }, - { - 70, 20, 20, 20, - 70, 20, 10, 5, - }, - { - 90, 25, 20, 20, - 0, 0, 0, 0, - }, - { - 12, 12, 5, 6, - 12, 12, 5, 6, - }, - } - - fb := make([]uint16, 80*25) - var cons Ega - cons.Init(80, 25, uintptr(unsafe.Pointer(&fb[0]))) - - testPat := uint16(0xDEAD) - clearPat := (uint16(clearColor) << 8) | uint16(clearChar) - -nextSpec: - for specIndex, spec := range specs { - // Fill FB with test pattern - for i := 0; i < len(cons.fb); i++ { - fb[i] = testPat - } - - cons.Clear(spec.x, spec.y, spec.w, spec.h) - - var x, y uint16 - for y = 0; y < cons.height; y++ { - for x = 0; x < cons.width; x++ { - fbVal := fb[(y*cons.width)+x] - - if x < spec.expX || y < spec.expY || x >= spec.expX+spec.expW || y >= spec.expY+spec.expH { - if fbVal != testPat { - t.Errorf("[spec %d] expected char at (%d, %d) not to be cleared", specIndex, x, y) - continue nextSpec - } - } else { - if fbVal != clearPat { - t.Errorf("[spec %d] expected char at (%d, %d) to be cleared", specIndex, x, y) - continue nextSpec - } - } - } - } - } -} - -func TestEgaScrollUp(t *testing.T) { - specs := []uint16{ - 0, - 1, - 2, - } - - fb := make([]uint16, 80*25) - var cons Ega - cons.Init(80, 25, uintptr(unsafe.Pointer(&fb[0]))) - -nextSpec: - for specIndex, lines := range specs { - // Fill buffer with test pattern - var x, y, index uint16 - for y = 0; y < cons.height; y++ { - for x = 0; x < cons.width; x++ { - fb[index] = (y << 8) | x - index++ - } - } - - cons.Scroll(Up, lines) - - // Check that rows 1 to (height - lines) have been scrolled up - index = 0 - for y = 0; y < cons.height-lines; y++ { - for x = 0; x < cons.width; x++ { - expVal := ((y + lines) << 8) | x - if fb[index] != expVal { - t.Errorf("[spec %d] expected value at (%d, %d) to be %d; got %d", specIndex, x, y, expVal, cons.fb[index]) - continue nextSpec - } - index++ - } - } - } -} - -func TestEgaScrollDown(t *testing.T) { - specs := []uint16{ - 0, - 1, - 2, - } - - fb := make([]uint16, 80*25) - var cons Ega - cons.Init(80, 25, uintptr(unsafe.Pointer(&fb[0]))) - -nextSpec: - for specIndex, lines := range specs { - // Fill buffer with test pattern - var x, y, index uint16 - for y = 0; y < cons.height; y++ { - for x = 0; x < cons.width; x++ { - fb[index] = (y << 8) | x - index++ - } - } - - cons.Scroll(Down, lines) - - // Check that rows lines to height have been scrolled down - index = lines * cons.width - for y = lines; y < cons.height-lines; y++ { - for x = 0; x < cons.width; x++ { - expVal := ((y - lines) << 8) | x - if fb[index] != expVal { - t.Errorf("[spec %d] expected value at (%d, %d) to be %d; got %d", specIndex, x, y, expVal, cons.fb[index]) - continue nextSpec - } - index++ - } - } - } -} - -func TestEgaWriteWithOffScreenCoords(t *testing.T) { - fb := make([]uint16, 80*25) - var cons Ega - cons.Init(80, 25, uintptr(unsafe.Pointer(&fb[0]))) - - specs := []struct { - x, y uint16 - }{ - {80, 25}, - {90, 24}, - {79, 30}, - {100, 100}, - } - -nextSpec: - for specIndex, spec := range specs { - for i := 0; i < len(cons.fb); i++ { - fb[i] = 0 - } - - cons.Write('!', Red, spec.x, spec.y) - - for i := 0; i < len(cons.fb); i++ { - if got := fb[i]; got != 0 { - t.Errorf("[spec %d] expected Write() with off-screen coords to be a no-op", specIndex) - continue nextSpec - } - } - } -} - -func TestEgaWrite(t *testing.T) { - fb := make([]uint16, 80*25) - var cons Ega - cons.Init(80, 25, uintptr(unsafe.Pointer(&fb[0]))) - - attr := (Black << 4) | Red - cons.Write('!', attr, 0, 0) - - expVal := uint16(attr<<8) | uint16('!') - if got := fb[0]; got != expVal { - t.Errorf("expected call to Write() to set fb[0] to %d; got %d", expVal, got) - } -}