在前面的實作中,我們雖然已經完成了螢幕設備的 API 介面與記憶體寫入行為的監控,但並沒有模擬一個真正的畫面,有的也只是在終端做文字輸出。
為了能夠真實輸出畫面,我們將使用 SDL2 把 frame buffer 畫出來。
而由於文字模式下向 0xB8000 寫入的東西其實是屬性 + 字元,並非單純像素,所以我們這裡會做查表並將自符打印到 SDL2 開的視窗中。
下方這個 screen_text_sdl_create 是整個顯示的建構器。它接收畫面尺寸與縮放倍數,建立一個視窗與渲染器 renderer,以及一張可被我們每幀以 CPU 更新的 streaming texture(ARGB8888)。為了保持像素銳利、不讓字糊掉,renderer 的設定為畫面原始像素,視窗只用 scale 去放大顯示。
struct screen_text_sdl *screen_text_sdl_create(size_t pixel_width,
size_t pixel_height,
int scale,
screen_text_input_cb input_cb,
void *opaque)
{
if (pixel_width == 0 || pixel_height == 0)
return NULL;
if (!screen_text_sdl_init_video())
return NULL;
struct screen_text_sdl *ui = calloc(1, sizeof(*ui));
if (!ui)
return NULL;
ui->tex_width = (int)pixel_width;
ui->tex_height = (int)pixel_height;
ui->scale = (scale > 0) ? scale : 1;
ui->input_cb = input_cb;
ui->input_opaque = opaque;
ui->window = SDL_CreateWindow("KVM Screen",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
ui->tex_width * ui->scale,
ui->tex_height * ui->scale,
SDL_WINDOW_SHOWN);
if (!ui->window)
goto fail;
ui->renderer = SDL_CreateRenderer(ui->window, -1, SDL_RENDERER_SOFTWARE);
if (!ui->renderer)
goto fail;
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest");
SDL_RenderSetLogicalSize(ui->renderer, ui->tex_width, ui->tex_height);
SDL_SetRenderDrawColor(ui->renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(ui->renderer);
SDL_RenderPresent(ui->renderer);
ui->texture = SDL_CreateTexture(ui->renderer,
SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_STREAMING,
ui->tex_width,
ui->tex_height);
if (!ui->texture)
goto fail;
SDL_StartTextInput();
ui->active = true;
return ui;
fail:
screen_text_sdl_destroy(ui);
return NULL;
}
BIOS 的文字模式不是單除的像素,而是一個紀錄字元的陣列。每格為 16 bit(低 8 位是 ASCII, 高 8 位是屬性)。要畫到 SDL 視窗上,我們要先把它轉成一塊真正的像素陣列。
在這裡我們準備一個 8×8 的字型表(font8x8),為了讓字更好讀,我們把每一列垂直複製一次,從 8 行拉成 16 行(8×16)。並使用一個固定的 VGA 調色盤,逐像素把結果寫進 ARGB8888 的 pixel_buffer。
概念上,對於一個 rows × cols 的文字畫面,占用像素大小就是:
當我們有了 pixel_buffer 後可直接將這塊 ARGB8888 的資料塞入 ui->texture,並將整張 texture 貼到當前 renderer 的畫面上,最後調用 Present 做會直接顯示。
int screen_text_sdl_update(struct screen_text_sdl *ui,
const uint32_t *pixels,
size_t width,
size_t height)
{
if (!ui || !pixels || width == 0 || height == 0)
return -1;
if (!ui->active)
return -1;
if ((int)width != ui->tex_width || (int)height != ui->tex_height)
return -1;
if (SDL_UpdateTexture(ui->texture, NULL, pixels,
ui->tex_width * (int)sizeof(uint32_t)) != 0) {
fprintf(stderr, "SDL_UpdateTexture error: %s\n", SDL_GetError());
return -1;
}
if (SDL_RenderClear(ui->renderer) != 0) {
fprintf(stderr, "SDL_RenderClear error: %s\n", SDL_GetError());
return -1;
}
if (SDL_RenderCopy(ui->renderer, ui->texture, NULL, NULL) != 0) {
fprintf(stderr, "SDL_RenderCopy error: %s\n", SDL_GetError());
return -1;
}
SDL_RenderPresent(ui->renderer);
return 0;
}
另外這裡做的是整張更新而非分區更新,這是因為文字模式下的緩衝區大小為 80 * 25 (~4KB),恰好接近一個 page。