Skip to content

Display Graphics & Fonts

Each of the 72 per-key OLED displays is a 72×40 pixel monochrome canvas. Rendering uses a fork of the Adafruit GFX library, adapted for the RP2040 and the FPC-connected display array.

The rendering pipeline

  1. The firmware maintains the keycap image data in a pool of 600 images (base/overlay.coverlay_pool[600][360]), addressed indirectly through a 90 keycap slots × 16 modifier variants lookup table.
  2. The Adafruit GFX fork provides the drawing primitives used to compose text and bitmaps into the per-keycap framebuffer.
  3. On a key or state change, base/disp_array.c selects the target keycap OLED (via shift-register multiplexing) and writes the bitmap over SPI; kdisp_invert() gives instant pressed-key feedback.
  4. PolyKybdHost pushes per-key overlay content over HID using 64-byte reports (protocol v0.7.0+). Overlays are sent RLE-compressed and decompressed in the firmware — optionally offloaded to RP2040 core1 (multicore_exec.c) to keep QMK’s main loop responsive. See the HID Protocol Reference for the overlay commands, and System Model & Data Flow for diagrams of the overlay data path on an app switch and the core0/core1 decompression offload.

An overlay is 360 bytes per keycap, and the display position is keycode_slot + 90 * modifier_variant. Since protocol v12 the modifier variant is the modifier bitmask (bit 0 Ctrl, bit 1 Shift, bit 2 Alt, bit 3 GUI), giving all 16 combinations; that position indexes display_to_pool[], which maps it onto one of the 600 pool slots, so variants sharing artwork share a slot.

Font generation pipeline

Keycap fonts are not hand-rolled C arrays. They are generated by a config-driven pipeline that drives the fontconvert tool to turn TTF/OTF fonts into Adafruit GFX .h bitmap headers.

The pipeline lives in the firmware repo under keyboards/polykybd/fonts/:

FileRole
fonts/fonts.yamlSingle source of truth — an ordered list of font entries (font file, size, variant, codepoint ranges, weight, bit depth, …) grouped into categories with shared defaults. The list order is the lookup priority (first match wins on overlapping ranges).
fonts/generate_fonts.pyReads the YAML, runs fontconvert once per entry, writes one header per category to base/fonts/generated/, and composes base/fonts/gfx_used_fonts.h (the ALL_FONTS[] table). --check flags stale headers for CI.
fonts/dl-fonts.shDownloads the Noto source fonts.
fonts/gen-lang-fonts.shGenerates the standalone headers for the language-selection layer — country flag glyphs and the small label font.

To regenerate after editing fonts.yaml (needs PyYAML and fontconvert on PATH, or $FONTCONVERT):

Terminal window
cd keyboards/polykybd/fonts
./dl-fonts.sh # one-time: fetch Noto source fonts
python generate_fonts.py # regenerate headers into base/fonts/generated/
python generate_fonts.py --check # CI: verify headers are up to date

The fontconvert tool

fontconvert is a standalone C tool in the Adafruit-GFX repo (fontconvert/). It converts a TTF/OTF font into an Adafruit GFX GFXfont header, rendering each glyph (FreeType, with HarfBuzz shaping for multi-codepoint emoji/flag sequences) and dithering it to a 1-bit bitmap. The firmware-side pipeline calls it once per fonts.yaml entry — fontconvert itself does not read the config; it stays a focused single-font converter. See the repo’s fontconvert/README.md for build and usage details.

Column-native glyph bitmap format

Generated glyph bitmaps are stored column-native (OLED page order), matching the SSD1306’s page memory so the firmware can copy a whole vertical column-byte at once instead of setting pixels individually. One byte holds 8 vertical pixels; a glyph is laid out as cb = (height + 7) / 8 page-bytes per column, so it occupies width × cb whole bytes. To read pixel (x, y):

byte = bitmapOffset + x*cb + (y >> 3) bit = 1 << (y & 7) // LSB = top of the page

This is not the classic Adafruit row-major layout (bit = y*width + x, MSB-first). The canonical types are PolyColGfx / PolyColGlyph (base/fonts/gfxfont.h), with GFXfont / GFXglyph kept as compatibility aliases so the generated headers still compile. fontconvert emits this layout directly; the dither/edge/outline passes still work row-major internally and are transposed at the single per-glyph emit point.