commit 86dacb2352c09c53273aa22f05183082225ee100 Author: tototomate123 Date: Fri Oct 24 15:22:19 2025 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..499820e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +kindle_fbink_go +test-go \ No newline at end of file diff --git a/build_kindle.sh b/build_kindle.sh new file mode 100755 index 0000000..78f92d3 --- /dev/null +++ b/build_kindle.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -e + +CONTAINER_NAME=kindle_go_builder +WORKDIR=/src +OUTFILE=kindle_fbink_go + +if ! docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}\$"; then + echo "[sh] Creating container" + docker run -dit --name ${CONTAINER_NAME} debian:bookworm sleep infinity +fi + +echo "[sh] Installing toolchain inside container" +docker exec ${CONTAINER_NAME} bash -c " + set -e + apt-get update + apt-get install -y golang-go curl xz-utils build-essential gcc-arm-linux-gnueabi g++-arm-linux-gnueabi + if [ ! -d /usr/local/arm-linux-musleabi-cross ]; then + echo '[sh] Downloading musl cross toolchain...' + cd /usr/local + curl -L https://musl.cc/arm-linux-musleabi-cross.tgz | tar xz + fi +" + +echo "[sh] Copying project into container" +docker exec ${CONTAINER_NAME} rm -rf ${WORKDIR} +docker cp ./go ${CONTAINER_NAME}:${WORKDIR} + +echo "[sh] Building for ARMv7 soft-float (musl static)" +docker exec ${CONTAINER_NAME} bash -c " + set -e + export PATH=/usr/local/arm-linux-musleabi-cross/bin:\$PATH + cd ${WORKDIR} + export GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=1 CC=arm-linux-gnueabi-gcc + go build -ldflags '-linkmode external -extldflags "-static"' -o kindle_fbink_go . +" + +echo "[sh] Copying binary back to hsot" +docker cp ${CONTAINER_NAME}:${WORKDIR}/${OUTFILE} ./${OUTFILE} + +echo "[sh] Build complete: ${OUTFILE}" diff --git a/src/display.go b/src/display.go new file mode 100644 index 0000000..48758f5 --- /dev/null +++ b/src/display.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "os" + "strconv" + "strings" +) + +type DisplayController struct { + brightness BrightnessInfo +} + +type BrightnessInfo struct { + maxBrightness int + currentBrightness int + brightnessPath string +} + +const BACKLIGHT_DIR = "/sys/class/backlight/" + +func (a *App) initBrightness() { + entries, err := os.ReadDir(BACKLIGHT_DIR) + if err != nil || len(entries) != 1 { + return + } + + entry := entries[0].Name() + brightnessPath := fmt.Sprintf("%s%s/brightness", BACKLIGHT_DIR, entry) + maxPath := fmt.Sprintf("%s%s/max_brightness", BACKLIGHT_DIR, entry) + + if err := os.WriteFile(brightnessPath, []byte("256\n"), 0644); err != nil { + return + } + + maxBytes, err := os.ReadFile(maxPath) + if err != nil { + return + } + + maxVal, err := strconv.Atoi(strings.TrimSpace(string(maxBytes))) + if err != nil { + return + } + + controller := DisplayController{ + brightness: BrightnessInfo{ + maxBrightness: maxVal, + currentBrightness: 256, + brightnessPath: brightnessPath, + }, + } + fmt.Println("display controller assembled") + a.dispCtrl = controller +} + +func (a *App) setBrightness(newBrightness int) { + filepath := a.dispCtrl.brightness.brightnessPath + if filepath == "" { + fmt.Println("no filepath to change brightness") + return + } + if newBrightness > a.dispCtrl.brightness.maxBrightness { + newBrightness = a.dispCtrl.brightness.maxBrightness + } + if newBrightness < 0 { + newBrightness = 0 + } + + os.WriteFile(filepath, []byte(strconv.Itoa(newBrightness)+"\n"), 0644) + a.dispCtrl.brightness.currentBrightness = newBrightness + +} diff --git a/src/draw.go b/src/draw.go new file mode 100644 index 0000000..d6bc68a --- /dev/null +++ b/src/draw.go @@ -0,0 +1,236 @@ +package main + +import "math" + +type Vec3 struct{ x, y, z float64 } + +func project(v Vec3, w, h int, scale float64) (int, int) { + return int(v.x*scale) + w/2, int(v.y*scale) + h/2 +} +func rotate(v Vec3, ax, ay, az float64) Vec3 { + cx, sx := math.Cos(ax), math.Sin(ax) + cy, sy := math.Cos(ay), math.Sin(ay) + cz, sz := math.Cos(az), math.Sin(az) + x := v.x*cy*cz + v.y*(sx*sy*cz-cx*sz) + v.z*(cx*sy*cz+sx*sz) + y := v.x*cy*sz + v.y*(sx*sy*sz+cx*cz) + v.z*(cx*sy*sz-sx*cz) + z := v.x*-sy + v.y*sx*cy + v.z*cx*cy + return Vec3{x, y, z} +} + +func setPixel(buf []byte, stride, x, y int, color byte) { + if x < 0 || y < 0 || x >= stride { + return + } + idx := y*stride + x + if idx < 0 || idx >= len(buf) { + return + } + buf[idx] = color +} + +func drawHSpan(buf []byte, stride, x0, x1, y int, color byte) { + if x0 > x1 { + return + } + if y < 0 || y*stride >= len(buf) { + return + } + if x0 < 0 { + x0 = 0 + } + if x1 >= stride { + x1 = stride - 1 + } + row := y * stride + if row < 0 || row >= len(buf) { + return + } + for x := x0; x <= x1; x++ { + idx := row + x + if idx >= 0 && idx < len(buf) { + buf[idx] = color + } + } +} + +func drawLine(buf []byte, stride, x0, y0, x1, y1 int, color byte) { + dx := int(math.Abs(float64(x1 - x0))) + sx := -1 + if x0 < x1 { + sx = 1 + } + dy := -int(math.Abs(float64(y1 - y0))) + sy := -1 + if y0 < y1 { + sy = 1 + } + e := dx + dy + for { + if x0 >= 0 && x0 < stride && y0 >= 0 && y0*stride+x0 < len(buf) { + buf[y0*stride+x0] = color + } + if x0 == x1 && y0 == y1 { + break + } + e2 := 2 * e + if e2 >= dy { + e += dy + x0 += sx + } + if e2 <= dx { + e += dx + y0 += sy + } + } +} + +func drawRect(buf []byte, stride, x, y, w, h int, color byte) { + for j := y; j < y+h; j++ { + if j < 0 || j*stride >= len(buf) { + continue + } + row := j * stride + for i := x; i < x+w; i++ { + if i >= 0 && i < stride && row+i < len(buf) { + buf[row+i] = color + } + } + } +} + +func strokeRect(buf []byte, stride, x, y, w, h, thickness int, color byte) { + if thickness <= 0 || w <= 0 || h <= 0 { + return + } + if thickness > w/2 { + thickness = w / 2 + } + if thickness > h/2 { + thickness = h / 2 + } + drawRect(buf, stride, x, y, w, thickness, color) + drawRect(buf, stride, x, y+h-thickness, w, thickness, color) + drawRect(buf, stride, x, y, thickness, h, color) + drawRect(buf, stride, x+w-thickness, y, thickness, h, color) +} + +func fillRoundedRect(buf []byte, stride, x, y, w, h, radius int, color byte) { + if w <= 0 || h <= 0 { + return + } + if radius <= 0 { + drawRect(buf, stride, x, y, w, h, color) + return + } + maxRadius := w + if h < maxRadius { + maxRadius = h + } + maxRadius /= 2 + if radius > maxRadius { + radius = maxRadius + } + if radius <= 0 { + drawRect(buf, stride, x, y, w, h, color) + return + } + innerHeight := h - 2*radius + if innerHeight > 0 { + drawRect(buf, stride, x, y+radius, w, innerHeight, color) + } + floatR := float64(radius) + for oy := 0; oy < radius; oy++ { + dy := floatR - (float64(oy) + 0.5) + delta := floatR*floatR - dy*dy + if delta < 0 { + continue + } + span := int(math.Sqrt(delta)) + left := x + radius - span + right := x + w - radius + span - 1 + drawHSpan(buf, stride, left, right, y+oy, color) + drawHSpan(buf, stride, left, right, y+h-1-oy, color) + } +} + +func strokeRoundedRect(buf []byte, stride, x, y, w, h, thickness, radius int, color byte) { + if thickness <= 0 || w <= 0 || h <= 0 { + return + } + if thickness > w/2 { + thickness = w / 2 + } + if thickness > h/2 { + thickness = h / 2 + } + if radius <= 0 { + strokeRect(buf, stride, x, y, w, h, thickness, color) + return + } + maxRadius := w + if h < maxRadius { + maxRadius = h + } + maxRadius /= 2 + if radius > maxRadius { + radius = maxRadius + } + if radius <= 0 { + strokeRect(buf, stride, x, y, w, h, thickness, color) + return + } + innerRadius := radius - thickness + if innerRadius < 0 { + innerRadius = 0 + } + topWidth := w - 2*radius + if topWidth < 0 { + topWidth = 0 + } + if topWidth > 0 { + drawRect(buf, stride, x+radius, y, topWidth, thickness, color) + drawRect(buf, stride, x+radius, y+h-thickness, topWidth, thickness, color) + } + sideHeight := h - 2*radius + if sideHeight < 0 { + sideHeight = 0 + } + if sideHeight > 0 { + drawRect(buf, stride, x, y+radius, thickness, sideHeight, color) + drawRect(buf, stride, x+w-thickness, y+radius, thickness, sideHeight, color) + } + floatOuter := float64(radius) + floatInner := float64(innerRadius) + outerSq := floatOuter * floatOuter + innerSq := floatInner * floatInner + for oy := 0; oy < radius; oy++ { + pyTop := y + oy + pyBottom := y + h - 1 - oy + dy := floatOuter - (float64(oy) + 0.5) + outerDelta := outerSq - dy*dy + if outerDelta < 0 { + continue + } + outerSpan := int(math.Sqrt(outerDelta)) + outerLeft := x + radius - outerSpan + outerRight := x + w - radius + outerSpan - 1 + if innerRadius <= 0 { + drawHSpan(buf, stride, outerLeft, outerRight, pyTop, color) + drawHSpan(buf, stride, outerLeft, outerRight, pyBottom, color) + continue + } + innerDelta := innerSq - dy*dy + if innerDelta <= 0 { + drawHSpan(buf, stride, outerLeft, outerRight, pyTop, color) + drawHSpan(buf, stride, outerLeft, outerRight, pyBottom, color) + continue + } + innerSpan := int(math.Sqrt(innerDelta)) + innerLeft := x + radius - innerSpan + innerRight := x + w - radius + innerSpan - 1 + drawHSpan(buf, stride, outerLeft, innerLeft-1, pyTop, color) + drawHSpan(buf, stride, innerRight+1, outerRight, pyTop, color) + drawHSpan(buf, stride, outerLeft, innerLeft-1, pyBottom, color) + drawHSpan(buf, stride, innerRight+1, outerRight, pyBottom, color) + } +} diff --git a/src/fbink/fbink.h b/src/fbink/fbink.h new file mode 100644 index 0000000..3fb3202 --- /dev/null +++ b/src/fbink/fbink.h @@ -0,0 +1,1804 @@ +/* + FBInk: FrameBuffer eInker, a library to print text & images to an eInk Linux framebuffer + Copyright (C) 2018-2024 NiLuJe + SPDX-License-Identifier: GPL-3.0-or-later + + ---- + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef __FBINK_H +#define __FBINK_H + +// Because we're pretty much Linux-bound ;). +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include + +#include + +// Be friendly with C++ compilers (both GCC & Clang support __restrict__). +#ifdef __cplusplus +# define restrict __restrict__ +extern "C" { +#endif + +// Symbol visibility shenanigans... +// c.f., https://gcc.gnu.org/wiki/Visibility +#if __GNUC__ >= 4 +# define DLL_PUBLIC __attribute__((visibility("default"))) +# define DLL_LOCAL __attribute__((visibility("hidden"))) +#else +# define DLL_PUBLIC +# define DLL_LOCAL +#endif + +// Are we actually building the shared lib? +#ifdef FBINK_SHAREDLIB +# define FBINK_API DLL_PUBLIC +# define FBINK_LOCAL DLL_LOCAL +#else +# define FBINK_API +# define FBINK_LOCAL +#endif + +// +//// +// +// Magic number for automatic fbfd handling +#define FBFD_AUTO -1 +// As 0 is an invalid marker value, we can coopt it to try to retrieve our own last sent marker +#define LAST_MARKER 0U + +// NOTE: There's a dirty bit of trickery involved here to coerce enums into a specific data type (instead of an int): +// * The packed attribute, which does locally what GCC's -fshort-enums does globally, +// ensuring the enum's data type is only as wide as the actual values require. +// * A tail element with a value corresponding to the MAX of the target data type, +// mainly used to enforce an unsigned data type. +// BUT, while this "works" in C, this is essentially non-standard, and would break most automatic bindings, +// which happily assume we're not breaking the C standard, and as such expect an enum to be int-sized... +// So, in addition to that, instead of using the enum's typedef directly, we go through an explicit typedef. +// This obviously still works in C because enums are dumb, they're basically unscoped constants, +// and this makes bindings happy because they'll have an explicit typedef to rely on. +// Fun fact: this is essentially what the Go bindings were already doing ;). +// +// Supported targets +typedef enum +{ + FBINK_TARGET_LINUX = 0U, + FBINK_TARGET_KOBO, + FBINK_TARGET_KINDLE, + FBINK_TARGET_KINDLE_LEGACY, + FBINK_TARGET_CERVANTES, + FBINK_TARGET_REMARKABLE, + FBINK_TARGET_POCKETBOOK, + FBINK_TARGET_MAX = UINT8_MAX, +} __attribute__((packed)) FBINK_TARGET_E; +typedef uint8_t FBINK_TARGET_T; + +// Supported feature flags +#define FBINK_FEATURE_MINIMAL 0 +#define FBINK_FEATURE_DRAW (1 << 0) // Basic draw primitives +#define FBINK_FEATURE_BITMAP (1 << 1) // Fixed-cell font rendering, plus the base IBM font +#define FBINK_FEATURE_FONTS (1 << 2) // The full set of fixed-cell fonts +#define FBINK_FEATURE_UNIFONT (1 << 3) // Unifont for the fixed-cell font rendering +#define FBINK_FEATURE_OPENTYPE (1 << 4) // TrueType/OpenType font rendering +#define FBINK_FEATURE_IMAGE (1 << 5) // Image support +#define FBINK_FEATURE_BUTTON_SCAN (1 << 6) // Button scan support (Kobo only, deprecated) +#define FBINK_FEATURE_INPUT (1 << 7) // Input utilities (e.g., fbink_input_scan) +#define FBINK_FEATURE_FULL \ + (FBINK_FEATURE_DRAW | FBINK_FEATURE_BITMAP | FBINK_FEATURE_FONTS | FBINK_FEATURE_OPENTYPE | \ + FBINK_FEATURE_IMAGE | FBINK_FEATURE_INPUT) + +// List of available fonts +typedef enum +{ + IBM = 0U, // font8x8 + UNSCII, // unscii-8 + UNSCII_ALT, // unscii-8-alt + UNSCII_THIN, // unscii-8-thin + UNSCII_FANTASY, // unscii-8-fantasy + UNSCII_MCR, // unscii-8-mcr + UNSCII_TALL, // unscii-16 + BLOCK, // block + LEGGIE, // leggie (regular) + VEGGIE, // leggie EGA/VGA/FB + KATES, // kates (nexus) + FKP, // fkp + CTRLD, // ctrld + ORP, // orp (regular) + ORPB, // orp (bold) + ORPI, // orp (italic) + SCIENTIFICA, // scientifica (regular) + SCIENTIFICAB, // scientifica (bold) + SCIENTIFICAI, // scientifica (italic) + TERMINUS, // terminus (regular) + TERMINUSB, // terminus (bold) + FATTY, // fatty + SPLEEN, // spleen + TEWI, // tewi (medium) + TEWIB, // tewi (bold) + TOPAZ, // Topaz+ A1200 + MICROKNIGHT, // MicroKnight+ + VGA, // IBM VGA 8x16 + UNIFONT, // Unifont (single-wide glyphs only) + UNIFONTDW, // Unifont (double-wide glyphs only) + COZETTE, // Cozette + FONT_MAX = UINT8_MAX, // uint8_t +} __attribute__((packed)) FONT_INDEX_E; +typedef uint8_t FONT_INDEX_T; + +// List of supported font styles +typedef enum +{ + FNT_REGULAR = 0U, + FNT_ITALIC, + FNT_BOLD, + FNT_BOLD_ITALIC, +} FONT_STYLE_E; +// NOTE: This one is not packed to avoid breaking ABI w/ fbink_add_ot_font +typedef int FONT_STYLE_T; + +// List of available halign/valign values +typedef enum +{ + NONE = 0U, // i.e., LEFT for halign, TOP for valign + CENTER, // + EDGE, // i.e., RIGHT for halign, BOTTOM for valign + ALIGN_MAX = UINT8_MAX, // uint8_t +} __attribute__((packed)) ALIGN_INDEX_E; +typedef uint8_t ALIGN_INDEX_T; + +// List of available padding values +typedef enum +{ + NO_PADDING = 0U, + HORI_PADDING, + VERT_PADDING, + FULL_PADDING, + MAX_PADDING = UINT8_MAX, // uint8_t +} __attribute__((packed)) PADDING_INDEX_E; +typedef uint8_t PADDING_INDEX_T; + +// List of available colors in the eInk color map +// NOTE: This is split in FG & BG to ensure that the default values lead to a sane result (i.e., black on white) +typedef enum +{ + FG_BLACK = 0U, // 0x00 + FG_GRAY1, // 0x11 + FG_GRAY2, // 0x22 + FG_GRAY3, // 0x33 + FG_GRAY4, // 0x44 + FG_GRAY5, // 0x55 + FG_GRAY6, // 0x66 + FG_GRAY7, // 0x77 + FG_GRAY8, // 0x88 + FG_GRAY9, // 0x99 + FG_GRAYA, // 0xAA + FG_GRAYB, // 0xBB + FG_GRAYC, // 0xCC + FG_GRAYD, // 0xDD + FG_GRAYE, // 0xEE + FG_WHITE, // 0xFF + FG_MAX = UINT8_MAX, // uint8_t +} __attribute__((packed)) FG_COLOR_INDEX_E; +typedef uint8_t FG_COLOR_INDEX_T; + +typedef enum +{ + BG_WHITE = 0U, + BG_GRAYE, + BG_GRAYD, + BG_GRAYC, + BG_GRAYB, + BG_GRAYA, + BG_GRAY9, + BG_GRAY8, + BG_GRAY7, + BG_GRAY6, + BG_GRAY5, + BG_GRAY4, + BG_GRAY3, + BG_GRAY2, + BG_GRAY1, + BG_BLACK, + BG_MAX = UINT8_MAX, // uint8_t +} __attribute__((packed)) BG_COLOR_INDEX_E; +typedef uint8_t BG_COLOR_INDEX_T; + +// List of Cervantes device IDs (HWConfig PCB index) +typedef enum +{ + DEVICE_CERVANTES_TOUCH = 22U, + DEVICE_CERVANTES_TOUCHLIGHT = 23U, + DEVICE_CERVANTES_2013 = 33U, + DEVICE_CERVANTES_3 = 51U, + DEVICE_CERVANTES_4 = 68U, + DEVICE_CERVANTES_MAX = UINT16_MAX, // uint16_t +} __attribute__((packed)) CERVANTES_DEVICE_ID_E; + +// List of Kobo device IDs +typedef enum +{ + DEVICE_KOBO_TOUCH_A = 300U, // Not an actual Kobo ID, Nickel bundles it with the B + DEVICE_KOBO_TOUCH_B = 310U, + DEVICE_KOBO_TOUCH_C = 320U, + DEVICE_KOBO_MINI = 340U, + DEVICE_KOBO_GLO = 330U, + DEVICE_KOBO_GLO_HD = 371U, + DEVICE_TOLINO_SHINE_2HD = DEVICE_KOBO_GLO_HD + 300U, + DEVICE_KOBO_TOUCH_2 = 372U, + DEVICE_KOBO_AURA = 360U, + DEVICE_KOBO_AURA_HD = 350U, + DEVICE_KOBO_AURA_H2O = 370U, + DEVICE_KOBO_AURA_H2O_2 = 374U, + DEVICE_KOBO_AURA_H2O_2_R2 = 378U, + DEVICE_KOBO_AURA_ONE = 373U, + DEVICE_KOBO_AURA_ONE_LE = 381U, + DEVICE_KOBO_AURA_SE = 375U, + DEVICE_TOLINO_VISION = DEVICE_KOBO_AURA_SE + 300U, + DEVICE_KOBO_AURA_SE_R2 = 379U, + DEVICE_KOBO_CLARA_HD = 376U, + DEVICE_TOLINO_SHINE_3 = DEVICE_KOBO_CLARA_HD + 300U, + DEVICE_KOBO_FORMA = 377U, + DEVICE_TOLINO_EPOS_2 = DEVICE_KOBO_FORMA + 300U, + DEVICE_KOBO_FORMA_32GB = 380U, + DEVICE_KOBO_LIBRA_H2O = 384U, + DEVICE_TOLINO_VISION_5 = DEVICE_KOBO_LIBRA_H2O + 300U, + DEVICE_KOBO_NIA = 382U, + DEVICE_KOBO_ELIPSA = 387U, + DEVICE_KOBO_LIBRA_2 = 388U, + DEVICE_KOBO_SAGE = 383U, + DEVICE_TOLINO_EPOS_3 = DEVICE_KOBO_SAGE + 300U, + DEVICE_KOBO_CLARA_2E = 386U, + DEVICE_KOBO_ELIPSA_2E = 389U, + DEVICE_KOBO_LIBRA_COLOUR = 390U, + DEVICE_TOLINO_VISION_COLOR = DEVICE_KOBO_LIBRA_COLOUR + 300U, + DEVICE_KOBO_CLARA_BW = 391U, + DEVICE_TOLINO_SHINE_BW = DEVICE_KOBO_CLARA_BW + 300U, + DEVICE_KOBO_CLARA_COLOUR = 393U, + DEVICE_TOLINO_SHINE_COLOR = DEVICE_KOBO_CLARA_COLOUR + 300U, + DEVICE_KOBO_CLARA_BW_TPV = 395U, + DEVICE_KOBO_MAX = UINT16_MAX, // uint16_t +} __attribute__((packed)) KOBO_DEVICE_ID_E; + +// List of device IDs for mainline kernels +// c.f., https://github.com/NiLuJe/FBInk/issues/70#issuecomment-1242274710 for Tolinos +typedef enum +{ + DEVICE_MAINLINE_TOLINO_SHINE_2HD = ('T' << 8U) | ('o' << 8U) | ('l' << 8U) | DEVICE_KOBO_GLO_HD, + DEVICE_MAINLINE_TOLINO_SHINE_3 = ('T' << 8U) | ('o' << 8U) | ('l' << 8U) | DEVICE_KOBO_CLARA_HD, + DEVICE_MAINLINE_TOLINO_VISION = ('T' << 8U) | ('o' << 8U) | ('l' << 8U) | DEVICE_KOBO_AURA_SE, + DEVICE_MAINLINE_TOLINO_VISION_5 = ('T' << 8U) | ('o' << 8U) | ('l' << 8U) | DEVICE_KOBO_LIBRA_H2O, + DEVICE_MAINLINE_GENERIC_IMX5 = ('i' << 8U) | ('.' << 8U) | 'M' | 'X' | '5', + DEVICE_MAINLINE_GENERIC_IMX6 = ('i' << 8U) | ('.' << 8U) | 'M' | 'X' | '6', + DEVICE_MAINLINE_GENERIC_SUNXI_B300 = ('A' << 8U) | ('W' << 8U) | 'B' | '3' | '0' | '0', + DEVICE_MAINLINE_MAX = UINT16_MAX, // uint16_t +} __attribute__((packed)) MAINLINE_DEVICE_ID_E; + +// List of reMarkable device IDs +typedef enum +{ + DEVICE_REMARKABLE_1 = 1U, + DEVICE_REMARKABLE_2 = 2U, + DEVICE_REMARKABLE_MAX = UINT16_MAX, // uint16_t +} __attribute__((packed)) REMARKABLE_DEVICE_ID_E; + +// List of PocketBook device IDs +typedef enum +{ + DEVICE_POCKETBOOK_MINI = 515U, + DEVICE_POCKETBOOK_606 = 606U, + DEVICE_POCKETBOOK_611 = 611U, + DEVICE_POCKETBOOK_613 = 613U, + DEVICE_POCKETBOOK_614 = 614U, + DEVICE_POCKETBOOK_615 = 615U, + DEVICE_POCKETBOOK_616 = 616U, + DEVICE_POCKETBOOK_617 = 617U, + DEVICE_POCKETBOOK_618 = 618U, + DEVICE_POCKETBOOK_TOUCH = 622U, + DEVICE_POCKETBOOK_LUX = 623U, + DEVICE_POCKETBOOK_BASIC_TOUCH = 624U, + DEVICE_POCKETBOOK_BASIC_TOUCH_2 = 625U, + DEVICE_POCKETBOOK_LUX_3 = 626U, + DEVICE_POCKETBOOK_LUX_4 = 627U, + DEVICE_POCKETBOOK_LUX_5 = 628U, + DEVICE_POCKETBOOK_VERSE = 629U, + DEVICE_POCKETBOOK_SENSE = 630U, + DEVICE_POCKETBOOK_TOUCH_HD = 631U, + DEVICE_POCKETBOOK_TOUCH_HD_PLUS = 632U, + DEVICE_POCKETBOOK_COLOR = 633U, + DEVICE_POCKETBOOK_VERSE_PRO = 634U, + DEVICE_POCKETBOOK_VERSE_PRO_COLOR = 634U | 'K', + DEVICE_POCKETBOOK_AQUA = 640U, + DEVICE_POCKETBOOK_AQUA2 = 641U, + DEVICE_POCKETBOOK_ULTRA = 650U, + DEVICE_POCKETBOOK_ERA = 700U, + DEVICE_POCKETBOOK_ERA_COLOR = 700U | 'K', + DEVICE_POCKETBOOK_INKPAD_3 = 740U, + DEVICE_POCKETBOOK_INKPAD_3_PRO = 742U, + DEVICE_POCKETBOOK_INKPAD_COLOR = 741U, + DEVICE_POCKETBOOK_INKPAD_COLOR_2 = 743U | 'C', + DEVICE_POCKETBOOK_INKPAD_COLOR_3 = 743U | 'K', + DEVICE_POCKETBOOK_INKPAD = 840U, + DEVICE_POCKETBOOK_INKPAD_X = 1040U, + DEVICE_POCKETBOOK_INKPAD_4 = 743U | 'G', + DEVICE_POCKETBOOK_COLOR_LUX = + ('C' << 8U) | ('o' << 8U) | ('l' << 8U) | ('o' << 8U) | ('r' << 8U) | 'L' | 'u' | 'x', + DEVICE_POCKETBOOK_INKPAD_LITE = 970U, + DEVICE_POCKETBOOK_MAX = UINT16_MAX, // uint16_t +} __attribute__((packed)) POCKETBOOK_DEVICE_ID_E; + +// If device detection failed... +#define DEVICE_UNKNOWN 0U + +// NOTE: There's no enum for Kindles, because there are an insane number of device IDs per model, +// so it doesn't really fit into this model. Use the deviceName instead. +typedef uint16_t DEVICE_ID_T; + +// List of *potentially* available waveform modes. +// NOTE: On mxcfb EPDC v1 (as well as all Kindle) & most MTK devices, REAGL & REAGLD generally expect to *always* be flashing. +// The same is also true for Kaleido-specific modes (GCC16 & GLRC16). +// This is currently left at your own discretion, though. +// c.f., https://github.com/NiLuJe/FBInk/commit/32acece78f7cc92b06faa4a668feead260b8ce24 +// See also the comments around the relevant refresh_* functions in fbink.c +// NOTE: On very old devices (e.g., Kobo Mk. 3 & 4; possibly early PB), only AUTO, DU & GC16 may be relied on. +// GC4 will probably behave, but A2 & GL16 are not a given at all: +// e.g., GL16 is actively broken on Kobo <= Mk. 4: c.f., https://github.com/baskerville/plato/issues/158#issuecomment-787520759. +// If a waveform mode produces unexpected/broken results, and/or if you start to hit unexpected EPDC timeouts (or even an OOPS), +// that's usually a strong hint that you're trying to use something you shouldn't ;). +// NOTE: See the various mxcfb headers in the eink folder for more details about what's available on your platform. +// Platform-specific quirks, if any, are also commented upon in the relevant refresh_* functions in fbink.c +// NOTE: If you're curious about how to deal with all this stuff in practice for real world use-cases, +// I'd recommend looking at my various comments in the KOReader backends for some more context, +// c.f., https://github.com/koreader/koreader-base/blob/master/ffi/framebuffer_mxcfb.lua +typedef enum +{ + WFM_AUTO = 0U, // Let the EPDC choose, via histogram analysis of the refresh region. + // May *not* always (or ever) opt to use REAGL on devices where it is otherwise available. + // This is the default. + // If you request a flashing update w/ AUTO, FBInk automatically uses GC16 instead. + // NOTE: On sunxi SoCs, this analysis is done on CPU, instead of by the PxP. + // As such, it's going to be slower. Prefer explicitly choosing a mode instead. + // (When in doubt, GL16 is usually a good middle ground). + // Common + WFM_DU, // From any to B&W, fast (~260ms), some light ghosting. + // On-screen pixels will be left as-is for new content that is *not* B&W. + // Great for UI highlights, or tracing touch/pen input. + // Will never flash. + // DU stands for "Direct Update". + WFM_GC16, // From any to any, ~450ms, high fidelity (i.e., lowest risk of ghosting). + // Ideal for image content. + // If flashing, will flash and update the full region. + // If not, only changed pixels will update. + // GC stands for "Grayscale Clearing" + WFM_GC4, // From any to B/W/GRAYA/GRAY5, (~290ms), some ghosting. (may be implemented as DU4 on some devices). + // Will *probably* never flash, especially if the device doesn't implement any other 4 color modes. + // Limited use-cases in practice. + WFM_A2, // From B&W to B&W, fast (~120ms), some ghosting. + // On-screen pixels will be left as-is for new content that is *not* B&W. + // FBInk will ask the EPDC to enforce quantization to B&W to honor the "to" requirement, + // (via EPDC_FLAG_FORCE_MONOCHROME). + // Will never flash. + // Consider bracketing a series of A2 refreshes between white screens to transition in and out of A2, + // so as to honor the "from" requirement, + // (especially given that FORCE_MONOCHROME may not be reliably able to do so, c.f., refresh_kobo_mk7): + // non-flashing GC16 for the in transition, A2 or GC16 for the out transition. + // A stands for "Animation" + WFM_GL16, // From white to any, ~450ms, some ghosting. + // Typically optimized for text on a white background. + // Newer generation devices only + WFM_REAGL, // From white to any, ~450ms, with ghosting and flashing reduction. + // When available, best option for text (in place of GL16). + // May enforce timing constraints if in collision with another waveform mode, e.g., + // it may, to some extent, wait for completion of previous updates to have access to HW resources. + // Marketing term for the feature is "Regal". Technically called 5-bit waveform modes. + WFM_REAGLD, // From white to any, ~450ms, with more ghosting reduction, but less flashing reduction. + // Should only be used when flashing, which should yield a less noticeable flash than GC16. + // Rarely used in practice, because still optimized for text or lightly mixed content, + // not pure image content. + // (Mostly) Kindle only + WFM_GC16_FAST, // Better latency at the expense of lower fidelity than GC16. + WFM_GL16_FAST, // Better latency at the expense of lower fidelity than GL16. + WFM_DU4, // From any to B/W/GRAYA/GRAY5. (e.g., GC4. Will never flash. Also available on Kobo Mk. 9). + WFM_GL4, // From white to B/W/GRAYA/GRAY5. + WFM_GL16_INV, // From black to any. Optimized for text on a black background (e.g., nightmode). + // "Nightmode" waveform modes (dubbed "eclipse" in Kobo-land). + // Only available on some devices (Zelda on Kindle, Mk. 8+ on Kobo). + // If you need to check at runtime whether it's actually supported, on an i.MX board, + // check if /sys/class/graphics/fb0/waveform_mode_gck16 exists ;). + // Otherwise, refer to the hasEclipseWfm deviceQuirks. + WFM_GCK16, // From black to any. Goes hand-in-hand with GLKW16, should only be used when flashing. + WFM_GLKW16, // From black to any. Newer variant of GL16_INV. (On Kobo, Mk. 9, 11 & 12 only. It's GLK16 on sunxi). + // For documentation purposes + WFM_INIT, // May flash several times to end up with a white screen, slow (~2000ms). + WFM_UNKNOWN, + // reMarkable only + WFM_INIT2, + // PocketBook only + WFM_A2IN, + WFM_A2OUT, + WFM_GC16HQ, // Only available on i.MX SoCs. Alias for REAGL, or REAGLD when flashing. + WFM_GS16, // Only available on B288 SoCs. Fidelity supposedly somewhere between GL16 and GC16. + // Kobo Sunxi only + WFM_GU16, // GL16, but honoring the in-kernel DISP_EINK_SET_GC_CNT + //WFM_GCK16, // GC16, but for white-on-black. + WFM_GLK16, // GL16, but for white-on-black. + WFM_CLEAR, // GC16 local (NOTE: Appears to crash the EPDC... [Elipsa on FW 4.28.17826]) + WFM_GC4L, // GC4 local (NOTE: Appears to crash the EPDC... [Elipsa on FW 4.28.17826]) + // Kobo Sunxi; MTK only + WFM_GCC16, // GCC16, for color image content on Kaleido panels. + WFM_GLRC16, // GLRC16, for color highlights on text on Kaleido panels. + // Kindle MTK only + WFM_GC16_PARTIAL, // Internal use only, GC16 + PARTIAL + WFM_GCK16_PARTIAL, // Internal use only, GCK16 + PARTIAL + WFM_DUNM, // DU, but for white-on-black. + WFM_P2SW, // Internal use only, used by the swipe animation. + // Kindle MTK + CFA only + WFM_GCCK16, // Nightmode GCC16 + WFM_GLRCK16, // Nightmode GLRC16 + + WFM_MAX = UINT8_MAX, // uint8_t +} __attribute__((packed)) WFM_MODE_INDEX_E; +typedef uint8_t WFM_MODE_INDEX_T; + +// List of *potentially* available HW dithering modes +typedef enum +{ + HWD_PASSTHROUGH = 0U, + HWD_FLOYD_STEINBERG, + HWD_ATKINSON, + HWD_ORDERED, // Generally the only supported HW variant on EPDC v2 + HWD_QUANT_ONLY, + HWD_LEGACY = UINT8_MAX, // Use legacy EPDC v1 dithering instead (if available). + // Note that it is *not* offloaded to the PxP, it's purely software, in-kernel. + // Usually based on Atkinson's algo. The most useful one being the Y8->Y1 one, + // which we request with A2/DU refreshes. +} __attribute__((packed)) HW_DITHER_INDEX_E; +typedef uint8_t HW_DITHER_INDEX_T; + +// List of *potentially* available CFA post-process modes +typedef enum +{ + CFA_DEFAULT = 0U, + CFA_AIE_S4, + CFA_AIE_S7, + CFA_AIE_S9, + CFA_G0, + CFA_G1, + CFA_G2, + CFA_NTX, + CFA_NTX_SF, + CFA_SKIP, + CFA_MAX = UINT8_MAX, +} __attribute__((packed)) CFA_MODE_INDEX_E; +typedef uint8_t CFA_MODE_INDEX_T; + +// List of NTX rotation quirk types (c.f., mxc_epdc_fb_check_var @ drivers/video/fbdev/mxc/mxc_epdc_v2_fb.c)... +typedef enum +{ + NTX_ROTA_STRAIGHT = 0U, // No shenanigans (at least as far as ioctls are concerned) + NTX_ROTA_ALL_INVERTED, // Every rotation is inverted by the kernel + NTX_ROTA_ODD_INVERTED, // Only Landscape (odd) rotations are inverted by the kernel + // NOTE: Everything below this line should be considered mostly deprecated, as those were attempts to piggyback on this flag + // to handle input translations. That was a mistake, we've since switched to dedicated flags for touch input quirks. + // As the comment above implies, this is only meant to deal with the kernel screwing with us when setting the fb rotation. + NTX_ROTA_SANE, // NTX_ROTA_STRAIGHT, and ntxBootRota is the native Portrait orientation. + // Optionally, bonus points if that's actually UR, and the panel is natively mounted UR, + // like on the Kobo Libra. + // Triple whammy if the touch layer rotation matches! + NTX_ROTA_SUNXI, // The rotate flag is technically meaningless, but *may* be set by third-party code (we don't). + NTX_ROTA_CW_TOUCH, // No kernel shenanigans, and Touch panel mounted in the invert of the usual rotation. + NTX_ROTA_CCW_TOUCH, // No kernel shenanigans, and Touch panel mounted in the usual rotation. + NTX_ROTA_MAX = UINT8_MAX, // uint8_t +} __attribute__((packed)) NTX_ROTA_INDEX_E; +typedef uint8_t NTX_ROTA_INDEX_T; + +// Available states for fbink_sunxi_ntx_enforce_rota +typedef enum +{ + FORCE_ROTA_NOTSUP = INT8_MIN, // For FBInkState on non-sunxi platforms + FORCE_ROTA_CURRENT_ROTA = + -5, // Honor the gyro if it matches the working buffer's rotation; match the wb otherwise (NOTE: Requires fbdamage) + FORCE_ROTA_CURRENT_LAYOUT = + -4, // Honor the gyro if it matches the working buffer's layout; match the wb otherwise (NOTE: Requires fbdamage) + FORCE_ROTA_PORTRAIT = -3, // Honor the gyro if it matches a Portrait layout + FORCE_ROTA_LANDSCAPE = -2, // Honor the gyro if it matches a Landscape layout + FORCE_ROTA_GYRO = -1, // Honor the gyro (NOTE: default) + FORCE_ROTA_UR = 0, // FB_ROTATE_UR + FORCE_ROTA_CW = 1, // FB_ROTATE_CW + FORCE_ROTA_UD = 2, // FB_ROTATE_UD + FORCE_ROTA_CCW = 3, // FB_ROTATE_CCW + FORCE_ROTA_WORKBUF = 4, // Match the working buffer's rotation (NOTE: Requires fbdamage) + FORCE_ROTA_MAX = INT8_MAX, // int8_t +} __attribute__((packed)) SUNXI_FORCE_ROTA_INDEX_E; +typedef int8_t SUNXI_FORCE_ROTA_INDEX_T; + +// List of swipe directions for fbink_mtk_set_swipe_data +typedef enum +{ + MTK_SWIPE_DIR_DOWN = 0, + MTK_SWIPE_DIR_UP = 1, + MTK_SWIPE_DIR_LEFT = 2, + MTK_SWIPE_DIR_RIGHT = 3, + MTK_SWIPE_DIR_MAX = UINT8_MAX, // uint8_t +} __attribute__((packed)) MTK_SWIPE_DIRECTION_INDEX_E; +typedef uint8_t MTK_SWIPE_DIRECTION_INDEX_T; + +// List of halftone pattern modes for fbink_mtk_set_halftone +typedef enum +{ + MTK_HALFTONE_DISABLED = 0, + MTK_HALFTONE_DEFAULT_CHECKER_SIZE = 1, + MTK_HALFTONE_MAX_CHECKER_SIZE = INT32_MAX, // int +} __attribute__((packed)) MTK_HALFTONE_MODE_INDEX_E; +typedef int32_t MTK_HALFTONE_MODE_INDEX_T; + +// List of supported pixel formats +typedef enum +{ + FBINK_PXFMT_UNKNOWN = 0, + FBINK_PXFMT_Y4 = 4, + FBINK_PXFMT_Y8 = 8, + FBINK_PXFMT_BGR565 = 16, + FBINK_PXFMT_RGB565, + FBINK_PXFMT_BGR24 = 24, + FBINK_PXFMT_RGB24, + FBINK_PXFMT_BGRA = 32, + FBINK_PXFMT_RGBA, + FBINK_PXFMT_BGR32, + FBINK_PXFMT_RGB32, + FBINK_PXFMT_MAX = UINT8_MAX, // uint8_t +} __attribute__((packed)) FBINK_PXFMT_INDEX_E; +typedef uint8_t FBINK_PXFMT_INDEX_T; + +// +// A struct to dump FBInk's internal state into, like fbink_state_dump() would, but in C ;) +typedef struct +{ + long int user_hz; // USER_HZ (should pretty much always be 100) + const char* restrict font_name; // fbink_cfg->fontname (c.f., fontname_to_string()) + uint32_t view_width; // viewWidth (MAY be different than screen_width on devices with a viewport) + uint32_t view_height; // viewHeight (ditto) + uint32_t screen_width; // screenWidth (Effective width, c.f., is_ntx_quirky_landscape & initialize_fbink()) + uint32_t screen_height; // screenHeight (ditto) + uint32_t scanline_stride; // fInfo.line_length (scanline length in bytes, padding included) + uint32_t bpp; // vInfo.bits_per_pixel + bool inverted_grayscale; // true if vInfo.grayscale is set to GRAYSCALE_8BIT_INVERTED (@ 8bpp) + char device_name[32]; // deviceQuirks.deviceName (short common name, no brand) + char device_codename[32]; // deviceQuirks.deviceCodename + char device_platform[32]; // deviceQuirks.devicePlatform (often a codename, too) + DEVICE_ID_T device_id; // deviceQuirks.deviceId (decimal value, c.f., identify_device() on Kindle!) + uint8_t pen_fg_color; // penFGColor (Actual grayscale value, not FG_COLOR_INDEX_E) + uint8_t pen_bg_color; // penBGColor (ditto) + unsigned short int screen_dpi; // deviceQuirks.screenDPI + unsigned short int font_w; // FONTW (effective width of a glyph cell, i.e. scaled) + unsigned short int font_h; // FONTH (effective height of a glyph cell, i.e. scaled) + unsigned short int max_cols; // MAXCOLS (at current cell size) + unsigned short int max_rows; // MAXROWS (ditto) + uint8_t view_hori_origin; // viewHoriOrigin (would be non-zero on devices with a horizontal viewport) + uint8_t view_vert_origin; // viewVertOrigin (origin in px of row 0, includes viewport + viewVertOffset) + uint8_t view_vert_offset; // viewVertOffset (shift in px needed to vertically balance rows over viewHeight) + uint8_t fontsize_mult; // FONTSIZE_MULT (current cell scaling multiplier) + uint8_t glyph_width; // glyphWidth (native width of a glyph cell, i.e. unscaled) + uint8_t glyph_height; // glyphHeight (native height of a glyph cell, i.e. unscaled) + bool is_perfect_fit; // deviceQuirks.isPerfectFit (horizontal column balance is perfect over viewWidth) + bool is_mtk; // deviceQuirks.isMTK (device is running on a MediaTek SoC) + bool is_sunxi; // deviceQuirks.isSunxi (device is running on an AllWinner SoC) + bool sunxi_has_fbdamage; // sunxiCtx.has_fbdamage (true when fbdamage module is loaded) + SUNXI_FORCE_ROTA_INDEX_T sunxi_force_rota; // sunxiCtx.force_rota (current effective value) + bool is_kindle_legacy; // deviceQuirks.isKindleLegacy (device is a Kindle using the original einkfb EPDC API) + bool is_kobo_non_mt; // deviceQuirks.isKoboNonMT (device is a Kobo with no MultiTouch input support) + bool unreliable_wait_for; // deviceQuirks.unreliableWaitFor (MXCFB_WAIT_FOR_UPDATE_COMPLETE may timeout) + bool can_wake_epdc; // deviceQuirks.canWakeEPDC (i.e., fbink_wakeup_epdc is *NOT* a NOP) + uint8_t ntx_boot_rota; // deviceQuirks.ntxBootRota (Native rotation at boot) + NTX_ROTA_INDEX_T ntx_rota_quirk; // deviceQuirks.ntxRotaQuirk (c.f., tools/rota_map.c) + uint8_t rotation_map[4]; // deviceQuirks.rotationMap (index is native, value is canonical) + bool touch_swap_axes; // deviceQuirks.touchSwapAxes (panel reports swapped coordinates axes; handle this first) + bool touch_mirror_x; // deviceQuirks.touchMirrorX (post-swap, panel reports inverted x coordinates) + bool touch_mirror_y; // deviceQuirks.touchMirrorY (post-swap, panel reports inverted y coordinates) + bool is_ntx_quirky_landscape; // deviceQuirks.isNTX16bLandscape (rotation compensation is in effect) + uint8_t current_rota; // vInfo.rotate (current native rotation, ; Device rotation, not buffer!) + bool can_rotate; // deviceQuirks.canRotate (device has a gyro) + bool can_hw_invert; // deviceQuirks.canHWInvert (device can use EPDC inversion) + bool has_eclipse_wfm; // deviceQuirks.hasEclipseWfm (device can use nightmode waveform modes) + bool has_color_panel; // deviceQuirks.hasColorPanel (device features a Kaleido/CFA color panel) + FBINK_PXFMT_INDEX_T pixel_format; // deviceQuirks.pixelFormat + bool can_wait_for_submission; // deviceQuirks.canWaitForSubmission (devices supports fbink_wait_for_submission) +} FBInkState; + +// What a FBInk config should look like. Perfectly sane when fully zero-initialized. +typedef struct +{ + short int row; // y axis (i.e., line), counts down from the bottom of the screen if negative + short int col; // x axis (i.e., column), counts down from the right edge of the screen if negative + uint8_t fontmult; // Font scaling multiplier (i.e., 4 -> x4), 0 means automatic. + FONT_INDEX_T fontname; // Request a specific bundled font + bool is_inverted; // Invert colors when *drawing* stuff (e.g., text, clear & images). + // This is *NOT* mutually exclusive with is_nightmode, and is *always* supported. + bool is_flashing; // Request a black flash on refresh (e.g., UPDATE_MODE_FULL instead of PARTIAL) + bool is_cleared; // Clear the full screen beforehand (honors bg_color & is_inverted) + bool is_centered; // Center the text (horizontally) + short int hoffset; // Horizontal offset (in pixels) for text position + short int voffset; // Vertical offset (in pixels) for text position + bool is_halfway; // Vertically center the text, honoring row offsets + bool is_padded; // Pad the text with blanks (on the left, or on both sides if is_centered) + bool is_rpadded; // Right pad the text with blanks + FG_COLOR_INDEX_T fg_color; // Requested foreground color for text (palette index) + BG_COLOR_INDEX_T bg_color; // Requested background color for text (palette index) + bool is_overlay; // Don't draw bg and use inverse of fb's underlying pixel as pen fg color + bool is_bgless; // Don't draw bg (mutually exclusive with is_overlay, which will take precedence) + bool is_fgless; // Don't draw fg (takes precendence over is_overlay/is_bgless) + bool no_viewport; // Ignore viewport corrections, whether hardware-related on Kobo, or to center rows + bool is_verbose; // Print verbose diagnostic informations on stdout + bool is_quiet; // Hide fbink_init()'s hardware setup info (sent to stderr) + bool ignore_alpha; // Ignore any potential alpha channel in source image (i.e., flatten the image) + ALIGN_INDEX_T halign; // Horizontal alignment of images/dumps + ALIGN_INDEX_T valign; // Vertical alignment of images/dumps + short int scaled_width; // Output width of images/dumps (0 for no scaling, -1 for viewport width) + short int scaled_height; // Output height of images/dumps (0 for no scaling, -1 for viewport height) + // If only *one* of them is left at 0, the image's aspect ratio will be honored. + // If *either* of them is set to < -1, fit to screen while respecting AR. + // NOTE: Scaling is inherently costly. I highly recommend not relying on it, + // preferring instead proper preprocessing of your input images, + // c.f., https://www.mobileread.com/forums/showpost.php?p=3728291&postcount=17 + WFM_MODE_INDEX_T wfm_mode; // Request a specific waveform mode (defaults to AUTO) + HW_DITHER_INDEX_T dithering_mode; // Request a specific dithering mode (defaults to PASSTHROUGH) + bool sw_dithering; // Request (ordered) *software* dithering when printing an image. + // This is *NOT* mutually exclusive with dithering_mode! + CFA_MODE_INDEX_T cfa_mode; // Request a specific CFA post-process mode (defaults to NONE, relevant wfm only). + bool is_nightmode; // Request hardware inversion (via EPDC_FLAG_ENABLE_INVERSION, if supported/safe). + // This is *NOT* mutually exclusive with is_inverted! + // NOTE: If the HW doesn't support inversion, a warning is printed during init. + // If you're convinced this is in error (i.e., up to date kernel), + // you can bypass that check by setting FBINK_ALLOW_HW_INVERT in your env. + bool no_refresh; // Skip actually refreshing the eInk screen (useful when drawing in batches) + bool no_merge; // Set the EINK_NO_MERGE flag (Kobo sunxi only) + bool is_animated; // Enable refresh animation, following fbink_mtk_set_swipe_data (Kindle MTK only) + uint8_t saturation_boost; // Boost image saturation, in %. Useful on Kaleido panels. Only affects 32bpp. + bool to_syslog; // Send messages & errors to the syslog instead of stdout/stderr +} FBInkConfig; + +// Same, but for OT/TTF specific stuff. MUST be zero-initialized. +typedef struct +{ + void* font; // NOTE: This is essentially a pointer to a local FBInkOTFonts instance, + // in order to use a set of fonts specific to an FBInkOTConfig, + // via fbink_add_ot_font_v2() & fbink_free_ot_fonts_v2(). + // Consider it *private*: it needs to be NULL on init to be sane, but after that, + // it's only used & memory managed by FBInk itself (via the aforemented _v2 API), not the user. + struct + { + short int top; // Top margin in pixels (if negative, counts backwards from the bottom edge) + short int bottom; // Bottom margin in pixels (supports negative values, too) + short int left; // Left margin in pixels (if negative, counts backwards from the right edge) + short int right; // Right margin in pixels (supports negative values, too) + } margins; // Margins are to the top-left edge of the bounding box, e.g., assuming no alignment/centering, + // zero margins will lead to a bounding box flush with the top-left corner of the screen. + FONT_STYLE_T style; // Default font style to use when !is_formatted (defaults to Regular) + float size_pt; // Size of text in points. If not set (0.0f), defaults to 12pt + unsigned short int size_px; // Size of text in pixels. Optional, but takes precedence over size_pt. + bool is_centered; // Horizontal centering + PADDING_INDEX_T padding; // Pad the drawing area (i.e., paint it in the background color). + // Unlike in the fixed-cell codepath, this always applies to both sides (L&R/T&B), + // no matter the chosen axis. + // e.g., HORI_PADDING is useful to prevent overlaps when drawing + // consecutive strings on the same line(s). + bool is_formatted; // Is string "formatted"? Bold/Italic support only, markdown like syntax + bool compute_only; // Abort early after the line-break computation pass (no actual rendering). + // NOTE: This is early enough that it will *NOT* be able to predict *every* + // potential case of truncation. + // In particular, broken metrics may yield a late truncation at rendering time. + bool no_truncation; // Abort as early as possible (but not necessarily before the rendering pass), + // if the string cannot fit in the available area at the current font size. +} FBInkOTConfig; + +// Optionally used with fbink_print_ot, if you need more details about the line-breaking computations, +// for instance if you want to dynamically compute a best-fit font size for n lines in a specific area. +typedef struct +{ + unsigned short int computed_lines; // Expected amount of lines needed, according to font metrics. + unsigned short int rendered_lines; // Actually rendered amount of lines. + // Will stay 0 in case of an early abort (or a compute_only run), + // or < computed_lines in case of an unexpected truncation due to broken metrics. + struct + { + unsigned short int width; + unsigned short int height; + } bbox; // Bounding box of the string (at computation time, padding excluded). + bool truncated; // true if the string was truncated (at computation or rendering time). +} FBInkOTFit; + +// This maps to an mxcfb rectangle, used for fbink_get_last_rect, as well as in FBInkDump +// NOTE: Unlike an mxcfb rectangle, left (x) comes *before* top (y)! +typedef struct +{ + unsigned short int left; // x + unsigned short int top; // y + unsigned short int width; + unsigned short int height; +} FBInkRect; + +// For use with fbink_dump & fbink_restore +typedef struct +{ + unsigned char* restrict data; + size_t stride; + size_t size; + FBInkRect area; + FBInkRect clip; // Only restore this rectangular area of the screen (has to intersect w/ the dump's area) + uint8_t rota; + uint8_t bpp; + bool is_full; +} FBInkDump; + +// +//// +// +// NOTE: Unless otherwise specified, +// stuff returns a negative value (-(EXIT_FAILURE) by default) on failure & EXIT_SUCCESS otherwise ;). + +// Returns the version of the currently loaded FBInk library. +FBINK_API const char* fbink_version(void) __attribute__((const)); + +// Returns the target platform of the currently loaded FBInk library. +// c.f., FBINK_TARGET_E enum +FBINK_API FBINK_TARGET_T fbink_target(void) __attribute__((const)); + +// Returns a bitmask of the features available in the currently loaded FBInk library. +// c.f., FBINK_FEATURE_ defines +FBINK_API uint32_t fbink_features(void) __attribute__((const)); + +// +// Open the framebuffer character device, +// and returns the newly opened file descriptor. +FBINK_API int fbink_open(void); + +// Unmap the framebuffer (if need be) and close its file descriptor, +// (c.f., the recap at the bottom if you're concerned about mmap handling). +// fbfd: Open file descriptor to the framebuffer character device, as returned by fbink_open() +// NOTE: This is safe to call if fbfd is FBFD_AUTO (i.e., -1, which means this is also safe to call after an fbink_open failure). +FBINK_API int fbink_close(int fbfd); + +// Initialize internal variables keeping track of the framebuffer's configuration and state, as well as the device's hardware. +// MUST be called at least *once* before any fbink_print*, fbink_dump/restore, fbink_cls or fbink_grid* functions. +// CAN safely be called multiple times, +// but doing so is only necessary if the framebuffer's state has changed (although fbink_reinit is preferred in this case), +// or if you modified one of the FBInkConfig fields that affects its results (listed below). +// Returns -(ENOSYS) if the device is unsupported (NOTE: Only on reMarkable!) +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened for the duration of this call. +// fbink_cfg: Pointer to an FBInkConfig struct. +// If you wish to customize them, the fields: +// is_centered, fontmult, fontname, fg_color, bg_color, +// no_viewport, is_verbose, is_quiet & to_syslog +// MUST be set beforehand. +// This means you MUST call fbink_init() again when you update them, too! +// (This also means the effects from those fields "stick" across the lifetime of your application, +// or until a subsequent fbink_init() (or effective fbink_reinit()) call gets fed different values). +// NOTE: For fg_color & bg_color, see fbink_update_pen_colors(). +// NOTE: For is_verbose, is_quiet & to_syslog, see fbink_update_verbosity(). +// NOTE: By virtue of, well, setting global variables, do NOT consider this thread-safe. +// The rest of the API should be, though, so make sure you init in your main thread *before* threading begins... +// NOTE: If you just need to make sure the framebuffer state is still up to date before an fbink_* call, +// (e.g., because you're running on a Kobo, which may switch from 16bpp to 32bpp, or simply change orientation), +// prefer using fbink_reinit instead of calling fbink_init *again*, as it's tailored for this use case. +// c.f., KFMon for an example of this use case in the wild. +// NOTE: You can perfectly well keep a few different FBInkConfig structs around, instead of modifying the same one over and over. +// Just remember that some fields *require* an fbink_init() call to be taken into account (see above), +// but if the only fields that differ don't fall into that category, you do *NOT* need an fbink_init() per FBInkConfig... +FBINK_API int fbink_init(int fbfd, const FBInkConfig* restrict fbink_cfg) __attribute__((nonnull)); + +// +// Dump a few of our internal state variables to stdout, in a format easily consumable by a shell (i.e., eval). +FBINK_API void fbink_state_dump(const FBInkConfig* restrict fbink_cfg) __attribute__((nonnull)); + +// Dump a few of our internal state variables to the FBInkState struct pointed to by fbink_state. +// NOTE: This includes quite a few useful things related to device identification, c.f., the FBInkState struct ;). +// You can also peek at the output of fbink -e to get a hint of what the data actually looks like. +FBINK_API void fbink_get_state(const FBInkConfig* restrict fbink_cfg, FBInkState* restrict fbink_state) + __attribute__((nonnull)); + +// +// Print a string on screen. +// NOTE: The string is expected to be encoded in valid UTF-8: +// * Invalid UTF-8 sequences will be *rejected* and the call will abort early with -(EILSEQ) +// * We assume a single multibyte sequence will occupy a maximum of 4 bytes. +// c.f., my rant about Kobo's broken libc in fbink_internal.h for more details behind this choice. +// Since any decent system built in the last decade should default to UTF-8, that should be pretty much transparent... +// Returns the amount of lines printed on success (helpful when you keep track of which row you're printing to). +// Returns -(EINVAL) if string is empty. +// Returns -(EILSEQ) if string is not a valid UTF-8 sequence. +// Returns -(ENOSYS) when fixed-cell font support is disabled (MINIMAL build w/o BITMAP). +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// string: UTF-8 encoded string to print. +// fbink_cfg: Pointer to an FBInkConfig struct. +// Honors every field not specifically related to image/dump support. +FBINK_API int fbink_print(int fbfd, const char* restrict string, const FBInkConfig* restrict fbink_cfg) + __attribute__((nonnull)); + +// +// Add an OpenType font to FBInk. +// NOTE: At least one font must be added in order to use fbink_print_ot(). +// filename: Path to the font file. This should be a valid *.otf or *.ttf font. +// style: Defines the specific style of the specified font (FNT_REGULAR, FNT_ITALIC, FNT_BOLD or FNT_BOLD_ITALIC). +// NOTE: You MUST free the fonts loaded when you are done with all of them by calling fbink_free_ot_fonts(). +// NOTE: You MAY replace a font without first calling fbink_free_ot_fonts(). +// NOTE: Default fonts are secreted away in /usr/java/lib/fonts on Kindle, +// and in /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/fonts on Kobo, +// but you can't use the Kobo ones because they're obfuscated... +// Which leads me to a final, critical warning: +// NOTE: Don't try to pass non-font files or encrypted/obfuscated font files, because it *will* horribly segfault! +FBINK_API int fbink_add_ot_font(const char* filename, FONT_STYLE_T style) __attribute__((nonnull)); +// Same API and behavior, except that the set of fonts being loaded is tied to this specific FBInkOTConfig instance, +// instead of being global. +// In which case, resources MUST be released via fbink_free_ot_fonts_v2()! +// NOTE: You can mix & match the v2 and legacy API, but for every fbink_add_ot_font() there must be an fbink_free_ot_fonts(), +// and for every fbink_add_ot_font_v2(), there must be an fbink_free_ot_fonts_v2() +// (for each matching FBInkOTConfig instance). +FBINK_API int fbink_add_ot_font_v2(const char* filename, FONT_STYLE_T style, FBInkOTConfig* restrict cfg) + __attribute__((nonnull)); + +// Free all loaded OpenType fonts. You MUST call this when you have finished all OT printing. +// NOTE: Safe to call even if no fonts were actually loaded. +FBINK_API int fbink_free_ot_fonts(void); +// Same, but for a specific FBInkOTConfig instance if fbink_add_ot_font_v2 was used. +// NOTE: Safe to call even if no fonts were actually loaded, in which case it'll return -(EINVAL)! +FBINK_API int fbink_free_ot_fonts_v2(FBInkOTConfig* restrict cfg) __attribute__((nonnull)); + +// Print a string using an OpenType font. +// NOTE: The caller MUST have loaded at least one font via fbink_add_ot_font() FIRST. +// This function uses margins (in pixels) instead of rows/columns for positioning and setting the printable area. +// Returns a new top margin for use in subsequent calls, if the return value is positive. +// NOTE: A zero return value indicates there is no room left to print another row of text at the current margins or font size. +// Returns -(ERANGE) if the provided margins are out of range, or sum to < view height or width. +// Returns -(ENOSYS) when OT support is disabled (MINIMAL build w/o OPENTYPE). +// Returns -(ENODATA) if fbink_add_ot_font() hasn't been called yet. +// Returns -(EINVAL) if string is empty. +// Returns -(EILSEQ) if string is not a valid UTF-8 sequence. +// Returns -(ENOSPC) if no_truncation is true, and string needs to be truncated to fit in the available draw area. +// NOTE: This *cannot* prevent *drawing* truncated content on screen in *every* case, +// because broken metrics may skew our initial computations. +// As such, if the intent is to compute a "best fit" font size, +// no_truncation ought to be combined with no_refresh on eInk, +// (as we otherwise do *NOT* inhibit the refresh, in order to preserve get_last_rect's accuracy). +// You'll also probably want to do a cheaper compute_only pass first, +// to catch more obviously predictable truncations. +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// string: UTF-8 encoded string to print. +// cfg: Pointer to an FBInkOTConfig struct. +// fbink_cfg: Optional pointer to an FBInkConfig struct. If set, the fields +// is_inverted, is_flashing, is_cleared, is_centered, is_halfway, +// is_overlay, is_fgless, is_bgless, fg_color, bg_color, valign, halign, +// wfm_mode, dithering_mode, is_nightmode, no_refresh will be honored. +// Pass a NULL pointer if unneeded. +// fit: Optional pointer to an FBInkOTFit struct. +// If set, it will be used to return information about the amount of lines needed to render +// the string at the requested font size, and whether it was truncated or not. +// Pass a NULL pointer if unneeded. +// NOTE: Alignment is relative to the printable area, as defined by the margins. +// As such, it only makes sense in the context of a single, specific print call. +FBINK_API int fbink_print_ot(int fbfd, + const char* restrict string, + const FBInkOTConfig* restrict cfg, + const FBInkConfig* restrict fbink_cfg, + FBInkOTFit* restrict fit) __attribute__((nonnull(2))); + +// +// Brings printf formatting to fbink_print and fbink_print_ot ;). +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// ot_cfg: Optional pointer to an FBInkOTConfig struct. +// fbink_cfg: Optional pointer to an FBInkConfig struct. +// ot_fit: Optional pointer to an FBInkOTFit struct. +// NOTE: If ot_cfg is NULL, will call fbink_print, otherwise, fbink_print_ot! +// If ot_cfg is valid, fbink_cfg MAY be NULL (same behavior as fbink_print_ot). +// If ot_cfg is NULL, fbink_cfg MUST be valid. +// NOTE: Meaning you MUST at least pass either an fbink_cfg or an ot_cfg pointer! +// NOTE: ot_fit behaves like in fbink_print_ot (i.e., it's only relevant if you pass an ot_cfg pointer, but it may be NULL). +FBINK_API int fbink_printf(int fbfd, + const FBInkOTConfig* restrict ot_cfg, + const FBInkConfig* restrict fbink_cfg, + FBInkOTFit* restrict ot_fit, + const char* fmt, + ...) __attribute__((format(printf, 5, 6))); + +// +// A simple wrapper around the internal screen refresh handling, without requiring you to include einkfb/mxcfb headers. +// NOTE: Unlike FBInkRect, we *do* honor the original mxcfb rect order here (top before left). +// Returns -(ENOSYS) on non-eInk devices (i.e., pure Linux builds) +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened for the duration of this call. +// region_top: top (y) field of an mxcfb rectangle. +// region_left: left (x) field of an mxcfb rectangle. +// region_width: width field of an mxcfb rectangle. +// region_height: height field of an mxcfb rectangle. +// fbink_cfg: Pointer to an FBInkConfig struct. Honors wfm_mode, dithering_mode, is_nightmode, is_flashing. +// NOTE: If you request an empty region (0x0 @ (0, 0), a full-screen refresh will be performed! +// NOTE: This *ignores* no_refresh ;). +// NOTE: As far as dithering is concerned, c.f., HW_DITHER_INDEX_E enum. +// True HW dithering is only supported on devices with a recent EPDC (>= v2)! +// On Kindle, that's everything since the KOA2 (KOA2, PW4, KT4, KOA3), +// On Kobo, that's everything since Mk.7. +// NOTE: Even then, your device may not actually support anything other than PASSTHROUGH & ORDERED! +// On slightly older devices, the EPDC may support some sort of in-kernel software dithering, hence HWD_LEGACY. +// NOTE: If you do NOT want to request any dithering, set FBInkConfig's dithering_mode field to HWD_PASSTHROUGH (i.e., 0). +// This is also the fallback value. +// NOTE: On Kobo devices with a sunxi SoC, you will not be able to refresh content that you haven't drawn yourself first. +// (There's no "shared" framebuffer, each process gets its own private, zero-initialized (i.e., solid black) buffer). +// NOTE: In case of ioctl failure, errno *should* be preserved, +// allowing the caller to possibly handle some very specific edge-cases. +// NOTE: The confusing coordinates order is inherited from the layout of an mxcfb_rect struct. +// See fbink_refresh_rect below for a variant that takes an FBInkRect instead. +FBINK_API int fbink_refresh(int fbfd, + uint32_t region_top, + uint32_t region_left, + uint32_t region_width, + uint32_t region_height, + const FBInkConfig* restrict fbink_cfg) __attribute__((nonnull)); + +// +// Variant of fbink_refresh that takes an FBInkRect instead of broken out coordinates. +// Completely identical behavior. +FBINK_API int fbink_refresh_rect(int fbfd, const FBInkRect* restrict rect, const FBInkConfig* restrict fbink_cfg) + __attribute__((nonnull)); + +// A simple wrapper around the MXCFB_WAIT_FOR_UPDATE_SUBMISSION ioctl, without requiring you to include mxcfb headers. +// Returns -(EINVAL) when the update marker is invalid. +// Returns -(ENOSYS) on devices where this ioctl is unsupported. +// NOTE: It is only implemented by Kindle kernels (K5+), and Kobo kernels for MTK SoCs (Mk. 11)! +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened for the duration of this call. +// marker: The update marker you want to wait for. +// NOTE: If marker is set to LAST_MARKER (0U), the one from the last update sent by this FBInk session will be used instead. +// If there aren't any, the call will fail and return -(EINVAL)! +// NOTE: Waiting for a random marker *should* simply return early. +FBINK_API int fbink_wait_for_submission(int fbfd, uint32_t marker); + +// A simple wrapper around the MXCFB_WAIT_FOR_UPDATE_COMPLETE ioctl, without requiring you to include mxcfb headers. +// Returns -(EINVAL) when the update marker is invalid. +// Returns -(ENOSYS) on non-eInk devices (i.e., pure Linux builds). +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened for the duration of this call. +// marker: The update marker you want to wait for. +// NOTE: If marker is set to LAST_MARKER (0U), the one from the last update sent by this FBInk session will be used instead. +// If there aren't any, the call will fail and return -(EINVAL)! +// NOTE: Waiting for a random marker *should* simply return early. +FBINK_API int fbink_wait_for_complete(int fbfd, uint32_t marker); +// NOTE: For most single-threaded use-cases, you *probably* don't need to bother with this, +// as all your writes to the framebuffer should obviously be serialized. +// NOTE: All our target devices should default to the QUEUE_AND_MERGE update scheme, +// which means the EPDC itself will attempt to bundle updates together in order to do the least amount of work possible. +// (By work, I mean moving the eInk capsules around, i.e., limiting to amount of effective refreshes). +// A fun example of this can be seen with the dump/restore tests in utils/dump.c: +// By default, it will call fbink_wait_for_complete at sensible times, but if you pass a random argument to it, +// it won't: the difference that makes in practcie should be extremely obvious, especially for the first set of tests! +// NOTE: I encourage you to strace your stock reader to see how it makes use of those ioctls: +// they're mostly used before and/or after FULL (i.e., flashing) updates, +// to make sure they don't get affected by surrounding updates. +// They can also be used to more predictably fence A2 updates. +// In fact, for the most part, you can think of them as a kind of vsync fence. +// Be aware that the ioctl will block for (relatively) longer than it takes for the refresh to visually end, +// and that the delay depends for the most part on the waveform mode (flashing & region size have a much smaller impact). +// With some waveform modes (mainly A2/DU), it'll return significantly earlier if the region's fb content hasn't changed. +// NOTE: See KOReader's mxc_update @ https://github.com/koreader/koreader-base/blob/master/ffi/framebuffer_mxcfb.lua +// for some fancier examples in a complex application, where one might want to wait for completion of previous updates +// right before sending a flashing one, for example. +// NOTE: Prior to FBInk 1.20.0, we used to *enforce* a wait_for_complete *after* *every* flashing (FULL) update. +// This was originally done to mimic eips's behavior when displaying an image. +// While blocking right after a refresh made sense for a one-off CLI tool, it's different for API users: +// in most cases, it probably makes more sense to only block *before* the *following* flashing refresh, +// thus ensuring that the wait will be shorter (or near zero), since time has probably passed between those two refreshes. +// But, if you know that you won't be busy for a while after a flashing update, it might make sense to wait right after it, +// in order to avoid an ioctl on the next refresh that might end up hurting reactivity... +// Incidentally, as all of this depends on specific use-cases, this is why it is entirely left to the user, +// and why there's no compatibility flag in FBInkConfig to restore the FBInk < 1.20 behavior ;). +// NOTE: Beware that on a few NTX boards, this ioctl appears to be buggy, and *may* randomly timeout. +// c.f., devices flagged as deviceQuirks.unreliableWaitFor in fbink_device_id.c + +// Return the update marker from the last *refresh* (explicit or implicit) done in this FBInk session. +// NOTE: Returns LAST_MARKER (0U) if there wasn't any, or on non-eInk devices (i.e., pure Linux builds). +// NOTE: Mainly useful if you want to do fairly fancy stuff with wait_for_complete/wait_for_submission, +// otherwise, simply passing LAST_MARKER to 'em should do the trick. +FBINK_API uint32_t fbink_get_last_marker(void); + +// +// Returns true if the device appears to be in a quirky framebuffer state that *may* require a reinit to produce sane results. +// NOTE: The intended use-case is for long running apps which may trigger prints across different framebuffer states, +// to allow them to call fbink_init again at specific points only (instead of enforcing a reinit on every print). +// This is of interest on a few devices, where trying to print based on a "stale" init state would fail, +// or produce unwanted results (e.g., rotation). +// NOTE: Right now, this only checks for the isNTX16bLandscape device quirk, +// because that's the only one that is not permanent (i.e., hardware specific), +// but instead software specific (here, because of pickel). +// In practical terms, this means the Kobo's fb is in 16bpp mode, with its origin in the top-right corner (i.e., Landscape). +// NOTE: Deprecated in favor of fbink_reinit ;). +FBINK_API bool fbink_is_fb_quirky(void) __attribute__((pure, deprecated)); + +// We'll need those for fbink_reinit (start > 256 to stay clear of errno values) +#define OK_BPP_CHANGE (1 << 9) +#define OK_ROTA_CHANGE (1 << 10) +#define OK_LAYOUT_CHANGE (1 << 11) +#define OK_GRAYSCALE_CHANGE (1 << 12) +// Attempt to detect changes in framebuffer states (between this call and the last time fbink_init/fbink_reinit was called), +// doing a reinit (i.e., calling fbink_init again) if needed, while doing the least amount of work possible in the process. +// NOTE: The intended use-case is for long running apps which may trigger prints across different framebuffer states, +// to allow them to ensure they'll be using up-to-date init data at key points in their lifecycle +// (without needing to bruteforce a full reinit on every print). +// This is of interest on a few devices, where trying to print based on a "stale" init state would at worst fail, +// at best produce unwanted results (e.g., after a bitdepth change or a hw rotation). +// NOTE: This obviously supercedes fbink_is_fb_quirky, because it should be smarter, +// by catching more scenarios where a reinit would be useful, +// and it can avoid running the same ioctl twice when an ioctl already done by init is needed to detect a state change. +// NOTE: Using fbink_reinit does NOT lift the requirement of having to run fbink_init at least ONCE, +// i.e., you cannot replace the initial fbink_init call by fbink_reinit! +// If reinitialization was *successful*, returns a bitmask with one or more of these flags set: +// bit OK_BPP_CHANGE is set if there was a bitdepth change. +// bit OK_ROTA_CHANGE is set if there was a rotation change. +// bit OK_LAYOUT_CHANGE is set if a rotation change caused a layout change (i.e., an orientation swap, Portrait <-> Landscape), +// this obviously implies OK_ROTA_CHANGE. +// If *only* OK_ROTA_CHANGE is set, it means the rotation change was a simple inversion of the current orientation, +// (i.e., Portrait <-> Inverted Portrait or Landscape <-> Inverted Landscape). +// bit OK_GRAYSCALE_CHANGE is set if there was a grayscale flag change. +// This is only set if the current & last known bitdepth is 8bpp. +// On mxcfb-like platforms, this flag is used by the epdc driver to toggle global HW inversion (a.k.a., night mode). +// NOTE: This means that it may return a *positive* non-zero value on *success*. +// This is helpful for callers that need to track FBInk's internal state via fbink_get_state or fbink_get_fb_pointer, +// because a reinit *might* affect the screen layout, signaling that their current state copy *may* be stale. +// TL;DR: Assume that *any* OK_*_CHANGE return value means that you need to refresh your state tracking. +// NOTE: You'll probably want to take action (changing pen colors or enabling inversion) after an OK_GRAYSCALE_CHANGE, +// especially if it's unexpected. +// NOTE: In turn, this means that a simple EXIT_SUCCESS means that no reinitialization was needed. +// NOTE: On Kobo devices with a sunxi SoC, OK_BPP_CHANGE will *never* happen, +// as the state of the actual framebuffer device is (unfortunately) meaningless there. +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened for the duration of this call. +// fbink_cfg: Pointer to an FBInkConfig struct. +FBINK_API int fbink_reinit(int fbfd, const FBInkConfig* restrict fbink_cfg) __attribute__((warn_unused_result, nonnull)); + +// Update FBInk's internal verbosity flags +// As mentioned in fbink_init(), the is_verbose, is_quiet & to_syslog fields in an FBInkConfig +// are only processed at initialization time. +// This function allows doing *just* that, without having to go through a more costly full (re)-init. +// fbink_cfg: Pointer to an FBInkConfig struct (is_verbose, is_quiet & to_syslog). +FBINK_API void fbink_update_verbosity(const FBInkConfig* restrict fbink_cfg) __attribute__((nonnull)); + +// Update FBInk's internal representation of pen colors +// As mentioned in fbink_init(), the fg_color & bg_color fields in an FBInkConfig are only processed at initialization time. +// This is because they're not used as-is (because they're not actually colors, just a custom palette index), +// they're just used to pack the matching palette color value into the right pixel format for the target framebuffer. +// This function allows doing *just* that, without having to go through a more costly full (re)-init. +// Returns -(ENOSYS) when drawing primitives are disabled (MINIMAL build w/o DRAW). +// fbink_cfg: Pointer to an FBInkConfig struct (honors fg_color & bg_color). +FBINK_API int fbink_update_pen_colors(const FBInkConfig* restrict fbink_cfg) __attribute__((nonnull)); + +// We'll need those for fbink_set_*_pen_* (start > 256 to stay clear of errno values) +#define OK_ALREADY_SAME (1 << 9) +// Alternatively, you can choose to set the pen colors *directly*, without relying on FBInk's eInk palette handling. +// This is mostly of interest if you want to use color values you're getting from somewhere outside FBInk. +// You will *NOT* have to call fbink_update_pen_colors() when using these, they'll take care of updating the internal state. +// NOTE: The *optional* quantization pass *should* match what the EPDC itself will do anyway (i.e., it's redundant). +// Returns -(ENOSYS) when drawing primitives are disabled (MINIMAL build w/o DRAW). +// y: 8-bit luminance value +// quantize: If true, round to the nearest eInk palette color. +// update: If true, abort early and return OK_ALREADY_SAME if that's already the current color. +FBINK_API int fbink_set_fg_pen_gray(uint8_t y, bool quantize, bool update); +FBINK_API int fbink_set_bg_pen_gray(uint8_t y, bool quantize, bool update); +// NOTE: You should be aware that subsequent fbink_init calls (and fbink_reinit when it leads to a reinit) *will* +// reset the pen colors to a grayscale representation of whatever RGB values you set this way! +// r: 8-bit red component value +// g: 8-bit green component value +// b: 8-bit blue component value +// a: 8-bit alpha component value (opaque is 0xFFu). +// quantize: If true, round to the nearest eInk palette color. This implies a grayscaling pass! +// update: If true, abort early and return OK_ALREADY_SAME if that's already the current color. +// Keep in mind that the comparison is done *after* grayscaling, even without quantize set. +FBINK_API int fbink_set_fg_pen_rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a, bool quantize, bool update); +FBINK_API int fbink_set_bg_pen_rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a, bool quantize, bool update); + +// +// Print a full-width progress bar on screen. +// Returns -(ENOSYS) when fixed-cell font support is disabled (MINIMAL build w/o BITMAP). +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// percentage: 0-100 value to set the progress bar's progression. +// fbink_cfg: Pointer to an FBInkConfig struct (ignores is_overlay, col & hoffset; +// as well as is_centered & is_padded). +FBINK_API int fbink_print_progress_bar(int fbfd, uint8_t percentage, const FBInkConfig* restrict fbink_cfg) + __attribute__((nonnull)); + +// Print a full-width activity bar on screen (i.e., an infinite progress bar). +// Returns -(ENOSYS) when fixed-cell font support is disabled (MINIMAL build w/o BITMAP). +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// progress: 0-16 value to set the progress thumb's position in the bar. +// fbink_cfg: Pointer to an FBInkConfig struct (ignores is_overlay, is_fgless, col & hoffset; +// as well as is_centered & is_padded). +FBINK_API int fbink_print_activity_bar(int fbfd, uint8_t progress, const FBInkConfig* restrict fbink_cfg) + __attribute__((nonnull)); + +// +// Print an image on screen. +// Returns -(ENOSYS) when image support is disabled (MINIMAL build w/o IMAGE). +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// filename: Path to the image file (Supported formats: JPEG, PNG, TGA, BMP, GIF & PNM). +// If set to "-" and stdin is not attached to a terminal, +// will attempt to read image data from stdin. +// x_off: Target coordinates, x (honors negative offsets). +// y_off: Target coordinates, y (honors negative offsets). +// fbink_cfg: Pointer to an FBInkConfig struct. +// Where positioning is concerned, honors any combination of halign/valign, row/col & x_off/y_off; +// otherwise, honors pretty much every other field not specifically concerned with text rendering. +// NOTE: Much like fbink_print_raw_data, for best performance, +// an image that decodes in a pixel format close to the one used by the target device fb is best. +// Generally, that'd be a Grayscale (color-type 0) PNG, ideally dithered down to the eInk palette +// (c.f., https://www.mobileread.com/forums/showpost.php?p=3728291&postcount=17). +// If you can't pre-process your images, dithering can be handled by the hardware on recent devices (c.f. dithering_mode), +// or by FBInk itself (c.f., sw_dithering), but the pixel format still matters: +// On a 32bpp fb, Gray will still be faster than RGB. +// On a 8bpp fb, try to only use Gray for the best performance possible, +// as an RGB input will need to be grayscaled, making it slower than if it were rendered on a 32bpp fb! +// Try to avoid using a 16bpp fb, as conversion to/from RGB565 will generally slow things down. +// If you know you won't need to handle an alpha channel, don't forget ignore_alpha, too ;). +// As expected, the fastest codepath is Gray on an 8bpp fb ;). +// NOTE: There's a direct copy fast path in the very specific case of printing a Grayscale image *without* alpha, +// inversion or dithering on an 8bpp fb. +// NOTE: No such luck on 32bpp, because of a mandatory RGB <-> BGR conversion ;). +FBINK_API int fbink_print_image(int fbfd, + const char* filename, + short int x_off, + short int y_off, + const FBInkConfig* restrict fbink_cfg) __attribute__((nonnull)); + +// Print raw scanlines on screen (packed pixels). +// Returns -(ENOSYS) when image support is disabled (MINIMAL build w/o IMAGE). +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// data: Pointer to a buffer holding the image data (Supported pixel formats: Y/YA/RGB/RGBA, +// 8-bit components, the first pixel should be the top-left of the image). +// w: Width (in pixels) of a single scanline of the input image data. +// h: Height (in pixels) of the full image data (i.e., amount of scanlines). +// len: *Exact* size of the input buffer. +// Input pixel format is simply computed as len / h / w, so this *needs* to be exact, +// do not pass a padded length (or pad the data itself in any way)! +// x_off: Target coordinates, x (honors negative offsets). +// y_off: Target coordinates, y (honors negative offsets). +// fbink_cfg: Pointer to an FBInkConfig struct. +// Where positioning is concerned, honors any combination of halign/valign, row/col & x_off/y_off; +// otherwise, honors pretty much every other field not specifically concerned with text rendering. +// NOTE: While we do accept a various range of input formats (as far as component interleaving is concerned), +// our display code only handles a few specific combinations, depending on the target hardware. +// To make everyone happy, this will transparently handle the pixel format conversion *as needed*, +// a process which incurs a single copy of the input buffer (same behavior as in the non-raw image codepath). +// If this is a concern to you, make sure your input buffer is formatted in a manner adapted to your output device: +// Generally, that'd be RGBA (32bpp) on Kobo (or RGB (24bpp) with ignore_alpha), +// and YA (grayscale + alpha) on Kindle (or Y (8bpp) with ignore_alpha). +FBINK_API int fbink_print_raw_data(int fbfd, + const unsigned char* restrict data, + const int w, + const int h, + const size_t len, + short int x_off, + short int y_off, + const FBInkConfig* restrict fbink_cfg) __attribute__((nonnull)); + +// +// Just clear the screen (or a region of it), using the background pen color, eInk refresh included (or not ;)). +// Returns -(ENOSYS) when drawing primitives are disabled (MINIMAL build w/o DRAW). +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// fbink_cfg: Pointer to an FBInkConfig struct (honors is_inverted, wfm_mode, dithering_mode, is_nightmode, is_flashing, +// as well as no_refresh & bg_color). +// rect: Optional pointer to an FBInkRect rectangle (as, say, returned by fbink_get_last_rect), +// describing the specific region of screen to clear (in absolute coordinates). +// If the rectangle is empty (i.e., width or height is zero) or the pointer is NULL, +// the full screen will be cleared. +// no_rota: Optional, and only useful in very limited cases. When in doubt, set to false. +// When passing a rect, this requests *not* applying any further rotation hacks, +// (e.g., isNTX16bLandscape). +// This is mildly useful if you got a *rotated* rect out of fbink_get_last_rect +// on such a quirky framebuffer state, +// and just want to re-use it as-is without mangling the rotation again. +// NOTE: This can be used to draw arbitrary filled rectangles (using the bg pen color), +// but, for convenience, fbink_fill_rect_gray & fbink_fill_rect_rgba are also available. +FBINK_API int fbink_cls(int fbfd, const FBInkConfig* restrict fbink_cfg, const FBInkRect* restrict rect, bool no_rota) + __attribute__((nonnull(2))); + +// Like fbink_cls, but instead of absolute coordinates, rely on grid coordinates like fbink_print. +// Honors all the same positioning trickery than fbink_print (i.e., row/col mixed w/ hoffset/voffset). +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// fbink_cfg: Pointer to an FBInkConfig struct (honors col, row, is_halfway, is_centered, is_padded, is_rpadded, +// voffset, hoffset, is_overlay, is_bgless, +// wfm_mode, dithering_mode, is_nightmode, is_flashing, no_refresh). +// cols: Amount of columns to clear (i.e., width). +// rows: Amount of rows to clear (i.e., height). +FBINK_API int fbink_grid_clear(int fbfd, + unsigned short int cols, + unsigned short int rows, + const FBInkConfig* restrict fbink_cfg) __attribute__((nonnull)); + +// Like fbink_refresh, but instead of absolute coordinates, rely on grid coordinates like fbink_print. +// Honors all the same positioning trickery than fbink_print (i.e., row/col mixed w/ hoffset/voffset). +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened for the duration of this call. +// fbink_cfg: Pointer to an FBInkConfig struct (honors col, row, is_halfway, is_centered, is_padded, is_rpadded, +// voffset, hoffset, is_overlay, is_bgless, +// wfm_mode, dithering_mode, is_nightmode, is_flashing). +// cols: Amount of columns to refresh (i.e., width). +// rows: Amount of rows to refresh (i.e., height). +// NOTE: This *ignores* no_refresh ;). +FBINK_API int fbink_grid_refresh(int fbfd, + unsigned short int cols, + unsigned short int rows, + const FBInkConfig* restrict fbink_cfg) __attribute__((nonnull)); + +// +// Dump the full screen. +// Returns -(ENOSYS) when image support is disabled (MINIMAL build w/o IMAGE). +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// dump: Pointer to an FBInkDump struct (will be recycled if already used). +// NOTE: As with all FBInk structs, FBInkDump *must* be zero-initialized. +// Storage for the dump will be allocated on the heap by FBInk, +// but releasing that memory (i.e., free(dump.data);) is the caller's burden. +// Care should be taken not to leave that pointer dangling (i.e., dump.data = NULL;), +// as a subsequent call to fbink_*_dump with that same struct would otherwise trip the recycling check, +// causing a double free! +// You can use the fbink_free_dump_data() helper function to do just that. +// There are no error codepaths after storage allocation (i.e., you are assured that it has NOT been allocated on error). +// Note that a recycling *will* clear the clip FBInkRect! +// NOTE: On *most* devices (the exceptions being 4bpp & 16bpp fbs), +// the data being dumped is perfectly valid input for fbink_print_raw_data, +// in case you'd ever want to do some more exotic things with it... +// NOTE: On Kobo devices with a sunxi SoC, you will not be able to capture content that you haven't drawn yourself first. +// (There's no "shared" framebuffer, each process gets its own private, zero-initialized (i.e., solid black) buffer). +FBINK_API int fbink_dump(int fbfd, FBInkDump* restrict dump) __attribute__((nonnull)); + +// Dump a specific region of the screen. +// Returns -(ENOSYS) when image support is disabled (MINIMAL build w/o IMAGE). +// Returns -(EINVAL) when trying to dump an empty region. +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// x_off: Dump coordinates, x (honors negative offsets). +// y_off: Dump coordinates, y (honors negative offsets). +// w: Width of the region to dump. +// h: Height of the region to dump. +// fbink_cfg: Pointer to an FBInkConfig struct (honors any combination of halign/valign, row/col & x_off/y_off). +// dump: Pointer to an FBInkDump struct (will be recycled if already used). +// NOTE: The same considerations as in fbink_dump should be taken regarding the handling of FBInkDump structs. +FBINK_API int fbink_region_dump(int fbfd, + short int x_off, + short int y_off, + unsigned short int w, + unsigned short int h, + const FBInkConfig* restrict fbink_cfg, + FBInkDump* restrict dump) __attribute__((nonnull)); + +// Like fbink_region_dump, but takes an FBInkRect as input, and uses it *as is* (i.e., no rotation/positioning tricks). +// Returns -(ENOSYS) when image support is disabled (MINIMAL build w/o IMAGE). +// Returns -(EINVAL) when trying to dump an OOB region. +// The intended use case is being able to use a rect returned by fbink_get_last_rect +// without having to think about the potential fallout from positioning or rotation hacks. +// (c.f., also the "no_rota" flag for fbink_cls). +// NOTE: If NULL or an empty rect is passed, a full dump will be made instead. +// NOTE: The same considerations as in fbink_dump should be taken regarding the handling of FBInkDump structs. +FBINK_API int fbink_rect_dump(int fbfd, const FBInkRect* restrict rect, FBInkDump* restrict dump) + __attribute__((nonnull(3))); + +// Restore a framebuffer dump made by fbink_dump/fbink_region_dump/fbink_rect_dump. +// Returns -(ENOSYS) when image support is disabled (MINIMAL build w/o IMAGE). +// Otherwise, returns a few different things on failure: +// -(ENOTSUP) when the dump cannot be restored because it wasn't taken at the current bitdepth and/or rotation, +// or because it's wider/taller/larger than the current framebuffer, or if the crop is invalid (OOB). +// -(EINVAL) when there's no data to restore. +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// fbink_cfg: Pointer to an FBInkConfig struct (honors wfm_mode, dithering_mode, is_nightmode, +// is_flashing & no_refresh). +// dump: Pointer to an FBInkDump struct, as setup by fbink_dump or fbink_region_dump. +// NOTE: In case the dump was regional, it will be restored in the exact same coordinates it was taken from, +// no actual positioning is needed/supported at restore time. +// NOTE: This does not support any kind of software processing, at all! +// If you somehow need inversion or dithering, it has to be supported at the hardware level at refresh time by your device, +// (i.e., dithering_mode vs. sw_dithering, and is_nightmode vs. is_inverted). +// At most common bitdepths, you can somewhat work around these restrictions, obviously at a performance premium, +// by using fbink_print_raw_data instead (see the relevant notes for fbink_dump), with a few quirky caveats... +// c.f., the last few tests in utils/dump.c for highly convoluted examples that I don't recommend replicating in production. +// NOTE: "current" actually means "at last init/reinit time". +// Call fbink_reinit first if you really want to make sure bitdepth/rotation still match. +// NOTE: If you need to restore only part of a dump, you can do so via the clip field of the FBInkDump struct. +// This FBInkRect is the only field you should ever modify yourself. +// This clip rectangle is relative to the *screen*, not the dump's area (i.e., these are absolute screen coordinates). +// As such, it has to intersect with the dump's area, or the call will fail. +// And while it can safely completely overlap the dump's area, it still needs to be constrained to the screen's dimension. +// Of course, only the intersection of this rectangle with the dump's area will be restored. +// Be aware that you'll also need to flip the is_full field yourself first if you ever need to crop a full dump. +// NOTE: This does *NOT* free data.dump! +FBINK_API int fbink_restore(int fbfd, const FBInkConfig* restrict fbink_cfg, const FBInkDump* restrict dump) + __attribute__((nonnull)); + +// Free the data allocated by a previous fbink_dump() or fbink_region_dump() call. +// Returns -(ENOSYS) when image support is disabled (MINIMAL build w/o IMAGE). +// Otherwise, returns a few different things on failure: +// -(EINVAL) when the dump has already been freed. +// dump: Pointer to an FBInkDump struct. +// NOTE: You MUST call this when you have no further use for this specific data. +// NOTE: But, you MAY re-use a single FBInkDump struct across different dump() calls *without* calling this in between, +// as dump() will implicitly free a dirty struct in order to recycle it. +FBINK_API int fbink_free_dump_data(FBInkDump* restrict dump) __attribute__((nonnull)); + +// +// Return the coordinates & dimensions of the last thing that was *drawn*. +// Returns an empty (i.e., {0, 0, 0, 0}) rectangle if nothing was drawn. +// rotated: Returns rotated coordinates if applicable. +// NOTE: These are unfiltered *framebuffer* coordinates. +// If your goal is to use that for input detection, mapping that to input coordinates is your responsibility. +// On Kobo, fbink_get_state should contain enough data to help you figure out what kinds of quirks you need to account for. +// NOTE: While this *generally* maps to the refresh region, this does not always hold true: +// this will get updated regardless of no_refresh, +// and will ignore what is_flashing might do to make the refresh region fullscreen. +// i.e., it corresponds to what's drawn to the fb, not necessarily to what's refreshed on screen. +// NOTE: On devices where we may fudge the coordinates to account for broken rotation (i.e., most Kobos @ 16bpp), +// these are, by default, the *unrotated* coordinates! +// i.e., they will *NOT* match with what we actually send to mxcfb (and where we actually drew on the fb)! +// Nothing in our public API actually expects any other kind of coordinates, +// so having this return the rotated coordinates would be confusing... +// If, for some reason (e.g., comparing against actual ioctl values), +// you *do* need the rotated variant, set rotated to true. +FBINK_API FBInkRect fbink_get_last_rect(bool rotated); + +// +// Scan the screen for Kobo's "Connect" button in the "USB plugged in" popup, +// and optionally generate an input event to press that button. +// NOTE: This is deprecated, and no longer built by default on Kobo. +// NickelMenu and/or NickelDBus provide far more robust solutions for this. +// KOBO i.MX Only! Returns -(ENOSYS) when disabled (!KOBO, or Kobo on a sunxi SoC, as well as MINIMAL builds w/o BUTTON_SCAN). +// Otherwise, returns a few different things on failure: +// -(EXIT_FAILURE) when the button was not found. +// With press_button: +// -(ENODEV) when we couldn't generate a touch event at all (unlikely to ever happen on current HW). +// -(ENOTSUP) when the generated touch event appeared to have failed to actually tap the button. +// Emphasis on "appeared to", it's tricky to be perfectly sure the right thing happened... +// CANNOT happen when nosleep is true (because it skips this very codepath). +// NOTE: For the duration of this call, screen updates should be kept to a minimum: in particular, +// we of course expect to be able to see the "Connect" button, +// but we also expect the middle section of the final line to be untouched! +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// press_button: Generate an input event to press the button if true, +// MAY sleep up to 5s to confirm that input was successful! (unless nosleep is true). +// nosleep: If true, don't try to confirm that press_button's input event was successful, +// avoiding the nanosleep() calls that would incur... +// NOTE: Thread-safety obviously goes out the window with press_button enabled, +// since you can then only reasonably expect to be able to concurrently run a single instance of that function ;). +FBINK_API int fbink_button_scan(int fbfd, bool press_button, bool nosleep) __attribute__((deprecated)); + +// Wait for the end of a Kobo USBMS session, trying to detect a successful content import in the process. +// NOTE: Expects to be called while in the "Connected" state (like after a successful fbink_button_scan() call w/ press_buton)! +// It will abort early if that's not the case. +// NOTE: For the duration of this call (which is obviously blocking!), screen updates should be kept to a minimum: +// in particular, we expect the middle section of the final line to be untouched! +// KOBO i.MX Only! Returns -(ENOSYS) when disabled (!KOBO, or Kobo on a sunxi SoC, as well as MINIMAL builds w/o BUTTON_SCAN). +// Otherwise, returns a few different things on failure: +// -(EXIT_FAILURE) when the expected chain of events fails to be detected properly. +// -(ENODATA) when there was no new content to import at the end of the USBMS session. +// -(ETIME) when we failed to detect the end of the import session itself, because it ran longer than 5 minutes. +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// force_unplug: After having made sure to be in USBMS mode, generate a fake USB unplug event to force Nickel to wake up. +// This makes sense if you want to do stuff behind Nickel's back during the USBMS session, +// instead of simply monitoring it, especially with fake USBMS sessions ;). +// NOTE: Obviously, if this was a real USBMS session, and not an entirely faked one, +// if you force an unplug while onboard is still mounted on the connected to machine, +// shit will go horribly wrong! +// NOTE: Thread-safety obviously goes out the window with force_unplug enabled, +// since you can then only reasonably expect to be able to concurrently run a single instance of that function ;). +FBINK_API int fbink_wait_for_usbms_processing(int fbfd, bool force_unplug); + +// +// Attempt to untangle the rotation state on Kobo devices, converting between the murky "native" value +// (i.e., what's in the fb vinfo), and a "canonical" one, representing how the device is actually physically laid out. +// KOBO Only! Returns ENOSYS when disabled (!KOBO), or ERANGE if the input rotation is invalid. +// Yes, those are positive values, given the function's signature ;). +// See fbink_rota_quirks.c & utils/fbdepth.c for more details. +FBINK_API uint8_t fbink_rota_native_to_canonical(uint32_t rotate); +FBINK_API uint32_t fbink_rota_canonical_to_native(uint8_t rotate); + +// +// Inverts the *existing* content of the *full* screen. +// This is mildly useful on devices with no HW inversion support, +// to trigger a full "nightmode" swap, without actually having to redraw anything. +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// fbink_cfg: Pointer to an FBInkConfig struct. +// NOTE: On Kobo devices with a sunxi SoC, you will not be able to affect content that you haven't drawn yourself first. +// i.e., this will only apply to stuff drawn via FBInk's own framebuffer pointer (be it by FBInk or yourself). +FBINK_API int fbink_invert_screen(int fbfd, const FBInkConfig* restrict fbink_cfg) __attribute__((nonnull)); + +// +// Inverts the *existing* content of a specific *region* of the screen. +// This is mildly useful on devices with no HW inversion support, +// to implement inversion after the fact when you don't necessarily control the drawing. +// NOTE: Unlike fbink_invert_screen, this does *NOT* trigger a refresh! +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// rect: Optional pointer to an FBInkRect rectangle (as, say, returned by fbink_get_last_rect), +// describing the specific region of screen to invert (in absolute coordinates). +// If the rectangle is empty (i.e., width or height is zero) or the pointer is NULL, +// the full screen will be inverted. +// no_rota: Optional, and only useful in very limited cases. When in doubt, set to false. +// When passing a rect, this requests *not* applying any further rotation hacks, +// (e.g., isNTX16bLandscape). +// This is mildly useful if you got a *rotated* rect out of fbink_get_last_rect +// on such a quirky framebuffer state, +// and just want to re-use it as-is without mangling the rotation again. +// NOTE: On Kobo devices with a sunxi SoC, you will not be able to affect content that you haven't drawn yourself first. +// i.e., this will only apply to stuff drawn via FBInk's own framebuffer pointer (be it by FBInk or yourself). +FBINK_API int fbink_invert_rect(int fbfd, const FBInkRect* restrict rect, bool no_rota); + +// +// The functions below are much lower level than the rest of the API: +// outside of GUI toolkit implementations and very specific workflows, you shouldn't need to rely on them. +// +// Grants direct access to the backing buffer's base pointer, as well as its size (in bytes; e.g., smem_len). +// MUST NOT be called before fbink_init +// MUST NOT be called with an FBFD_AUTO fbfd +// MAY be called before before any fbink_print*, fbink_dump/restore, fbink_cls or fbink_grid* functions. +// (i.e., it'll implicitly setup the backing buffer if necessary). +// Returns NULL on failure (in which case, *buffer_size is set to 0). +// NOTE: This *may* need to be refreshed after a framebuffer state change, c.f., fbink_reinit! +// (In practice, though, the pointer itself is stable; +// only the buffer/mapping size may change on some quirky platforms (usually, PB)). +// fbfd: Open file descriptor to the framebuffer character device, +// cannot be set to FBFD_AUTO! +// buffer_size: Out parameter. On success, will be set to the buffer's size, in bytes. +FBINK_API unsigned char* fbink_get_fb_pointer(int fbfd, size_t* buffer_size) __attribute__((nonnull)); + +// For when you *really* need a mostly untouched copy of the full linuxfb structs... +// NOTE: Prefer fbink_get_state, unless you *really* have no other choices... +FBINK_API void fbink_get_fb_info(struct fb_var_screeninfo* var_info, struct fb_fix_screeninfo* fix_info) + __attribute__((nonnull)); + +// Magic constants for fbink_set_fb_info (> INT8_MAX to steer clear of legitimate values) +#define KEEP_CURRENT_ROTATE (1 << 7) +#define KEEP_CURRENT_BITDEPTH (1 << 7) +#define KEEP_CURRENT_GRAYSCALE (1 << 7) +#define TOGGLE_GRAYSCALE (1 << 6) +// Sets the framebuffer's bitdepth and/or native rotation. +// MUST NOT be called before fbink_init +// Only tested on Kobo & Kindle, here be dragons on other platforms! +// Returns a few different things on failure: +// -(ENODEV) if called before fbink_init +// -(EINVAL) when one of rota/bpp/grayscale is invalid +// -(ECANCELED) if an ioctl failed, meaning the fb state may be left in an undefined state. +// This is *highly* unlikely, but, if it happens, +// checking the sanity of the fb state is the caller's responsibilty! +// (i.e., you'll have to *at least* run fbink_reinit yourself). +// NOTE: On sunxi, only the rotation can be controlled: i.e., this will simply invoke fbink_sunxi_ntx_enforce_rota, +// except we only accept values matching linuxfb rotation constants. +// Prefer using fbink_sunxi_ntx_enforce_rota directly yourself. +// NOTE: On success, this will reinit the state *now* (returning the exact same values as fbink_reinit). +// In particular, if you're using fbink_get_state and/or fbink_get_fb_pointer, +// check and handle that return value properly (as you would an actual fbink_reinit call), +// or you may be left with stale state data if you don't refresh it when necessary :). +// fbfd: Open file descriptor to the framebuffer character device. +// if set to FBFD_AUTO, the fb is opened for the duration of this call. +// rota: *native* linuxfb rotation value (c.f., fbink_rota_canonical_to_native). +// Untouched if set to KEEP_CURRENT_ROTATE +// bpp: bitdepth value (in bits). +// Supported values: 4, 8, 16, 32 +// Untouched if set to KEEP_CURRENT_BITDEPTH +// grayscale: grayscale value. +// (enforced to 0 if bpp != 8). +// If bpp == 8, only meaningful on mxcfb or mtk: +// Generally set to GRAYSCALE_8BIT (1), +// setting it to GRAYSCALE_8BIT_INVERTED (2) +// will automagically enforce HW inversion via EPDC_FLAG_ENABLE_INVERSION (or similar). +// On Kindle MTK+Bellatrix4+CFA, GRAYSCALE_COLOR (0) and GRAYSCALE_COLOR_NIGHTMODE (3) are also supported. +// Untouched if set to KEEP_CURRENT_GRAYSCALE +// If set to TOGGLE_GRAYSCALE, will toggle between INVERTED & not @ 8bpp +// fbink_cfg: Pointer to an FBInkConfig struct. +FBINK_API int fbink_set_fb_info(int fbfd, + uint32_t rota, + uint8_t bpp, + uint8_t grayscale, + const FBInkConfig* restrict fbink_cfg) __attribute__((warn_unused_result, nonnull)); + +// These behave exactly like fbink_cls, but allow you to choose a color directly, instead of relying on the pen's bg color. +// Mostly useful for GUI toolkit backends, but depending on how it's actually used, +// remember that this honors `no_refresh` & `is_inverted`! +// Returns -(ENOSYS) when drawing primitives are disabled (MINIMAL build w/o DRAW). +// c.f., `fbink_cls` for documentation of the initial parameters they share. +// y: 8-bit luminance value +FBINK_API int fbink_fill_rect_gray(int fbfd, + const FBInkConfig* restrict fbink_cfg, + const FBInkRect* restrict rect, + bool no_rota, + uint8_t y) __attribute__((nonnull(2))); +// r: 8-bit red component value +// g: 8-bit green component value +// b: 8-bit blue component value +// a: 8-bit alpha component value (opaque is 0xFFu). +FBINK_API int fbink_fill_rect_rgba(int fbfd, + const FBInkConfig* restrict fbink_cfg, + const FBInkRect* restrict rect, + bool no_rota, + uint8_t r, + uint8_t g, + uint8_t b, + uint8_t a) __attribute__((nonnull(2))); + +// Convenience public wrappers for a per-pixel put/get. +// These are designed with *convenience* in mind, *not* performance. +// (In particular, a pixel needs to be packed on *each* call). +// I'd highly recommend handling drawing yourself if you can ;). +// NOTE: Unlike the fbink_fill_rect family of functions, +// the fbink_put_pixel family will *never* trigger a refresh. +// (Which explains why they don't need to take an FBInkConfig pointer). +// Returns -(ENOSYS) when drawing primitives are disabled (MINIMAL build w/o DRAW). +// x: x coordinates +// y: y coordinates +// v: 8-bit luminance value +FBINK_API int fbink_put_pixel_gray(int fbfd, uint16_t x, uint16_t y, uint8_t v); +// r: 8-bit red component value +// g: 8-bit green component value +// b: 8-bit blue component value +// a: 8-bit alpha component value (opaque is 0xFFu). +FBINK_API int fbink_put_pixel_rgba(int fbfd, uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b, uint8_t a); +// *r: out pointer, 8-bit red component value +// *g: out pointer, 8-bit green component value +// *b: out pointer, 8-bit blue component value +// *a: out pointer, 8-bit alpha component value (opaque is 0xFFu). +// NOTE: If pixelformat is grayscale, r = g = b and a = 0xFF +// NOTE: Red always means red, if there's a BGR swap involved, it's handled for you. +// Similarly, BGR565/RBG565 is unpacked to RGB32. +FBINK_API int fbink_get_pixel(int fbfd, uint16_t x, uint16_t y, uint8_t* r, uint8_t* g, uint8_t* b, uint8_t* a); + +// As a means to alleviate *some* of the pixel packing overhead mentioned above, +// the following set of functions allow you to actually *save* a packed pixel, +// and pass it to dedicated variants of put_pixel & fill_rect. +// This is helpful if you often reuse the same color. +// NOTE: The packing is only accurate for the *current* pixel format, +// consider re-packing after a potential bitdepth change! +// (e.g., when handling an fbink_reinit call). +// Returns -(ENOSYS) when drawing primitives are disabled (MINIMAL build w/o DRAW). +FBINK_API int fbink_pack_pixel_gray(uint8_t y, uint32_t* px); +FBINK_API int fbink_pack_pixel_rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a, uint32_t* px); +// c.f., `fbink_put_pixel_*` for documentation of the initial parameters they share. +// px: out pointer, packed pixel in the *current* framebuffer pixel format +FBINK_API int fbink_put_pixel(int fbfd, uint16_t x, uint16_t y, void* px); +FBINK_API int fbink_fill_rect(int fbfd, + const FBInkConfig* restrict fbink_cfg, + const FBInkRect* restrict rect, + bool no_rota, + void* px) __attribute__((nonnull(2))); +// c.f., `fbink_put_pixel_*` & `fbink_fill_rect_*` for documentation of the initial parameters they share. +// px: pointer to a packed pixel, as provided by the fbink_pack_pixel_* family of functions. + +// Forcefully wakeup the EPDC (Kobo Mk.8+ only) +// We've found this to be helpful on a few otherwise crashy devices, +// c.f., https://github.com/koreader/koreader-base/pull/1645 & https://github.com/koreader/koreader/pull/10771 +// Keep in mind that a few Mk. 7 devices can also be mildly temperamental in some circumstances, +// and that this feature isn't supported on those, c.f., https://github.com/koreader/koreader/issues/11511 +// NOTE: Nickel fires this off from its input handler, debounced at roughly 1.5s or 4s intervals, +// which sounds like a slightly more elegant approach than the one I opted for in KOReader ;). +// (As a fun quirk, it only does this after *touch* inputs, not *key* inputs). +// Returns -(ENOSYS) on unsupported platforms. +FBINK_API int fbink_wakeup_epdc(void); + +// +// The functions below are tied to specific capabilities on Kobo devices with a sunxi SoC (e.g., the Elipsa & Sage). +// +// Toggle the "pen" refresh mode. c.f., eink/sunxi-kobo.h @ DISP_EINK_SET_NTX_HANDWRITE_ONOFF for more details. +// The TL;DR being that it's only truly active when using A2 & DU waveform modes. +// And since, on sunxi, A2's MONOCHROME flag is just *software* dithering, you might actually prefer DU. +// Returns -(ENOSYS) on unsupported platforms. +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// NOTE: Outside of tracing pen input, it has another interesting side-effect: +// since it disables the layer overlap check, it allows you to display stuff in a different layout +// than the current working buffer without the (heavy) visual artifacts that would otherwise imply. +// Then again, it also leaves an eink kernel thread spinning at 100% CPU until the next standard update, +// so this might not be such a great idea after all... +// c.f., kobo_sunxi_fb_fixup @ fbink.c for more details. +// NOTE: Another option for "dealing" with these rotation mishaps is to just assume the screen is always Upright. +// You can achieve that by making sure FBINK_FORCE_ROTA is set properly in your env (*before* initializing FBInk). +// NOTE: Or, you can also affect how FBInk tries to honor the gyro (or not) at runtime, via fbink_sunxi_ntx_enforce_rota. +FBINK_API int fbink_sunxi_toggle_ntx_pen_mode(int fbfd, bool toggle); + +// Allows controlling at runtime how fbink_init & fbink_reinit handle rotation, +// potentially bypassing and/or selectively overriding the state returned by the accelerometer. +// Returns -(ENOSYS) on unsupported platforms. +// Otherwise, returns a few different things on failure: +// -(EINVAL) when mode is invalid +// -(ENOTSUP) when mode is unsupported +// NOTE: See the comments in the SUNXI_FORCE_ROTA_INDEX_E enum. +// In particular, the fact that the most interesting modes aren't actually supported because of technical limitations, +// unless the custom fbdamage module has been loaded (earlier than the disp client you're trying to match)... +// c.f., . +// NOTE: On success, this will reinit the state *now* (returning the exact same values as fbink_reinit). +FBINK_API int fbink_sunxi_ntx_enforce_rota(int fbfd, SUNXI_FORCE_ROTA_INDEX_T mode, const FBInkConfig* restrict fbink_cfg) + __attribute__((warn_unused_result, nonnull)); + +// +// The functions below are tied to specific capabilities on Kindle devices with a MediaTek SoC (e.g., the PW5). +// +// Setup the swipe animation direction & duration used by every refresh when is_animated is set in the FBInkConfig struct. +// Returns -(ENOSYS) on unsupported platforms. +// NOTE: Maximum value for steps is 60 on Bellatrix. The ioctl will throw an EBADF otherwise. +// NOTE: The refresh dimension that matches the animation direction (i.e., width for L/R & height for T/B) +// needs to be >= than the amount of steps, or the animation will be disabled. +// (Unlike the max steps, FBInk will sanity check this, as it currently crashes the driver ;)). +// NOTE: Malbec currrently uses 12 in the Reader. +FBINK_API int fbink_mtk_set_swipe_data(MTK_SWIPE_DIRECTION_INDEX_T direction, uint8_t steps); + +// Wait (up to 2s) for *every* pending refresh! +// Returns -(ENOSYS) on unsupported platforms. +// NOTE: When "fast mode" is enabled (c.f., fbink_mtk_toggle_auto_reagl below), +// no updates are able to be collected, meaning this will *always* timeout! +// TL;DR: Don't mix with fbink_mtk_toggle_auto_reagl(false)! +FBINK_API int fbink_wait_for_any_complete(int fbfd); + +// Setup the screen regions to gray out with a checkered pattern (Kindle only). +// Returns -(ENOSYS) on unsupported platforms. +// NOTE: Both of the regions are the *excluded* regions (i.e., the regions that will *NOT* be checkered). +// You *MAY* only set a single exclude region, in which case, use the first array member. +// NOTE: Setting the first region to an empty rectangle will disable the feature (as will setting size to MTK_HALFTONE_DISABLED). +// NOTE: For non-default sizes, the actual size is in pixel, and equal to size - 1 +// NOTE: This does *NOT* refresh the screen, it just sets things up for subsequent refreshes. +// NOTE: This will *NOT* apply to EPDC_FLAG_USE_ALT_BUFFER updates. +FBINK_API int fbink_mtk_set_halftone(int fbfd, const FBInkRect exclude_regions[2], MTK_HALFTONE_MODE_INDEX_T size); + +// Toggle whether large enough refresh regions will automatically be upgraded to REAGL (Kindle only). +// NOTE: Only applies to DU, GL16 & GC16 PARTIAL updates. +// NOTE: Only applies in day mode (e.g., GRAYSCALE_8BIT, not GRAYSCALE_8BIT_INVERTED). +// NOTE: Currently applies to regions both over a third of the screen's width and a fourth of the screen's height. +// NOTE: When this is *disabled* (i.e., the so-called "fast mode" is enabled), +// updates will *NOT* be collected by the WAIIT_FOR_ANY_UPDATE_COMPLETE ioctl! +// Returns -(ENOSYS) on unsupported platforms. +FBINK_API int fbink_mtk_toggle_auto_reagl(int fbfd, bool toggle); + +// Toggle the "pen" refresh mode (Kindle on Bellatrix3 only). +// Returns -(ENOSYS) on unsupported platforms (NOTE: We currently *allow* this on Bellatrix, where it will fail). +// fbfd: Open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call. +// NOTE: Among other things, this enforces similar behavior as in "fast" mode re: automatic REAGL upgrades. +// NOTE: There is a *strong* "no collision with standard mode" constraint that is left to *userland* to enforce (i.e., you). +// Pen mode updates *must* use DU (or DUNM iff in nightmode (e.g., GRAYSCALE_8BIT_INVERTED)), +// failing to do so will temporarily disable pen mode. +FBINK_API int fbink_mtk_toggle_pen_mode(int fbfd, bool toggle); + +// +// The functions below are small utilities to make working with input devices slightly less painful. +// + +// Input device classification +typedef enum +{ + INPUT_UNKNOWN = 0U, + // Standard udev classification + INPUT_POINTINGSTICK = 1U << 0U, + INPUT_MOUSE = 1U << 1U, + INPUT_TOUCHPAD = 1U << 2U, + INPUT_TOUCHSCREEN = 1U << 3U, + INPUT_JOYSTICK = 1U << 4U, + INPUT_TABLET = 1U << 5U, // Includes touchscreens w/ stylus support + INPUT_KEY = 1U << 6U, // Supports at least *1* EV_KEY keycode (may include touchscreens) + INPUT_KEYBOARD = 1U << 7U, + INPUT_ACCELEROMETER = 1U << 8U, + // Custom classification, tailored for our use-cases + INPUT_POWER_BUTTON = 1U << 16U, + INPUT_SLEEP_COVER = 1U << 17U, + INPUT_PAGINATION_BUTTONS = 1U << 18U, + INPUT_HOME_BUTTON = 1U << 19U, + INPUT_LIGHT_BUTTON = 1U << 20U, // e.g., the frontlight toggle button on some early Kobos + INPUT_MENU_BUTTON = 1U << 21U, + INPUT_DPAD = 1U << 22U, + INPUT_ROTATION_EVENT = 1U << 23U, // Where "device was rotated" events *may* be sent (gyro or not) + INPUT_SCALED_TABLET = 1U << 24U, // INPUT_TABLET, with max ABS_X/ABS_Y that match the fb's resolution + INPUT_VOLUME_BUTTONS = 1U << 25U, + INPUT_KINDLE_FRAME_TAP = 1U << 26U, // For the "double-tap on device" gestures introduced with the PW6 & CS +} __attribute__((packed)) INPUT_DEVICE_TYPE_E; +typedef uint32_t INPUT_DEVICE_TYPE_T; + +// Input classification settings +typedef enum +{ + SCAN_ONLY = 1U << 0U, // Do *NOT* leave any fd's open'ed + OPEN_BLOCKING = 1U << 1U, // Do *NOT* open fd's with O_NONBLOCK + MATCH_ALL = 1U << 2U, // Match on *all* the match_types bits instead of *any* + EXCLUDE_ALL = 1U << 3U, // Exclude on *all* the exclude_types bits instead of *any* + NO_RECAP = 1U << 4U, // Do *NOT* print a recap of the results of input device classification +} __attribute__((packed)) INPUT_SETTINGS_TYPE_E; +typedef uint32_t INPUT_SETTINGS_TYPE_T; + +typedef struct +{ + INPUT_DEVICE_TYPE_T type; // bitmask + int fd; // Set to -1 when not open + bool matched; // true if type matches the match/exclude combo + char name[256]; // As reported by EVIOCGNAME + char path[4096]; // e.g., /dev/input/event%d +} FBInkInputDevice; + +// Scan & classify input devices into actionable categories. +// Returns a pointer to the first element of an array of FBInkInputDevice structs, containing `dev_count` elements. +// Regardless of the filter you request, this will always contain *all* the device's input devices. +// The `matched` field will be set to true if that device matches *any/all* (depending on `MATCH_ALL`) of the bits in `match_types` +// and *not* *any/all* (depending on `EXCLUDE_ALL`) of the bits in `exclude_types`, meaning you can either cast a fairly wide net, +// and still catch everything you care about; or tackle an exclude mask on top for more fine-grained filtering. +// You *MUST* free the returned pointer after use (it's heap allocated). +// Returns NULL on failure (no input devices can be read, or MINIMAL build w/o INPUT). +// match_types: Bitmask used to filter the type of input devices you want to open. +// exclude_types: Bitmask used to filter *out* some input device types from results that matched match_types. +// Set to 0 to forgo an exclude mask. +// settings: Bitmask that controls some of the scan's behavior. +// if the OPEN_BLOCKING bit is set, fds will be opened in *blocking* mode. +// Otherwise, the default open flags are O_RDONLY|O_NONBLOCK|O_CLOEXEC +// if the SCAN_ONLY bit is set, *no* fds will be returned, regardless of the filter. +// if the MATCH_ALL bit is set, +// a device must feature *all* of the bits in match_types to be considered a match. +// Otherwise, any of each individal bit will be enough. +// if the EXCLUDE_ALL bit is set, +// a device must *not* feature *all* of the bits in exclude_types to be considered a match. +// Otherwise, any of each individal bit will be enough to warrant an exclusion. +// Set to 0 for default settings. +// dev_count: out pointer, will be set to the amount of array elements in the returned data. +// NOTE: This does *NOT* require fbink to be initialized, but *does* honor its internal verbosity state. +FBINK_API FBInkInputDevice* fbink_input_scan(INPUT_DEVICE_TYPE_T match_types, + INPUT_DEVICE_TYPE_T exclude_types, + INPUT_SETTINGS_TYPE_T settings, + size_t* dev_count); + +// Variant of the above that takes a filepath instead of scanning /dev/input/event* +// This is useful to handle hotplug events, for instance. +// Returns a pointer to an FBInkInputDevice struct. +// You *MUST* free the returned pointer after use (it's heap allocated). +// Returns NULL on failure (filepath cannot be read, or MINIMAL build w/o INPUT). +FBINK_API FBInkInputDevice* fbink_input_check(const char* filepath, + INPUT_DEVICE_TYPE_T match_types, + INPUT_DEVICE_TYPE_T exclude_types, + INPUT_SETTINGS_TYPE_T settings); +// +/// +// +// When you intend to keep the framebuffer fd open for the lifecycle of your program: +// fd = open() -> init(fd, ...) -> print*(fd, ...) -> ... -> close(fd) +// NOTE: This implies keeping the framebuffer's mmap around, too. +// The initial mmap will only happen on the first function call that actually needs to write to the fb, i.e., print*. +// On the upside, that's going to be the only mmap to ever happen, as subsequent print* calls will re-use it. +// +// Otherwise, you can simply forget about open() & close(), and just do: +// init(FBFD_AUTO, ...) +// And then whenever you want to print something: +// print*(FBFD_AUTO, ...) +// +// See fbink_cmd.c for an example of the former, and KFMon for an example of the latter. +// NOTE: Although fairly stupid in practice, utils/dump.c is less convoluted than fbink_cmd.c, making it worth a look... + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/fbink/libfbink.a b/src/fbink/libfbink.a new file mode 100644 index 0000000..fdedfc0 Binary files /dev/null and b/src/fbink/libfbink.a differ diff --git a/src/fbinkutil.go b/src/fbinkutil.go new file mode 100644 index 0000000..bbb5cc6 --- /dev/null +++ b/src/fbinkutil.go @@ -0,0 +1,76 @@ +package main + +/* +#cgo CFLAGS: -I./fbink +#cgo LDFLAGS: -L./fbink -lfbink -lm +#include +#include "fbink.h" +*/ +import "C" + +import ( + "fmt" + "os" + "unsafe" +) + +type FB struct { + file *os.File + fd C.int + cfg C.FBInkConfig + screenWidth int + screenHeight int +} + +func InitFBInk(width, height int) (*FB, error) { + fb, err := os.OpenFile("/dev/fb0", os.O_RDWR, 0) + if err != nil { + return nil, fmt.Errorf("open fb0: %w", err) + } + f := &FB{file: fb, fd: C.int(fb.Fd()), screenWidth: width, screenHeight: height} + C.fbink_init(f.fd, &f.cfg) + return f, nil +} + +func (f *FB) Close() { _ = f.file.Close() } + +func (f *FB) WriteBuffer(buf []byte) error { + _, err := f.file.WriteAt(buf, 0) + return err +} + +func (f *FB) Clear(doRefresh bool) { + var r C.FBInkRect + C.fbink_cls(f.fd, &f.cfg, &r, C._Bool(doRefresh)) + if doRefresh { + C.fbink_wait_for_complete(f.fd, 0) + } +} + +func (f *FB) PrintAtPixel(x, y int, text string, fontmult int) { + cstr := C.CString(text) + defer C.free(unsafe.Pointer(cstr)) + f.cfg.hoffset = C.short(x) + f.cfg.voffset = C.short(y) + f.cfg.fontmult = C.uint8_t(fontmult) + C.fbink_print(f.fd, cstr, &f.cfg) +} + +func (f *FB) RefreshRect(top, left, w, h int) { + if w <= 0 || h <= 0 { + return + } + C.fbink_refresh( + f.fd, + C.uint32_t(top), + C.uint32_t(left), + C.uint32_t(w), + C.uint32_t(h), + &f.cfg, + ) + C.fbink_wait_for_complete(f.fd, 0) +} + +func (f *FB) RefreshFull() { + f.RefreshRect(0, 0, f.screenWidth, f.screenHeight) +} diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..9f0058b --- /dev/null +++ b/src/go.mod @@ -0,0 +1,3 @@ +module kindle_fbink_go + +go 1.19 diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..d9f4862 --- /dev/null +++ b/src/main.go @@ -0,0 +1,185 @@ +package main + +import ( + "fmt" + "os" + "time" +) + +func main() { + fmt.Println("startup") + const ( + width = 1072 + height = 1448 + ) + + fb, err := InitFBInk(width, height) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + defer fb.Close() + + var evfd *os.File + var ( + touchAxisX AxisRange + touchAxisY AxisRange + haveAxis bool + ) + + if path, err := findTouchEventNode(); err == nil { + if f, e := openNonblock(path); e == nil { + evfd = f + fmt.Println("touch input:", path) + defer evfd.Close() + if xr, yr, err := ReadTouchAxisRanges(path); err == nil { + fmt.Printf("touch axis from sysfs X:[%d,%d] Y:[%d,%d]\n", xr.Min, xr.Max, yr.Min, yr.Max) + touchAxisX = xr + touchAxisY = yr + haveAxis = true + } else { + fmt.Println("warning: touch axis range:", err) + } + } else { + fmt.Println("warning: open touch:", e) + } + } else { + fmt.Println("warning:", err) + } + + app := NewApp(fb, evfd, width, height) + if os.Getenv("KINDLE_UI_TOUCH_DEBUG") != "" { + app.EnableTouchDebug(true) + } + if haveAxis { + app.SetTouchAxisRange(touchAxisX, touchAxisY) + } else { + fmt.Println("Using default touch axis range") + app.SetTouchAxisRange( + AxisRange{Min: 0, Max: 4095}, + AxisRange{Min: 0, Max: 4095}, + ) + } + + var message = "Welcome" + + styles := map[string]*ButtonStyle{ + "primary": { + BorderRadius: 20, + FillColor: bytePtr(0xE0), + StrokeColor: bytePtr(0x30), + StrokeThickness: 2, + FontMultiplier: 2, + }, + "secondary": { + BorderRadius: 20, + FillColor: bytePtr(0xF4), + StrokeColor: bytePtr(0x40), + StrokeThickness: 2, + FontMultiplier: 2, + }, + "ghost": { + BorderRadius: 20, + StrokeColor: bytePtr(0x30), + StrokeThickness: 2, + FontMultiplier: 2, + }, + } + + homePage := &Page{ + Name: "home", + Background: 0xFF, + OnDraw: func(a *App) { + a.DrawText(40, 80, "test go ui", 3) + a.DrawText(40, 140, message, 2) + }, + Buttons: []*Button{ + { + Rect: Rect{X: width/2 - 220, Y: 260, W: 440, H: 120}, + Label: "Say Hello", + Style: styles["primary"], + OnTap: func(a *App) { + message = "Hello from your Kindle!" + fmt.Println("Button pressed: Say Hello") + a.RequestRender() + }, + }, + { + Rect: Rect{X: width/2 - 220, Y: 420, W: 440, H: 120}, + Label: "Go To Counter", + Style: styles["primary"], + OnTap: func(a *App) { + a.Navigate("counter") + }, + }, + { + Rect: Rect{X: width/2 - 220, Y: 580, W: 440, H: 120}, + Label: "Log To Console", + Style: styles["secondary"], + OnTap: func(a *App) { + fmt.Println("console button tapped at", time.Now().Format(time.RFC3339)) + message = "Logged a message to the console." + a.RequestRender() + }, + }, + { + Rect: Rect{X: width/2 - 220, Y: 740, W: 440, H: 120}, + Label: "Higher brightness", + Style: styles["ghost"], + OnTap: func(a *App) { + fmt.Println("brightness change") + if a.dispCtrl.brightness.currentBrightness > 0 { + a.setBrightness(0) + } else { + a.setBrightness(256) + } + }, + }, + }, + } + + counter := 0 + + counterPage := &Page{ + Name: "counter", + Background: 0xFF, + OnDraw: func(a *App) { + a.DrawText(40, 80, "basically react start example", 3) + a.DrawText(40, 140, fmt.Sprintf("Current value: %d", counter), 2) + }, + Buttons: []*Button{ + { + Rect: Rect{X: width/2 - 220, Y: 260, W: 440, H: 120}, + Label: "Increment", + Style: styles["primary"], + OnTapCount: func(a *App, count int) { + counter += count + a.RequestRender() + }, + }, + { + Rect: Rect{X: width/2 - 220, Y: 420, W: 440, H: 120}, + Label: "Reset", + Style: styles["secondary"], + OnTap: func(a *App) { + counter = 0 + a.RequestRender() + }, + }, + { + Rect: Rect{X: width/2 - 220, Y: 580, W: 440, H: 120}, + Label: "Back", + Style: styles["ghost"], + OnTap: func(a *App) { + a.Navigate("home") + }, + }, + }, + } + + app.AddPage(homePage) + app.AddPage(counterPage) + app.Navigate("home") + + app.Run() +} diff --git a/src/touch.go b/src/touch.go new file mode 100644 index 0000000..1a416b2 --- /dev/null +++ b/src/touch.go @@ -0,0 +1,279 @@ +package main + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" +) + +const ( + // Event type categories (high-level groupings) + EV_SYN = 0x00 // Synchronization: marks end of a full event packet (a "frame" of updates) + EV_KEY = 0x01 // Key/button events: pressed/released toggles (also used for touch "down/up") + EV_ABS = 0x03 // Absolute axis events: reports positional or pressure data, not deltas + + // EV_SYN codes + SYN_REPORT = 0 // Sent after a batch of ABS/KEY updates; tells you "this frame is complete" + + // EV_KEY codes (pretend the touchscreen is a keyboard with one key) + BTN_TOUCH = 0x14a // Touch contact active (1 = finger down, 0 = finger up) + BTN_TOOL_FINGER = 0x145 // "A finger tool is in range" - capacitive proximity (optional) + + // EV_ABS axis codes: continuous values like coordinates and pressure + ABS_X = 0x00 // Generic absolute X position (used on single-touch panels) + ABS_Y = 0x01 // Generic absolute Y position + ABS_MT_POSITION_X = 0x35 // Multi-touch X position for a specific contact slot + ABS_MT_POSITION_Y = 0x36 // Multi-touch Y position + ABS_PRESSURE = 0x18 // Pressure value (force of touch, if hardware supports it) + ABS_MT_TRACKING_ID = 0x39 // Unique ID for a finger contact (changes when finger lifts/reappears) + ABS_MT_SLOT = 0x2f // Selects which multi-touch "slot" subsequent MT events apply to +) + +type inputEvent struct { + Sec int32 + USec int32 + Type uint16 + Code uint16 + Value int32 +} + +type AxisRange struct { + Min int + Max int +} + +func findTouchEventNode() (string, error) { + candidates, _ := filepath.Glob("/dev/input/event*") + for _, ev := range candidates { + namePath := fmt.Sprintf("/sys/class/input/%s/device/name", filepath.Base(ev)) + if b, err := os.ReadFile(namePath); err == nil { + name := strings.ToLower(strings.TrimSpace(string(b))) + if strings.Contains(name, "touch") || strings.Contains(name, "synaptics") || + strings.Contains(name, "atmel") || strings.Contains(name, "elan") || + strings.Contains(name, "ft") || strings.Contains(name, "mxt") { + return ev, nil + } + } + } + for _, ev := range []string{"/dev/input/event1", "/dev/input/event0"} { + if _, err := os.Stat(ev); err == nil { + return ev, nil + } + } + return "", fmt.Errorf("no input event node found") +} + +func openNonblock(path string) (*os.File, error) { + return os.OpenFile(path, os.O_RDONLY|syscall.O_NONBLOCK, 0) +} + +func ReadTouchAxisRanges(eventPath string) (AxisRange, AxisRange, error) { + base := filepath.Base(eventPath) + baseDir := filepath.Join("/sys/class/input", base) + searchRoots := []string{ + filepath.Join(baseDir, "device"), + filepath.Join(baseDir, "device", "device"), + } + var ( + data []byte + err error + tried []string + addPath = func(p string) { + if p == "" { + return + } + tried = append(tried, p) + } + ) + for _, root := range searchRoots { + direct := filepath.Join(root, "abs") + addPath(direct) + patterns := []string{ + filepath.Join(root, "*", "abs"), + filepath.Join(root, "*", "*", "abs"), + } + for _, pattern := range patterns { + if matches, e := filepath.Glob(pattern); e == nil { + for _, m := range matches { + addPath(m) + } + } + } + } + if len(tried) == 0 { + tried = append(tried, + filepath.Join(baseDir, "device", "abs"), + filepath.Join(baseDir, "device", "device", "abs"), + ) + } + for _, p := range tried { + if b, e := os.ReadFile(p); e == nil { + data = b + err = nil + break + } else { + err = e + } + } + if data == nil { + return AxisRange{}, AxisRange{}, fmt.Errorf("read axis ranges: %w", err) + } + ranges := parseAxisRanges(data) + var xr, yr AxisRange + var ok bool + if xr, ok = ranges[ABS_MT_POSITION_X]; !ok { + xr = ranges[ABS_X] + } + if yr, ok = ranges[ABS_MT_POSITION_Y]; !ok { + yr = ranges[ABS_Y] + } + if xr.Max <= xr.Min { + xr = AxisRange{Min: 0, Max: touchMaxX - 1} + } + if yr.Max <= yr.Min { + yr = AxisRange{Min: 0, Max: touchMaxY - 1} + } + return xr, yr, nil +} + +func parseAxisRanges(data []byte) map[uint16]AxisRange { + result := make(map[uint16]AxisRange) + scanner := bufio.NewScanner(bytes.NewReader(data)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + line = strings.ReplaceAll(line, ":", "") + fields := strings.Fields(line) + if len(fields) < 3 { + continue + } + codeVal, err := strconv.ParseInt(fields[0], 0, 32) + if err != nil { + continue + } + minVal, err := strconv.ParseInt(fields[1], 0, 32) + if err != nil { + continue + } + maxVal, err := strconv.ParseInt(fields[2], 0, 32) + if err != nil { + continue + } + result[uint16(codeVal)] = AxisRange{Min: int(minVal), Max: int(maxVal)} + } + return result +} + +var touchDebugEnabled bool + +func SetTouchDebugEnabled(enabled bool) { + touchDebugEnabled = enabled +} + +// PollTouch drains up to maxReads events. Returns last known x,y, whether a touch is down, and whether we have valid coordinates. +func PollTouch(evfd *os.File, maxReads int) (x, y int, pressed, haveXY bool) { + x = lastTouchX + y = lastTouchY + pressed = lastTouchPressed + haveXY = lastTouchHave + + currentX := x + currentY := y + currentPressed := pressed + currentHave := haveXY + trackingActive := pressed + + updated := false + debug := touchDebugEnabled + currentSlot := -1 + for i := 0; i < maxReads; i++ { + var ev inputEvent + if err := binary.Read(evfd, binary.LittleEndian, &ev); err != nil { + if errors.Is(err, syscall.EAGAIN) || errors.Is(err, syscall.EWOULDBLOCK) { + break + } + return + } + if debug { + fmt.Printf("event type=0x%X code=0x%X value=%d\n", ev.Type, ev.Code, ev.Value) + } + switch ev.Type { + case EV_ABS: + switch ev.Code { + case ABS_X, ABS_MT_POSITION_X: + if trackingActive { + currentX = int(ev.Value) + currentHave = true + } + case ABS_Y, ABS_MT_POSITION_Y: + if trackingActive { + currentY = int(ev.Value) + currentHave = true + } + case ABS_MT_TRACKING_ID: + if ev.Value >= 0 { + currentPressed = true + trackingActive = true + } else { + currentPressed = false + trackingActive = false + } + case ABS_PRESSURE: + if ev.Value > 0 { + currentPressed = true + } else { + currentPressed = false + } + case ABS_MT_SLOT: + currentSlot = int(ev.Value) + if debug { + fmt.Printf("event slot=%d\n", currentSlot) + } + } + case EV_KEY: + if ev.Code == BTN_TOUCH || ev.Code == BTN_TOOL_FINGER { + currentPressed = ev.Value != 0 + } + case EV_SYN: + if ev.Code == SYN_REPORT { + x = currentX + y = currentY + pressed = currentPressed + haveXY = currentHave + updated = true + // Return after a single report so callers observe each touch frame. + } + } + if updated { + break + } + } + if updated { + lastTouchX = x + lastTouchY = y + lastTouchPressed = pressed + lastTouchHave = haveXY + } else { + x = lastTouchX + y = lastTouchY + pressed = lastTouchPressed + haveXY = lastTouchHave + } + return +} + +var ( + lastTouchX int + lastTouchY int + lastTouchPressed bool + lastTouchHave bool +) diff --git a/src/ui.go b/src/ui.go new file mode 100644 index 0000000..9f60e34 --- /dev/null +++ b/src/ui.go @@ -0,0 +1,708 @@ +package main + +import ( + "fmt" + "os" + "time" +) + +const ( + touchMaxX = 4096 + touchMaxY = 4096 + maxInt = int(^uint(0) >> 1) + displayRefreshCooldown = 200 * time.Millisecond + tapQueueMaxDelay = 350 * time.Millisecond +) + +func bytePtr(v byte) *byte { + return &v +} + +type Rect struct { + X, Y, W, H int +} + +func (r Rect) Contains(x, y int) bool { + return x >= r.X && x < r.X+r.W && y >= r.Y && y < r.Y+r.H +} + +type ButtonStyle struct { + BorderRadius int + FillColor *byte + StrokeColor *byte + StrokeThickness int + FontMultiplier int +} + +type Button struct { + Rect Rect + Label string + FontMultiplier int + BorderRadius int + Style *ButtonStyle + OnTap func(*App) + OnTapCount func(*App, int) +} + +func (b *Button) fontSize() int { + if b.FontMultiplier > 0 { + return b.FontMultiplier + } + if b.Style != nil && b.Style.FontMultiplier > 0 { + return b.Style.FontMultiplier + } + return 2 +} + +func (b *Button) draw(buf []byte, stride int) { + if b.Rect.W <= 0 || b.Rect.H <= 0 { + return + } + fill := byte(0xF0) + strokeColor := byte(0x00) + strokeThickness := 2 + radius := b.BorderRadius + if b.Style != nil { + if b.Style.FillColor != nil { + fill = *b.Style.FillColor + } + if b.Style.StrokeColor != nil { + strokeColor = *b.Style.StrokeColor + } + if b.Style.StrokeThickness >= 0 { + strokeThickness = b.Style.StrokeThickness + } + if b.Style.BorderRadius > 0 { + radius = b.Style.BorderRadius + } + } + if radius > 0 { + fillRoundedRect(buf, stride, b.Rect.X, b.Rect.Y, b.Rect.W, b.Rect.H, radius, fill) + if strokeThickness > 0 { + strokeRoundedRect(buf, stride, b.Rect.X, b.Rect.Y, b.Rect.W, b.Rect.H, strokeThickness, radius, strokeColor) + } + return + } + drawRect(buf, stride, b.Rect.X, b.Rect.Y, b.Rect.W, b.Rect.H, fill) + if strokeThickness > 0 { + strokeRect(buf, stride, b.Rect.X, b.Rect.Y, b.Rect.W, b.Rect.H, strokeThickness, strokeColor) + } +} + +func (b *Button) drawLabel(fb *FB) { + if b.Label == "" { + return + } + font := b.fontSize() + textX := b.Rect.X + 16 + textY := b.Rect.Y + b.Rect.H/2 - (8 * font) + if textY < b.Rect.Y+4 { + textY = b.Rect.Y + 4 + } + fb.PrintAtPixel(textX, textY, b.Label, font) +} + +func (b *Button) Tap(app *App) { + if b.OnTap != nil { + b.OnTap(app) + } +} + +func (b *Button) TapTimes(app *App, count int) { + if count <= 0 { + return + } + if b.OnTapCount != nil { + b.OnTapCount(app, count) + return + } + for i := 0; i < count; i++ { + b.Tap(app) + } +} + +type Page struct { + Name string + Background byte + Buttons []*Button + OnDraw func(*App) +} + +func (p *Page) Render(app *App) { + if p == nil { + return + } + fill := p.Background + if fill == 0 { + fill = 0xFF + } + for i := range app.buffer { + app.buffer[i] = fill + } + for _, btn := range p.Buttons { + btn.draw(app.buffer, app.stride) + } + _ = app.fb.WriteBuffer(app.buffer) + if p.OnDraw != nil { + p.OnDraw(app) + } + for _, btn := range p.Buttons { + btn.drawLabel(app.fb) + } +} + +func (p *Page) buttonAt(x, y int) *Button { + for _, btn := range p.Buttons { + if btn.Rect.Contains(x, y) { + return btn + } + } + return nil +} + +type TouchTransform struct { + SwapAxes bool + InvertX bool + InvertY bool +} + +type GestureType int + +const ( + GestureTap GestureType = iota + GesturePanStart + GesturePanUpdate + GesturePanEnd +) + +type Gesture struct { + Type GestureType + StartX int + StartY int + X int + Y int + StartRawX int + StartRawY int + RawX int + RawY int + DeltaX int + DeltaY int + Duration time.Duration +} + +type touchSession struct { + active bool + startX int + startY int + lastX int + lastY int + startRawX int + startRawY int + lastRawX int + lastRawY int + startTime time.Time +} + +func (s *touchSession) begin(x, y int, rawX, rawY int) { + s.active = true + s.startX = x + s.startY = y + s.lastX = x + s.lastY = y + s.startRawX = rawX + s.startRawY = rawY + s.lastRawX = rawX + s.lastRawY = rawY + s.startTime = time.Now() +} + +func (s *touchSession) update(x, y int, rawX, rawY int) { + if !s.active { + return + } + s.lastX = x + s.lastY = y + s.lastRawX = rawX + s.lastRawY = rawY +} + +func (s *touchSession) end() (Gesture, bool) { + if !s.active { + return Gesture{}, false + } + duration := time.Duration(0) + if !s.startTime.IsZero() { + duration = time.Since(s.startTime) + } + g := Gesture{ + Type: GestureTap, + StartX: s.startX, + StartY: s.startY, + X: s.lastX, + Y: s.lastY, + StartRawX: s.startRawX, + StartRawY: s.startRawY, + RawX: s.lastRawX, + RawY: s.lastRawY, + DeltaX: s.lastX - s.startX, + DeltaY: s.lastY - s.startY, + Duration: duration, + } + s.active = false + return g, true +} + +type App struct { + fb *FB + evfd *os.File + width int + height int + stride int + dispCtrl DisplayController + + buffer []byte + + pages map[string]*Page + current *Page + + dirty bool + lastTouchX int + lastTouchY int + lastRawX int + lastRawY int + touchDebug bool + + axisMinX int + axisMaxX int + axisMinY int + axisMaxY int + axisSampleCnt int + axisFixed bool + + transform TouchTransform + session touchSession + + pendingTaps []pendingTap + displayBusyUntil time.Time + tapFlushDeadline time.Time +} + +type pendingTap struct { + button *Button + count int +} + +func NewApp(fb *FB, evfd *os.File, width, height int) *App { + return &App{ + fb: fb, + evfd: evfd, + width: width, + height: height, + stride: width, + buffer: make([]byte, width*height), + pages: make(map[string]*Page), + dirty: true, + axisMinX: maxInt, + axisMaxX: -1, + axisMinY: maxInt, + axisMaxY: -1, + } +} + +func (a *App) AddPage(p *Page) { + if p == nil || p.Name == "" { + return + } + a.pages[p.Name] = p + if a.current == nil { + a.current = p + a.dirty = true + } +} + +func (a *App) Navigate(name string) { + if p, ok := a.pages[name]; ok { + if a.current != p { + fmt.Printf("navigate to page %q\n", name) + } + a.current = p + a.RequestRender() + return + } + fmt.Printf("warning: unknown page %q\n", name) +} + +func (a *App) RequestRender() { + a.dirty = true +} + +func (a *App) DrawText(x, y int, text string, fontMultiplier int) { + if fontMultiplier <= 0 { + fontMultiplier = 1 + } + a.fb.PrintAtPixel(x, y, text, fontMultiplier) +} + +func (a *App) renderCurrent() { + if a.current == nil { + return + } + a.current.Render(a) + a.fb.RefreshFull() + a.displayBusyUntil = time.Now().Add(displayRefreshCooldown) +} + +func (a *App) EnableTouchDebug(enabled bool) { + a.touchDebug = enabled + SetTouchDebugEnabled(enabled) +} + +func (a *App) SetTouchTransform(t TouchTransform) { + a.transform = t + if a.touchDebug { + fmt.Printf("touch transform set: swap=%v invertX=%v invertY=%v\n", t.SwapAxes, t.InvertX, t.InvertY) + } +} + +func (a *App) SetTouchAxisRange(xRange, yRange AxisRange) { + if xRange.Max > xRange.Min { + span := xRange.Max - xRange.Min + if span > a.width*2 { + a.axisMinX = 0 + a.axisMaxX = a.width - 1 + } else { + a.axisMinX = xRange.Min + a.axisMaxX = xRange.Max + } + } + if yRange.Max > yRange.Min { + span := yRange.Max - yRange.Min + if span > a.height*2 { + a.axisMinY = 0 + a.axisMaxY = a.height - 1 + } else { + a.axisMinY = yRange.Min + a.axisMaxY = yRange.Max + } + } + a.axisFixed = true + if a.touchDebug { + fmt.Printf("touch axis range set X:[%d,%d] Y:[%d,%d]\n", a.axisMinX, a.axisMaxX, a.axisMinY, a.axisMaxY) + } +} + +func (a *App) Run() { + idleSleep := 20 * time.Millisecond + lastPressed := false + a.initBrightness() + a.setBrightness(0) + for { + if a.dirty { + a.renderCurrent() + a.dirty = false + } + + if a.evfd == nil { + time.Sleep(150 * time.Millisecond) + continue + } + + rawX, rawY, pressed, have := PollTouch(a.evfd, 32) + now := time.Now() + + if a.touchDebug && pressed != lastPressed { + fmt.Printf("PollTouch: raw=(%d,%d) pressed=%v have=%v\n", rawX, rawY, pressed, have) + } + + if have { + // Lock axis on very first coordinate we see + if !a.axisFixed { + a.observeRaw(rawX, rawY) + } + nx, ny := a.normalize(rawX, rawY) + a.lastTouchX, a.lastTouchY = nx, ny + a.lastRawX, a.lastRawY = rawX, rawY + a.processTouchSample(nx, ny, rawX, rawY, pressed, true) + } else { + a.processTouchSample(a.lastTouchX, a.lastTouchY, a.lastRawX, a.lastRawY, pressed, false) + } + a.tryFlushTapQueue(now, pressed) + + lastPressed = pressed + time.Sleep(idleSleep) + } +} + +func (a *App) processTouchSample(x, y int, rawX, rawY int, pressed bool, hasPosition bool) { + if pressed { + if !a.session.active { + a.session.begin(x, y, rawX, rawY) + if a.touchDebug { + fmt.Printf("touch start norm=(%d,%d) raw=(%d,%d)\n", x, y, rawX, rawY) + } + } else if hasPosition { + prevX, prevY := a.session.lastX, a.session.lastY + a.session.update(x, y, rawX, rawY) + if a.touchDebug && (prevX != x || prevY != y) { + fmt.Printf("touch move norm=(%d,%d)->(%d,%d) raw=(%d,%d)\n", prevX, prevY, x, y, rawX, rawY) + } + } + return + } + + if !a.session.active { + return + } + gesture, ok := a.session.end() + if !ok { + return + } + if a.touchDebug { + fmt.Printf("touch end duration=%s delta=(%d,%d) rawDelta=(%d,%d)\n", + gesture.Duration, gesture.DeltaX, gesture.DeltaY, + gesture.RawX-gesture.StartRawX, gesture.RawY-gesture.StartRawY) + } + a.handleGesture(gesture) +} + +func (a *App) handleGesture(g Gesture) { + switch g.Type { + case GestureTap: + a.handleTap(g) + } +} + +func (a *App) handleTap(g Gesture) { + if a.current == nil { + return + } + btn := a.current.buttonAt(g.X, g.Y) + closest, dxEdge, dyEdge, dist2 := a.closestButton(g.X, g.Y) + const tapCaptureRadius = 60 + if btn == nil && closest != nil && dist2 <= tapCaptureRadius*tapCaptureRadius { + btn = closest + } + if btn == nil { + if a.touchDebug { + fmt.Printf("tap norm=(%d,%d) raw=(%d,%d) -> no button; nearest=%q offset=(%d,%d)\n", + g.X, g.Y, g.RawX, g.RawY, buttonLabel(closest), dxEdge, dyEdge) + } + return + } + if a.touchDebug { + fmt.Printf("tap norm=(%d,%d) raw=(%d,%d) -> %q offset=(%d,%d) [queued]\n", + g.X, g.Y, g.RawX, g.RawY, btn.Label, dxEdge, dyEdge) + } + a.enqueueTap(btn) +} + +func (a *App) enqueueTap(btn *Button) { + if btn == nil { + return + } + if len(a.pendingTaps) > 0 { + last := &a.pendingTaps[len(a.pendingTaps)-1] + if last.button == btn { + last.count++ + a.tapFlushDeadline = time.Now().Add(tapQueueMaxDelay) + if a.touchDebug && last.count > 1 { + fmt.Printf("tap aggregate %q x%d\n", btn.Label, last.count) + } + return + } + } + a.pendingTaps = append(a.pendingTaps, pendingTap{button: btn, count: 1}) + a.tapFlushDeadline = time.Now().Add(tapQueueMaxDelay) + if a.touchDebug { + fmt.Printf("tap queued %q x1\n", btn.Label) + } +} + +func (a *App) flushTapQueue() { + if len(a.pendingTaps) == 0 { + return + } + for _, entry := range a.pendingTaps { + if entry.button == nil || entry.count <= 0 { + continue + } + if a.touchDebug { + label := entry.button.Label + if entry.count > 1 { + fmt.Printf("tap dispatch %q x%d\n", label, entry.count) + } else { + fmt.Printf("tap dispatch %q\n", label) + } + } + entry.button.TapTimes(a, entry.count) + a.RequestRender() + } + a.pendingTaps = a.pendingTaps[:0] + a.displayBusyUntil = time.Now().Add(displayRefreshCooldown) + a.tapFlushDeadline = time.Time{} +} + +func (a *App) tryFlushTapQueue(now time.Time, pressed bool) { + if len(a.pendingTaps) == 0 { + return + } + if pressed && a.session.active { + return + } + deadlineReached := !a.tapFlushDeadline.IsZero() && !now.Before(a.tapFlushDeadline) + if !deadlineReached { + if a.dirty { + return + } + if !a.displayReady(now) { + return + } + } + a.flushTapQueue() +} + +func (a *App) displayReady(now time.Time) bool { + if a.displayBusyUntil.IsZero() { + return true + } + return !now.Before(a.displayBusyUntil) +} + +func buttonLabel(b *Button) string { + if b == nil { + return "" + } + return b.Label +} + +func (a *App) closestButton(x, y int) (*Button, int, int, int) { + if a.current == nil || len(a.current.Buttons) == 0 { + return nil, 0, 0, maxInt + } + closest := a.current.Buttons[0] + bestDX, bestDY := pointRectOffset(closest.Rect, x, y) + bestDist := distSquared(bestDX, bestDY) + for _, btn := range a.current.Buttons[1:] { + dx, dy := pointRectOffset(btn.Rect, x, y) + d := distSquared(dx, dy) + if d < bestDist { + closest = btn + bestDist = d + bestDX = dx + bestDY = dy + } + } + return closest, bestDX, bestDY, bestDist +} + +func pointRectOffset(r Rect, x, y int) (int, int) { + clampedX := clamp(x, r.X, r.X+r.W-1) + clampedY := clamp(y, r.Y, r.Y+r.H-1) + return x - clampedX, y - clampedY +} + +func clamp(v, min, max int) int { + if v < min { + return min + } + if v > max { + return max + } + return v +} + +func distSquared(dx, dy int) int { + return dx*dx + dy*dy +} + +func (a *App) observeRaw(x, y int) { + if a.axisFixed { + return + } + + // collect samples to infer axis range (only called between touches) + changed := false + if a.axisMinX == maxInt || x < a.axisMinX { + a.axisMinX = x + changed = true + } + if a.axisMaxX < x { + a.axisMaxX = x + changed = true + } + if a.axisMinY == maxInt || y < a.axisMinY { + a.axisMinY = y + changed = true + } + if a.axisMaxY < y { + a.axisMaxY = y + changed = true + } + + if changed { + a.axisSampleCnt++ + if a.touchDebug { + fmt.Printf("touch axis inferred X:[%d,%d] Y:[%d,%d] samples=%d\n", a.axisMinX, a.axisMaxX, a.axisMinY, a.axisMaxY, a.axisSampleCnt) + } + } + + // lock axis range immediately after first sample + if a.axisSampleCnt >= 1 { + a.axisFixed = true + if a.touchDebug { + fmt.Printf("touch axis range LOCKED at X:[%d,%d] Y:[%d,%d]\n", a.axisMinX, a.axisMaxX, a.axisMinY, a.axisMaxY) + } + } +} + +func (a *App) normalize(rawX, rawY int) (int, int) { + minX := a.axisMinX + maxX := a.axisMaxX + minY := a.axisMinY + maxY := a.axisMaxY + + if minX == maxInt || maxX <= minX { + minX = 0 + maxX = touchMaxX - 1 + } + if minY == maxInt || maxY <= minY { + minY = 0 + maxY = touchMaxY - 1 + } + + spanX := maxX - minX + spanY := maxY - minY + if spanX <= 0 { + spanX = 1 + } + if spanY <= 0 { + spanY = 1 + } + + nx := (rawX - minX) * (a.width - 1) / spanX + ny := (rawY - minY) * (a.height - 1) / spanY + + if a.transform.SwapAxes { + nx, ny = ny, nx + } + if a.transform.InvertX { + nx = (a.width - 1) - nx + } + if a.transform.InvertY { + ny = (a.height - 1) - ny + } + + if nx < 0 { + nx = 0 + } + if nx >= a.width { + nx = a.width - 1 + } + if ny < 0 { + ny = 0 + } + if ny >= a.height { + ny = a.height - 1 + } + return nx, ny +}