console: rewrite buffer implementation

We now use a proper cache for the current screen and a linked list for the
scrollback buffer. This allows fast rotations and fast access.

Signed-off-by: David Herrmann <dh.herrmann@googlemail.com>
This commit is contained in:
David Herrmann 2011-12-21 14:19:52 +01:00
parent f7eca00957
commit 994aef69fc
2 changed files with 349 additions and 124 deletions

View File

@ -68,14 +68,24 @@ int kmscon_font_draw(struct kmscon_font *font, const struct kmscon_char *ch,
/* console buffer with cell objects */
int kmscon_buffer_new(struct kmscon_buffer **out, uint32_t x, uint32_t y);
int kmscon_buffer_new(struct kmscon_buffer **out, unsigned int x,
unsigned int y);
void kmscon_buffer_ref(struct kmscon_buffer *buf);
void kmscon_buffer_unref(struct kmscon_buffer *buf);
int kmscon_buffer_resize(struct kmscon_buffer *buf, uint32_t x, uint32_t y);
int kmscon_buffer_resize(struct kmscon_buffer *buf, unsigned int x,
unsigned int y);
void kmscon_buffer_draw(struct kmscon_buffer *buf, struct kmscon_font *font,
void *dcr, unsigned int width, unsigned int height);
unsigned int kmscon_buffer_get_width(struct kmscon_buffer *buf);
unsigned int kmscon_buffer_get_height(struct kmscon_buffer *buf);
void kmscon_buffer_write(struct kmscon_buffer *buf, unsigned int x,
unsigned int y, const struct kmscon_char *ch);
void kmscon_buffer_read(struct kmscon_buffer *buf, unsigned int x,
unsigned int y, struct kmscon_char *ch);
void kmscon_buffer_rotate(struct kmscon_buffer *buf);
/* console objects */
int kmscon_console_new(struct kmscon_console **out);

View File

@ -26,15 +26,48 @@
/*
* Console Buffer and Cell Objects
* A console buffer contains all the characters that are printed to the screen
* and the whole scrollback buffer. It has a fixed height and width measured in
* characters which can be changed on the fly.
* The buffer is a linked list of lines. The tail of the list is the current
* screen buffer which can be modified by the application. The rest of the list
* is the scrollback-buffer.
* The linked-list allows fast rotations but prevents fast access. Therefore,
* modifications of the scrollback-buffer is prohibited.
* The current screen position can be any line of the scrollback-buffer.
* A console buffer maintains an array of the lines of the current screen buffer
* and a list of lines in the scrollback buffer. The screen buffer can be
* modified, the scrollback buffer is constant.
*
* Current buffer:
* The current buffer is an array of lines. These lines can be modified by the
* application and represent the screen. They are also normally displayed on the
* screen if the user is not looking through the scrollback buffer currently.
* The buffer may have empty lines. Those are simply NULL pointers. If the
* buffer is not filled, yet, all lines following the first NULL line will also
* be NULL.
* When pushing new lines we simply find the first NULL line and add a new line
* there. If no NULL line is available (this is almost always the case) we
* simply push the first line into the scrollback buffer and move every line one
* line upwards. The new line will be added at the tail of the array.
* The current buffer can be modified and accessed by the caller with x/y
* coordinates. Modifications and access is supposed to be fast. Rotations are
* kind of slow, though.
*
* Scrollback buffer:
* The scrollback buffer contains all lines that were pushed out of the current
* screen. It's a linked list of lines which cannot be accessed by the
* application. It has an upper bound so we do not consume too much memory.
* If the current buffer is resized to a bigger size, then lines from the
* scrollback buffer may get back into the current buffer to fill the screen.
*
* Lines:
* A single line is represented by a "struct line". It has an array of cells
* which can be accessed directly. The length of each line may vary and for
* faster resizing we also keep a \size member.
* A line may be shorter than the current buffer width. We do not resize them to
* speed up the buffer operations. If a line is printed which is longer or
* shorted than the screen width, it is simply filled with spaces or truncated
* to screen width.
* If such a line is accessed outside the bounds, the line is resized to the
* current screen width to allow access.
*
* Screen position:
* The current screen position may be any line inside the scrollback buffer. If
* it is NULL, the current position is set to the current screen buffer.
* If it is non-NULL it will stick to the given line and will not scroll back on
* new input.
*
* Cells
* A single cell describes a single character that is printed in that cell. The
@ -72,15 +105,17 @@ struct line {
struct kmscon_buffer {
unsigned long ref;
unsigned int count;
struct line *first;
struct line *last;
unsigned int max_scrollback;
unsigned int sb_count;
struct line *sb_first;
struct line *sb_last;
unsigned int sb_max;
struct line *position;
unsigned int size_x;
unsigned int size_y;
struct line *current;
bool stick;
unsigned int fill;
struct line **current;
};
static void destroy_cell(struct cell *cell)
@ -169,65 +204,8 @@ static int resize_line(struct line *line, unsigned int width)
return 0;
}
/*
* Allocates a new line of width \width and pushes it to the tail of the buffer.
* The line is filled with blanks. If the maximum number of lines is already
* reached, the first line is removed and pushed to the tail.
*/
static int push_line(struct kmscon_buffer *buf, unsigned int width)
{
struct line *line;
int ret;
if (!buf)
return -EINVAL;
if (buf->count > (buf->size_y + buf->max_scrollback)) {
line = buf->first;
buf->first = line->next;
if (buf->first)
buf->first->prev = NULL;
if (buf->current == line)
buf->current = buf->first;
line->next = NULL;
line->prev = NULL;
--buf->count;
} else {
ret = new_line(&line);
if (ret)
return ret;
}
ret = resize_line(line, width);
if (ret)
goto err_free;
if (buf->last) {
line->prev = buf->last;
buf->last->next = line;
buf->last = line;
} else {
buf->first = line;
buf->last = line;
}
++buf->count;
if (!buf->current)
buf->current = buf->first;
else if (!buf->stick && buf->count > buf->size_y)
buf->current = buf->current->next;
return 0;
err_free:
free_line(line);
return ret;
}
int kmscon_buffer_new(struct kmscon_buffer **out, uint32_t x, uint32_t y)
int kmscon_buffer_new(struct kmscon_buffer **out, unsigned int x,
unsigned int y)
{
struct kmscon_buffer *buf;
int ret;
@ -239,9 +217,12 @@ int kmscon_buffer_new(struct kmscon_buffer **out, uint32_t x, uint32_t y)
if (!buf)
return -ENOMEM;
log_debug("console: new buffer with size %ux%u\n", buf->size_x,
buf->size_y);
memset(buf, 0, sizeof(*buf));
buf->ref = 1;
buf->max_scrollback = DEFAULT_SCROLLBACK;
buf->sb_max = DEFAULT_SCROLLBACK;
ret = kmscon_buffer_resize(buf, x, y);
if (ret)
@ -266,6 +247,7 @@ void kmscon_buffer_ref(struct kmscon_buffer *buf)
void kmscon_buffer_unref(struct kmscon_buffer *buf)
{
struct line *iter, *tmp;
unsigned int i;
if (!buf || !buf->ref)
return;
@ -273,31 +255,92 @@ void kmscon_buffer_unref(struct kmscon_buffer *buf)
if (--buf->ref)
return;
for (iter = buf->first; iter; ) {
log_debug("console: destroying buffer object\n");
for (iter = buf->sb_first; iter; ) {
tmp = iter;
iter = iter->next;
free_line(tmp);
}
for (i = 0; i < buf->size_y; ++i)
free_line(buf->current[i]);
free(buf->current);
free(buf);
}
/*
* Resize the current console buffer
* This adjusts the x/y size of the viewable part of the buffer. It does never
* modify the scroll-back buffer as this would take too long.
*
* x-resize:
* We only resize the visible lines to have at least width \x. If resizing fails
* we leave the buffer in the current state. This may make some lines shorter
* than others but there is currently no better way to handle memory failures
* here.
* This links the given line into the scrollback-buffer. This always succeeds.
*/
int kmscon_buffer_resize(struct kmscon_buffer *buf, uint32_t x, uint32_t y)
static void link_to_scrollback(struct kmscon_buffer *buf, struct line *line)
{
unsigned int i;
struct line *iter;
int ret;
struct line *tmp;
if (!buf || !line)
return;
if (buf->sb_count >= buf->sb_max) {
tmp = buf->sb_first;
buf->sb_first = tmp->next;
if (tmp->next)
tmp->next->prev = NULL;
else
buf->sb_last = NULL;
buf->sb_count--;
if (buf->position == tmp)
buf->position = line;
free_line(tmp);
}
line->next = NULL;
line->prev = buf->sb_last;
if (buf->sb_last)
buf->sb_last->next = line;
else
buf->sb_first = line;
buf->sb_last = line;
buf->sb_count++;
}
/*
* Unlinks last line from the scrollback buffer. Returns NULL if it is empty.
*/
static struct line *get_from_scrollback(struct kmscon_buffer *buf)
{
struct line *line;
if (!buf || !buf->sb_last)
return NULL;
line = buf->sb_last;
buf->sb_last = line->prev;
if (line->prev)
line->prev->next = NULL;
else
buf->sb_first = NULL;
buf->sb_count--;
if (buf->position == line)
buf->position = NULL;
line->next = NULL;
line->prev = NULL;
return line;
}
/*
* Resize the current console buffer
* This resizes the current buffer. We do not resize the lines or modify them in
* any way. This would take too long if multiple resize-operations are
* performed.
*/
int kmscon_buffer_resize(struct kmscon_buffer *buf, unsigned int x,
unsigned int y)
{
unsigned int i, fill;
struct line *iter, **cache;
if (!buf)
return -EINVAL;
@ -307,37 +350,74 @@ int kmscon_buffer_resize(struct kmscon_buffer *buf, uint32_t x, uint32_t y)
if (!y)
y = DEFAULT_HEIGHT;
/* push at least one line into the buffer so we have \current */
if (!buf->count) {
ret = push_line(buf, x);
if (ret)
return ret;
}
/* Resize y size by adjusting the current buffer size */
if (y < buf->size_y) {
/*
* Shrink current buffer. First move enough elements from the
* current buffer into the scroll-back buffer so we can shrink
* it without loosing data.
* Then reallocate the buffer (we shrink it so we never fail
* here) and correctly set values in \buf. If the buffer has
* empty lines, we can shrink it down without moving lines into
* the scrollback-buffer so first calculate the current fill of
* the buffer and then move appropriate amount of elements to
* the scrollback buffer.
*/
if (buf->size_x != x) {
iter = buf->last;
for (i = 0; i < buf->size_y; ++i) {
ret = resize_line(iter, x);
if (ret)
return ret;
iter = iter->prev;
if (buf->fill > y) {
for (i = y; i < buf->fill; ++i)
link_to_scrollback(buf, buf->current[i - y]);
memmove(buf->current, &buf->current[buf->fill - y],
y * sizeof(struct line*));
}
}
buf->size_x = x;
buf->size_y = y;
buf->current = realloc(buf->current, y * sizeof(struct line*));
buf->size_y = y;
if (buf->fill > y)
buf->fill = y;
} else if (y > buf->size_y) {
/*
* Increase current buffer to new size. Reset all new elements
* to NULL so they are empty. Copy existing buffer into new
* buffer and correctly set values in \buf.
* If we have more space in the buffer, we simply move lines
* from the scroll-back buffer into our current buffer if they
* are available. Otherwise, we simply add NULL lines.
*/
/* correctly move \current to new top position */
if (!buf->stick) {
buf->current = buf->last;
for (i = 1; i < buf->size_y; ++i) {
if (!buf->current->prev)
cache = malloc(sizeof(struct line*) * y);
if (!cache)
return -ENOMEM;
memset(cache, 0, sizeof(struct line*) * y);
fill = y - buf->size_y;
for (i = 0; i < fill; ++i) {
iter = get_from_scrollback(buf);
if (!iter)
break;
buf->current = buf->current->prev;
cache[y - i - 1] = iter;
}
buf->fill += i;
memmove(cache, &cache[y - i], i * sizeof(struct line*));
if (buf->size_y) {
memcpy(&cache[i], buf->current,
sizeof(struct line*) * buf->size_y);
free(buf->current);
}
buf->current = cache;
buf->size_y = y;
}
/* Adjust x size by simply setting the new value */
buf->size_x = x;
log_debug("console: resize buffer to %ux%u\n", x, y);
return 0;
}
@ -346,10 +426,9 @@ void kmscon_buffer_draw(struct kmscon_buffer *buf, struct kmscon_font *font,
{
cairo_t *cr;
double xs, ys, cx, cy;
unsigned int i, j;
struct line *iter;
unsigned int i, j, k, num;
struct line *iter, *line;
struct cell *cell;
struct kmscon_char *ch;
if (!buf || !font || !dcr)
return;
@ -361,22 +440,158 @@ void kmscon_buffer_draw(struct kmscon_buffer *buf, struct kmscon_font *font,
xs = width / (double)buf->size_x;
ys = height / (double)buf->size_y;
iter = buf->current;
iter = buf->position;
k = 0;
cy = 0;
for (i = 0; i < buf->size_y; ++i) {
if (!iter)
cx = 0;
if (iter) {
line = iter;
iter = iter->next;
} else {
line = buf->current[k];
k++;
}
if (!line)
break;
cx = 0;
for (j = 0; j < iter->num; ++j) {
cell = &iter->cells[j];
ch = cell->ch;
if (line->num < buf->size_x)
num = line->num;
else
num = buf->size_x;
kmscon_font_draw(font, ch, cr, cx, cy);
for (j = 0; j < num; ++j) {
cell = &line->cells[j];
kmscon_font_draw(font, cell->ch, cr, cx, cy);
cx += xs;
}
cy += ys;
iter = iter->next;
}
}
unsigned int kmscon_buffer_get_width(struct kmscon_buffer *buf)
{
if (!buf)
return 0;
return buf->size_x;
}
unsigned int kmscon_buffer_get_height(struct kmscon_buffer *buf)
{
if (!buf)
return 0;
return buf->size_y;
}
void kmscon_buffer_write(struct kmscon_buffer *buf, unsigned int x,
unsigned int y, const struct kmscon_char *ch)
{
struct line *line;
int ret;
if (!buf || !ch)
return;
if (x >= buf->size_x || y >= buf->size_y) {
log_warning("console: writing beyond buffer boundary\n");
return;
}
if (!buf->current[y]) {
while (buf->fill <= y) {
ret = new_line(&line);
if (ret) {
log_warning("console: cannot allocate line "
"(%d); dropping input\n", ret);
return;
}
buf->current[buf->fill] = line;
buf->fill++;
}
}
line = buf->current[y];
if (x >= line->num) {
ret = resize_line(line, buf->size_x);
if (ret) {
log_warning("console: cannot resize line (%d); "
"dropping input\n", ret);
return;
}
}
ret = kmscon_char_set(line->cells[x].ch, ch);
if (ret) {
log_warning("console: cannot copy character (%d); "
"dropping input\n", ret);
return;
}
}
void kmscon_buffer_read(struct kmscon_buffer *buf, unsigned int x,
unsigned int y, struct kmscon_char *ch)
{
struct line *line;
if (!ch)
return;
if (!buf)
goto err_out;
if (x >= buf->size_x || y >= buf->size_y) {
log_warning("console: reading out of buffer bounds\n");
goto err_out;
}
line = buf->current[y];
if (!line)
goto err_out;
if (x >= line->num)
goto err_out;
kmscon_char_set(ch, line->cells[x].ch);
return;
err_out:
kmscon_char_reset(ch);
}
void kmscon_buffer_rotate(struct kmscon_buffer *buf)
{
struct line *line, *nl;
int ret;
if (!buf)
return;
ret = new_line(&nl);
if (ret) {
log_warning("console: cannot allocate line (%d); "
"dropping input\n", ret);
return;
}
if (buf->fill >= buf->size_y) {
line = buf->current[0];
if (!line)
return;
link_to_scrollback(buf, line);
memmove(buf->current, &buf->current[1],
(buf->size_y - 1) * sizeof(struct line*));
buf->current[buf->size_y - 1] = NULL;
buf->fill--;
}
buf->current[buf->fill] = nl;
buf->fill++;
}