From 60475e23e6dd1c3159ef57bcb22570641aacd215 Mon Sep 17 00:00:00 2001 From: Frank Ray <52075808+FrankRay78@users.noreply.github.com> Date: Sat, 13 Apr 2024 18:03:26 +0100 Subject: [PATCH] Console supports scrolling --- build/build.cmd | 5 +- src/PatienceOS.Kernel.Tests/ConsoleTests.cs | 143 +++++++++++++++++- .../FrameBufferTests.cs | 24 +++ src/PatienceOS.Kernel/Color.cs | 28 ++++ src/PatienceOS.Kernel/Console.cs | 129 +++++++++++----- src/PatienceOS.Kernel/FrameBuffer.cs | 12 +- src/PatienceOS.Kernel/kernel.cs | 2 +- 7 files changed, 295 insertions(+), 48 deletions(-) create mode 100644 src/PatienceOS.Kernel/Color.cs diff --git a/build/build.cmd b/build/build.cmd index 86f92bc..7fa1db2 100644 --- a/build/build.cmd +++ b/build/build.cmd @@ -13,7 +13,7 @@ rmdir /S /Q ..\bin >nul 2>&1 mkdir ..\bin cd ..\bin -csc /debug:embedded /noconfig /nostdlib /runtimemetadataversion:v4.0.30319 ../src/PatienceOS.Kernel/kernel.cs ../src/PatienceOS.Kernel/Console.cs ../src/PatienceOS.Kernel/FrameBuffer.cs ../src/PatienceOS.Kernel/zerosharp.cs /out:kernel.ilexe /langversion:latest /unsafe || goto Error +csc /debug:embedded /noconfig /nostdlib /runtimemetadataversion:v4.0.30319 ../src/PatienceOS.Kernel/Color.cs ../src/PatienceOS.Kernel/Console.cs ../src/PatienceOS.Kernel/FrameBuffer.cs ../src/PatienceOS.Kernel/kernel.cs ../src/PatienceOS.Kernel/zerosharp.cs /out:kernel.ilexe /langversion:latest /unsafe || goto Error ilc --targetos windows --targetarch x86 --instruction-set base --verbose kernel.ilexe -g -o kernel.obj --systemmodule kernel --map kernel.map -O || goto Error nasm -f win32 -o loader.obj ../src/loader.asm || goto Error @@ -23,7 +23,8 @@ nasm -f win32 -o loader.obj ../src/loader.asm || goto Error ld -m i386pe -T ../src/linker.ld -o kernel.bin loader.obj kernel.obj || goto Error objcopy -O elf32-i386 kernel.bin kernel.elf || goto Error -qemu-system-i386 -cpu pentium3 -kernel kernel.elf -d int -no-reboot -no-shutdown || goto Error +qemu-system-i386 -cpu pentium3 -kernel kernel.elf -no-reboot -no-shutdown || goto Error +:: qemu-system-i386 -cpu pentium3 -kernel kernel.elf -d int -no-reboot -no-shutdown || goto Error cd ..\build exit /B diff --git a/src/PatienceOS.Kernel.Tests/ConsoleTests.cs b/src/PatienceOS.Kernel.Tests/ConsoleTests.cs index 5489012..81d9c33 100644 --- a/src/PatienceOS.Kernel.Tests/ConsoleTests.cs +++ b/src/PatienceOS.Kernel.Tests/ConsoleTests.cs @@ -73,7 +73,7 @@ public void Should_Clear_A_EOL_B() unsafe public class ThreeXThree { [Fact] - public void Should_Write_ABC_EOL_DEF_EOL_GHI() + public void Should_Write_ABC_DEF_GHI() { // Given byte* buffer = stackalloc byte[3 * 3 * 2]; @@ -94,12 +94,143 @@ public void Should_Write_ABC_EOL_DEF_EOL_GHI() Assert.Equal((byte)'H', buffer[14]); Assert.Equal((byte)'I', buffer[16]); } + + [Fact] + public void Should_Clear_ABC_DEF_GHI() + { + // Given + byte* buffer = stackalloc byte[3 * 3 * 2]; + var frameBuffer = new FrameBuffer(buffer); + var console = new Console(3, 3, frameBuffer); + + // When + console.Print("ABCDEFGHI"); + console.Clear(); + + // Then + Assert.Equal((byte)' ', buffer[0]); + Assert.Equal((byte)' ', buffer[2]); + Assert.Equal((byte)' ', buffer[4]); + Assert.Equal((byte)' ', buffer[6]); + Assert.Equal((byte)' ', buffer[8]); + Assert.Equal((byte)' ', buffer[10]); + Assert.Equal((byte)' ', buffer[12]); + Assert.Equal((byte)' ', buffer[14]); + Assert.Equal((byte)' ', buffer[16]); + } + + [Fact] + public void Should_Scroll_One_Line() + { + // Given + byte* buffer = stackalloc byte[3 * 3 * 2]; + var frameBuffer = new FrameBuffer(buffer); + var console = new Console(3, 3, frameBuffer); + + // When + console.Print("AAA"); + console.Print("BBB"); + console.Print("CCC"); + console.Print("DDD"); + + // Then + Assert.Equal((byte)'B', buffer[0]); + Assert.Equal((byte)'B', buffer[2]); + Assert.Equal((byte)'B', buffer[4]); + Assert.Equal((byte)'C', buffer[6]); + Assert.Equal((byte)'C', buffer[8]); + Assert.Equal((byte)'C', buffer[10]); + Assert.Equal((byte)'D', buffer[12]); + Assert.Equal((byte)'D', buffer[14]); + Assert.Equal((byte)'D', buffer[16]); + } + + [Fact] + public void Should_Scroll_Two_Lines() + { + // Given + byte* buffer = stackalloc byte[3 * 3 * 2]; + var frameBuffer = new FrameBuffer(buffer); + var console = new Console(3, 3, frameBuffer); + + // When + console.Print("AAA"); + console.Print("BBB"); + console.Print("CCC"); + console.Print("DDD"); + console.Print("EEE"); + + // Then + Assert.Equal((byte)'C', buffer[0]); + Assert.Equal((byte)'C', buffer[2]); + Assert.Equal((byte)'C', buffer[4]); + Assert.Equal((byte)'D', buffer[6]); + Assert.Equal((byte)'D', buffer[8]); + Assert.Equal((byte)'D', buffer[10]); + Assert.Equal((byte)'E', buffer[12]); + Assert.Equal((byte)'E', buffer[14]); + Assert.Equal((byte)'E', buffer[16]); + } + + [Fact] + public void Should_Scroll_Three_Lines() + { + // Given + byte* buffer = stackalloc byte[3 * 3 * 2]; + var frameBuffer = new FrameBuffer(buffer); + var console = new Console(3, 3, frameBuffer); + + // When + console.Print("AAA"); + console.Print("BBB"); + console.Print("CCC"); + console.Print("DDD"); + console.Print("EEE"); + console.Print("FFF"); + + // Then + Assert.Equal((byte)'D', buffer[0]); + Assert.Equal((byte)'D', buffer[2]); + Assert.Equal((byte)'D', buffer[4]); + Assert.Equal((byte)'E', buffer[6]); + Assert.Equal((byte)'E', buffer[8]); + Assert.Equal((byte)'E', buffer[10]); + Assert.Equal((byte)'F', buffer[12]); + Assert.Equal((byte)'F', buffer[14]); + Assert.Equal((byte)'F', buffer[16]); + } + + [Fact] + public void Should_Scroll_And_Blank_Last_Line() + { + // Given + byte* buffer = stackalloc byte[3 * 3 * 2]; + var frameBuffer = new FrameBuffer(buffer); + var console = new Console(3, 3, frameBuffer); + + // When + console.Print("AAA"); + console.Print("BBB"); + console.Print("CCC"); + console.Print("D"); + + // Then + Assert.Equal((byte)'B', buffer[0]); + Assert.Equal((byte)'B', buffer[2]); + Assert.Equal((byte)'B', buffer[4]); + Assert.Equal((byte)'C', buffer[6]); + Assert.Equal((byte)'C', buffer[8]); + Assert.Equal((byte)'C', buffer[10]); + Assert.Equal((byte)'D', buffer[12]); + Assert.Equal((byte)' ', buffer[14]); + Assert.Equal((byte)' ', buffer[16]); + } } unsafe public class HelloWorld { [Fact] - public void Should_Write_Hello_Space_World_With_Print_Statement() + public void Should_Write_Hello_World_With_Print_Statement() { // Given byte* buffer = stackalloc byte[80 * 25 * 2]; @@ -124,7 +255,7 @@ public void Should_Write_Hello_Space_World_With_Print_Statement() } [Fact] - public void Should_Write_Hello_Space_World_With_Print_Statements() + public void Should_Write_Hello_World_With_Print_Statements() { // Given byte* buffer = stackalloc byte[80 * 25 * 2]; @@ -151,7 +282,7 @@ public void Should_Write_Hello_Space_World_With_Print_Statements() } [Fact] - public void Should_Write_Hello_EOL_World_EOL_With_Print_Statement() + public void Should_Write_Hello_CRLF_World_With_Print_Statement() { // Given byte* buffer = stackalloc byte[80 * 25 * 2]; @@ -175,7 +306,7 @@ public void Should_Write_Hello_EOL_World_EOL_With_Print_Statement() } [Fact] - public void Should_Write_Hello_EOL_World_EOL_With_Print_Statements() + public void Should_Write_Hello_CRLF_World_With_Print_Statements() { // Given byte* buffer = stackalloc byte[80 * 25 * 2]; @@ -200,7 +331,7 @@ public void Should_Write_Hello_EOL_World_EOL_With_Print_Statements() } [Fact] - public void Should_Write_Hello_EOL_World_EOL_With_PrintLine_Statements() + public void Should_Write_Hello_CRLF_World_With_PrintLine_Statements() { // Given byte* buffer = stackalloc byte[80 * 25 * 2]; diff --git a/src/PatienceOS.Kernel.Tests/FrameBufferTests.cs b/src/PatienceOS.Kernel.Tests/FrameBufferTests.cs index dea9620..55125fd 100644 --- a/src/PatienceOS.Kernel.Tests/FrameBufferTests.cs +++ b/src/PatienceOS.Kernel.Tests/FrameBufferTests.cs @@ -25,5 +25,29 @@ public void FrameBuffer_Should_Contain_Hello() Assert.Equal((byte)'l', buffer[3]); Assert.Equal((byte)'o', buffer[4]); } + + [Fact] + public void FrameBuffer_Should_Copy() + { + // Given + byte* buffer = stackalloc byte[5]; + var frameBuffer = new FrameBuffer(buffer); + + frameBuffer.Write(0, (byte)'H'); + frameBuffer.Write(1, (byte)'e'); + frameBuffer.Write(2, (byte)'l'); + frameBuffer.Write(3, (byte)'l'); + frameBuffer.Write(4, (byte)'o'); + + // When + frameBuffer.Copy(3, 0, 2); + + // Then + Assert.Equal((byte)'l', buffer[0]); + Assert.Equal((byte)'o', buffer[1]); + Assert.Equal((byte)'l', buffer[2]); + Assert.Equal((byte)'l', buffer[3]); + Assert.Equal((byte)'o', buffer[4]); + } } } \ No newline at end of file diff --git a/src/PatienceOS.Kernel/Color.cs b/src/PatienceOS.Kernel/Color.cs new file mode 100644 index 0000000..2e5f328 --- /dev/null +++ b/src/PatienceOS.Kernel/Color.cs @@ -0,0 +1,28 @@ +namespace PatienceOS.Kernel +{ + /// + /// Allowable terminal colors + /// + /// + /// List of colours sourced from here: + /// + public enum Color + { + Black = 0x00, + Blue = 0x01, + Green = 0x02, + Cyan = 0x03, + Red = 0x04, + Magenta = 0x05, + Brown = 0x06, + LightGrey = 0x07, + DarkGrey = 0x08, + LightBlue = 0x09, + LightGreen = 0x0A, + LightCyan = 0x0B, + LightRed = 0x0C, + LightMagenta = 0x0D, + Yellow = 0x0E, + White = 0x0F, + } +} diff --git a/src/PatienceOS.Kernel/Console.cs b/src/PatienceOS.Kernel/Console.cs index e305cb6..bc4dec4 100644 --- a/src/PatienceOS.Kernel/Console.cs +++ b/src/PatienceOS.Kernel/Console.cs @@ -10,28 +10,44 @@ unsafe public struct Console private FrameBuffer frameBuffer; - private int column = 0; // nb. Incremented by two for each write, to account for the text colouring + private int column = 0; private int row = 0; - private byte foregroundColor = 0x0F; // White + private Color foregroundColor; + public Console(int width, int height, FrameBuffer frameBuffer) { this.width = width; this.height = height; + this.foregroundColor = Color.White; + this.frameBuffer = frameBuffer; + } + + public Console(int width, int height, Color foregroundColor, FrameBuffer frameBuffer) + { + this.width = width; + this.height = height; + this.foregroundColor = foregroundColor; this.frameBuffer = frameBuffer; } + /// /// Clear the screen /// + /// + /// Blanks the screen by writing the ASCII space character to each character cell + /// public void Clear() { - for (int i = 0; i < width * height * 2; i += 2) + // Reset the cursor position + column = 0; + row = 0; + + for (int i = 0; i < width * height; i++) { - // Write directly to the video memory - frameBuffer.Write(i, (byte)' '); - frameBuffer.Write(i + 1, foregroundColor); + Print(' '); } // Reset the cursor position @@ -39,54 +55,91 @@ public void Clear() row = 0; } + /// + /// Print a string to the current cursor position + /// and then moves the cursor to the next line + /// + public void PrintLine(string s) + { + Print(s); + Print("\n"); + } + /// /// Print a string to the current cursor position /// - /// - /// Assumes each screen character is represented by two bytes aligned as a 16-bit word, - /// see - /// public void Print(string s) { fixed (char* ps = s) { for (int i = 0; i < s.Length; i++) { - // Perform a CRLF if we encounter a Newline character - if (ps[i] == '\n') - { - column = 0; - row++; - - continue; - } - - // Perform a CRLF when the cursor reaches the end of the terminal line - // eg.column is 0 to 79, width = 80 - if (column >= width * 2) - { - column = 0; - row++; - } - - // Write directly to the video memory - frameBuffer.Write(row * width * 2 + column, (byte)ps[i]); - frameBuffer.Write(row * width * 2 + column + 1, foregroundColor); - - // Move the cursor right by one - column += 2; + Print(ps[i]); } } } /// - /// Prints a string to the current cursor position - /// and then moves the cursor to the next line + /// Print a character to the current cursor position /// - public void PrintLine(string s) + public void Print(char c) { - Print(s); - Print("\n"); + // Scroll if the cursor has dropped off the bottom of the terminal + if (row == height) + { + frameBuffer.Copy(width * 2, 0, (height - 1) * width * 2); + + row--; + + // Blank the last line ready for writing to + for (int i = 0; i < width; i++) + { + WriteVGATextCharacter(' '); + + // Move the cursor right by one character + column++; + } + + // Move the cursor to the beginning of the current line + column = 0; + } + + // Perform a CRLF if we encounter a Newline character + if (c == '\n') + { + column = 0; + row++; + + return; + } + + WriteVGATextCharacter(c); + + // Move the cursor right by one character + column++; + + // Perform a CRLF when the cursor reaches the end of the terminal line + // eg.column is 0 to 79, width = 80 + if (column == width) + { + column = 0; + row++; + } + } + + /// + /// Write directly to the video memory, calculating the + /// positional index required for the linear framebuffer + /// + /// + /// Assumes each screen character is represented by two bytes aligned as a 16-bit word, + /// see + /// + private void WriteVGATextCharacter(char c) + { + frameBuffer.Write(row * width * 2 + column * 2, (byte)c); + frameBuffer.Write(row * width * 2 + column * 2 + 1, (byte)foregroundColor); + //TODO: background color } } } diff --git a/src/PatienceOS.Kernel/FrameBuffer.cs b/src/PatienceOS.Kernel/FrameBuffer.cs index bee0396..2261776 100644 --- a/src/PatienceOS.Kernel/FrameBuffer.cs +++ b/src/PatienceOS.Kernel/FrameBuffer.cs @@ -1,4 +1,6 @@ -namespace PatienceOS.Kernel +using System; + +namespace PatienceOS.Kernel { /// /// A linear framebuffer of bytes @@ -16,5 +18,13 @@ public void Write(int position, byte value) { buffer[position] = value; } + + public void Copy(int sourcePosition, int destinationPosition, int length) + { + for (int i = 0; i < length; i++) + { + buffer[destinationPosition + i] = buffer[sourcePosition + i]; + } + } } } diff --git a/src/PatienceOS.Kernel/kernel.cs b/src/PatienceOS.Kernel/kernel.cs index 9ebea6f..3b5a346 100644 --- a/src/PatienceOS.Kernel/kernel.cs +++ b/src/PatienceOS.Kernel/kernel.cs @@ -15,7 +15,7 @@ static int Main() // https://migeel.sk/blog/2023/09/15/reverse-engineering-natively-compiled-dotnet-apps/ var frameBuffer = new FrameBuffer((byte*)VideoBaseAddress); - var console = new Console(Width, Height, frameBuffer); + var console = new Console(Width, Height, Color.Green, frameBuffer); console.Clear();