diff --git a/README.md b/README.md
index 0f023f7..643b0c0 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,7 @@ Name|Description
 [render](scanvideo/render)| A very dated rendering library used by [demo1](scanvideo/demo1) - avoid!
 [sprite](scanvideo/sprite)| A small sprite library used by [sprite_demo](scanvideo/scanvideo_minimal)
 [sprite_demo](scanvideo/sprite_demo)| Some bouncing Eben heads
+[test_pattern](scanvideo/test_pattern)| Display color bars
 [textmode](scanvideo/textmode)| Shows off chained DMA to generate scanlines out of glyph fragments via DMA/PIO
 
 
diff --git a/scanvideo/CMakeLists.txt b/scanvideo/CMakeLists.txt
index 51f6b0a..1168ca2 100644
--- a/scanvideo/CMakeLists.txt
+++ b/scanvideo/CMakeLists.txt
@@ -10,5 +10,6 @@ if (TARGET pico_scanvideo) # not all build types support it
     add_subdirectory(mario_tiles)
     add_subdirectory(scanvideo_minimal)
     add_subdirectory(sprite_demo)
+    add_subdirectory(test_pattern)
     add_subdirectory(textmode)
 endif()
\ No newline at end of file
diff --git a/scanvideo/scanvideo_minimal/scanvideo_minimal.c b/scanvideo/scanvideo_minimal/scanvideo_minimal.c
index fd8c6f9..6d95e93 100644
--- a/scanvideo/scanvideo_minimal/scanvideo_minimal.c
+++ b/scanvideo/scanvideo_minimal/scanvideo_minimal.c
@@ -1,3 +1,9 @@
+/*
+ * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
 #include <stdio.h>
 
 #include "pico.h"
diff --git a/scanvideo/test_pattern/CMakeLists.txt b/scanvideo/test_pattern/CMakeLists.txt
new file mode 100644
index 0000000..dc73144
--- /dev/null
+++ b/scanvideo/test_pattern/CMakeLists.txt
@@ -0,0 +1,15 @@
+if (TARGET pico_scanvideo_dpi)
+    add_executable(test_pattern
+            test_pattern.c
+            )
+
+    target_link_libraries(test_pattern PRIVATE
+            pico_multicore
+            pico_stdlib
+            pico_scanvideo_dpi)
+
+    pico_add_extra_outputs(test_pattern)
+
+    pico_enable_stdio_uart(test_pattern 1)
+    pico_enable_stdio_usb(test_pattern 1)
+endif ()
diff --git a/scanvideo/test_pattern/test_pattern.c b/scanvideo/test_pattern/test_pattern.c
new file mode 100644
index 0000000..d189920
--- /dev/null
+++ b/scanvideo/test_pattern/test_pattern.c
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2021 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include <stdio.h>
+
+#include "pico.h"
+#include "pico/stdlib.h"
+#include "pico/multicore.h"
+#include "pico/scanvideo.h"
+#include "pico/scanvideo/composable_scanline.h"
+#include "pico/sync.h"
+
+#define vga_mode vga_mode_320x240_60
+
+void core1_func();
+
+// Simple color bar program, which draws 7 colored bars: red, green, yellow, blow, magenta, cyan, white
+// Can be used to check resister DAC correctness.
+//
+// Note this program also demonstrates running video on core 1, leaving core 0 free. It supports
+// user input over USB or UART stdin, although all it does with it is invert the colors when you press SPACE
+
+static semaphore_t video_initted;
+static bool invert;
+
+int main(void) {
+    set_sys_clock_48mhz();
+    stdio_init_all();
+
+    // create a semaphore to be posted when video init is complete
+    sem_init(&video_initted, 0, 1);
+
+    // launch all the video on core 1, so it isn't affected by USB handling on core 0
+    multicore_launch_core1(core1_func);
+
+    // wait for initialization of video to be complete
+    sem_acquire_blocking(&video_initted);
+
+    puts("Color bars ready, press SPACE to invert...");
+
+    while (true) {
+        // prevent tearing when we invert - if you're astute you'll notice this actually causes
+        // a fixed tear a number of scanlines from the top. this is caused by pre-buffering of scanlines
+        // and is too detailed a topic to fix here.
+        scanvideo_wait_for_vblank();
+        int c = getchar_timeout_us(0);
+        switch (c) {
+            case ' ':
+                invert = !invert;
+                printf("Inverted: %d\n", invert);
+                break;
+        }
+    }
+}
+
+void draw_color_bar(scanvideo_scanline_buffer_t *buffer) {
+    // figure out 1/32 of the color value
+    uint line_num = scanvideo_scanline_number(buffer->scanline_id);
+    int32_t color_step = 1 + (line_num * 7 / vga_mode.height);
+    color_step = PICO_SCANVIDEO_PIXEL_FROM_RGB5(color_step & 1u, (color_step >> 1u) & 1u, (color_step >> 2u) & 1u);
+    if (invert) color_step = -color_step;
+    uint bar_width = vga_mode.width / 32;
+
+    uint16_t *p = (uint16_t *) buffer->data;
+    int32_t color = invert ? PICO_SCANVIDEO_PIXEL_FROM_RGB8(255, 255, 255) : 0;
+
+    for (uint bar = 0; bar < 32; bar++) {
+        *p++ = COMPOSABLE_COLOR_RUN;
+        *p++ = color;
+        *p++ = bar_width - 3;
+        color += color_step;
+    }
+
+    // 32 * 3, so we should be word aligned
+    assert(!(3u & (uintptr_t) p));
+
+    // black pixel to end line
+    *p++ = COMPOSABLE_RAW_1P;
+    *p++ = 0;
+    // end of line with alignment padding
+    *p++ = COMPOSABLE_EOL_SKIP_ALIGN;
+    *p++ = 0;
+
+    buffer->data_used = ((uint32_t *) p) - buffer->data;
+    assert(buffer->data_used < buffer->data_max);
+
+    buffer->status = SCANLINE_OK;
+}
+
+void core1_func() {
+    // initialize video and interrupts on core 1
+    scanvideo_setup(&vga_mode);
+    scanvideo_timing_enable(true);
+    sem_release(&video_initted);
+
+    while (true) {
+        scanvideo_scanline_buffer_t *scanline_buffer = scanvideo_begin_scanline_generation(true);
+        draw_color_bar(scanline_buffer);
+        scanvideo_end_scanline_generation(scanline_buffer);
+    }
+}