From 248649678a747085a62271c57770c8c5c85b109e Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Tue, 1 Sep 2026 21:38:14 +0800 Subject: [PATCH 1/2] Fix Linux external texture integration --- .github/workflows/run-flutter-builds.yml | 10 +++++ ...d_channel_platform_texture_descriptor.dart | 3 ++ .../src/native_texture_surface_manager.dart | 1 + .../src/platform_texture_descriptor.dart | 7 +++ ...rm_texture_descriptor_registry_native.dart | 16 ++++--- .../texture_widget_builder.dart | 9 +++- .../lib/src/widgets/src/viewer_widget.dart | 2 - .../lib/src/widgets/widgets.dart | 1 + .../thermion_flutter/linux/CMakeLists.txt | 9 ++-- ...hermion_widget_resize_transition_test.dart | 43 +++++++++++++++++++ 10 files changed, 88 insertions(+), 13 deletions(-) diff --git a/.github/workflows/run-flutter-builds.yml b/.github/workflows/run-flutter-builds.yml index bcd64586a..c3f85b2bd 100644 --- a/.github/workflows/run-flutter-builds.yml +++ b/.github/workflows/run-flutter-builds.yml @@ -124,6 +124,16 @@ jobs: flutter pub get flutter test + # The checked-in example overrides are relative monorepo paths. Real + # consumers of a Git or hosted package receive absolute file:/// root + # URIs in package_config.json, so exercise that resolution path too. + - name: Exercise absolute package URIs (Linux) + if: matrix.name == 'Linux' + shell: bash + run: | + sed -i "s|path: ../../../thermion_dart|path: ${GITHUB_WORKSPACE}/thermion_dart|" quickstart/pubspec.yaml + sed -i "s|path: ../../../thermion_dart|path: ${GITHUB_WORKSPACE}/thermion_dart|" picking/pubspec.yaml + - name: Build quickstart (Linux) if: matrix.name == 'Linux' run: cd quickstart && flutter pub get && flutter build linux diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/method_channel_platform_texture_descriptor.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/method_channel_platform_texture_descriptor.dart index d35ce6ef0..ccbd8b835 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/method_channel_platform_texture_descriptor.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/method_channel_platform_texture_descriptor.dart @@ -12,6 +12,7 @@ class MethodChannelPlatformTextureDescriptor extends PlatformTextureDescriptor { required super.windowHandle, required super.width, required super.height, + super.flipVertically, bool deferred = false, }) : _deferred = deferred; @@ -38,6 +39,7 @@ class MethodChannelPlatformTextureDescriptor extends PlatformTextureDescriptor { int width, int height, { bool deferred = false, + bool flipVertically = false, }) async { final allocation = await allocateMethodChannelTexture( channel, @@ -52,6 +54,7 @@ class MethodChannelPlatformTextureDescriptor extends PlatformTextureDescriptor { width: width, height: height, deferred: deferred, + flipVertically: flipVertically, ); } diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/native_texture_surface_manager.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/native_texture_surface_manager.dart index 1e4b26348..c075f758c 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/native_texture_surface_manager.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/native_texture_surface_manager.dart @@ -62,6 +62,7 @@ class NativeTextureSurfaceManager { resumeRenderingIfReady: lifecycle.resumeRenderingIfReady, runTextureMutation: lifecycle.duringTextureMutation, androidTextureSource: () => options().androidTextureSource, + flipLinuxTextureVertically: () => options().backend != Backend.VULKAN, ); static final _logger = Logger('NativeTextureSurfaceManager'); diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor.dart index d2544070b..715258ed9 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor.dart @@ -11,6 +11,12 @@ abstract class PlatformTextureDescriptor { int height; + /// Whether Flutter must invert this texture vertically when presenting it. + /// + /// OpenGL render targets use a bottom-left origin, while Flutter's widget + /// coordinate system uses a top-left origin. + final bool flipVertically; + View? _boundView; /// The (possibly null) view this descriptor is bound to @@ -28,6 +34,7 @@ abstract class PlatformTextureDescriptor { required this.width, required this.height, this.windowHandle, + this.flipVertically = false, }); /// Descriptors are identified by their Flutter texture id. This is stable diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart index 6e4bdc2d0..ec90e9fe8 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart @@ -11,9 +11,8 @@ import 'method_channel_platform_texture_descriptor.dart'; import 'platform_texture_descriptor.dart'; import 'platform_texture_descriptor_registry.dart'; -typedef TextureMutationRunner = Future Function( - Future Function() operation, -); +typedef TextureMutationRunner = + Future Function(Future Function() operation); class FilamentRenderingContext { const FilamentRenderingContext({ @@ -39,12 +38,17 @@ class NativePlatformTextureDescriptorRegistry required void Function() resumeRenderingIfReady, required TextureMutationRunner runTextureMutation, required AndroidTextureSource Function() androidTextureSource, + required bool Function() flipLinuxTextureVertically, }) : _pauseRendering = pauseRendering, _resumeRenderingIfReady = resumeRenderingIfReady, _runTextureMutation = runTextureMutation, super( - allocator: (width, height) => - _allocate(width, height, androidTextureSource()), + allocator: (width, height) => _allocate( + width, + height, + androidTextureSource(), + flipLinuxTextureVertically(), + ), ) { if (Platform.isAndroid) { channel.setMethodCallHandler(_handlePlatformMethodCall); @@ -62,6 +66,7 @@ class NativePlatformTextureDescriptorRegistry int width, int height, AndroidTextureSource androidTextureSource, + bool flipLinuxTextureVertically, ) { if (Platform.isMacOS || Platform.isIOS) { return Future.value( @@ -89,6 +94,7 @@ class NativePlatformTextureDescriptorRegistry width, height, deferred: true, + flipVertically: flipLinuxTextureVertically, ); } throw UnsupportedError('Platform textures are not supported on $Platform'); diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_internal/texture_widget_builder.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_internal/texture_widget_builder.dart index 33145bd84..cec18cc69 100644 --- a/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_internal/texture_widget_builder.dart +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget_internal/texture_widget_builder.dart @@ -7,10 +7,17 @@ Widget surfaceWidgetBuilder(PlatformTextureDescriptor? descriptor, View view) { if (descriptor == null) { return const SizedBox.shrink(); } - return Texture( + return buildTextureWidget(descriptor); +} + +@visibleForTesting +Widget buildTextureWidget(PlatformTextureDescriptor descriptor) { + final texture = Texture( key: ObjectKey("flutter_texture_${descriptor.flutterTextureId}"), textureId: descriptor.flutterTextureId, filterQuality: FilterQuality.none, freeze: false, ); + if (!descriptor.flipVertically) return texture; + return Transform.flip(flipY: true, child: texture); } diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart index 2d5040d02..a35b38c19 100644 --- a/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/viewer_widget.dart @@ -4,8 +4,6 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:thermion_flutter/thermion_flutter.dart' hide Texture; -import 'texture_bootstrap.dart'; - enum ManipulatorType { NONE, ORBIT, FREE_FLIGHT } class ViewerWidget extends StatefulWidget { diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/widgets.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/widgets.dart index 6fd21afd1..baa752f22 100644 --- a/thermion_flutter/thermion_flutter/lib/src/widgets/widgets.dart +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/widgets.dart @@ -2,6 +2,7 @@ library; export 'src/thermion_widget.dart'; export 'src/viewer_widget.dart'; +export 'src/texture_bootstrap.dart'; export 'src/thermion_listener_widget.dart'; export 'src/camera/camera_selector_widget.dart'; export 'src/camera/camera_orientation_widget.dart'; diff --git a/thermion_flutter/thermion_flutter/linux/CMakeLists.txt b/thermion_flutter/thermion_flutter/linux/CMakeLists.txt index f88e3f42c..7c9113830 100644 --- a/thermion_flutter/thermion_flutter/linux/CMakeLists.txt +++ b/thermion_flutter/thermion_flutter/linux/CMakeLists.txt @@ -106,11 +106,10 @@ if(_PKG_CONFIG) endforeach() if(_td_rootUri) - # rootUri may be a file:/// URI (e.g. file:///D:/path on Windows, - # file:///home on Linux) or a relative path like "../../thermion_dart". - # Strip file:/// (3 slashes) first, then file:// (2 slashes) as fallback. - string(REGEX REPLACE "^file:///" "" _td_root "${_td_rootUri}") - string(REGEX REPLACE "^file://" "" _td_root "${_td_root}") + # rootUri may be an absolute file:/// URI or a relative path such as + # "../../thermion_dart". Strip only the file:// scheme prefix: the + # remaining slash is the root of an absolute Linux path. + string(REGEX REPLACE "^file://" "" _td_root "${_td_rootUri}") if(NOT IS_ABSOLUTE "${_td_root}") get_filename_component(_pkg_dir "${_PKG_CONFIG}" DIRECTORY) get_filename_component(_td_root "${_pkg_dir}/${_td_root}" ABSOLUTE) diff --git a/thermion_flutter/thermion_flutter/test/thermion_widget_resize_transition_test.dart b/thermion_flutter/thermion_flutter/test/thermion_widget_resize_transition_test.dart index fdc079ff1..d27858abe 100644 --- a/thermion_flutter/thermion_flutter/test/thermion_widget_resize_transition_test.dart +++ b/thermion_flutter/thermion_flutter/test/thermion_widget_resize_transition_test.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:thermion_flutter/src/platform/src/platform_texture_descriptor.dart'; import 'package:thermion_flutter/src/widgets/src/thermion_widget.dart'; +import 'package:thermion_flutter/src/widgets/src/thermion_widget_internal/texture_widget_builder.dart'; void main() { testWidgets('staged replacement is mounted below the last valid frame', ( @@ -48,4 +50,45 @@ void main() { expect(find.byKey(currentKey), findsOneWidget); expect(find.byType(Stack), findsNothing); }); + + testWidgets('flagged platform texture is vertically flipped', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: buildTextureWidget(_TextureDescriptor(flipVertically: true)), + ), + ); + + final transform = tester.widget(find.byType(Transform)); + expect(transform.transform.entry(0, 0), 1); + expect(transform.transform.entry(1, 1), -1); + expect(find.byType(Texture), findsOneWidget); + }); + + testWidgets('unflagged platform texture is not transformed', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: buildTextureWidget(_TextureDescriptor(flipVertically: false)), + ), + ); + + expect(find.byType(Transform), findsNothing); + expect(find.byType(Texture), findsOneWidget); + }); +} + +class _TextureDescriptor extends PlatformTextureDescriptor { + _TextureDescriptor({required bool flipVertically}) + : super( + flutterTextureId: 1, + hardwareId: 2, + width: 3, + height: 4, + flipVertically: flipVertically, + ); + + @override + Future destroy() async {} + + @override + Future markTextureFrameAvailable() async {} } From 341eef5d360f63dedb294177824f420de5da861a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 1 Sep 2026 14:17:38 +0000 Subject: [PATCH 2/2] chore: update generated artifacts + format (CI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with GitHub Actions --- .../src/platform_texture_descriptor_registry_native.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart index ec90e9fe8..1533228e4 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/platform_texture_descriptor_registry_native.dart @@ -11,8 +11,9 @@ import 'method_channel_platform_texture_descriptor.dart'; import 'platform_texture_descriptor.dart'; import 'platform_texture_descriptor_registry.dart'; -typedef TextureMutationRunner = - Future Function(Future Function() operation); +typedef TextureMutationRunner = Future Function( + Future Function() operation, +); class FilamentRenderingContext { const FilamentRenderingContext({