Example project · Builds & projects
Round eye (ESP32-S3 + round TFT)
A glowing robot eye roams a 1.28″ round IPS TFT LCD at 60 frames per second: quick darts and slow glides, a scanning lens ring, blinks and changing moods. Original C firmware on the emulated ESP32-S3 redraws only what changed and streams it through SPI DMA to a GC9A01A. A fixed random seed makes every run identical; there is no network connection or external asset. Open Code to read the complete project, including the Apache-2.0 Espressif display driver.

What's on the bench
- Battery
- ESP32-S3
- Round IPS TFT 1.28″ 240×240 (GC9A01)
How it's wired
- Battery · pos→ESP32-S3 · vin
- Battery · neg→ESP32-S3 · gnd
- ESP32-S3 · 3v3→Round IPS TFT 1.28″ 240×240 (GC9A01) · vcc
- ESP32-S3 · gnd2→Round IPS TFT 1.28″ 240×240 (GC9A01) · gnd
- ESP32-S3 · g7→Round IPS TFT 1.28″ 240×240 (GC9A01) · din
- ESP32-S3 · g6→Round IPS TFT 1.28″ 240×240 (GC9A01) · clk
- ESP32-S3 · g5→Round IPS TFT 1.28″ 240×240 (GC9A01) · cs
- ESP32-S3 · g3→Round IPS TFT 1.28″ 240×240 (GC9A01) · dc
- ESP32-S3 · g4→Round IPS TFT 1.28″ 240×240 (GC9A01) · rst
The code
The exact ESP-IDF v5.3.5 project the bundled firmware was compiled from — shown in the simulator’s Code tab, where you can edit the sources and press Compile & run to rebuild the board’s firmware in the cloud sandbox.
README.md
# Round eye · ESP32-S3 and GC9A01A An original robot eye on a 1.28-inch round 240×240 IPS TFT **LCD**, drawn at 60 frames per second. The glossy lens roams the whole round screen: quick darts with a small overshoot, squash and stretch and settle, slow glides, an occasional orbit around the rim, and still fixations broken by micro-saccades. A segmented ring scans around the lens and whirs when the eye darts; the pupil refocuses after each dart and a glint sweeps across the glass now and then. It blinks (sometimes twice, the lids motion-blurred as they sweep), its pupil breathes, and it can look happy, surprised, curious, squinting or sleepy. As it looks toward the rim the lens foreshortens like a turning eyeball. How it reaches 60 fps on an ESP32-S3 at 240 MHz (`main/main.c`, `main/eye.h`): - **Pacing:** absolute `esp_timer` deadlines, exactly 60 per second. The panel's TE output is not on this seven-pin module. - **Dirty rectangles:** each frame sends only the rectangles that changed (the iris with its glow, the lid bands), each in one SPI DMA transaction of at most 32,000 bytes, at the driver's default 80 MHz clock. - **Overlap:** two DMA buffers; the next rectangle renders while the previous one is on the wire. - **Fixed point and tables:** a dithered static background, the iris colour by radius in RGB565 (patched each frame for the pupil and its rim), and lid colours are built once at boot. Lids are column fills and gradient tables; their edge ramps and shadows, the highlights and the glint blend in RGB565. Only the segment ring band and the pupil's anti-aliased edge need more than a table read per pixel. Integer arithmetic only; the host preview checks every frame against the full per-pixel model (`eye_pixel`). - **Still when still:** between moves the lens rests exactly, the ring rests between scans and settled lids stop, so a quiet frame redraws only the breathing pupil. - **Determinism:** a fixed-seed xorshift generator drives the behaviour, so every run shows the same frames. There is no hardware random number, clock seed, network, font or external art. Serial output: one start line, `[round-eye] frame=0 …` after the first full frame, then one progress line every two seconds with frames per second, the render share of the CPU budget, bytes and rectangles per frame and late frames. Wiring: 3V3→VCC, GND→GND, G7→DIN, G6→CLK, G5→CS, G3→DC, G4→RST. The seven-pin module's backlight is tied to VCC. The selected module uses BGR element order and inversion enabled for ordinary colours. Build with ESP-IDF **v5.5.2** (commit `30aaf64524299d3bde422ca9a2848090d1bc5d0f`), Xtensa GCC `esp-14.2.0_20251107`, `idf.py set-target esp32s3`, then `idf.py build`. The app image is `build/round-eye.bin`; Breadboard loads it at offset 0x10000. For byte-identical rebuilds set `SOURCE_DATE_EPOCH=1767225600` and retain the reproducible-build sdkconfig settings and pinned versions. The repository’s internal fixture build script is the local acceptance recipe; it is not needed for a normal IDF build or hardware flash. To flash a physical board, run `idf.py -p PORT flash monitor`; this builds and flashes the bootloader, partition table and application. Use the board’s USB-to-UART port for UART0 logs. Wire a separate BL pin to 3V3 if present. All first-party project files are MIT (see LICENSE). The two unchanged Espressif GC9A01 driver files are Apache-2.0 with their own retained license. Their [provenance](components/gc9a01/UPSTREAM.md) explains the first-party CMake integration. ESP-IDF and the target runtime retain their own notices beside the served application image. Code opens the example as a standard editable ESP-IDF full project. Compile & run uses the advertised full-project compiler (Pro), currently IDF v5.3.5. The bundled fixture was built with IDF v5.5.2. The startup banner is a fixed string: cloud rebuilds still print `IDF v5.5.2` in their first line, even though they use the compiler’s pinned version and produce a complete merged flash image. Both paths use the unchanged in-tree GC9A01 driver.
LICENSE
MIT License Copyright (c) 2026 Breadboard Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
CMakeLists.txt
cmake_minimum_required(VERSION 3.16) set(COMPONENTS main) include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(round-eye) idf_build_get_property(round_components BUILD_COMPONENTS) foreach(component bt esp_wifi esp_phy esp_coex esp_netif esp_eth lwip openthread wpa_supplicant) if(component IN_LIST round_components) message(FATAL_ERROR "Unexpected network/radio component: ${component}") endif() endforeach()
sdkconfig.defaults
CONFIG_IDF_TARGET="esp32s3" CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y CONFIG_COMPILER_OPTIMIZATION_PERF=y CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y CONFIG_FREERTOS_UNICORE=y CONFIG_ESP_CONSOLE_SECONDARY_NONE=y CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 CONFIG_APP_PROJECT_VER_FROM_CONFIG=y CONFIG_APP_PROJECT_VER="round-eye-2" CONFIG_APP_REPRODUCIBLE_BUILD=y
main/CMakeLists.txt
idf_component_register(SRCS "main.c" INCLUDE_DIRS "." REQUIRES gc9a01 esp_lcd esp_driver_spi esp_driver_gpio esp_driver_uart esp_timer esp_hw_support vfs)
main/main.c
/* SPDX-License-Identifier: MIT * Copyright (c) 2026 Breadboard * Original robot eye on a 240x240 round GC9A01A, 60 frames per second on the * ESP32-S3: esp_timer pacing, dirty rectangles and two DMA buffers. */ #include <assert.h> #include <stdint.h> #include <stdio.h> #include "driver/spi_master.h" #include "esp_cpu.h" #include "esp_heap_caps.h" #include "esp_lcd_gc9a01.h" #include "esp_lcd_panel_io.h" #include "esp_lcd_panel_ops.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "eye.h" #define PCLK_HZ (80 * 1000 * 1000) /* the pinned driver's own default clock */ #define REPORT_FRAMES (2 * EYE_FPS) static esp_lcd_panel_handle_t panel; static uint16_t *buffers[2]; static unsigned next_buffer; static TaskHandle_t eye_task; static void frame_due(void *arg) { (void)arg; xTaskNotifyGive(eye_task); } /* Render each rectangle into the buffer the bus is not reading and queue it. * draw_bitmap first waits for the previous color transfer, so the buffer * written here was released by the transfer before last. */ static unsigned send(const eye_frame_t *f, const eye_rect_t *rects, int n, uint32_t *render_cycles) { unsigned bytes = 0; for (int i = 0; i < n; i++) { const eye_rect_t r = rects[i]; uint16_t *pixels = buffers[next_buffer]; next_buffer ^= 1; const uint32_t start = esp_cpu_get_cycle_count(); eye_render_rect(f, r, pixels); *render_cycles += esp_cpu_get_cycle_count() - start; ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(panel, r.x0, r.y0, r.x1, r.y1, pixels)); bytes += 2u * (unsigned)((r.x1 - r.x0) * (r.y1 - r.y0)) + 11; /* + CASET/RASET/RAMWR */ } return bytes; } void app_main(void) { printf("[round-eye] ESP32-S3 / GC9A01A 240x240 / IDF v5.5.2 / SPI 80 MHz / %d fps\n", EYE_FPS); eye_task = xTaskGetCurrentTaskHandle(); const spi_bus_config_t bus = { .sclk_io_num = 6, .mosi_io_num = 7, .miso_io_num = -1, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = MAX_RECT_PX * 2, }; ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus, SPI_DMA_CH_AUTO)); const esp_lcd_panel_io_spi_config_t io_config = { .dc_gpio_num = 3, .cs_gpio_num = 5, .pclk_hz = PCLK_HZ, .lcd_cmd_bits = 8, .lcd_param_bits = 8, .spi_mode = 0, .trans_queue_depth = 4, }; esp_lcd_panel_io_handle_t io; ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &io)); const esp_lcd_panel_dev_config_t panel_config = { .reset_gpio_num = 4, .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR, .bits_per_pixel = 16, }; ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(io, &panel_config, &panel)); ESP_ERROR_CHECK(esp_lcd_panel_reset(panel)); ESP_ERROR_CHECK(esp_lcd_panel_init(panel)); ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel, true)); ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel, true)); for (int i = 0; i < 2; i++) { buffers[i] = heap_caps_malloc(MAX_RECT_PX * 2, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); assert(buffers[i]); } eye_init(); static eye_brain_t brain; static eye_frame_t frames[2]; eye_pose_t pose; eye_rect_t rects[MAX_RECTS]; eye_brain_init(&brain, EYE_SEED); eye_brain_step(&brain, &pose); eye_prepare(&pose, &frames[0]); uint32_t render_cycles = 0; unsigned bytes = send(&frames[0], rects, eye_full(rects), &render_cycles); printf("[round-eye] frame=0 full redraw %u bytes\n", bytes); esp_timer_handle_t timer; const esp_timer_create_args_t timer_args = { .callback = frame_due, .name = "eye-frame" }; ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer)); const int64_t start = esp_timer_get_time(); int64_t report_at = start; unsigned report_rects = 0, late = 0; bytes = 0; render_cycles = 0; for (uint32_t n = 1;; n++) { /* Deadlines are absolute, so 60 frames take exactly one second. */ const int64_t due = start + (int64_t)n * 1000000 / EYE_FPS; const int64_t now = esp_timer_get_time(); if (due > now) { ESP_ERROR_CHECK(esp_timer_start_once(timer, (uint64_t)(due - now))); ulTaskNotifyTake(pdTRUE, portMAX_DELAY); } else { late++; } const int64_t woke = esp_timer_get_time(); eye_frame_t *prev = &frames[(n - 1) & 1], *next = &frames[n & 1]; eye_brain_step(&brain, &pose); eye_prepare(&pose, next); const int count = eye_dirty(prev, next, rects); bytes += send(next, rects, count, &render_cycles); report_rects += (unsigned)count; if (n % REPORT_FRAMES == 0) { /* Frame starts, deadline to deadline: exactly 60.00 unless frames run late. */ const unsigned centi_fps = (unsigned)(REPORT_FRAMES * 100000000LL / (woke - report_at)); /* Render share of the CPU budget, in tenths of a percent. */ const unsigned busy = (unsigned)((uint64_t)render_cycles * 1000 / ((uint64_t)REPORT_FRAMES * (240000000 / EYE_FPS))); printf("[round-eye] t=%lus frames=%lu fps=%u.%02u render=%u.%u%% bytes/frame=%u rects/frame=%u.%02u late=%u\n", (unsigned long)(n / EYE_FPS), (unsigned long)n, centi_fps / 100, centi_fps % 100, busy / 10, busy % 10, bytes / REPORT_FRAMES, report_rects / REPORT_FRAMES, report_rects * 100 / REPORT_FRAMES % 100, late); report_at = woke; bytes = 0; render_cycles = 0; report_rects = 0; } } }
main/eye.h
/* SPDX-License-Identifier: MIT * Copyright (c) 2026 Breadboard * Original robot eye for a 240x240 round display: behaviour, dirty rectangles * and a fixed-point renderer. Plain C with no ESP-IDF calls. */ #pragma once #include <math.h> #include <stdint.h> #include <string.h> #define EYE_W 240 #define EYE_FPS 60 #define IRIS_R 52 /* iris radius, px */ #define HALO 9 /* glow beyond the iris edge, px */ #define IRIS_RH (IRIS_R + HALO) #define GAZE_MAX 54.0f /* furthest resting iris offset from the centre, px */ #define GAZE_CLAMP 58.0f /* hard limit, leaves room for spring overshoot */ #define EYEBALL_R 108.0f /* virtual eyeball radius: the iris foreshortens as it turns */ #define LID_SHADOW 7 /* lid shadow depth on the eye, px */ #define LID_DEPTH 6 /* lid shading depth beyond its edge, px */ #define CLOSE_Y 150.0f /* where the lids meet in a blink */ #define MAX_RECTS 24 #define LID_SLICE 30 /* columns per lid band rectangle */ #define RECT_COST_PX 3000 /* a rectangle's driver overhead, in mostly-background pixels */ #define MAX_RECT_PX 16000 /* one SPI DMA transaction of at most 32,000 bytes */ #define EYE_SEED 0x5eedb0b5u /* fixed seed: every run shows the same frames */ #ifndef EYE_STAT #define EYE_STAT(name) #endif typedef struct { int16_t x0, y0, x1, y1; } eye_rect_t; /* x1/y1 exclusive */ /* Everything one frame shows. Lid edges: y = c_y + c_k*dx^2 + tilt*dx with * dx measured from the screen centre (the bottom lid uses -c_k). */ typedef struct { float gx, gy, pupil; float top_y, top_k, top_tilt; float bot_y, bot_k, bot_tilt; float vx, vy; /* iris velocity, px/s: squash and stretch along fast moves */ float scale; /* iris size, 1 = IRIS_R */ float spin; /* segment ring rotation, turns */ float glint; /* glint sweep progress 0..1, < 0 when none */ float top_blur, bot_blur; /* lid edge softness, px: motion blur while the lids move fast */ } eye_pose_t; typedef struct { int32_t ix4, iy4; /* iris centre, 1/16 px */ int32_t m00, m01, m10, m11; /* screen offset -> iris plane (undo size, stretch, turn), Q12 */ int32_t ext4; /* screen radius that contains iris and glow, 1/16 px */ int32_t rp2, rp_lo, rp_hi, rp_m; /* pupil radius terms, 1/256 px^2 */ int32_t rp_in; /* plain pupil inside its rim, 1/256 px^2 */ uint16_t spin16; /* segment ring phase, 1/65536 turn */ uint8_t gl_on; /* a glint is crossing the lens */ int32_t gl_p4; /* glint band offset along GLINT_N from the iris centre, 1/16 px */ int32_t h1x, h1y, h2x, h2y; /* highlight offsets from the iris centre, 1/16 px */ int16_t top4[EYE_W], bot4[EYE_W]; /* lid edges per column, 1/16 px */ int32_t top_min4, bot_max4; /* the top edge's highest point, the bottom edge's lowest */ int32_t thw, bhw; /* lid edge half-widths (anti-aliasing or motion blur), 1/16 px */ int32_t thw_inv, bhw_inv; /* 128/half-width, Q16: the ramp without a division */ uint8_t top_bowl, bot_bowl; /* edge curvature lets the row test use the span ends */ } eye_frame_t; /* ---- lookup tables, built once at boot ------------------------------------ */ #define RH2 (IRIS_RH * IRIS_RH) #define RH4 (IRIS_RH * 16) #define RING_Q (RH2 * 256) #define IRIS_R2Q (IRIS_R * IRIS_R * 256) #define VIG_S4 (4 * 97 * 97) /* rim darkening starts at r = 97 px */ #define BG_RGB 0x0c151eu #define PUPIL_RGB 0x02060au #define PUPIL_RIM_RGB 0x0e5670u /* a dim lens rim just inside the pupil */ #define LID_RIM_RGB 0x46697du #define SEG_RGB 0x8cf2ffu /* lit segments of the rotating ring */ #define SEG_R0 39.25f /* segment ring band, px (0.755 and 0.86 of IRIS_R) */ #define SEG_R1 44.75f #define SEG_LO 1540 /* floor(SEG_R0^2) */ #define SEG_HI 2003 /* ceil(SEG_R1^2) */ #define SEG_LO_Q (SEG_LO * 256) #define SEG_HI_Q (SEG_HI * 256) #define ANG_W (2 * IRIS_RH + 1) #define GLINT_NX 3138 /* glint sweep direction (cos 40, sin 40), Q12 */ #define GLINT_NY 2633 #define GLINT_W4 (7 * 16) /* glint half width, 1/16 px */ static uint32_t ring_lut[RH2 + 1]; /* iris and glow over the background, by r^2 (px^2) */ static uint8_t vig_lut[3601]; /* rim darkening by r^2/4 */ static uint32_t lid_lut[LID_DEPTH * 4 + 1]; /* lid colour by depth beyond its edge, 1/4 px */ static uint8_t shade_top[LID_SHADOW * 16 + 16], shade_bot[LID_SHADOW * 16 + 16]; /* of 32 */ static uint8_t hl1_lut[4 * 11 * 11 + 1], hl2_lut[4 * 5 * 5 + 1]; static uint16_t angle_lut[ANG_W * ANG_W]; /* iris-plane angle by whole-pixel offset, 1/65536 turn */ static uint8_t seg_lut[1024]; /* 32 segments under a sweeping highlight, one turn */ static uint8_t segr_lut[SEG_HI - SEG_LO]; /* the ring band's radial profile by r^2 */ #define SEG_STEPS 40 /* radial steps across the band (about 0.14 px) */ #define SEG_K (SEG_STEPS * 65536 / ((SEG_HI - SEG_LO) * 16)) static uint16_t seg_r2[SEG_STEPS]; /* each step's centre, r^2 index into ring_lut */ static uint8_t seg_rq[SEG_STEPS]; /* each step's radial profile */ static uint8_t prim_lut[33]; /* pupil rim glow by depth inside the edge, 1/16 px */ static uint8_t glint_lut[GLINT_W4 / 4 + 1];/* glint profile across the band, 1/4 px */ /* Static frame parts: the dithered background with its rim darkening, the * iris and lids in RGB565 for the four 2x2 dither phases, and each row's * aperture and darkening-free column spans. */ static uint16_t bg565[EYE_W * EYE_W]; static uint16_t lid565[4][LID_DEPTH * 4 + 1]; static uint16_t deep565[4][256]; /* the deep lid darkened toward the screen rim, by vig_lut level */ static uint16_t pupil565; /* The iris by r^2 in RGB565 for the four 2x2 dither phases, with the plain * pupil patched in per frame; MID565 marks entries that need iris_rgb (the * segment ring band and the pupil edge and rim). */ #define MID565 0x0100u static uint16_t fast565[4][RH2 + 1]; static int32_t fast_rp2 = -1, fast_in = -1, fast_hi = -1; static uint16_t seg565[4][SEG_STEPS][64]; /* the segment band by (step, level), per dither phase */ static const uint8_t phase_dither[4] = {0, 8, 12, 4}; /* 2x2 ordered thresholds of 16 */ static uint8_t ap_x0[EYE_W], ap_x1[EYE_W], vig_x0[EYE_W], vig_x1[EYE_W]; static float eye_clampf(float v, float lo, float hi) { return v < lo ? lo : v > hi ? hi : v; } static float eye_smooth(float e0, float e1, float v) { const float t = eye_clampf((v - e0) / (e1 - e0), 0.f, 1.f); return t * t * (3.f - 2.f * t); } static uint32_t eye_rgbf(float r, float g, float b) { return ((uint32_t)eye_clampf(r, 0, 255) << 16) | ((uint32_t)eye_clampf(g, 0, 255) << 8) | (uint32_t)eye_clampf(b, 0, 255); } /* ---- colour helpers ------------------------------------------------------- */ static inline uint32_t eye_lerp(uint32_t c0, uint32_t c1, uint32_t a) /* a in 0..256, 256 = c1 */ { const uint32_t ia = 256 - a; const uint32_t rb = (((c0 & 0xff00ffu) * ia + (c1 & 0xff00ffu) * a) >> 8) & 0xff00ffu; const uint32_t g = (((c0 & 0x00ff00u) * ia + (c1 & 0x00ff00u) * a) >> 8) & 0x00ff00u; return rb | g; } static inline uint32_t eye_scale(uint32_t c, uint32_t s) /* s in 0..255 */ { return ((((c & 0xff00ffu) * s) >> 8) & 0xff00ffu) | ((((c & 0x00ff00u) * s) >> 8) & 0x00ff00u); } static const uint8_t eye_bayer[4][4] = {{0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}}; static inline uint16_t eye_dither565(uint32_t c, uint32_t d) /* d: threshold of 16 */ { uint32_t r = (c >> 16) + (d >> 1), g = ((c >> 8) & 255) + (d >> 2), b = (c & 255) + (d >> 1); r = r > 255 ? 255 : r; g = g > 255 ? 255 : g; b = b > 255 ? 255 : b; const uint16_t v = (uint16_t)(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)); return (uint16_t)((v >> 8) | (v << 8)); /* RGB565, high byte first on the wire */ } static inline uint16_t eye_pack(uint32_t c, int x, int y) { return eye_dither565(c, eye_bayer[y & 3][x & 3]); } /* Iris colour profile by radius: a glowing lens with a bright collar, * two fine rings and a dark limbal edge. */ static void iris_color(float r, float *cr, float *cg, float *cb) { static const float stops[][4] = { {0.00f, 190, 250, 255}, {0.30f, 120, 236, 255}, {0.46f, 64, 208, 246}, {0.70f, 26, 142, 206}, {0.88f, 12, 80, 150}, {0.96f, 6, 38, 82}, {1.01f, 4, 22, 48}, }; const float t = r / IRIS_R; int i = 0; while (i < 5 && t > stops[i + 1][0]) i++; const float u = eye_clampf((t - stops[i][0]) / (stops[i + 1][0] - stops[i][0]), 0, 1); float c[3]; for (int k = 0; k < 3; k++) c[k] = stops[i][k + 1] + (stops[i + 1][k + 1] - stops[i][k + 1]) * u; const float collar = expf(-(r - 0.47f * IRIS_R) * (r - 0.47f * IRIS_R) / (2 * 1.6f * 1.6f)); const float ring1 = expf(-(r - 0.63f * IRIS_R) * (r - 0.63f * IRIS_R) / (2 * 0.55f * 0.55f)); c[0] += 70 * collar + 40 * ring1; c[1] += 60 * collar + 46 * ring1; c[2] += 30 * collar + 30 * ring1; *cr = c[0]; *cg = c[1]; *cb = c[2]; } static void eye_init_luts(void) { const float bg[3] = {(BG_RGB >> 16) & 255, (BG_RGB >> 8) & 255, BG_RGB & 255}; for (int i = 0; i <= RH2; i++) { const float r = sqrtf(i + 0.5f); float c[3]; iris_color(fminf(r, IRIS_R - 0.5f), &c[0], &c[1], &c[2]); static const float glow[3] = {40, 190, 255}; const float u = eye_clampf((r - IRIS_R) / HALO, 0, 1); const float ha = 0.42f * powf(1 - u, 2.2f) * (r < IRIS_RH ? 1 : 0); const float cov = eye_clampf(IRIS_R + 0.5f - r, 0, 1); /* anti-aliased iris edge */ float o[3]; for (int k = 0; k < 3; k++) o[k] = cov * c[k] + (1 - cov) * (ha * glow[k] + (1 - ha) * bg[k]); ring_lut[i] = eye_rgbf(o[0], o[1], o[2]); } for (int i = 0; i <= 3600; i++) vig_lut[i] = (uint8_t)(255 * (1 - eye_smooth(97, 120.5f, sqrtf(i * 4.f + 2))) + 0.5f); for (int i = 0; i <= LID_DEPTH * 4; i++) { const float d = i / 4.f; const float rim = 1 - eye_smooth(0.4f, 2.2f, d); const float body = eye_smooth(1.5f, LID_DEPTH, d); lid_lut[i] = eye_rgbf(70 * rim + (1 - rim) * (9 - 5 * body), 105 * rim + (1 - rim) * (13 - 7 * body), 125 * rim + (1 - rim) * (17 - 9 * body)); } for (int i = 0; i < (int)sizeof shade_top; i++) { const float d = i / 16.f; shade_top[i] = (uint8_t)lroundf(32 * (0.42f + 0.58f * eye_smooth(-0.5f, LID_SHADOW, d))); shade_bot[i] = (uint8_t)lroundf(32 * (0.70f + 0.30f * eye_smooth(-0.5f, LID_SHADOW * 0.6f, d))); } for (int i = 0; i < (int)sizeof hl1_lut; i++) hl1_lut[i] = (uint8_t)(235 * (1 - eye_smooth(4.5f, 11, sqrtf(i / 4.f)))); for (int i = 0; i < (int)sizeof hl2_lut; i++) hl2_lut[i] = (uint8_t)(200 * (1 - eye_smooth(1.6f, 5, sqrtf(i / 4.f)))); for (int y = 0; y < ANG_W; y++) for (int x = 0; x < ANG_W; x++) { const float a = atan2f((float)(y - IRIS_RH), (float)(x - IRIS_RH)) / 6.2831853f; angle_lut[y * ANG_W + x] = (uint16_t)(int32_t)lroundf((a < 0 ? a + 1 : a) * 65536); } for (int i = 0; i < 1024; i++) { /* Dashes of period 32 (lit for about 20, soft edges) under a scanning * sweep: brightest at the leading dash, fading over the turn behind it. */ const float q = (float)(i & 31), behind = (1023 - i) / 1023.f; const float sweep = 0.22f + 0.78f * powf(1 - behind, 1.6f); seg_lut[i] = (uint8_t)(205 * sweep * eye_smooth(4, 7, q) * (1 - eye_smooth(24, 27, q))); } for (int i = 0; i < SEG_HI - SEG_LO; i++) { const float r = sqrtf(SEG_LO + i + 0.5f); segr_lut[i] = (uint8_t)(255 * eye_smooth(SEG_R0, SEG_R0 + 1.2f, r) * (1 - eye_smooth(SEG_R1 - 1.2f, SEG_R1, r))); } for (int i = 0; i < SEG_STEPS; i++) { const int k = SEG_LO + (int)(((2 * i + 1) * (SEG_HI - SEG_LO)) / (2 * SEG_STEPS)); seg_r2[i] = (uint16_t)k; seg_rq[i] = segr_lut[k - SEG_LO]; } for (int i = 0; i < (int)sizeof prim_lut; i++) prim_lut[i] = (uint8_t)(150 * (1 - eye_smooth(0, 2, i / 16.f))); for (int i = 0; i < (int)sizeof glint_lut; i++) { const float d = i / 4.f; glint_lut[i] = (uint8_t)(100 * expf(-d * d / (2 * 2.6f * 2.6f))); /* a soft sheen */ } } /* ---- per-frame geometry ----------------------------------------------------- */ static void eye_prepare(const eye_pose_t *p, eye_frame_t *f) { f->ix4 = (int32_t)lroundf((120 + p->gx) * 16); f->iy4 = (int32_t)lroundf((120 + p->gy) * 16); /* Screen = size * stretch * turn * iris plane; the renderer needs the * inverse. Turned toward the rim, the iris foreshortens along the turn * (undone by I + k*u*u^T, k = 1/cos(turn) - 1). Moving fast, it stretches * by e along its velocity and narrows by 1/sqrt(e) across it. */ const float d = sqrtf(p->gx * p->gx + p->gy * p->gy); float ux = 1, uy = 0, k = 0; if (d > 0.01f) { ux = p->gx / d; uy = p->gy / d; const float q = eye_clampf(d / EYEBALL_R, 0, 0.9f); k = 1 / sqrtf(1 - q * q) - 1; } const float speed = sqrtf(p->vx * p->vx + p->vy * p->vy); float wx = 1, wy = 0; const float e = speed < 20 ? 1 : 1 + eye_clampf(speed * 0.00009f, 0, 0.14f); if (speed > 1) { wx = p->vx / speed; wy = p->vy / speed; } const float se = sqrtf(e), sw = 1 / e - se; /* stretch inverse: se*I + sw*w*w^T */ const float s00 = se + sw * wx * wx, s01 = sw * wx * wy, s11 = se + sw * wy * wy; const float f00 = 1 + k * ux * ux, f01 = k * ux * uy, f11 = 1 + k * uy * uy; const float inv = 4096 / p->scale; f->m00 = (int32_t)lroundf((f00 * s00 + f01 * s01) * inv); f->m01 = (int32_t)lroundf((f00 * s01 + f01 * s11) * inv); f->m10 = (int32_t)lroundf((f01 * s00 + f11 * s01) * inv); f->m11 = (int32_t)lroundf((f01 * s01 + f11 * s11) * inv); f->ext4 = (int32_t)ceilf(IRIS_RH * p->scale * e * 16) + 1; f->spin16 = (uint16_t)(int32_t)lroundf((p->spin - floorf(p->spin)) * 65536); f->gl_on = p->glint >= 0; if (f->gl_on) { /* eased sweep from one side of the lens to the other */ const float g = p->glint * p->glint * (3 - 2 * p->glint); f->gl_p4 = (int32_t)lroundf((-(IRIS_R + 8) + 2 * (IRIS_R + 8) * g) * p->scale * 16); } const float rp = p->pupil; f->rp2 = (int32_t)lroundf(rp * rp * 256); f->rp_lo = (int32_t)lroundf((rp - 0.5f) * (rp - 0.5f) * 256); f->rp_hi = (int32_t)lroundf((rp + 0.5f) * (rp + 0.5f) * 256); f->rp_m = (int32_t)lroundf(65536 / (2 * rp)); f->rp_in = rp > 2.5f ? (int32_t)lroundf((rp - 2.2f) * (rp - 2.2f) * 256) : 0; /* Corneal reflections stay put against the light, drifting slightly with gaze. */ f->h1x = (int32_t)lroundf((-15 - 0.10f * p->gx) * 16); f->h1y = (int32_t)lroundf((-17 - 0.10f * p->gy) * 16); f->h2x = (int32_t)lroundf((14 - 0.06f * p->gx) * 16); f->h2y = (int32_t)lroundf((15 - 0.06f * p->gy) * 16); f->thw = (int32_t)lroundf(eye_clampf(p->top_blur, 0.5f, 4) * 16); f->bhw = (int32_t)lroundf(eye_clampf(p->bot_blur, 0.5f, 4) * 16); f->thw_inv = (128 << 16) / f->thw; f->bhw_inv = (128 << 16) / f->bhw; f->top_bowl = p->top_k >= 0; /* top edge convex: its lowest point over a span is at an end */ f->bot_bowl = p->bot_k >= 0; /* bottom edge concave: its highest point is at an end */ /* Lids at rest (most frames) reuse the last edges. */ static float lid_key[6] = {1e30f}; static int16_t top_last[EYE_W], bot_last[EYE_W]; static int32_t top_min, bot_max; const float key[6] = {p->top_y, p->top_k, p->top_tilt, p->bot_y, p->bot_k, p->bot_tilt}; if (memcmp(key, lid_key, sizeof key) != 0) { memcpy(lid_key, key, sizeof key); for (int x = 0; x < EYE_W; x++) { const float dx = x + 0.5f - 120; const float t = eye_clampf(p->top_y + p->top_k * dx * dx + p->top_tilt * dx, -300, 600) * 16; const float b = eye_clampf(p->bot_y - p->bot_k * dx * dx + p->bot_tilt * dx, -300, 600) * 16; top_last[x] = (int16_t)(int32_t)(t + (t >= 0 ? 0.5f : -0.5f)); bot_last[x] = (int16_t)(int32_t)(b + (b >= 0 ? 0.5f : -0.5f)); if (x == 0 || top_last[x] < top_min) top_min = top_last[x]; if (x == 0 || bot_last[x] > bot_max) bot_max = bot_last[x]; } } f->top_min4 = top_min; f->bot_max4 = bot_max; memcpy(f->top4, top_last, sizeof top_last); memcpy(f->bot4, bot_last, sizeof bot_last); } /* ---- renderer ------------------------------------------------------------- */ /* The segment ring band, quantized (radial step, segment level) so the * renderer can take it from a table: the band never reaches the pupil. */ static inline int seg_step(int r2q) { return (((r2q >> 4) - SEG_LO * 16) * SEG_K) >> 16; } static inline int seg_level(const eye_frame_t *f, int a, int b) { const int ia = (a + 8) >> 4, ib = (b + 8) >> 4; const uint16_t turn = (uint16_t)(angle_lut[(ib + IRIS_RH) * ANG_W + ia + IRIS_RH] + f->spin16); return seg_lut[turn >> 6] >> 2; /* 0..51 */ } static inline uint32_t seg_rgb(int step, int level) { const uint32_t blend = level ? ((uint32_t)((level << 2) + 2) * seg_rq[step]) >> 8 : 0; return eye_lerp(ring_lut[seg_r2[step]], SEG_RGB, blend); } /* Iris, glow, segment ring and pupil at iris-plane offset (a, b), 1/16 px. */ static inline uint32_t iris_rgb(const eye_frame_t *f, int a, int b, int r2q) { if (r2q >= SEG_LO_Q && r2q < SEG_HI_Q) return seg_rgb(seg_step(r2q), seg_level(f, a, b)); uint32_t c = ring_lut[r2q >> 8]; if (r2q < f->rp_hi) { /* pupil: anti-aliased edge and a dim rim inside it */ const int depth = ((f->rp2 - r2q) * f->rp_m) >> 16; /* inside the edge, 1/256 px */ const uint32_t pupil = depth < 512 ? eye_lerp(PUPIL_RGB, PUPIL_RIM_RGB, prim_lut[depth < 0 ? 0 : depth >> 4]) : PUPIL_RGB; const int cov = 128 + depth; c = eye_lerp(c, pupil, cov < 0 ? 0 : cov > 256 ? 256 : (uint32_t)cov); } return c; } /* Lid colour at depth d (1/16 px) beyond its edge; the lit rim at the edge. */ static inline uint16_t scale565(uint16_t v, uint32_t s) /* byte-swapped RGB565 times s/256 */ { const uint32_t w = (uint32_t)((v >> 8) | (v << 8)) & 0xffffu; const uint32_t r = (((w >> 11) * s) >> 8) << 11, g = ((((w >> 5) & 63) * s) >> 8) << 5, b = ((w & 31) * s) >> 8; const uint32_t o = r | g | b; return (uint16_t)((o >> 8) | (o << 8)); } /* Byte-swapped RGB565 spread out (green apart from red and blue) so one * multiply weighs all three channels by a 5-bit weight, and back. */ #define X565 0x07e0f81fu static inline uint32_t x565(uint16_t v) { const uint32_t w = (uint32_t)((v >> 8) | (v << 8)) & 0xffffu; return (w | (w << 16)) & X565; } static inline uint16_t p565(uint32_t e) { const uint32_t o = (e | (e >> 16)) & 0xffffu; return (uint16_t)((o >> 8) | (o << 8)); } /* A lid's body at depth d >= 0 beyond its edge, darkened toward the screen * rim (the deep lid from its own table). */ static inline uint16_t lid_body(int d, int x, int y, int s4) { const int i = d >> 2 > LID_DEPTH * 4 ? LID_DEPTH * 4 : d >> 2, p = ((y & 1) << 1) | (x & 1); if (s4 <= VIG_S4) return lid565[p][i]; return i == LID_DEPTH * 4 ? deep565[p][vig_lut[s4 >> 4]] : scale565(lid565[p][i], vig_lut[s4 >> 4]); } /* The lids over a lid-free pixel `base` (byte-swapped RGB565): the top lid's * body, else the bottom lid's, else their shadows and edge ramps (one pixel * of anti-aliasing at rest, wider while a lid moves fast). Shared by the * renderer and the full model, so a pixel never depends on its path. */ static uint16_t lid_over(const eye_frame_t *f, uint16_t base, int x, int y, int s4) { const int y4 = y * 16 + 8, dT = y4 - f->top4[x], dB = f->bot4[x] - y4; if (dT <= -f->thw) return lid_body(-dT, x, y, s4); if (dB <= -f->bhw) return lid_body(-dB, x, y, s4); uint32_t s = 32; if (dT < LID_SHADOW * 16) s = shade_top[dT < 0 ? 0 : dT]; if (dB < LID_SHADOW * 16) s = (s * shade_bot[dB < 0 ? 0 : dB]) >> 5; uint32_t e = ((x565(base) * s) >> 5) & X565; /* s = 32 leaves it as is */ if (dT < f->thw) { /* coverage of the eye, of 32 */ const uint32_t a = (uint32_t)(((dT + f->thw) * f->thw_inv) >> 19); e = ((x565(lid_body(dT < 0 ? -dT : 0, x, y, s4)) * (32 - a) + e * a) >> 5) & X565; } if (dB < f->bhw) { const uint32_t a = (uint32_t)(((dB + f->bhw) * f->bhw_inv) >> 19); e = ((x565(lid_body(dB < 0 ? -dB : 0, x, y, s4)) * (32 - a) + e * a) >> 5) & X565; } return p565(e); } /* The lit lens at screen offset (dx, dy) from the iris centre, 1/16 px: * iris, glow, segment ring, pupil, glint and highlights; else background. */ static uint32_t eye_lens(const eye_frame_t *f, int dx, int dy) { if (dy <= -f->ext4 || dy >= f->ext4 || dx <= -f->ext4 || dx >= f->ext4) return BG_RGB; const int ex = (f->m00 * dx + f->m01 * dy) >> 12, ey = (f->m10 * dx + f->m11 * dy) >> 12; const int r2q = ex * ex + ey * ey; if (r2q >= RING_Q) return BG_RGB; uint32_t c = iris_rgb(f, ex, ey, r2q); if (r2q < IRIS_R2Q) { if (f->gl_on) { /* a soft light band sweeping across the lens */ const int u = (dx * GLINT_NX + dy * GLINT_NY) >> 12, g = u - f->gl_p4; const int ag = g < 0 ? -g : g; if (ag < GLINT_W4) c = eye_lerp(c, 0xffffffu, glint_lut[ag >> 2]); } int hx = dx - f->h1x, hy = dy - f->h1y; int d2 = (hx * hx + hy * hy) >> 6; if (d2 < (int)sizeof hl1_lut) c = eye_lerp(c, 0xffffffu, hl1_lut[d2]); hx = dx - f->h2x; hy = dy - f->h2y; d2 = (hx * hx + hy * hy) >> 6; if (d2 < (int)sizeof hl2_lut) c = eye_lerp(c, 0xffffffu, hl2_lut[d2]); } return c; } /* The eye without lids at (x, y): lens or background, rim darkening, RGB565. */ static uint16_t spot_pixel(const eye_frame_t *f, int x, int y) { const int X = 2 * x + 1 - EYE_W, Y = 2 * y + 1 - EYE_W, s4 = X * X + Y * Y; uint32_t c = eye_lens(f, x * 16 + 8 - f->ix4, y * 16 + 8 - f->iy4); if (s4 > VIG_S4) c = eye_scale(c, vig_lut[s4 >> 4]); return eye_pack(c, x, y); } /* The complete per-pixel model: the lid-free eye, then the lids. The span * renderer reproduces it (within a 2x2 vs 4x4 dither step) for the common * cases from tables. */ static uint16_t eye_pixel(const eye_frame_t *f, int x, int y) { const int X = 2 * x + 1 - EYE_W, Y = 2 * y + 1 - EYE_W, s4 = X * X + Y * Y; if (s4 > 57600) return 0; /* outside the round aperture */ return lid_over(f, spot_pixel(f, x, y), x, y, s4); } /* One iris entry: the RGB565 colour, or MID565 inside the segment ring band. */ static uint16_t ring_entry(int p, int k) { if (k >= SEG_LO && k < SEG_HI) return MID565; const uint16_t v = eye_dither565(ring_lut[k], phase_dither[p]); return v == MID565 ? MID565 ^ 0x2000u : v; /* keep the sentinel unique */ } static void eye_init_frame_parts(void) { static eye_frame_t open; /* no iris, lids far away: the bare background */ open.ix4 = open.iy4 = -100000; open.thw = open.bhw = 8; open.thw_inv = open.bhw_inv = (128 << 16) / 8; for (int x = 0; x < EYE_W; x++) { open.top4[x] = -4000; open.bot4[x] = 8000; } open.top_min4 = -4000; open.bot_max4 = 8000; for (int y = 0; y < EYE_W; y++) { const int Y = 2 * y + 1 - EYE_W; int a0 = EYE_W, a1 = 0, v0 = EYE_W, v1 = 0; for (int x = 0; x < EYE_W; x++) { const int s4 = (2 * x + 1 - EYE_W) * (2 * x + 1 - EYE_W) + Y * Y; if (s4 <= 57600) { if (x < a0) a0 = x; a1 = x + 1; } if (s4 <= VIG_S4) { if (x < v0) v0 = x; v1 = x + 1; } bg565[y * EYE_W + x] = eye_pixel(&open, x, y); } ap_x0[y] = (uint8_t)(a0 < a1 ? a0 : 0); ap_x1[y] = (uint8_t)(a0 < a1 ? a1 : 0); vig_x0[y] = (uint8_t)(v0 < v1 ? v0 : 0); vig_x1[y] = (uint8_t)(v0 < v1 ? v1 : 0); } for (int p = 0; p < 4; p++) { for (int i = 0; i <= RH2; i++) fast565[p][i] = ring_entry(p, i); for (int i = 0; i <= LID_DEPTH * 4; i++) lid565[p][i] = eye_dither565(lid_lut[i], phase_dither[p]); for (int v = 0; v < 256; v++) deep565[p][v] = eye_dither565(eye_scale(lid_lut[LID_DEPTH * 4], (uint32_t)v), phase_dither[p]); for (int i = 0; i < SEG_STEPS; i++) for (int l = 0; l < 64; l++) seg565[p][i][l] = eye_dither565(seg_rgb(i, l), phase_dither[p]); } pupil565 = eye_dither565(PUPIL_RGB, 0); } static void eye_init(void) { eye_init_luts(); eye_init_frame_parts(); } /* Bring fast565's pupil entries to this frame's pupil: the plain pupil, its * dim rim by r^2 (a gentle gradient: a 1 px^2 step is under 0.05 px), MID565 * for the anti-aliased edge (exact per pixel), then the iris. */ static void eye_patch_pupil(const eye_frame_t *f) { if (f->rp2 == fast_rp2) return; const int in = f->rp_in >= 255 ? (f->rp_in - 255) >> 8 : -1; /* whole entries inside the plain pupil */ const int edge = f->rp_lo >> 8; /* first entry the edge reaches */ const int hi = f->rp_hi >> 8; /* last entry touching the edge */ const int lo = (in < fast_in ? in : fast_in) + 1, top = hi > fast_hi ? hi : fast_hi; for (int k = lo < 0 ? 0 : lo; k <= top && k <= RH2; k++) { if (k <= in) { for (int p = 0; p < 4; p++) fast565[p][k] = pupil565; continue; } if (k > hi) { for (int p = 0; p < 4; p++) fast565[p][k] = ring_entry(p, k); continue; } if (k >= edge) { for (int p = 0; p < 4; p++) fast565[p][k] = MID565; continue; } const uint32_t c = iris_rgb(f, 0, 0, k * 256 + 128); /* the band never reaches the pupil */ for (int p = 0; p < 4; p++) { const uint16_t v = eye_dither565(c, phase_dither[p]); fast565[p][k] = v == MID565 ? MID565 ^ 0x2000u : v; /* keep the sentinel unique */ } } fast_rp2 = f->rp2; fast_in = in; fast_hi = hi; } /* Iris, glow and pupil for columns [x0, x1) of one row that no lid or * highlight reaches; rim > 0 applies the rim darkening. The turn transform * steps per column. */ /* The iris pixels the table cannot give (the segment ring band and the * pupil's anti-aliased edge), out of line so the span loop keeps its * registers. */ static void __attribute__((noinline)) iris_mid(const eye_frame_t *f, uint16_t *dst, int a, int b, int r2q, int x, int y) { if (r2q >= SEG_LO_Q) { EYE_STAT(ring); *dst = seg565[((y & 1) << 1) | (x & 1)][seg_step(r2q)][seg_level(f, a, b)]; return; } EYE_STAT(rim); *dst = eye_pack(iris_rgb(f, a, b, r2q), x, y); } #define IRIS_PX(TAB, X) \ do { \ const int a_ = ex >> 12, b_ = ey >> 12, q_ = a_ * a_ + b_ * b_; \ if (q_ < RING_Q) { /* else background, already copied */ \ const uint16_t v_ = (TAB)[q_ >> 8]; \ if (v_ != MID565) { EYE_STAT(ring); row[X] = v_; } \ else *mid++ = (uint8_t)(X); \ } \ ex += sx; ey += sy; \ } while (0) static void iris_span(const eye_frame_t *f, uint16_t *row, int y, int x0, int x1, int rim) { const int dy = y * 16 + 8 - f->iy4, dx = x0 * 16 + 8 - f->ix4; int ex = f->m00 * dx + f->m01 * dy, ey = f->m10 * dx + f->m11 * dy; const int sx = 16 * f->m00, sy = 16 * f->m10; const uint16_t *even = fast565[(y & 1) << 1], *odd = fast565[((y & 1) << 1) | 1]; if (!rim) { /* two pixels per step: fixed dither tables, no per-pixel select */ uint8_t mids[EYE_W], *mid = mids; /* columns the tables leave to iris_mid */ int x = x0; if (x & 1) { IRIS_PX(odd, x); x++; } for (; x + 1 < x1; x += 2) { IRIS_PX(even, x); IRIS_PX(odd, x + 1); } if (x < x1) IRIS_PX(even, x); for (const uint8_t *m = mids; m < mid; m++) { const int mx = *m, mdx = mx * 16 + 8 - f->ix4; const int a = (f->m00 * mdx + f->m01 * dy) >> 12, b = (f->m10 * mdx + f->m11 * dy) >> 12; iris_mid(f, &row[mx], a, b, a * a + b * b, mx, y); } return; } const uint16_t (*seg_e)[64] = seg565[(y & 1) << 1], (*seg_o)[64] = seg565[((y & 1) << 1) | 1]; const int Y = 2 * y + 1 - EYE_W, Y2 = Y * Y; for (int x = x0; x < x1; x++, ex += sx, ey += sy) { /* the rim-darkening zone */ const int a = ex >> 12, b = ey >> 12; const int r2q = a * a + b * b; if (r2q >= RING_Q) continue; const int X = 2 * x + 1 - EYE_W; const uint32_t vig = vig_lut[(X * X + Y2) >> 4]; const uint16_t v = ((x & 1) ? odd : even)[r2q >> 8]; EYE_STAT(rim); if (v != MID565) row[x] = scale565(v, vig); else if (r2q >= SEG_LO_Q) row[x] = scale565(((x & 1) ? seg_o : seg_e)[seg_step(r2q)][seg_level(f, a, b)], vig); else row[x] = eye_pack(eye_scale(iris_rgb(f, a, b, r2q), vig), x, y); } } /* A highlight's columns on one row, widened by a pixel each side. */ static int spot_span(int y4, int cx4, int cy4, int rad4, int16_t *span) { const int dy = y4 - cy4; if (dy <= -rad4 || dy >= rad4) return 0; const int half = (int)sqrtf((float)(rad4 * rad4 - dy * dy)) + 16; span[0] = (int16_t)((cx4 - half) >> 4); span[1] = (int16_t)(((cx4 + half) >> 4) + 1); return 1; } /* Rows y with 16y+8 < v (clamped to the screen). */ static inline int rows_before(int v) { const int n = (v - 8 + 15) >> 4; return n < 0 ? 0 : n > EYE_W ? EYE_W : n; } /* Rows [ya, yb) of one column under one lid's edge ramp and shadow only * (no body, the other lid out of reach): lid_over without its tests. sign -1 * is the top lid (eye below its edge e4), +1 the bottom lid. */ static void lid_ramp1(uint16_t *px, int w, int x, int ya, int yb, int e4, int sign, int hw, int hw_inv, const uint8_t *shade) { const uint16_t *even = lid565[x & 1], *odd = lid565[2 | (x & 1)]; const int v0 = vig_x0[x], v1 = vig_x1[x]; int d = -sign * (ya * 16 + 8 - e4); /* depth into the eye, 1/16 px */ for (int y = ya; y < yb; y++, px += w, d -= sign * 16) { EYE_STAT(lid); uint32_t e = ((x565(*px) * shade[d < 0 ? 0 : d]) >> 5) & X565; if (d < hw) { const uint32_t a = (uint32_t)(((d + hw) * hw_inv) >> 19); uint16_t lid; if (y < v0 || y >= v1) { const int X = 2 * x + 1 - EYE_W, Y = 2 * y + 1 - EYE_W; lid = lid_body(d < 0 ? -d : 0, x, y, X * X + Y * Y); } else lid = ((y & 1) ? odd : even)[d < 0 ? (-d >> 2 > LID_DEPTH * 4 ? LID_DEPTH * 4 : -d >> 2) : 0]; e = ((x565(lid) * (32 - a) + e * a) >> 5) & X565; } *px = p565(e); } } /* Rows [ya, yb) of one column under lid edge ramps and shadows. */ static void lid_ramps(const eye_frame_t *f, uint16_t *px, int w, int x, int ya, int yb) { const int X = 2 * x + 1 - EYE_W; for (int y = ya; y < yb; y++, px += w) { EYE_STAT(lid); const int Y = 2 * y + 1 - EYE_W; *px = lid_over(f, *px, x, y, X * X + Y * Y); } } /* Rows [ya, yb) of one column under a lid's body, at depth 16y+8 - e4 * (sign -1: the top lid, deeper upward) from its edge e4. The deep lid, rows * [deep_a, deep_b) at one end, is a two-tone fill; the rest takes the gradient. */ static inline void lid_rows(uint16_t *px, int w, int x, int ya, int yb, int e4, int sign, int deep_a, int deep_b) { const uint16_t *even = lid565[x & 1], *odd = lid565[2 | (x & 1)]; const int v0 = vig_x0[x], v1 = vig_x1[x]; int ga = ya, gb = yb; /* the gradient rows */ if (deep_b > deep_a) { /* Rows [n0, n1) clear of the rim darkening take the fill. */ const int n0 = v0 < deep_a ? deep_a : v0 > deep_b ? deep_b : v0; const int n1 = v1 < n0 ? n0 : v1 > deep_b ? deep_b : v1; const uint16_t p = ((n0 & 1) ? odd : even)[LID_DEPTH * 4], q = ((n0 & 1) ? even : odd)[LID_DEPTH * 4]; uint16_t *d = px + (n0 - ya) * w; const int n = n1 - n0; for (int k = 0; k + 1 < n; k += 2, d += 2 * w) { d[0] = p; d[w] = q; } if (n & 1) d[0] = p; const uint16_t *deep_e = deep565[x & 1], *deep_o = deep565[2 | (x & 1)]; for (int k = 0; k < 2; k++) { /* the rim rows [deep_a, n0) and [n1, deep_b) */ const int y0 = k ? n1 : deep_a, y1 = k ? deep_b : n0; int Y = 2 * y0 + 1 - EYE_W, s4 = (2 * x + 1 - EYE_W) * (2 * x + 1 - EYE_W) + Y * Y; uint16_t *q = px + (y0 - ya) * w; for (int y = y0; y < y1; y++, q += w, s4 += 4 * Y + 4, Y += 2) *q = ((y & 1) ? deep_o : deep_e)[vig_lut[s4 >> 4]]; } if (deep_a == ya) ga = deep_b; else gb = deep_a; } px += (ga - ya) * w; int d = sign * (ga * 16 + 8 - e4); for (int y = ga; y < gb; y++, px += w, d += sign * 16) { EYE_STAT(lidbody); if (y < v0 || y >= v1) { const int X = 2 * x + 1 - EYE_W, Y = 2 * y + 1 - EYE_W; *px = lid_body(d, x, y, X * X + Y * Y); } else *px = ((y & 1) ? odd : even)[d >> 2 > LID_DEPTH * 4 ? LID_DEPTH * 4 : d >> 2]; } } /* Lay both lids over columns of a rendered rectangle. Each column's rows are * classified once from its two edges, top lid first, exactly as lid_over * would: the top body (rows above t_body), edge ramps and shadows, the bottom * body (rows from b_body). */ static void lid_columns(const eye_frame_t *f, eye_rect_t r, uint16_t *out) { const int w = r.x1 - r.x0; const int reach_t = f->thw > LID_SHADOW * 16 ? f->thw : LID_SHADOW * 16; const int reach_b = f->bhw > LID_SHADOW * 16 ? f->bhw : LID_SHADOW * 16; const int deep_t = f->thw > LID_DEPTH * 16 ? f->thw : LID_DEPTH * 16; const int deep_b = f->bhw > LID_DEPTH * 16 ? f->bhw : LID_DEPTH * 16; for (int x = r.x0; x < r.x1; x++) { int y0 = r.y0 > ap_x0[x] ? r.y0 : ap_x0[x], y1 = r.y1 < ap_x1[x] ? r.y1 : ap_x1[x]; if (y0 >= y1) continue; /* the aperture is symmetric: column spans equal row spans */ const int et = f->top4[x], eb = f->bot4[x]; const int t_end = rows_before(et + reach_t); /* top lid reaches rows < t_end */ const int b_start = EYE_W - rows_before(EYE_W * 16 - eb + reach_b); /* bottom lid: rows >= b_start */ if (t_end <= y0 && b_start >= y1) continue; const int t_deep = rows_before(et - deep_t + 1), t_body = rows_before(et - f->thw + 1); const int b_deep = EYE_W - rows_before(EYE_W * 16 - eb - deep_b + 1); const int b_body = EYE_W - rows_before(EYE_W * 16 - eb - f->bhw + 1); uint16_t *col = out + (x - r.x0); #define LID_ROW(y) (col + ((y) - r.y0) * w) int y = y0; const int tb = t_body < y1 ? t_body : y1; if (y < tb) { lid_rows(LID_ROW(y), w, x, y, tb, et, -1, y, t_deep < tb ? t_deep : tb); y = tb; } const int bb = b_body > y ? (b_body < y1 ? b_body : y1) : y; /* the bottom body starts at bb */ const int te = t_end < bb ? t_end : bb; /* top ramps and shadows: rows [y, te) */ int bs = b_start > y ? b_start : y; /* bottom ramps and shadows: rows [bs, bb) */ if (bs < te) bs = te; /* Where the two lids' reaches overlap, the general model; else one lid. */ const int both0 = b_start > y ? b_start : y, both1 = t_end < bb ? t_end : bb; if (both0 < both1) { if (y < both0) lid_ramp1(LID_ROW(y), w, x, y, both0, et, -1, f->thw, f->thw_inv, shade_top); lid_ramps(f, LID_ROW(both0), w, x, both0, both1); if (both1 < bb) lid_ramp1(LID_ROW(both1), w, x, both1, bb, eb, 1, f->bhw, f->bhw_inv, shade_bot); } else { if (y < te) lid_ramp1(LID_ROW(y), w, x, y, te, et, -1, f->thw, f->thw_inv, shade_top); if (bs < bb) lid_ramp1(LID_ROW(bs), w, x, bs, bb, eb, 1, f->bhw, f->bhw_inv, shade_bot); } y = bb; if (y < y1) lid_rows(LID_ROW(y), w, x, y, y1, eb, 1, b_deep > y ? (b_deep < y1 ? b_deep : y1) : y, y1); #undef LID_ROW } } /* Byte-swapped RGB565 moved a/256 of the way to white darkened by v/255, in * RGB565 units with the 4x4 ordered threshold d of eye_pack. */ static inline uint16_t light565(uint16_t base, uint32_t a, uint32_t v, uint32_t d) { const uint32_t w = (uint32_t)((base >> 8) | (base << 8)) & 0xffffu, ia = 256 - a; const uint32_t t5 = (31 * v + 128) >> 8, t6 = (63 * v + 128) >> 8; const uint32_t rb = ((w >> 11) << 16) | (w & 31); /* red and blue apart: room for 8-bit weights */ const uint32_t o_rb = ((rb * ia + ((t5 << 16) | t5) * a + d * 0x00100010u) >> 8) & 0x001f001fu; const uint32_t o_g = (((w >> 5) & 63) * ia + t6 * a + d * 16) >> 8; const uint32_t o = ((o_rb >> 16) << 11) | (o_g << 5) | (o_rb & 31); return (uint16_t)((o >> 8) | (o << 8)); } /* Highlights and the glint over the rendered lens on columns [a, b) of row y: * a blend toward white (darkened at the rim like the lens), as in eye_lens. * lights: bit 0 glint, 1 and 2 the highlights; the others are dark here. */ static void spot_blend(const eye_frame_t *f, uint16_t *row, int y, int a, int b, unsigned lights) { const int dy = y * 16 + 8, Y = 2 * y + 1 - EYE_W, Y2 = Y * Y; const int dx0 = a * 16 + 8 - f->ix4, ddy = dy - f->iy4; int gn = dx0 * GLINT_NX + ddy * GLINT_NY; /* glint offset, Q12 */ int ex = f->m00 * dx0 + f->m01 * ddy, ey = f->m10 * dx0 + f->m11 * ddy; const int h1y = ddy - f->h1y, h2y = ddy - f->h2y, h1y2 = h1y * h1y, h2y2 = h2y * h2y; int h1x = dx0 - f->h1x, h2x = dx0 - f->h2x; for (int x = a; x < b; x++, gn += 16 * GLINT_NX, ex += 16 * f->m00, ey += 16 * f->m10, h1x += 16, h2x += 16) { uint32_t al0 = 0, al1 = 0, al2 = 0; if (lights & 1) { const int g = (gn >> 12) - f->gl_p4, ag = g < 0 ? -g : g; if (ag < GLINT_W4) al0 = glint_lut[ag >> 2]; } if (lights & 2) { const int d2 = (h1x * h1x + h1y2) >> 6; if (d2 < (int)sizeof hl1_lut) al1 = hl1_lut[d2]; } if (lights & 4) { const int d2 = (h2x * h2x + h2y2) >> 6; if (d2 < (int)sizeof hl2_lut) al2 = hl2_lut[d2]; } if (!(al0 | al1 | al2)) continue; const int ea = ex >> 12, eb = ey >> 12; if (ea * ea + eb * eb >= IRIS_R2Q) continue; /* light only the lens */ EYE_STAT(slow); /* One blend toward white for all the lights: 1 - (1-a0)(1-a1)(1-a2). */ const uint32_t keep = (((256 - al0) * (256 - al1)) >> 8) * (256 - al2) >> 8; const int X = 2 * x + 1 - EYE_W, s4 = X * X + Y2; row[x] = light565(row[x], 256 - keep, s4 > VIG_S4 ? vig_lut[s4 >> 4] : 255, eye_bayer[y & 3][x & 3]); } } /* Render one rectangle row-major into out (width*height pixels): first the * eye without lids, row by row, skipping columns a lid covers; then its * highlights; then the lids, column by column. */ static void eye_render_rect(const eye_frame_t *f, eye_rect_t r, uint16_t *out) { eye_patch_pupil(f); const int w = r.x1 - r.x0; uint16_t *rowp = out; for (int y = r.y0; y < r.y1; y++, rowp += w) { memcpy(rowp, &bg565[y * EYE_W + r.x0], (size_t)w * 2); /* untouched background */ EYE_STAT(rows); const int lo = r.x0 > ap_x0[y] ? r.x0 : ap_x0[y], hi = r.x1 < ap_x1[y] ? r.x1 : ap_x1[y]; if (lo >= hi) continue; uint16_t *row = rowp - r.x0; const int y4 = y * 16 + 8, dy = y4 - f->iy4; if (dy <= -f->ext4 || dy >= f->ext4) continue; if (f->top_min4 - y4 >= f->thw || y4 - f->bot_max4 >= f->bhw) continue; /* a lid body covers the row */ const int half = (int)sqrtf((float)(f->ext4 * f->ext4 - dy * dy)) + 16; int i0 = (f->ix4 - half) >> 4, i1 = ((f->ix4 + half) >> 4) + 1; /* lens columns */ if (i0 < lo) i0 = lo; if (i1 > hi) i1 = hi; /* Columns a lid body covers need no lens. */ while (i0 < i1 && (f->top4[i0] - y4 >= f->thw || y4 - f->bot4[i0] >= f->bhw)) i0++; while (i1 > i0 && (f->top4[i1 - 1] - y4 >= f->thw || y4 - f->bot4[i1 - 1] >= f->bhw)) i1--; if (i0 >= i1) continue; const int v0 = vig_x0[y], v1 = vig_x1[y]; for (int x = i0; x < i1;) { /* split at the rim-darkening boundaries */ int end = i1; if (v0 > x && v0 < end) end = v0; if (v1 > x && v1 < end) end = v1; iris_span(f, row, y, x, end, x < v0 || x >= v1); x = end; } /* Highlights and the glint, on their own columns. */ int16_t sp[3][3]; /* start, end, light bit */ int ns = 0; if (spot_span(y4, f->ix4 + f->h1x, f->iy4 + f->h1y, 11 * 16, sp[ns])) sp[ns++][2] = 2; if (spot_span(y4, f->ix4 + f->h2x, f->iy4 + f->h2y, 5 * 16, sp[ns])) sp[ns++][2] = 4; if (f->gl_on) { /* columns where |u - p| < w, u = (dx, dy) . n */ const int c0 = ((f->gl_p4 - GLINT_W4) * 4096 - dy * GLINT_NY) / GLINT_NX; const int c1 = ((f->gl_p4 + GLINT_W4) * 4096 - dy * GLINT_NY) / GLINT_NX; sp[ns][0] = (int16_t)((f->ix4 + c0 - 8) / 16 - 1); sp[ns][1] = (int16_t)((f->ix4 + c1 - 8) / 16 + 2); sp[ns++][2] = 1; } /* Overlapping spans merge: the blend weighs each of their lights per * pixel (zero outside it), so each pixel is lit once. */ for (int k = 1; k < ns; k++) /* sort by start */ for (int j = k; j > 0 && sp[j][0] < sp[j - 1][0]; j--) for (int c = 0; c < 3; c++) { const int16_t t = sp[j][c]; sp[j][c] = sp[j - 1][c]; sp[j - 1][c] = t; } for (int k = 0; k < ns;) { int a = sp[k][0], b = sp[k][1]; unsigned lights = (unsigned)sp[k][2]; for (k++; k < ns && sp[k][0] <= b; k++) { if (sp[k][1] > b) b = sp[k][1]; lights |= (unsigned)sp[k][2]; } if (a < i0) a = i0; if (b > i1) b = i1; if (a < b) spot_blend(f, row, y, a, b, lights); } } lid_columns(f, r, out); } /* ---- dirty rectangles ----------------------------------------------------- */ static int rect_area(eye_rect_t r) { return (r.x1 - r.x0) * (r.y1 - r.y0); } static eye_rect_t rect_union(eye_rect_t a, eye_rect_t b) { eye_rect_t u = a; if (b.x0 < u.x0) u.x0 = b.x0; if (b.y0 < u.y0) u.y0 = b.y0; if (b.x1 > u.x1) u.x1 = b.x1; if (b.y1 > u.y1) u.y1 = b.y1; return u; } static eye_rect_t rect_clip(int x0, int y0, int x1, int y1) { eye_rect_t r = {(int16_t)(x0 < 0 ? 0 : x0), (int16_t)(y0 < 0 ? 0 : y0), (int16_t)(x1 > EYE_W ? EYE_W : x1), (int16_t)(y1 > EYE_W ? EYE_W : y1)}; if (r.x1 < r.x0) r.x1 = r.x0; if (r.y1 < r.y0) r.y1 = r.y0; return r; } /* Everything but the pupil that decides the lens's pixels is unchanged. */ static int lens_same(const eye_frame_t *a, const eye_frame_t *b) { return a->ix4 == b->ix4 && a->iy4 == b->iy4 && a->m00 == b->m00 && a->m01 == b->m01 && a->m10 == b->m10 && a->m11 == b->m11 && a->ext4 == b->ext4 && a->spin16 == b->spin16 && a->gl_on == b->gl_on && (!a->gl_on || a->gl_p4 == b->gl_p4) && a->h1x == b->h1x && a->h1y == b->h1y && a->h2x == b->h2x && a->h2y == b->h2y; } /* The screen box around the pupil and its rim (the lens's scale bounds it). */ static eye_rect_t pupil_box(const eye_frame_t *f) { const int r4 = (int)(sqrtf((float)f->rp_hi) * f->ext4 / (IRIS_RH * 16)) + 32; return rect_clip((f->ix4 - r4) / 16 - 1, (f->iy4 - r4) / 16 - 1, (f->ix4 + r4) / 16 + 2, (f->iy4 + r4) / 16 + 2); } static eye_rect_t iris_box(const eye_frame_t *f) { return rect_clip((f->ix4 - f->ext4) / 16 - 1, (f->iy4 - f->ext4) / 16 - 1, (f->ix4 + f->ext4) / 16 + 2, (f->iy4 + f->ext4) / 16 + 2); } /* Rows [y0, y1) of columns [x0, x1), narrowed to the rows the round aperture * shows in those columns. */ static eye_rect_t aperture_slice(int x0, int x1, int y0, int y1) { const int near = x0 >= 120 ? x0 : x1 <= 120 ? x1 - 1 : 120; /* column nearest the centre */ const int X = 2 * near + 1 - EYE_W; const int half = (int)sqrtf((float)(57600 - X * X)) / 2 + 2; if (y0 < 120 - half) y0 = 120 - half; if (y1 > 120 + half) y1 = 120 + half; return rect_clip(x0, y0, x1, y1); } /* Where one lid's pixels changed, per slice of columns: between the old and new * edge, widened by the lid's shading depth on one side and by its shadow and * edge ramp on the other (the wider of the two frames' ramps). */ static int lid_bands(const int16_t *a, const int16_t *b, int hw, int shadow_below, int all, eye_rect_t *out) { const int ramp = hw > LID_SHADOW * 16 ? hw : LID_SHADOW * 16; const int eye_side = ramp + 16, lid_side = (hw > LID_DEPTH * 16 ? hw : LID_DEPTH * 16) + 16; int n = 0; for (int x0 = 0; x0 < EYE_W; x0 += LID_SLICE) { int lo = 1 << 30, hi = -(1 << 30); for (int x = x0; x < x0 + LID_SLICE; x++) { if (!all && a[x] == b[x]) continue; if (a[x] < lo) lo = a[x]; if (b[x] < lo) lo = b[x]; if (a[x] > hi) hi = a[x]; if (b[x] > hi) hi = b[x]; } if (hi < lo) continue; lo -= shadow_below ? lid_side : eye_side; hi += shadow_below ? eye_side : lid_side; const eye_rect_t r = aperture_slice(x0, x0 + LID_SLICE, (lo >> 4) - 1, (hi >> 4) + 2); if (rect_area(r)) out[n++] = r; } return n; } static int eye_split(const eye_rect_t *in, int n, eye_rect_t *out) { int m = 0; for (int i = 0; i < n; i++) { const eye_rect_t r = in[i]; const int w = r.x1 - r.x0; if (w <= 0 || r.y1 <= r.y0) continue; const int rows = MAX_RECT_PX / w; for (int y = r.y0; y < r.y1; y += rows) { if (m == MAX_RECTS) return -1; out[m++] = (eye_rect_t){r.x0, (int16_t)y, r.x1, (int16_t)(y + rows < r.y1 ? y + rows : r.y1)}; } } return m; } static int eye_full(eye_rect_t *out) { const eye_rect_t all = {0, 0, EYE_W, EYE_W}; return eye_split(&all, 1, out); } /* Every changed pixel lies in the returned rectangles. */ static int eye_dirty(const eye_frame_t *prev, const eye_frame_t *next, eye_rect_t *out) { eye_rect_t r[2 + 2 * (EYE_W / LID_SLICE)]; int n = 0; if (!lens_same(prev, next)) { const eye_rect_t a = iris_box(prev), b = iris_box(next); const eye_rect_t u = rect_union(a, b); if (rect_area(u) <= rect_area(a) + rect_area(b)) r[n++] = u; else { r[n++] = a; r[n++] = b; } } else if (prev->rp_hi != next->rp_hi || prev->rp_in != next->rp_in || prev->rp2 != next->rp2) { r[n++] = rect_union(pupil_box(prev), pupil_box(next)); /* a still lens: only the pupil breathes */ } /* A changed edge ramp alone (lids at rest, motion blur fading) changes the * same rows: compare the ramp widths too. */ const int top_hw = prev->thw > next->thw ? prev->thw : next->thw; const int bot_hw = prev->bhw > next->bhw ? prev->bhw : next->bhw; n += lid_bands(prev->top4, next->top4, top_hw, 1, prev->thw != next->thw, &r[n]); n += lid_bands(prev->bot4, next->bot4, bot_hw, 0, prev->bhw != next->bhw, &r[n]); for (int merged = 1; merged;) { /* merge pairs whose union costs no more pixels */ merged = 0; for (int i = 0; i < n && !merged; i++) for (int j = i + 1; j < n && !merged; j++) { const eye_rect_t m = rect_union(r[i], r[j]); const int sum = rect_area(r[i]) + rect_area(r[j]), um = rect_area(m); if (um <= sum || (um <= MAX_RECT_PX && um <= sum + RECT_COST_PX)) { r[i] = m; r[j] = r[--n]; merged = 1; } } } const int m = eye_split(r, n, out); return m < 0 ? eye_full(out) : m; /* never drop a changed pixel */ } /* ---- behaviour ------------------------------------------------------------ */ typedef struct { uint32_t rng; uint32_t frame; float px, py, vx, vy, tx, ty, fx, fy, drx, dry, omega, zeta; int mode, mode_left, micro_left; /* mode 0 fixate, 1 orbit */ float orbit_a, orbit_w, orbit_r; int blink_t, blink_next, blink_again, repeat_in; int expr, expr_left; float tilt_sign; float top_y, top_k, top_tilt, bot_y, bot_k, pupil_f, scale; float v_top_y, v_top_k, v_tilt, v_bot_y, v_bot_k, v_pupil, v_scale; /* spring velocities */ float last_top, last_bot; /* last frame's lid centres, for motion blur */ float spin, focus, focus_v; /* ring turns; pupil refocus pulse */ int darting, glint_t, glint_next, scan_t, scan_next; } eye_brain_t; static uint32_t eye_rand(eye_brain_t *b) { b->rng ^= b->rng << 13; b->rng ^= b->rng >> 17; b->rng ^= b->rng << 5; return b->rng * 2654435761u; /* scramble: consecutive xorshift outputs correlate */ } static float eye_frand(eye_brain_t *b) { return (eye_rand(b) >> 8) * (1.0f / 16777216.0f); } static float eye_range(eye_brain_t *b, float lo, float hi) { return lo + (hi - lo) * eye_frand(b); } static int eye_frames(eye_brain_t *b, float lo_s, float hi_s) { return (int)(eye_range(b, lo_s, hi_s) * EYE_FPS); } enum { EXPR_NEUTRAL, EXPR_SQUINT, EXPR_CURIOUS, EXPR_SLEEPY, EXPR_HAPPY, EXPR_SURPRISED }; static const float expr_table[6][8] = { /* top_y, top_k, tilt, bot_y, bot_k, pupil, size, spring rad/s */ {-4, 0.0042f, 0, 246, 0.0034f, 1.00f, 1.00f, 9}, /* lids just out of sight */ {60, 0.0030f, 0, 186, 0.0052f, 0.84f, 1.00f, 9}, {10, 0.0040f, 0.20f, 228, 0.0030f, 1.26f, 1.03f, 9}, {80, 0.0016f, 0, 236, 0.0034f, 0.94f, 0.98f, 6.5f}, {-8, 0.0042f, 0, 142, -0.0042f, 1.14f, 1.00f, 10.5f}, /* lower lid arches up: a smile */ {-14, 0.0042f, 0, 252, 0.0034f, 0.60f, 1.10f, 24}, /* wide open, pupil small */ }; /* Critically damped approach (smooth start and stop) that comes to rest * exactly, so settled lids stop costing redraws. */ static void eye_approach(float *v, float *vel, float target, float w, float eps) { const float dt = 1.0f / EYE_FPS; *vel += (w * w * (target - *v) - 2 * w * *vel) * dt; *v += *vel * dt; if (fabsf(target - *v) < eps && fabsf(*vel) < eps * w) { *v = target; *vel = 0; } } static void eye_brain_init(eye_brain_t *b, uint32_t seed) { memset(b, 0, sizeof *b); b->rng = seed ? seed : 1; b->omega = 30; b->zeta = 0.65f; b->mode_left = eye_frames(b, 0.8f, 1.4f); b->micro_left = eye_frames(b, 0.3f, 0.8f); b->blink_t = -1; b->blink_next = eye_frames(b, 1.2f, 2.2f); b->expr_left = eye_frames(b, 6, 9); b->tilt_sign = 1; const float *e = expr_table[EXPR_NEUTRAL]; b->top_y = e[0]; b->top_k = e[1]; b->top_tilt = e[2]; b->bot_y = e[3]; b->bot_k = e[4]; b->pupil_f = e[5]; b->scale = e[6]; b->last_top = e[0]; b->last_bot = e[3]; b->glint_t = -1; b->glint_next = eye_frames(b, 3, 5); b->scan_t = -1; b->scan_next = eye_frames(b, 1.5f, 3); } static void eye_pick_target(eye_brain_t *b) { const float ox = b->fx, oy = b->fy; for (int tries = 0; tries < 3; tries++) { const float kind = eye_frand(b), a = eye_range(b, 0, 6.2831853f); float r; if (kind < 0.22f) r = eye_range(b, 0, 8); else if (kind < 0.55f) r = GAZE_MAX * eye_range(b, 0.78f, 1.0f); else r = GAZE_MAX * sqrtf(eye_frand(b)); b->fx = r * cosf(a); b->fy = r * sinf(a); if (b->expr == EXPR_CURIOUS) { b->fx = 0.6f * b->fx + 26 * b->tilt_sign; b->fy = 0.6f * b->fy - 10; } if (b->expr == EXPR_HAPPY) { b->fx *= 0.6f; b->fy = 0.25f * b->fy - 8; } /* the smile cuts the iris */ if ((b->fx - ox) * (b->fx - ox) + (b->fy - oy) * (b->fy - oy) > 14 * 14) break; } } static void eye_start_blink(eye_brain_t *b, int may_repeat) { if (b->blink_t >= 0) return; b->blink_t = 0; b->blink_again = may_repeat && eye_frand(b) < 0.16f; /* a double blink, never more */ b->blink_next = eye_frames(b, 3.0f, 7.0f); } #define BLINK_CLOSE 7 /* frames: an eased close, a short hold, a slower open */ #define BLINK_HOLD 2 #define BLINK_OPEN 10 #define BLINK_LEN (BLINK_CLOSE + BLINK_HOLD + BLINK_OPEN) static float eye_blink_closure(int t) { if (t < 0) return 0; if (t < BLINK_CLOSE) { const float u = (t + 1) / (float)BLINK_CLOSE; return u * u * (3 - 2 * u); } if (t < BLINK_CLOSE + BLINK_HOLD) return 1; if (t < BLINK_LEN) { const float u = (t - BLINK_CLOSE - BLINK_HOLD + 1) / (float)BLINK_OPEN; return 1 - u * u * (3 - 2 * u); } return 0; } static void eye_brain_step(eye_brain_t *b, eye_pose_t *pose) { const float dt = 1.0f / EYE_FPS; b->frame++; /* Expressions: occasional squint, curiosity or sleepiness, eased in and out. */ if (--b->expr_left <= 0) { if (b->expr != EXPR_NEUTRAL) { if (b->expr == EXPR_SLEEPY) eye_start_blink(b, 1); b->expr = EXPR_NEUTRAL; b->expr_left = eye_frames(b, 7, 13); } else { const float k = eye_frand(b); b->expr = k < 0.24f ? EXPR_CURIOUS : k < 0.52f ? EXPR_HAPPY : k < 0.72f ? EXPR_SURPRISED : k < 0.87f ? EXPR_SQUINT : EXPR_SLEEPY; b->tilt_sign = eye_frand(b) < 0.5f ? -1.f : 1.f; b->expr_left = b->expr == EXPR_SLEEPY ? eye_frames(b, 3, 4.5f) : b->expr == EXPR_SURPRISED ? eye_frames(b, 0.9f, 1.5f) : eye_frames(b, 1.8f, 3.6f); if (b->expr == EXPR_CURIOUS || b->expr == EXPR_HAPPY) b->mode_left = 1; } } const float *e = expr_table[b->expr]; const float w = e[7]; eye_approach(&b->top_y, &b->v_top_y, e[0], w, 0.01f); eye_approach(&b->top_k, &b->v_top_k, e[1], w, 1e-6f); eye_approach(&b->top_tilt, &b->v_tilt, e[2] * b->tilt_sign, w, 1e-4f); eye_approach(&b->bot_y, &b->v_bot_y, e[3], w, 0.01f); eye_approach(&b->bot_k, &b->v_bot_k, e[4], w, 1e-6f); eye_approach(&b->pupil_f, &b->v_pupil, e[5], w * 0.6f, 1e-3f); eye_approach(&b->scale, &b->v_scale, e[6], w, 1e-4f); /* Gaze: saccades on an underdamped spring (small overshoot and settle), * slow glides and orbits on a critically damped one, fixational drift and * micro-saccades in between. */ if (b->mode == 1) { b->orbit_a += b->orbit_w * dt; b->tx = b->orbit_r * cosf(b->orbit_a); b->ty = b->orbit_r * sinf(b->orbit_a); if (--b->mode_left <= 0) { b->mode = 0; b->fx = b->tx * 0.2f; b->fy = b->ty * 0.2f; b->omega = 30; b->zeta = 0.65f; b->mode_left = eye_frames(b, 0.6f, 1.6f); } } else { if (--b->mode_left <= 0) { const float kind = eye_frand(b); if (kind < 0.10f && b->expr == EXPR_NEUTRAL) { b->mode = 1; b->orbit_r = GAZE_MAX * 0.9f; b->orbit_a = atan2f(b->py, b->px); if (b->px * b->px + b->py * b->py < 100) b->orbit_a = eye_range(b, 0, 6.2831853f); b->orbit_w = (eye_frand(b) < 0.5f ? -1 : 1) * eye_range(b, 2.2f, 3.2f); b->omega = 14; b->zeta = 1.0f; b->mode_left = eye_frames(b, 1.6f, 2.8f); } else { eye_pick_target(b); const float jump = sqrtf((b->fx - b->px) * (b->fx - b->px) + (b->fy - b->py) * (b->fy - b->py)); if (kind < 0.24f) { b->omega = 5.5f; b->zeta = 1.0f; } /* slow glide */ else { b->omega = 34; b->zeta = 0.62f; } /* saccade */ if (jump > 0.7f * GAZE_MAX && b->blink_next > 60 && eye_frand(b) < 0.2f) eye_start_blink(b, 1); b->mode_left = eye_frand(b) < 0.15f ? eye_frames(b, 0.18f, 0.36f) : (int)(EYE_FPS * (0.4f + 2.2f * eye_frand(b) * eye_frand(b))); b->drx = b->dry = 0; } } if (--b->micro_left <= 0) { /* micro-saccade: a small fast hop of the fixation point */ const float a = eye_range(b, 0, 6.2831853f), r = eye_range(b, 1.2f, 3.4f); if (b->omega >= 20) { /* never cut a slow glide short */ b->fx += r * cosf(a); b->fy += r * sinf(a); const float f = sqrtf(b->fx * b->fx + b->fy * b->fy); if (f > GAZE_MAX) { b->fx *= GAZE_MAX / f; b->fy *= GAZE_MAX / f; } } b->micro_left = eye_frames(b, 0.8f, 2.0f); } b->tx = b->fx; b->ty = b->fy; /* holds still between micro-saccades */ } const float ax = b->omega * b->omega * (b->tx - b->px) - 2 * b->zeta * b->omega * b->vx; const float ay = b->omega * b->omega * (b->ty - b->py) - 2 * b->zeta * b->omega * b->vy; b->vx += ax * dt; b->vy += ay * dt; b->px += b->vx * dt; b->py += b->vy * dt; const float d = sqrtf(b->px * b->px + b->py * b->py); if (d > GAZE_CLAMP) { b->px *= GAZE_CLAMP / d; b->py *= GAZE_CLAMP / d; } if (fabsf(b->tx - b->px) < 0.05f && fabsf(b->ty - b->py) < 0.05f && fabsf(b->vx) < 1.5f && fabsf(b->vy) < 1.5f) { b->px = b->tx; b->py = b->ty; b->vx = b->vy = 0; /* settled: exactly still, nothing to redraw */ } /* After a fast dart lands, the pupil refocuses: a quick dip and recovery. */ const float speed = sqrtf(b->vx * b->vx + b->vy * b->vy); if (speed > 450) b->darting = 1; else if (b->darting && speed < 40) { b->darting = 0; b->focus_v -= 2.4f; } b->focus_v += (-196.f * b->focus - 19.6f * b->focus_v) * dt; b->focus += b->focus_v * dt; /* The segment ring whirs with every dart and now and then scans a quarter * turn; between those it rests, so a still lens costs no redraw. */ b->spin += speed * 0.0004f * dt; if (b->scan_t >= 0) { const float u0 = b->scan_t / 36.f, u1 = (b->scan_t + 1) / 36.f; b->spin += 0.25f * (u1 * u1 * (3 - 2 * u1) - u0 * u0 * (3 - 2 * u0)); if (++b->scan_t >= 36) b->scan_t = -1; } else if (--b->scan_next <= 0) { b->scan_t = 0; b->scan_next = eye_frames(b, 2.5f, 5.5f); } b->spin -= floorf(b->spin); /* Now and then a glint of light sweeps across the lens. */ if (b->glint_t >= 0) { if (++b->glint_t >= 26) b->glint_t = -1; } else if (--b->glint_next <= 0) { b->glint_t = 0; b->glint_next = eye_frames(b, 6, 11); } /* Blinks, sometimes doubled. */ if (b->blink_t < 0 && b->repeat_in > 0) { if (--b->repeat_in == 0) eye_start_blink(b, 0); /* the second of a double blink */ } else if (b->blink_t < 0 && --b->blink_next <= 0) { eye_start_blink(b, 1); } float closure = 0; if (b->blink_t >= 0) { closure = eye_blink_closure(b->blink_t); if (++b->blink_t >= BLINK_LEN) { b->blink_t = -1; if (b->blink_again) { b->blink_again = 0; b->repeat_in = 3; } } } const float breath = 1 + 0.055f * sinf(b->frame * (6.2831853f * 0.17f / EYE_FPS)) + 0.03f * sinf(b->frame * (6.2831853f * 0.41f / EYE_FPS)); pose->gx = b->px; pose->gy = b->py; pose->vx = b->vx; pose->vy = b->vy; pose->scale = b->scale; pose->spin = b->spin; pose->glint = b->glint_t >= 0 && b->blink_t < 0 ? b->glint_t / 25.f : -1; pose->pupil = 0.37f * IRIS_R * b->pupil_f * breath * (1 + b->focus); pose->top_y = b->top_y + (CLOSE_Y - b->top_y) * closure; pose->top_k = b->top_k + (0.0012f - b->top_k) * closure; pose->top_tilt = b->top_tilt * (1 - closure); pose->bot_y = b->bot_y + (CLOSE_Y - 2 - b->bot_y) * closure; pose->bot_k = b->bot_k + (-0.0012f - b->bot_k) * closure; pose->bot_tilt = 0; /* Fast lid travel (a blink) gets a motion-blurred edge, like a camera's. */ const float top_move = fabsf(pose->top_y - b->last_top), bot_move = fabsf(pose->bot_y - b->last_bot); b->last_top = pose->top_y; b->last_bot = pose->bot_y; pose->top_blur = top_move > 3 ? 0.2f * top_move : 0.5f; pose->bot_blur = bot_move > 3 ? 0.2f * bot_move : 0.5f; }
components/gc9a01/CMakeLists.txt
# First-party build integration. The C/header are unchanged Espressif 2.0.4. # Avoid the upstream unpinned cmake_utilities package; only its version # definitions are needed by the driver's informational log line. idf_component_register(SRCS "esp_lcd_gc9a01.c" INCLUDE_DIRS "include" REQUIRES esp_lcd PRIV_REQUIRES esp_driver_gpio) target_compile_definitions(${COMPONENT_LIB} PRIVATE ESP_LCD_GC9A01_VER_MAJOR=2 ESP_LCD_GC9A01_VER_MINOR=0 ESP_LCD_GC9A01_VER_PATCH=4)
components/gc9a01/UPSTREAM.md
# Espressif GC9A01 2.0.4 The C source, public header and `license.txt` are byte-identical to the [Espressif component archive](https://components-file.espressif.com/components/espressif/esp_lcd_gc9a01/2.0.4/espressif__esp_lcd_gc9a01-v2.0.4.zip), SHA-256 `2220c907c900d99055c9b0e218bd3886f8e64bea65c90ee4b3a4b1d75f357c40`. Source commit: `618b05297dae1aa3fab986d5ca7b89b328e1919a`, `components/lcd/esp_lcd_gc9a01` in Espressif's esp-bsp repository. Copyright Espressif Systems (Shanghai) CO LTD; Apache-2.0. The local CMake file is first-party integration, supplying the three version macros directly so the upstream unpinned `cmake_utilities` dependency is absent. The driver source and its init sequence are unchanged.
components/gc9a01/license.txt
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
components/gc9a01/esp_lcd_gc9a01.c
/* * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include <stdlib.h> #include <sys/cdefs.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_lcd_panel_interface.h" #include "esp_lcd_panel_io.h" #include "esp_lcd_panel_vendor.h" #include "esp_lcd_panel_ops.h" #include "esp_lcd_panel_commands.h" #include "driver/gpio.h" #include "esp_log.h" #include "esp_check.h" #include "esp_lcd_gc9a01.h" static const char *TAG = "gc9a01"; static esp_err_t panel_gc9a01_del(esp_lcd_panel_t *panel); static esp_err_t panel_gc9a01_reset(esp_lcd_panel_t *panel); static esp_err_t panel_gc9a01_init(esp_lcd_panel_t *panel); static esp_err_t panel_gc9a01_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data); static esp_err_t panel_gc9a01_invert_color(esp_lcd_panel_t *panel, bool invert_color_data); static esp_err_t panel_gc9a01_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y); static esp_err_t panel_gc9a01_swap_xy(esp_lcd_panel_t *panel, bool swap_axes); static esp_err_t panel_gc9a01_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap); static esp_err_t panel_gc9a01_disp_on_off(esp_lcd_panel_t *panel, bool off); typedef struct { esp_lcd_panel_t base; esp_lcd_panel_io_handle_t io; int reset_gpio_num; bool reset_level; int x_gap; int y_gap; uint8_t fb_bits_per_pixel; uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register uint8_t colmod_val; // save current value of LCD_CMD_COLMOD register const gc9a01_lcd_init_cmd_t *init_cmds; uint16_t init_cmds_size; } gc9a01_panel_t; esp_err_t esp_lcd_new_panel_gc9a01(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel) { esp_err_t ret = ESP_OK; gc9a01_panel_t *gc9a01 = NULL; gpio_config_t io_conf = { 0 }; ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); gc9a01 = (gc9a01_panel_t *)calloc(1, sizeof(gc9a01_panel_t)); ESP_GOTO_ON_FALSE(gc9a01, ESP_ERR_NO_MEM, err, TAG, "no mem for gc9a01 panel"); if (panel_dev_config->reset_gpio_num >= 0) { io_conf.mode = GPIO_MODE_OUTPUT; io_conf.pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num; ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed"); } #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) switch (panel_dev_config->color_space) { case ESP_LCD_COLOR_SPACE_RGB: gc9a01->madctl_val = 0; break; case ESP_LCD_COLOR_SPACE_BGR: gc9a01->madctl_val |= LCD_CMD_BGR_BIT; break; default: ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space"); break; } #elif ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) switch (panel_dev_config->rgb_endian) { case LCD_RGB_ENDIAN_RGB: gc9a01->madctl_val = 0; break; case LCD_RGB_ENDIAN_BGR: gc9a01->madctl_val |= LCD_CMD_BGR_BIT; break; default: ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported rgb endian"); break; } #else switch (panel_dev_config->rgb_ele_order) { case LCD_RGB_ELEMENT_ORDER_RGB: gc9a01->madctl_val = 0; break; case LCD_RGB_ELEMENT_ORDER_BGR: gc9a01->madctl_val |= LCD_CMD_BGR_BIT; break; default: ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported rgb element order"); break; } #endif switch (panel_dev_config->bits_per_pixel) { case 16: // RGB565 gc9a01->colmod_val = 0x55; gc9a01->fb_bits_per_pixel = 16; break; case 18: // RGB666 gc9a01->colmod_val = 0x66; // each color component (R/G/B) should occupy the 6 high bits of a byte, which means 3 full bytes are required for a pixel gc9a01->fb_bits_per_pixel = 24; break; default: ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width"); break; } gc9a01->io = io; gc9a01->reset_gpio_num = panel_dev_config->reset_gpio_num; gc9a01->reset_level = panel_dev_config->flags.reset_active_high; if (panel_dev_config->vendor_config) { gc9a01->init_cmds = ((gc9a01_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds; gc9a01->init_cmds_size = ((gc9a01_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds_size; } gc9a01->base.del = panel_gc9a01_del; gc9a01->base.reset = panel_gc9a01_reset; gc9a01->base.init = panel_gc9a01_init; gc9a01->base.draw_bitmap = panel_gc9a01_draw_bitmap; gc9a01->base.invert_color = panel_gc9a01_invert_color; gc9a01->base.set_gap = panel_gc9a01_set_gap; gc9a01->base.mirror = panel_gc9a01_mirror; gc9a01->base.swap_xy = panel_gc9a01_swap_xy; #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) gc9a01->base.disp_off = panel_gc9a01_disp_on_off; #else gc9a01->base.disp_on_off = panel_gc9a01_disp_on_off; #endif *ret_panel = &(gc9a01->base); ESP_LOGD(TAG, "new gc9a01 panel @%p", gc9a01); ESP_LOGI(TAG, "LCD panel create success, version: %d.%d.%d", ESP_LCD_GC9A01_VER_MAJOR, ESP_LCD_GC9A01_VER_MINOR, ESP_LCD_GC9A01_VER_PATCH); return ESP_OK; err: if (gc9a01) { if (panel_dev_config->reset_gpio_num >= 0) { gpio_reset_pin(panel_dev_config->reset_gpio_num); } free(gc9a01); } return ret; } static esp_err_t panel_gc9a01_del(esp_lcd_panel_t *panel) { gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); if (gc9a01->reset_gpio_num >= 0) { gpio_reset_pin(gc9a01->reset_gpio_num); } ESP_LOGD(TAG, "del gc9a01 panel @%p", gc9a01); free(gc9a01); return ESP_OK; } static esp_err_t panel_gc9a01_reset(esp_lcd_panel_t *panel) { gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); esp_lcd_panel_io_handle_t io = gc9a01->io; // perform hardware reset if (gc9a01->reset_gpio_num >= 0) { gpio_set_level(gc9a01->reset_gpio_num, gc9a01->reset_level); vTaskDelay(pdMS_TO_TICKS(10)); gpio_set_level(gc9a01->reset_gpio_num, !gc9a01->reset_level); vTaskDelay(pdMS_TO_TICKS(10)); } else { // perform software reset ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, NULL, 0), TAG, "send command failed"); vTaskDelay(pdMS_TO_TICKS(20)); // spec, wait at least 5ms before sending new command } return ESP_OK; } static const gc9a01_lcd_init_cmd_t vendor_specific_init_default[] = { // {cmd, { data }, data_size, delay_ms} // Enable Inter Register {0xfe, (uint8_t []){0x00}, 0, 0}, {0xef, (uint8_t []){0x00}, 0, 0}, {0xeb, (uint8_t []){0x14}, 1, 0}, {0x84, (uint8_t []){0x60}, 1, 0}, {0x85, (uint8_t []){0xff}, 1, 0}, {0x86, (uint8_t []){0xff}, 1, 0}, {0x87, (uint8_t []){0xff}, 1, 0}, {0x8e, (uint8_t []){0xff}, 1, 0}, {0x8f, (uint8_t []){0xff}, 1, 0}, {0x88, (uint8_t []){0x0a}, 1, 0}, {0x89, (uint8_t []){0x23}, 1, 0}, {0x8a, (uint8_t []){0x00}, 1, 0}, {0x8b, (uint8_t []){0x80}, 1, 0}, {0x8c, (uint8_t []){0x01}, 1, 0}, {0x8d, (uint8_t []){0x03}, 1, 0}, {0x90, (uint8_t []){0x08, 0x08, 0x08, 0x08}, 4, 0}, {0xff, (uint8_t []){0x60, 0x01, 0x04}, 3, 0}, {0xC3, (uint8_t []){0x13}, 1, 0}, {0xC4, (uint8_t []){0x13}, 1, 0}, {0xC9, (uint8_t []){0x30}, 1, 0}, {0xbe, (uint8_t []){0x11}, 1, 0}, {0xe1, (uint8_t []){0x10, 0x0e}, 2, 0}, {0xdf, (uint8_t []){0x21, 0x0c, 0x02}, 3, 0}, // Set gamma {0xF0, (uint8_t []){0x45, 0x09, 0x08, 0x08, 0x26, 0x2a}, 6, 0}, {0xF1, (uint8_t []){0x43, 0x70, 0x72, 0x36, 0x37, 0x6f}, 6, 0}, {0xF2, (uint8_t []){0x45, 0x09, 0x08, 0x08, 0x26, 0x2a}, 6, 0}, {0xF3, (uint8_t []){0x43, 0x70, 0x72, 0x36, 0x37, 0x6f}, 6, 0}, {0xed, (uint8_t []){0x1b, 0x0b}, 2, 0}, {0xae, (uint8_t []){0x77}, 1, 0}, {0xcd, (uint8_t []){0x63}, 1, 0}, {0x70, (uint8_t []){0x07, 0x07, 0x04, 0x0e, 0x0f, 0x09, 0x07, 0x08, 0x03}, 9, 0}, {0xE8, (uint8_t []){0x34}, 1, 0}, // 4 dot inversion {0x60, (uint8_t []){0x38, 0x0b, 0x6D, 0x6D, 0x39, 0xf0, 0x6D, 0x6D}, 8, 0}, {0x61, (uint8_t []){0x38, 0xf4, 0x6D, 0x6D, 0x38, 0xf7, 0x6D, 0x6D}, 8, 0}, {0x62, (uint8_t []){0x38, 0x0D, 0x71, 0xED, 0x70, 0x70, 0x38, 0x0F, 0x71, 0xEF, 0x70, 0x70}, 12, 0}, {0x63, (uint8_t []){0x38, 0x11, 0x71, 0xF1, 0x70, 0x70, 0x38, 0x13, 0x71, 0xF3, 0x70, 0x70}, 12, 0}, {0x64, (uint8_t []){0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07}, 7, 0}, {0x66, (uint8_t []){0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00}, 10, 0}, {0x67, (uint8_t []){0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98}, 10, 0}, {0x74, (uint8_t []){0x10, 0x45, 0x80, 0x00, 0x00, 0x4E, 0x00}, 7, 0}, {0x98, (uint8_t []){0x3e, 0x07}, 2, 0}, {0x99, (uint8_t []){0x3e, 0x07}, 2, 0}, }; static esp_err_t panel_gc9a01_init(esp_lcd_panel_t *panel) { gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); esp_lcd_panel_io_handle_t io = gc9a01->io; // LCD goes into sleep mode and display will be turned off after power on reset, exit sleep mode first ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SLPOUT, NULL, 0), TAG, "send command failed"); vTaskDelay(pdMS_TO_TICKS(100)); ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { gc9a01->madctl_val, }, 1), TAG, "send command failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_COLMOD, (uint8_t[]) { gc9a01->colmod_val, }, 1), TAG, "send command failed"); const gc9a01_lcd_init_cmd_t *init_cmds = NULL; uint16_t init_cmds_size = 0; if (gc9a01->init_cmds) { init_cmds = gc9a01->init_cmds; init_cmds_size = gc9a01->init_cmds_size; } else { init_cmds = vendor_specific_init_default; init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(gc9a01_lcd_init_cmd_t); } bool is_cmd_overwritten = false; for (int i = 0; i < init_cmds_size; i++) { // Check if the command has been used or conflicts with the internal switch (init_cmds[i].cmd) { case LCD_CMD_MADCTL: is_cmd_overwritten = true; gc9a01->madctl_val = ((uint8_t *)init_cmds[i].data)[0]; break; case LCD_CMD_COLMOD: is_cmd_overwritten = true; gc9a01->colmod_val = ((uint8_t *)init_cmds[i].data)[0]; break; default: is_cmd_overwritten = false; break; } if (is_cmd_overwritten) { ESP_LOGW(TAG, "The %02Xh command has been used and will be overwritten by external initialization sequence", init_cmds[i].cmd); } ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), TAG, "send command failed"); vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms)); } ESP_LOGD(TAG, "send init commands success"); return ESP_OK; } static esp_err_t panel_gc9a01_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data) { gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position"); esp_lcd_panel_io_handle_t io = gc9a01->io; x_start += gc9a01->x_gap; x_end += gc9a01->x_gap; y_start += gc9a01->y_gap; y_end += gc9a01->y_gap; // define an area of frame memory where MCU can access ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_CASET, (uint8_t[]) { (x_start >> 8) & 0xFF, x_start & 0xFF, ((x_end - 1) >> 8) & 0xFF, (x_end - 1) & 0xFF, }, 4), TAG, "send command failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_RASET, (uint8_t[]) { (y_start >> 8) & 0xFF, y_start & 0xFF, ((y_end - 1) >> 8) & 0xFF, (y_end - 1) & 0xFF, }, 4), TAG, "send command failed"); // transfer frame buffer size_t len = (x_end - x_start) * (y_end - y_start) * gc9a01->fb_bits_per_pixel / 8; ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_color(io, LCD_CMD_RAMWR, color_data, len), TAG, "send color failed"); return ESP_OK; } static esp_err_t panel_gc9a01_invert_color(esp_lcd_panel_t *panel, bool invert_color_data) { gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); esp_lcd_panel_io_handle_t io = gc9a01->io; int command = 0; if (invert_color_data) { command = LCD_CMD_INVON; } else { command = LCD_CMD_INVOFF; } ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); return ESP_OK; } static esp_err_t panel_gc9a01_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y) { gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); esp_lcd_panel_io_handle_t io = gc9a01->io; if (mirror_x) { gc9a01->madctl_val |= LCD_CMD_MX_BIT; } else { gc9a01->madctl_val &= ~LCD_CMD_MX_BIT; } if (mirror_y) { gc9a01->madctl_val |= LCD_CMD_MY_BIT; } else { gc9a01->madctl_val &= ~LCD_CMD_MY_BIT; } ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { gc9a01->madctl_val }, 1), TAG, "send command failed"); return ESP_OK; } static esp_err_t panel_gc9a01_swap_xy(esp_lcd_panel_t *panel, bool swap_axes) { gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); esp_lcd_panel_io_handle_t io = gc9a01->io; if (swap_axes) { gc9a01->madctl_val |= LCD_CMD_MV_BIT; } else { gc9a01->madctl_val &= ~LCD_CMD_MV_BIT; } ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { gc9a01->madctl_val }, 1), TAG, "send command failed"); return ESP_OK; } static esp_err_t panel_gc9a01_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap) { gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); gc9a01->x_gap = x_gap; gc9a01->y_gap = y_gap; return ESP_OK; } static esp_err_t panel_gc9a01_disp_on_off(esp_lcd_panel_t *panel, bool on_off) { gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); esp_lcd_panel_io_handle_t io = gc9a01->io; int command = 0; #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) on_off = !on_off; #endif if (on_off) { command = LCD_CMD_DISPON; } else { command = LCD_CMD_DISPOFF; } ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); return ESP_OK; }
components/gc9a01/include/esp_lcd_gc9a01.h
/* * SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ /** * @file * @brief ESP LCD: GC9A01 */ #pragma once #include "esp_lcd_panel_vendor.h" #ifdef __cplusplus extern "C" { #endif /** * @brief LCD panel initialization commands. * */ typedef struct { int cmd; /*<! The specific LCD command */ const void *data; /*<! Buffer that holds the command specific data */ size_t data_bytes; /*<! Size of `data` in memory, in bytes */ unsigned int delay_ms; /*<! Delay in milliseconds after this command */ } gc9a01_lcd_init_cmd_t; /** * @brief LCD panel vendor configuration. * * @note This structure needs to be passed to the `vendor_config` field in `esp_lcd_panel_dev_config_t`. * */ typedef struct { const gc9a01_lcd_init_cmd_t *init_cmds; /*!< Pointer to initialization commands array. Set to NULL if using default commands. * The array should be declared as `static const` and positioned outside the function. * Please refer to `vendor_specific_init_default` in source file. */ uint16_t init_cmds_size; /*<! Number of commands in above array */ } gc9a01_vendor_config_t; /** * @brief Create LCD panel for model GC9A01 * * @note Vendor specific initialization can be different between manufacturers, should consult the LCD supplier for initialization sequence code. * * @param[in] io LCD panel IO handle * @param[in] panel_dev_config general panel device configuration * @param[out] ret_panel Returned LCD panel handle * @return * - ESP_ERR_INVALID_ARG if parameter is invalid * - ESP_ERR_NO_MEM if out of memory * - ESP_OK on success */ esp_err_t esp_lcd_new_panel_gc9a01(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel); /** * @brief LCD panel bus configuration structure * * @param[in] sclk SPI clock pin number * @param[in] mosi SPI MOSI pin number * @param[in] max_trans_sz Maximum transfer size in bytes * */ #define GC9A01_PANEL_BUS_SPI_CONFIG(sclk, mosi, max_trans_sz) \ { \ .mosi_io_num = mosi, \ .miso_io_num = -1, \ .sclk_io_num = sclk, \ .quadwp_io_num = -1, \ .quadhd_io_num = -1, \ .data4_io_num = -1, \ .data5_io_num = -1, \ .data6_io_num = -1, \ .data7_io_num = -1, \ .max_transfer_sz = max_trans_sz, \ } /** * @brief LCD panel IO configuration structure * * @param[in] cs SPI chip select pin number * @param[in] dc SPI data/command pin number * @param[in] cb Callback function when SPI transfer is done * @param[in] cb_ctx Callback function context * */ #define GC9A01_PANEL_IO_SPI_CONFIG(cs, dc, callback, callback_ctx) \ { \ .cs_gpio_num = cs, \ .dc_gpio_num = dc, \ .spi_mode = 0, \ .pclk_hz = 80 * 1000 * 1000, \ .trans_queue_depth = 10, \ .on_color_trans_done = callback, \ .user_ctx = callback_ctx, \ .lcd_cmd_bits = 8, \ .lcd_param_bits = 8, \ .flags = {} \ } #ifdef __cplusplus } #endif



