diff --git a/components/badge23/CMakeLists.txt b/components/badge23/CMakeLists.txt
index 2fa07d9f4d6a057db613d19141233fb1da58a8e9..90a2355b754ba8851d2b898152d338b63283a3cb 100644
--- a/components/badge23/CMakeLists.txt
+++ b/components/badge23/CMakeLists.txt
@@ -13,5 +13,6 @@ idf_component_register(
         include
     REQUIRES
         flow3r_bsp
+        st3m
         espressif__led_strip
 )
diff --git a/components/badge23/display.c b/components/badge23/display.c
index 92d234956b730875a4a8dac5691f80ea51d5b608..3a8629f0969506b7dd85ebd4f6c93a0c447e011f 100644
--- a/components/badge23/display.c
+++ b/components/badge23/display.c
@@ -7,65 +7,92 @@
 #include "esp_log.h"
 #include "esp_system.h"
 #include "freertos/FreeRTOS.h"
-#include "freertos/task.h"
 
 
 #include "../../usermodule/uctx/uctx/ctx.h"
 
 #include "flow3r_bsp.h"
+#include "st3m_gfx.h"
 
-static Ctx *the_ctx = NULL;
-static volatile bool initialized = false;
-static DMA_ATTR uint16_t ctx_framebuffer[FLOW3R_BSP_DISPLAY_WIDTH * FLOW3R_BSP_DISPLAY_HEIGHT];
 
-void display_ctx_init() {
-    the_ctx = ctx_new_for_framebuffer(
-        ctx_framebuffer,
+typedef struct {
+    Ctx *ctx;
+    st3m_framebuffer_desc_t *desc;
+} ctx_target_t;
+
+static void new_ctx_target(ctx_target_t *target, st3m_framebuffer_desc_t *desc) {
+    Ctx *ctx = ctx_new_for_framebuffer(
+        desc->buffer,
         FLOW3R_BSP_DISPLAY_WIDTH,
         FLOW3R_BSP_DISPLAY_HEIGHT,
         FLOW3R_BSP_DISPLAY_WIDTH * 2,
         CTX_FORMAT_RGB565_BYTESWAPPED
     );
+    assert(ctx != NULL);
 
 	int32_t offset_x = FLOW3R_BSP_DISPLAY_WIDTH / 2;
 	int32_t offset_y = FLOW3R_BSP_DISPLAY_HEIGHT / 2;
     // rotate by 180 deg and translate x and y by 120 px to have (0,0) at the center of the screen
-    ctx_apply_transform(the_ctx,-1,0,offset_x,0,-1,offset_y,0,0,1);
+    ctx_apply_transform(ctx,-1,0,offset_x,0,-1,offset_y,0,0,1);
+
+    target->ctx = ctx;
+    target->desc = desc;
 }
 
-static void display_loading_splash(void) {
-    ctx_rgb(the_ctx, 0.157, 0.129, 0.167);
-    ctx_rectangle(the_ctx, -120, -120, 240, 240);
-    ctx_fill(the_ctx);
+static ctx_target_t targets[ST3M_GFX_NBUFFERS];
+
+static void display_loading_splash(ctx_target_t *target) {
+    ctx_rgb(target->ctx, 0.157, 0.129, 0.167);
+    ctx_rectangle(target->ctx, -120, -120, 240, 240);
+    ctx_fill(target->ctx);
     
-    ctx_move_to(the_ctx, 0, 0);
-    ctx_rgb(the_ctx, 0.9, 0.9, 0.9);
-    ctx_text_align(the_ctx, CTX_TEXT_ALIGN_CENTER);
-    ctx_text_baseline(the_ctx, CTX_TEXT_BASELINE_ALPHABETIC);
-    ctx_text(the_ctx, "Loading...");
+    ctx_move_to(target->ctx, 0, 0);
+    ctx_rgb(target->ctx, 0.9, 0.9, 0.9);
+    ctx_text_align(target->ctx, CTX_TEXT_ALIGN_CENTER);
+    ctx_text_baseline(target->ctx, CTX_TEXT_BASELINE_ALPHABETIC);
+    ctx_text(target->ctx, "Loading...");
+}
 
-    flow3r_bsp_display_send_fb(ctx_framebuffer);
+static ctx_target_t *next_target(void) {
+    st3m_framebuffer_desc_t *desc = st3m_gfx_framebuffer_get(portMAX_DELAY);
+    if (targets[desc->num].ctx == NULL) {
+        new_ctx_target(&targets[desc->num], desc);
+    }
+
+    return &targets[desc->num];
 }
 
+static bool initialized = false;
+static ctx_target_t *sync_target = NULL;
+
 void display_init() {
-    flow3r_bsp_display_init();
-    display_ctx_init();
-    display_loading_splash();
-    // Delay turning on backlight, otherwise we get a flash of some old
-    // buffer..? Is the display RAMWR not synchronous..?
-    vTaskDelay(100 / portTICK_PERIOD_MS);
-    flow3r_bsp_display_set_backlight(100);
+    // HACK: needed until we have new async gfx api.
+    assert(ST3M_GFX_NBUFFERS == 1);
+
+    st3m_gfx_init();
+    for (int i = 0; i < ST3M_GFX_NBUFFERS; i++) {
+        targets[i].ctx = NULL;
+        targets[i].desc = NULL;
+    }
 
-    memset(ctx_framebuffer, 0, FLOW3R_BSP_DISPLAY_WIDTH * FLOW3R_BSP_DISPLAY_HEIGHT * 2);
+	ctx_target_t *tgt = next_target();
+    display_loading_splash(tgt);
+    st3m_gfx_framebuffer_queue(tgt->desc);
 
+	sync_target = next_target();
+    flow3r_bsp_display_set_backlight(100);
     initialized = true;
 }
 
+
 void display_update(){
     if (!initialized) {
         return;
     }
-    flow3r_bsp_display_send_fb(ctx_framebuffer);
+
+    st3m_gfx_framebuffer_queue(sync_target->desc);
+    ctx_target_t *tgt = next_target();
+    assert(tgt == sync_target);
 }
 
 void display_set_backlight(uint8_t percent) {
@@ -79,5 +106,5 @@ Ctx *display_global_ctx(void) {
     if (!initialized) {
         return NULL;
     }
-    return the_ctx;
+    return sync_target->ctx;
 }
\ No newline at end of file
diff --git a/components/st3m/CMakeLists.txt b/components/st3m/CMakeLists.txt
index 29dde06d87a05743d6223e465947118c811b76d3..7196e6632afe7bed752925c7b8348f807d8f2c00 100644
--- a/components/st3m/CMakeLists.txt
+++ b/components/st3m/CMakeLists.txt
@@ -1,5 +1,6 @@
 idf_component_register(
     SRCS
+        st3m_gfx.c
     INCLUDE_DIRS
         .
     REQUIRES
diff --git a/components/st3m/st3m_gfx.c b/components/st3m/st3m_gfx.c
new file mode 100644
index 0000000000000000000000000000000000000000..8aa4ae0e665eddeb6bedd1fa71c626027f312b5d
--- /dev/null
+++ b/components/st3m/st3m_gfx.c
@@ -0,0 +1,77 @@
+#include "st3m_gfx.h"
+
+#include <string.h>
+
+#include "esp_system.h"
+#include "esp_log.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/queue.h"
+
+#include "flow3r_bsp.h"
+
+// Actual framebuffers, efficiently accessible via DMA.
+static DMA_ATTR uint16_t framebuffers[ST3M_GFX_NBUFFERS][FLOW3R_BSP_DISPLAY_WIDTH * FLOW3R_BSP_DISPLAY_HEIGHT];
+
+static st3m_framebuffer_desc_t framebuffer_descs[ST3M_GFX_NBUFFERS];
+
+// Queue of free framebuffer descriptors, written into by crtc once rendered,
+// read from by rasterizer when new frame starts.
+static QueueHandle_t framebuffer_freeq = NULL;
+
+// Queue of framebuffer descriptors to blit out.
+static QueueHandle_t framebuffer_blitq = NULL;
+
+static void st3m_gfx_crtc_task(void *_arg) {
+	(void)_arg;
+
+	while (true) {
+		int descno;
+		xQueueReceive(framebuffer_blitq, &descno, portMAX_DELAY);
+
+    	flow3r_bsp_display_send_fb(framebuffer_descs[descno].buffer);
+
+		xQueueSend(framebuffer_freeq, &descno, portMAX_DELAY);
+	}
+}
+
+void st3m_gfx_init(void) {
+	// Make sure we're not being re-initialized.
+	assert(framebuffer_freeq == NULL);
+
+    flow3r_bsp_display_init();
+
+	// Create framebuffer queues.
+	framebuffer_freeq = xQueueCreate(2, sizeof(int));
+	assert(framebuffer_freeq != NULL);
+	framebuffer_blitq = xQueueCreate(2, sizeof(int));
+	assert(framebuffer_blitq != NULL);
+
+	// Zero out framebuffers and set up descriptors.
+	for (int i = 0; i < ST3M_GFX_NBUFFERS; i++) {
+		memset(&framebuffers[i], 0, FLOW3R_BSP_DISPLAY_WIDTH * FLOW3R_BSP_DISPLAY_HEIGHT * 2);
+		st3m_framebuffer_desc_t *desc = &framebuffer_descs[i];
+		desc->num = i;
+		desc->buffer = framebuffers[i];
+
+		// Push descriptor to freeq.
+		BaseType_t res = xQueueSend(framebuffer_freeq, &i, 0);
+		assert(res == pdTRUE);
+	}
+
+	// Start crtc.
+	BaseType_t res = xTaskCreate(st3m_gfx_crtc_task, "crtc", 2048, NULL, configMAX_PRIORITIES - 2, NULL);
+	assert(res == pdPASS);
+}
+
+st3m_framebuffer_desc_t *st3m_gfx_framebuffer_get(TickType_t ticks_to_wait) {
+	int descno;
+	BaseType_t res = xQueueReceive(framebuffer_freeq, &descno, ticks_to_wait);
+	if (res != pdTRUE) {
+		return NULL;
+	}
+	return &framebuffer_descs[descno];
+}
+
+void st3m_gfx_framebuffer_queue(st3m_framebuffer_desc_t *desc) {
+	xQueueSend(framebuffer_blitq, &desc->num, portMAX_DELAY);
+}
\ No newline at end of file
diff --git a/components/st3m/st3m_gfx.h b/components/st3m/st3m_gfx.h
new file mode 100644
index 0000000000000000000000000000000000000000..55849fd682ae98170eff8788051e94060868aecc
--- /dev/null
+++ b/components/st3m/st3m_gfx.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "freertos/FreeRTOS.h"
+
+// Each buffer  takes ~116kB SRAM. While one framebuffer is being blitted, the
+// other one is being written to by the rasterizer.
+#define ST3M_GFX_NBUFFERS 1
+
+// A framebuffer descriptor, pointing at a framebuffer.
+typedef struct {
+	int num;
+	uint16_t *buffer;
+} st3m_framebuffer_desc_t;
+
+void st3m_gfx_init(void);
+
+st3m_framebuffer_desc_t *st3m_gfx_framebuffer_get(TickType_t ticks_to_wait);
+
+void st3m_gfx_framebuffer_queue(st3m_framebuffer_desc_t *desc);
\ No newline at end of file