From 842aa28f09da1f713e5d125d8f85660ba64a39d5 Mon Sep 17 00:00:00 2001 From: Jimmy Stridh Date: Wed, 19 Aug 2026 11:53:46 +0200 Subject: [PATCH 1/2] feat: expose model-aware device surfaces --- CLEANROOM.md | 3 +- scripts/check-release.sh | 30 ++++- scripts/test-host.sh | 13 +- src/input_names.h | 39 ++++++ src/pen_transform.h | 65 ++++++++++ src/pixel_format.h | 75 ++++++++++++ src/quill.h | 55 ++++++++- src/quill_c.cpp | 81 +++++++++++-- src/quill_surface.h | 115 ++++++++++++++++++ src/vendor_probe.cpp | 63 +++++++--- src/vendor_probe.h | 3 + tests/device_acceptance_probe.c | 81 ++++++++----- tests/device_swap_probe.c | 22 ++-- tests/device_visual_probe.c | 36 +++--- tests/test_portable_helpers.c | 209 ++++++++++++++++++++++++++++++++ 15 files changed, 785 insertions(+), 105 deletions(-) create mode 100644 src/input_names.h create mode 100644 src/pen_transform.h create mode 100644 src/pixel_format.h create mode 100644 src/quill_surface.h create mode 100644 tests/test_portable_helpers.c diff --git a/CLEANROOM.md b/CLEANROOM.md index 1c35e13..c8655d9 100644 --- a/CLEANROOM.md +++ b/CLEANROOM.md @@ -19,9 +19,10 @@ history as part of the provenance record. - The clean-room replacement specification supplied by the project owner - Qt 6 headers from the matching reMarkable SDK +- The matching official reMarkable SDK artifacts and developer documentation - ELF metadata and dynamic-linking behavior - `libqsgepaper.so` obtained by the device owner and not redistributed -- Black-box behavior of the owner's reMarkable Paper Pro +- Black-box behavior of the owner's reMarkable 2 and reMarkable Paper Pro ## Verification boundary diff --git a/scripts/check-release.sh b/scripts/check-release.sh index 3bac972..db2fb5c 100755 --- a/scripts/check-release.sh +++ b/scripts/check-release.sh @@ -3,21 +3,43 @@ set -eu cd "$(dirname "$0")/.." fail() { echo "release check: $*" >&2; exit 1; } +warn() { echo "release check: warning: $*" >&2; } + +strict=${QUILL_RELEASE_STRICT:-0} +case "$strict" in + 0|1) ;; + *) fail "QUILL_RELEASE_STRICT must be 0 or 1" ;; +esac [ -f LICENSE ] || fail "LICENSE is missing" [ ! -e src/epfb.cpp ] || fail "prohibited historical implementation is present" [ ! -e src/epframebuffer.h ] || fail "prohibited historical header is present" -if git ls-files 'vendor/*.so' | grep -q .; then +if git ls-files | grep -Eq '(^|/)libqsgepaper\.so([.]|$)'; then fail "a proprietary vendor library is tracked" fi +if [ "$strict" -eq 1 ] && + [ -n "$(git status --porcelain --untracked-files=all)" ]; then + fail "strict release checks require a clean worktree" +fi + if [ -f build/libquill.so ]; then - required='quill_init quill_width quill_height quill_stride quill_format quill_buffer quill_swap_ex quill_swap quill_swap_mono_fast quill_swap_mono_quality quill_swap_color quill_swap_color_full quill_process_events' + required='quill_init quill_width quill_height quill_stride quill_format quill_buffer quill_get_surface quill_supports_color quill_swap_ex quill_swap quill_swap_mono_fast quill_swap_mono_quality quill_swap_color quill_swap_color_full quill_process_events' exports=$(nm -D --defined-only build/libquill.so) for symbol in $required; do - echo "$exports" | grep -Eq "[[:space:]]${symbol}$" || fail "missing export $symbol" + printf '%s\n' "$exports" | grep -Eq "[[:space:]]${symbol}$" || \ + fail "missing export $symbol" + done + for constructor in C1 C2; do + printf '%s\n' "$exports" | grep -Eq \ + "_ZN6QImage${constructor}EPhiiiNS_6FormatEPFvPvES2_$|_ZN6QImage${constructor}EPhiixNS_6FormatEPFvPvES2_$" || \ + fail "missing QImage $constructor constructor interposition export" done +elif [ "$strict" -eq 1 ]; then + fail "build/libquill.so is required for a strict release check" +else + warn "build/libquill.so is absent; binary ABI checks were skipped" fi -git diff --check +git diff --check HEAD -- echo "release checks passed" diff --git a/scripts/test-host.sh b/scripts/test-host.sh index 9db2644..9cad7fa 100755 --- a/scripts/test-host.sh +++ b/scripts/test-host.sh @@ -7,6 +7,17 @@ mkdir -p build/tests "$CXX" -std=c++17 -Wall -Wextra -Werror -pedantic \ -fsanitize=address,undefined -fno-omit-frame-pointer \ -Isrc tests/test_clipping.cpp -o build/tests/test_clipping -ASAN_OPTIONS=detect_leaks=1 build/tests/test_clipping +detect_leaks=1 +if [ "$(uname -s)" = Darwin ]; then + detect_leaks=0 +fi +ASAN_OPTIONS=detect_leaks=$detect_leaks build/tests/test_clipping + +CC=${CC:-cc} +"$CC" -std=c11 -Wall -Wextra -Werror -pedantic \ + -fsanitize=address,undefined -fno-omit-frame-pointer \ + -Isrc tests/test_portable_helpers.c \ + -o build/tests/test_portable_helpers +ASAN_OPTIONS=detect_leaks=$detect_leaks build/tests/test_portable_helpers echo "host tests passed" diff --git a/src/input_names.h b/src/input_names.h new file mode 100644 index 0000000..47a7994 --- /dev/null +++ b/src/input_names.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +enum quill_input_role { + QUILL_INPUT_ROLE_PEN = 0, + QUILL_INPUT_ROLE_POWER, + QUILL_INPUT_ROLE_TOUCH, + QUILL_INPUT_ROLE_COUNT, +}; + +static inline const char *quill_input_role_name(enum quill_input_role role) { + switch (role) { + case QUILL_INPUT_ROLE_PEN: return "pen"; + case QUILL_INPUT_ROLE_POWER: return "power"; + case QUILL_INPUT_ROLE_TOUCH: return "touch"; + default: return "unknown"; + } +} + +static inline int quill_input_name_matches(const char *name, + enum quill_input_role role) { + char lower[128] = {0}; + for (size_t i = 0; i + 1 < sizeof lower && name[i]; ++i) + lower[i] = name[i] >= 'A' && name[i] <= 'Z' ? name[i] + ('a' - 'A') : name[i]; + + switch (role) { + case QUILL_INPUT_ROLE_PEN: + return strstr(lower, "marker") || strstr(lower, "wacom") || + strstr(lower, "digitizer"); + case QUILL_INPUT_ROLE_POWER: + return strstr(lower, "powerkey") || strstr(lower, "power key"); + case QUILL_INPUT_ROLE_TOUCH: + return strstr(lower, "touch") || strstr(lower, "_mt"); + default: + return 0; + } +} diff --git a/src/pen_transform.h b/src/pen_transform.h new file mode 100644 index 0000000..4833e7b --- /dev/null +++ b/src/pen_transform.h @@ -0,0 +1,65 @@ +#pragma once + +#include "quill.h" + +#include + +struct quill_pen_transform { + int raw_min_x; + int raw_max_x; + int raw_min_y; + int raw_max_y; + int screen_width; + int screen_height; + enum quill_pen_orientation orientation; +}; + +static inline int quill_pen_transform_init( + struct quill_pen_transform *transform, + int raw_min_x, int raw_max_x, int raw_min_y, int raw_max_y, + int screen_width, int screen_height, + enum quill_pen_orientation orientation) { + if (!transform || raw_max_x <= raw_min_x || raw_max_y <= raw_min_y || + screen_width <= 0 || screen_height <= 0 || + (orientation != QUILL_PEN_DIRECT && + orientation != QUILL_PEN_ROTATE_270)) + return -1; + transform->raw_min_x = raw_min_x; + transform->raw_max_x = raw_max_x; + transform->raw_min_y = raw_min_y; + transform->raw_max_y = raw_max_y; + transform->screen_width = screen_width; + transform->screen_height = screen_height; + transform->orientation = orientation; + return 0; +} + +static inline int quill_map_pen_axis(int value, int minimum, int maximum, + int extent, int invert) { + if (value < minimum) value = minimum; + if (value > maximum) value = maximum; + int64_t offset = (int64_t)value - (int64_t)minimum; + int64_t range = (int64_t)maximum - (int64_t)minimum; + if (invert) offset = range - offset; + return range > 0 && extent > 1 ? (int)(offset * (extent - 1) / range) : 0; +} + +static inline void quill_map_pen(const struct quill_pen_transform *transform, + int raw_x, int raw_y, int *screen_x, int *screen_y) { + if (transform->orientation == QUILL_PEN_ROTATE_270) { + *screen_x = quill_map_pen_axis(raw_y, transform->raw_min_y, + transform->raw_max_y, + transform->screen_width, 0); + *screen_y = quill_map_pen_axis(raw_x, transform->raw_min_x, + transform->raw_max_x, + transform->screen_height, 1); + return; + } + + *screen_x = quill_map_pen_axis(raw_x, transform->raw_min_x, + transform->raw_max_x, + transform->screen_width, 0); + *screen_y = quill_map_pen_axis(raw_y, transform->raw_min_y, + transform->raw_max_y, + transform->screen_height, 0); +} diff --git a/src/pixel_format.h b/src/pixel_format.h new file mode 100644 index 0000000..4bcdd31 --- /dev/null +++ b/src/pixel_format.h @@ -0,0 +1,75 @@ +#pragma once + +#include "quill.h" + +#include +#include + +static inline int quill_bytes_per_pixel(int format) { + if (format == QUILL_FORMAT_RGB32) return 4; + if (format == QUILL_FORMAT_RGB16) return 2; + return 0; +} + +static inline uint16_t quill_rgb16(unsigned char red, unsigned char green, + unsigned char blue) { + return (uint16_t)(((uint16_t)(red & 0xf8) << 8) | + ((uint16_t)(green & 0xfc) << 3) | + (blue >> 3)); +} + +static inline uint16_t quill_rgb16_gray(unsigned char value) { + return quill_rgb16(value, value, value); +} + +static inline int quill_store_rgb(unsigned char *pixel, int format, + unsigned char red, unsigned char green, + unsigned char blue) { + if (!pixel) return -1; + if (format == QUILL_FORMAT_RGB32) { + const uint32_t rgb32 = UINT32_C(0xff000000) | + (uint32_t)red << 16 | + (uint32_t)green << 8 | + blue; + memcpy(pixel, &rgb32, sizeof rgb32); + return 0; + } + + if (format == QUILL_FORMAT_RGB16) { + const uint16_t rgb16 = quill_rgb16(red, green, blue); + memcpy(pixel, &rgb16, sizeof rgb16); + return 0; + } + return -1; +} + +static inline int quill_load_rgb(const unsigned char *pixel, int format, + unsigned char *red, unsigned char *green, + unsigned char *blue) { + if (!pixel || !red || !green || !blue) return -1; + if (format == QUILL_FORMAT_RGB32) { + uint32_t rgb32; + memcpy(&rgb32, pixel, sizeof rgb32); + *red = (unsigned char)(rgb32 >> 16); + *green = (unsigned char)(rgb32 >> 8); + *blue = (unsigned char)rgb32; + return 0; + } + if (format == QUILL_FORMAT_RGB16) { + uint16_t rgb16; + memcpy(&rgb16, pixel, sizeof rgb16); + const unsigned int red5 = (rgb16 >> 11) & 0x1f; + const unsigned int green6 = (rgb16 >> 5) & 0x3f; + const unsigned int blue5 = rgb16 & 0x1f; + *red = (unsigned char)((red5 << 3) | (red5 >> 2)); + *green = (unsigned char)((green6 << 2) | (green6 >> 4)); + *blue = (unsigned char)((blue5 << 3) | (blue5 >> 2)); + return 0; + } + return -1; +} + +static inline int quill_store_gray(unsigned char *pixel, int format, + unsigned char value) { + return quill_store_rgb(pixel, format, value, value, value); +} diff --git a/src/quill.h b/src/quill.h index 681887d..d948ff4 100644 --- a/src/quill.h +++ b/src/quill.h @@ -1,10 +1,12 @@ #pragma once +#include + #ifdef __cplusplus extern "C" { #endif -// quill C ABI: direct access to the reMarkable Paper Pro e-paper engine. +// quill C ABI: direct access to the reMarkable e-paper engine. // Lifecycle: // quill_init(); // unsigned char *fb = quill_buffer(); @@ -14,9 +16,50 @@ extern "C" { #define QUILL_CONTENT_MONO 0 #define QUILL_CONTENT_COLOR 1 -// Observed useful screen modes on Paper Pro / libqsgepaper ACEP backend. -#define QUILL_MODE_FASTEST 0 // low-latency mono ink -#define QUILL_MODE_FAST 1 // grayscale/mono; not useful for color +#define QUILL_FORMAT_RGB32 4 +#define QUILL_FORMAT_RGB16 7 + +enum quill_model { + QUILL_MODEL_UNKNOWN = 0, + QUILL_MODEL_REMARKABLE_2, + QUILL_MODEL_PAPER_PRO, +}; + +enum quill_capability { + QUILL_CAPABILITY_COLOR = 1u << 0, +}; + +enum quill_pen_orientation { + QUILL_PEN_ORIENTATION_UNKNOWN = -1, + QUILL_PEN_DIRECT = 0, + QUILL_PEN_ROTATE_270, +}; + +// Canonical display view. A successful quill_init() guarantees that the +// returned surface has a supported format and coherent geometry, stride, and +// QImage-reported logical byte span. An unprofiled geometry is exposed as +// QUILL_MODEL_UNKNOWN with no capabilities and no pen orientation, allowing +// generic mono discovery while gated features remain unavailable. The vendor's +// external-buffer contract supplies the backing storage, which remains +// vendor-owned. Obtain this view through quill_get_surface(); callers cannot +// validate fabricated storage. +struct quill_surface { + unsigned char *pixels; + int width; + int height; + int stride; + int format; + int bytes_per_pixel; + size_t size; + enum quill_model model; + unsigned int capabilities; + enum quill_pen_orientation pen_orientation; +}; + +// Observed useful screen modes. The reMarkable 2 is monochrome; the color +// modes and content type below are meaningful only on the Paper Pro. +#define QUILL_MODE_FASTEST 0 // low-latency mono ink on RM2 and Paper Pro +#define QUILL_MODE_FAST 1 // grayscale/mono #define QUILL_MODE_COLOR3 3 // color-capable #define QUILL_MODE_COLOR4 4 // color-capable; default color mode #define QUILL_MODE_COLOR5 5 // color-capable @@ -28,6 +71,10 @@ int quill_stride(void); int quill_format(void); unsigned char *quill_buffer(void); +// Copies the initialized surface into `surface`. Returns zero on success. +int quill_get_surface(struct quill_surface *surface); +int quill_supports_color(void); + // Raw swap. content_type: QUILL_CONTENT_MONO or QUILL_CONTENT_COLOR. // Rectangles are clipped to the panel. Beware: full_refresh may be promoted // to a panel-wide operation by the vendor backend even for a small rectangle; diff --git a/src/quill_c.cpp b/src/quill_c.cpp index 46b3c75..4d72022 100644 --- a/src/quill_c.cpp +++ b/src/quill_c.cpp @@ -1,11 +1,15 @@ #include "quill.h" #include "clip_rect.h" +#include "quill_surface.h" #include "vendor_probe.h" #include #include #include +#include +#include +#include #include #include #include @@ -13,9 +17,48 @@ namespace { std::mutex init_mutex; QCoreApplication *app = nullptr; -quill_vendor::FramebufferView framebuffer; +quill_surface surface{}; bool initialized = false; int last_error = 0; + +int surface_from_trusted_vendor_framebuffer( + const quill_vendor::FramebufferView &framebuffer, + quill_surface *resolved) { + if (!resolved || !framebuffer.pixels || framebuffer.stride <= 0 || + framebuffer.stride > INT_MAX || framebuffer.logical_size <= 0 || + static_cast(framebuffer.logical_size) > + std::numeric_limits::max()) + return 7; + + int bytes_per_pixel; + std::size_t logical_size; + if (quill_surface_measure_layout( + framebuffer.width, framebuffer.height, + static_cast(framebuffer.stride), + static_cast(framebuffer.format), + &bytes_per_pixel, &logical_size) != 0 || + logical_size != static_cast(framebuffer.logical_size)) + return 7; + + const quill_display_profile profile = + quill_display_profile_for_geometry(framebuffer.width, + framebuffer.height); + const quill_surface candidate = { + framebuffer.pixels, + framebuffer.width, + framebuffer.height, + static_cast(framebuffer.stride), + static_cast(framebuffer.format), + bytes_per_pixel, + logical_size, + profile.model, + profile.capabilities, + profile.pen_orientation, + }; + if (!quill_surface_is_coherent(&candidate)) return 7; + *resolved = candidate; + return 0; +} } extern "C" { @@ -37,24 +80,36 @@ int quill_init(void) { } if (!app) return last_error = 1; + quill_vendor::FramebufferView framebuffer; int result = quill_vendor::initialize(&framebuffer); if (result) return last_error = result; - if (!framebuffer.pixels || framebuffer.width <= 0 || framebuffer.height <= 0 || - framebuffer.stride <= 0 || framebuffer.stride > std::numeric_limits::max()) + if (surface_from_trusted_vendor_framebuffer(framebuffer, &surface) != 0) return last_error = 7; initialized = true; - std::fprintf(stderr, "quill: framebuffer %dx%d stride=%lld format=%d\n", - framebuffer.width, framebuffer.height, - static_cast(framebuffer.stride), int(framebuffer.format)); + std::fprintf(stderr, + "quill: framebuffer %dx%d stride=%d format=%d model=%d\n", + surface.width, surface.height, surface.stride, surface.format, + static_cast(surface.model)); return 0; } -int quill_width(void) { return initialized ? framebuffer.width : 0; } -int quill_height(void) { return initialized ? framebuffer.height : 0; } -int quill_stride(void) { return initialized ? int(framebuffer.stride) : 0; } -int quill_format(void) { return initialized ? int(framebuffer.format) : -1; } -unsigned char *quill_buffer(void) { return initialized ? framebuffer.pixels : nullptr; } +int quill_width(void) { return initialized ? surface.width : 0; } +int quill_height(void) { return initialized ? surface.height : 0; } +int quill_stride(void) { return initialized ? surface.stride : 0; } +int quill_format(void) { return initialized ? surface.format : -1; } +unsigned char *quill_buffer(void) { return initialized ? surface.pixels : nullptr; } + +int quill_get_surface(struct quill_surface *result) { + if (!initialized || !result) return 1; + *result = surface; + return 0; +} + +int quill_supports_color(void) { + return initialized && + (surface.capabilities & QUILL_CAPABILITY_COLOR) != 0; +} unsigned long quill_swap_ex(int x, int y, int width, int height, int mode, int full_refresh, int content_type) { @@ -63,8 +118,8 @@ unsigned long quill_swap_ex(int x, int y, int width, int height, int mode, return 0; quill_detail::ClippedRect clipped{}; - if (!quill_detail::clip_rect(x, y, width, height, framebuffer.width, - framebuffer.height, &clipped)) + if (!quill_detail::clip_rect(x, y, width, height, surface.width, + surface.height, &clipped)) return 0; return quill_vendor::swap( diff --git a/src/quill_surface.h b/src/quill_surface.h new file mode 100644 index 0000000..671e017 --- /dev/null +++ b/src/quill_surface.h @@ -0,0 +1,115 @@ +#pragma once + +#include "pixel_format.h" +#include "quill.h" + +#include + +#define QUILL_RM2_WIDTH 1404 +#define QUILL_RM2_HEIGHT 1872 +#define QUILL_PAPER_PRO_WIDTH 1620 +#define QUILL_PAPER_PRO_HEIGHT 2160 + +struct quill_display_profile { + enum quill_model model; + unsigned int capabilities; + enum quill_pen_orientation pen_orientation; +}; + +static inline struct quill_display_profile quill_display_profile_for_geometry( + int width, int height) { + struct quill_display_profile profile = { + QUILL_MODEL_UNKNOWN, + 0, + QUILL_PEN_ORIENTATION_UNKNOWN, + }; + if (width == QUILL_RM2_WIDTH && height == QUILL_RM2_HEIGHT) { + profile.model = QUILL_MODEL_REMARKABLE_2; + profile.pen_orientation = QUILL_PEN_ROTATE_270; + } else if (width == QUILL_PAPER_PRO_WIDTH && + height == QUILL_PAPER_PRO_HEIGHT) { + profile.model = QUILL_MODEL_PAPER_PRO; + profile.capabilities = QUILL_CAPABILITY_COLOR; + profile.pen_orientation = QUILL_PEN_DIRECT; + } + return profile; +} + +static inline int quill_surface_measure_layout(int width, int height, + int stride, int format, + int *resolved_bytes_per_pixel, + size_t *resolved_size) { + if (!resolved_bytes_per_pixel || !resolved_size) return -1; + *resolved_bytes_per_pixel = 0; + *resolved_size = 0; + const int bytes_per_pixel = quill_bytes_per_pixel(format); + if (width <= 0 || height <= 0 || stride <= 0 || !bytes_per_pixel) + return -1; + + const uint64_t minimum_stride = + (uint64_t)(unsigned int)width * (unsigned int)bytes_per_pixel; + if (minimum_stride > (uint64_t)(unsigned int)stride || + (size_t)(unsigned int)height > + SIZE_MAX / (size_t)(unsigned int)stride) + return -1; + *resolved_bytes_per_pixel = bytes_per_pixel; + *resolved_size = + (size_t)(unsigned int)stride * (size_t)(unsigned int)height; + return 0; +} + +// Checks that the descriptor fields agree with one another. This cannot prove +// how much storage backs `pixels`; renderers accept only trusted library +// surfaces or test fixtures with independently sized allocations. +static inline int quill_surface_is_coherent( + const struct quill_surface *surface) { + if (!surface || !surface->pixels) return 0; + int bytes_per_pixel; + size_t size; + if (quill_surface_measure_layout(surface->width, surface->height, + surface->stride, surface->format, + &bytes_per_pixel, &size) != 0) + return 0; + const struct quill_display_profile profile = + quill_display_profile_for_geometry(surface->width, surface->height); + return surface->bytes_per_pixel == bytes_per_pixel && + surface->size == size && surface->model == profile.model && + surface->capabilities == profile.capabilities && + surface->pen_orientation == profile.pen_orientation; +} + +static inline unsigned char *quill_surface_pixel( + const struct quill_surface *surface, int x, int y) { + if (!surface || !surface->pixels || x < 0 || y < 0 || + x >= surface->width || y >= surface->height) + return NULL; + return surface->pixels + (size_t)y * (size_t)surface->stride + + (size_t)x * (size_t)surface->bytes_per_pixel; +} + +static inline int quill_surface_store_gray(struct quill_surface *surface, + int x, int y, + unsigned char value) { + unsigned char *pixel = quill_surface_pixel(surface, x, y); + return pixel ? quill_store_gray(pixel, surface->format, value) : -1; +} + +static inline int quill_surface_store_rgb(struct quill_surface *surface, + int x, int y, + unsigned char red, + unsigned char green, + unsigned char blue) { + unsigned char *pixel = quill_surface_pixel(surface, x, y); + return pixel ? quill_store_rgb(pixel, surface->format, red, green, blue) + : -1; +} + +static inline int quill_surface_load_rgb(const struct quill_surface *surface, + int x, int y, + unsigned char *red, + unsigned char *green, + unsigned char *blue) { + unsigned char *pixel = quill_surface_pixel(surface, x, y); + return pixel ? quill_load_rgb(pixel, surface->format, red, green, blue) + : -1; +} diff --git a/src/vendor_probe.cpp b/src/vendor_probe.cpp index b974648..8bee782 100644 --- a/src/vendor_probe.cpp +++ b/src/vendor_probe.cpp @@ -14,7 +14,7 @@ namespace { using Cleanup = void (*)(void *); -using ImageCtor = void (*)(QImage *, unsigned char *, int, int, qint64, +using ImageCtor = void (*)(QImage *, unsigned char *, int, int, qsizetype, QImage::Format, Cleanup, void *); struct Candidate { @@ -23,6 +23,7 @@ struct Candidate { int width; int height; qsizetype stride; + qsizetype logical_size; QImage::Format format; Cleanup cleanup; }; @@ -63,24 +64,31 @@ ImageCtor resolve_ctor(const char *name, std::atomic &slot) { void observe(QImage *image, unsigned char *pixels, Cleanup cleanup) { if (!capture.load(std::memory_order_acquire)) return; Candidate candidate{image, pixels, image->width(), image->height(), - image->bytesPerLine(), image->format(), cleanup}; + image->bytesPerLine(), image->sizeInBytes(), + image->format(), cleanup}; std::lock_guard lock(candidates_mutex); candidates.push_back(candidate); } void forward_ctor(const char *name, std::atomic &slot, QImage *self, - unsigned char *pixels, int width, int height, qint64 stride, + unsigned char *pixels, int width, int height, qsizetype stride, QImage::Format format, Cleanup cleanup, void *info) { resolve_ctor(name, slot)(self, pixels, width, height, stride, format, cleanup, info); observe(self, pixels, cleanup); } bool plausible(const Candidate &c) { - if (!c.object || !c.pixels || c.width <= 0 || c.height <= 0 || c.stride <= 0) + if (!c.object || !c.pixels || c.width <= 0 || c.height <= 0 || + c.stride <= 0 || c.logical_size <= 0) return false; - if (c.format != QImage::Format_RGB32) return false; - const qint64 minimum = static_cast(c.width) * 4; - return c.stride >= minimum && c.stride <= INT_MAX; + int bytes_per_pixel = 0; + if (c.format == QImage::Format_RGB32) bytes_per_pixel = 4; + if (c.format == QImage::Format_RGB16) bytes_per_pixel = 2; + if (!bytes_per_pixel) return false; + const qint64 minimum = static_cast(c.width) * bytes_per_pixel; + return c.stride >= minimum && c.stride <= INT_MAX && + c.logical_size / c.stride == c.height && + c.logical_size % c.stride == 0; } int requested_index() { @@ -95,28 +103,41 @@ int requested_index() { } // namespace -// Exact Qt 6/AArch64 constructor entry points. Both are provided because Qt -// builds are allowed to bind either complete-object or base-object aliases. +#if QT_POINTER_SIZE == 4 +#define QUILL_QIMAGE_C1_SYMBOL "_ZN6QImageC1EPhiiiNS_6FormatEPFvPvES2_" +#define QUILL_QIMAGE_C2_SYMBOL "_ZN6QImageC2EPhiiiNS_6FormatEPFvPvES2_" +#elif QT_POINTER_SIZE == 8 +#define QUILL_QIMAGE_C1_SYMBOL "_ZN6QImageC1EPhiixNS_6FormatEPFvPvES2_" +#define QUILL_QIMAGE_C2_SYMBOL "_ZN6QImageC2EPhiixNS_6FormatEPFvPvES2_" +#else +#error "quill supports only 32-bit and 64-bit Qt targets" +#endif + +// Exact Qt 6 constructor entry points. Both are provided because Qt builds +// are allowed to bind either complete-object or base-object aliases. extern "C" void qimage_external_c1(QImage *self, unsigned char *pixels, - int width, int height, qint64 stride, QImage::Format format, - Cleanup cleanup, void *info) asm("_ZN6QImageC1EPhiixNS_6FormatEPFvPvES2_"); + int width, int height, qsizetype stride, QImage::Format format, + Cleanup cleanup, void *info) asm(QUILL_QIMAGE_C1_SYMBOL); extern "C" void qimage_external_c1(QImage *self, unsigned char *pixels, - int width, int height, qint64 stride, QImage::Format format, + int width, int height, qsizetype stride, QImage::Format format, Cleanup cleanup, void *info) { - forward_ctor("_ZN6QImageC1EPhiixNS_6FormatEPFvPvES2_", real_c1, self, + forward_ctor(QUILL_QIMAGE_C1_SYMBOL, real_c1, self, pixels, width, height, stride, format, cleanup, info); } extern "C" void qimage_external_c2(QImage *self, unsigned char *pixels, - int width, int height, qint64 stride, QImage::Format format, - Cleanup cleanup, void *info) asm("_ZN6QImageC2EPhiixNS_6FormatEPFvPvES2_"); + int width, int height, qsizetype stride, QImage::Format format, + Cleanup cleanup, void *info) asm(QUILL_QIMAGE_C2_SYMBOL); extern "C" void qimage_external_c2(QImage *self, unsigned char *pixels, - int width, int height, qint64 stride, QImage::Format format, + int width, int height, qsizetype stride, QImage::Format format, Cleanup cleanup, void *info) { - forward_ctor("_ZN6QImageC2EPhiixNS_6FormatEPFvPvES2_", real_c2, self, + forward_ctor(QUILL_QIMAGE_C2_SYMBOL, real_c2, self, pixels, width, height, stride, format, cleanup, info); } +#undef QUILL_QIMAGE_C1_SYMBOL +#undef QUILL_QIMAGE_C2_SYMBOL + namespace quill_vendor { int initialize(FramebufferView *view) { @@ -167,16 +188,18 @@ int initialize(FramebufferView *view) { std::fprintf(stderr, "quill: %zu plausible framebuffer images; selection is ambiguous\n", valid.size()); for (size_t i = 0; i < valid.size(); ++i) - std::fprintf(stderr, "quill: candidate %zu: %dx%d stride=%lld format=%d cleanup=%s\n", + std::fprintf(stderr, "quill: candidate %zu: %dx%d stride=%lld logical-bytes=%lld format=%d cleanup=%s\n", i, valid[i].width, valid[i].height, - static_cast(valid[i].stride), int(valid[i].format), + static_cast(valid[i].stride), + static_cast(valid[i].logical_size), + int(valid[i].format), valid[i].cleanup ? "yes" : "no"); std::fputs("quill: validate on-device, then set QUILL_AUX_BUFFER_INDEX\n", stderr); return 6; } const Candidate &selected = valid[choice < 0 ? 0 : choice]; *view = {selected.pixels, selected.width, selected.height, - selected.stride, selected.format}; + selected.stride, selected.logical_size, selected.format}; return 0; } diff --git a/src/vendor_probe.h b/src/vendor_probe.h index c4cad43..3d0c7a2 100644 --- a/src/vendor_probe.h +++ b/src/vendor_probe.h @@ -10,6 +10,9 @@ struct FramebufferView { int width = 0; int height = 0; qsizetype stride = 0; + // QImage::sizeInBytes(). This is derived from image geometry; the + // intercepted constructor does not expose physical allocation capacity. + qsizetype logical_size = 0; QImage::Format format = QImage::Format_Invalid; }; diff --git a/tests/device_acceptance_probe.c b/tests/device_acceptance_probe.c index 5697ec3..07e7851 100644 --- a/tests/device_acceptance_probe.c +++ b/tests/device_acceptance_probe.c @@ -1,4 +1,5 @@ #include "quill.h" +#include "quill_surface.h" #include #include @@ -13,20 +14,31 @@ static void check(int condition, const char *name) { } int main(void) { + struct quill_surface surface; + check(quill_get_surface(NULL) != 0, "null surface rejected"); + check(quill_get_surface(&surface) != 0, + "surface unavailable before initialize"); + check(!quill_supports_color(), + "capabilities unavailable before initialize"); check(quill_init() == 0, "initialize"); check(quill_init() == 0, "idempotent initialize"); if (failures) return failures; - const int width = quill_width(), height = quill_height(); - const int stride = quill_stride(); - unsigned char *buffer = quill_buffer(); - check(width == 1620 && height == 2160, "known geometry"); - check(quill_format() == 4, "RGB32 format"); - check(stride >= width * 4 && buffer != NULL, "buffer and stride"); + check(quill_get_surface(&surface) == 0, "get coherent surface"); + if (failures) return failures; + const int width = surface.width, height = surface.height; + const int stride = surface.stride; + const int bytes_per_pixel = surface.bytes_per_pixel; + unsigned char *buffer = surface.pixels; + const int supports_color = quill_supports_color(); + check(quill_surface_is_coherent(&surface), "surface descriptor coherence"); + check(supports_color == + !!(surface.capabilities & QUILL_CAPABILITY_COLOR), + "color capability query"); check(quill_swap_ex(0, 0, 0, 10, 0, 0, QUILL_CONTENT_MONO) == 0, "empty rectangle rejected"); - check(quill_swap_ex(2000, 0, 10, 10, 0, 0, QUILL_CONTENT_MONO) == 0, + check(quill_swap_ex(width, 0, 10, 10, 0, 0, QUILL_CONTENT_MONO) == 0, "offscreen rectangle rejected"); check(quill_swap_ex(INT_MAX, INT_MIN, INT_MAX, INT_MAX, 0, 0, QUILL_CONTENT_MONO) == 0, @@ -34,24 +46,23 @@ int main(void) { check(quill_swap_ex(0, 0, 1, 1, 0, 0, 99) == 0, "invalid content type rejected"); - if (!buffer || stride < width * 4) return failures + 10; - const int x = 8, y = 8, size = 8; - unsigned char saved[size * size * 4]; + int size = width < height ? width : height; + if (size > 8) size = 8; + const int x = (width - size) / 2; + const int y = (height - size) / 2; + unsigned char saved[8 * 8 * 4]; + const size_t row_bytes = (size_t)size * bytes_per_pixel; for (int row = 0; row < size; ++row) - memcpy(saved + row * size * 4, buffer + (y + row) * stride + x * 4, - size * 4); + memcpy(saved + row * row_bytes, + buffer + (y + row) * stride + x * bytes_per_pixel, row_bytes); unsigned long last = 0; for (int iteration = 0; iteration < 200; ++iteration) { unsigned char value = (iteration & 1) ? 0xff : 0; for (int row = 0; row < size; ++row) { - unsigned char *pixel = buffer + (y + row) * stride + x * 4; - for (int column = 0; column < size; ++column) { - pixel[column * 4] = value; - pixel[column * 4 + 1] = value; - pixel[column * 4 + 2] = value; - pixel[column * 4 + 3] = 0xff; - } + for (int column = 0; column < size; ++column) + quill_surface_store_gray(&surface, x + column, y + row, + value); } last = quill_swap_mono_fast(x, y, size, size); quill_process_events(); @@ -64,21 +75,27 @@ int main(void) { check(quill_swap_mono_fast(width - 1, height - 1, 8, 8) != 0, "bottom-right clipping"); - /* Exercise every documented mode on a tiny region before restoring it. */ - check(quill_swap_ex(x, y, size, size, 1, 0, QUILL_CONTENT_COLOR) != 0, - "color content mode 1 call"); - check(quill_swap_ex(x, y, size, size, 3, 0, QUILL_CONTENT_COLOR) != 0, - "color mode 3 call"); - check(quill_swap_ex(x, y, size, size, 4, 0, QUILL_CONTENT_COLOR) != 0, - "color mode 4 call"); - check(quill_swap_ex(x, y, size, size, 5, 0, QUILL_CONTENT_COLOR) != 0, - "color mode 5 call"); + if (supports_color) { + /* Exercise every documented color mode on the color-capable panel. */ + check(quill_swap_ex(x, y, size, size, 1, 0, QUILL_CONTENT_COLOR) != 0, + "color content mode 1 call"); + check(quill_swap_ex(x, y, size, size, 3, 0, QUILL_CONTENT_COLOR) != 0, + "color mode 3 call"); + check(quill_swap_ex(x, y, size, size, 4, 0, QUILL_CONTENT_COLOR) != 0, + "color mode 4 call"); + check(quill_swap_ex(x, y, size, size, 5, 0, QUILL_CONTENT_COLOR) != 0, + "color mode 5 call"); + } for (int row = 0; row < size; ++row) - memcpy(buffer + (y + row) * stride + x * 4, - saved + row * size * 4, size * 4); - check(quill_swap_color_full(x, y, size, size) != 0, - "complete color refresh call"); + memcpy(buffer + (y + row) * stride + x * bytes_per_pixel, + saved + row * row_bytes, row_bytes); + if (supports_color) + check(quill_swap_color_full(x, y, size, size) != 0, + "complete color refresh call"); + else + check(quill_swap_mono_quality(x, y, size, size) != 0, + "quality mono restore call"); quill_process_events(); fprintf(stderr, "acceptance failures=%d\n", failures); diff --git a/tests/device_swap_probe.c b/tests/device_swap_probe.c index f615a53..af59c80 100644 --- a/tests/device_swap_probe.c +++ b/tests/device_swap_probe.c @@ -1,4 +1,5 @@ #include "quill.h" +#include "pixel_format.h" #include #include @@ -14,21 +15,21 @@ int main(void) { const int x = 20, y = 20, width = 24, height = 24; const int stride = quill_stride(); + const int format = quill_format(); + const int bytes_per_pixel = quill_bytes_per_pixel(format); unsigned char *buffer = quill_buffer(); - if (!buffer || quill_format() != 4 || stride < quill_width() * 4) return 20; + if (!buffer || !bytes_per_pixel || stride < quill_width() * bytes_per_pixel) + return 20; - const size_t row_bytes = (size_t)width * 4; + const size_t row_bytes = (size_t)width * bytes_per_pixel; unsigned char *saved = malloc(row_bytes * height); if (!saved) return 21; for (int row = 0; row < height; ++row) { - unsigned char *target = buffer + (size_t)(y + row) * stride + (size_t)x * 4; + unsigned char *target = buffer + (size_t)(y + row) * stride + + (size_t)x * bytes_per_pixel; memcpy(saved + (size_t)row * row_bytes, target, row_bytes); - for (int column = 0; column < width; ++column) { - target[column * 4 + 0] = 0; - target[column * 4 + 1] = 0; - target[column * 4 + 2] = 0; - target[column * 4 + 3] = 0xff; - } + for (int column = 0; column < width; ++column) + quill_store_gray(target + column * bytes_per_pixel, format, 0); } unsigned long draw_token = quill_swap_mono_fast(x, y, width, height); @@ -36,7 +37,8 @@ int main(void) { usleep(300000); for (int row = 0; row < height; ++row) { - unsigned char *target = buffer + (size_t)(y + row) * stride + (size_t)x * 4; + unsigned char *target = buffer + (size_t)(y + row) * stride + + (size_t)x * bytes_per_pixel; memcpy(target, saved + (size_t)row * row_bytes, row_bytes); } unsigned long restore_token = quill_swap_mono_quality(x, y, width, height); diff --git a/tests/device_visual_probe.c b/tests/device_visual_probe.c index 875c9bc..19f52ce 100644 --- a/tests/device_visual_probe.c +++ b/tests/device_visual_probe.c @@ -9,29 +9,23 @@ * Holds the composite, then exits so the wrapper restores xochitl. */ #include "quill.h" +#include "quill_surface.h" #include #include #include -static unsigned char *fb; -static int W, H, S; +static struct quill_surface surface; static void fill(int x, int y, int w, int h, unsigned char r, unsigned char g, unsigned char b) { if (x < 0) { w += x; x = 0; } if (y < 0) { h += y; y = 0; } - if (x + w > W) w = W - x; - if (y + h > H) h = H - y; - for (int row = 0; row < h; ++row) { - unsigned char *p = fb + (size_t)(y + row) * S + (size_t)x * 4; - for (int col = 0; col < w; ++col) { - p[col * 4 + 0] = b; - p[col * 4 + 1] = g; - p[col * 4 + 2] = r; - p[col * 4 + 3] = 0xff; - } - } + if (x + w > surface.width) w = surface.width - x; + if (y + h > surface.height) h = surface.height - y; + for (int row = 0; row < h; ++row) + for (int col = 0; col < w; ++col) + quill_surface_store_rgb(&surface, x + col, y + row, r, g, b); } int main(void) { @@ -39,14 +33,16 @@ int main(void) { fprintf(stderr, "visual: init failed\n"); return 1; } - W = quill_width(); - H = quill_height(); - S = quill_stride(); - fb = quill_buffer(); - if (!fb || W <= 0 || H <= 0) return 1; + if (quill_get_surface(&surface) != 0) return 1; + const int W = surface.width; + const int H = surface.height; + if (!quill_supports_color()) { + fprintf(stderr, "visual: SKIP (model %d has no color capability)\n", + (int)surface.model); + return 0; + } - for (int row = 0; row < H; ++row) - memset(fb + (size_t)row * S, 0xff, (size_t)W * 4); + memset(surface.pixels, 0xff, surface.size); fill(0, 0, W, 60, 0, 0, 0); fill(0, 0, 60, H, 0, 0, 0); fill(W - 260, H - 260, 200, 200, 0, 0, 0); diff --git a/tests/test_portable_helpers.c b/tests/test_portable_helpers.c new file mode 100644 index 0000000..ec8fdc6 --- /dev/null +++ b/tests/test_portable_helpers.c @@ -0,0 +1,209 @@ +#include "input_names.h" +#include "pen_transform.h" +#include "quill_surface.h" + +#include +#include +#include +#include + +static struct quill_surface make_test_surface(unsigned char *pixels, + size_t allocation_size, + int width, int height, + int stride, int format) { + int bytes_per_pixel; + size_t logical_size; + assert(pixels); + assert(quill_surface_measure_layout(width, height, stride, format, + &bytes_per_pixel, + &logical_size) == 0); + assert(logical_size <= allocation_size); + const struct quill_display_profile profile = + quill_display_profile_for_geometry(width, height); + const struct quill_surface surface = { + pixels, width, height, stride, format, bytes_per_pixel, logical_size, + profile.model, profile.capabilities, profile.pen_orientation, + }; + assert(quill_surface_is_coherent(&surface)); + return surface; +} + +static void verify_input_names(void) { + assert(quill_input_name_matches("Wacom I2C Digitizer", + QUILL_INPUT_ROLE_PEN)); + assert(quill_input_name_matches("reMarkable Marker", + QUILL_INPUT_ROLE_PEN)); + assert(quill_input_name_matches("SNVS power key", + QUILL_INPUT_ROLE_POWER)); + assert(quill_input_name_matches("30370000.snvs:snvs-powerkey", + QUILL_INPUT_ROLE_POWER)); + assert(quill_input_name_matches("pt_mt", QUILL_INPUT_ROLE_TOUCH)); + assert(quill_input_name_matches("Capacitive TouchScreen", + QUILL_INPUT_ROLE_TOUCH)); + assert(!quill_input_name_matches("pt_mt", QUILL_INPUT_ROLE_POWER)); + assert(!quill_input_name_matches("SNVS power key", QUILL_INPUT_ROLE_PEN)); +} + +static void verify_rm2_pen_transform(void) { + struct quill_pen_transform transform; + assert(quill_pen_transform_init(&transform, 0, 20966, 0, 15725, + QUILL_RM2_WIDTH, QUILL_RM2_HEIGHT, + QUILL_PEN_ROTATE_270) == 0); + assert(transform.orientation == QUILL_PEN_ROTATE_270); + + int x, y; + quill_map_pen(&transform, 0, 0, &x, &y); + assert(x == 0 && y == QUILL_RM2_HEIGHT - 1); + quill_map_pen(&transform, 20966, 15725, &x, &y); + assert(x == QUILL_RM2_WIDTH - 1 && y == 0); + quill_map_pen(&transform, 0, 15725, &x, &y); + assert(x == QUILL_RM2_WIDTH - 1 && y == QUILL_RM2_HEIGHT - 1); + quill_map_pen(&transform, 20966, 0, &x, &y); + assert(x == 0 && y == 0); +} + +static void verify_paper_pro_pen_transform(void) { + struct quill_pen_transform transform; + assert(quill_pen_transform_init(&transform, 0, 11180, 0, 15340, + QUILL_PAPER_PRO_WIDTH, + QUILL_PAPER_PRO_HEIGHT, + QUILL_PEN_DIRECT) == 0); + assert(transform.orientation == QUILL_PEN_DIRECT); + + int x, y; + quill_map_pen(&transform, 0, 0, &x, &y); + assert(x == 0 && y == 0); + quill_map_pen(&transform, 11180, 15340, &x, &y); + assert(x == QUILL_PAPER_PRO_WIDTH - 1 && + y == QUILL_PAPER_PRO_HEIGHT - 1); + quill_map_pen(&transform, -1, 20000, &x, &y); + assert(x == 0 && y == QUILL_PAPER_PRO_HEIGHT - 1); +} + +static void verify_pen_transform_edges(void) { + struct quill_pen_transform transform; + assert(quill_pen_transform_init(&transform, -100, 100, 20, 220, + 11, 21, QUILL_PEN_DIRECT) == 0); + int x, y; + quill_map_pen(&transform, 0, 120, &x, &y); + assert(x == 5 && y == 10); + + assert(quill_pen_transform_init(&transform, INT_MIN, INT_MAX, + INT_MIN, INT_MAX, 2, 2, + QUILL_PEN_DIRECT) == 0); + quill_map_pen(&transform, INT_MAX, INT_MIN, &x, &y); + assert(x == 1 && y == 0); + + assert(quill_pen_transform_init(&transform, 1, 1, 0, 1, 10, 10, + QUILL_PEN_DIRECT) == -1); + assert(quill_pen_transform_init(&transform, 1, 0, 0, 1, 10, 10, + QUILL_PEN_DIRECT) == -1); + assert(quill_pen_transform_init(&transform, 0, 1, 0, 1, 0, 10, + QUILL_PEN_DIRECT) == -1); + assert(quill_pen_transform_init(&transform, 0, 1, 0, 1, 10, 10, + QUILL_PEN_ORIENTATION_UNKNOWN) == -1); +} + +static void verify_pixel_formats(void) { + unsigned char pixel[4]; + memset(pixel, 0xa5, sizeof pixel); + assert(quill_store_gray(pixel, -1, 0x33) != 0); + for (size_t i = 0; i < sizeof pixel; ++i) assert(pixel[i] == 0xa5); + + unsigned char red, green, blue; + assert(quill_store_rgb(pixel, QUILL_FORMAT_RGB32, 0x12, 0x34, 0x56) == 0); + assert(quill_load_rgb(pixel, QUILL_FORMAT_RGB32, + &red, &green, &blue) == 0); + assert(red == 0x12 && green == 0x34 && blue == 0x56); + + assert(quill_store_rgb(pixel, QUILL_FORMAT_RGB16, 0xff, 0x80, 0x00) == 0); + assert(quill_load_rgb(pixel, QUILL_FORMAT_RGB16, + &red, &green, &blue) == 0); + assert(red == 0xff && green == 0x82 && blue == 0x00); + + red = green = blue = 0xa5; + assert(quill_load_rgb(pixel, -1, &red, &green, &blue) != 0); + assert(red == 0xa5 && green == 0xa5 && blue == 0xa5); + assert(quill_store_rgb(NULL, QUILL_FORMAT_RGB32, 0, 0, 0) != 0); +} + +static void verify_surface_profiles(void) { + struct quill_surface surface; + + const int rm2_stride = QUILL_RM2_WIDTH * 2; + const size_t rm2_capacity = (size_t)rm2_stride * QUILL_RM2_HEIGHT; + unsigned char *rm2_pixels = calloc(1, rm2_capacity); + assert(rm2_pixels); + surface = make_test_surface(rm2_pixels, rm2_capacity, + QUILL_RM2_WIDTH, QUILL_RM2_HEIGHT, + rm2_stride, QUILL_FORMAT_RGB16); + assert(surface.model == QUILL_MODEL_REMARKABLE_2); + assert(surface.capabilities == 0); + assert(surface.pen_orientation == QUILL_PEN_ROTATE_270); + assert(surface.bytes_per_pixel == 2); + assert(surface.size == + (size_t)(QUILL_RM2_WIDTH * 2) * QUILL_RM2_HEIGHT); + assert(quill_surface_is_coherent(&surface)); + + struct quill_surface inconsistent = surface; + inconsistent.bytes_per_pixel = 4; + assert(!quill_surface_is_coherent(&inconsistent)); + inconsistent = surface; + inconsistent.model = QUILL_MODEL_PAPER_PRO; + assert(!quill_surface_is_coherent(&inconsistent)); + inconsistent = surface; + inconsistent.capabilities = QUILL_CAPABILITY_COLOR; + assert(!quill_surface_is_coherent(&inconsistent)); + inconsistent = surface; + inconsistent.pen_orientation = QUILL_PEN_DIRECT; + assert(!quill_surface_is_coherent(&inconsistent)); + free(rm2_pixels); + + const int paper_pro_stride = QUILL_PAPER_PRO_WIDTH * 4; + const size_t paper_pro_capacity = + (size_t)paper_pro_stride * QUILL_PAPER_PRO_HEIGHT; + unsigned char *paper_pro_pixels = calloc(1, paper_pro_capacity); + assert(paper_pro_pixels); + surface = make_test_surface(paper_pro_pixels, paper_pro_capacity, + QUILL_PAPER_PRO_WIDTH, + QUILL_PAPER_PRO_HEIGHT, + paper_pro_stride, QUILL_FORMAT_RGB32); + assert(surface.model == QUILL_MODEL_PAPER_PRO); + assert(surface.capabilities & QUILL_CAPABILITY_COLOR); + assert(surface.pen_orientation == QUILL_PEN_DIRECT); + assert(quill_surface_is_coherent(&surface)); + free(paper_pro_pixels); + + unsigned char pixels[16] = {0}; + surface = make_test_surface(pixels, sizeof pixels, 2, 2, 8, + QUILL_FORMAT_RGB32); + assert(surface.model == QUILL_MODEL_UNKNOWN); + assert(surface.capabilities == 0); + assert(surface.pen_orientation == QUILL_PEN_ORIENTATION_UNKNOWN); + assert(quill_surface_store_rgb(&surface, 1, 1, 1, 2, 3) == 0); + unsigned char red, green, blue; + assert(quill_surface_load_rgb(&surface, 1, 1, + &red, &green, &blue) == 0); + assert(red == 1 && green == 2 && blue == 3); + assert(quill_surface_store_gray(&surface, -1, 0, 0) != 0); + assert(quill_surface_load_rgb(&surface, 2, 0, + &red, &green, &blue) != 0); + + unsigned char mono_pixels[8] = {0xff}; + surface = make_test_surface(mono_pixels, sizeof mono_pixels, 2, 2, 4, + QUILL_FORMAT_RGB16); + assert(surface.model == QUILL_MODEL_UNKNOWN); + assert(surface.capabilities == 0); + assert(surface.pen_orientation == QUILL_PEN_ORIENTATION_UNKNOWN); + assert(quill_surface_store_gray(&surface, 1, 1, 0) == 0); +} + +int main(void) { + verify_input_names(); + verify_rm2_pen_transform(); + verify_paper_pro_pen_transform(); + verify_pen_transform_edges(); + verify_pixel_formats(); + verify_surface_profiles(); + return 0; +} From 0f342f3e77af1a24fb511404b6cd2b38cefbe15b Mon Sep 17 00:00:00 2001 From: Jimmy Stridh Date: Wed, 19 Aug 2026 12:00:24 +0200 Subject: [PATCH 2/2] build: support reMarkable 2 SDK targets --- README.md | 73 +++++++++++++++++++++++++++++--------------- build.sh | 79 ++++++++++++++++++++++++++++++++++-------------- vendor/README.md | 20 +++++++++--- 3 files changed, 120 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index b8d39f5..5c86208 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,18 @@ # quill -Takeover display host for the reMarkable Paper Pro: stops xochitl and drives -the e-ink panel directly through the vendor waveform engine +Takeover display host for the reMarkable 2 and reMarkable Paper Pro: stops +xochitl and drives the e-ink panel through the vendor waveform engine (`libqsgepaper`'s `EPFramebuffer`), with raw evdev input. -This is the lowest-latency third-party drawing path that exists on this device -short of reverse-engineering the FPGA transport frame format. +Quill is designed as a low-latency third-party drawing path without +reverse-engineering the FPGA transport frame format. + +Verified targets: + +- **reMarkable 2** — ARMv7 hard-float, monochrome SWTCON backend, reMarkable OS + 3.27.3.0 with the 3.27.0.97 / 5.7.119 RM2 SDK. +- **reMarkable Paper Pro** — AArch64, monochrome and color ACEP backend, + reMarkable OS 3.27.3.0. Quill supports two verified update classes on the Paper Pro: @@ -15,14 +22,18 @@ Quill supports two verified update classes on the Paper Pro: region updates. Mode `1` was observed to collapse to grayscale even with `EPContentType::Color`. -Color is written through the captured RGB32 aux framebuffer (`B,G,R,0xFF`) and -pushed with `quill_swap_ex(..., QUILL_CONTENT_COLOR)` or the semantic wrappers -in `src/quill.h`. +On the Paper Pro, color is written through the captured RGB32 aux framebuffer +(`B,G,R,0xFF`) and pushed with `quill_swap_ex(..., QUILL_CONTENT_COLOR)` or the +semantic wrappers in `src/quill.h`. Monochrome clients also support the +vendor's RGB16 framebuffer variant with correctly packed RGB565 values. - `src/vendor_probe.cpp`, `src/vendor_probe.h` — clean-room Qt/vendor boundary and controlled framebuffer discovery -- `src/quill.h`, `src/quill_c.cpp` — C ABI over the engine (init/buffer/swap) - for C and Rust apps, including semantic mono/color swap wrappers +- `src/quill.h`, `src/quill_c.cpp`, `src/quill_surface.h` — C ABI over the + engine, its model-aware surface/profile, pixel helpers, and semantic + mono/color swap wrappers +- `src/input_names.h`, `src/pen_transform.h` — portable input-role matching and + model-aware pen-coordinate transforms - `src/scribble.c` — C1 milestone: pen-to-glass latency demo (exit: pen side-button in hover, 5-finger tap, power button, or SIGTERM) - `examples/vanishing_trail.c` — archived no-AI drawing experiment demonstrating live ink and delayed erasure @@ -31,7 +42,7 @@ in `src/quill.h`. `medium-map.gif` / `deviant-map.gif` are recordings of it running) - `src/image_demo.cpp` — render a PNG/JPEG/etc. image through Qt's QImage loader, scaled to the panel -- `src/color_probe.c` — full-screen RGB/CMY color path probe +- `src/color_probe.c` — Paper Pro full-screen RGB/CMY color path probe - `src/color_mode_compare.c` — side-by-side mode comparison (`1`, `3`, `4`, `5`) - `src/color_partial_probe.c` — small dirty-rect color add/erase/churn probe - `src/color_blend_probe.c` — software alpha, color mixing, and stacked partial @@ -39,27 +50,33 @@ in `src/quill.h`. - `src/image_anim_demo.cpp`, `src/gif_demo.cpp` — partial-update animation experiments (sprites over a still image; GIF playback) - `scripts/takeover.sh` — stop xochitl, run app, ALWAYS restore xochitl -- `build.sh` — cross-build against the ferrari SDK (~/rm-sdk-3.26) + - `vendor/libqsgepaper.so` pulled from the device (the SDK comes from - reMarkable's developer program; build.sh expects it unpacked at - `~/rm-sdk-3.26` and the tablet reachable over ssh to fetch the vendor lib) +- `build.sh` — cross-build against the matching official RM2 or ferrari SDK; + current SDKs include the target's `libqsgepaper.so`, with `vendor/` retained + as a fallback for older SDKs Exit the demos: power button, 5-finger tap, or SIGTERM. +The RM2 target currently builds the core adapter and device probes only. The +interactive demos in this branch retain their Paper Pro input and pixel +assumptions. + ## Build and verification +The official SDK requires Linux. Set `QUILL_SDK` to the installed SDK root. + ```sh -./build.sh # cross-build with the reMarkable SDK -./scripts/test-host.sh # clipping tests with ASan and UBSan +QUILL_SDK=/path/to/rm2-sdk ./build.sh rm2 +# or: QUILL_SDK=/path/to/paper-pro-sdk ./build.sh ferrari + +./scripts/test-host.sh # clipping and portable profile tests ./scripts/check-release.sh # ABI and redistribution checks -./scripts/device-suite.sh # on-device acceptance suite (run ON the tablet, +./scripts/device-suite.sh # Paper Pro on-device suite (run ON the tablet, # detached — see the header comment) ``` -Takeover sessions hold a kernel wakelock (`/sys/power/wake_lock`): autosleep -is active on this firmware and a mid-session suspend resumes into a second -xochitl instance fighting the app for the panel. `scripts/takeover.sh` -handles this; see `docs/device-test-2026-07-12.md` for the incident record. +On Paper Pro firmware exposing legacy kernel wakelocks, `scripts/takeover.sh` +holds one while xochitl is stopped. The RM2 firmware tested here does not +expose that interface. Framebuffer selection and display behavior must also be tested on the target hardware. If initialization reports multiple valid candidates, validate them @@ -72,8 +89,16 @@ proprietary and is neither covered by that license nor redistributed here. ## C ABI policy -Use `quill_buffer()` to write pixels and one of these swap calls to push dirty -rectangles: +After `quill_init()` succeeds, `quill_get_surface()` returns the canonical +display view: vendor-owned pixels together with coherent geometry, stride, +format, model, capabilities, and pen orientation. The scalar +`quill_buffer()`/geometry getters remain for ABI compatibility. Pixel helpers +in `src/quill_surface.h` support both RGB32 and RGB16 surfaces. + +Unknown panel geometry initializes as `QUILL_MODEL_UNKNOWN` with no optional +capabilities or pen orientation, keeping generic monochrome discovery usable +without enabling model-specific behavior. Write pixels, then use one of these +swap calls to push dirty rectangles: ```c quill_swap_mono_fast(x, y, w, h); // Mono, mode 0, full=0: live ink @@ -89,7 +114,7 @@ work: the vendor backend may promote `CompleteRefresh` to a whole-panel update even when the supplied rectangle is small. `quill_swap_mono_fast()`, `quill_swap_mono_quality()`, and `quill_swap_color()` are partial by contract. -Empirical Paper Pro findings: +Empirical Paper Pro findings (the RM2 is monochrome): - `QUILL_CONTENT_COLOR + mode 3/4/5` renders color. - `QUILL_CONTENT_COLOR + mode 1` renders grayscale/mono. diff --git a/build.sh b/build.sh index 1a5b9a8..8383c14 100755 --- a/build.sh +++ b/build.sh @@ -1,20 +1,46 @@ #!/bin/bash -# Cross-build quill against the ferrari SDK (OS 3.26 toolchain). -# Prereq: ~/rm-sdk-3.26 installed; libqsgepaper.so pulled from the device into ./vendor/. +# Cross-build quill against a matching reMarkable SDK. +# Usage: ./build.sh [ferrari|rm2] set -euo pipefail cd "$(dirname "$0")" -SDK=~/rm-sdk-3.26 -ENV=$(ls $SDK/environment-setup-* | head -n1) +TARGET=${1:-${QUILL_TARGET:-ferrari}} +case "$TARGET" in + ferrari) DEFAULT_SDK="$HOME/rm-sdk-3.26"; EXPECTED_ARCH=aarch64 ;; + rm2) DEFAULT_SDK="$HOME/rm-sdk-3.27-rm2"; EXPECTED_ARCH=arm ;; + *) echo "unsupported target: $TARGET (expected ferrari or rm2)" >&2; exit 2 ;; +esac + +SDK=${QUILL_SDK:-$DEFAULT_SDK} +ENV= +for candidate in "$SDK"/environment-setup-*; do + if [ -f "$candidate" ]; then + ENV=$candidate + break + fi +done +if [ -z "$ENV" ]; then + echo "no SDK environment found under $SDK" >&2 + exit 2 +fi # The SDK env script sets CC/CXX with target flags and $SDKTARGETSYSROOT. # It refuses to load when LD_LIBRARY_PATH is set. unset LD_LIBRARY_PATH source "$ENV" +if [ "${OECORE_TARGET_ARCH:-}" != "$EXPECTED_ARCH" ]; then + echo "SDK target architecture '${OECORE_TARGET_ARCH:-unknown}' does not match $TARGET ($EXPECTED_ARCH)" >&2 + exit 2 +fi mkdir -p build vendor -if [ ! -f vendor/libqsgepaper.so ]; then - echo "pulling libqsgepaper.so from device..." - scp -O rm:/usr/lib/plugins/scenegraph/libqsgepaper.so vendor/ +SDK_VENDOR_DIR="$SDKTARGETSYSROOT/usr/lib/plugins/scenegraph" +if [ -f "$SDK_VENDOR_DIR/libqsgepaper.so" ]; then + VENDOR_DIR=$SDK_VENDOR_DIR +elif [ -f vendor/libqsgepaper.so ]; then + VENDOR_DIR=vendor +else + echo "libqsgepaper.so was not found in the target SDK or vendor/" >&2 + exit 2 fi QTINC="$SDKTARGETSYSROOT/usr/include" @@ -24,13 +50,13 @@ QTINC="$SDKTARGETSYSROOT/usr/include" $CXX -fPIC -shared -O2 -std=c++17 \ -I "$QTINC" -I "$QTINC/QtCore" -I "$QTINC/QtGui" \ src/vendor_probe.cpp src/quill_c.cpp \ - -L vendor -lqsgepaper -lQt6Gui -lQt6Core -ldl \ + -L "$VENDOR_DIR" -lqsgepaper -lQt6Gui -lQt6Core -ldl \ -o build/libquill.so # Non-destructive on-device initialization/ABI probe. $CC -O2 -I src tests/device_init_probe.c \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/device_init_probe @@ -38,7 +64,7 @@ $CC -O2 -I src tests/device_init_probe.c \ # Small self-restoring on-device swap/ABI probe. $CC -O2 -I src tests/device_swap_probe.c \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/device_swap_probe @@ -46,7 +72,7 @@ $CC -O2 -I src tests/device_swap_probe.c \ # Automated on-device clipping, mode, stress, and refresh acceptance probe. $CC -O2 -I src tests/device_acceptance_probe.c \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/device_acceptance_probe @@ -54,7 +80,7 @@ $CC -O2 -I src tests/device_acceptance_probe.c \ # Human-verified visual acceptance probe (orientation, channel order, modes). $CC -O2 -I src tests/device_visual_probe.c \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/device_visual_probe @@ -62,15 +88,22 @@ $CC -O2 -I src tests/device_visual_probe.c \ # Signal-termination lifecycle probe without input or display modifications. $CC -O2 -I src tests/device_term_probe.c \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/device_term_probe +# The demos below still use Paper Pro-specific input and pixel assumptions. +# Keep the RM2 target limited to the portable adapter and device probes. +if [ "$TARGET" = rm2 ]; then + echo "built: build/libquill.so build/device_init_probe build/device_swap_probe build/device_acceptance_probe build/device_visual_probe build/device_term_probe" + exit 0 +fi + # scribble: the C1 latency demo. $CC -O2 src/scribble.c \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/scribble @@ -78,7 +111,7 @@ $CC -O2 src/scribble.c \ # map_demo: static full-screen map + tiny partial-update footsteps. $CC -O2 src/map_demo.c \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/map_demo @@ -88,7 +121,7 @@ $CXX -O2 \ -I "$QTINC" -I "$QTINC/QtCore" -I "$QTINC/QtGui" \ src/image_demo.cpp \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/image_demo @@ -96,7 +129,7 @@ $CXX -O2 \ # color_probe: experimental EPContentType::Color / ACEP path probe. $CC -O2 src/color_probe.c \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/color_probe @@ -104,7 +137,7 @@ $CC -O2 src/color_probe.c \ # color_mode_compare: same pattern, side-by-side, refreshed with different modes. $CC -O2 src/color_mode_compare.c \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/color_mode_compare @@ -112,7 +145,7 @@ $CC -O2 src/color_mode_compare.c \ # color_partial_probe: small dirty-rect color additions/erasures/churn. $CC -O2 src/color_partial_probe.c \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/color_partial_probe @@ -120,7 +153,7 @@ $CC -O2 src/color_partial_probe.c \ # color_blend_probe: software alpha blending, color mixing, and stacking. $CC -O2 src/color_blend_probe.c \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/color_blend_probe @@ -130,7 +163,7 @@ $CXX -O2 \ -I "$QTINC" -I "$QTINC/QtCore" -I "$QTINC/QtGui" \ src/color_image_demo.cpp \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/color_image_demo @@ -140,7 +173,7 @@ $CXX -O2 \ -I "$QTINC" -I "$QTINC/QtCore" -I "$QTINC/QtGui" \ src/image_anim_demo.cpp \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/image_anim_demo @@ -150,7 +183,7 @@ $CXX -O2 \ -I "$QTINC" -I "$QTINC/QtCore" -I "$QTINC/QtGui" \ src/gif_demo.cpp \ -L build -lquill \ - -L vendor -lqsgepaper \ + -L "$VENDOR_DIR" -lqsgepaper \ -lQt6Gui -lQt6Core -lstdc++ \ -Wl,-rpath,/home/root/quill \ -o build/gif_demo diff --git a/vendor/README.md b/vendor/README.md index 7d93fbf..af62bf7 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -4,15 +4,25 @@ quill links against **`libqsgepaper.so`**, reMarkable's proprietary e-ink waveform engine. It is **not** included in this repo — it's reMarkable's copyrighted software, and redistributing it isn't ours to do. -You already have a copy on your own tablet. Copy it from there: +Current official reMarkable SDKs include the matching target library in their +sysroot, and `build.sh` uses that copy by default. Set `QUILL_SDK` to the SDK +root and select the tablet model: ```sh -scp root@10.11.99.1:/usr/lib/plugins/scenegraph/libqsgepaper.so vendor/ +QUILL_SDK=/path/to/rm-sdk ./build.sh rm2 +``` + +For an older SDK that does not contain the plugin, `build.sh` can use an +untracked personal copy in this directory. Copy it from the target tablet: + +```sh +KEY="/path/to/remarkable/private-key" +scp -i "$KEY" \ + root@10.11.99.1:/usr/lib/plugins/scenegraph/libqsgepaper.so vendor/ ``` (On some OS versions it lives elsewhere under `/usr/lib` — find it with `ssh root@10.11.99.1 'find /usr/lib -name libqsgepaper.so'`.) -That's a personal copy from a device you own, for building software that runs on -that same device — not redistribution. With the file in place, `./build.sh` -links against it. +That fallback remains a personal device copy and must not be committed or +redistributed.