Skip to content

Fix integer division order in FBController::markedUpdate (partial updates collapse to full repaints)#63

Open
jmiehau wants to merge 2 commits into
asivery:masterfrom
jmiehau:fix-partial-rect-scaling
Open

Fix integer division order in FBController::markedUpdate (partial updates collapse to full repaints)#63
jmiehau wants to merge 2 commits into
asivery:masterfrom
jmiehau:fix-partial-rect-scaling

Conversation

@jmiehau

@jmiehau jmiehau commented Jul 10, 2026

Copy link
Copy Markdown

Fixes #33.

Impact

On a reMarkable 2 this fix dramatically improves writing fluidity and latency for windowed qtfb clients: sustained handwriting stops lagging behind the pen. Before the fix, a stylus app streaming small ink rects hits a ceiling of roughly 70 updates/s, because every partial update degenerates into a full-item repaint; with the fix, partial repaints land as intended and the queue of pending ink disappears. Field-tested on firmware 3.27.3.0 — the difference is immediately visible when writing fast.

The bug

FBController::markedUpdate scales the incoming partial rect with integer division before multiplying:

(rect.x() / image->width()) * this->width()

For any rect.x() < image->width() this truncates to 0, so every partial update becomes a null QRect — and a null rect tells Qt to repaint the whole item. Partial updates still work, but each one costs a full-item repaint.

The fix

Multiply before dividing — the same operand order convertPointToQTFBPixels already uses a few lines below — and round the far edge up (qCeil) so a scaled rect always covers the source region instead of clipping its border. A null input rect still yields a null rect, so UPDATE_ALL semantics are unchanged. Gestures, full refreshes and scaling behave exactly as before.

🤖 Generated with Claude Code

Scaling a partial rect with integer division before multiplying truncates
every coordinate to zero, so partial updates collapse to a null QRect and
Qt repaints the whole item on each one. Multiply first (the same operand
order convertPointToQTFBPixels already uses) and round the far edge up so
a scaled rect always covers the source region. A null input rect still
yields a null rect, so UPDATE_ALL semantics are unchanged.

Fixes asivery#33

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@icetingyu

icetingyu commented Jul 11, 2026

Copy link
Copy Markdown

Confirmed on a reMarkable 2, firmware 3.28.0.162: without this fix every UPDATE_PARTIAL degenerates into a whole-item repaint and sustained handwriting in a windowed qtfb app lags far behind the pen; with it, ink lands as native-feeling partial updates. 👍

One heads-up this fix newly exposes: QQuickPaintedItem encodes "whole item" as a null dirty rect, and a later partial update() in the same frame ORs into it and silently narrows the full-repaint request (stale pixels outside the partial rect). Before this PR that path was unreachable — every rect collapsed to null. Note that UPDATE_ALL reaches markedUpdate() with a default-constructed (null) QRect, and with allowScaling on it enters the scaled branch and comes out as QRect(0,0,0,0) — so the guard has to run before the scaling math:

void FBController::markedUpdate(const QRect &rect) {
    isMidPaint = true;
    if (rect.isNull()) {
        // whole-item request: pass an explicit full rect so later partials union into it
        update(QRect(0, 0, qCeil(width()), qCeil(height())));
    } else if (_allowScaling && image) {
        // ... this PR's scaled-rect math ...
    } else {
        update(rect);
    }
}

(Also opened #64 with a complementary paint() change that blits only the dirty region.)

UPDATE_ALL reaches markedUpdate() as a default-constructed (null) QRect.
Passing it through the scaled-rect math would turn it into an empty
QRect(0,0,0,0), and forwarding a null to QQuickPaintedItem::update()
relies on the 'null means whole item' encoding, which a later partial
update() in the same frame can union with and silently narrow. Request
the full item explicitly instead, before any scaling.

Spotted on-device by @icetingyu (see PR discussion).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jmiehau

jmiehau commented Jul 18, 2026

Copy link
Copy Markdown
Author

Thanks a lot for the on-device confirmation and for catching the null-rect edge case, @icetingyu — you're right on both counts: the null must be guarded before the scaling math, and forwarding it relies on the "null means whole item" encoding that a later partial update() in the same frame can silently narrow.

Pushed exactly that guard: rect.isNull() now requests the full item explicitly (QRect(0, 0, qCeil(width()), qCeil(height()))) before any scaling. Builds clean for ARM32 with the official toolchain; I'll report back after it has run on our reMarkable 2 field setup (a windowed qtfb app under sustained handwriting).

#64 looks like a great complement — happy to rebase on top of it if the maintainer prefers to land that first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

shim quirks

2 participants