Search code examples
gccterminalx86osdevvga

OSDev: Why does my VGA terminal scrolling not work?


I am developing an operating system as a personal hobby so I can learn about software engineering and computer architecture.

I am trying to get the VGA terminal to "scroll" when text reaches past the bottom, or VGA_HEIGHT. I am using code from the OSDev wiki mixed with my own code.

My goal is to copy each line, and then write it to the line just above it.

Here is the code that I am using:

void terminal_putentryat(unsigned char c, uint8_t color, size_t x, size_t y) {
    const size_t index = y * VGA_WIDTH + x;
    terminal_buffer[index] = vga_entry(c, color);
}

void terminal_putchar(char c) {
    unsigned char uc = c;

    switch(c) {
      case NEWLINE:
        terminal_row++;
        terminal_column = 0;
        terminal_putentryat(' ', terminal_color, terminal_column, terminal_row);
        update_cursor(terminal_column + 1, terminal_row);
        break;

      case '\t':
        /* TODO: Implement tab */
        terminal_column += 4;
        break;

      default:
        terminal_putentryat(uc, terminal_color, terminal_column, terminal_row);
        update_cursor(terminal_column + 1, terminal_row);
        if (++terminal_column == VGA_WIDTH) {
          terminal_column = 0;
          if (++terminal_row == VGA_HEIGHT)
            terminal_row = 0;
        }
    }
    if(terminal_row >= VGA_HEIGHT) {
      terminal_print_error();
      terminal_buffer[(15 * VGA_WIDTH) + 15] = terminal_buffer[(0 * VGA_WIDTH) + 4];
      size_t i, j;
      for(i = 0; i < VGA_WIDTH-1; i++) {
        for(j = VGA_HEIGHT-2; j > 0; j--)
          terminal_buffer[(j * VGA_WIDTH) + i] = terminal_buffer[((j+1) * VGA_WIDTH) + i];
      }
    }
}

But this function only partially works. SPecifically, this section:

if(terminal_row >= VGA_HEIGHT) {
      terminal_print_error();
      terminal_buffer[(15 * VGA_WIDTH) + 15] = terminal_buffer[(0 * VGA_WIDTH) + 4];
      size_t i, j;
      for(i = 0; i < VGA_WIDTH-1; i++) {
        for(j = VGA_HEIGHT-2; j > 0; j--)
          terminal_buffer[(j * VGA_WIDTH) + i] = terminal_buffer[((j+1) * VGA_WIDTH) + i];
      }
    }

It only partially copies the data. For example, when I write to the terminal with 'printf()', if the string is longer than the data being scrolled, it will not scroll.

/*
 * This is the screen driver. It contains functions which print
 * characters and colors to the screen
 * using the VGA controller.
 */

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>

#include <kernel/tty.h>

#include "vga.h"

#define REG_SCREEN_CTRL 0x3D4
#define REG_SCREEN_DATA 0x3D5

#define NEWLINE 0x0A
#define TAB 0x09

static const size_t VGA_WIDTH = 80;
static const size_t VGA_HEIGHT = 25;
static uint16_t *const VGA_MEMORY = (uint16_t *)0xC03FF000;

static size_t terminal_row;
static size_t terminal_column;
static uint8_t terminal_color;
static uint16_t *terminal_buffer;

int get_offset(int col, int row) {
  return 2 * (row * VGA_WIDTH + col);
}

int get_offset_row(int offset) {
  return offset / (2 * VGA_WIDTH);
}

int get_offset_col(int offset) {
  return (offset - (get_offset_row(offset) * 2 * VGA_WIDTH)) / 2;
}

static void scroll() {
  if(terminal_row >= VGA_HEIGHT) {

  }
}
void terminal_print_error(void) {
  if(terminal_row >= VGA_HEIGHT) {
    terminal_row = 0;
    /* print white/red E to bottom right corner of screen */
    terminal_putentryat('E', vga_entry_color(VGA_COLOR_RED, VGA_COLOR_WHITE),
                        VGA_WIDTH - 1, VGA_HEIGHT - 1);
  }
}

void terminal_initialize(void) {
    terminal_row = 0;
    terminal_column = 0;
    terminal_color = vga_entry_color(VGA_COLOR_BLACK, VGA_COLOR_CYAN);
    terminal_buffer = VGA_MEMORY;
    for (size_t y = 0; y < VGA_HEIGHT; y++) {
        for (size_t x = 0; x < VGA_WIDTH; x++) {
            const size_t index = y * VGA_WIDTH + x;
            terminal_buffer[index] = vga_entry(' ', terminal_color);
        }
    }
}

void terminal_setcolor(uint8_t color) {
    terminal_color = color;
}

void terminal_putentryat(unsigned char c, uint8_t color, size_t x, size_t y) {
    const size_t index = y * VGA_WIDTH + x;
    terminal_buffer[index] = vga_entry(c, color);
}

void terminal_putchar(char c) {
    unsigned char uc = c;

    switch(c) {
      case NEWLINE:
        terminal_row++;
        terminal_column = 0;
        terminal_putentryat(' ', terminal_color, terminal_column, terminal_row);
        update_cursor(terminal_column + 1, terminal_row);
        break;

      case '\t':
        /* TODO: Implement tab */
        terminal_column += 4;
        break;

      default:
        terminal_putentryat(uc, terminal_color, terminal_column, terminal_row);
        update_cursor(terminal_column + 1, terminal_row);
        if (++terminal_column == VGA_WIDTH) {
          terminal_column = 0;
          if (++terminal_row == VGA_HEIGHT)
            terminal_row = 0;
        }
    }
    if(terminal_row >= VGA_HEIGHT) {
      terminal_print_error();
      terminal_buffer[(15 * VGA_WIDTH) + 15] = terminal_buffer[(0 * VGA_WIDTH) + 4];
      size_t i, j;
      for(i = 0; i < VGA_WIDTH-1; i++) {
        for(j = VGA_HEIGHT-2; j > 0; j--)
          terminal_buffer[(j * VGA_WIDTH) + i] = terminal_buffer[((j+1) * VGA_WIDTH) + i];
      }
    }
}

void terminal_write(const char *data, size_t size) {
    for (size_t i = 0; i < size; i++)
        terminal_putchar(data[i]);
}

void terminal_writestring(const char *data) {
    terminal_write(data, strlen(data));
}

/* inb */
unsigned char port_byte_in(unsigned short port) {
  unsigned char result;
  asm ("in %%dx, %%al" : "=a" (result) : "d" (port));
  return result;
}

void port_byte_out(unsigned short port, unsigned char data) {
  asm ("out %%al, %%dx" : : "a" (data), "d" (port));
}

int get_cursor_offset() {
  /* Use the VGA ports to get the current cursor position
   * 1. Ask for high byte of the cursor offset (data 14)
   * 2. Ask for low byte (data 15)
   */
  port_byte_out(REG_SCREEN_CTRL, 14);
  int offset = port_byte_in(REG_SCREEN_DATA) << 8; /* High byte: << 8 */
  port_byte_out(REG_SCREEN_CTRL, 15);
  offset += port_byte_in(REG_SCREEN_DATA);
  return offset * 2; /* Position * size of character cell */
}

void set_cursor_offset(int offset) {
  /* Similar to get_cursor_offset, but instead of reading we write data */
  offset /= 2;
  port_byte_out(REG_SCREEN_CTRL, 14);
  port_byte_out(REG_SCREEN_DATA, (unsigned char)(offset >> 8));
  port_byte_out(REG_SCREEN_CTRL, 15);
  port_byte_out(REG_SCREEN_DATA, (unsigned char)(offset & 0xff));
}

void update_cursor(int x, int y) {
  uint16_t pos = y * VGA_WIDTH + x;

  port_byte_out(REG_SCREEN_CTRL, 15);
  port_byte_out(REG_SCREEN_DATA, (uint8_t)(pos & 0xFF));
  port_byte_out(REG_SCREEN_CTRL, 14);
  port_byte_out(REG_SCREEN_DATA, (uint8_t)(pos >> 8) & 0xFF);
}

/*
void enable_cursor(uint8_t cursor_start, uint8_t cursor_end) {
  outb(0x3D4, 0x0A);
  outb(0x3D, (inb(0x3D5) & 0xC0) | cursor_start);

  outb(0x3D4, 0x0B);
  outb(0x3D5, (inb(0x3D5) & 0xE0) | cursor_end);
}
*/

Solution

  • I believe the loop scroll works mostly correct however a lot of the code before it is causing some of the bugs you're running into.

    The default case in your switch statement is incrementing your cursor row when the cursor width goes past the right edge of the screen. The if (++terminal_row == VGA_HEIGHT) is then resetting your cursor row to 0 if the row increment goes past the bottom edge of the screen. This prevents the scroll code from ever running. You should remove if (++terminal_row == VGA_HEIGHT) terminal_row = 0; and replace with just terminal_row++; since the logic immediately following your switch handles the terminal row variable.

    I would recommend separating the logic that modifies terminal_row and terminal_column from the logic that validates, resets and scrolls these variables. For example your handling of the '\t' character, if placed within the last 3 characters of a line, will overflow characters onto the next line without updating the terminal_row and terminal_column vars to where they should be.

    • Your newline character will always leave a blank character at the start of the row because you do terminal_putentryat after modifying the cursor to a newline, not before. In fact, you shouldn't have to do terminal_putentryat for any newline because visibly no characters change, only the cursor position.
    • You may want to modify the handling of \t to call terminal_write(' '); instead of modifying the column variable directly. This simplifies the logic that actually updates your terminal. The second paragraph above details some of the issues this change resolves.
    • update_cursor() should only be called once, at the end of terminal_putchar() since every char you put should update the cursor. This may change if you want terminal_putchar() to handle 0-width characters but this seems counter-intuitive to me since this function is specifically designed to handle characters which are displayed.
    • The loop to modify the terminal buffer upwards to scroll never clears the bottom row of characters
    • The logic at the bottom of your function to handle terminal_row >= VGA_HEIGHT never resets terminal_row to a valid value. It does call terminal_print_error() but this function resets your row to 0 when you want to keep the row at the bottom.

      void terminal_putchar(char c) {
          unsigned char uc = c;
      
          // Handle character output and terminal_row/column modification
          switch(c) {
            case NEWLINE:
              terminal_row++;
              terminal_column = 0;
              break;
      
            case '\t':
              terminal_write('    ');
              break;
      
            default:
              terminal_putentryat(uc, terminal_color, terminal_column, terminal_row);
              terminal_column++;
          }
      
          // Handle validation on terminal_column before terminal_row, since the logic in terminal_column can update terminal_row
          if(terminal_column >= VGA_WIDTH) {
                terminal_column = 0;
                terminal_row++;
          }
      
          // Handle validating terminal_row, and scrolling the screen upwards if necessary.
          if(terminal_row >= VGA_HEIGHT) {
              // You shouldn't need terminal_print_error() since you are handling the case where terminal_row >= VGA_HEIGHT
              // terminal_print_error();
      
              // What does this line do? Appears to set the 16th character of the 16th row to the same value as the 5th character of the 1st row.
              // terminal_buffer[(15 * VGA_WIDTH) + 15] = terminal_buffer[(0 * VGA_WIDTH) + 4];
      
              size_t i, j;
              for(i = 0; i < VGA_WIDTH-1; i++) {
                  for(j = VGA_HEIGHT-2; j > 0; j--) {
                      terminal_buffer[(j * VGA_WIDTH) + i] = terminal_buffer[((j+1) * VGA_WIDTH) + i];
                  }               
              }
      
              // Also clear out the bottom row
              for(i = 0; i < VGA_WIDTH-1; i++) {
                  terminal_putentryat(' ', terminal_color, i, VGA_HEIGHT-1);
              }
      
              terminal_row = VGA_HEIGHT-1;
          }
      
          update_cursor(terminal_column, terminal_row);
      }