fix(display): classify X3 grayscale from loaded LUTs
This commit is contained in:
+132
-15
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user