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/device/driver.go b/src/gopheros/device/driver.go new file mode 100644 index 0000000..aec2c38 --- /dev/null +++ b/src/gopheros/device/driver.go @@ -0,0 +1,19 @@ +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 +} + +// 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 new file mode 100644 index 0000000..d2f87a3 --- /dev/null +++ b/src/gopheros/device/tty/device.go @@ -0,0 +1,52 @@ +package tty + +import ( + "gopheros/device/video/console" + "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 + +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/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 new file mode 100644 index 0000000..fe4dd41 --- /dev/null +++ b/src/gopheros/device/tty/vt.go @@ -0,0 +1,265 @@ +package tty + +import ( + "gopheros/device" + "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 } + +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 new file mode 100644 index 0000000..9f95280 --- /dev/null +++ b/src/gopheros/device/tty/vt_test.go @@ -0,0 +1,430 @@ +package tty + +import ( + "gopheros/device" + "gopheros/device/video/console" + "image/color" + "io" + "reflect" + "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") + } +} + +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 + 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/device/video/console/device.go b/src/gopheros/device/video/console/device.go new file mode 100644 index 0000000..1e7b7e3 --- /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 ( + ScrollDirUp ScrollDir = iota + ScrollDirDown +) + +// 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/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 new file mode 100644 index 0000000..c058635 --- /dev/null +++ b/src/gopheros/device/video/console/vga_text.go @@ -0,0 +1,212 @@ +package console + +import ( + "gopheros/device" + "gopheros/kernel" + "gopheros/kernel/cpu" + "gopheros/kernel/hal/multiboot" + "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 } + +// 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 new file mode 100644 index 0000000..d852fb6 --- /dev/null +++ b/src/gopheros/device/video/console/vga_text_test.go @@ -0,0 +1,358 @@ +package console + +import ( + "gopheros/device" + "gopheros/kernel/cpu" + "gopheros/kernel/hal/multiboot" + "image/color" + "reflect" + "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") + } +} + +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/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() -} 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) - } - } -} 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) -} 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) - } -} 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) 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/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/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..90d9bf0 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" + Printf(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) + } +} diff --git a/src/gopheros/kernel/panic.go b/src/gopheros/kernel/kfmt/panic.go similarity index 67% rename from src/gopheros/kernel/panic.go rename to src/gopheros/kernel/kfmt/panic.go index a326266..73b320b 100644 --- a/src/gopheros/kernel/panic.go +++ b/src/gopheros/kernel/kfmt/panic.go @@ -1,15 +1,15 @@ -package kernel +package kfmt import ( + "gopheros/kernel" "gopheros/kernel/cpu" - "gopheros/kernel/kfmt/early" ) 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 +17,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) @@ -30,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/panic_test.go b/src/gopheros/kernel/kfmt/panic_test.go similarity index 61% rename from src/gopheros/kernel/panic_test.go rename to src/gopheros/kernel/kfmt/panic_test.go index 68bfad8..0888ddb 100644 --- a/src/gopheros/kernel/panic_test.go +++ b/src/gopheros/kernel/kfmt/panic_test.go @@ -1,20 +1,22 @@ -package kernel +package kfmt import ( "bytes" "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 @@ -22,14 +24,14 @@ 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"} + 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) } @@ -40,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) } @@ -58,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) } @@ -76,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) } @@ -91,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/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() +} 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) } 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 -}