diff --git a/repos/gems/run/text_painter.run b/repos/gems/run/text_painter.run
new file mode 100644
index 0000000000..46285ff531
--- /dev/null
+++ b/repos/gems/run/text_painter.run
@@ -0,0 +1,75 @@
+create_boot_directory
+
+import_from_depot genodelabs/src/[base_src] \
+ genodelabs/pkg/[drivers_interactive_pkg] \
+ genodelabs/src/init \
+ genodelabs/src/libc \
+ genodelabs/src/vfs \
+ genodelabs/raw/ttf-bitstream-vera-minimal
+
+install_config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+build { server/vfs test/text_painter lib/vfs/ttf }
+
+build_boot_image { vfs test-text_painter vfs_ttf.lib.so }
+
+run_genode_until forever
diff --git a/repos/gems/src/test/text_painter/main.cc b/repos/gems/src/test/text_painter/main.cc
new file mode 100644
index 0000000000..48cf91e379
--- /dev/null
+++ b/repos/gems/src/test/text_painter/main.cc
@@ -0,0 +1,228 @@
+/*
+ * \brief Playground for painting text
+ * \author Norman Feske
+ * \date 2018-03-08
+ */
+
+/*
+ * Copyright (C) 2018 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+/* Genode includes */
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* gems includes */
+#include
+#include
+#include
+#include
+
+namespace Test {
+ using namespace Genode;
+
+ typedef Surface_base::Point Point;
+ typedef Surface_base::Area Area;
+ typedef Surface_base::Rect Rect;
+ struct Main;
+};
+
+
+/**
+ * Statically linked binary data
+ */
+extern char _binary_droidsansb10_tff_start[];
+extern char _binary_default_tff_start[];
+
+
+struct Test::Main
+{
+ Env &_env;
+
+ Framebuffer::Connection _fb { _env, Framebuffer::Mode() };
+
+ Attached_dataspace _fb_ds { _env.rm(), _fb.dataspace() };
+
+ typedef Pixel_rgb565 PT;
+
+ Surface_base::Area const _size { (unsigned)_fb.mode().width(),
+ (unsigned)_fb.mode().height() };
+
+ Surface _surface { _fb_ds.local_addr(), _size };
+
+ char _glyph_buffer_array[8*1024];
+
+ Tff_font::Glyph_buffer _glyph_buffer { _glyph_buffer_array, sizeof(_glyph_buffer_array) };
+
+ Tff_font _font_1 { _binary_droidsansb10_tff_start, _glyph_buffer };
+ Tff_font _font_2 { _binary_default_tff_start, _glyph_buffer };
+
+ Attached_rom_dataspace _vera_ttf_ds { _env, "Vera.ttf" };
+
+ Heap _heap { _env.ram(), _env.rm() };
+
+ Ttf_font _font_3 { _heap, _vera_ttf_ds.local_addr(), 13 };
+
+ Attached_rom_dataspace _config { _env, "config" };
+
+ Root_directory _root { _env, _heap, _config.xml().sub_node("vfs") };
+
+ Vfs_font _font_4 { _heap, _root, "fonts/regular" };
+
+ void _refresh() { _fb.refresh(0, 0, _size.w(), _size.h()); }
+
+ Main(Env &env) : _env(env)
+ {
+ /* test positioning of text */
+ _surface.clip(Rect(Point(0, 0), _size));
+ Box_painter::paint(_surface, Rect(Point(200, 10), Area(250, 50)), Color(0, 100, 0));
+ Text_painter::paint(_surface,
+ Text_painter::Position(200, 10), _font_1,
+ Color(255, 255, 255),
+ "Text aligned at the top-left corner");
+
+ Box_painter::paint(_surface, Rect(Point(200, 100), Area(250, 50)), Color(0, 100, 0));
+ Text_painter::paint(_surface,
+ Text_painter::Position(210, (int)(100 - _font_1.baseline())), _font_1,
+ Color(255, 255, 255),
+ "Baseline of text aligned at the top");
+
+ /* test horizontal clipping boundaries */
+ _surface.clip(Rect(Point(20, 15), Area(40, 300)));
+ Box_painter::paint(_surface, Rect(Point(0, 0), _size), Color(150, 20, 10));
+
+ for (int x = 0, y = -30; y < (int)_size.h() + 30; x++, y += _font_2.bounding_box().h())
+ Text_painter::paint(_surface,
+ Text_painter::Position(x, y), _font_2,
+ Color(255, 255, 255),
+ "Text painter at work");
+
+ /* test horizontal subpixel positioning */
+ _surface.clip(Rect(Point(90, 15), Area(100, 300)));
+ Box_painter::paint(_surface, Rect(Point(0, 0), _size), Color(150, 20, 10));
+
+ for (float x = 90, y = -30; y < (int)_size.h() + 30; x += 0.2, y += _font_3.bounding_box().h())
+ Text_painter::paint(_surface,
+ Text_painter::Position(x, y), _font_3,
+ Color(255, 255, 255),
+ "This is a real textSub-=_HT-+=%@pixel positioning");
+
+ _surface.clip(Rect(Point(90, 320), Area(100, 300)));
+ Box_painter::paint(_surface, Rect(Point(0, 0), _size), Color(255, 255, 255));
+
+ for (float x = 90, y = 300; y < (int)_size.h() + 30; x += 0.2, y += _font_3.bounding_box().h())
+ Text_painter::paint(_surface,
+ Text_painter::Position(x, y), _font_3,
+ Color(0, 0, 0),
+ "This is a real textSub-=_HT-+=%@pixel positioning");
+ _refresh();
+
+ struct Lut
+ {
+ unsigned char value[256];
+
+ Lut()
+ {
+ auto fill_segment = [&] (long x1, long y1, long x2, long)
+ {
+ for (long i = x1>>8; i < x2>>8; i++) value[i] = min(255, y1>>8);
+ };
+
+ bezier(0, 0, 0, 130<<8, 256<<8, 260<<8, fill_segment, 7);
+ }
+ };
+
+ static Lut const lut;
+ _surface.clip(Rect(Point(0, 0), _size));
+
+ for (unsigned x = 0; x < 256; x++)
+ Box_painter::paint(_surface,
+ Rect(Point(x + 512, 280 - lut.value[x]), Area(1, 1)),
+ Color(255, 255, 255));
+ _refresh();
+
+ _surface.clip(Rect(Point(0, 0), _size));
+ char const *vfs_text_string = "Glyphs obtained from VFS";
+ {
+ Timer::Connection timer(_env);
+
+ unsigned long const start_us = timer.elapsed_us();
+
+ enum { ITERATIONS = 40 };
+ for (int i = 0; i < ITERATIONS; i++)
+ Text_painter::paint(_surface,
+ Text_painter::Position(260 + (i*133 % 500),
+ 320 + (i*87 % 400)),
+ _font_4, Color(150 + i*73, 0, 200),
+ "Glyphs obtained from VFS");
+
+ unsigned long const end_us = timer.elapsed_us();
+ unsigned long num_glyphs = strlen(vfs_text_string)*ITERATIONS;
+
+ log("uncached painting: ", (float)(end_us - start_us)/num_glyphs, " us/glyph");
+ _refresh();
+ }
+
+ for (size_t limit_kib = 32; limit_kib < 192; limit_kib += 16)
+ {
+ Cached_font cached_font(_heap, _font_4, Cached_font::Limit{limit_kib*1024});
+
+ Timer::Connection timer(_env);
+
+ unsigned long const start_us = timer.elapsed_us();
+
+ /* use less iterations for small cache sizes */
+ int const iterations = (limit_kib < 100) ? 200 : 2000;
+ for (int i = 0; i < iterations; i++)
+ Text_painter::paint(_surface,
+ Text_painter::Position(260 + (i*83 % 500),
+ 320 + (i*153 % 400)),
+ cached_font, Color(30, limit_kib, 150 + i*73),
+ "Glyphs obtained from VFS");
+
+ unsigned long const end_us = timer.elapsed_us();
+ unsigned long num_glyphs = strlen(vfs_text_string)*iterations;
+
+ log("cached painting: ", (float)(end_us - start_us)/num_glyphs, " us/glyph"
+ " (", cached_font.stats(), ")");
+ _refresh();
+ }
+ }
+};
+
+
+void Component::construct(Genode::Env &env)
+{
+ /*
+ * The indirect dependency from libc (via ttf_font) introduces a global
+ * constructor in the binary. (typeinfo for 'Genode::Exception').
+ *
+ * XXX Why is this ctors entry generated?
+ */
+ env.exec_static_constructors();
+
+ static Test::Main main(env);
+}
+
+
+/*
+ * Resolve symbol required by libc. It is unused as we implement
+ * 'Component::construct' directly instead of initializing the libc.
+ */
+
+#include
+
+void Libc::Component::construct(Libc::Env &) { }
+
diff --git a/repos/gems/src/test/text_painter/target.mk b/repos/gems/src/test/text_painter/target.mk
new file mode 100644
index 0000000000..3c8bee85a2
--- /dev/null
+++ b/repos/gems/src/test/text_painter/target.mk
@@ -0,0 +1,8 @@
+TARGET = test-text_painter
+SRC_CC = main.cc
+LIBS = base ttf_font vfs
+
+SRC_BIN += droidsansb10.tff default.tff
+
+vpath %.tff $(call select_from_repositories,src/app/scout/data)
+vpath %.tff $(call select_from_repositories,src/server/nitpicker)