From dc37e86421d669c301e62fbc47f5e74aa02fafce Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Sat, 1 Jul 2017 18:27:31 +0100 Subject: [PATCH 01/11] Run the init() function for all kernel packages and their dependencies Things like error messages (e.g in the io pkg) are actually allocated when init() is executed. Unless we trigger a call to init(), values like this will be nil causing various problems when we try to use functions from the stdlib. --- src/gopheros/kernel/goruntime/bootstrap_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gopheros/kernel/goruntime/bootstrap_test.go b/src/gopheros/kernel/goruntime/bootstrap_test.go index cec83a7..37d1287 100644 --- a/src/gopheros/kernel/goruntime/bootstrap_test.go +++ b/src/gopheros/kernel/goruntime/bootstrap_test.go @@ -266,6 +266,7 @@ func TestInit(t *testing.T) { modulesInitFn = modulesInit typeLinksInitFn = typeLinksInit itabsInitFn = itabsInit + initGoPackagesFn = initGoPackages }() mallocInitFn = func() {} @@ -273,6 +274,7 @@ func TestInit(t *testing.T) { modulesInitFn = func() {} typeLinksInitFn = func() {} itabsInitFn = func() {} + initGoPackagesFn = func() {} if err := Init(); err != nil { t.Fatal(t) From d8793bd53041f7ed779effe966eca41fa26215b7 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Thu, 6 Jul 2017 06:56:22 +0100 Subject: [PATCH 02/11] Move kernel panic implementation to the kfmt package --- src/gopheros/kernel/{ => kfmt}/panic.go | 9 +++++---- src/gopheros/kernel/{ => kfmt}/panic_test.go | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) rename src/gopheros/kernel/{ => kfmt}/panic.go (87%) rename src/gopheros/kernel/{ => kfmt}/panic_test.go (96%) diff --git a/src/gopheros/kernel/panic.go b/src/gopheros/kernel/kfmt/panic.go similarity index 87% rename from src/gopheros/kernel/panic.go rename to src/gopheros/kernel/kfmt/panic.go index a326266..5ff9cb5 100644 --- a/src/gopheros/kernel/panic.go +++ b/src/gopheros/kernel/kfmt/panic.go @@ -1,6 +1,7 @@ -package kernel +package kfmt import ( + "gopheros/kernel" "gopheros/kernel/cpu" "gopheros/kernel/kfmt/early" ) @@ -9,7 +10,7 @@ var ( // cpuHaltFn is mocked by tests and is automatically inlined by the compiler. cpuHaltFn = cpu.Halt - errRuntimePanic = &Error{Module: "rt", Message: "unknown cause"} + errRuntimePanic = &kernel.Error{Module: "rt", Message: "unknown cause"} ) // Panic outputs the supplied error (if not nil) to the console and halts the @@ -17,10 +18,10 @@ var ( // for calls to panic() (resolved via runtime.gopanic) //go:redirect-from runtime.gopanic func Panic(e interface{}) { - var err *Error + var err *kernel.Error switch t := e.(type) { - case *Error: + case *kernel.Error: err = t case string: panicString(t) diff --git a/src/gopheros/kernel/panic_test.go b/src/gopheros/kernel/kfmt/panic_test.go similarity index 96% rename from src/gopheros/kernel/panic_test.go rename to src/gopheros/kernel/kfmt/panic_test.go index 68bfad8..e8ceacf 100644 --- a/src/gopheros/kernel/panic_test.go +++ b/src/gopheros/kernel/kfmt/panic_test.go @@ -1,8 +1,9 @@ -package kernel +package kfmt import ( "bytes" "errors" + "gopheros/kernel" "gopheros/kernel/cpu" "gopheros/kernel/driver/video/console" "gopheros/kernel/hal" @@ -23,7 +24,7 @@ func TestPanic(t *testing.T) { t.Run("with *kernel.Error", func(t *testing.T) { cpuHaltCalled = false fb := mockTTY() - err := &Error{Module: "test", Message: "panic test"} + err := &kernel.Error{Module: "test", Message: "panic test"} Panic(err) From 425ad7319b9fa35b0106e05a2bbce5ef486ee442 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Wed, 5 Jul 2017 19:36:08 +0100 Subject: [PATCH 03/11] Define driver interface This interface must be implemented by all device drivers in the driver sub-packages. --- src/gopheros/device/driver.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/gopheros/device/driver.go diff --git a/src/gopheros/device/driver.go b/src/gopheros/device/driver.go new file mode 100644 index 0000000..11cab21 --- /dev/null +++ b/src/gopheros/device/driver.go @@ -0,0 +1,15 @@ +package device + +import "gopheros/kernel" + +// Driver is an interface implemented by all drivers. +type Driver interface { + // DriverName returns the name of the driver. + DriverName() string + + // DriverVersion returns the driver version. + DriverVersion() (major uint16, minor uint16, patch uint16) + + // DriverInit initializes the device driver. + DriverInit() *kernel.Error +} From b72725742ab5554f8c25d22005fe8fdf77e8f205 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Tue, 4 Jul 2017 07:07:59 +0100 Subject: [PATCH 04/11] Revise console interface --- src/gopheros/device/video/console/device.go | 45 +++++++++++++++++ .../kernel/driver/video/console/console.go | 48 ------------------- 2 files changed, 45 insertions(+), 48 deletions(-) create mode 100644 src/gopheros/device/video/console/device.go delete mode 100644 src/gopheros/kernel/driver/video/console/console.go diff --git a/src/gopheros/device/video/console/device.go b/src/gopheros/device/video/console/device.go new file mode 100644 index 0000000..8129216 --- /dev/null +++ b/src/gopheros/device/video/console/device.go @@ -0,0 +1,45 @@ +package console + +import "image/color" + +// ScrollDir defines a scroll direction. +type ScrollDir uint8 + +// The supported list of scroll directions for the console Scroll() calls. +const ( + Up ScrollDir = iota + Down +) + +// The Device interface is implemented by objects that can function as system +// consoles. +type Device interface { + // Dimensions returns the width and height of the console in characters. + Dimensions() (uint16, uint16) + + // DefaultColors returns the default foreground and background colors + // used by this console. + DefaultColors() (fg, bg uint8) + + // Fill sets the contents of the specified rectangular region to the + // requested color. Both x and y coordinates are 1-based (top-left + // corner has coordinates 1,1). + Fill(x, y, width, height uint16, fg, bg uint8) + + // 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. + Scroll(dir ScrollDir, lines uint16) + + // Write a char to the specified location. Both x and y coordinates are + // 1-based (top-left corner has coordinates 1,1). + Write(ch byte, fg, bg uint8, x, y uint16) + + // Palette returns the active color palette for this console. + Palette() color.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. + SetPaletteColor(uint8, color.RGBA) +} diff --git a/src/gopheros/kernel/driver/video/console/console.go b/src/gopheros/kernel/driver/video/console/console.go deleted file mode 100644 index 0b2cf1f..0000000 --- a/src/gopheros/kernel/driver/video/console/console.go +++ /dev/null @@ -1,48 +0,0 @@ -package console - -// Attr defines a color attribute. -type Attr uint16 - -// The set of attributes that can be passed to Write(). -const ( - Black Attr = iota - Blue - Green - Cyan - Red - Magenta - Brown - LightGrey - Grey - LightBlue - LightGreen - LightCyan - LightRed - LightMagenta - LightBrown - White -) - -// ScrollDir defines a scroll direction. -type ScrollDir uint8 - -// The supported list of scroll directions for the console Scroll() calls. -const ( - Up ScrollDir = iota - Down -) - -// The Console interface is implemented by objects that can function as physical consoles. -type Console interface { - // Dimensions returns the width and height of the console in characters. - Dimensions() (uint16, uint16) - - // Clear clears the specified rectangular region - Clear(x, y, width, height uint16) - - // Scroll a particular number of lines to the specified direction. - Scroll(dir ScrollDir, lines uint16) - - // Write a char to the specified location. - Write(ch byte, attr Attr, x, y uint16) -} From 286b8d9c71858161fb34a48a019f06500d44c67e Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Tue, 4 Jul 2017 07:08:45 +0100 Subject: [PATCH 05/11] 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) - } -} From 78c87ab165d2a60c17778c68eb9932d6b1f6ce0c Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Wed, 5 Jul 2017 15:52:45 +0100 Subject: [PATCH 06/11] Cleanup and revise TTY interface --- src/gopheros/device/tty/device.go | 44 +++++++++++++++++++++++++++ src/gopheros/kernel/driver/tty/tty.go | 19 ------------ 2 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 src/gopheros/device/tty/device.go delete mode 100644 src/gopheros/kernel/driver/tty/tty.go diff --git a/src/gopheros/device/tty/device.go b/src/gopheros/device/tty/device.go new file mode 100644 index 0000000..a0f4e4d --- /dev/null +++ b/src/gopheros/device/tty/device.go @@ -0,0 +1,44 @@ +package tty + +import ( + "gopheros/device/video/console" + "io" +) + +// State defines the supported terminal state values. +type State uint8 + +const ( + // StateInactive marks the terminal as inactive. Any writes will be + // buffered and not synced to the attached console. + StateInactive State = iota + + // StateActive marks the terminal as active. Any writes will be + // buffered and also synced to the attached console. + StateActive +) + +// Device is implemented by objects that can be used as a terminal device. +type Device interface { + io.Writer + io.ByteWriter + + // AttachTo connects a TTY to a console instance. + AttachTo(console.Device) + + // State returns the TTY's state. + State() State + + // SetState updates the TTY's state. + SetState(State) + + // CursorPosition returns the current cursor x,y coordinates. Both + // coordinates are 1-based (top-left corner has coordinates 1,1). + CursorPosition() (uint16, uint16) + + // SetCursorPosition sets the current cursor position to (x,y). Both + // coordinates are 1-based (top-left corner has coordinates 1,1). + // Implementations are expected to clip the cursor position to their + // viewport. + SetCursorPosition(x, y uint16) +} diff --git a/src/gopheros/kernel/driver/tty/tty.go b/src/gopheros/kernel/driver/tty/tty.go deleted file mode 100644 index f46ce2f..0000000 --- a/src/gopheros/kernel/driver/tty/tty.go +++ /dev/null @@ -1,19 +0,0 @@ -package tty - -import "io" - -// Tty is implemented by objects that can register themselves as ttys. -type Tty interface { - io.Writer - io.ByteWriter - - // Position returns the current cursor position (x, y). - Position() (uint16, uint16) - - // SetPosition sets the current cursor position to (x,y). Console implementations - // must clip the provided cursor position if it exceeds the console dimensions. - SetPosition(x, y uint16) - - // Clear clears the terminal. - Clear() -} From 0f3af2e78d3723e02822700ceb90485a9634490c Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Wed, 5 Jul 2017 15:56:27 +0100 Subject: [PATCH 07/11] Refactor VT implementation This commit refactors the old VT implementation to work with the revised TTY interface and adds support for: - scrollback - terminal state handling When a terminal becomes activated, it overwrites the attached console contents with the contents of its viewport. --- src/gopheros/device/tty/vt.go | 260 ++++++++++++++ src/gopheros/device/tty/vt_test.go | 407 ++++++++++++++++++++++ src/gopheros/kernel/driver/tty/vt.go | 128 ------- src/gopheros/kernel/driver/tty/vt_test.go | 88 ----- 4 files changed, 667 insertions(+), 216 deletions(-) create mode 100644 src/gopheros/device/tty/vt.go create mode 100644 src/gopheros/device/tty/vt_test.go delete mode 100644 src/gopheros/kernel/driver/tty/vt.go delete mode 100644 src/gopheros/kernel/driver/tty/vt_test.go diff --git a/src/gopheros/device/tty/vt.go b/src/gopheros/device/tty/vt.go new file mode 100644 index 0000000..ea63238 --- /dev/null +++ b/src/gopheros/device/tty/vt.go @@ -0,0 +1,260 @@ +package tty + +import ( + "gopheros/device/video/console" + "gopheros/kernel" + "io" +) + +// VT implements a terminal supporting scrollback. The terminal interprets the +// following special characters: +// - \r (carriage-return) +// - \n (line-feed) +// - \b (backspace) +// - \t (tab; expanded to tabWidth spaces) +type VT struct { + cons console.Device + + // Terminal dimensions + termWidth uint16 + termHeight uint16 + viewportWidth uint16 + viewportHeight uint16 + + // The number of additional lines of output that are buffered by the + // terminal to support scrolling up. + scrollback uint16 + + // The terminal contents. Each character occupies 3 bytes and uses the + // format: (ASCII char, fg, bg) + data []uint8 + + // Terminal state. + tabWidth uint8 + defaultFg, curFg uint8 + defaultBg, curBg uint8 + cursorX uint16 + cursorY uint16 + viewportY uint16 + dataOffset uint + state State +} + +// NewVT creates a new virtual terminal device. The tabWidth parameter controls +// tab expansion whereas the scrollback parameter defines the line count that +// gets buffered by the terminal to provide scrolling beyond the console +// height. +func NewVT(tabWidth uint8, scrollback uint16) *VT { + return &VT{ + tabWidth: tabWidth, + scrollback: scrollback, + cursorX: 1, + cursorY: 1, + } +} + +// AttachTo connects a TTY to a console instance. +func (t *VT) AttachTo(cons console.Device) { + if cons == nil { + return + } + + t.cons = cons + t.viewportWidth, t.viewportHeight = cons.Dimensions() + t.viewportY = 0 + t.defaultFg, t.defaultBg = cons.DefaultColors() + t.curFg, t.curBg = t.defaultFg, t.defaultBg + t.termWidth, t.termHeight = t.viewportWidth, t.viewportHeight+t.scrollback + t.cursorX, t.cursorY = 1, 1 + + // Allocate space for the contents and fill it with empty characters + // using the default fg/bg colors for the attached console. + t.data = make([]uint8, t.termWidth*t.termHeight*3) + for i := 0; i < len(t.data); i += 3 { + t.data[i] = ' ' + t.data[i+1] = t.defaultFg + t.data[i+2] = t.defaultBg + } +} + +// State returns the TTY's state. +func (t *VT) State() State { + return t.state +} + +// SetState updates the TTY's state. +func (t *VT) SetState(newState State) { + if t.state == newState { + return + } + + t.state = newState + + // If the terminal became active, update the console with its contents + if t.state == StateActive && t.cons != nil { + for y := uint16(1); y <= t.viewportHeight; y++ { + offset := (y - 1 + t.viewportY) * (t.viewportWidth * 3) + for x := uint16(1); x <= t.viewportWidth; x, offset = x+1, offset+3 { + t.cons.Write(t.data[offset], t.data[offset+1], t.data[offset+2], x, y) + } + } + } +} + +// CursorPosition returns the current cursor position. +func (t *VT) CursorPosition() (uint16, uint16) { + return t.cursorX, t.cursorY +} + +// SetCursorPosition sets the current cursor position to (x,y). +func (t *VT) SetCursorPosition(x, y uint16) { + if t.cons == nil { + return + } + + if x < 1 { + x = 1 + } else if x > t.viewportWidth { + x = t.viewportWidth + } + + if y < 1 { + y = 1 + } else if y > t.viewportHeight { + y = t.viewportHeight + } + + t.cursorX, t.cursorY = x, y + t.updateDataOffset() +} + +// Write implements io.Writer. +func (t *VT) Write(data []byte) (int, error) { + for count, b := range data { + err := t.WriteByte(b) + if err != nil { + return count, err + } + } + + return len(data), nil +} + +// WriteByte implements io.ByteWriter. +func (t *VT) WriteByte(b byte) error { + if t.cons == nil { + return io.ErrClosedPipe + } + + switch b { + case '\r': + t.cr() + case '\n': + t.lf(true) + case '\b': + if t.cursorX > 1 { + t.SetCursorPosition(t.cursorX-1, t.cursorY) + t.doWrite(' ', false) + } + case '\t': + for i := uint8(0); i < t.tabWidth; i++ { + t.doWrite(' ', true) + } + default: + t.doWrite(b, true) + } + + return nil +} + +// doWrite writes the specified character together with the current fg/bg +// attributes at the current data offset advancing the cursor position if +// advanceCursor is true. If the terminal is active, then doWrite also writes +// the character to the attached console. +func (t *VT) doWrite(b byte, advanceCursor bool) { + if t.state == StateActive { + t.cons.Write(b, t.curFg, t.curBg, t.cursorX, t.cursorY) + } + + t.data[t.dataOffset] = b + t.data[t.dataOffset+1] = t.curFg + t.data[t.dataOffset+2] = t.curBg + + if advanceCursor { + // Advance x position and handle wrapping when the cursor reaches the + // end of the current line + t.dataOffset += 3 + t.cursorX++ + if t.cursorX > t.viewportWidth { + t.lf(true) + } + } +} + +// cr resets the x coordinate of the terminal cursor to 0. +func (t *VT) cr() { + t.cursorX = 1 + t.updateDataOffset() +} + +// lf advances the y coordinate of the terminal cursor by one line scrolling +// the terminal contents if the end of the last terminal line is reached. +func (t *VT) lf(withCR bool) { + if withCR { + t.cursorX = 1 + } + + switch { + // Cursor has not reached the end of the viewport + case t.cursorY+1 <= t.viewportHeight: + t.cursorY++ + default: + // Check if the viewport can be scrolled down + if t.viewportY+t.viewportHeight < t.termHeight { + t.viewportY++ + } else { + // We have reached the bottom of the terminal buffer. + // We need to scroll its contents up and clear the last line + var stride = int(t.viewportWidth * 3) + var startOffset = int(t.viewportY) * stride + var endOffset = int(t.viewportY+t.viewportHeight-1) * stride + + for offset := startOffset; offset < endOffset; offset++ { + t.data[offset] = t.data[offset+stride] + } + + for offset := endOffset; offset < endOffset+stride; offset += 3 { + t.data[offset+0] = ' ' + t.data[offset+1] = t.defaultFg + t.data[offset+2] = t.defaultBg + } + } + + // Sync console + if t.state == StateActive { + t.cons.Scroll(console.ScrollDirUp, 1) + t.cons.Fill(1, t.cursorY, t.termWidth, 1, t.defaultFg, t.defaultBg) + } + } + + t.updateDataOffset() +} + +// updateDataOffset calculates the offset in the data buffer taking into account +// the cursor position and the viewportY value. +func (t *VT) updateDataOffset() { + t.dataOffset = uint((t.viewportY+(t.cursorY-1))*(t.viewportWidth*3) + ((t.cursorX - 1) * 3)) +} + +// DriverName returns the name of this driver. +func (t *VT) DriverName() string { + return "vt" +} + +// DriverVersion returns the version of this driver. +func (t *VT) DriverVersion() (uint16, uint16, uint16) { + return 0, 0, 1 +} + +// DriverInit initializes this driver. +func (t *VT) DriverInit() *kernel.Error { return nil } diff --git a/src/gopheros/device/tty/vt_test.go b/src/gopheros/device/tty/vt_test.go new file mode 100644 index 0000000..678acba --- /dev/null +++ b/src/gopheros/device/tty/vt_test.go @@ -0,0 +1,407 @@ +package tty + +import ( + "gopheros/device" + "gopheros/device/video/console" + "image/color" + "io" + "testing" +) + +func TestVtPosition(t *testing.T) { + specs := []struct { + inX, inY uint16 + expX, expY uint16 + }{ + {20, 20, 20, 20}, + {100, 20, 80, 20}, + {10, 200, 10, 25}, + {10, 200, 10, 25}, + {100, 100, 80, 25}, + } + + term := NewVT(4, 0) + + // SetCursorPosition without an attached console is a no-op + term.SetCursorPosition(2, 2) + + if curX, curY := term.CursorPosition(); curX != 1 || curY != 1 { + t.Fatalf("expected terminal initial position to be (1, 1); got (%d, %d)", curX, curY) + } + + cons := newMockConsole(80, 25) + term.AttachTo(cons) + + for specIndex, spec := range specs { + term.SetCursorPosition(spec.inX, spec.inY) + if x, y := term.CursorPosition(); x != spec.expX || y != spec.expY { + t.Errorf("[spec %d] expected setting position to (%d, %d) to update the position to (%d, %d); got (%d, %d)", specIndex, spec.inX, spec.inY, spec.expX, spec.expY, x, y) + } + } +} + +func TestVtWrite(t *testing.T) { + t.Run("inactive terminal", func(t *testing.T) { + cons := newMockConsole(80, 25) + + term := NewVT(4, 0) + if _, err := term.Write([]byte("foo")); err != io.ErrClosedPipe { + t.Fatal("expected calling Write on a terminal without an attached console to return ErrClosedPipe") + } + + term.AttachTo(cons) + + term.curFg = 2 + term.curBg = 3 + + data := []byte("\b123\b4\t5\n67\r68") + count, err := term.Write(data) + if err != nil { + t.Fatal(err) + } + + if count != len(data) { + t.Fatalf("expected to write %d bytes; wrote %d", len(data), count) + } + + if cons.bytesWritten != 0 { + t.Fatalf("expected writes not to be synced with console when terminal is inactive; %d bytes written", cons.bytesWritten) + } + + specs := []struct { + x, y uint16 + expByte uint8 + }{ + {1, 1, '1'}, + {2, 1, '2'}, + {3, 1, '4'}, + {8, 1, '5'}, // 2 + tabWidth + 1 + {1, 2, '6'}, + {2, 2, '8'}, + } + + for specIndex, spec := range specs { + offset := ((spec.y - 1) * term.viewportWidth * 3) + ((spec.x - 1) * 3) + if term.data[offset] != spec.expByte { + t.Errorf("[spec %d] expected char at (%d, %d) to be %q; got %q", specIndex, spec.x, spec.y, spec.expByte, term.data[offset]) + } + + if term.data[offset+1] != term.curFg { + t.Errorf("[spec %d] expected fg attribute at (%d, %d) to be %d; got %d", specIndex, spec.x, spec.y, term.curFg, term.data[offset+1]) + } + + if term.data[offset+2] != term.curBg { + t.Errorf("[spec %d] expected bg attribute at (%d, %d) to be %d; got %d", specIndex, spec.x, spec.y, term.curBg, term.data[offset+2]) + } + } + }) + + t.Run("active terminal", func(t *testing.T) { + cons := newMockConsole(80, 25) + + term := NewVT(4, 0) + term.SetState(StateActive) + term.SetState(StateActive) // calling SetState with the same state is a no-op + + if got := term.State(); got != StateActive { + t.Fatalf("expected terminal state to be %d; got %d", StateActive, got) + } + + term.AttachTo(cons) + + term.curFg = 2 + term.curBg = 3 + + data := []byte("\b123\b4\t5\n67\r68") + term.Write(data) + + if expCount := len(data); cons.bytesWritten != expCount { + t.Fatalf("expected writes to be synced with console when terminal is active. %d bytes written; expected %d", cons.bytesWritten, expCount) + } + + specs := []struct { + x, y uint16 + expByte uint8 + }{ + {1, 1, '1'}, + {2, 1, '2'}, + {3, 1, '4'}, + {8, 1, '5'}, // 2 + tabWidth + 1 + {1, 2, '6'}, + {2, 2, '8'}, + } + + for specIndex, spec := range specs { + offset := ((spec.y - 1) * cons.width) + (spec.x - 1) + if cons.chars[offset] != spec.expByte { + t.Errorf("[spec %d] expected console char at (%d, %d) to be %q; got %q", specIndex, spec.x, spec.y, spec.expByte, cons.chars[offset]) + } + + if cons.fgAttrs[offset] != term.curFg { + t.Errorf("[spec %d] expected console fg attribute at (%d, %d) to be %d; got %d", specIndex, spec.x, spec.y, term.curFg, cons.fgAttrs[offset]) + } + + if cons.bgAttrs[offset] != term.curBg { + t.Errorf("[spec %d] expected console bg attribute at (%d, %d) to be %d; got %d", specIndex, spec.x, spec.y, term.curBg, cons.bgAttrs[offset]) + } + } + }) +} + +func TestVtLineFeedHandling(t *testing.T) { + t.Run("viewport at end of terminal", func(t *testing.T) { + cons := newMockConsole(80, 25) + + term := NewVT(4, 0) + term.SetState(StateActive) + term.AttachTo(cons) + + // Fill last line except the last column which will trigger a + // line feed. Cursor position will be automatically clipped to + // the viewport bounds + term.SetCursorPosition(1, term.viewportHeight+1) + for i := uint16(0); i < term.viewportWidth-1; i++ { + term.WriteByte(byte('0' + (i % 10))) + } + + // Emulate viewportHeight line feeds. The last one should cause a scroll + term.SetCursorPosition(0, 0) // cursor is set to (1,1) + for i := uint16(0); i < term.viewportHeight; i++ { + term.lf(true) + } + + if cons.scrollUpCount != 1 { + t.Fatalf("expected console to be scrolled up 1 time; got %d", cons.scrollUpCount) + } + + // Set cursor one line above the last; this line should now + // contain the scrolled contents + term.SetCursorPosition(1, term.viewportHeight-1) + for col, offset := uint16(1), term.dataOffset; col <= term.viewportWidth; col, offset = col+1, offset+3 { + expByte := byte('0' + ((col - 1) % 10)) + if col == term.viewportWidth { + expByte = ' ' + } + + if term.data[offset] != expByte { + t.Errorf("expected char at (%d, %d) to be %q; got %q", col, term.viewportHeight-1, expByte, term.data[offset]) + } + } + + // Set cursor to the last line. This line should now be cleared + term.SetCursorPosition(1, term.viewportHeight) + for col, offset := uint16(1), term.dataOffset; col <= term.viewportWidth; col, offset = col+1, offset+3 { + expByte := uint8(' ') + if term.data[offset] != expByte { + t.Errorf("expected char at (%d, %d) to be %q; got %q", col, term.viewportHeight, expByte, term.data[offset]) + } + } + }) + + t.Run("viewport not at end of terminal", func(t *testing.T) { + cons := newMockConsole(80, 25) + + term := NewVT(4, 1) + term.SetState(StateActive) + term.AttachTo(cons) + + // Fill last line except the last column which will trigger a + // line feed. Cursor position will be automatically clipped to + // the viewport bounds + term.SetCursorPosition(1, term.viewportHeight+1) + for i := uint16(0); i < term.viewportWidth-1; i++ { + term.WriteByte(byte('0' + (i % 10))) + } + + // Fill first line including the last column + term.SetCursorPosition(1, 1) + for i := uint16(0); i < term.viewportWidth; i++ { + term.WriteByte(byte('0' + (i % 10))) + } + + // Emulate viewportHeight line feeds. The last one should cause a scroll + // in the console but only a viewport adjustment in the terminal + term.SetCursorPosition(0, 0) // cursor is set to (1,1) + for i := uint16(0); i < term.viewportHeight; i++ { + term.lf(true) + } + + if cons.scrollUpCount != 1 { + t.Fatalf("expected console to be scrolled up 1 time; got %d", cons.scrollUpCount) + } + + if expViewportY := uint16(1); term.viewportY != expViewportY { + t.Fatalf("expected terminal viewportY to be adjusted to %d; got %d", expViewportY, term.viewportY) + } + + // Check that first line is still available in the terminal buffer + // that is not currently visible + term.SetCursorPosition(1, 1) + offset := term.dataOffset - uint(term.viewportWidth*3) + + for col := uint16(1); col <= term.viewportWidth; col, offset = col+1, offset+3 { + expByte := byte('0' + ((col - 1) % 10)) + + if term.data[offset] != expByte { + t.Errorf("expected char at hidden region (%d, -1) to be %q; got %q", col, expByte, term.data[offset]) + } + } + }) +} + +func TestVtAttach(t *testing.T) { + cons := newMockConsole(80, 25) + + term := NewVT(4, 1) + + // AttachTo with a nil console should be a no-op + term.AttachTo(nil) + if term.termWidth != 0 || term.termHeight != 0 || term.viewportWidth != 0 || term.viewportHeight != 0 { + t.Fatal("expected attaching a nil console to be a no-op") + } + + term.AttachTo(cons) + if term.termWidth != cons.width || + term.termHeight != cons.height+term.scrollback || + term.viewportWidth != cons.width || + term.viewportHeight != cons.height || + term.data == nil { + t.Fatal("expected the terminal to initialize using the attached console info") + } +} + +func TestVtSetState(t *testing.T) { + cons := newMockConsole(80, 25) + term := NewVT(4, 1) + term.AttachTo(cons) + + // Fill terminal viewport using a rotating pattern. Writing the last + // character will cause a scroll operation moving the viewport down + row := 0 + for index := 0; index < int(term.viewportWidth*term.viewportHeight); index++ { + if index != 0 && index%int(term.viewportWidth) == 0 { + row++ + } + term.curFg = uint8((row + index + 1) % 10) + term.curBg = uint8((row + index + 2) % 10) + term.WriteByte(byte('0' + (row+index)%10)) + } + + // Activating this terminal should trigger a copy of the terminal viewport + // contents to the console. + term.SetState(StateActive) + row = 1 + for index := 0; index < len(cons.chars); index++ { + if index != 0 && index%int(cons.width) == 0 { + row++ + } + + expCh := uint8('0' + (row+index)%10) + expFg := uint8((row + index + 1) % 10) + expBg := uint8((row + index + 2) % 10) + + // last line should be cleared due to the scroll operation + if row == int(cons.height) { + expCh = ' ' + expFg = 7 + expBg = 0 + } + + if cons.chars[index] != expCh { + t.Errorf("expected console char at index %d to be %q; got %q", index, expCh, cons.chars[index]) + } + + if cons.fgAttrs[index] != expFg { + t.Errorf("expected console fg attr at index %d to be %d; got %d", index, expFg, cons.fgAttrs[index]) + } + + if cons.bgAttrs[index] != expBg { + t.Errorf("expected console bg attr at index %d to be %d; got %d", index, expBg, cons.bgAttrs[index]) + } + } +} + +func TestVTDriverInterface(t *testing.T) { + var dev device.Driver = NewVT(0, 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") + } +} + +type mockConsole struct { + width, height uint16 + fg, bg uint8 + chars []uint8 + fgAttrs []uint8 + bgAttrs []uint8 + bytesWritten int + scrollUpCount int + scrollDownCount int +} + +func newMockConsole(w, h uint16) *mockConsole { + return &mockConsole{ + width: w, + height: h, + fg: 7, + bg: 0, + chars: make([]uint8, w*h), + fgAttrs: make([]uint8, w*h), + bgAttrs: make([]uint8, w*h), + } +} + +func (cons *mockConsole) Dimensions() (uint16, uint16) { + return cons.width, cons.height +} + +func (cons *mockConsole) DefaultColors() (uint8, uint8) { + return cons.fg, cons.bg +} + +func (cons *mockConsole) Fill(x, y, width, height uint16, fg, bg uint8) { + yEnd := y + height - 1 + xEnd := x + width - 1 + + for fy := y; fy <= yEnd; fy++ { + offset := ((fy - 1) * cons.width) + for fx := x; fx <= xEnd; fx, offset = fx+1, offset+1 { + cons.chars[offset] = ' ' + cons.fgAttrs[offset] = fg + cons.bgAttrs[offset] = bg + } + } +} + +func (cons *mockConsole) Scroll(dir console.ScrollDir, lines uint16) { + switch dir { + case console.ScrollDirUp: + cons.scrollUpCount++ + case console.ScrollDirDown: + cons.scrollDownCount++ + } +} + +func (cons *mockConsole) Palette() color.Palette { + return nil +} + +func (cons *mockConsole) SetPaletteColor(index uint8, color color.RGBA) { +} + +func (cons *mockConsole) Write(b byte, fg, bg uint8, x, y uint16) { + offset := ((y - 1) * cons.width) + (x - 1) + cons.chars[offset] = b + cons.fgAttrs[offset] = fg + cons.bgAttrs[offset] = bg + cons.bytesWritten++ +} diff --git a/src/gopheros/kernel/driver/tty/vt.go b/src/gopheros/kernel/driver/tty/vt.go deleted file mode 100644 index f18cef2..0000000 --- a/src/gopheros/kernel/driver/tty/vt.go +++ /dev/null @@ -1,128 +0,0 @@ -package tty - -import "gopheros/kernel/driver/video/console" - -const ( - defaultFg = console.LightGrey - defaultBg = console.Black - tabWidth = 4 -) - -// Vt implements a simple terminal that can process LF and CR characters. The -// terminal uses a console device for its output. -type Vt struct { - // Go interfaces will not work before we can get memory allocation working. - // Till then we need to use concrete types instead. - cons *console.Ega - - width uint16 - height uint16 - - curX uint16 - curY uint16 - curAttr console.Attr -} - -// AttachTo links the terminal with the specified console device and updates -// the terminal's dimensions to match the ones reported by the attached device. -func (t *Vt) AttachTo(cons *console.Ega) { - t.cons = cons - t.width, t.height = cons.Dimensions() - t.curX = 0 - t.curY = 0 - - // Default to lightgrey on black text. - t.curAttr = makeAttr(defaultFg, defaultBg) -} - -// Clear clears the terminal. -func (t *Vt) Clear() { - t.clear() -} - -// Position returns the current cursor position (x, y). -func (t *Vt) Position() (uint16, uint16) { - return t.curX, t.curY -} - -// SetPosition sets the current cursor position to (x,y). -func (t *Vt) SetPosition(x, y uint16) { - if x >= t.width { - x = t.width - 1 - } - - if y >= t.height { - y = t.height - 1 - } - - t.curX, t.curY = x, y -} - -// Write implements io.Writer. -func (t *Vt) Write(data []byte) (int, error) { - for _, b := range data { - t.WriteByte(b) - } - - return len(data), nil -} - -// WriteByte implements io.ByteWriter. -func (t *Vt) WriteByte(b byte) error { - switch b { - case '\r': - t.cr() - case '\n': - t.cr() - t.lf() - case '\b': - if t.curX > 0 { - t.cons.Write(' ', t.curAttr, t.curX, t.curY) - t.curX-- - } - case '\t': - for i := 0; i < tabWidth; i++ { - t.cons.Write(' ', t.curAttr, t.curX, t.curY) - t.curX++ - if t.curX == t.width { - t.cr() - t.lf() - } - } - default: - t.cons.Write(b, t.curAttr, t.curX, t.curY) - t.curX++ - if t.curX == t.width { - t.cr() - t.lf() - } - } - - return nil -} - -// cls clears the terminal. -func (t *Vt) clear() { - t.cons.Clear(0, 0, t.width, t.height) -} - -// cr resets the x coordinate of the terminal cursor to 0. -func (t *Vt) cr() { - t.curX = 0 -} - -// lf advances the y coordinate of the terminal cursor by one line scrolling -// the terminal contents if the end of the last terminal line is reached. -func (t *Vt) lf() { - if t.curY+1 < t.height { - t.curY++ - return - } - - t.cons.Scroll(console.Up, 1) - t.cons.Clear(0, t.height-1, t.width, 1) -} - -func makeAttr(fg, bg console.Attr) console.Attr { - return (bg << 4) | (fg & 0xF) -} diff --git a/src/gopheros/kernel/driver/tty/vt_test.go b/src/gopheros/kernel/driver/tty/vt_test.go deleted file mode 100644 index 4648266..0000000 --- a/src/gopheros/kernel/driver/tty/vt_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package tty - -import ( - "gopheros/kernel/driver/video/console" - "testing" - "unsafe" -) - -func TestVtPosition(t *testing.T) { - specs := []struct { - inX, inY uint16 - expX, expY uint16 - }{ - {20, 20, 20, 20}, - {100, 20, 79, 20}, - {10, 200, 10, 24}, - {10, 200, 10, 24}, - {100, 100, 79, 24}, - } - - fb := make([]uint16, 80*25) - var cons console.Ega - cons.Init(80, 25, uintptr(unsafe.Pointer(&fb[0]))) - - var vt Vt - vt.AttachTo(&cons) - - for specIndex, spec := range specs { - vt.SetPosition(spec.inX, spec.inY) - if x, y := vt.Position(); x != spec.expX || y != spec.expY { - t.Errorf("[spec %d] expected setting position to (%d, %d) to update the position to (%d, %d); got (%d, %d)", specIndex, spec.inX, spec.inY, spec.expX, spec.expY, x, y) - } - } -} - -func TestWrite(t *testing.T) { - fb := make([]uint16, 80*25) - var cons console.Ega - cons.Init(80, 25, uintptr(unsafe.Pointer(&fb[0]))) - - var vt Vt - vt.AttachTo(&cons) - - vt.Clear() - vt.SetPosition(0, 1) - vt.Write([]byte("12\n\t3\n4\r567\b8")) - - // Tab spanning rows - vt.SetPosition(78, 4) - vt.WriteByte('\t') - vt.WriteByte('9') - - // Trigger scroll - vt.SetPosition(79, 24) - vt.Write([]byte{'!'}) - - specs := []struct { - x, y uint16 - expChar byte - }{ - {0, 0, '1'}, - {1, 0, '2'}, - // tabs - {0, 1, ' '}, - {1, 1, ' '}, - {2, 1, ' '}, - {3, 1, ' '}, - {4, 1, '3'}, - // tab spanning 2 rows - {78, 3, ' '}, - {79, 3, ' '}, - {0, 4, ' '}, - {1, 4, ' '}, - {2, 4, '9'}, - // - {0, 2, '5'}, - {1, 2, '6'}, - {2, 2, '8'}, // overwritten by BS - {79, 23, '!'}, - } - - for specIndex, spec := range specs { - ch := (byte)(fb[(spec.y*vt.width)+spec.x] & 0xFF) - if ch != spec.expChar { - t.Errorf("[spec %d] expected char at (%d, %d) to be %c; got %c", specIndex, spec.x, spec.y, spec.expChar, ch) - } - } -} From f691d75b290b4916151f9613a8130549ad4e5418 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Fri, 30 Jun 2017 21:22:19 +0100 Subject: [PATCH 08/11] Implement ring-buffer for capturing early printf output The ring-buffer implements both io.Reader and io.Writer and uses a fixed size of 2048 bytes (set by the ringBufferSize constant). This provides enough space to hold a standard 80x25 screen's output. --- src/gopheros/kernel/kfmt/ringbuf.go | 65 ++++++++++++++++ src/gopheros/kernel/kfmt/ringbuf_test.go | 96 ++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 src/gopheros/kernel/kfmt/ringbuf.go create mode 100644 src/gopheros/kernel/kfmt/ringbuf_test.go diff --git a/src/gopheros/kernel/kfmt/ringbuf.go b/src/gopheros/kernel/kfmt/ringbuf.go new file mode 100644 index 0000000..2bd5baa --- /dev/null +++ b/src/gopheros/kernel/kfmt/ringbuf.go @@ -0,0 +1,65 @@ +package kfmt + +import "io" + +// ringBufferSize defines size of the ring buffer that buffers early Printf +// output. Its default size is selected so it can buffer the contents of a +// standard 80*25 text-mode console. The ring buffer size must always be a +// power of 2. +const ringBufferSize = 2048 + +// ringBuffer models a ring buffer of size ringBufferSize. This buffer is used +// for capturing the output of Printf before the tty and console systems are +// initialized. +type ringBuffer struct { + buffer [ringBufferSize]byte + rIndex, wIndex int +} + +// Write writes len(p) bytes from p to the ringBuffer. +func (rb *ringBuffer) Write(p []byte) (int, error) { + for _, b := range p { + rb.buffer[rb.wIndex] = b + rb.wIndex = (rb.wIndex + 1) & (ringBufferSize - 1) + if rb.rIndex == rb.wIndex { + rb.rIndex = (rb.rIndex + 1) & (ringBufferSize - 1) + } + } + + return len(p), nil +} + +// Read reads up to len(p) bytes into p. It returns the number of bytes read (0 +// <= n <= len(p)) and any error encountered. +func (rb *ringBuffer) Read(p []byte) (n int, err error) { + switch { + case rb.rIndex < rb.wIndex: + // read up to min(wIndex - rIndex, len(p)) bytes + n = rb.wIndex - rb.rIndex + if pLen := len(p); pLen < n { + n = pLen + } + + copy(p, rb.buffer[rb.rIndex:rb.rIndex+n]) + rb.rIndex += n + + return n, nil + case rb.rIndex > rb.wIndex: + // Read up to min(len(buf) - rIndex, len(p)) bytes + n = len(rb.buffer) - rb.rIndex + if pLen := len(p); pLen < n { + n = pLen + } + + copy(p, rb.buffer[rb.rIndex:rb.rIndex+n]) + rb.rIndex += n + + if rb.rIndex == len(rb.buffer) { + rb.rIndex = 0 + } + + return n, nil + default: // rIndex == wIndex + return 0, io.EOF + } +} diff --git a/src/gopheros/kernel/kfmt/ringbuf_test.go b/src/gopheros/kernel/kfmt/ringbuf_test.go new file mode 100644 index 0000000..0f153bd --- /dev/null +++ b/src/gopheros/kernel/kfmt/ringbuf_test.go @@ -0,0 +1,96 @@ +package kfmt + +import ( + "bytes" + "io" + "testing" +) + +func TestRingBuffer(t *testing.T) { + var ( + buf bytes.Buffer + expStr = "the big brown fox jumped over the lazy dog" + rb ringBuffer + ) + + t.Run("read/write", func(t *testing.T) { + rb.wIndex = 0 + rb.rIndex = 0 + n, err := rb.Write([]byte(expStr)) + if err != nil { + t.Fatal(err) + } + + if n != len(expStr) { + t.Fatalf("expected to write %d bytes; wrote %d", len(expStr), n) + } + + if got := readByteByByte(&buf, &rb); got != expStr { + t.Fatalf("expected to read %q; got %q", expStr, got) + } + }) + + t.Run("write moves read pointer", func(t *testing.T) { + rb.wIndex = ringBufferSize - 1 + rb.rIndex = 0 + _, err := rb.Write([]byte{'!'}) + if err != nil { + t.Fatal(err) + } + + if exp := 1; rb.rIndex != exp { + t.Fatalf("expected write to push rIndex to %d; got %d", exp, rb.rIndex) + } + }) + + t.Run("wIndex < rIndex", func(t *testing.T) { + rb.wIndex = ringBufferSize - 2 + rb.rIndex = ringBufferSize - 2 + n, err := rb.Write([]byte(expStr)) + if err != nil { + t.Fatal(err) + } + + if n != len(expStr) { + t.Fatalf("expected to write %d bytes; wrote %d", len(expStr), n) + } + + if got := readByteByByte(&buf, &rb); got != expStr { + t.Fatalf("expected to read %q; got %q", expStr, got) + } + }) + + t.Run("with io.WriteTo", func(t *testing.T) { + rb.wIndex = ringBufferSize - 2 + rb.rIndex = ringBufferSize - 2 + n, err := rb.Write([]byte(expStr)) + if err != nil { + t.Fatal(err) + } + + if n != len(expStr) { + t.Fatalf("expected to write %d bytes; wrote %d", len(expStr), n) + } + + var buf bytes.Buffer + io.Copy(&buf, &rb) + + if got := buf.String(); got != expStr { + t.Fatalf("expected to read %q; got %q", expStr, got) + } + }) +} + +func readByteByByte(buf *bytes.Buffer, r io.Reader) string { + buf.Reset() + var b = make([]byte, 1) + for { + _, err := r.Read(b) + if err == io.EOF { + break + } + + buf.Write(b) + } + return buf.String() +} From 545a18fcccda39b59343990ff3b4cd9149eed0d4 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Sat, 1 Jul 2017 07:02:16 +0100 Subject: [PATCH 09/11] Support pluggable output sinks for Printf with a ring-buffer fallback The implementation of Printf has been moved from the early package to the kfmt package. The dependency to ActiveTerminal has been removed and the code now uses an io.Writer for its output. As Go interfaces cannot be used before bootstrapping the Go runtime, the code uses a ring-buffer fallback for storing any kernel output emitted before that point. --- Makefile | 1 + .../kfmt/{early/early_fmt.go => fmt.go} | 177 +++++++++++++----- .../{early/early_fmt_test.go => fmt_test.go} | 70 ++++--- 3 files changed, 168 insertions(+), 80 deletions(-) rename src/gopheros/kernel/kfmt/{early/early_fmt.go => fmt.go} (51%) rename src/gopheros/kernel/kfmt/{early/early_fmt_test.go => fmt_test.go} (79%) diff --git a/Makefile b/Makefile index ec80919..c9bcaa7 100644 --- a/Makefile +++ b/Makefile @@ -170,6 +170,7 @@ lint: lint-check-deps --deadline 300s \ --exclude 'return value not checked' \ --exclude 'possible misuse of unsafe.Pointer' \ + --exclude 'x \^ 0 always equals x' \ ./... lint-check-deps: diff --git a/src/gopheros/kernel/kfmt/early/early_fmt.go b/src/gopheros/kernel/kfmt/fmt.go similarity index 51% rename from src/gopheros/kernel/kfmt/early/early_fmt.go rename to src/gopheros/kernel/kfmt/fmt.go index 224152c..7780dcd 100644 --- a/src/gopheros/kernel/kfmt/early/early_fmt.go +++ b/src/gopheros/kernel/kfmt/fmt.go @@ -1,20 +1,48 @@ -package early +package kfmt -import "gopheros/kernel/hal" +import ( + "io" + "unsafe" +) + +// maxBufSize defines the buffer size for formatting numbers. +const maxBufSize = 32 var ( errMissingArg = []byte("(MISSING)") errWrongArgType = []byte("%!(WRONGTYPE)") errNoVerb = []byte("%!(NOVERB)") errExtraArg = []byte("%!(EXTRA)") - padding = byte(' ') trueValue = []byte("true") falseValue = []byte("false") + + numFmtBuf = []byte("012345678901234567890123456789012") + + // singleByte is used as a shared buffer for passing single characters + // to doWrite. + singleByte = []byte(" ") + + // earlyPrintBuffer is a ring buffer that stores Printf output before the + // console and TTYs are initialized. + earlyPrintBuffer ringBuffer + + // outputSink is a io.Writer where Printf will send its output. If set + // to nil, then the output will be redirected to the earlyPrintBuffer. + outputSink io.Writer ) -// Printf provides a minimal Printf implementation that can be used before the -// Go runtime has been properly initialized. This version of printf does not -// allocate any memory and uses hal.ActiveTerminal for its output. +// SetOutputSink sets the default target for calls to Printf to w and copies +// any data accumulated in the earlyPrintBuffer to itt . +func SetOutputSink(w io.Writer) { + outputSink = w + if w != nil { + io.Copy(w, &earlyPrintBuffer) + } +} + +// Printf provides a minimal Printf implementation that can be safely used +// before the Go runtime has been properly initialized. This implementation +// does not allocate any memory. // // Similar to fmt.Printf, this version of printf supports the following subset // of formatting verbs: @@ -46,7 +74,17 @@ var ( // starts generating calls to runtime.convT2E (which calls runtime.newobject) // when assembling the argument slice which obviously will crash the kernel since // memory management is not yet available. +// +// The output of Printf is written to the currently active TTY. If no TTY is +// available, then the output is buffered into a ring-buffer and can be +// retrieved by a call to FlushRingBuffer. func Printf(format string, args ...interface{}) { + Fprintf(outputSink, format, args...) +} + +// Fprintf behaves exactly like Printf but it writes the formatted output to +// the specified io.Writer. +func Fprintf(w io.Writer, format string, args ...interface{}) { var ( nextCh byte nextArgIndex int @@ -62,8 +100,11 @@ func Printf(format string, args ...interface{}) { } if blockStart < blockEnd { + // passing format[blockStart:blockEnd] to doWrite triggers a + // memory allocation so we need to do this one byte at a time. for i := blockStart; i < blockEnd; i++ { - hal.ActiveTerminal.WriteByte(format[i]) + singleByte[0] = format[i] + doWrite(w, singleByte) } } @@ -75,7 +116,8 @@ func Printf(format string, args ...interface{}) { nextCh = format[blockEnd] switch { case nextCh == '%': - hal.ActiveTerminal.Write([]byte{'%'}) + singleByte[0] = '%' + doWrite(w, singleByte) break parseFmt case nextCh >= '0' && nextCh <= '9': padLen = (padLen * 10) + int(nextCh-'0') @@ -83,21 +125,21 @@ func Printf(format string, args ...interface{}) { case nextCh == 'd' || nextCh == 'x' || nextCh == 'o' || nextCh == 's' || nextCh == 't': // Run out of args to print if nextArgIndex >= len(args) { - hal.ActiveTerminal.Write(errMissingArg) + doWrite(w, errMissingArg) break parseFmt } switch nextCh { case 'o': - fmtInt(args[nextArgIndex], 8, padLen) + fmtInt(w, args[nextArgIndex], 8, padLen) case 'd': - fmtInt(args[nextArgIndex], 10, padLen) + fmtInt(w, args[nextArgIndex], 10, padLen) case 'x': - fmtInt(args[nextArgIndex], 16, padLen) + fmtInt(w, args[nextArgIndex], 16, padLen) case 's': - fmtString(args[nextArgIndex], padLen) + fmtString(w, args[nextArgIndex], padLen) case 't': - fmtBool(args[nextArgIndex]) + fmtBool(w, args[nextArgIndex]) } nextArgIndex++ @@ -105,80 +147,87 @@ func Printf(format string, args ...interface{}) { } // reached end of formatting string without finding a verb - hal.ActiveTerminal.Write(errNoVerb) + doWrite(w, errNoVerb) } blockStart, blockEnd = blockEnd+1, blockEnd+1 } if blockStart != blockEnd { + // passing format[blockStart:blockEnd] to doWrite triggers a + // memory allocation so we need to do this one byte at a time. for i := blockStart; i < blockEnd; i++ { - hal.ActiveTerminal.WriteByte(format[i]) + singleByte[0] = format[i] + doWrite(w, singleByte) } } // Check for unused args for ; nextArgIndex < len(args); nextArgIndex++ { - hal.ActiveTerminal.Write(errExtraArg) + doWrite(w, errExtraArg) } } -// fmtBool prints a formatted version of boolean value v using hal.ActiveTerminal -// for its output. -func fmtBool(v interface{}) { +// fmtBool prints a formatted version of boolean value v. +func fmtBool(w io.Writer, v interface{}) { switch bVal := v.(type) { case bool: switch bVal { case true: - hal.ActiveTerminal.Write(trueValue) + doWrite(w, trueValue) case false: - hal.ActiveTerminal.Write(falseValue) + doWrite(w, falseValue) } default: - hal.ActiveTerminal.Write(errWrongArgType) + doWrite(w, errWrongArgType) return } } -// fmtString prints a formatted version of string or []byte value v, applying the -// padding specified by padLen. This function uses hal.ActiveTerminal for its -// output. -func fmtString(v interface{}, padLen int) { +// fmtString prints a formatted version of string or []byte value v, applying +// the padding specified by padLen. +func fmtString(w io.Writer, v interface{}, padLen int) { switch castedVal := v.(type) { case string: - fmtRepeat(padding, padLen-len(castedVal)) + fmtRepeat(w, ' ', padLen-len(castedVal)) + // converting the string to a byte slice triggers a memory allocation + // so we need to do this one byte at a time. for i := 0; i < len(castedVal); i++ { - hal.ActiveTerminal.WriteByte(castedVal[i]) + singleByte[0] = castedVal[i] + doWrite(w, singleByte) } case []byte: - fmtRepeat(padding, padLen-len(castedVal)) - hal.ActiveTerminal.Write(castedVal) + fmtRepeat(w, ' ', padLen-len(castedVal)) + doWrite(w, castedVal) default: - hal.ActiveTerminal.Write(errWrongArgType) + doWrite(w, errWrongArgType) } } -// fmtRepeat writes count bytes with value ch to the hal.ActiveTerminal. -func fmtRepeat(ch byte, count int) { +// fmtRepeat writes count bytes with value ch. +func fmtRepeat(w io.Writer, ch byte, count int) { + singleByte[0] = ch for i := 0; i < count; i++ { - hal.ActiveTerminal.WriteByte(ch) + doWrite(w, singleByte) } } -// fmtInt prints out a formatted version of v in the requested base, applying the -// padding specified by padLen. This function uses hal.ActiveTerminal for its -// output, supports all built-in signed and unsigned integer types and supports -// base 8, 10 and 16 output. -func fmtInt(v interface{}, base, padLen int) { +// fmtInt prints out a formatted version of v in the requested base, applying +// the padding specified by padLen. This function supports all built-in signed +// and unsigned integer types and base 8, 10 and 16 output. +func fmtInt(w io.Writer, v interface{}, base, padLen int) { var ( sval int64 uval uint64 divider uint64 remainder uint64 - buf [20]byte padCh byte left, right, end int ) + if padLen >= maxBufSize { + padLen = maxBufSize - 1 + } + switch base { case 8: divider = 8 @@ -213,7 +262,7 @@ func fmtInt(v interface{}, base, padLen int) { case int: sval = int64(v.(int)) default: - hal.ActiveTerminal.Write(errWrongArgType) + doWrite(w, errWrongArgType) return } @@ -224,13 +273,13 @@ func fmtInt(v interface{}, base, padLen int) { uval = uint64(sval) } - for { + for right < maxBufSize { remainder = uval % divider if remainder < 10 { - buf[right] = byte(remainder) + '0' + numFmtBuf[right] = byte(remainder) + '0' } else { // map values from 10 to 15 -> a-f - buf[right] = byte(remainder-10) + 'a' + numFmtBuf[right] = byte(remainder-10) + 'a' } right++ @@ -243,27 +292,55 @@ func fmtInt(v interface{}, base, padLen int) { // Apply padding if required for ; right-left < padLen; right++ { - buf[right] = padCh + numFmtBuf[right] = padCh } // Apply negative sign to the rightmost blank character (if using enough padding); // otherwise append the sign as a new char if sval < 0 { - for end = right - 1; buf[end] == ' '; end-- { + for end = right - 1; numFmtBuf[end] == ' '; end-- { } if end == right-1 { right++ } - buf[end+1] = '-' + numFmtBuf[end+1] = '-' } // Reverse in place end = right for right = right - 1; left < right; left, right = left+1, right-1 { - buf[left], buf[right] = buf[right], buf[left] + numFmtBuf[left], numFmtBuf[right] = numFmtBuf[right], numFmtBuf[left] } - hal.ActiveTerminal.Write(buf[0:end]) + doWrite(w, numFmtBuf[0:end]) +} + +// doWrite is a proxy that uses the runtime.noescape hack to hide p from the +// compiler's escape analysis. Without this hack, the compiler cannot properly +// detect that p does not escape (due to the call to the yet unknown outputSink +// io.Writer) and plays it safe by flagging it as escaping. This causes all +// calls to Printf to call runtime.convT2E which triggers a memory allocation +// causing the kernel to crash if a call to Printf is made before the Go +// allocator is initialized. +func doWrite(w io.Writer, p []byte) { + doRealWrite(w, noEscape(unsafe.Pointer(&p))) +} + +func doRealWrite(w io.Writer, bufPtr unsafe.Pointer) { + p := *(*[]byte)(bufPtr) + if w != nil { + w.Write(p) + } else { + earlyPrintBuffer.Write(p) + } +} + +// noEscape hides a pointer from escape analysis. This function is copied over +// from runtime/stubs.go +//go:nosplit +func noEscape(p unsafe.Pointer) unsafe.Pointer { + x := uintptr(p) + return unsafe.Pointer(x ^ 0) } diff --git a/src/gopheros/kernel/kfmt/early/early_fmt_test.go b/src/gopheros/kernel/kfmt/fmt_test.go similarity index 79% rename from src/gopheros/kernel/kfmt/early/early_fmt_test.go rename to src/gopheros/kernel/kfmt/fmt_test.go index 32440a6..0a660fd 100644 --- a/src/gopheros/kernel/kfmt/early/early_fmt_test.go +++ b/src/gopheros/kernel/kfmt/fmt_test.go @@ -1,31 +1,20 @@ -package early +package kfmt import ( "bytes" - "gopheros/kernel/driver/tty" - "gopheros/kernel/driver/video/console" - "gopheros/kernel/hal" + "fmt" + "strings" "testing" - "unsafe" ) func TestPrintf(t *testing.T) { - origTerm := hal.ActiveTerminal defer func() { - hal.ActiveTerminal = origTerm + outputSink = nil }() // mute vet warnings about malformed printf formatting strings printfn := Printf - ega := &console.Ega{} - fb := make([]uint8, 160*25) - ega.Init(80, 25, uintptr(unsafe.Pointer(&fb[0]))) - - vt := &tty.Vt{} - vt.AttachTo(ega) - hal.ActiveTerminal = vt - specs := []struct { fn func() expOutput string @@ -124,6 +113,10 @@ func TestPrintf(t *testing.T) { func() { printfn("int arg longer than padding: '%5x'", int(-0xbadf00d)) }, "int arg longer than padding: '-badf00d'", }, + { + func() { printfn("padding longer than maxBufSize '%128x'", int(-0xbadf00d)) }, + fmt.Sprintf("padding longer than maxBufSize '-%sbadf00d'", strings.Repeat("0", maxBufSize-8)), + }, // multiple arguments { func() { printfn("%%%s%d%t", "foo", 123, true) }, @@ -156,25 +149,42 @@ func TestPrintf(t *testing.T) { }, } - for specIndex, spec := range specs { - for index := 0; index < len(fb); index++ { - fb[index] = 0 - } - vt.SetPosition(0, 0) + var buf bytes.Buffer + SetOutputSink(&buf) + for specIndex, spec := range specs { + buf.Reset() spec.fn() - var buf bytes.Buffer - for index := 0; ; index += 2 { - if fb[index] == 0 { - break - } - - buf.WriteByte(fb[index]) - } - if got := buf.String(); got != spec.expOutput { - t.Errorf("[spec %d] expected to get %q; got %q", specIndex, spec.expOutput, got) + t.Errorf("[spec %d] expected to get\n%q\ngot:\n%q", specIndex, spec.expOutput, got) } } } + +func TestPrintfToRingBuffer(t *testing.T) { + defer func() { + outputSink = nil + }() + + exp := "hello world" + Fprintf(&buf, exp) + + var buf bytes.Buffer + SetOutputSink(buf) + + if got := buf.String(); got != exp { + t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) + } +} + +func TestFprintf(t *testing.T) { + var buf bytes.Buffer + + exp := "hello world" + Fprintf(&buf, exp) + + if got := buf.String(); got != exp { + t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) + } +} From eca1f6c26efd2f1031b71149c71e6d275b5927b6 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Sat, 1 Jul 2017 19:00:11 +0100 Subject: [PATCH 10/11] Change early.Printf calls to kfmt.Printf --- src/gopheros/kernel/irq/interrupt_amd64.go | 24 ++++---- .../kernel/irq/interrupt_amd64_test.go | 53 ++++++----------- src/gopheros/kernel/kfmt/fmt_test.go | 4 +- src/gopheros/kernel/kfmt/panic.go | 9 ++- src/gopheros/kernel/kfmt/panic_test.go | 58 +++++-------------- .../mem/pmm/allocator/bitmap_allocator.go | 4 +- .../pmm/allocator/bitmap_allocator_test.go | 1 - .../kernel/mem/pmm/allocator/bootmem.go | 12 ++-- .../kernel/mem/pmm/allocator/bootmem_test.go | 12 ---- src/gopheros/kernel/mem/vmm/translate_test.go | 9 --- src/gopheros/kernel/mem/vmm/vmm.go | 26 ++++----- src/gopheros/kernel/mem/vmm/vmm_test.go | 43 +++----------- 12 files changed, 81 insertions(+), 174 deletions(-) diff --git a/src/gopheros/kernel/irq/interrupt_amd64.go b/src/gopheros/kernel/irq/interrupt_amd64.go index 6525d3e..ef92ab1 100644 --- a/src/gopheros/kernel/irq/interrupt_amd64.go +++ b/src/gopheros/kernel/irq/interrupt_amd64.go @@ -1,6 +1,6 @@ package irq -import "gopheros/kernel/kfmt/early" +import "gopheros/kernel/kfmt" // Regs contains a snapshot of the register values when an interrupt occurred. type Regs struct { @@ -23,14 +23,14 @@ type Regs struct { // Print outputs a dump of the register values to the active console. func (r *Regs) Print() { - early.Printf("RAX = %16x RBX = %16x\n", r.RAX, r.RBX) - early.Printf("RCX = %16x RDX = %16x\n", r.RCX, r.RDX) - early.Printf("RSI = %16x RDI = %16x\n", r.RSI, r.RDI) - early.Printf("RBP = %16x\n", r.RBP) - early.Printf("R8 = %16x R9 = %16x\n", r.R8, r.R9) - early.Printf("R10 = %16x R11 = %16x\n", r.R10, r.R11) - early.Printf("R12 = %16x R13 = %16x\n", r.R12, r.R13) - early.Printf("R14 = %16x R15 = %16x\n", r.R14, r.R15) + kfmt.Printf("RAX = %16x RBX = %16x\n", r.RAX, r.RBX) + kfmt.Printf("RCX = %16x RDX = %16x\n", r.RCX, r.RDX) + kfmt.Printf("RSI = %16x RDI = %16x\n", r.RSI, r.RDI) + kfmt.Printf("RBP = %16x\n", r.RBP) + kfmt.Printf("R8 = %16x R9 = %16x\n", r.R8, r.R9) + kfmt.Printf("R10 = %16x R11 = %16x\n", r.R10, r.R11) + kfmt.Printf("R12 = %16x R13 = %16x\n", r.R12, r.R13) + kfmt.Printf("R14 = %16x R15 = %16x\n", r.R14, r.R15) } // Frame describes an exception frame that is automatically pushed by the CPU @@ -45,7 +45,7 @@ type Frame struct { // Print outputs a dump of the exception frame to the active console. func (f *Frame) Print() { - early.Printf("RIP = %16x CS = %16x\n", f.RIP, f.CS) - early.Printf("RSP = %16x SS = %16x\n", f.RSP, f.SS) - early.Printf("RFL = %16x\n", f.RFlags) + kfmt.Printf("RIP = %16x CS = %16x\n", f.RIP, f.CS) + kfmt.Printf("RSP = %16x SS = %16x\n", f.RSP, f.SS) + kfmt.Printf("RFL = %16x\n", f.RFlags) } diff --git a/src/gopheros/kernel/irq/interrupt_amd64_test.go b/src/gopheros/kernel/irq/interrupt_amd64_test.go index 750482b..79bb5a2 100644 --- a/src/gopheros/kernel/irq/interrupt_amd64_test.go +++ b/src/gopheros/kernel/irq/interrupt_amd64_test.go @@ -2,14 +2,16 @@ package irq import ( "bytes" - "gopheros/kernel/driver/video/console" - "gopheros/kernel/hal" + "gopheros/kernel/kfmt" "testing" - "unsafe" ) func TestRegsPrint(t *testing.T) { - fb := mockTTY() + defer func() { + kfmt.SetOutputSink(nil) + }() + var buf bytes.Buffer + regs := Regs{ RAX: 1, RBX: 2, @@ -29,15 +31,20 @@ func TestRegsPrint(t *testing.T) { } regs.Print() - exp := "RAX = 0000000000000001 RBX = 0000000000000002\nRCX = 0000000000000003 RDX = 0000000000000004\nRSI = 0000000000000005 RDI = 0000000000000006\nRBP = 0000000000000007\nR8 = 0000000000000008 R9 = 0000000000000009\nR10 = 000000000000000a R11 = 000000000000000b\nR12 = 000000000000000c R13 = 000000000000000d\nR14 = 000000000000000e R15 = 000000000000000f" + exp := "RAX = 0000000000000001 RBX = 0000000000000002\nRCX = 0000000000000003 RDX = 0000000000000004\nRSI = 0000000000000005 RDI = 0000000000000006\nRBP = 0000000000000007\nR8 = 0000000000000008 R9 = 0000000000000009\nR10 = 000000000000000a R11 = 000000000000000b\nR12 = 000000000000000c R13 = 000000000000000d\nR14 = 000000000000000e R15 = 000000000000000f\n" - if got := readTTY(fb); got != exp { + kfmt.SetOutputSink(&buf) + if got := buf.String(); got != exp { t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) } } func TestFramePrint(t *testing.T) { - fb := mockTTY() + defer func() { + kfmt.SetOutputSink(nil) + }() + var buf bytes.Buffer + frame := Frame{ RIP: 1, CS: 2, @@ -47,37 +54,11 @@ func TestFramePrint(t *testing.T) { } frame.Print() - exp := "RIP = 0000000000000001 CS = 0000000000000002\nRSP = 0000000000000004 SS = 0000000000000005\nRFL = 0000000000000003" + exp := "RIP = 0000000000000001 CS = 0000000000000002\nRSP = 0000000000000004 SS = 0000000000000005\nRFL = 0000000000000003\n" - if got := readTTY(fb); got != exp { + kfmt.SetOutputSink(&buf) + if got := buf.String(); got != exp { t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) } } - -func readTTY(fb []byte) string { - var buf bytes.Buffer - for i := 0; i < len(fb); i += 2 { - ch := fb[i] - if ch == 0 { - if i+2 < len(fb) && fb[i+2] != 0 { - buf.WriteByte('\n') - } - continue - } - - buf.WriteByte(ch) - } - - return buf.String() -} - -func mockTTY() []byte { - // Mock a tty to handle early.Printf output - mockConsoleFb := make([]byte, 160*25) - mockConsole := &console.Ega{} - mockConsole.Init(80, 25, uintptr(unsafe.Pointer(&mockConsoleFb[0]))) - hal.ActiveTerminal.AttachTo(mockConsole) - - return mockConsoleFb -} diff --git a/src/gopheros/kernel/kfmt/fmt_test.go b/src/gopheros/kernel/kfmt/fmt_test.go index 0a660fd..90d9bf0 100644 --- a/src/gopheros/kernel/kfmt/fmt_test.go +++ b/src/gopheros/kernel/kfmt/fmt_test.go @@ -168,10 +168,10 @@ func TestPrintfToRingBuffer(t *testing.T) { }() exp := "hello world" - Fprintf(&buf, exp) + Printf(exp) var buf bytes.Buffer - SetOutputSink(buf) + SetOutputSink(&buf) if got := buf.String(); got != exp { t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) diff --git a/src/gopheros/kernel/kfmt/panic.go b/src/gopheros/kernel/kfmt/panic.go index 5ff9cb5..73b320b 100644 --- a/src/gopheros/kernel/kfmt/panic.go +++ b/src/gopheros/kernel/kfmt/panic.go @@ -3,7 +3,6 @@ package kfmt import ( "gopheros/kernel" "gopheros/kernel/cpu" - "gopheros/kernel/kfmt/early" ) var ( @@ -31,12 +30,12 @@ func Panic(e interface{}) { err = errRuntimePanic } - early.Printf("\n-----------------------------------\n") + Printf("\n-----------------------------------\n") if err != nil { - early.Printf("[%s] unrecoverable error: %s\n", err.Module, err.Message) + Printf("[%s] unrecoverable error: %s\n", err.Module, err.Message) } - early.Printf("*** kernel panic: system halted ***") - early.Printf("\n-----------------------------------\n") + Printf("*** kernel panic: system halted ***") + Printf("\n-----------------------------------\n") cpuHaltFn() } diff --git a/src/gopheros/kernel/kfmt/panic_test.go b/src/gopheros/kernel/kfmt/panic_test.go index e8ceacf..0888ddb 100644 --- a/src/gopheros/kernel/kfmt/panic_test.go +++ b/src/gopheros/kernel/kfmt/panic_test.go @@ -5,17 +5,18 @@ import ( "errors" "gopheros/kernel" "gopheros/kernel/cpu" - "gopheros/kernel/driver/video/console" - "gopheros/kernel/hal" "testing" - "unsafe" ) func TestPanic(t *testing.T) { defer func() { cpuHaltFn = cpu.Halt + SetOutputSink(nil) }() + var buf bytes.Buffer + SetOutputSink(&buf) + var cpuHaltCalled bool cpuHaltFn = func() { cpuHaltCalled = true @@ -23,14 +24,14 @@ func TestPanic(t *testing.T) { t.Run("with *kernel.Error", func(t *testing.T) { cpuHaltCalled = false - fb := mockTTY() + buf.Reset() err := &kernel.Error{Module: "test", Message: "panic test"} Panic(err) - exp := "\n-----------------------------------\n[test] unrecoverable error: panic test\n*** kernel panic: system halted ***\n-----------------------------------" + exp := "\n-----------------------------------\n[test] unrecoverable error: panic test\n*** kernel panic: system halted ***\n-----------------------------------\n" - if got := readTTY(fb); got != exp { + if got := buf.String(); got != exp { t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) } @@ -41,14 +42,14 @@ func TestPanic(t *testing.T) { t.Run("with error", func(t *testing.T) { cpuHaltCalled = false - fb := mockTTY() + buf.Reset() err := errors.New("go error") Panic(err) - exp := "\n-----------------------------------\n[rt] unrecoverable error: go error\n*** kernel panic: system halted ***\n-----------------------------------" + exp := "\n-----------------------------------\n[rt] unrecoverable error: go error\n*** kernel panic: system halted ***\n-----------------------------------\n" - if got := readTTY(fb); got != exp { + if got := buf.String(); got != exp { t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) } @@ -59,14 +60,14 @@ func TestPanic(t *testing.T) { t.Run("with string", func(t *testing.T) { cpuHaltCalled = false - fb := mockTTY() + buf.Reset() err := "string error" Panic(err) - exp := "\n-----------------------------------\n[rt] unrecoverable error: string error\n*** kernel panic: system halted ***\n-----------------------------------" + exp := "\n-----------------------------------\n[rt] unrecoverable error: string error\n*** kernel panic: system halted ***\n-----------------------------------\n" - if got := readTTY(fb); got != exp { + if got := buf.String(); got != exp { t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) } @@ -77,13 +78,13 @@ func TestPanic(t *testing.T) { t.Run("without error", func(t *testing.T) { cpuHaltCalled = false - fb := mockTTY() + buf.Reset() Panic(nil) - exp := "\n-----------------------------------\n*** kernel panic: system halted ***\n-----------------------------------" + exp := "\n-----------------------------------\n*** kernel panic: system halted ***\n-----------------------------------\n" - if got := readTTY(fb); got != exp { + if got := buf.String(); got != exp { t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) } @@ -92,30 +93,3 @@ func TestPanic(t *testing.T) { } }) } - -func readTTY(fb []byte) string { - var buf bytes.Buffer - for i := 0; i < len(fb); i += 2 { - ch := fb[i] - if ch == 0 { - if i+2 < len(fb) && fb[i+2] != 0 { - buf.WriteByte('\n') - } - continue - } - - buf.WriteByte(ch) - } - - return buf.String() -} - -func mockTTY() []byte { - // Mock a tty to handle early.Printf output - mockConsoleFb := make([]byte, 160*25) - mockConsole := &console.Ega{} - mockConsole.Init(80, 25, uintptr(unsafe.Pointer(&mockConsoleFb[0]))) - hal.ActiveTerminal.AttachTo(mockConsole) - - return mockConsoleFb -} diff --git a/src/gopheros/kernel/mem/pmm/allocator/bitmap_allocator.go b/src/gopheros/kernel/mem/pmm/allocator/bitmap_allocator.go index 1030b47..4afc144 100644 --- a/src/gopheros/kernel/mem/pmm/allocator/bitmap_allocator.go +++ b/src/gopheros/kernel/mem/pmm/allocator/bitmap_allocator.go @@ -3,7 +3,7 @@ package allocator import ( "gopheros/kernel" "gopheros/kernel/hal/multiboot" - "gopheros/kernel/kfmt/early" + "gopheros/kernel/kfmt" "gopheros/kernel/mem" "gopheros/kernel/mem/pmm" "gopheros/kernel/mem/vmm" @@ -234,7 +234,7 @@ func (alloc *BitmapAllocator) reserveEarlyAllocatorFrames() { } func (alloc *BitmapAllocator) printStats() { - early.Printf( + kfmt.Printf( "[bitmap_alloc] page stats: free: %d/%d (%d reserved)\n", alloc.totalPages-alloc.reservedPages, alloc.totalPages, diff --git a/src/gopheros/kernel/mem/pmm/allocator/bitmap_allocator_test.go b/src/gopheros/kernel/mem/pmm/allocator/bitmap_allocator_test.go index a36c80d..b3c0896 100644 --- a/src/gopheros/kernel/mem/pmm/allocator/bitmap_allocator_test.go +++ b/src/gopheros/kernel/mem/pmm/allocator/bitmap_allocator_test.go @@ -406,7 +406,6 @@ func TestAllocatorPackageInit(t *testing.T) { return uintptr(unsafe.Pointer(&physMem[0])), nil } - mockTTY() if err := Init(0x100000, 0x1fa7c8); err != nil { t.Fatal(err) } diff --git a/src/gopheros/kernel/mem/pmm/allocator/bootmem.go b/src/gopheros/kernel/mem/pmm/allocator/bootmem.go index 4970348..3d7ab1f 100644 --- a/src/gopheros/kernel/mem/pmm/allocator/bootmem.go +++ b/src/gopheros/kernel/mem/pmm/allocator/bootmem.go @@ -3,7 +3,7 @@ package allocator import ( "gopheros/kernel" "gopheros/kernel/hal/multiboot" - "gopheros/kernel/kfmt/early" + "gopheros/kernel/kfmt" "gopheros/kernel/mem" "gopheros/kernel/mem/pmm" ) @@ -117,19 +117,19 @@ func (alloc *bootMemAllocator) AllocFrame() (pmm.Frame, *kernel.Error) { // printMemoryMap scans the memory region information provided by the // bootloader and prints out the system's memory map. func (alloc *bootMemAllocator) printMemoryMap() { - early.Printf("[boot_mem_alloc] system memory map:\n") + kfmt.Printf("[boot_mem_alloc] system memory map:\n") var totalFree mem.Size multiboot.VisitMemRegions(func(region *multiboot.MemoryMapEntry) bool { - early.Printf("\t[0x%10x - 0x%10x], size: %10d, type: %s\n", region.PhysAddress, region.PhysAddress+region.Length, region.Length, region.Type.String()) + kfmt.Printf("\t[0x%10x - 0x%10x], size: %10d, type: %s\n", region.PhysAddress, region.PhysAddress+region.Length, region.Length, region.Type.String()) if region.Type == multiboot.MemAvailable { totalFree += mem.Size(region.Length) } return true }) - early.Printf("[boot_mem_alloc] available memory: %dKb\n", uint64(totalFree/mem.Kb)) - early.Printf("[boot_mem_alloc] kernel loaded at 0x%x - 0x%x\n", alloc.kernelStartAddr, alloc.kernelEndAddr) - early.Printf("[boot_mem_alloc] size: %d bytes, reserved pages: %d\n", + kfmt.Printf("[boot_mem_alloc] available memory: %dKb\n", uint64(totalFree/mem.Kb)) + kfmt.Printf("[boot_mem_alloc] kernel loaded at 0x%x - 0x%x\n", alloc.kernelStartAddr, alloc.kernelEndAddr) + kfmt.Printf("[boot_mem_alloc] size: %d bytes, reserved pages: %d\n", uint64(alloc.kernelEndAddr-alloc.kernelStartAddr), uint64(alloc.kernelEndFrame-alloc.kernelStartFrame+1), ) diff --git a/src/gopheros/kernel/mem/pmm/allocator/bootmem_test.go b/src/gopheros/kernel/mem/pmm/allocator/bootmem_test.go index 0494d5a..8a0d937 100644 --- a/src/gopheros/kernel/mem/pmm/allocator/bootmem_test.go +++ b/src/gopheros/kernel/mem/pmm/allocator/bootmem_test.go @@ -1,8 +1,6 @@ package allocator import ( - "gopheros/kernel/driver/video/console" - "gopheros/kernel/hal" "gopheros/kernel/hal/multiboot" "testing" "unsafe" @@ -118,13 +116,3 @@ var ( 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, } ) - -func mockTTY() []byte { - // Mock a tty to handle early.Printf output - mockConsoleFb := make([]byte, 160*25) - mockConsole := &console.Ega{} - mockConsole.Init(80, 25, uintptr(unsafe.Pointer(&mockConsoleFb[0]))) - hal.ActiveTerminal.AttachTo(mockConsole) - - return mockConsoleFb -} diff --git a/src/gopheros/kernel/mem/vmm/translate_test.go b/src/gopheros/kernel/mem/vmm/translate_test.go index 6d7cca5..35dd1db 100644 --- a/src/gopheros/kernel/mem/vmm/translate_test.go +++ b/src/gopheros/kernel/mem/vmm/translate_test.go @@ -61,12 +61,3 @@ func TestTranslateAmd64(t *testing.T) { } } } - -/* - phys, err := vmm.Translate(uintptr(100 * mem.Mb)) - if err != nil { - early.Printf("err: %s\n", err.Error()) - } else { - early.Printf("phys: 0x%x\n", phys) - } -*/ diff --git a/src/gopheros/kernel/mem/vmm/vmm.go b/src/gopheros/kernel/mem/vmm/vmm.go index 3814347..b63b1f9 100644 --- a/src/gopheros/kernel/mem/vmm/vmm.go +++ b/src/gopheros/kernel/mem/vmm/vmm.go @@ -4,7 +4,7 @@ import ( "gopheros/kernel" "gopheros/kernel/cpu" "gopheros/kernel/irq" - "gopheros/kernel/kfmt/early" + "gopheros/kernel/kfmt" "gopheros/kernel/mem" "gopheros/kernel/mem/pmm" ) @@ -83,27 +83,27 @@ func pageFaultHandler(errorCode uint64, frame *irq.Frame, regs *irq.Regs) { } func nonRecoverablePageFault(faultAddress uintptr, errorCode uint64, frame *irq.Frame, regs *irq.Regs, err *kernel.Error) { - early.Printf("\nPage fault while accessing address: 0x%16x\nReason: ", faultAddress) + kfmt.Printf("\nPage fault while accessing address: 0x%16x\nReason: ", faultAddress) switch { case errorCode == 0: - early.Printf("read from non-present page") + kfmt.Printf("read from non-present page") case errorCode == 1: - early.Printf("page protection violation (read)") + kfmt.Printf("page protection violation (read)") case errorCode == 2: - early.Printf("write to non-present page") + kfmt.Printf("write to non-present page") case errorCode == 3: - early.Printf("page protection violation (write)") + kfmt.Printf("page protection violation (write)") case errorCode == 4: - early.Printf("page-fault in user-mode") + kfmt.Printf("page-fault in user-mode") case errorCode == 8: - early.Printf("page table has reserved bit set") + kfmt.Printf("page table has reserved bit set") case errorCode == 16: - early.Printf("instruction fetch") + kfmt.Printf("instruction fetch") default: - early.Printf("unknown") + kfmt.Printf("unknown") } - early.Printf("\n\nRegisters:\n") + kfmt.Printf("\n\nRegisters:\n") regs.Print() frame.Print() @@ -112,8 +112,8 @@ func nonRecoverablePageFault(faultAddress uintptr, errorCode uint64, frame *irq. } func generalProtectionFaultHandler(_ uint64, frame *irq.Frame, regs *irq.Regs) { - early.Printf("\nGeneral protection fault while accessing address: 0x%x\n", readCR2Fn()) - early.Printf("Registers:\n") + kfmt.Printf("\nGeneral protection fault while accessing address: 0x%x\n", readCR2Fn()) + kfmt.Printf("Registers:\n") regs.Print() frame.Print() diff --git a/src/gopheros/kernel/mem/vmm/vmm_test.go b/src/gopheros/kernel/mem/vmm/vmm_test.go index bec50d4..c41bd79 100644 --- a/src/gopheros/kernel/mem/vmm/vmm_test.go +++ b/src/gopheros/kernel/mem/vmm/vmm_test.go @@ -5,9 +5,8 @@ import ( "fmt" "gopheros/kernel" "gopheros/kernel/cpu" - "gopheros/kernel/driver/video/console" - "gopheros/kernel/hal" "gopheros/kernel/irq" + "gopheros/kernel/kfmt" "gopheros/kernel/mem" "gopheros/kernel/mem/pmm" "strings" @@ -54,8 +53,6 @@ func TestRecoverablePageFault(t *testing.T) { {FlagPresent | FlagCopyOnWrite, nil, nil, false}, } - mockTTY() - ptePtrFn = func(entry uintptr) unsafe.Pointer { return unsafe.Pointer(&pageEntry) } readCR2Fn = func() uint64 { return uint64(uintptr(unsafe.Pointer(&origPage[0]))) } unmapFn = func(_ Page) *kernel.Error { return nil } @@ -102,6 +99,10 @@ func TestRecoverablePageFault(t *testing.T) { } func TestNonRecoverablePageFault(t *testing.T) { + defer func() { + kfmt.SetOutputSink(nil) + }() + specs := []struct { errCode uint64 expReason string @@ -143,19 +144,21 @@ func TestNonRecoverablePageFault(t *testing.T) { var ( regs irq.Regs frame irq.Frame + buf bytes.Buffer ) + kfmt.SetOutputSink(&buf) for specIndex, spec := range specs { t.Run(fmt.Sprint(specIndex), func(t *testing.T) { + buf.Reset() defer func() { if err := recover(); err != errUnrecoverableFault { t.Errorf("expected a panic with errUnrecoverableFault; got %v", err) } }() - fb := mockTTY() nonRecoverablePageFault(0xbadf00d000, spec.errCode, &frame, ®s, errUnrecoverableFault) - if got := readTTY(fb); !strings.Contains(got, spec.expReason) { + if got := buf.String(); !strings.Contains(got, spec.expReason) { t.Errorf("expected reason %q; got output:\n%q", spec.expReason, got) } }) @@ -182,7 +185,6 @@ func TestGPtHandler(t *testing.T) { } }() - mockTTY() generalProtectionFaultHandler(0, &frame, ®s) } @@ -252,30 +254,3 @@ func TestInit(t *testing.T) { } }) } - -func readTTY(fb []byte) string { - var buf bytes.Buffer - for i := 0; i < len(fb); i += 2 { - ch := fb[i] - if ch == 0 { - if i+2 < len(fb) && fb[i+2] != 0 { - buf.WriteByte('\n') - } - continue - } - - buf.WriteByte(ch) - } - - return buf.String() -} - -func mockTTY() []byte { - // Mock a tty to handle early.Printf output - mockConsoleFb := make([]byte, 160*25) - mockConsole := &console.Ega{} - mockConsole.Init(80, 25, uintptr(unsafe.Pointer(&mockConsoleFb[0]))) - hal.ActiveTerminal.AttachTo(mockConsole) - - return mockConsoleFb -} From 562fae2028cbab1b551d00e2efa4abf77f212dc3 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Thu, 6 Jul 2017 09:24:41 +0100 Subject: [PATCH 11/11] Detect hw and wire active console and TTY --- src/gopheros/device/driver.go | 4 + src/gopheros/device/tty/device.go | 8 ++ src/gopheros/device/tty/probe.go | 11 +++ src/gopheros/device/tty/vt.go | 5 ++ src/gopheros/device/tty/vt_test.go | 23 ++++++ src/gopheros/device/video/console/probe.go | 16 ++++ src/gopheros/device/video/console/vga_text.go | 14 ++++ .../device/video/console/vga_text_test.go | 38 +++++++++ src/gopheros/kernel/hal/hal.go | 78 +++++++++++++++---- src/gopheros/kernel/kmain/kmain.go | 11 +-- 10 files changed, 186 insertions(+), 22 deletions(-) create mode 100644 src/gopheros/device/tty/probe.go create mode 100644 src/gopheros/device/video/console/probe.go diff --git a/src/gopheros/device/driver.go b/src/gopheros/device/driver.go index 11cab21..aec2c38 100644 --- a/src/gopheros/device/driver.go +++ b/src/gopheros/device/driver.go @@ -13,3 +13,7 @@ type Driver interface { // DriverInit initializes the device driver. DriverInit() *kernel.Error } + +// ProbeFn is a function that scans for the presence of a particular +// piece of hardware and returns a driver for it. +type ProbeFn func() Driver diff --git a/src/gopheros/device/tty/device.go b/src/gopheros/device/tty/device.go index a0f4e4d..d2f87a3 100644 --- a/src/gopheros/device/tty/device.go +++ b/src/gopheros/device/tty/device.go @@ -5,6 +5,14 @@ import ( "io" ) +const ( + // DefaultScrollback defines the terminal scrollback in lines. + DefaultScrollback = 80 + + // DefaultTabWidth defines the number of spaces that tabs expand to. + DefaultTabWidth = 4 +) + // State defines the supported terminal state values. type State uint8 diff --git a/src/gopheros/device/tty/probe.go b/src/gopheros/device/tty/probe.go new file mode 100644 index 0000000..6911b0b --- /dev/null +++ b/src/gopheros/device/tty/probe.go @@ -0,0 +1,11 @@ +package tty + +import "gopheros/device" + +// HWProbes returns a slice of device.ProbeFn that can be used by the hal +// package to probe for TTY device hardware. +func HWProbes() []device.ProbeFn { + return []device.ProbeFn{ + probeForVT, + } +} diff --git a/src/gopheros/device/tty/vt.go b/src/gopheros/device/tty/vt.go index ea63238..fe4dd41 100644 --- a/src/gopheros/device/tty/vt.go +++ b/src/gopheros/device/tty/vt.go @@ -1,6 +1,7 @@ package tty import ( + "gopheros/device" "gopheros/device/video/console" "gopheros/kernel" "io" @@ -258,3 +259,7 @@ func (t *VT) DriverVersion() (uint16, uint16, uint16) { // DriverInit initializes this driver. func (t *VT) DriverInit() *kernel.Error { return nil } + +func probeForVT() device.Driver { + return NewVT(DefaultTabWidth, DefaultScrollback) +} diff --git a/src/gopheros/device/tty/vt_test.go b/src/gopheros/device/tty/vt_test.go index 678acba..9f95280 100644 --- a/src/gopheros/device/tty/vt_test.go +++ b/src/gopheros/device/tty/vt_test.go @@ -5,6 +5,7 @@ import ( "gopheros/device/video/console" "image/color" "io" + "reflect" "testing" ) @@ -337,6 +338,28 @@ func TestVTDriverInterface(t *testing.T) { } } +func TestVTProbe(t *testing.T) { + var ( + expProbePtr = reflect.ValueOf(probeForVT).Pointer() + foundProbe bool + ) + + for _, probeFn := range HWProbes() { + if reflect.ValueOf(probeFn).Pointer() == expProbePtr { + foundProbe = true + break + } + } + + if !foundProbe { + t.Fatal("expected probeForVT to be part of the probes returned by HWProbes") + } + + if drv := probeForVT(); drv == nil { + t.Fatal("expected probeForVT to return a driver") + } +} + type mockConsole struct { width, height uint16 fg, bg uint8 diff --git a/src/gopheros/device/video/console/probe.go b/src/gopheros/device/video/console/probe.go new file mode 100644 index 0000000..a2a47d3 --- /dev/null +++ b/src/gopheros/device/video/console/probe.go @@ -0,0 +1,16 @@ +package console + +import "gopheros/device" +import "gopheros/kernel/hal/multiboot" + +var ( + getFramebufferInfoFn = multiboot.GetFramebufferInfo +) + +// HWProbes returns a slice of device.ProbeFn that can be used by the hal +// package to probe for console device hardware. +func HWProbes() []device.ProbeFn { + return []device.ProbeFn{ + probeForVgaTextConsole, + } +} diff --git a/src/gopheros/device/video/console/vga_text.go b/src/gopheros/device/video/console/vga_text.go index 76aecbf..c058635 100644 --- a/src/gopheros/device/video/console/vga_text.go +++ b/src/gopheros/device/video/console/vga_text.go @@ -1,8 +1,10 @@ package console import ( + "gopheros/device" "gopheros/kernel" "gopheros/kernel/cpu" + "gopheros/kernel/hal/multiboot" "image/color" "reflect" "unsafe" @@ -196,3 +198,15 @@ func (cons *VgaTextConsole) DriverVersion() (uint16, uint16, uint16) { // DriverInit initializes this driver. func (cons *VgaTextConsole) DriverInit() *kernel.Error { return nil } + +// probeForVgaTextConsole checks for the presence of a vga text console. +func probeForVgaTextConsole() device.Driver { + var drv device.Driver + + fbInfo := getFramebufferInfoFn() + if fbInfo.Type == multiboot.FramebufferTypeEGA { + drv = NewVgaTextConsole(uint16(fbInfo.Width), uint16(fbInfo.Height), uintptr(fbInfo.PhysAddr)) + } + + return drv +} diff --git a/src/gopheros/device/video/console/vga_text_test.go b/src/gopheros/device/video/console/vga_text_test.go index 9213299..d852fb6 100644 --- a/src/gopheros/device/video/console/vga_text_test.go +++ b/src/gopheros/device/video/console/vga_text_test.go @@ -3,7 +3,9 @@ package console import ( "gopheros/device" "gopheros/kernel/cpu" + "gopheros/kernel/hal/multiboot" "image/color" + "reflect" "testing" "unsafe" ) @@ -318,3 +320,39 @@ func TestVgaTextDriverInterface(t *testing.T) { t.Fatal("DriverVersion() returned an invalid version number") } } + +func TestVgaTextProbe(t *testing.T) { + defer func() { + getFramebufferInfoFn = multiboot.GetFramebufferInfo + }() + + var ( + expProbePtr = reflect.ValueOf(probeForVgaTextConsole).Pointer() + foundProbe bool + ) + + for _, probeFn := range HWProbes() { + if reflect.ValueOf(probeFn).Pointer() == expProbePtr { + foundProbe = true + break + } + } + + if !foundProbe { + t.Fatal("expected probeForVgaTextConsole to be part of the probes returned by HWProbes") + } + + getFramebufferInfoFn = func() *multiboot.FramebufferInfo { + return &multiboot.FramebufferInfo{ + Width: 80, + Height: 25, + Pitch: 160, + PhysAddr: 0xb80000, + Type: multiboot.FramebufferTypeEGA, + } + } + + if drv := probeForVgaTextConsole(); drv == nil { + t.Fatal("expected probeForVgaTextConsole to return a driver") + } +} diff --git a/src/gopheros/kernel/hal/hal.go b/src/gopheros/kernel/hal/hal.go index 36ea7e0..036be4e 100644 --- a/src/gopheros/kernel/hal/hal.go +++ b/src/gopheros/kernel/hal/hal.go @@ -1,23 +1,67 @@ package hal import ( - "gopheros/kernel/driver/tty" - "gopheros/kernel/driver/video/console" - "gopheros/kernel/hal/multiboot" + "gopheros/device" + "gopheros/device/tty" + "gopheros/device/video/console" + "gopheros/kernel/kfmt" ) -var ( - egaConsole = &console.Ega{} - - // ActiveTerminal points to the currently active terminal. - ActiveTerminal = &tty.Vt{} -) - -// InitTerminal provides a basic terminal to allow the kernel to emit some output -// till everything is properly setup -func InitTerminal() { - fbInfo := multiboot.GetFramebufferInfo() - - egaConsole.Init(uint16(fbInfo.Width), uint16(fbInfo.Height), uintptr(fbInfo.PhysAddr)) - ActiveTerminal.AttachTo(egaConsole) +// managedDevices contains the devices discovered by the HAL. +type managedDevices struct { + activeConsole console.Device + activeTTY tty.Device +} + +var devices managedDevices + +// ActiveTTY returns the currently active TTY +func ActiveTTY() tty.Device { + return devices.activeTTY +} + +// DetectHardware probes for hardware devices and initializes the appropriate +// drivers. +func DetectHardware() { + consoles := probe(console.HWProbes()) + if len(consoles) != 0 { + devices.activeConsole = consoles[0].(console.Device) + } + + ttys := probe(tty.HWProbes()) + if len(ttys) != 0 { + devices.activeTTY = ttys[0].(tty.Device) + devices.activeTTY.AttachTo(devices.activeConsole) + kfmt.SetOutputSink(devices.activeTTY) + + // Sync terminal contents with console + devices.activeTTY.SetState(tty.StateActive) + } +} + +// probe executes the supplied hw probe functions and attempts to initialize +// each detected device. The function returns a list of device drivers that +// were successfully initialized. +func probe(hwProbeFns []device.ProbeFn) []device.Driver { + var drivers []device.Driver + + for _, probeFn := range hwProbeFns { + drv := probeFn() + if drv == nil { + continue + } + + major, minor, patch := drv.DriverVersion() + + kfmt.Printf("[hal] %s(%d.%d.%d): ", drv.DriverName(), major, minor, patch) + if err := drv.DriverInit(); err != nil { + kfmt.Printf("init failed: %s\n", err.Message) + continue + } + + drivers = append(drivers, drv) + kfmt.Printf("initialized\n") + } + + return drivers } diff --git a/src/gopheros/kernel/kmain/kmain.go b/src/gopheros/kernel/kmain/kmain.go index c39861e..7274f59 100644 --- a/src/gopheros/kernel/kmain/kmain.go +++ b/src/gopheros/kernel/kmain/kmain.go @@ -5,6 +5,7 @@ import ( "gopheros/kernel/goruntime" "gopheros/kernel/hal" "gopheros/kernel/hal/multiboot" + "gopheros/kernel/kfmt" "gopheros/kernel/mem/pmm/allocator" "gopheros/kernel/mem/vmm" ) @@ -27,9 +28,6 @@ var ( func Kmain(multibootInfoPtr, kernelStart, kernelEnd uintptr) { multiboot.SetInfoPtr(multibootInfoPtr) - hal.InitTerminal() - hal.ActiveTerminal.Clear() - var err *kernel.Error if err = allocator.Init(kernelStart, kernelEnd); err != nil { panic(err) @@ -39,7 +37,10 @@ func Kmain(multibootInfoPtr, kernelStart, kernelEnd uintptr) { panic(err) } - // Use kernel.Panic instead of panic to prevent the compiler from + // Detect and initialize hardware + hal.DetectHardware() + + // Use kfmt.Panic instead of panic to prevent the compiler from // treating kernel.Panic as dead-code and eliminating it. - kernel.Panic(errKmainReturned) + kfmt.Panic(errKmainReturned) }