diff --git a/hw/display/xteink_x3_eink.c b/hw/display/xteink_x3_eink.c index fed136f56f..03cb622e91 100644 --- a/hw/display/xteink_x3_eink.c +++ b/hw/display/xteink_x3_eink.c @@ -6,6 +6,8 @@ #include "qemu/osdep.h" #include "qemu/module.h" +#include "qemu/error-report.h" +#include "qemu/cutils.h" #include "hw/irq.h" #include "hw/display/xteink_x3_eink.h" @@ -68,9 +70,118 @@ static void xteink_wasm_frame_updated(QemuConsole *console) #define CMD_DTM1 0x10 #define CMD_REFRESH 0x12 #define CMD_DTM2 0x13 +#define CMD_LUT_FIRST 0x20 +#define CMD_LUT_LAST 0x24 #define BUSY_TIME_NS (5 * SCALE_MS) +#define XTEINK_X3_LUT_PREFIX_SIZE 13 + +typedef struct XteinkX3KnownLut { + bool grayscale; + uint8_t prefix[XTEINK_X3_LUT_COUNT][XTEINK_X3_LUT_PREFIX_SIZE]; +} XteinkX3KnownLut; + +/* These known banks have zero-filled bytes 13..41. Comparing both the listed + * prefixes and those tails identifies the complete 210-byte bank exactly. */ +static const XteinkX3KnownLut xteink_x3_known_luts[] = { + { false, { + { 0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + { 0x20, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + { 0xaa, 0x06, 0x01, 0x06, 0x06, 0x01, 0xa0, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + { 0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + { 0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x04, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + } }, + { false, { + { 0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + { 0xaa, 0x06, 0x01, 0x06, 0x06, 0x01, 0xa0, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + { 0xaa, 0x06, 0x01, 0x06, 0x06, 0x01, 0xa0, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + { 0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + { 0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + } }, + { false, { + { 0x00, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 }, + { 0x20, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 }, + { 0xaa, 0x04, 0x02, 0x04, 0x04, 0x01, 0x80, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 }, + { 0x55, 0x04, 0x02, 0x04, 0x04, 0x01, 0x40, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 }, + { 0x10, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 }, + } }, + { false, { + { 0x00, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 }, + { 0x4a, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 }, + { 0x0a, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 }, + { 0x04, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x40, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 }, + { 0x84, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x40, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 }, + } }, + { true, { + { 0x00, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x20, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x80, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x54, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + } }, + { false, { + { 0x00, 0x06, 0x03, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + { 0x20, 0x06, 0x03, 0x06, 0x06, 0x01, 0x20, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + { 0xaa, 0x06, 0x03, 0x06, 0x06, 0x01, 0xa8, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + { 0x55, 0x06, 0x03, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + { 0x10, 0x06, 0x03, 0x06, 0x06, 0x01, 0x14, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 }, + } }, + { false, { + { 0x01, 0x07, 0x01, 0x06, 0x06, 0x01, 0x01, 0x01, 0x06, 0x01, 0x00, 0x00, 0x01 }, + { 0x01, 0x07, 0x81, 0x06, 0x06, 0x01, 0x01, 0x01, 0x06, 0x01, 0x00, 0x00, 0x01 }, + { 0x01, 0x87, 0x81, 0x86, 0x86, 0x01, 0x01, 0x01, 0x86, 0x01, 0x00, 0x00, 0x01 }, + { 0x01, 0x47, 0x41, 0x46, 0x46, 0x01, 0x01, 0x01, 0x46, 0x01, 0x00, 0x00, 0x01 }, + { 0x01, 0x07, 0x01, 0x06, 0x06, 0x01, 0x01, 0x01, 0x06, 0x01, 0x00, 0x00, 0x01 }, + } }, +}; + +static bool xteink_x3_eink_lut_matches(const XteinkX3EinkState *s, + const XteinkX3KnownLut *known) +{ + for (unsigned i = 0; i < XTEINK_X3_LUT_COUNT; i++) { + if (memcmp(s->lut_bank[i], known->prefix[i], + XTEINK_X3_LUT_PREFIX_SIZE)) { + return false; + } + for (unsigned j = XTEINK_X3_LUT_PREFIX_SIZE; + j < XTEINK_X3_LUT_SIZE; j++) { + if (s->lut_bank[i][j]) { + return false; + } + } + } + return true; +} + +static bool xteink_x3_eink_classify_lut(XteinkX3EinkState *s) +{ + for (unsigned i = 0; i < XTEINK_X3_LUT_COUNT; i++) { + if (s->lut_pos[i] != XTEINK_X3_LUT_SIZE) { + warn_report("xteink-x3-eink: incomplete LUT bank"); + return false; + } + } + + for (unsigned i = 0; i < ARRAY_SIZE(xteink_x3_known_luts); i++) { + if (xteink_x3_eink_lut_matches(s, &xteink_x3_known_luts[i])) { + s->grayscale_lut = xteink_x3_known_luts[i].grayscale; + s->lut_known = true; + s->unknown_lut_reported = false; + return true; + } + } + + if (!s->unknown_lut_reported) { + warn_report("xteink-x3-eink: unknown LUT bank; refresh skipped"); + qemu_hexdump(stderr, "xteink-x3-eink: LUT ", s->lut_bank, + sizeof(s->lut_bank)); + s->unknown_lut_reported = true; + } + s->lut_known = false; + return false; +} + /* 4-level grayscale drive shades for levels 1..3; level 0 = no drive (keep). */ static const uint32_t xteink_x3_gray[4] = { 0x00ffffff, 0x00aaaaaa, 0x00555555, 0x00000000, @@ -81,19 +192,6 @@ static void xteink_x3_eink_render(XteinkX3EinkState *s) DisplaySurface *surface = qemu_console_surface(s->console); uint32_t *pixels = (uint32_t *)surface_data(surface); - /* Mode Detection - BW and grayscale LUTs use inverted plane polarity and - * differ only in LUT waveform bytes we don't model. BW pages drive white as - * DTM2 bit=1 (mostly-set plane); the reader's 4-level grayscale drives - * white as level 0 = (DTM2,DTM1)=(0,0) (mostly-clear plane). The background - * is the pixel majority and is white, so DTM2's set-fraction separates them. - * Ceiling: a genuinely mostly-black BW frame would flip; fine for text. - * Upgrade path: decode the loaded LUT directly. */ - unsigned dtm2_set = 0; - for (unsigned i = 0; i < XTEINK_X3_PLANE_SIZE; i++) { - dtm2_set += __builtin_popcount(s->new_plane[i]); - } - bool grayscale = dtm2_set * 2 < XTEINK_X3_PLANE_SIZE * 8; - /* Persistence - E-ink retains undriven pixels. The reader's grayscale pass * is a sparse antialiasing overlay (planes near-empty) on top of the crisp * BW page from the prior full refresh; level 0 = no drive, so keep the @@ -107,7 +205,7 @@ static void xteink_x3_eink_render(XteinkX3EinkState *s) uint8_t bit = 0x80 >> (x % 8); unsigned screen_x = XTEINK_X3_HEIGHT - 1 - y; unsigned screen_y = x; - if (grayscale) { + if (s->grayscale_lut) { unsigned level = ((msb[x / 8] & bit) ? 2 : 0) | ((lsb[x / 8] & bit) ? 1 : 0); if (level == 0) { @@ -147,6 +245,11 @@ static void xteink_x3_eink_reset(DeviceState *dev) s->plane_pos = 0; memset(s->old_plane, 0xff, sizeof(s->old_plane)); memset(s->new_plane, 0xff, sizeof(s->new_plane)); + memset(s->lut_pos, 0, sizeof(s->lut_pos)); + s->lut_dirty = false; + s->lut_known = true; + s->grayscale_lut = false; + s->unknown_lut_reported = false; qemu_set_irq(s->busy, 1); xteink_x3_eink_render(s); } @@ -174,12 +277,26 @@ static uint32_t xteink_x3_eink_transfer(SSIPeripheral *peripheral, s->command = byte; if (byte == CMD_DTM1 || byte == CMD_DTM2) { s->plane_pos = 0; + } else if (byte >= CMD_LUT_FIRST && byte <= CMD_LUT_LAST) { + s->lut_pos[byte - CMD_LUT_FIRST] = 0; + s->lut_dirty = true; } else if (byte == CMD_REFRESH) { - xteink_x3_eink_render(s); + if (s->lut_dirty) { + s->lut_known = xteink_x3_eink_classify_lut(s); + s->lut_dirty = false; + } + if (s->lut_known) { + xteink_x3_eink_render(s); + } xteink_x3_eink_pulse_busy(s); } else if (byte == CMD_POWER_ON || byte == CMD_POWER_OFF) { xteink_x3_eink_pulse_busy(s); } + } else if (s->command >= CMD_LUT_FIRST && s->command <= CMD_LUT_LAST) { + unsigned lut = s->command - CMD_LUT_FIRST; + if (s->lut_pos[lut] < XTEINK_X3_LUT_SIZE) { + s->lut_bank[lut][s->lut_pos[lut]++] = byte; + } } else if (s->plane_pos < XTEINK_X3_PLANE_SIZE) { if (s->command == CMD_DTM1) { s->old_plane[s->plane_pos++] = byte; diff --git a/include/hw/display/xteink_x3_eink.h b/include/hw/display/xteink_x3_eink.h index 4adc5fca5a..ca18e59105 100644 --- a/include/hw/display/xteink_x3_eink.h +++ b/include/hw/display/xteink_x3_eink.h @@ -16,6 +16,8 @@ OBJECT_DECLARE_SIMPLE_TYPE(XteinkX3EinkState, XTEINK_X3_EINK) #define XTEINK_X3_HEIGHT 528 #define XTEINK_X3_WIDTH_BYTES (XTEINK_X3_WIDTH / 8) #define XTEINK_X3_PLANE_SIZE (XTEINK_X3_WIDTH_BYTES * XTEINK_X3_HEIGHT) +#define XTEINK_X3_LUT_COUNT 5 +#define XTEINK_X3_LUT_SIZE 42 struct XteinkX3EinkState { SSIPeripheral parent_obj; @@ -27,4 +29,10 @@ struct XteinkX3EinkState { uint32_t plane_pos; uint8_t old_plane[XTEINK_X3_PLANE_SIZE]; uint8_t new_plane[XTEINK_X3_PLANE_SIZE]; + uint8_t lut_bank[XTEINK_X3_LUT_COUNT][XTEINK_X3_LUT_SIZE]; + uint8_t lut_pos[XTEINK_X3_LUT_COUNT]; + bool lut_dirty; + bool lut_known; + bool grayscale_lut; + bool unknown_lut_reported; };