Render X3 reader grayscale correctly (persistence + 4-level overlay)
The reader draws a crisp BW page, then a 4-level grayscale antialiasing pass with inverted plane polarity (white = level 0 = both planes clear) and near-empty planes. The old model replaced the whole frame with those planes, wiping the page to an inverted muddle. Model it as e-ink actually behaves: keep a persistent surface, detect grayscale vs BW by DTM2's set-fraction (background is the white majority), and in grayscale treat level 0 as no-drive (keep pixel) so only the sparse antialiased edges repaint over the retained BW page. BW refreshes still repaint every pixel. Reader text now renders sharp.
This commit is contained in:
@@ -71,19 +71,54 @@ static void xteink_wasm_frame_updated(QemuConsole *console)
|
||||
|
||||
#define BUSY_TIME_NS (5 * SCALE_MS)
|
||||
|
||||
/* 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,
|
||||
};
|
||||
|
||||
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
|
||||
* existing pixel and only repaint driven (level>0) ones. BW refreshes drive
|
||||
* every pixel, so they repaint in full. The surface is never cleared. */
|
||||
for (unsigned y = 0; y < XTEINK_X3_HEIGHT; y++) {
|
||||
const uint8_t *row = &s->new_plane[(XTEINK_X3_HEIGHT - 1 - y) *
|
||||
XTEINK_X3_WIDTH_BYTES];
|
||||
unsigned row_off = (XTEINK_X3_HEIGHT - 1 - y) * XTEINK_X3_WIDTH_BYTES;
|
||||
const uint8_t *msb = &s->new_plane[row_off];
|
||||
const uint8_t *lsb = &s->old_plane[row_off];
|
||||
for (unsigned x = 0; x < XTEINK_X3_WIDTH; x++) {
|
||||
uint8_t bit = 0x80 >> (x % 8);
|
||||
unsigned screen_x = XTEINK_X3_HEIGHT - 1 - y;
|
||||
unsigned screen_y = x;
|
||||
if (grayscale) {
|
||||
unsigned level = ((msb[x / 8] & bit) ? 2 : 0) |
|
||||
((lsb[x / 8] & bit) ? 1 : 0);
|
||||
if (level == 0) {
|
||||
continue;
|
||||
}
|
||||
pixels[screen_y * XTEINK_X3_HEIGHT + screen_x] =
|
||||
(row[x / 8] & (0x80 >> (x % 8))) ? 0x00ffffff : 0;
|
||||
xteink_x3_gray[level];
|
||||
} else {
|
||||
pixels[screen_y * XTEINK_X3_HEIGHT + screen_x] =
|
||||
(msb[x / 8] & bit) ? 0x00ffffff : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user