TSOS/src/kernel/drivers/vga/screen.c

273 lines
7.9 KiB
C

/*
Copyright 2022 Mattia Giambirtone & Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Implementation of a simple text-only VGA driver
#include "kernel/drivers/vga/screen.h"
#include "kernel/types.h"
#include "kernel/util.h"
i32 getCursorOffset(void);
void setCursorOffset(i32 offset);
i32 putchar(byte ch, i32 col, i32 row, byte attr);
i32 getOffset(i32 col, i32 row);
i32 getRow(i32 offset);
i32 getColumn(i32 offset);
// Public API below
void kprintAt(char* message, i32 col, i32 row) {
/*
Prints a null-terminated string to the VGA
text buffer at the specified row and column.
The cursor's position is not updated. If both
col and row are negative, the current cursor
location is used
*/
i32 offset;
if (col >= 0 && row >= 0) {
offset = getOffset(col, row);
}
else {
offset = getCursorOffset();
row = getRow(offset);
col = getColumn(offset);
}
for (i32 i = 0; message[i]; i++) {
offset = putchar(message[i], col, row, 0);
// We compute the new row and column for the
// next iteration. Hopefully the compiler inlines
// these two calls
row = getRow(offset);
col = getColumn(offset);
}
}
void inline kprint(char* message) {
/*
Prints a null-terminated string to the
VGA text buffer
*/
kprintAt(message, -1, -1);
}
void inline kprintln(char* message) {
/*
Identical to kprint, but calls
kprint("\n") afterwards
*/
kprint(message);
kprint("\n");
}
// Private API below
i32 putchar(byte ch, i32 col, i32 row, byte attr) {
/*
Writes a single character to the VGA text buffer
at the given row and column and returns the new
location of the cursor. If the attr byte is zero,
its value is the result of the expansion of the
LIGHT_GREY_ON_BLACK macro. If both row and col
are equal to -1, the cursor location is retrieved
from the VGA controller using I/O ports. The following
escape sequences are handled:
- '\n' -> Goes to the next line
- '\t' -> Prints VGA_TABSIZE spaces
- '\r' -> Resets the cursor's column, but not the row
Upon error, a negative value is returned; The possible
errors are codes are
- -1 -> Column out of bounds
- -2 -> Row out of bounds
*/
if (!attr) {
// No color data provided? We supply our own
attr = LIGHT_GREY_ON_BLACK;
}
if (col >= MAX_COLS) {
return -1;
}
if (row >= MAX_ROWS) {
return -2;
}
i32 offset = (col >= 0 && row >= 0)? getOffset(col, row): getCursorOffset();
switch (ch) {
// Note the difference between a carriage return (which
// only brings the write head back at 0 without changing
// the row) and the newline (which increases the row as well)
case '\r':
offset = getOffset(0, getRow(offset));
break;
case '\n':
offset = getOffset(0, getRow(offset) + 1);
break;
case '\t':
for (i32 i = 0; i < VGA_TABSIZE; i++) {
VMEM_BUF[offset] = ' ';
VMEM_BUF[offset + 1] = attr;
offset += 2;
}
break;
default:
VMEM_BUF[offset] = ch;
VMEM_BUF[offset + 1] = attr;
offset += 2;
break;
}
// We check if we reached the end of the screen, in which
// case we scroll
if (offset >= SCREEN_SIZE * 2) {
for (i32 i = 1; i < MAX_ROWS; i ++) {
// This loop will take the bytes of row i and copy
// them to row i - 1, effectively erasing the first
// one and causing the text on the screen to scroll!
copystr((char *)(getOffset(0, i) + VMEM_ADDRESS),
(char*)(getOffset(0, i - 1) + VMEM_ADDRESS),
MAX_COLS * 2);
}
// We empty the last line
char* last = (char*)(getOffset(0, MAX_ROWS - 1) + VMEM_ADDRESS);
for (i32 i = 0; i < MAX_COLS * 2; i++) {
last[i] = 0;
}
offset -= 2 * MAX_COLS;
}
setCursorOffset(offset);
return offset;
}
i32 getCursorOffset(void) {
/*
Returns the current offset of
the VGA text cursor
*/
// We request the high byte of the
// cursor position by writing to the
// VGA control port. We can then read
// the result on the VGA data port.
// Memory-mapped I/O is great, isn't it?
writeByte(REG_SCREEN_CTRL, 14);
// And we read it
i32 offset = readByte(REG_SCREEN_DATA);
// Since we requested the high byte
// (i.e. the 8 most significant bits)
// we now want to move them by 8 places.
// Conveniently, that's exactly what a
// left shift is for :)
offset <<= 8;
// Now we request the low byte of the cursor
// position
writeByte(REG_SCREEN_CTRL, 15);
offset += readByte(REG_SCREEN_DATA);
return offset * 2; // Times 2 because VGA cells are 2 bytes long
}
void setCursorOffset(i32 offset) {
/*
Sets the offset of the VGA text
cursor to the desired value. Note
that, this being a low level wrapper
around I/O ports, no bounds checking
is performed: the caller should make
sure that the offset fits within the
80x25 grid of the VGA screen!
*/
// We now divide by 2 because while we count
// in bytes, the VGA controller counts in cells!
offset /= 2;
// This function is almost identical to getCursorOffset,
// except we're now writing on the bus instead of reading
// from it, so the VGA controller updates the cursor position
// We write the high bits first
writeByte(REG_SCREEN_CTRL, 14);
writeByte(REG_SCREEN_DATA, (byte)(offset >> 8));
// Then the low bits
writeByte(REG_SCREEN_CTRL, 15);
writeByte(REG_SCREEN_DATA, (byte)(offset & 0xff));
// These bitwise tricks *seem* like black
// magic, but they're quite simple: shifting
// the offset by 8 bits moves the high bits 8
// positions down (so we lose the low bits and
// the high bits now fit into a single byte, which
// is what writeByte wants). Then we get rid of the
// high bits (0xff is 11110000) with a bitwise and,
// leaving us with only the low bits set which we
// then feed to the I/O port again; The reason this
// works is because 0xff (which is smaller than our
// offset), is zero-extended from the beginning with
// zeroes, so when we perform the operation the high
// bits are cancelled out
}
void clearScreen(void) {
/*
Clears the screen and resets
the cursor position to 0, 0
*/
for (i32 i = 0; i < SCREEN_SIZE; i++) {
VMEM_BUF[i * 2] = ' '; // The screen is actually never "empty", just filled with spaces!
VMEM_BUF[i * 2 + 1] = LIGHT_GREY_ON_BLACK;
}
setCursorOffset(getOffset(0, 0));
}
i32 inline getOffset(i32 col, i32 row) {
/*
Converts a column, row pair into
an absolute offset into the VGA
text buffer
*/
// We multiply by 2 because each VGA
// character cell is 2 bytes long
return (row * MAX_COLS + col) * 2;
}
i32 inline getRow(i32 offset) {
/*
Converts an absolute offset into
the VGA text buffer into a row
*/
return offset / (2 * MAX_COLS);
}
i32 inline getColumn(i32 offset) {
/*
Converts an absolute offset into
the VGA text buffer into a column
*/
return (offset - (getRow(offset) * 2 * MAX_COLS)) / 2;
}