Device
Xteink X3
Affected Version
1.6.0, confirmed by the device's /api/status. The same rendering omission is also present on develop at 2bad1d09fd7db7dc6f21738cdb9986c51591f73d.
Describe the Bug
Opening a grayscale BMP through Browse Files leaves its gray pixels black. Displaying the same file as the custom sleep screen initially shows the same dark image, then approximately 1–2 seconds later a further refresh makes the gray tones appear. The ordinary image viewer never performs that second stage.
This is particularly obvious in a checkerboard comparison containing alternating regions of a pre-dithered black/white photo and a grayscale version: the black/white regions display normally, while grayscale regions become nearly solid black in the viewer. Both regions render as intended once the sleep-screen refresh finishes.
The observed file is an uncompressed 528×792, 4-bit indexed BMP using only native grayscale palette entries. No scaling or high-color quantization is needed. Sleep Screen is Custom and Sleep Screen Cover Filter is None.
Steps to Reproduce
- Copy a native-palette grayscale BMP to the SD card. A minimal four-band test image can be generated with the dependency-free Python script below.
- Open it in Browse Files and wait for rendering to finish. The gray bands remain black, rather than receiving their grayscale update.
- Set the same BMP as the custom sleep screen, with the cover filter set to None.
- Put the device to sleep and wait for the complete refresh. The gray tones appear after the initial black base image.
Expected Behavior
The image viewer should complete grayscale rendering and show the same tones as the final sleep-screen image. Black/white images should retain their existing rendering behavior.
Source Analysis
Links below are pinned to the 1.6.0 release commit, 54337e6d73fc628f4ba523ddc89a743ca8c6e4c5.
BmpViewerActivity::onEnter()callsdrawBitmap(), draws button hints, and unconditionally finishes withdisplayBuffer(FAST_REFRESH). There is no grayscale branch or subsequent gray-plane rendering.GfxRenderer::drawBitmap()deliberately paints all decoded values below 3 black in BW mode. That includes both intermediate gray levels. This is the base image that remains visible in the viewer.SleepActivity::renderBitmapSleepScreen()instead callsdisplayGrayscaleBase(HALF_REFRESH), rewinds and renders the LSB and MSB planes, copies both gray buffers, callsdisplayGrayBuffer(), and restores BW mode. This matches the extra refresh observed on the hardware.
The viewer appears to need the equivalent grayscale-aware sequence, with its button-hint regions preserved and render mode restored afterward. The sleep implementation specifically documents why its grayscale base uses HALF refresh, so merely changing FAST to FULL would not add the missing gray planes.
Related: #2879 discusses a sleep-screen grayscale failure in an older release. In this report the sleep-screen path succeeds; the viewer stops before the grayscale passes.
Minimal Test Image
This creates four vertical solid bands with palette values 0, 85, 170, and 255. It isolates the missing render passes from photographic content and dithering. The file generator and resulting BMP were checked locally; hardware observations above are from the photographic comparison.
from pathlib import Path
import struct
width, height = 528, 792
levels = (0, 85, 170, 255)
palette = b''.join(bytes((g, g, g, 0)) for g in levels)
row = b''.join(bytes(((i << 4) | i,)) * (width // 8) for i in range(4))
stride = ((width * 4 + 31) // 32) * 4
pixels = (row + bytes(stride - len(row))) * height
offset = 14 + 40 + len(palette)
header = struct.pack('<2sIHHI', b'BM', offset + len(pixels), 0, 0, offset)
dib = struct.pack('<IiiHHIIiiII', 40, width, height, 1, 4, 0,
len(pixels), 2835, 2835, 4, 4)
Path('gray-levels.bmp').write_bytes(header + dib + palette + pixels)Relevant Log Output/Screenshots
No serial logs captured. The missing calls were confirmed by source inspection of 1.6.0 and current develop; no patched firmware has been flashed or tested.
Activity
0No activity yet.