From 1e4388771fa5349462afe697ee9c745f96fa672c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 24 Sep 2026 14:16:46 +0800 Subject: [PATCH] cursor: wait for a native cursor at the raster asked for, not any raster A shape let its pixels go once it had a native cursor at some raster. After a zoom, or on a display of another scale, an animation's frames had native cursors at the old raster only; each was fetched again at the new one, and a restore that finished after the next switch let its pixels go at once, so the frames never got a native cursor at the new raster and the cursor stayed on the one shown before. A lookup that finds no native cursor at the raster asked for now counts the shape as waiting for one, so a late restore keeps its pixels until it has one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab --- flutter/lib/models/model.dart | 8 ++++++-- flutter/test/cursor_replay_test.dart | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index a4527eda6..d0c7bc11c 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -3617,8 +3617,12 @@ class CursorModel with ChangeNotifier { static int _nextKeyScope = 0; final int _keyScope = _nextKeyScope++; - String nativeKey(CursorData cache, double scale) => - '${_keyScope}_${cache.updateGetKey(scale)}'; + String nativeKey(CursorData cache, double scale) { + final key = '${_keyScope}_${cache.updateGetKey(scale)}'; + // A native cursor at another raster does not hold the pixels this one is made from. + if (!_cacheKeys.contains(key)) _nativeIds.remove(cache.id); + return key; + } // Native cursors stay for the session, one per raster of each shape, and only [clear] // deletes them, as before; unlike the painted images they have no LRU, for now. A delete diff --git a/flutter/test/cursor_replay_test.dart b/flutter/test/cursor_replay_test.dart index 552b16cbb..1bccddd7d 100644 --- a/flutter/test/cursor_replay_test.dart +++ b/flutter/test/cursor_replay_test.dart @@ -239,6 +239,34 @@ void main() { expect(registered.where((key) => key.contains('_B_')), isNotEmpty); }); + test('a zoom still gets native cursors for shapes that keep decoding late', + () async { + final cursor = ffi.cursorModel; + // An animation whose frames have native cursors at 1.0 only. + for (final (id, seed) in [('A', 1), ('B', 2)]) { + await _feed(ffi, id, size: 32, seed: seed); + buildCursorOfCache(cursor, 1.0, cursor.cache); + await _settle(); + } + // At 0.5 each frame is fetched again, and each fetch answers after the next switch. + ffi.cursor.holdFetches = true; + for (var i = 0; i < 6; i++) { + _select(ffi, i.isEven ? 'A' : 'B'); + ffi.cursor.answerFetches(); + await _settle(); + buildCursorOfCache(cursor, 0.5, cursor.cache); + await _settle(); + } + expect( + registered + .where((key) => key.contains('_A_') && key.endsWith('_16_16')), + isNotEmpty); + expect( + registered + .where((key) => key.contains('_B_') && key.endsWith('_16_16')), + isNotEmpty); + }); + test('shapes waiting for a native cursor keep their pixels, within the limit', () async { const max = CursorModel.kRecentShapes;