User-mode emulation for PocketBook 6.1-6.11 firmware on Linux desktop (podman + Wayland)
README.md

viewer — Host-side Wayland framebuffer viewer #

viewer is the host-side Wayland client that renders the active PocketBook task's framebuffer and forwards pointer + keyboard input back to the guest firmware via /hwevent.

Build #

# From repo root (builds inside container, outputs to host path):
./pbemu build [firmware]

Output: src/viewer/build-pc/viewer

Main loop overview #

viewer_app_run() drives a poll() loop over five file descriptors:

Slot FD Event
POLL_FD_WL Wayland socket Compositor events (configure, frame, pointer)
POLL_FD_TIMER timerfd Periodic SHM poll (default 33 ms ≈ 30 fps)
POLL_FD_SIGNAL signalfd SIGINT/SIGTERM for clean shutdown
POLL_FD_SETTLE timerfd One-shot geometry-settle timer (default 150 ms)
POLL_FD_KEY_REPEAT timerfd Compositor-configured keyboard repeat synthesis

On each timer tick the viewer:

  1. Copies one informer snapshot using the shared odd/even sequence protocol.
  2. Updates its window title, touch matrix, and active framebuffer SHM attachment from that snapshot.
  3. Commits any pending framebuffer changes to Wayland, paced by frame callbacks.

When a frame is committed, further timer-driven repaints are suppressed until the compositor's frame callback fires.

Source map #

src/main.c — entry point #

Creates a viewer_app, calls viewer_app_run(), destroys it, exits. No logic here.

src/viewer_app.c — main orchestration loop #

Owns the viewer_app struct, wires all subsystems together, and runs the poll() event loop.

Key functions:

  • viewer_app_create() — allocates and initialises all subsystems
  • viewer_app_run() — poll() loop; handles Wayland, timer, settle-timer, and signal events
  • viewer_app_destroy() — tears down SHM mapping, Wayland, timers
  • try_repaint() — reads informer state, decodes framebuffer geometry, and schedules/commits repaints
  • update_active_task() — attaches/detaches task SHM when the informer-selected task changes; refreshes title + touch matrix
  • attach_shm() / detach_shm() — SysV SHM lifecycle management
  • commit_frame() — copies SHM pixels to a free buffer_pool slot, converts 8bpp→XRGB if needed, commits to Wayland
  • on_pointer_event() — translates Wayland pointer press/move/release to /hwevent messages via input_bridge
  • on_key_event() — maps xkbcommon-translated Wayland key events to PocketBook EVT_KEY* / EVT_EXT_KB messages

The viewer_app struct tracks:

  • current_task_id, shm_key, shm_id, shm_addr, shm_size — active task SHM state
  • current_info / pending_info — framebuffer geometry (pending has a settle timer to ignore transient changes; settles after 150 ms of stability)
  • touch_matrix[8] — loaded from informer state; falls back to the default matrix until informer publishes one
  • fb_y_offset — wrapped scanout origin from the iv_fbinfo header: SHM row r is presented at window row (r + fb_y_offset) % fb_h, replicating the device display pipeline (the guest's panel painter draws the system bar into the bottom SHM rows; the wrap shows it at the top). A change forces a full repaint. Pointer input is sent in physical window coordinates — the guest subtracts fb_y_offset itself
  • title[256] — Wayland window title assembled from informer task/subtask names
  • repaint_state / last_repaint_seq — read-only attachment to the pb_repaint_state SHM segment written by the shim; used to limit blits to dirty rectangles or skip unchanged frames entirely

src/buffer_pool.c — double-buffered wl_shm pool #

Manages two memfd-backed XRGB8888 Wayland buffers. Only one buffer is submitted to the compositor at a time; the other is free for writing. Buffer release is signalled by the compositor via wl_buffer.release.

Key functions:

  • buffer_pool_init() — sets up the pool struct (no allocation yet)
  • buffer_pool_resize() — destroys and re-creates all slots at the new width×height
  • buffer_pool_get_free() — returns a slot not currently held by the compositor
  • buffer_pool_all_free() — true when both slots are free (safe to resize)
  • buffer_pool_destroy() — unmaps and closes all slots

Each slot (buffer_pool_slot): fd (memfd), data (mmap'd XRGB8888 pixels), data_size, wl_buf, busy flag.

src/wayland_client.c — Wayland protocol objects and callbacks #

Manages the Wayland connection, surfaces, input seats, and xkbcommon keyboard state.

Keyboard specifics:

  • binds wl_keyboard when the seat advertises keyboard capability
  • loads the compositor keymap into xkbcommon
  • applies locale-aware compose rules for dead keys / accented input
  • uses a timerfd to generate repeat callbacks at the compositor's configured rate/delay
  • emits high-level callbacks with translated keysym + UTF-32 codepoint data

Current key mapping strategy in viewer_app:

  • navigation / device-like keys (Esc, arrows, PageUp, PageDown, Home, Menu, Tab, keypad +/-) become PocketBook EVT_KEYDOWN / EVT_KEYUP / EVT_KEYREPEAT with IV_KEY_* codes
  • Backspace, Delete, and other text-edit control characters travel through EVT_EXT_KB
  • Enter still emits IV_KEY_OK, and on keydown it also mirrors U+000D through EVT_EXT_KB so PocketBook text widgets can submit
  • printable Unicode text becomes PocketBook EVT_EXT_KB carrying the translated UTF-32 character

Key objects managed: wl_display, wl_registry, wl_compositor, wl_shm, xdg_wm_base, xdg_surface, xdg_toplevel, wp_viewporter, wp_viewport, xdg_decoration_manager, wl_seat, wl_pointer.

Key functions:

  • wl_state_connect() — connects to Wayland, runs registry roundtrip, creates surface and xdg_toplevel
  • wl_state_set_title() — updates the xdg_toplevel title (called with task label)
  • wl_state_dispatch() — wraps wl_display_dispatch()
  • wl_state_repeat_fd() / wl_state_dispatch_key_repeat() — expose timerfd-based key repeat to the outer poll loop
  • wl_state_disconnect() — tears down all Wayland objects

Pointer event callbacks: pointer_enter, pointer_leave, pointer_motion, pointer_button, pointer_axis — delegate to wl_state.callbacks.on_pointer_*.

Touch matrix convention: 8 int32 values. Default {0,0,10000,0,10000,0,0,0} maps hw_x=logical_y, hw_y=logical_x (rotated-landscape panel). Forward transform: [x*10000; y*10000] = M * [hw_x; hw_y] + [m[0]; m[3]] where M = [m[1],m[2]; m[4],m[5]].

The following modules are compiled into libpbcore (see src/pbcore/README.md) and linked by both the viewer and the host probe tools.

src/pbcore/src/frame_source.c — informer snapshot helpers #

Attaches the informer SysV SHM segment, validates its header, and returns consistent snapshot copies to the rest of the viewer. Also provides helpers for decoding framebuffer geometry, task titles, and touch-matrix fallback.

Key functions:

  • read_informer_state() — attaches the informer segment if needed, then copies a consistent snapshot using the odd/even sequence protocol
  • load_touch_matrix_from_informer() — returns informer touch-matrix data when present, otherwise the default {0,0,10000,0,10000,0,0,0}
  • decode_informer_framebuffer_info() — validates the informer framebuffer fields and converts them to framebuffer_info
  • format_informer_title() — formats PocketBook – <task> — <subtask> from informer names when available
  • fnv1a_64() — FNV-1a 64-bit hash for frame change detection

src/pbcore/src/geometry_map.c — surface-to-framebuffer coordinate mapping #

Pure computation: maps a Wayland surface pointer coordinate to a framebuffer pixel coordinate, accounting for the wp_viewporter scaling ratio.

Key function:

  • map_surface_to_framebuffer(map, surf_x, surf_y, fb_x_out, fb_y_out) — scales (surf_x, surf_y) by fb_w/win_w × fb_h/win_h; without viewporter falls back to 1:1; clamps to framebuffer bounds

geometry_map fields: fb_w, fb_h (framebuffer dims), win_w, win_h (compositor window dims, 0 until first configure), viewporter_enabled.

src/pbcore/src/input_bridge.c — /hwevent POSIX message queue output #

Encodes Wayland pointer and keyboard events into PocketBook /hwevent 10×int32 messages and sends them via POSIX mq.

Key functions:

  • pb_input_open() — opens /hwevent mq O_WRONLY|O_NONBLOCK
  • pb_input_close() — closes the mq descriptor
  • compute_hw_coords() — inverts the 8-element affine touch matrix via Cramer's rule to map (logical_x, logical_y) → (hw_x, hw_y); adds fb_y_offset to logical_y before inversion (modular wrap by framebuffer height); falls back to (hw_x=y, hw_y=x) when determinant is zero
  • pb_input_build_event() — fills a pointer-shaped hwevent_msg struct (pressure/devtype for POINTERDOWN/MOVE/UP; MTSYNC zeroes x/y and uses DEV_UNKNOWN; timestamps via gettimeofday)
  • pb_input_build_common_event() — fills the generic {mod, par1, par2} payload used by keyboard/button events
  • pb_input_send() / pb_input_send_common() — build and send one hwevent_msg via mq_send()

Informer snapshot (pb_informer_state, src/common/pb_informer.h) #

The viewer's control plane comes from the informer snapshot rather than shim-owned sidecar files or monitor SHM offsets.

Field Meaning
magic, version, struct_size Protocol validation for cross-arch compatibility
sequence Odd/even seqlock counter used to copy a stable snapshot
flags Validity bits for task, framebuffer, touch matrix, and names
active_task_id, active_subtask_id Current monitor foreground task and subtask
fbshmkey, fbshmsize SysV SHM key and size for the active task framebuffer
width, height, depth, stride Normalised framebuffer geometry published by the informer
pixel_data_offset, fb_y_offset Byte offset to pixels within the task SHM and wrapped scanout origin
touch_matrix[8] Touch-panel affine transform read through official hwconfig APIs
task_name, subtask_name Human-facing window-title strings

The task SHM segment itself still begins with an iv_fbinfo header on the ARM side, but that header is decoded by the informer before the viewer sees it.

Repaint hint (pb_repaint_state, src/common/pb_repaint.h) #

A second SysV SHM segment (PB_REPAINT_SHM_KEY) carries a seqlocked dirty-region hint written by the shim's update_hooks.c and read by the viewer's try_repaint(). The viewer attaches it read-only on first successful shmget().

Field Meaning
sequence Seqlock counter; odd while writer is active, even when a stable snapshot is available
flags PB_REPAINT_FULL — full repaint requested; PB_REPAINT_RECT — dirty rectangle in x/y/w/h
x, y, w, h Dirty rectangle coordinates (valid when PB_REPAINT_RECT is set)