fix(display): classify X3 grayscale from loaded LUTs

This commit is contained in:
2026-07-21 11:42:04 -04:00
parent e6ed78243d
commit cbeba3fbe9
2 changed files with 140 additions and 15 deletions
+132 -15
View File
@@ -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;
+8
View File
@@ -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;
};