Index: emu/all.h
===================================================================
--- emu/all.h	(revision ea878ba2bbb1317626a831650411a7fb11f77203)
+++ emu/all.h	(revision bdd5a633c5295089a7d3640f3c1b310dbe247803)
@@ -63,7 +63,14 @@
 extern const char *disk;
 
+extern SDL_atomic_t run;
+
 extern void sdl_init(void);
 extern void sdl_quit(void);
+extern void sdl_loop(void);
 
+extern SDL_mutex *cpu_mutex;
+
+extern void cpu_init(void);
+extern void cpu_quit(void);
 extern void cpu_loop(void);
 
@@ -98,4 +105,5 @@
 extern void ser_write(uint32_t off, int32_t sz, uint32_t val);
 
+extern void ser_sdl(void);
 extern void ser_text(SDL_TextInputEvent *ev);
 extern void ser_key(SDL_KeyboardEvent *ev);
Index: emu/cpu.c
===================================================================
--- emu/cpu.c	(revision ea878ba2bbb1317626a831650411a7fb11f77203)
+++ emu/cpu.c	(revision bdd5a633c5295089a7d3640f3c1b310dbe247803)
@@ -52,4 +52,9 @@
 } hw_t;
 
+static uint64_t freq;
+static uint64_t quan;
+
+SDL_mutex *cpu_mutex;
+
 static bool reset = true;
 
@@ -620,27 +625,42 @@
 }
 
-void cpu_loop(void)
-{
+void cpu_init(void)
+{
+	cpu_mutex = SDL_CreateMutex();
+
+	if (cpu_mutex == NULL) {
+		fail("SDL_CreateMutex() failed: %s", SDL_GetError());
+	}
+
+	freq = SDL_GetPerformanceFrequency();
+	quan = freq / PER_SEC;
+
+	inf("freq %" PRIu64 " quan %" PRIu64, freq, quan);
+
 	hw_init();
 	bios_init();
 
-	inf("entering CPU loop");
 	m68k_init();
 	m68k_set_cpu_type(M68K_CPU_TYPE_68000);
 	m68k_set_instr_hook_callback(inst_cb);
 	m68k_pulse_reset();
-
-	uint64_t freq = SDL_GetPerformanceFrequency();
-	uint64_t quan = freq / PER_SEC;
-	inf("freq %" PRIu64 " quan %" PRIu64, freq, quan);
-
-	bool run = true;
-
-#if defined EMU_LINUX
-	SDL_Scancode down = SDL_SCANCODE_UNKNOWN;
-#endif
-
-	while (run) {
+}
+
+void cpu_quit(void)
+{
+	hw_quit();
+	SDL_DestroyMutex(cpu_mutex);
+}
+
+void cpu_loop(void)
+{
+	inf("entering CPU loop");
+
+	while (SDL_AtomicGet(&run) != 0) {
 		uint64_t until = SDL_GetPerformanceCounter() + quan;
+
+		if (SDL_LockMutex(cpu_mutex) < 0) {
+			fail("SDL_LockMutex() failed: %s", SDL_GetError());
+		}
 
 		m68k_execute(CPU_FREQ / PER_SEC);
@@ -653,37 +673,6 @@
 		m68k_set_irq(irq);
 
-		SDL_Event ev;
-
-		while (SDL_PollEvent(&ev) > 0) {
-#if defined EMU_LINUX
-			// Work around duplicate key-down events on Linux.
-
-			if (ev.type == SDL_KEYDOWN) {
-				if (down == ev.key.keysym.scancode) {
-					continue;
-				}
-
-				down = ev.key.keysym.scancode;
-			}
-			else if (ev.type == SDL_KEYUP) {
-				down = SDL_SCANCODE_UNKNOWN;
-			}
-#endif
-
-			if (ev.type == SDL_QUIT ||
-					(ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_ESCAPE)) {
-				run = false;
-				continue;
-			}
-
-			if (ev.type == SDL_TEXTINPUT) {
-				ser_text(&ev.text);
-				continue;
-			}
-
-			if (ev.type == SDL_KEYDOWN) {
-				ser_key(&ev.key);
-				continue;
-			}
+		if (SDL_UnlockMutex(cpu_mutex) < 0) {
+			fail("SDL_UnlockMutex() failed: %s", SDL_GetError());
 		}
 
@@ -694,4 +683,3 @@
 
 	inf("leaving CPU loop");
-	hw_quit();
-}
+}
Index: emu/main.c
===================================================================
--- emu/main.c	(revision ea878ba2bbb1317626a831650411a7fb11f77203)
+++ emu/main.c	(revision bdd5a633c5295089a7d3640f3c1b310dbe247803)
@@ -40,4 +40,6 @@
 const char *bios = "bios.abs";
 const char *disk = "buchla.disk";
+
+SDL_atomic_t run;
 
 static void usage(FILE *fh)
@@ -118,11 +120,29 @@
 }
 
+static int32_t cpu_thread(void *data)
+{
+	(void)data;
+
+	cpu_loop();
+	return 0;
+}
+
 int32_t main(int32_t argc, char *argv[])
 {
 	parse_args(argc, argv);
 	sdl_init();
+	cpu_init();
 
-	cpu_loop();
+	SDL_AtomicSet(&run, 1);
+	SDL_Thread *thr = SDL_CreateThread(cpu_thread, "cpu", NULL);
 
+	if (thr == NULL) {
+		fail("SDL_CreateThread() failed: %s", SDL_GetError());
+	}
+
+	sdl_loop();
+	SDL_WaitThread(thr, NULL);
+
+	cpu_quit();
 	sdl_quit();
 	return 0;
Index: emu/sdl.c
===================================================================
--- emu/sdl.c	(revision ea878ba2bbb1317626a831650411a7fb11f77203)
+++ emu/sdl.c	(revision bdd5a633c5295089a7d3640f3c1b310dbe247803)
@@ -51,2 +51,55 @@
 	SDL_Quit();
 }
+
+void sdl_loop(void)
+{
+	inf("entering SDL loop");
+
+#if defined EMU_LINUX
+	SDL_Scancode down = SDL_SCANCODE_UNKNOWN;
+#endif
+
+	while (SDL_AtomicGet(&run) != 0) {
+		ser_sdl();
+
+		SDL_Event ev;
+
+		while (SDL_PollEvent(&ev) > 0) {
+#if defined EMU_LINUX
+			// Work around duplicate key-down events on Linux.
+
+			if (ev.type == SDL_KEYDOWN) {
+				if (down == ev.key.keysym.scancode) {
+					continue;
+				}
+
+				down = ev.key.keysym.scancode;
+			}
+			else if (ev.type == SDL_KEYUP) {
+				down = SDL_SCANCODE_UNKNOWN;
+			}
+#endif
+
+			if (ev.type == SDL_QUIT ||
+					(ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_ESCAPE)) {
+				ver("quit");
+				SDL_AtomicSet(&run, 0);
+				continue;
+			}
+
+			if (ev.type == SDL_TEXTINPUT) {
+				ser_text(&ev.text);
+				continue;
+			}
+
+			if (ev.type == SDL_KEYDOWN) {
+				ser_key(&ev.key);
+				continue;
+			}
+
+			SDL_Delay(50);
+		}
+	}
+
+	inf("leaving SDL loop");
+}
Index: emu/ser.c
===================================================================
--- emu/ser.c	(revision ea878ba2bbb1317626a831650411a7fb11f77203)
+++ emu/ser.c	(revision bdd5a633c5295089a7d3640f3c1b310dbe247803)
@@ -60,4 +60,5 @@
 static SDL_Window *win;
 static SDL_Renderer *ren;
+static SDL_atomic_t frame;
 
 static TTF_Font *fon;
@@ -70,6 +71,115 @@
 static int32_t bel = 0;
 
-static void update(void)
-{
+static void scroll(void)
+{
+	memmove(mem, mem + 1, (CON_H - 1) * (CON_W + 1));
+	memset(mem + (CON_H - 1), ' ', CON_W);
+}
+
+static void forw(void)
+{
+	if (cur_x < CON_W - 1) {
+		++cur_x;
+		return;
+	}
+
+	if (cur_y == CON_H - 1) {
+		cur_x = 0;
+		scroll();
+		return;
+	}
+
+	cur_x = 0;
+	++cur_y;
+}
+
+static void back(void)
+{
+	if (cur_x > 0) {
+		--cur_x;
+		return;
+	}
+
+	if (cur_y == 0) {
+		return;
+	}
+
+	cur_x = CON_W - 1;
+	--cur_y;
+}
+
+static void down(void)
+{
+	if (cur_y < CON_H - 1) {
+		++cur_y;
+		return;
+	}
+
+	scroll();
+}
+
+static void echo(uint8_t c)
+{
+	if (c < 32) {
+		switch (c) {
+		case '\r':
+			cur_x = 0;
+			break;
+
+		case '\n':
+			down();
+			break;
+
+		case '\b':
+			back();
+			break;
+
+		case '\a':
+			bel = BEL_CYC;
+			break;
+
+		default:
+			echo('^');
+			echo((uint8_t)(c + '@'));
+			return;
+		}
+	}
+	else {
+		mem[cur_y][cur_x] = c;
+		forw();
+	}
+
+	SDL_AtomicAdd(&frame, 1);
+}
+
+static void out(int32_t un, uint8_t c)
+{
+	if (SDL_LockMutex(cpu_mutex) < 0) {
+		fail("SDL_LockMutex() failed: %s", SDL_GetError());
+	}
+
+	state[un].rdr = c;
+	state[un].rdr_ok = true;
+	state[un].irq_r = true;
+
+	if (SDL_UnlockMutex(cpu_mutex) < 0) {
+		fail("SDL_UnlockMutex() failed: %s", SDL_GetError());
+	}
+}
+
+void ser_sdl(void)
+{
+	ver3("ser_sdl()");
+
+	static int32_t last = 0;
+	int32_t now = SDL_AtomicGet(&frame);
+
+	if (last == now) {
+		ver3("no update");
+		return;
+	}
+
+	last = now;
+
 	if (SDL_FillRect(sur, NULL, bel == 0 ? CON_BGR : CON_BEL) < 0) {
 		fail("SDL_FillRect() failed: %s", SDL_GetError());
@@ -86,5 +196,17 @@
 
 	for (int32_t y = 0; y < CON_H; ++y) {
-		SDL_Surface *lin = TTF_RenderText_Blended(fon, (char *)mem[y], CON_FGR);
+		char line[CON_W + 1];
+
+		if (SDL_LockMutex(cpu_mutex) < 0) {
+			fail("SDL_LockMutex() failed: %s", SDL_GetError());
+		}
+
+		memcpy(line, mem[y], CON_W + 1);
+
+		if (SDL_UnlockMutex(cpu_mutex) < 0) {
+			fail("SDL_UnlockMutex() failed: %s", SDL_GetError());
+		}
+
+		SDL_Surface *lin = TTF_RenderText_Blended(fon, line, CON_FGR);
 
 		if (lin == NULL) {
@@ -118,93 +240,4 @@
 }
 
-static void scroll(void)
-{
-	memmove(mem, mem + 1, (CON_H - 1) * (CON_W + 1));
-	memset(mem + (CON_H - 1), ' ', CON_W);
-}
-
-static void forw(void)
-{
-	if (cur_x < CON_W - 1) {
-		++cur_x;
-		return;
-	}
-
-	if (cur_y == CON_H - 1) {
-		cur_x = 0;
-		scroll();
-		return;
-	}
-
-	cur_x = 0;
-	++cur_y;
-}
-
-static void back(void)
-{
-	if (cur_x > 0) {
-		--cur_x;
-		return;
-	}
-
-	if (cur_y == 0) {
-		return;
-	}
-
-	cur_x = CON_W - 1;
-	--cur_y;
-}
-
-static void down(void)
-{
-	if (cur_y < CON_H - 1) {
-		++cur_y;
-		return;
-	}
-
-	scroll();
-}
-
-static void echo(uint8_t c)
-{
-	if (c < 32) {
-		switch (c) {
-		case '\r':
-			cur_x = 0;
-			break;
-
-		case '\n':
-			down();
-			break;
-
-		case '\b':
-			back();
-			break;
-
-		case '\a':
-			bel = BEL_CYC;
-			break;
-
-		default:
-			echo('^');
-			echo((uint8_t)(c + '@'));
-			return;
-		}
-	}
-	else {
-		mem[cur_y][cur_x] = c;
-		forw();
-	}
-
-	update();
-}
-
-static void out(int32_t un, uint8_t c)
-{
-	state[un].rdr = c;
-	state[un].rdr_ok = true;
-	state[un].irq_r = true;
-}
-
 void ser_key(SDL_KeyboardEvent *ev)
 {
@@ -252,4 +285,6 @@
 	}
 
+	SDL_AtomicSet(&frame, 1);
+
 	SDL_RWops *ops = SDL_RWFromFile(CON_FONT, "rb");
 
@@ -286,6 +321,4 @@
 		mem[y][CON_W] = 0;
 	}
-
-	update();
 }
 
@@ -309,5 +342,5 @@
 
 		if (bel == BEL_CYC - 1 || bel == 0) {
-			update();
+			SDL_AtomicAdd(&frame, 1);
 		}
 	}
