1// Copyright 2016 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include <assert.h>
6#include <fcntl.h>
7#include <stdlib.h>
8#include <string.h>
9#include <sys/param.h>
10
11#include <zircon/process.h>
12#include <zircon/syscalls.h>
13
14#include <fbl/auto_lock.h>
15
16#include "vc.h"
17
18static uint32_t default_palette[] = {
19    // 0-7 Normal/dark versions of colors
20    0xff000000, // black
21    0xffaa0000, // red
22    0xff00aa00, // green
23    0xffaa5500, // brown
24    0xff0000aa, // blue
25    0xffaa00aa, // zircon
26    0xff00aaaa, // cyan
27    0xffaaaaaa, // grey
28    // 8-15 Bright/light versions of colors
29    0xff555555, // dark grey
30    0xffff5555, // bright red
31    0xff55ff55, // bright green
32    0xffffff55, // yellow
33    0xff5555ff, // bright blue
34    0xffff55ff, // bright zircon
35    0xff55ffff, // bright cyan
36    0xffffffff, // white
37};
38
39#define DEFAULT_FRONT_COLOR 0x0 // black
40#define DEFAULT_BACK_COLOR 0xf  // white
41
42#define SPECIAL_FRONT_COLOR 0xf // white
43#define SPECIAL_BACK_COLOR 0x4  // blue
44
45// Default height/width (in px) of console before any displays are
46// attached, since we need somewhere to put any data that is recieved.
47#define DEFAULT_WIDTH 1024
48#define DEFAULT_HEIGHT 768
49#define SCROLLBACK_ROWS 1024 // TODO make configurable
50
51#define ABS(val) (((val) >= 0) ? (val) : -(val))
52
53// shared with vc-gfx.cpp
54extern gfx_surface* vc_gfx;
55extern gfx_surface* vc_tb_gfx;
56extern const gfx_font* vc_font;
57
58static zx_status_t vc_setup(vc_t* vc, bool special) {
59    // calculate how many rows/columns we have
60    vc->rows = DEFAULT_HEIGHT / vc->charh;
61    vc->columns = DEFAULT_WIDTH / vc->charw;
62    vc->scrollback_rows_max = SCROLLBACK_ROWS;
63    vc->scrollback_rows_count = 0;
64    vc->scrollback_offset = 0;
65
66    // allocate the text buffer
67    vc->text_buf = reinterpret_cast<vc_char_t*>(
68        calloc(1, vc->rows * vc->columns * sizeof(vc_char_t)));
69    if (!vc->text_buf)
70        return ZX_ERR_NO_MEMORY;
71
72    // allocate the scrollback buffer
73    vc->scrollback_buf = reinterpret_cast<vc_char_t*>(
74        calloc(1, vc->scrollback_rows_max * vc->columns * sizeof(vc_char_t)));
75    if (!vc->scrollback_buf) {
76        free(vc->text_buf);
77        return ZX_ERR_NO_MEMORY;
78    }
79
80    // set up the default palette
81    memcpy(&vc->palette, default_palette, sizeof(default_palette));
82    if (special) {
83        vc->front_color = SPECIAL_FRONT_COLOR;
84        vc->back_color = SPECIAL_BACK_COLOR;
85    } else {
86        vc->front_color = DEFAULT_FRONT_COLOR;
87        vc->back_color = DEFAULT_BACK_COLOR;
88    }
89
90    return ZX_OK;
91}
92
93static void vc_invalidate(void* cookie, int x0, int y0, int w, int h) {
94    vc_t* vc = reinterpret_cast<vc_t*>(cookie);
95
96    if (!g_vc_owns_display || !vc->active || !vc_gfx) {
97        return;
98    }
99
100    assert(h >= 0);
101    int y1 = y0 + h;
102    assert(y0 <= static_cast<int>(vc->rows));
103    assert(y1 <= static_cast<int>(vc->rows));
104
105    // Clip the y range so that we don't unnecessarily draw characters
106    // outside the visible range, and so that we don't draw characters into
107    // the bottom margin.
108    int visible_y0 = vc->viewport_y;
109    int visible_y1 = vc->viewport_y + vc_rows(vc);
110    y0 = MAX(y0, visible_y0);
111    y1 = MIN(y1, visible_y1);
112
113    for (int y = y0; y < y1; y++) {
114        if (y < 0) {
115            // Scrollback row.
116            vc_char_t* row = vc_get_scrollback_line_ptr(
117                vc, y + vc->scrollback_rows_count);
118            for (int x = x0; x < x0 + w; x++) {
119                vc_gfx_draw_char(vc, row[x], x, y - vc->viewport_y,
120                                 /* invert= */ false);
121            }
122        } else {
123            // Row in the main console region (non-scrollback).
124            vc_char_t* row = &vc->text_buf[y * vc->columns];
125            for (int x = x0; x < x0 + w; x++) {
126                // Check whether we should display the cursor at this
127                // position.  Note that it's possible that the cursor is
128                // outside the display area (vc->cursor_x ==
129                // vc->columns).  In that case, we won't display the
130                // cursor, even if there's a margin.  This matches
131                // gnome-terminal.
132                bool invert = (!vc->hide_cursor &&
133                               static_cast<unsigned>(x) == vc->cursor_x &&
134                               static_cast<unsigned>(y) == vc->cursor_y);
135                vc_gfx_draw_char(vc, row[x], x, y - vc->viewport_y, invert);
136            }
137        }
138    }
139}
140
141// implement tc callbacks:
142
143static inline void vc_invalidate_lines(vc_t* vc, int y, int h) {
144    if (y < vc->invy0) {
145        vc->invy0 = y;
146    }
147    y += h;
148    if (y > vc->invy1) {
149        vc->invy1 = y;
150    }
151}
152
153static void vc_tc_invalidate(void* cookie, int x0, int y0, int w, int h){
154    vc_t* vc = reinterpret_cast<vc_t*>(cookie);
155    vc_invalidate(cookie, x0, y0, w, h);
156    vc_invalidate_lines(vc, y0, h);
157}
158
159static void vc_tc_movecursor(void* cookie, int x, int y) {
160    vc_t* vc = reinterpret_cast<vc_t*>(cookie);
161    unsigned old_x = vc->cursor_x;
162    unsigned old_y = vc->cursor_y;
163    vc->cursor_x = x;
164    vc->cursor_y = y;
165    if (g_vc_owns_display && vc->active && !vc->hide_cursor) {
166        // Clear the cursor from its old position.
167        vc_invalidate(cookie, old_x, old_y, 1, 1);
168        vc_invalidate_lines(vc, old_y, 1);
169
170        // Display the cursor in its new position.
171        vc_invalidate(cookie, vc->cursor_x, vc->cursor_y, 1, 1);
172        vc_invalidate_lines(vc, vc->cursor_y, 1);
173    }
174}
175
176static void vc_tc_scrollback_buffer_push(vc_t* vc, vc_char_t* src) {
177    unsigned dest_row;
178    assert(vc->scrollback_rows_count <= vc->scrollback_rows_max);
179    if (vc->scrollback_rows_count < vc->scrollback_rows_max) {
180        // Add a row without dropping any existing rows.
181        assert(vc->scrollback_offset == 0);
182        dest_row = vc->scrollback_rows_count++;
183    } else {
184        // Add a row and drop an existing row.
185        assert(vc->scrollback_offset < vc->scrollback_rows_max);
186        dest_row = vc->scrollback_offset++;
187        if (vc->scrollback_offset == vc->scrollback_rows_max)
188            vc->scrollback_offset = 0;
189    }
190    vc_char_t* dst = &vc->scrollback_buf[dest_row * vc->columns];
191    memcpy(dst, src, vc->columns * sizeof(vc_char_t));
192}
193
194static void vc_tc_push_scrollback_line(void* cookie, int y) {
195    vc_t* vc = reinterpret_cast<vc_t*>(cookie);
196    vc_char_t* src = &vc->text_buf[y * vc->columns];
197    vc_tc_scrollback_buffer_push(vc, src);
198
199    // If we're displaying only the main console region (and no
200    // scrollback), then keep displaying that (i.e. don't modify
201    // viewport_y).
202    if (vc->viewport_y < 0) {
203        // We are displaying some of the scrollback buffer.
204        if (vc->viewport_y > -static_cast<int>(vc->scrollback_rows_max)) {
205            // Scroll the viewport to continue displaying the same point in
206            // the scrollback buffer.
207            --vc->viewport_y;
208        } else {
209            // We were displaying the line at the top of the scrollback
210            // buffer, but we dropped that line from the buffer.  We could
211            // leave the display as it was (which is what gnome-terminal
212            // does) and not scroll the display.  However, that causes
213            // problems.  If the user later scrolls down, we won't
214            // necessarily be able to display the lines below -- we might
215            // have dropped those too.  So, instead, let's scroll the
216            // display and remove the scrollback line that was lost.
217            //
218            // For simplicity, fall back to redrawing everything.
219            vc_invalidate(vc, 0, -vc->scrollback_rows_max,
220                                 vc->columns, vc_rows(vc));
221            vc_render(vc);
222        }
223    }
224}
225
226static void vc_set_cursor_hidden(vc_t* vc, bool hide) {
227    if (vc->hide_cursor == hide)
228        return;
229    vc->hide_cursor = hide;
230    if (g_vc_owns_display && vc->active) {
231        vc_invalidate(vc, vc->cursor_x, vc->cursor_y, 1, 1);
232        vc_invalidate_lines(vc, vc->cursor_y, 1);
233    }
234}
235
236static void vc_tc_copy_lines(void* cookie, int y_dest, int y_src, int line_count) {
237    vc_t* vc = reinterpret_cast<vc_t*>(cookie);
238
239    if (vc->viewport_y < 0) {
240        tc_copy_lines(&vc->textcon, y_dest, y_src, line_count);
241
242        // The viewport is scrolled.  For simplicity, fall back to
243        // redrawing all of the non-scrollback lines in this case.
244        int rows = vc_rows(vc);
245        vc_invalidate(vc, 0, 0, vc->columns, rows);
246        vc_invalidate_lines(vc, 0, rows);
247        return;
248    }
249
250    // Remove the cursor from the display before copying the lines on
251    // screen, otherwise we might be copying a rendering of the cursor to a
252    // position where the cursor isn't.  This must be done before the
253    // tc_copy_lines() call, otherwise we might render the wrong character.
254    bool old_hide_cursor = vc->hide_cursor;
255    if (g_vc_owns_display && vc->active) {
256        vc_set_cursor_hidden(vc, true);
257    }
258
259    // The next two calls can be done in any order.
260    tc_copy_lines(&vc->textcon, y_dest, y_src, line_count);
261
262    if (g_vc_owns_display && vc->active && vc_gfx) {
263        gfx_copyrect(vc_gfx, 0, y_src * vc->charh,
264                     vc_gfx->width, line_count * vc->charh,
265                     0, y_dest * vc->charh);
266
267        // Restore the cursor.
268        vc_set_cursor_hidden(vc, old_hide_cursor);
269
270        vc_status_update();
271        vc_gfx_invalidate_status();
272        vc_invalidate_lines(vc, 0, vc_rows(vc));
273    }
274}
275
276static void vc_tc_setparam(void* cookie, int param, uint8_t* arg, size_t arglen) {
277    vc_t* vc = reinterpret_cast<vc_t*>(cookie);
278    switch (param) {
279    case TC_SET_TITLE:
280        strncpy(vc->title, (char*)arg, sizeof(vc->title));
281        vc->title[sizeof(vc->title) - 1] = '\0';
282        vc_status_update();
283        if (g_vc_owns_display && vc_gfx) {
284            vc_gfx_invalidate_status();
285        }
286        break;
287    case TC_SHOW_CURSOR:
288        vc_set_cursor_hidden(vc, false);
289        break;
290    case TC_HIDE_CURSOR:
291        vc_set_cursor_hidden(vc, true);
292        break;
293    default:; // nothing
294    }
295}
296
297static void vc_clear_gfx(vc_t* vc) {
298    // Fill display with background color
299    if (g_vc_owns_display && vc->active && vc_gfx) {
300        gfx_fillrect(vc_gfx, 0, 0, vc_gfx->width, vc_gfx->height,
301                     palette_to_color(vc, vc->back_color));
302    }
303}
304
305static void vc_reset(vc_t* vc) {
306    // reset the cursor
307    vc->cursor_x = 0;
308    vc->cursor_y = 0;
309    // reset the viewport position
310    vc->viewport_y = 0;
311
312    tc_init(&vc->textcon, vc->columns, vc_rows(vc), vc->text_buf,
313            vc->front_color, vc->back_color, vc->cursor_x, vc->cursor_y);
314    vc->textcon.cookie = vc;
315    vc->textcon.invalidate = vc_tc_invalidate;
316    vc->textcon.movecursor = vc_tc_movecursor;
317    vc->textcon.push_scrollback_line = vc_tc_push_scrollback_line;
318    vc->textcon.copy_lines = vc_tc_copy_lines;
319    vc->textcon.setparam = vc_tc_setparam;
320
321    // fill textbuffer with blank characters
322    size_t count = vc->rows * vc->columns;
323    vc_char_t* ptr = vc->text_buf;
324    while (count--) {
325        *ptr++ = vc_char_make(' ', vc->front_color, vc->back_color);
326    }
327
328    vc_clear_gfx(vc);
329    if (vc_gfx) {
330        vc_gfx_invalidate_all(vc);
331    }
332}
333
334void vc_status_clear() {
335    if (g_vc_owns_display && vc_gfx) {
336        gfx_fillrect(vc_tb_gfx, 0, 0,
337                     vc_tb_gfx->width, vc_tb_gfx->height,
338                     default_palette[STATUS_COLOR_BG]);
339    }
340}
341
342void vc_status_commit() {
343    if (g_vc_owns_display && vc_gfx) {
344        vc_gfx_invalidate_status();
345    }
346}
347
348void vc_status_write(int x, unsigned color, const char* text) {
349    char c;
350    unsigned fg = default_palette[color];
351    unsigned bg = default_palette[STATUS_COLOR_BG];
352
353    if (g_vc_owns_display && vc_gfx) {
354        x *= vc_font->width;
355        while ((c = *text++) != 0) {
356            gfx_putchar(vc_tb_gfx, vc_font, c, x, 0, fg, bg);
357            x += vc_font->width;
358        }
359    }
360}
361
362void vc_render(vc_t* vc) {
363    if (g_vc_owns_display && vc->active && vc_gfx) {
364        vc_status_update();
365        vc_gfx_invalidate_all(vc);
366    }
367}
368
369void vc_full_repaint(vc_t* vc) {
370    if (g_vc_owns_display && vc_gfx) {
371        vc_clear_gfx(vc);
372        int scrollback_lines = vc_get_scrollback_lines(vc);
373        vc_invalidate(vc, 0, -scrollback_lines,
374                             vc->columns, scrollback_lines + vc->rows);
375    }
376}
377
378int vc_get_scrollback_lines(vc_t* vc) {
379    return vc->scrollback_rows_count;
380}
381
382vc_char_t* vc_get_scrollback_line_ptr(vc_t* vc, unsigned row) {
383    assert(row < vc->scrollback_rows_count);
384    row += vc->scrollback_offset;
385    if (row >= vc->scrollback_rows_max)
386        row -= vc->scrollback_rows_max;
387    return &vc->scrollback_buf[row * vc->columns];
388}
389
390static void vc_scroll_viewport_abs(vc_t* vc, int vpy) {
391    vpy = MIN(vpy, 0);
392    vpy = MAX(vpy, -vc_get_scrollback_lines(vc));
393    int diff = vpy - vc->viewport_y;
394    if (diff == 0)
395        return;
396    int diff_abs = ABS(diff);
397    vc->viewport_y = vpy;
398    int rows = vc_rows(vc);
399    if (!g_vc_owns_display || !vc->active || !vc_gfx) {
400        return;
401    }
402    if (diff_abs >= rows) {
403        // We are scrolling the viewport by a large delta.  Invalidate all
404        // of the visible area of the console.
405        vc_invalidate(vc, 0, vpy, vc->columns, rows);
406    } else {
407        if (diff > 0) {
408            gfx_copyrect(vc_gfx, 0, diff_abs * vc->charh,
409                         vc_gfx->width, (rows - diff_abs) * vc->charh, 0, 0);
410            vc_invalidate(vc, 0, vpy + rows - diff_abs, vc->columns,
411                                 diff_abs);
412        } else {
413            gfx_copyrect(vc_gfx, 0, 0, vc_gfx->width,
414                         (rows - diff_abs) * vc->charh, 0,
415                         diff_abs * vc->charh);
416            vc_invalidate(vc, 0, vpy, vc->columns, diff_abs);
417        }
418    }
419    vc_render(vc);
420}
421
422void vc_scroll_viewport(vc_t* vc, int dir) {
423    vc_scroll_viewport_abs(vc, vc->viewport_y + dir);
424}
425
426void vc_scroll_viewport_top(vc_t* vc) {
427    vc_scroll_viewport_abs(vc, INT_MIN);
428}
429
430void vc_scroll_viewport_bottom(vc_t* vc) {
431    vc_scroll_viewport_abs(vc, 0);
432}
433
434void vc_set_fullscreen(vc_t* vc, bool fullscreen) {
435    unsigned flags;
436    if (fullscreen) {
437        flags = vc->flags | VC_FLAG_FULLSCREEN;
438    } else {
439        flags = vc->flags & ~VC_FLAG_FULLSCREEN;
440    }
441    if (flags != vc->flags) {
442        vc->flags = flags;
443        tc_seth(&vc->textcon, vc_rows(vc));
444    }
445    vc_render(vc);
446}
447
448const gfx_font* vc_get_font() {
449    char* fname = getenv("virtcon.font");
450    if (fname) {
451        if (!strcmp(fname, "9x16")) {
452            return &font9x16;
453        } else if (!strcmp(fname, "18x32")) {
454            return &font18x32;
455        } else {
456            printf("gfxconsole: no such font '%s'\n", fname);
457        }
458    }
459    return &font9x16;
460}
461
462void vc_attach_gfx(vc_t* vc) {
463    // If the size of the new gfx console doesn't match what we had been
464    // attached to, we need to allocate new memory and copy the existing
465    // data over.
466    unsigned rows = vc_gfx->height / vc->charh;
467    unsigned columns = vc_gfx->width / vc->charw;
468    if (rows == vc->rows && columns == vc->columns) {
469        return;
470    }
471
472    // allocate the new buffers
473    vc_char_t* text_buf = reinterpret_cast<vc_char_t*>(
474        calloc(1, rows * columns * sizeof(vc_char_t)));
475    vc_char_t* scrollback_buf = reinterpret_cast<vc_char_t*>(
476        calloc(1, vc->scrollback_rows_max * columns * sizeof(vc_char_t)));
477    if (text_buf && scrollback_buf) {
478        // fill new text buffer with blank characters
479        size_t count = rows * columns;
480        vc_char_t* ptr = text_buf;
481        while (count--) {
482            *ptr++ = vc_char_make(' ', vc->front_color, vc->back_color);
483        }
484
485        // Copy the most recent data from the old console to the new one. There are
486        // (vc->cursor_y + 1) rows available, and we want (rows - (vc->rows - vc_rows(vc))
487        // rows. Subtract to get the first row index to copy.
488        unsigned old_i = MAX(
489                static_cast<int>((vc->cursor_y + 1) - (rows - (vc->rows - vc_rows(vc)))), 0);
490        unsigned old_data_start = old_i;
491        unsigned new_i = 0;
492        size_t len = (vc->columns < columns ? vc->columns : columns) * sizeof(vc_char_t);
493        while (new_i < rows && old_i <= vc->cursor_y) {
494            memcpy(text_buf + columns * (new_i++), vc->text_buf + vc->columns * (old_i++), len);
495        }
496
497        // copy the old scrollback buffer
498        for (int i = 0; i < SCROLLBACK_ROWS; i++) {
499            memcpy(scrollback_buf + columns * i, vc->scrollback_buf + vc->columns * i, len);
500        }
501
502        vc_char_t* old_text_buf = vc->text_buf;
503        unsigned old_columns = vc->columns;
504        free(vc->scrollback_buf);
505
506        vc->text_buf = text_buf;
507        vc->scrollback_buf = scrollback_buf;
508        vc->rows = rows;
509        vc->columns = columns;
510
511        // Push any data that fell off of text_buf. Use a temporary buffer of the
512        // right length to handle going to a wider console. Set it to ' 's before
513        // pushing, so we don't merge data from old rows.
514        if (old_data_start) {
515            vc_char_t buf[columns];
516            for (unsigned i = 0; i < old_data_start; i++) {
517                vc_char_t* ptr = buf;
518                while (ptr < buf + columns) {
519                    *ptr++ = vc_char_make(' ', vc->front_color, vc->back_color);
520                }
521                ptr = old_text_buf + i * old_columns;
522                memcpy(buf, ptr, len);
523
524                vc_tc_scrollback_buffer_push(vc, buf);
525            }
526        }
527
528        free(old_text_buf);
529    } else {
530        // If we failed to allocate new buffers, use the old ones as best we can
531        free(text_buf);
532        free(scrollback_buf);
533
534        vc->rows = MIN(vc->rows, rows);
535        vc->columns = MIN(vc->columns, columns);
536
537        printf("vc: buffer resize failed, reusing old buffers (%dx%d)\n", vc->rows, vc->columns);
538    }
539
540    vc->viewport_y = 0;
541    if (vc->cursor_x >= vc->columns) {
542        vc->cursor_x = vc->columns - 1;
543    }
544    if (static_cast<int>(vc->cursor_y) >= vc_rows(vc)) {
545        vc->cursor_y = vc_rows(vc) - 1;
546    }
547
548    tc_init(&vc->textcon, vc->columns, vc_rows(vc), vc->text_buf,
549            vc->front_color, vc->back_color, vc->cursor_x, vc->cursor_y);
550}
551
552zx_status_t vc_alloc(vc_t** out, bool special) {
553    vc_t* vc =
554        reinterpret_cast<vc_t*>(calloc(1, sizeof(vc_t)));
555    if (!vc) {
556        return ZX_ERR_NO_MEMORY;
557    }
558    vc->fd = -1;
559
560    vc->keymap = qwerty_map;
561    char* keys = getenv("virtcon.keymap");
562    if (keys) {
563        if (!strcmp(keys, "qwerty")) {
564            vc->keymap = qwerty_map;
565        } else if (!strcmp(keys, "dvorak")) {
566            vc->keymap = dvorak_map;
567        } else {
568            printf("gfxconsole: no such keymap '%s'\n", keys);
569        }
570    }
571
572    vc->font = vc_get_font();
573    vc->charw = vc->font->width;
574    vc->charh = vc->font->height;
575
576    zx_status_t status = vc_setup(vc, special);
577    if (status != ZX_OK) {
578        free(vc);
579        return status;
580    }
581
582    if (vc_gfx) {
583        vc_attach_gfx(vc);
584    }
585    vc_reset(vc);
586
587    *out = vc;
588    return ZX_OK;
589}
590
591void vc_free(vc_t* vc) {
592    if (vc->fd >= 0) {
593        close(vc->fd);
594    }
595    free(vc->text_buf);
596    free(vc->scrollback_buf);
597    free(vc);
598}
599
600void vc_flush(vc_t* vc) {
601    if (g_vc_owns_display && vc_gfx && vc->invy1 >= 0) {
602        int rows = vc_rows(vc);
603        // Adjust for the current viewport position.  Convert
604        // console-relative row numbers to screen-relative row numbers.
605        int invalidate_y0 = MIN(vc->invy0 - vc->viewport_y, rows);
606        int invalidate_y1 = MIN(vc->invy1 - vc->viewport_y, rows);
607        vc_gfx_invalidate(vc, 0, invalidate_y0,
608                          vc->columns, invalidate_y1 - invalidate_y0);
609    }
610}
611
612void vc_flush_all(vc_t* vc) {
613    if (g_vc_owns_display && vc_gfx) {
614        vc_gfx_invalidate_all(vc);
615    }
616}
617