diff --git a/components/micropython/usermodule/mp_sys_display.c b/components/micropython/usermodule/mp_sys_display.c
index fbc7cdca301d8273114c7b770709f945e32b8c5c..e0c4cbaf831b81e873f4cf0bc8b072a23dc4397d 100644
--- a/components/micropython/usermodule/mp_sys_display.c
+++ b/components/micropython/usermodule/mp_sys_display.c
@@ -36,18 +36,76 @@ STATIC mp_obj_t mp_set_backlight(mp_obj_t percent_in) {
 }
 STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_set_backlight_obj, mp_set_backlight);
 
-static Ctx *global_ctx = NULL;
-STATIC mp_obj_t mp_get_ctx(void) {
-    if (!global_ctx) global_ctx = st3m_ctx(0);
-    if (global_ctx == NULL) return mp_const_none;
-    return mp_ctx_from_ctx(global_ctx);
+STATIC mp_obj_t mp_set_gfx_mode(mp_obj_t mode) {
+    st3m_set_gfx_mode(mp_obj_get_int(mode));
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_set_gfx_mode_obj, mp_set_gfx_mode);
+
+STATIC mp_obj_t mp_get_gfx_mode(void) {
+    return mp_obj_new_int(st3m_get_gfx_mode());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_gfx_mode_obj, mp_get_gfx_mode);
+
+STATIC mp_obj_t mp_set_palette(mp_obj_t pal_in) {
+    size_t count = mp_obj_get_int(mp_obj_len(pal_in));
+    uint8_t *pal = m_malloc(count);
+    for (size_t i = 0; i < count; i++) {
+        pal[i] = mp_obj_get_int(
+            mp_obj_subscr(pal_in, mp_obj_new_int(i), MP_OBJ_SENTINEL));
+    }
+    st3m_gfx_set_palette(pal, count / 3);
+#if MICROPY_MALLOC_USES_ALLOCATED_SIZE
+    m_free(pal, count);
+#else
+    m_free(pal);
+#endif
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_set_palette_obj, mp_set_palette);
+
+STATIC mp_obj_t mp_ctx(mp_obj_t mode_in) {
+    int mode = mp_obj_get_int(mode_in);
+    Ctx *ctx = NULL;
+    switch (mode) {
+        case 0:
+        case 16:
+            ctx = st3m_ctx(0);
+            if (ctx == NULL) return mp_const_none;
+            break;
+        case st3m_gfx_overlay:
+        case 8:
+        case 24:
+        case 32:
+            ctx = st3m_overlay_ctx();
+            break;
+    }
+    return mp_ctx_from_ctx(ctx);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_ctx_obj, mp_get_ctx);
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_ctx_obj, mp_ctx);
 
-STATIC mp_obj_t mp_get_overlay_ctx(void) {
-    return mp_ctx_from_ctx(st3m_overlay_ctx());
+STATIC mp_obj_t mp_fb(mp_obj_t mode_in) {
+    int mode = mp_obj_get_int(mode_in);
+    int size = 240 * 240;
+    switch (mode) {
+        case 0:
+            size *= 2;
+            mode = 16;
+            break;
+        case 16:
+            size *= 2;
+            break;
+        case 24:
+            size *= 3;
+            break;
+        case st3m_gfx_overlay:
+        case 32:
+            size *= 4;
+            break;
+    }
+    return mp_obj_new_bytearray_by_ref(size, st3m_gfx_fb(mode));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_overlay_ctx_obj, mp_get_overlay_ctx);
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_fb_obj, mp_fb);
 
 STATIC mp_obj_t mp_update(mp_obj_t ctx_in) {
     mp_ctx_obj_t *self = MP_OBJ_TO_PTR(ctx_in);
@@ -55,10 +113,7 @@ STATIC mp_obj_t mp_update(mp_obj_t ctx_in) {
         mp_raise_ValueError("not a ctx");
         return mp_const_none;
     }
-    if (global_ctx) {
-        st3m_ctx_end_frame(self->ctx);
-        global_ctx = NULL;
-    }
+    st3m_ctx_end_frame(self->ctx);
     return mp_const_none;
 }
 STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_update_obj, mp_update);
@@ -84,9 +139,11 @@ STATIC const mp_rom_map_elem_t mp_module_sys_display_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR_set_backlight), MP_ROM_PTR(&mp_set_backlight_obj) },
     { MP_ROM_QSTR(MP_QSTR_overlay_clip), MP_ROM_PTR(&mp_overlay_clip_obj) },
     { MP_ROM_QSTR(MP_QSTR_update), MP_ROM_PTR(&mp_update_obj) },
-    { MP_ROM_QSTR(MP_QSTR_get_ctx), MP_ROM_PTR(&mp_get_ctx_obj) },
-    { MP_ROM_QSTR(MP_QSTR_get_overlay_ctx),
-      MP_ROM_PTR(&mp_get_overlay_ctx_obj) },
+    { MP_ROM_QSTR(MP_QSTR_fb), MP_ROM_PTR(&mp_fb_obj) },
+    { MP_ROM_QSTR(MP_QSTR_ctx), MP_ROM_PTR(&mp_ctx_obj) },
+    { MP_ROM_QSTR(MP_QSTR_set_gfx_mode), MP_ROM_PTR(&mp_set_gfx_mode_obj) },
+    { MP_ROM_QSTR(MP_QSTR_set_palette), MP_ROM_PTR(&mp_set_palette_obj) },
+    { MP_ROM_QSTR(MP_QSTR_get_gfx_mode), MP_ROM_PTR(&mp_get_gfx_mode_obj) },
     { MP_ROM_QSTR(MP_QSTR_fps), MP_ROM_PTR(&mp_fps_obj) },
 };
 
diff --git a/components/st3m/st3m_gfx.c b/components/st3m/st3m_gfx.c
index 2e63ed758c4c43130d9813ab7df9c5574a164694..48048d0c8138603109932a354333d12006d9861e 100644
--- a/components/st3m/st3m_gfx.c
+++ b/components/st3m/st3m_gfx.c
@@ -19,75 +19,55 @@
 #include "st3m_counter.h"
 #include "st3m_version.h"
 
-#define ST3M_GFX_NBUFFERS 1
-#define ST3M_GFX_NCTX 1
-
-// A framebuffer descriptor, pointing at a framebuffer.
-typedef struct {
-    // The numeric ID of this descriptor.
-    int num;
-    // SPIRAM buffer.
-    uint16_t buffer[240 * 240];
-    Ctx *ctx;
-} st3m_framebuffer_desc_t;
+static EXT_RAM_BSS_ATTR uint16_t fb[240 * 240];
 
 // Get a free drawlist ctx to draw into.
 //
 // ticks_to_wait can be used to limit the time to wait for a free ctx
 // descriptor, or portDELAY_MAX can be specified to wait forever. If the timeout
 // expires, NULL will be returned.
-static st3m_ctx_desc_t *st3m_gfx_drawctx_free_get(TickType_t ticks_to_wait);
+static Ctx *st3m_gfx_drawctx_free_get(TickType_t ticks_to_wait);
 
 // Submit a filled ctx descriptor to the rasterization pipeline.
-static void st3m_gfx_drawctx_pipe_put(st3m_ctx_desc_t *desc);
+static void st3m_gfx_drawctx_pipe_put(void);
 
 static const char *TAG = "st3m-gfx";
 
-// Framebuffer descriptors, containing framebuffer and ctx for each of the
-// framebuffers.
-//
-// These live in SPIRAM, as we don't have enough space in SRAM/IRAM.
-EXT_RAM_BSS_ATTR static st3m_framebuffer_desc_t
-    framebuffer_descs[ST3M_GFX_NBUFFERS];
-
 #define OVERLAY_WIDTH 240
 #define OVERLAY_HEIGHT 240
 #define OVERLAY_X 0
 #define OVERLAY_Y 0
 
-static int _st3m_overlay_y1 = 0;
-static int _st3m_overlay_x1 = 0;
-static int _st3m_overlay_y0 = 0;
-static int _st3m_overlay_x0 = 0;
+static st3m_gfx_mode _st3m_gfx_mode = st3m_gfx_default;
 
 EXT_RAM_BSS_ATTR static uint8_t
     st3m_overlay_fb[OVERLAY_WIDTH * OVERLAY_HEIGHT * 4];
 EXT_RAM_BSS_ATTR uint16_t st3m_overlay_backup[OVERLAY_WIDTH * OVERLAY_HEIGHT];
-static Ctx *_st3m_overlay_ctx = NULL;
 
-static st3m_ctx_desc_t dctx_descs[ST3M_GFX_NCTX];
+// drawlist ctx
+static Ctx *user_ctx = NULL;
 
-// 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;
+// RGB565_byteswapped framebuffer ctx - our global ctx
+//   user_ctx gets replayed on this
+static Ctx *fb_ctx = NULL;
 
-static st3m_counter_rate_t blit_rate;
-static st3m_counter_timer_t blit_read_time;
-static st3m_counter_timer_t blit_work_time;
-static st3m_counter_timer_t blit_write_time;
+// RGBA8 overlay ctx
+static Ctx *overlay_ctx = NULL;
+
+// corner pixel coordinates for overlay clip
+static int _st3m_overlay_y1 = 0;
+static int _st3m_overlay_x1 = 0;
+static int _st3m_overlay_y0 = 0;
+static int _st3m_overlay_x0 = 0;
 
-static QueueHandle_t dctx_freeq = NULL;
-static QueueHandle_t dctx_rastq = NULL;
+static float smoothed_fps = 0.0f;
+static QueueHandle_t user_ctx_freeq = NULL;
+static QueueHandle_t user_ctx_rastq = NULL;
 
 static st3m_counter_rate_t rast_rate;
-static st3m_counter_timer_t rast_read_fb_time;
-static st3m_counter_timer_t rast_read_dctx_time;
-static st3m_counter_timer_t rast_work_time;
-static st3m_counter_timer_t rast_write_time;
+static TaskHandle_t graphics_task;
 
-static TaskHandle_t rast_task;
+//////////////////////////
 
 // Attempt to receive from a queue forever, but log an error if it takes longer
 // than two seconds to get something.
@@ -111,46 +91,129 @@ void st3m_ctx_merge_overlay(uint16_t *fb, uint8_t *overlay, int ostride,
 void st3m_ctx_unmerge_overlay(uint16_t *fb, uint16_t *overlay_backup, int x0,
                               int y0, int w, int h);
 
-static float smoothed_fps = 0.0f;
-
 float st3m_gfx_fps(void) { return smoothed_fps; }
 
-static void st3m_gfx_rast_task(void *_arg) {
+void st3m_gfx_set_palette(uint8_t *pal_in, int count) {
+    if (count > 256) count = 256;
+    if (count < 0) count = 0;
+    uint8_t *pal =
+        ((uint8_t *)st3m_overlay_fb) + sizeof(st3m_overlay_fb) - 256 * 3;
+    for (int i = 0; i < count * 3; i++) pal[i] = pal_in[i];
+}
+
+static void grayscale_palette(void) {
+    uint8_t pal[256 * 3];
+    for (int i = 0; i < 256; i++) {
+        pal[i * 3 + 0] = i;
+        pal[i * 3 + 1] = i;
+        pal[i * 3 + 2] = i;
+    }
+    st3m_gfx_set_palette(pal, 256);
+}
+
+static void fire_palette(void) {
+    uint8_t pal[256 * 3];
+    for (int i = 0; i < 256; i++) {
+        pal[i * 3 + 0] = i;
+        pal[i * 3 + 1] = (i / 255.0) * (i / 255.0) * 255;
+        pal[i * 3 + 2] = (i / 255.0) * (i / 255.0) * (i / 255.0) * 255;
+    }
+    st3m_gfx_set_palette(pal, 256);
+}
+
+static void ice_palette(void) {
+    uint8_t pal[256 * 3];
+    for (int i = 0; i < 256; i++) {
+        pal[i * 3 + 0] = (i / 255.0) * (i / 255.0) * (i / 255.0) * 255;
+        pal[i * 3 + 1] = (i / 255.0) * (i / 255.0) * 255;
+        pal[i * 3 + 2] = i;
+    }
+    st3m_gfx_set_palette(pal, 256);
+}
+
+void st3m_set_gfx_mode(st3m_gfx_mode mode) {
+    memset(fb, 0, sizeof(fb));
+    memset(st3m_overlay_fb, 0, sizeof(st3m_overlay_fb));
+
+    switch ((int)mode) {
+        case 8:
+            fire_palette();
+            break;
+        case 9:
+            ice_palette();
+            mode = 8;
+            break;
+        case 10:
+            grayscale_palette();
+            mode = 8;
+            break;
+    }
+    _st3m_gfx_mode = mode;
+}
+st3m_gfx_mode st3m_get_gfx_mode(void) { return _st3m_gfx_mode; }
+uint8_t *st3m_gfx_fb(st3m_gfx_mode mode) {
+    switch (_st3m_gfx_mode) {
+        case st3m_gfx_default:
+        case st3m_gfx_16bpp:
+            return (uint8_t *)fb;
+        case st3m_gfx_32bpp:
+        case st3m_gfx_24bpp:
+        case st3m_gfx_8bpp:
+        case st3m_gfx_overlay:
+            return st3m_overlay_fb;
+    }
+    return st3m_overlay_fb;
+}
+
+static void st3m_gfx_task(void *_arg) {
     (void)_arg;
 
     while (true) {
         int descno = 0;
-        st3m_framebuffer_desc_t *fb = &framebuffer_descs[descno];
-
-        xQueueReceiveNotifyStarved(dctx_rastq, &descno,
-                                   "rast task starved (dctx)!");
-        st3m_ctx_desc_t *draw = &dctx_descs[descno];
 
-        ctx_set_textureclock(framebuffer_descs[0].ctx,
-                             ctx_textureclock(framebuffer_descs[0].ctx) + 1);
+        xQueueReceiveNotifyStarved(user_ctx_rastq, &descno,
+                                   "rast task starved (user_ctx)!");
 
-        ctx_render_ctx(draw->ctx, fb->ctx);
-        ctx_drawlist_clear(draw->ctx);
+        ctx_set_textureclock(fb_ctx, ctx_textureclock(fb_ctx) + 1);
 
         st3m_overlay_ctx();
-        if (_st3m_overlay_y1 != _st3m_overlay_y0)
-            st3m_ctx_merge_overlay(
-                framebuffer_descs[descno].buffer,
-                st3m_overlay_fb +
-                    4 * ((_st3m_overlay_y0 - OVERLAY_Y) * OVERLAY_WIDTH +
-                         (_st3m_overlay_x0 - OVERLAY_X)),
-                OVERLAY_WIDTH * 4, st3m_overlay_backup, _st3m_overlay_x0,
-                _st3m_overlay_y0, _st3m_overlay_x1 - _st3m_overlay_x0 + 1,
-                _st3m_overlay_y1 - _st3m_overlay_y0 + 1);
-        flow3r_bsp_display_send_fb(framebuffer_descs[descno].buffer, 16);
-        if (_st3m_overlay_y1 != _st3m_overlay_y0)
-            st3m_ctx_unmerge_overlay(framebuffer_descs[descno].buffer,
-                                     st3m_overlay_backup, _st3m_overlay_x0,
-                                     _st3m_overlay_y0,
-                                     _st3m_overlay_x1 - _st3m_overlay_x0 + 1,
-                                     _st3m_overlay_y1 - _st3m_overlay_y0 + 1);
-
-        xQueueSend(dctx_freeq, &descno, portMAX_DELAY);
+
+        switch (_st3m_gfx_mode) {
+            case st3m_gfx_8bpp:
+            case st3m_gfx_24bpp:
+            case st3m_gfx_32bpp:
+                flow3r_bsp_display_send_fb(st3m_overlay_fb, _st3m_gfx_mode);
+                break;
+            case st3m_gfx_overlay:
+                flow3r_bsp_display_send_fb(st3m_overlay_fb, 32);
+                break;
+            case st3m_gfx_default:
+                ctx_render_ctx(user_ctx, fb_ctx);
+                if (_st3m_overlay_y1 != _st3m_overlay_y0)
+                    st3m_ctx_merge_overlay(
+                        fb,
+                        st3m_overlay_fb + 4 * ((_st3m_overlay_y0 - OVERLAY_Y) *
+                                                   OVERLAY_WIDTH +
+                                               (_st3m_overlay_x0 - OVERLAY_X)),
+                        OVERLAY_WIDTH * 4, st3m_overlay_backup,
+                        _st3m_overlay_x0, _st3m_overlay_y0,
+                        _st3m_overlay_x1 - _st3m_overlay_x0 + 1,
+                        _st3m_overlay_y1 - _st3m_overlay_y0 + 1);
+                flow3r_bsp_display_send_fb(fb, 16);
+                if (_st3m_overlay_y1 != _st3m_overlay_y0)
+                    st3m_ctx_unmerge_overlay(
+                        fb, st3m_overlay_backup, _st3m_overlay_x0,
+                        _st3m_overlay_y0,
+                        _st3m_overlay_x1 - _st3m_overlay_x0 + 1,
+                        _st3m_overlay_y1 - _st3m_overlay_y0 + 1);
+                break;
+            case st3m_gfx_16bpp:
+                flow3r_bsp_display_send_fb(fb, 16);
+                break;
+        }
+        ctx_drawlist_clear(user_ctx);
+
+        xQueueSend(user_ctx_freeq, &descno, portMAX_DELAY);
         st3m_counter_rate_sample(&rast_rate);
         float rate = 1000000.0 / st3m_counter_rate_average(&rast_rate);
         smoothed_fps = smoothed_fps * 0.9 + 0.1 * rate;
@@ -338,181 +401,139 @@ void st3m_gfx_show_textview(st3m_gfx_textview_t *tv) {
         return;
     }
 
-    st3m_ctx_desc_t *target = st3m_gfx_drawctx_free_get(portMAX_DELAY);
+    st3m_gfx_drawctx_free_get(portMAX_DELAY);
 
-    ctx_save(target->ctx);
+    ctx_save(user_ctx);
 
     // Draw background.
-    ctx_rgb(target->ctx, 0, 0, 0);
-    ctx_rectangle(target->ctx, -120, -120, 240, 240);
-    ctx_fill(target->ctx);
+    ctx_rgb(user_ctx, 0, 0, 0);
+    ctx_rectangle(user_ctx, -120, -120, 240, 240);
+    ctx_fill(user_ctx);
 
-    st3m_gfx_flow3r_logo(target->ctx, 0, -30, 150);
+    st3m_gfx_flow3r_logo(user_ctx, 0, -30, 150);
 
     int y = 20;
 
-    ctx_gray(target->ctx, 1.0);
-    ctx_text_align(target->ctx, CTX_TEXT_ALIGN_CENTER);
-    ctx_text_baseline(target->ctx, CTX_TEXT_BASELINE_MIDDLE);
-    ctx_font_size(target->ctx, 20.0);
+    ctx_gray(user_ctx, 1.0);
+    ctx_text_align(user_ctx, CTX_TEXT_ALIGN_CENTER);
+    ctx_text_baseline(user_ctx, CTX_TEXT_BASELINE_MIDDLE);
+    ctx_font_size(user_ctx, 20.0);
 
     // Draw title, if any.
     if (tv->title != NULL) {
-        ctx_move_to(target->ctx, 0, y);
-        ctx_text(target->ctx, tv->title);
+        ctx_move_to(user_ctx, 0, y);
+        ctx_text(user_ctx, tv->title);
         y += 20;
     }
 
-    ctx_font_size(target->ctx, 15.0);
-    ctx_gray(target->ctx, 0.8);
+    ctx_font_size(user_ctx, 15.0);
+    ctx_gray(user_ctx, 0.8);
 
     // Draw messages.
     const char **lines = tv->lines;
     if (lines != NULL) {
         while (*lines != NULL) {
             const char *text = *lines++;
-            ctx_move_to(target->ctx, 0, y);
-            ctx_text(target->ctx, text);
+            ctx_move_to(user_ctx, 0, y);
+            ctx_text(user_ctx, text);
             y += 15;
         }
     }
 
     // Draw version.
-    ctx_font_size(target->ctx, 15.0);
-    ctx_gray(target->ctx, 0.6);
-    ctx_move_to(target->ctx, 0, 100);
-    ctx_text(target->ctx, st3m_version);
+    ctx_font_size(user_ctx, 15.0);
+    ctx_gray(user_ctx, 0.6);
+    ctx_move_to(user_ctx, 0, 100);
+    ctx_text(user_ctx, st3m_version);
 
-    ctx_restore(target->ctx);
+    ctx_restore(user_ctx);
 
-    st3m_gfx_drawctx_pipe_put(target);
+    st3m_gfx_drawctx_pipe_put();
 }
 
 void st3m_gfx_init(void) {
     // Make sure we're not being re-initialized.
-    assert(framebuffer_freeq == NULL);
-
-    st3m_counter_rate_init(&blit_rate);
-    st3m_counter_timer_init(&blit_read_time);
-    st3m_counter_timer_init(&blit_work_time);
-    st3m_counter_timer_init(&blit_write_time);
 
     st3m_counter_rate_init(&rast_rate);
-    st3m_counter_timer_init(&rast_read_fb_time);
-    st3m_counter_timer_init(&rast_read_dctx_time);
-    st3m_counter_timer_init(&rast_work_time);
-    st3m_counter_timer_init(&rast_write_time);
 
     flow3r_bsp_display_init();
 
-    // Create framebuffer queues.
-    framebuffer_freeq = xQueueCreate(ST3M_GFX_NBUFFERS + 1, sizeof(int));
-    assert(framebuffer_freeq != NULL);
-    framebuffer_blitq = xQueueCreate(ST3M_GFX_NBUFFERS + 1, sizeof(int));
-    assert(framebuffer_blitq != NULL);
-
     // Create drawlist ctx queues.
-    dctx_freeq = xQueueCreate(ST3M_GFX_NCTX + 1, sizeof(int));
-    assert(dctx_freeq != NULL);
-    dctx_rastq = xQueueCreate(ST3M_GFX_NCTX + 1, sizeof(int));
-    assert(dctx_rastq != NULL);
-
-    for (int i = 0; i < ST3M_GFX_NBUFFERS; i++) {
-        // Setup framebuffer descriptor.
-        st3m_framebuffer_desc_t *fb_desc = &framebuffer_descs[i];
-        fb_desc->num = i;
-        fb_desc->ctx = ctx_new_for_framebuffer(
-            fb_desc->buffer, FLOW3R_BSP_DISPLAY_WIDTH,
-            FLOW3R_BSP_DISPLAY_HEIGHT, FLOW3R_BSP_DISPLAY_WIDTH * 2,
-            CTX_FORMAT_RGB565_BYTESWAPPED);
-        if (i) {
-            ctx_set_texture_source(fb_desc->ctx, framebuffer_descs[0].ctx);
-            ctx_set_texture_cache(fb_desc->ctx, framebuffer_descs[0].ctx);
-        }
-        assert(fb_desc->ctx != NULL);
-        // translate x and y by 120 px to have (0,0) at center of the screen
-        int32_t offset_x = FLOW3R_BSP_DISPLAY_WIDTH / 2;
-        int32_t offset_y = FLOW3R_BSP_DISPLAY_HEIGHT / 2;
-        ctx_apply_transform(fb_desc->ctx, 1, 0, offset_x, 0, 1, offset_y, 0, 0,
-                            1);
-
-        // Push descriptor to freeq.
-        BaseType_t res = xQueueSend(framebuffer_freeq, &i, 0);
-        assert(res == pdTRUE);
-    }
-
-    for (int i = 0; i < ST3M_GFX_NCTX; i++) {
-        // Setup dctx descriptor.
-        st3m_ctx_desc_t *dctx_desc = &dctx_descs[i];
-        dctx_desc->num = i;
-        dctx_desc->ctx = ctx_new_drawlist(FLOW3R_BSP_DISPLAY_WIDTH,
-                                          FLOW3R_BSP_DISPLAY_HEIGHT);
-        ctx_set_texture_cache(dctx_desc->ctx, framebuffer_descs[0].ctx);
-        assert(dctx_desc->ctx != NULL);
-
-        // Push descriptor to freeq.
-        BaseType_t res = xQueueSend(dctx_freeq, &i, 0);
-        assert(res == pdTRUE);
-    }
-
-    // Start rast.
-    BaseType_t res = xTaskCreate(st3m_gfx_rast_task, "rast", 8192, NULL,
-                                 ESP_TASK_PRIO_MIN + 1, &rast_task);
+    user_ctx_freeq = xQueueCreate(1 + 1, sizeof(int));
+    assert(user_ctx_freeq != NULL);
+    user_ctx_rastq = xQueueCreate(1 + 1, sizeof(int));
+    assert(user_ctx_rastq != NULL);
+
+    // Setup framebuffer descriptor.
+    fb_ctx = ctx_new_for_framebuffer(
+        fb, FLOW3R_BSP_DISPLAY_WIDTH, FLOW3R_BSP_DISPLAY_HEIGHT,
+        FLOW3R_BSP_DISPLAY_WIDTH * 2, CTX_FORMAT_RGB565_BYTESWAPPED);
+    assert(fb_ctx != NULL);
+    // translate x and y by 120 px to have (0,0) at center of the screen
+    int32_t offset_x = FLOW3R_BSP_DISPLAY_WIDTH / 2;
+    int32_t offset_y = FLOW3R_BSP_DISPLAY_HEIGHT / 2;
+    ctx_apply_transform(fb_ctx, 1, 0, offset_x, 0, 1, offset_y, 0, 0, 1);
+
+    // Setup user_ctx descriptor.
+    user_ctx =
+        ctx_new_drawlist(FLOW3R_BSP_DISPLAY_WIDTH, FLOW3R_BSP_DISPLAY_HEIGHT);
+    assert(user_ctx != NULL);
+    ctx_set_texture_cache(user_ctx, fb_ctx);
+
+    // Push descriptor to freeq.
+    int i = 0;
+    BaseType_t res = xQueueSend(user_ctx_freeq, &i, 0);
+    assert(res == pdTRUE);
+
+    // Start rasterization, compositing and scan-out
+    res = xTaskCreate(st3m_gfx_task, "graphics", 8192, NULL,
+                      ESP_TASK_PRIO_MIN + 1, &graphics_task);
     assert(res == pdPASS);
 }
 
-static st3m_ctx_desc_t *st3m_gfx_drawctx_free_get(TickType_t ticks_to_wait) {
+static Ctx *st3m_gfx_drawctx_free_get(TickType_t ticks_to_wait) {
     int descno;
-    BaseType_t res = xQueueReceive(dctx_freeq, &descno, ticks_to_wait);
+    BaseType_t res = xQueueReceive(user_ctx_freeq, &descno, ticks_to_wait);
     if (res != pdTRUE) {
         return NULL;
     }
-    return &dctx_descs[descno];
+    return user_ctx;
 }
 
-static void st3m_gfx_drawctx_pipe_put(st3m_ctx_desc_t *desc) {
-    xQueueSend(dctx_rastq, &desc->num, portMAX_DELAY);
+static void st3m_gfx_drawctx_pipe_put(void) {
+    int i = 0;
+    xQueueSend(user_ctx_rastq, &i, portMAX_DELAY);
 }
 
 uint8_t st3m_gfx_drawctx_pipe_full(void) {
-    return uxQueueSpacesAvailable(dctx_rastq) == 0;
+    return uxQueueSpacesAvailable(user_ctx_rastq) == 0;
 }
 
 void st3m_gfx_flush(void) {
     ESP_LOGW(TAG, "Pipeline flush/reset requested...");
     // Drain all workqs and freeqs.
-    xQueueReset(dctx_freeq);
-    xQueueReset(dctx_rastq);
-    xQueueReset(framebuffer_freeq);
-    xQueueReset(framebuffer_blitq);
+    xQueueReset(user_ctx_freeq);
+    xQueueReset(user_ctx_rastq);
 
     // Delay, making sure pipeline tasks have returned all used descriptors. One
     // second is enough to make sure we've processed everything.
     vTaskDelay(1000 / portTICK_PERIOD_MS);
 
     // And drain again.
-    xQueueReset(framebuffer_freeq);
-    xQueueReset(dctx_freeq);
+    xQueueReset(user_ctx_freeq);
 
-    // Now, re-submit all descriptors to freeqs.
-    for (int i = 0; i < ST3M_GFX_NBUFFERS; i++) {
-        BaseType_t res = xQueueSend(framebuffer_freeq, &i, 0);
-        assert(res == pdTRUE);
-    }
-    for (int i = 0; i < ST3M_GFX_NCTX; i++) {
-        BaseType_t res = xQueueSend(dctx_freeq, &i, 0);
-        assert(res == pdTRUE);
-    }
+    int i = 0;
+    BaseType_t res = xQueueSend(user_ctx_freeq, &i, 0);
+    assert(res == pdTRUE);
     ESP_LOGW(TAG, "Pipeline flush/reset done.");
 }
 
-static int st3m_dctx_no = 0;  // always 0 - but this keeps pipelined
-                              // rendering working as a compile time opt-in
+static int st3m_user_ctx_no = 0;  // always 0 - but this keeps pipelined
+                                  // rendering working as a compile time opt-in
 Ctx *st3m_ctx(TickType_t ticks_to_wait) {
-    st3m_ctx_desc_t *foo = st3m_gfx_drawctx_free_get(ticks_to_wait);
-    if (!foo) return NULL;
-    st3m_dctx_no = foo->num;
-    return foo->ctx;
+    Ctx *ctx = st3m_gfx_drawctx_free_get(ticks_to_wait);
+    if (!ctx) return NULL;
+    return ctx;
 }
 
 void st3m_overlay_clear(void) {
@@ -526,19 +547,19 @@ void st3m_overlay_clear(void) {
 }
 
 Ctx *st3m_overlay_ctx(void) {
-    if (!_st3m_overlay_ctx) {
-        Ctx *ctx = _st3m_overlay_ctx = ctx_new_for_framebuffer(
+    if (!overlay_ctx) {
+        Ctx *ctx = overlay_ctx = ctx_new_for_framebuffer(
             st3m_overlay_fb, OVERLAY_WIDTH, OVERLAY_HEIGHT, OVERLAY_WIDTH * 4,
             CTX_FORMAT_RGBA8);
 
         ctx_translate(ctx, 120 - OVERLAY_X, 120 - OVERLAY_Y);
         memset(st3m_overlay_fb, 0, sizeof(st3m_overlay_fb));
     }
-    return _st3m_overlay_ctx;
+    return overlay_ctx;
 }
 
 void st3m_ctx_end_frame(Ctx *ctx) {
-    xQueueSend(dctx_rastq, &st3m_dctx_no, portMAX_DELAY);
+    xQueueSend(user_ctx_rastq, &st3m_user_ctx_no, portMAX_DELAY);
 }
 
 void st3m_gfx_overlay_clip(int x0, int y0, int x1, int y1) {
diff --git a/components/st3m/st3m_gfx.h b/components/st3m/st3m_gfx.h
index 5b394dac2540eace90c4e0523fa58eb4a747313c..dd9ff51d29d1661cb07414f4116a06fce7e0b375 100644
--- a/components/st3m/st3m_gfx.h
+++ b/components/st3m/st3m_gfx.h
@@ -7,9 +7,38 @@
 #include "ctx.h"
 // clang-format on
 
-Ctx *st3m_overlay_ctx(void);
+// There are three separate graphics modes that can be set, on application
+// exit RGBA8_over_RGB565_BYTESWAPPED should be restored.
+//
+// The two other modes cause a scan-out of without compositing/clipping
+typedef enum {
+    st3m_gfx_default = 0,
+    st3m_gfx_8bpp = 8,
+    st3m_gfx_16bpp = 16,
+    st3m_gfx_24bpp = 24,
+    st3m_gfx_32bpp = 32,
+    st3m_gfx_overlay = 128,
+} st3m_gfx_mode;
+
+// sets the current graphics mode
+void st3m_set_gfx_mode(st3m_gfx_mode mode);
+
+st3m_gfx_mode st3m_get_gfx_mode(void);
+
+uint8_t *st3m_gfx_fb(st3m_gfx_mode mode);
+
+void st3m_gfx_set_palette(uint8_t *pal_in, int count);
+
+// specifies the corners of the clipping rectangle
+// for compositing overlay
+void st3m_gfx_overlay_clip(int x0, int y0, int x1, int y1);
+
+// returns a running average of fps
+float st3m_gfx_fps(void);
+
+Ctx *st3m_overlay_ctx(void);              // XXX to be removed
+Ctx *st3m_ctx(TickType_t ticks_to_wait);  // XXX: will get mode as arg
 
-Ctx *st3m_ctx(TickType_t ticks_to_wait);
 void st3m_ctx_end_frame(Ctx *ctx);  // temporary, signature compatible
                                     // with ctx_end_frame()
 
@@ -17,13 +46,6 @@ void st3m_ctx_end_frame(Ctx *ctx);  // temporary, signature compatible
 // crtx/blitter pipeline.
 void st3m_gfx_init(void);
 
-// A drawlist ctx descriptor, pointing to a drawlist-backed Ctx.
-typedef struct {
-    // The numeric ID of this descriptor.
-    int num;
-    Ctx *ctx;
-} st3m_ctx_desc_t;
-
 // Returns true if the rasterizaiton pipeline submission would block.
 uint8_t st3m_gfx_drawctx_pipe_full(void);
 
@@ -55,10 +77,3 @@ void st3m_gfx_splash(const char *text);
 // Draw the flow3r multi-coloured logo at coordinates x,y and with given
 // dimension (approx. bounding box size).
 void st3m_gfx_flow3r_logo(Ctx *ctx, float x, float y, float dim);
-
-// specifies the corners of the clipping rectangle
-// for compositing overlay
-void st3m_gfx_overlay_clip(int x0, int y0, int x1, int y1);
-
-// returns a running average of fps
-float st3m_gfx_fps(void);
diff --git a/python_payload/mypystubs/sys_display.pyi b/python_payload/mypystubs/sys_display.pyi
index 38e4e6325c14990a7d108f61b09d7cf4a68a04c1..6cfc507c77257765c5a476b81286e5bcb8d66672 100644
--- a/python_payload/mypystubs/sys_display.pyi
+++ b/python_payload/mypystubs/sys_display.pyi
@@ -1,6 +1,6 @@
 from ctx import Context
 
-def get_ctx() -> Context: ...
+def ctx() -> Context: ...
 def pipe_full() -> bool: ...
 def pipe_flush() -> None: ...
 def update(c: Context) -> None: ...
diff --git a/python_payload/st3m/application.py b/python_payload/st3m/application.py
index e80e1de6c9f3d0ddb4e880ebbe2445b61c0cc1a4..fd64c5108495340fb1ac8d026c2124b03cc766cb 100644
--- a/python_payload/st3m/application.py
+++ b/python_payload/st3m/application.py
@@ -16,6 +16,7 @@ import os
 import os.path
 import stat
 import sys
+import sys_display
 import random
 
 log = Log(__name__)
@@ -73,6 +74,9 @@ class Application(BaseView):
         if fully_exiting and self._wifi_preference is not None:
             st3m.wifi._onoff_wifi_update()
         super().on_exit()
+        # set the default graphics mode, this is a no-op if
+        # it is already set
+        sys_display.set_gfx_mode(0)
 
     def think(self, ins: InputState, delta_ms: int) -> None:
         super().think(ins, delta_ms)
diff --git a/python_payload/st3m/reactor.py b/python_payload/st3m/reactor.py
index 229ac48d9453085723b37a3b659655f0b90f4aed..9895334dd738f78ae2721c33d8fc177adf9044e8 100644
--- a/python_payload/st3m/reactor.py
+++ b/python_payload/st3m/reactor.py
@@ -157,7 +157,7 @@ class Reactor:
 
         # Draw!
         if self._ctx is None:
-            self._ctx = sys_display.get_ctx()
+            self._ctx = sys_display.ctx(0)
             if self._ctx is not None:
                 if self._last_ctx_get is not None:
                     diff = start - self._last_ctx_get
diff --git a/python_payload/st3m/ui/elements/overlays.py b/python_payload/st3m/ui/elements/overlays.py
index c3e64726a4f6c7209a78075e9be80b1e064ff490..f69791bcb08ae559edf4278165e263f0a49d8318 100644
--- a/python_payload/st3m/ui/elements/overlays.py
+++ b/python_payload/st3m/ui/elements/overlays.py
@@ -81,8 +81,13 @@ class Compositor(Responder):
 
     def think(self, ins: InputState, delta_ms: int) -> None:
         self.main.think(ins, delta_ms)
+        if sys_display.get_gfx_mode() != 0:
+            return
         if self._frame_skip <= 0:
-            if not settings.onoff_show_fps.value:
+            if (
+                not settings.onoff_show_fps.value
+                and not sys_display.get_gfx_mode() != 0
+            ):
                 for overlay in self._enabled_overlays():
                     overlay.think(ins, delta_ms)
 
@@ -91,10 +96,11 @@ class Compositor(Responder):
         global _clip_y0
         global _clip_x1
         global _clip_y1
-
         self.main.draw(ctx)
+        if sys_display.get_gfx_mode() != 0:
+            return
         if self._frame_skip <= 0:
-            octx = sys_display.get_overlay_ctx()
+            octx = sys_display.ctx(128)  # add symbolic name or overlay
             if settings.onoff_show_fps.value:
                 _clip_x0 = 110
                 _clip_y1 = 0
diff --git a/sim/fakes/sys_display.py b/sim/fakes/sys_display.py
index 84897e7f0e5ca54fb00616888f444a2586874c1b..b3ea8d0510662b5c64172262a52ae8e004a91d2f 100644
--- a/sim/fakes/sys_display.py
+++ b/sim/fakes/sys_display.py
@@ -9,6 +9,18 @@ def overlay_clip(x0, y0, x1, y1):
     pass
 
 
+def get_gfx_mode():
+    return 0
+
+
+def set_gfx_mode(no):
+    pass
+
+
 update = _sim.display_update
 get_ctx = _sim.get_ctx
 get_overlay_ctx = _sim.get_overlay_ctx
+
+
+def ctx(foo):
+    return _sim.get_ctx()