Index: emu/all.h
===================================================================
--- emu/all.h	(revision e26a6322873655ebf2194f02b2dfb27a3f4ceeca)
+++ emu/all.h	(revision 67fecc358073769f00f225ecdf43fd63be6167c7)
@@ -69,4 +69,5 @@
 extern uint32_t vid_win;
 extern uint32_t ser_win;
+extern uint32_t lcd_win;
 
 extern void sdl_init(void);
@@ -113,4 +114,6 @@
 extern uint32_t lcd_read(uint32_t off, int32_t sz);
 extern void lcd_write(uint32_t off, int32_t sz, uint32_t val);
+
+extern void lcd_sdl(void);
 
 extern void ser_init(void);
@@ -162,3 +165,3 @@
 extern void kbd_write(uint32_t off, int32_t sz, uint32_t val);
 
-extern void kbd_key(SDL_KeyboardEvent *ev, bool dn);
+extern void kbd_key(SDL_KeyboardEvent *ev, bool vid, bool dn);
Index: emu/kbd.c
===================================================================
--- emu/kbd.c	(revision e26a6322873655ebf2194f02b2dfb27a3f4ceeca)
+++ emu/kbd.c	(revision 67fecc358073769f00f225ecdf43fd63be6167c7)
@@ -104,20 +104,20 @@
 }
 
+static void slid(int32_t sig, bool on, int32_t val)
+{
+	out((uint8_t)(0x80 | sig));
+	out(on ? 0x01 : 0x00);
+	out((uint8_t)val);
+}
+
 #if defined NOT_YET
-static void pot_128(int32_t sig, int32_t val)
+static void pot(int32_t sig, int32_t val)
 {
 	out((uint8_t)(0x80 | sig));
 	out((uint8_t)val);
 }
-
-static void pot_256(int32_t sig, int32_t val)
-{
-	out((uint8_t)(0x80 | sig));
-	out((uint8_t)((val >> 7) & 0x01));
-	out((uint8_t)(val & 0x7f));
-}
 #endif
 
-void kbd_key(SDL_KeyboardEvent *ev, bool dn)
+static void vid_key(SDL_KeyboardEvent *ev, bool dn)
 {
 	if ((ev->keysym.mod & KMOD_SHIFT) != 0 &&
@@ -173,4 +173,56 @@
 }
 
+static void lcd_key(SDL_KeyboardEvent *ev, bool dn)
+{
+	if ((ev->keysym.mod & KMOD_CTRL) != 0 &&
+			ev->keysym.sym >= SDLK_a && ev->keysym.sym <= SDLK_n) {
+		int32_t i = ev->keysym.sym - SDLK_a;
+		ver2("kbd lcd %d %s", i, dn ? "dn" : "up");
+
+		if (dn) {
+			but_on(39 + i);
+		}
+		else {
+			but_off(39 + i);
+		}
+
+		return;
+	}
+
+	if (ev->keysym.sym >= SDLK_a && ev->keysym.sym <= SDLK_n) {
+		static int32_t lev[14] = {
+			64, 0, 0, 0, 0, 64, 64, 64, 64, 0, 0, 0, 64, 0
+		};
+
+		int32_t i = ev->keysym.sym - SDLK_a;
+		int32_t val = lev[i];
+
+		if (!dn) {
+			if ((ev->keysym.mod & KMOD_SHIFT) != 0) {
+				val = val > 10 ? val - 10 : 0;
+			}
+			else {
+				val = val < 117 ? val + 10 : 127;
+			}
+		}
+
+		ver2("kbd sli %d %s %d", i, dn ? "dn" : "up", val);
+		slid(25 + i, dn, val);
+
+		lev[i] = val;
+		return;
+	}
+}
+
+void kbd_key(SDL_KeyboardEvent *ev, bool vid, bool dn)
+{
+	if (vid) {
+		vid_key(ev, dn);
+	}
+	else {
+		lcd_key(ev, dn);
+	}
+}
+
 void kbd_init(void)
 {
Index: emu/lcd.c
===================================================================
--- emu/lcd.c	(revision e26a6322873655ebf2194f02b2dfb27a3f4ceeca)
+++ emu/lcd.c	(revision 67fecc358073769f00f225ecdf43fd63be6167c7)
@@ -24,7 +24,241 @@
 int32_t lcd_verbose = 0;
 
+#define WIN_W (1615 * 2 / 3)
+#define WIN_H (304 * 2 / 3)
+
+#define GFX_BGR 0x00000000
+#define GFX_FGR 0xFFFFFFFF
+
+#define TXT_BGR 0x000000FF
+#define TXT_FGR ((SDL_Color){ .r = 255, .b = 255, .g = 255, .a = 255 })
+
+#define REG_ARG 0
+#define REG_COM 1
+
+#define COM_NONE	0x00
+#define	COM_MWRITE	0x42
+#define	COM_MREAD	0x43
+#define	COM_CRSWR	0x46
+#define	COM_CRSMRT	0x4C
+#define	COM_CRSMUP	0x4E
+#define	COM_CRSMDN	0x4F
+#define	COM_DSPOFF	0x58
+#define	COM_DSPON	0x59
+
+#define TXT_W 85
+#define TXT_H 8
+
+#define GFX_W 85
+#define GFX_H 64
+#define GFX_PIX 6
+
+#define BASE_TXT 0x0000
+#define BASE_GFX 0x2000
+
+#define DIR_UP -85
+#define DIR_DOWN 85
+#define DIR_RIGHT 1
+
+static uint8_t mem_txt[TXT_H * TXT_W];
+static uint8_t mem_gfx[GFX_H * GFX_W];
+
+static SDL_Window *win;
+uint32_t lcd_win;
+
+static SDL_Renderer *ren;
+static SDL_atomic_t frame;
+static SDL_atomic_t ena;
+
+static TTF_Font *fon;
+static int32_t fon_w, fon_h;
+
+static int32_t txt_w, txt_h;
+static SDL_Surface *txt;
+static SDL_Texture *gfx;
+
+static int32_t com;
+static int32_t n_arg;
+static int32_t cur = BASE_TXT;
+static int32_t dir = DIR_RIGHT;
+
+void lcd_sdl(void)
+{
+	ver3("lcd_sdl()");
+
+	static int32_t last = 0;
+	int32_t now = SDL_AtomicGet(&frame);
+
+	if (last == now) {
+		ver3("no update");
+		return;
+	}
+
+	last = now;
+
+	if (SDL_FillRect(txt, NULL, TXT_BGR) < 0) {
+		fail("SDL_FillRect() failed: %s", SDL_GetError());
+	}
+
+	if (SDL_AtomicGet(&ena) == 0) {
+		SDL_Texture *tex = SDL_CreateTextureFromSurface(ren, txt);
+
+		if (tex == NULL) {
+			fail("SDL_CreateTextureFromSurface() failed: %s", SDL_GetError());
+		}
+
+		if (SDL_RenderCopy(ren, tex, NULL, NULL) < 0) {
+			fail("SDL_RenderCopy() failed: %s", SDL_GetError());
+		}
+
+		SDL_DestroyTexture(tex);
+		SDL_RenderPresent(ren);
+		return;
+	}
+
+	for (int32_t y = 0; y < TXT_H; ++y) {
+		char line[TXT_W + 1];
+		line[TXT_W] = 0;
+
+		if (SDL_LockMutex(cpu_mutex) < 0) {
+			fail("SDL_LockMutex() failed: %s", SDL_GetError());
+		}
+
+		memcpy(line, mem_txt + y * TXT_W, TXT_W);
+
+		if (SDL_UnlockMutex(cpu_mutex) < 0) {
+			fail("SDL_UnlockMutex() failed: %s", SDL_GetError());
+		}
+
+		for (int32_t x = 0; x < TXT_W; ++x) {
+			if (line[x] == 0x00) {
+				line[x] = ' ';
+			}
+		}
+
+		SDL_Surface *lin = TTF_RenderText_Blended(fon, line, TXT_FGR);
+
+		if (lin == NULL) {
+			fail("TTF_RenderText_Blended() failed: %s", TTF_GetError());
+		}
+
+		if (SDL_BlitSurface(lin, NULL, txt, &(SDL_Rect){
+			.x = 0,
+			.y = y * fon_h,
+			.w = TXT_W * fon_w,
+			.h = fon_h
+		})) {
+			fail("SDL_BlitSurface() failed: %s", SDL_GetError());
+		}
+
+		SDL_FreeSurface(lin);
+	}
+
+	SDL_Texture *tex = SDL_CreateTextureFromSurface(ren, txt);
+
+	if (tex == NULL) {
+		fail("SDL_CreateTextureFromSurface() failed: %s", SDL_GetError());
+	}
+
+	if (SDL_RenderCopy(ren, tex, NULL, NULL) < 0) {
+		fail("SDL_RenderCopy() failed: %s", SDL_GetError());
+	}
+
+	SDL_DestroyTexture(tex);
+
+	void *buf;
+	int32_t pitch;
+
+	if (SDL_LockTexture(gfx, NULL, &buf, &pitch) < 0) {
+		fail("SDL_LockTexture() failed: %s", SDL_GetError());
+	}
+
+	uint32_t *pix = buf;
+
+	for (int32_t y = 0; y < GFX_H; ++y) {
+		for (int32_t x = 0; x < GFX_W; ++x) {
+			uint8_t b = mem_gfx[y * GFX_W + x];
+
+			for (int32_t p = 0; p < GFX_PIX; ++p) {
+				bool set = (b & (1 << (7 - p))) != 0;
+				*pix++ = set ? GFX_FGR : GFX_BGR;
+			}
+		}
+
+		pix += pitch / 4 - GFX_W * GFX_PIX;
+	}
+
+	SDL_UnlockTexture(gfx);
+
+	if (SDL_RenderCopy(ren, gfx, NULL, NULL) < 0) {
+		fail("SDL_RenderCopy() failed: %s", SDL_GetError());
+	}
+
+	SDL_RenderPresent(ren);
+}
+
 void lcd_init(void)
 {
 	ver("lcd init");
+
+	win = SDL_CreateWindow("LCD", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
+			WIN_W, WIN_H, 0);
+
+	if (win == NULL) {
+		fail("SDL_CreateWindow() failed: %s", SDL_GetError());
+	}
+
+	lcd_win = SDL_GetWindowID(win);
+
+	if (lcd_win == 0) {
+		fail("SDL_GetWindowID() failed: %s", SDL_GetError());
+	}
+
+	ren = SDL_CreateRenderer(win, -1, 0);
+
+	if (ren == NULL) {
+		fail("SDL_CreateRenderer() failed: %s", SDL_GetError());
+	}
+
+	gfx = SDL_CreateTexture(ren, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING,
+			GFX_W * GFX_PIX, GFX_H);
+
+	if (gfx == NULL) {
+		fail("SDL_CreateTexture() failed: %s", SDL_GetError());
+	}
+
+	if (SDL_SetTextureBlendMode(gfx, SDL_BLENDMODE_BLEND) < 0) {
+		fail("SDL_SetTextureBlendMode() failed: %s", SDL_GetError());
+	}
+
+	SDL_RWops *ops = SDL_RWFromFile(font, "rb");
+
+	if (ops == NULL) {
+		fail("error while opening font file %s: %s", font, SDL_GetError());
+	}
+
+	fon = TTF_OpenFontRW(ops, 1, 32);
+
+	if (fon == NULL) {
+		fail("error while loading font file %s: %s", font, TTF_GetError());
+	}
+
+	fon_h = TTF_FontLineSkip(fon);
+
+	if (TTF_GlyphMetrics(fon, 'X', NULL, NULL, NULL, NULL, &fon_w) < 0) {
+		fail("error while measuring font width: %s", TTF_GetError());
+	}
+
+	txt_w = TXT_W * fon_w;
+	txt_h = TXT_H * fon_h;
+
+	txt = SDL_CreateRGBSurfaceWithFormat(0, txt_w, txt_h, 32, SDL_PIXELFORMAT_RGBA8888);
+
+	if (txt == NULL) {
+		fail("SDL_CreateRGBSurface() failed: %s", SDL_GetError());
+	}
+
+	for (int32_t i = 0; i < TXT_W * TXT_H; ++i) {
+		mem_txt[i] = ' ';
+	}
 }
 
@@ -32,4 +266,12 @@
 {
 	ver("lcd quit");
+
+	SDL_FreeSurface(txt);
+	TTF_CloseFont(fon);
+
+	SDL_DestroyTexture(gfx);
+
+	SDL_DestroyRenderer(ren);
+	SDL_DestroyWindow(win);
 }
 
@@ -43,5 +285,100 @@
 {
 	ver2("lcd rd %u:%d", off, sz * 8);
-	return 0;
+
+	if (sz != 1 || off != 1) {
+		fail("invalid lcd rd %u:%d", off, sz * 8);
+	}
+
+	switch (com) {
+	case COM_MREAD:
+		if (cur >= BASE_TXT && cur < BASE_TXT + TXT_W * TXT_H) {
+			return mem_txt[cur - BASE_TXT];
+		}
+
+		if (cur >= BASE_GFX && cur < BASE_GFX + GFX_W * GFX_H) {
+			return mem_gfx[cur - BASE_GFX];
+		}
+
+		return 0x00;
+
+	default:
+		return 0x00;
+	}
+}
+
+static void proc_arg(int32_t val)
+{
+	switch (com) {
+	case COM_MWRITE:
+		if (cur >= BASE_TXT && cur < BASE_TXT + TXT_W * TXT_H) {
+			mem_txt[cur - BASE_TXT] = (uint8_t)val;
+		}
+		else if (cur >= BASE_GFX && cur < BASE_GFX + GFX_W * GFX_H) {
+			mem_gfx[cur - BASE_GFX] = (uint8_t)val;
+		}
+
+		cur += dir;
+		SDL_AtomicAdd(&frame, 1);
+		break;
+
+	case COM_CRSWR:
+		if (n_arg == 0) {
+			cur = val;
+		}
+		else if (n_arg == 1) {
+			cur |= val << 8;
+
+			if (cur < BASE_TXT ||
+					(cur >= BASE_TXT + TXT_W * TXT_H && cur < BASE_GFX) ||
+					cur >= BASE_GFX + GFX_W * GFX_H) {
+				fail("invalid address 0x%04x", cur);
+			}
+		}
+
+		break;
+
+	default:
+		break;
+	}
+}
+
+static void proc_com(int32_t val)
+{
+	switch (val) {
+	case COM_CRSWR:
+	case COM_MREAD:
+	case COM_MWRITE:
+		com = val;
+		break;
+
+	case COM_CRSMRT:
+		dir = DIR_RIGHT;
+		com = COM_NONE;
+		break;
+
+	case COM_CRSMUP:
+		dir = DIR_UP;
+		com = COM_NONE;
+		break;
+
+	case COM_CRSMDN:
+		dir = DIR_DOWN;
+		com = COM_NONE;
+		break;
+
+	case COM_DSPOFF:
+		SDL_AtomicSet(&ena, 0);
+		com = COM_NONE;
+		break;
+
+	case COM_DSPON:
+		SDL_AtomicSet(&ena, 1);
+		com = COM_NONE;
+		break;
+
+	default:
+		com = COM_NONE;
+		break;
+	}
 }
 
@@ -49,3 +386,22 @@
 {
 	ver2("lcd wr %u:%d 0x%0*x", off, sz * 8, sz * 2, val);
-}
+
+	if (sz != 1 || off > 1) {
+		fail("invalid lcd wr %u:%d", off, sz * 8);
+	}
+
+	switch (off) {
+	case REG_ARG:
+		proc_arg((int32_t)val);
+		++n_arg;
+		break;
+
+	case REG_COM:
+		proc_com((int32_t)val);
+		n_arg = 0;
+		break;
+
+	default:
+		break;
+	}
+}
Index: emu/sdl.c
===================================================================
--- emu/sdl.c	(revision e26a6322873655ebf2194f02b2dfb27a3f4ceeca)
+++ emu/sdl.c	(revision 67fecc358073769f00f225ecdf43fd63be6167c7)
@@ -27,5 +27,5 @@
 
 static sdl_func_t sdl_funcs[] = {
-	ser_sdl, vid_sdl
+	lcd_sdl, ser_sdl, vid_sdl
 };
 
@@ -161,5 +161,8 @@
 				}
 				else if (win == vid_win) {
-					kbd_key(&ev.key, true);
+					kbd_key(&ev.key, true, true);
+				}
+				else if (win == lcd_win) {
+					kbd_key(&ev.key, false, true);
 				}
 
@@ -171,5 +174,8 @@
 
 				if (win == vid_win) {
-					kbd_key(&ev.key, false);
+					kbd_key(&ev.key, true, false);
+				}
+				else if (win == lcd_win) {
+					kbd_key(&ev.key, false, false);
 				}
 
Index: emu/vid.c
===================================================================
--- emu/vid.c	(revision e26a6322873655ebf2194f02b2dfb27a3f4ceeca)
+++ emu/vid.c	(revision 67fecc358073769f00f225ecdf43fd63be6167c7)
@@ -244,5 +244,5 @@
 	ver("vid init");
 
-	win = SDL_CreateWindow("Display", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
+	win = SDL_CreateWindow("Screen", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
 			WIN_W, WIN_H, 0);
 
