| // Copyright 2016 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <assert.h> |
| #include <fcntl.h> |
| #include <lib/gfx-font-data/gfx-font-data.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/param.h> |
| #include <zircon/process.h> |
| #include <zircon/syscalls.h> |
| |
| #include <fbl/auto_lock.h> |
| |
| #include "vc.h" |
| |
| // Default height/width (in px) of console before any displays are |
| // attached, since we need somewhere to put any data that is received. |
| #define DEFAULT_WIDTH 1024 |
| #define DEFAULT_HEIGHT 768 |
| #define SCROLLBACK_ROWS 1024 // TODO make configurable |
| |
| #define ABS(val) (((val) >= 0) ? (val) : -(val)) |
| |
| const gfx_font* g_font = &gfx_font_9x16; |
| const keychar_t* g_keymap = qwerty_map; |
| |
| static zx_status_t vc_setup(vc_t* vc, const color_scheme_t* color_scheme) { |
| // calculate how many rows/columns we have |
| vc->rows = DEFAULT_HEIGHT / vc->charh; |
| vc->columns = DEFAULT_WIDTH / vc->charw; |
| vc->scrollback_rows_max = SCROLLBACK_ROWS; |
| vc->scrollback_rows_count = 0; |
| vc->scrollback_offset = 0; |
| |
| // allocate the text buffer |
| vc->text_buf = |
| reinterpret_cast<vc_char_t*>(calloc(1, vc->rows * vc->columns * sizeof(vc_char_t))); |
| if (!vc->text_buf) |
| return ZX_ERR_NO_MEMORY; |
| |
| // allocate the scrollback buffer |
| vc->scrollback_buf = reinterpret_cast<vc_char_t*>( |
| calloc(1, vc->scrollback_rows_max * vc->columns * sizeof(vc_char_t))); |
| if (!vc->scrollback_buf) { |
| free(vc->text_buf); |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| // set up the default palette |
| memcpy(&vc->palette, default_palette, sizeof(default_palette)); |
| |
| vc->front_color = color_scheme->front; |
| vc->back_color = color_scheme->back; |
| |
| return ZX_OK; |
| } |
| |
| bool vc_graphics_enabled(vc_t* vc) { return vc->graphics && vc->graphics->vc_gfx; } |
| |
| static void vc_invalidate(void* cookie, int x0, int y0, int w, int h) { |
| vc_t* vc = reinterpret_cast<vc_t*>(cookie); |
| |
| if (!g_vc_owns_display || !vc->active || !vc_graphics_enabled(vc)) { |
| return; |
| } |
| |
| assert(h >= 0); |
| int y1 = y0 + h; |
| assert(y0 <= static_cast<int>(vc->rows)); |
| assert(y1 <= static_cast<int>(vc->rows)); |
| |
| // Clip the y range so that we don't unnecessarily draw characters |
| // outside the visible range, and so that we don't draw characters into |
| // the bottom margin. |
| int visible_y0 = vc->viewport_y; |
| int visible_y1 = vc->viewport_y + vc_rows(vc); |
| y0 = MAX(y0, visible_y0); |
| y1 = MIN(y1, visible_y1); |
| |
| for (int y = y0; y < y1; y++) { |
| if (y < 0) { |
| // Scrollback row. |
| vc_char_t* row = vc_get_scrollback_line_ptr(vc, y + vc->scrollback_rows_count); |
| for (int x = x0; x < x0 + w; x++) { |
| vc_gfx_draw_char(vc->graphics, vc, row[x], x, y - vc->viewport_y, |
| /* invert= */ false); |
| } |
| } else { |
| // Row in the main console region (non-scrollback). |
| vc_char_t* row = &vc->text_buf[y * vc->columns]; |
| for (int x = x0; x < x0 + w; x++) { |
| // Check whether we should display the cursor at this |
| // position. Note that it's possible that the cursor is |
| // outside the display area (vc->cursor_x == |
| // vc->columns). In that case, we won't display the |
| // cursor, even if there's a margin. This matches |
| // gnome-terminal. |
| bool invert = (!vc->hide_cursor && static_cast<unsigned>(x) == vc->cursor_x && |
| static_cast<unsigned>(y) == vc->cursor_y); |
| vc_gfx_draw_char(vc->graphics, vc, row[x], x, y - vc->viewport_y, invert); |
| } |
| } |
| } |
| } |
| |
| // implement tc callbacks: |
| |
| static inline void vc_invalidate_lines(vc_t* vc, int y, int h) { |
| if (y < vc->invy0) { |
| vc->invy0 = y; |
| } |
| y += h; |
| if (y > vc->invy1) { |
| vc->invy1 = y; |
| } |
| } |
| |
| static void vc_tc_invalidate(void* cookie, int x0, int y0, int w, int h) { |
| vc_t* vc = reinterpret_cast<vc_t*>(cookie); |
| vc_invalidate(cookie, x0, y0, w, h); |
| vc_invalidate_lines(vc, y0, h); |
| } |
| |
| static void vc_tc_movecursor(void* cookie, int x, int y) { |
| vc_t* vc = reinterpret_cast<vc_t*>(cookie); |
| unsigned old_x = vc->cursor_x; |
| unsigned old_y = vc->cursor_y; |
| vc->cursor_x = x; |
| vc->cursor_y = y; |
| if (g_vc_owns_display && vc->active && !vc->hide_cursor) { |
| // Clear the cursor from its old position. |
| vc_invalidate(cookie, old_x, old_y, 1, 1); |
| vc_invalidate_lines(vc, old_y, 1); |
| |
| // Display the cursor in its new position. |
| vc_invalidate(cookie, vc->cursor_x, vc->cursor_y, 1, 1); |
| vc_invalidate_lines(vc, vc->cursor_y, 1); |
| } |
| } |
| |
| static void vc_tc_scrollback_buffer_push(vc_t* vc, vc_char_t* src) { |
| unsigned dest_row; |
| assert(vc->scrollback_rows_count <= vc->scrollback_rows_max); |
| if (vc->scrollback_rows_count < vc->scrollback_rows_max) { |
| // Add a row without dropping any existing rows. |
| assert(vc->scrollback_offset == 0); |
| dest_row = vc->scrollback_rows_count++; |
| } else { |
| // Add a row and drop an existing row. |
| assert(vc->scrollback_offset < vc->scrollback_rows_max); |
| dest_row = vc->scrollback_offset++; |
| if (vc->scrollback_offset == vc->scrollback_rows_max) |
| vc->scrollback_offset = 0; |
| } |
| vc_char_t* dst = &vc->scrollback_buf[dest_row * vc->columns]; |
| memcpy(dst, src, vc->columns * sizeof(vc_char_t)); |
| } |
| |
| static void vc_tc_push_scrollback_line(void* cookie, int y) { |
| vc_t* vc = reinterpret_cast<vc_t*>(cookie); |
| vc_char_t* src = &vc->text_buf[y * vc->columns]; |
| vc_tc_scrollback_buffer_push(vc, src); |
| |
| // If we're displaying only the main console region (and no |
| // scrollback), then keep displaying that (i.e. don't modify |
| // viewport_y). |
| if (vc->viewport_y < 0) { |
| // We are displaying some of the scrollback buffer. |
| if (vc->viewport_y > -static_cast<int>(vc->scrollback_rows_max)) { |
| // Scroll the viewport to continue displaying the same point in |
| // the scrollback buffer. |
| --vc->viewport_y; |
| } else { |
| // We were displaying the line at the top of the scrollback |
| // buffer, but we dropped that line from the buffer. We could |
| // leave the display as it was (which is what gnome-terminal |
| // does) and not scroll the display. However, that causes |
| // problems. If the user later scrolls down, we won't |
| // necessarily be able to display the lines below -- we might |
| // have dropped those too. So, instead, let's scroll the |
| // display and remove the scrollback line that was lost. |
| // |
| // For simplicity, fall back to redrawing everything. |
| vc_invalidate(vc, 0, -vc->scrollback_rows_max, vc->columns, vc_rows(vc)); |
| vc_render(vc); |
| } |
| } |
| } |
| |
| static void vc_set_cursor_hidden(vc_t* vc, bool hide) { |
| if (vc->hide_cursor == hide) |
| return; |
| vc->hide_cursor = hide; |
| if (g_vc_owns_display && vc->active) { |
| vc_invalidate(vc, vc->cursor_x, vc->cursor_y, 1, 1); |
| vc_invalidate_lines(vc, vc->cursor_y, 1); |
| } |
| } |
| |
| static void vc_tc_copy_lines(void* cookie, int y_dest, int y_src, int line_count) { |
| vc_t* vc = reinterpret_cast<vc_t*>(cookie); |
| |
| if (vc->viewport_y < 0) { |
| tc_copy_lines(&vc->textcon, y_dest, y_src, line_count); |
| |
| // The viewport is scrolled. For simplicity, fall back to |
| // redrawing all of the non-scrollback lines in this case. |
| int rows = vc_rows(vc); |
| vc_invalidate(vc, 0, 0, vc->columns, rows); |
| vc_invalidate_lines(vc, 0, rows); |
| return; |
| } |
| |
| // Remove the cursor from the display before copying the lines on |
| // screen, otherwise we might be copying a rendering of the cursor to a |
| // position where the cursor isn't. This must be done before the |
| // tc_copy_lines() call, otherwise we might render the wrong character. |
| bool old_hide_cursor = vc->hide_cursor; |
| if (g_vc_owns_display && vc->active) { |
| vc_set_cursor_hidden(vc, true); |
| } |
| |
| // The next two calls can be done in any order. |
| tc_copy_lines(&vc->textcon, y_dest, y_src, line_count); |
| |
| if (g_vc_owns_display && vc->active && vc_graphics_enabled(vc)) { |
| gfx_copyrect(vc->graphics->vc_gfx, 0, y_src * vc->charh, vc->graphics->vc_gfx->width, |
| line_count * vc->charh, 0, y_dest * vc->charh); |
| |
| // Restore the cursor. |
| vc_set_cursor_hidden(vc, old_hide_cursor); |
| |
| vc_status_update(); |
| vc_gfx_invalidate_status(vc->graphics); |
| vc_invalidate_lines(vc, 0, vc_rows(vc)); |
| } |
| } |
| |
| static void vc_tc_setparam(void* cookie, int param, uint8_t* arg, size_t arglen) { |
| vc_t* vc = reinterpret_cast<vc_t*>(cookie); |
| switch (param) { |
| case TC_SET_TITLE: |
| strncpy(vc->title, (char*)arg, sizeof(vc->title)); |
| vc->title[sizeof(vc->title) - 1] = '\0'; |
| vc_status_update(); |
| if (g_vc_owns_display && vc_graphics_enabled(vc)) { |
| vc_gfx_invalidate_status(vc->graphics); |
| } |
| break; |
| case TC_SHOW_CURSOR: |
| vc_set_cursor_hidden(vc, false); |
| break; |
| case TC_HIDE_CURSOR: |
| vc_set_cursor_hidden(vc, true); |
| break; |
| default:; // nothing |
| } |
| } |
| |
| static void vc_clear_gfx(vc_t* vc) { |
| // Fill display with background color |
| if (g_vc_owns_display && vc->active && vc_graphics_enabled(vc)) { |
| gfx_fillrect(vc->graphics->vc_gfx, 0, 0, vc->graphics->vc_gfx->width, |
| vc->graphics->vc_gfx->height, palette_to_color(vc, vc->back_color)); |
| } |
| } |
| |
| static void vc_reset(vc_t* vc) { |
| // reset the cursor |
| vc->cursor_x = 0; |
| vc->cursor_y = 0; |
| // reset the viewport position |
| vc->viewport_y = 0; |
| |
| tc_init(&vc->textcon, vc->columns, vc_rows(vc), vc->text_buf, vc->front_color, vc->back_color, |
| vc->cursor_x, vc->cursor_y); |
| vc->textcon.cookie = vc; |
| vc->textcon.invalidate = vc_tc_invalidate; |
| vc->textcon.movecursor = vc_tc_movecursor; |
| vc->textcon.push_scrollback_line = vc_tc_push_scrollback_line; |
| vc->textcon.copy_lines = vc_tc_copy_lines; |
| vc->textcon.setparam = vc_tc_setparam; |
| |
| // fill textbuffer with blank characters |
| size_t count = vc->rows * vc->columns; |
| vc_char_t* ptr = vc->text_buf; |
| while (count--) { |
| *ptr++ = vc_char_make(' ', vc->front_color, vc->back_color); |
| } |
| |
| vc_clear_gfx(vc); |
| if (vc_graphics_enabled(vc)) { |
| vc_gfx_invalidate_all(vc->graphics, vc); |
| } |
| } |
| |
| void vc_status_clear() { |
| if (g_vc_owns_display && g_active_vc && vc_graphics_enabled(g_active_vc)) { |
| gfx_fillrect(g_active_vc->graphics->vc_status_bar_gfx, 0, 0, |
| g_active_vc->graphics->vc_status_bar_gfx->width, |
| g_active_vc->graphics->vc_status_bar_gfx->height, |
| default_palette[STATUS_COLOR_BG]); |
| } |
| } |
| |
| void vc_status_commit() { |
| if (g_vc_owns_display && g_active_vc && vc_graphics_enabled(g_active_vc)) { |
| vc_gfx_invalidate_status(g_active_vc->graphics); |
| } |
| } |
| |
| void vc_status_write(int x, unsigned color, const char* text) { |
| char c; |
| unsigned fg = default_palette[color]; |
| unsigned bg = default_palette[STATUS_COLOR_BG]; |
| vc_t* vc = g_active_vc; |
| if (!g_active_vc) { |
| return; |
| } |
| |
| if (g_vc_owns_display && vc_graphics_enabled(vc)) { |
| x *= vc->graphics->vc_font->width; |
| while ((c = *text++) != 0) { |
| gfx_putchar(vc->graphics->vc_status_bar_gfx, vc->graphics->vc_font, c, x, 0, fg, bg); |
| x += vc->graphics->vc_font->width; |
| } |
| } |
| } |
| |
| void vc_render(vc_t* vc) { |
| if (g_vc_owns_display && vc->active && vc_graphics_enabled(vc)) { |
| vc_status_update(); |
| vc_gfx_invalidate_all(vc->graphics, vc); |
| } |
| } |
| |
| void vc_full_repaint(vc_t* vc) { |
| if (g_vc_owns_display && vc_graphics_enabled(vc)) { |
| vc_clear_gfx(vc); |
| int scrollback_lines = vc_get_scrollback_lines(vc); |
| vc_invalidate(vc, 0, -scrollback_lines, vc->columns, scrollback_lines + vc->rows); |
| } |
| } |
| |
| int vc_get_scrollback_lines(vc_t* vc) { return vc->scrollback_rows_count; } |
| |
| vc_char_t* vc_get_scrollback_line_ptr(vc_t* vc, unsigned row) { |
| assert(row < vc->scrollback_rows_count); |
| row += vc->scrollback_offset; |
| if (row >= vc->scrollback_rows_max) |
| row -= vc->scrollback_rows_max; |
| return &vc->scrollback_buf[row * vc->columns]; |
| } |
| |
| static void vc_scroll_viewport_abs(vc_t* vc, int vpy) { |
| vpy = MIN(vpy, 0); |
| vpy = MAX(vpy, -vc_get_scrollback_lines(vc)); |
| int diff = vpy - vc->viewport_y; |
| if (diff == 0) |
| return; |
| int diff_abs = ABS(diff); |
| vc->viewport_y = vpy; |
| int rows = vc_rows(vc); |
| if (!g_vc_owns_display || !vc->active || !vc_graphics_enabled(vc)) { |
| return; |
| } |
| if (diff_abs >= rows) { |
| // We are scrolling the viewport by a large delta. Invalidate all |
| // of the visible area of the console. |
| vc_invalidate(vc, 0, vpy, vc->columns, rows); |
| } else { |
| if (diff > 0) { |
| gfx_copyrect(vc->graphics->vc_gfx, 0, diff_abs * vc->charh, vc->graphics->vc_gfx->width, |
| (rows - diff_abs) * vc->charh, 0, 0); |
| vc_invalidate(vc, 0, vpy + rows - diff_abs, vc->columns, diff_abs); |
| } else { |
| gfx_copyrect(vc->graphics->vc_gfx, 0, 0, vc->graphics->vc_gfx->width, |
| (rows - diff_abs) * vc->charh, 0, diff_abs * vc->charh); |
| vc_invalidate(vc, 0, vpy, vc->columns, diff_abs); |
| } |
| } |
| vc_render(vc); |
| } |
| |
| void vc_scroll_viewport(vc_t* vc, int dir) { vc_scroll_viewport_abs(vc, vc->viewport_y + dir); } |
| |
| void vc_scroll_viewport_top(vc_t* vc) { vc_scroll_viewport_abs(vc, INT_MIN); } |
| |
| void vc_scroll_viewport_bottom(vc_t* vc) { vc_scroll_viewport_abs(vc, 0); } |
| |
| void vc_set_fullscreen(vc_t* vc, bool fullscreen) { |
| unsigned flags; |
| if (fullscreen) { |
| flags = vc->flags | VC_FLAG_FULLSCREEN; |
| } else { |
| flags = vc->flags & ~VC_FLAG_FULLSCREEN; |
| } |
| if (flags != vc->flags) { |
| vc->flags = flags; |
| tc_seth(&vc->textcon, vc_rows(vc)); |
| } |
| vc_render(vc); |
| } |
| |
| const gfx_font* vc_get_font() { return g_font; } |
| |
| void vc_attach_gfx(vc_t* vc) { |
| if (vc->graphics == nullptr) { |
| return; |
| } |
| // If the size of the new gfx console doesn't match what we had been |
| // attached to, we need to allocate new memory and copy the existing |
| // data over. |
| unsigned rows = vc->graphics->vc_gfx->height / vc->charh; |
| unsigned columns = vc->graphics->vc_gfx->width / vc->charw; |
| if (rows == vc->rows && columns == vc->columns) { |
| return; |
| } |
| |
| // allocate the new buffers |
| vc_char_t* text_buf = reinterpret_cast<vc_char_t*>(calloc(1, rows * columns * sizeof(vc_char_t))); |
| vc_char_t* scrollback_buf = reinterpret_cast<vc_char_t*>( |
| calloc(1, vc->scrollback_rows_max * columns * sizeof(vc_char_t))); |
| if (text_buf && scrollback_buf) { |
| // fill new text buffer with blank characters |
| size_t count = rows * columns; |
| vc_char_t* ptr = text_buf; |
| while (count--) { |
| *ptr++ = vc_char_make(' ', vc->front_color, vc->back_color); |
| } |
| |
| // Copy the most recent data from the old console to the new one. There are |
| // (vc->cursor_y + 1) rows available, and we want (rows - (vc->rows - vc_rows(vc)) |
| // rows. Subtract to get the first row index to copy. |
| unsigned old_i = |
| MAX(static_cast<int>((vc->cursor_y + 1) - (rows - (vc->rows - vc_rows(vc)))), 0); |
| unsigned old_data_start = old_i; |
| unsigned new_i = 0; |
| size_t len = (vc->columns < columns ? vc->columns : columns) * sizeof(vc_char_t); |
| while (new_i < rows && old_i <= vc->cursor_y) { |
| memcpy(text_buf + columns * (new_i++), vc->text_buf + vc->columns * (old_i++), len); |
| } |
| |
| // copy the old scrollback buffer |
| for (int i = 0; i < SCROLLBACK_ROWS; i++) { |
| memcpy(scrollback_buf + columns * i, vc->scrollback_buf + vc->columns * i, len); |
| } |
| |
| vc_char_t* old_text_buf = vc->text_buf; |
| unsigned old_columns = vc->columns; |
| free(vc->scrollback_buf); |
| |
| vc->text_buf = text_buf; |
| vc->scrollback_buf = scrollback_buf; |
| vc->rows = rows; |
| vc->columns = columns; |
| |
| // Push any data that fell off of text_buf. Use a temporary buffer of the |
| // right length to handle going to a wider console. Set it to ' 's before |
| // pushing, so we don't merge data from old rows. |
| if (old_data_start) { |
| vc_char_t buf[columns]; |
| for (unsigned i = 0; i < old_data_start; i++) { |
| vc_char_t* ptr = buf; |
| while (ptr < buf + columns) { |
| *ptr++ = vc_char_make(' ', vc->front_color, vc->back_color); |
| } |
| ptr = old_text_buf + i * old_columns; |
| memcpy(buf, ptr, len); |
| |
| vc_tc_scrollback_buffer_push(vc, buf); |
| } |
| } |
| |
| free(old_text_buf); |
| } else { |
| // If we failed to allocate new buffers, use the old ones as best we can |
| free(text_buf); |
| free(scrollback_buf); |
| |
| vc->rows = MIN(vc->rows, rows); |
| vc->columns = MIN(vc->columns, columns); |
| |
| printf("vc: buffer resize failed, reusing old buffers (%dx%d)\n", vc->rows, vc->columns); |
| } |
| |
| vc->viewport_y = 0; |
| if (vc->cursor_x >= vc->columns) { |
| vc->cursor_x = vc->columns - 1; |
| } |
| if (static_cast<int>(vc->cursor_y) >= vc_rows(vc)) { |
| vc->cursor_y = vc_rows(vc) - 1; |
| } |
| |
| tc_init(&vc->textcon, vc->columns, vc_rows(vc), vc->text_buf, vc->front_color, vc->back_color, |
| vc->cursor_x, vc->cursor_y); |
| } |
| |
| zx_status_t vc_alloc(vc_t** out, const color_scheme_t* color_scheme) { |
| vc_t* vc = reinterpret_cast<vc_t*>(calloc(1, sizeof(vc_t))); |
| if (!vc) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| vc->fd = -1; |
| |
| vc->keymap = g_keymap; |
| vc->font = vc_get_font(); |
| vc->charw = vc->font->width; |
| vc->charh = vc->font->height; |
| |
| zx_status_t status = vc_setup(vc, color_scheme); |
| if (status != ZX_OK) { |
| free(vc); |
| return status; |
| } |
| |
| vc_attach_to_main_display(vc); |
| vc_reset(vc); |
| |
| *out = vc; |
| return ZX_OK; |
| } |
| |
| void vc_free(vc_t* vc) { |
| if (vc->fd >= 0) { |
| close(vc->fd); |
| } |
| free(vc->text_buf); |
| free(vc->scrollback_buf); |
| free(vc); |
| } |
| |
| void vc_flush(vc_t* vc) { |
| if (g_vc_owns_display && vc_graphics_enabled(vc) && vc->invy1 >= 0) { |
| int rows = vc_rows(vc); |
| // Adjust for the current viewport position. Convert |
| // console-relative row numbers to screen-relative row numbers. |
| int invalidate_y0 = MIN(vc->invy0 - vc->viewport_y, rows); |
| int invalidate_y1 = MIN(vc->invy1 - vc->viewport_y, rows); |
| vc_gfx_invalidate(vc->graphics, vc, 0, invalidate_y0, vc->columns, |
| invalidate_y1 - invalidate_y0); |
| } |
| } |
| |
| void vc_flush_all(vc_t* vc) { |
| if (g_vc_owns_display && vc_graphics_enabled(vc)) { |
| vc_gfx_invalidate_all(vc->graphics, vc); |
| } |
| } |
| |
| void vc_device_init(const gfx_font* font, const keychar_t* keymap) { |
| g_font = font; |
| g_keymap = keymap; |
| } |