From b8dce4fd63b3b57d26040bcf4b4a75221bf2a123 Mon Sep 17 00:00:00 2001 From: Jganenokk Date: Sat, 11 Jul 2026 15:45:39 +0700 Subject: [PATCH] =?UTF-8?q?opt:=20=D0=BB=D0=B5=D0=B3=D0=BE=D0=BD=D1=8C?= =?UTF-8?q?=D0=BA=D0=BE=20=D0=BD=D0=B0=D0=BE=D0=BF=D1=82=D0=B8=D0=BC=D0=B8?= =?UTF-8?q?=D0=B7=D0=B8=D0=B1=D1=83=D1=80=D0=BC=D0=B0=D0=BB=D0=B4=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B2=20rlottie=20=D1=81=D1=82=D0=B8=D0=BA=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=D1=85/=D1=8D=D0=BC=D0=BE=D0=B4=D0=B7=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/rlottie/rlottie_disk_cache.dart | 16 +++++- lib/core/media/rlottie/rlottie_engine.dart | 54 ++++++++++++------- lib/core/media/rlottie/rlottie_worker.dart | 36 ++++++------- lib/frontend/widgets/lottie_image.dart | 49 ++++++++++++----- 4 files changed, 103 insertions(+), 52 deletions(-) diff --git a/lib/core/media/rlottie/rlottie_disk_cache.dart b/lib/core/media/rlottie/rlottie_disk_cache.dart index 0756ab2..3022c11 100644 --- a/lib/core/media/rlottie/rlottie_disk_cache.dart +++ b/lib/core/media/rlottie/rlottie_disk_cache.dart @@ -29,7 +29,7 @@ class RlottieDiskCache { static final RlottieDiskCache instance = RlottieDiskCache._(); static const _magic = 0x4b524c46; - static const _version = 1; + static const _version = 2; static const int _maxBytes = 256 * 1024 * 1024; Directory? _dir; @@ -103,6 +103,13 @@ class RlottieDiskCache { for (var i = 0; i < frameCount; i++) { payload.setRange(i * frameBytes, (i + 1) * frameBytes, frames[i]); } + for (var i = frameCount - 1; i >= 1; i--) { + final cur = i * frameBytes; + final prev = (i - 1) * frameBytes; + for (var b = 0; b < frameBytes; b++) { + payload[cur + b] ^= payload[prev + b]; + } + } final compressed = gzip.encode(payload); final header = ByteData(28); header.setUint32(0, _magic); @@ -131,6 +138,13 @@ class RlottieDiskCache { final frameBytes = px * px * 4; final payload = Uint8List.fromList(gzip.decode(bytes.sublist(28))); if (payload.length != frameBytes * frameCount) return null; + for (var i = 1; i < frameCount; i++) { + final cur = i * frameBytes; + final prev = (i - 1) * frameBytes; + for (var b = 0; b < frameBytes; b++) { + payload[cur + b] ^= payload[prev + b]; + } + } final frames = []; for (var i = 0; i < frameCount; i++) { frames.add(Uint8List.sublistView( diff --git a/lib/core/media/rlottie/rlottie_engine.dart b/lib/core/media/rlottie/rlottie_engine.dart index 63089a8..151a8cb 100644 --- a/lib/core/media/rlottie/rlottie_engine.dart +++ b/lib/core/media/rlottie/rlottie_engine.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io' show Platform; import 'dart:isolate'; import 'dart:ui' as ui; @@ -81,7 +82,7 @@ class RlottieEngine { static String? debugLibraryPath; - static const int _maxBytes = 384 * 1024 * 1024; + static const int _maxBytes = 192 * 1024 * 1024; static const int _modelCacheBytes = 20 * 1024 * 1024; final Map _clips = {}; @@ -91,9 +92,10 @@ class RlottieEngine { int _totalBytes = 0; int _clock = 0; int _nextJobId = 1; + int _rrIndex = 0; bool? _available; - Future? _workerFuture; + Future>? _poolFuture; int _tick() => ++_clock; String _keyFor(String url, int px) => '$url@$px'; @@ -107,15 +109,23 @@ class RlottieEngine { }(); } - Future _worker() { - return _workerFuture ??= () async { + Future _worker() async { + final pool = await (_poolFuture ??= _spawnPool()); + return pool[_rrIndex++ % pool.length]; + } + + Future> _spawnPool() async { + final count = (Platform.numberOfProcessors - 1).clamp(1, 3); + final ports = []; + for (var i = 0; i < count; i++) { final receive = ReceivePort(); await Isolate.spawn(rlottieWorkerMain, receive.sendPort); final broadcast = receive.asBroadcastStream(); final port = await broadcast.first as SendPort; broadcast.listen(_onWorkerMessage); - return port; - }(); + ports.add(port); + } + return ports; } void _onWorkerMessage(dynamic message) { @@ -155,7 +165,7 @@ class RlottieEngine { bytes, frame.px, frame.px, - ui.PixelFormat.rgba8888, + ui.PixelFormat.bgra8888, (image) { job.clip._setFrame(frame.index, image); _totalBytes += frame.px * frame.px * 4; @@ -229,8 +239,13 @@ class RlottieEngine { if (inlineJson == null) { final disk = await RlottieDiskCache.instance.load(url, px); if (disk != null) { - final clip = await _clipFromDisk(key, px, disk); + final clip = RlottieClip(key: key, px: px) + ..frameCount = disk.frameCount + ..frameRate = disk.frameRate + ..durationMs = disk.durationMs; + clip._allocate(disk.frameCount); _clips[key] = clip; + unawaited(_decodeDiskProgressive(clip, disk)); return clip; } } @@ -255,26 +270,25 @@ class RlottieEngine { return completer.future; } - Future _clipFromDisk(String key, int px, DiskClip disk) async { - final clip = RlottieClip(key: key, px: px) - ..frameCount = disk.frameCount - ..frameRate = disk.frameRate - ..durationMs = disk.durationMs - ..complete = true; - clip._allocate(disk.frameCount); + Future _decodeDiskProgressive(RlottieClip clip, DiskClip disk) async { for (var i = 0; i < disk.frameCount; i++) { - final image = await _decode(disk.frames[i], px); + if (!identical(_clips[clip.key], clip)) return; + final image = await _decode(disk.frames[i], clip.px); + if (!identical(_clips[clip.key], clip)) { + image.dispose(); + return; + } clip._setFrame(i, image); - _totalBytes += px * px * 4; + _totalBytes += clip.px * clip.px * 4; } + clip.complete = true; _evictIfNeeded(); - return clip; } - Future _decode(Uint8List rgba, int px) { + Future _decode(Uint8List bgra, int px) { final completer = Completer(); ui.decodeImageFromPixels( - rgba, px, px, ui.PixelFormat.rgba8888, completer.complete); + bgra, px, px, ui.PixelFormat.bgra8888, completer.complete); return completer.future; } diff --git a/lib/core/media/rlottie/rlottie_worker.dart b/lib/core/media/rlottie/rlottie_worker.dart index 21961d1..b498096 100644 --- a/lib/core/media/rlottie/rlottie_worker.dart +++ b/lib/core/media/rlottie/rlottie_worker.dart @@ -66,6 +66,8 @@ class CancelJob { final int jobId; } +const double _maxCacheFps = 30.0; + void rlottieWorkerMain(SendPort toMain) { final port = ReceivePort(); toMain.send(port.sendPort); @@ -104,24 +106,33 @@ void rlottieWorkerMain(SendPort toMain) { final total = rl.totalFrame(anim); final fps = rl.frameRate(anim); final durationMs = fps <= 0 ? 1000 : (total / fps * 1000).round(); + + var outCount = total; + if (fps > _maxCacheFps && total > 1) { + outCount = (durationMs / 1000.0 * _maxCacheFps).round().clamp(2, total); + } + final outFps = durationMs <= 0 ? fps : outCount * 1000.0 / durationMs; toMain.send(ClipMeta( jobId: job.jobId, - totalFrame: total, - frameRate: fps, + totalFrame: outCount, + frameRate: outFps, durationMs: durationMs, )); final px = job.px; final buffer = calloc(px * px); + final byteView = buffer.cast().asTypedList(px * px * 4); try { - for (var i = 0; i < total; i++) { + for (var i = 0; i < outCount; i++) { if (cancelled.contains(job.jobId)) break; - rl.render(anim, i, buffer, px); - final rgba = _bgraToRgba(buffer, px * px); + final src = outCount == total + ? i + : (i * (total - 1) / (outCount - 1)).round().clamp(0, total - 1); + rl.render(anim, src, buffer, px); toMain.send(RenderedFrame( jobId: job.jobId, index: i, - data: TransferableTypedData.fromList([rgba]), + data: TransferableTypedData.fromList([Uint8List.fromList(byteView)]), px: px, )); } @@ -138,16 +149,3 @@ void rlottieWorkerMain(SendPort toMain) { }); } -Uint8List _bgraToRgba(Pointer buffer, int pixels) { - final src = buffer.asTypedList(pixels); - final out = Uint8List(pixels * 4); - for (var i = 0; i < pixels; i++) { - final v = src[i]; - final o = i * 4; - out[o] = (v >> 16) & 0xff; - out[o + 1] = (v >> 8) & 0xff; - out[o + 2] = v & 0xff; - out[o + 3] = (v >> 24) & 0xff; - } - return out; -} diff --git a/lib/frontend/widgets/lottie_image.dart b/lib/frontend/widgets/lottie_image.dart index b356466..1219125 100644 --- a/lib/frontend/widgets/lottie_image.dart +++ b/lib/frontend/widgets/lottie_image.dart @@ -84,6 +84,8 @@ class LottiePlayer extends StatefulWidget { class _LottiePlayerState extends State with SingleTickerProviderStateMixin { static const int _leadFrames = 6; + static const double _slowSpeed = 0.5; + static const double _rampMs = 200.0; final ValueNotifier _frameIndex = ValueNotifier(0); late final Ticker _ticker; @@ -94,6 +96,11 @@ class _LottiePlayerState extends State bool _started = false; bool _showedFrames = false; + double _speed = 1.0; + double _targetSpeed = 1.0; + double _playheadMs = 0.0; + double? _lastElapsedMs; + bool get _isScrolling => _scrollState?.value ?? false; bool get _canLoad => !_isScrolling && !LottieLoadGovernor.instance.throttled.value; @@ -125,6 +132,10 @@ class _LottiePlayerState extends State _releaseClip(); _started = false; _showedFrames = false; + _playheadMs = 0.0; + _speed = 1.0; + _targetSpeed = _isScrolling ? _slowSpeed : 1.0; + _lastElapsedMs = null; } } @@ -152,20 +163,31 @@ class _LottiePlayerState extends State if (clip == null || clip.frameCount <= 1) return; final periodMs = clip.durationMs; if (periodMs <= 0) return; - final t = (elapsed.inMilliseconds % periodMs) / periodMs; - final index = (t * (clip.frameCount - 1)).round().clamp( - 0, - clip.frameCount - 1, - ); + + final nowMs = elapsed.inMicroseconds / 1000.0; + final last = _lastElapsedMs; + _lastElapsedMs = nowMs; + if (last == null) return; + var dt = nowMs - last; + if (dt < 0) dt = 0; + if (dt > 64) dt = 64; + + if (_speed != _targetSpeed) { + final step = dt / _rampMs * (1.0 - _slowSpeed); + final diff = _targetSpeed - _speed; + _speed = diff.abs() <= step ? _targetSpeed : _speed + step * diff.sign; + } + + _playheadMs = (_playheadMs + dt * _speed) % periodMs; + final t = _playheadMs / periodMs; + final index = + (t * (clip.frameCount - 1)).round().clamp(0, clip.frameCount - 1); if (index != _frameIndex.value) _frameIndex.value = index; } void _onGateChanged() { if (!mounted) return; - if (_isScrolling) { - if (_ticker.isActive) _ticker.stop(); - return; - } + _targetSpeed = _isScrolling ? _slowSpeed : 1.0; final clip = _clip; if (clip != null) { _maybeStartTicker(clip); @@ -182,15 +204,18 @@ class _LottiePlayerState extends State } void _maybeStartTicker(RlottieClip clip) { - if (_isScrolling || clip.frameCount <= 1) return; + if (clip.frameCount <= 1) return; final lead = clip.frameCount < _leadFrames ? clip.frameCount : _leadFrames; - if (clip.ready.value >= lead && !_ticker.isActive) _ticker.start(); + if (clip.ready.value >= lead && !_ticker.isActive) { + _lastElapsedMs = null; + _ticker.start(); + } } void _ensure(double box) { if (_clip != null) return; final dpr = MediaQuery.devicePixelRatioOf(context); - final raw = (box * dpr.clamp(1.0, 2.0)).clamp(96.0, 512.0); + final raw = (box * dpr.clamp(1.0, 2.0)).clamp(96.0, 384.0); _px = (raw / 32).ceil() * 32; if (_started || !_canLoad) return; _startLoad();