feat: support multiple ThermionWidget on Android
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'thermion_flutter_method_channel_interface.dart';
|
||||
|
||||
class FlutterPlatformTexture extends MethodChannelFlutterTexture {
|
||||
final _logger = Logger("ThermionFlutterTexture");
|
||||
|
||||
final ThermionViewer viewer;
|
||||
final View view;
|
||||
|
||||
int flutterId = -1;
|
||||
int _lastFlutterId = -1;
|
||||
int _lastHardwareId = -1;
|
||||
int hardwareId = -1;
|
||||
int width = -1;
|
||||
int height = -1;
|
||||
|
||||
SwapChain? swapChain;
|
||||
|
||||
RenderTarget? _renderTarget;
|
||||
|
||||
late bool destroySwapChainOnResize;
|
||||
|
||||
FlutterPlatformTexture(
|
||||
super.channel, this.viewer, this.view, this.swapChain) {
|
||||
if (swapChain == null) {
|
||||
destroySwapChainOnResize = true;
|
||||
} else {
|
||||
destroySwapChainOnResize = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> resize(
|
||||
int newWidth, int newHeight, int newLeft, int newTop) async {
|
||||
if (newWidth == this.width &&
|
||||
newHeight == this.height &&
|
||||
newLeft == 0 &&
|
||||
newTop == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.width = newWidth;
|
||||
this.height = newHeight;
|
||||
|
||||
var result =
|
||||
await channel.invokeMethod("createTexture", [width, height, 0, 0]);
|
||||
if (result == null || (result[0] == -1)) {
|
||||
throw Exception("Failed to create texture");
|
||||
}
|
||||
_lastFlutterId = flutterId;
|
||||
_lastHardwareId = hardwareId;
|
||||
flutterId = result[0] as int;
|
||||
|
||||
hardwareId = result[1] as int;
|
||||
|
||||
if (destroySwapChainOnResize) {
|
||||
await swapChain?.destroy();
|
||||
swapChain = await viewer.createSwapChain(result[2]);
|
||||
}
|
||||
|
||||
_logger.info(
|
||||
"Created new texture: flutter id $flutterId, hardware id $hardwareId");
|
||||
|
||||
if (destroySwapChainOnResize) {
|
||||
await view.setRenderable(true, swapChain!);
|
||||
} else if (hardwareId != _lastHardwareId) {
|
||||
await _renderTarget?.destroy();
|
||||
_renderTarget =
|
||||
await viewer.createRenderTarget(width, height, hardwareId);
|
||||
await view.setRenderTarget(_renderTarget!);
|
||||
await view.setRenderable(true, swapChain!);
|
||||
await _destroyTexture(_lastFlutterId, _lastHardwareId);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _destroyTexture(int flutterId, int? hardwareId) async {
|
||||
try {
|
||||
await channel.invokeMethod("destroyTexture", [flutterId, hardwareId]);
|
||||
_logger.info(
|
||||
"Destroyed old texture: flutter id $flutterId, hardware id $hardwareId");
|
||||
} catch (e) {
|
||||
_logger.severe("Failed to destroy texture: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future destroy() async {
|
||||
await view.setRenderTarget(null);
|
||||
await _renderTarget?.destroy();
|
||||
await swapChain?.destroy();
|
||||
await channel.invokeMethod("destroyTexture", hardwareId);
|
||||
}
|
||||
|
||||
Future markFrameAvailable() async {
|
||||
await channel.invokeMethod("markTextureFrameAvailable", this.flutterId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
class TextureCacheEntry {
|
||||
final int flutterId;
|
||||
final int hardwareId;
|
||||
final DateTime creationTime;
|
||||
DateTime? removalTime;
|
||||
bool inUse;
|
||||
|
||||
TextureCacheEntry(this.flutterId, this.hardwareId,
|
||||
{this.removalTime, this.inUse = true})
|
||||
: creationTime = DateTime.now();
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_dart/src/viewer/src/ffi/thermion_viewer_ffi.dart';
|
||||
import 'package:thermion_flutter_ffi/thermion_flutter_method_channel_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
///
|
||||
/// An implementation of [ThermionFlutterPlatform] that uses
|
||||
/// Flutter platform channels to create a rendering context,
|
||||
/// resource loaders, and surface/render target(s).
|
||||
///
|
||||
class ThermionFlutterAndroid
|
||||
extends ThermionFlutterMethodChannelInterface {
|
||||
final _channel = const MethodChannel("dev.thermion.flutter/event");
|
||||
final _logger = Logger("ThermionFlutterFFI");
|
||||
|
||||
ThermionViewerFFI? _viewer;
|
||||
|
||||
ThermionFlutterAndroid._() {}
|
||||
|
||||
RenderTarget? _renderTarget;
|
||||
SwapChain? _swapChain;
|
||||
|
||||
static void registerWith() {
|
||||
ThermionFlutterPlatform.instance = ThermionFlutterAndroid._();
|
||||
}
|
||||
|
||||
final _textures = <ThermionFlutterTexture>{};
|
||||
|
||||
bool _creatingTexture = false;
|
||||
bool _destroyingTexture = false;
|
||||
|
||||
bool _resizing = false;
|
||||
|
||||
///
|
||||
/// Create a rendering surface.
|
||||
///
|
||||
/// This is internal; unless you are [thermion_*] package developer, don't
|
||||
/// call this yourself.
|
||||
///
|
||||
/// The name here is slightly misleading because we only create
|
||||
/// a texture render target on macOS and iOS; on Android, we render into
|
||||
/// a native window derived from a Surface, and on Windows we render into
|
||||
/// a HWND.
|
||||
///
|
||||
/// Currently, this only supports a single "texture" (aka rendering surface)
|
||||
/// at any given time. If a [ThermionWidget] is disposed, it will call
|
||||
/// [destroyTexture]; if it is resized, it will call [resizeTexture].
|
||||
///
|
||||
/// In future, we probably want to be able to create multiple distinct
|
||||
/// textures/render targets. This would make it possible to have multiple
|
||||
/// Flutter Texture widgets, each with its own Filament View attached.
|
||||
/// The current design doesn't accommodate this (for example, it seems we can
|
||||
/// only create a single native window from a Surface at any one time).
|
||||
///
|
||||
Future<ThermionFlutterTexture?> createTexture(int width, int height) async {
|
||||
throw Exception("TODO");
|
||||
// note that when [ThermionWidget] is disposed, we don't destroy the
|
||||
// texture; instead, we keep it around in case a subsequent call requests
|
||||
// a texture of the same size.
|
||||
|
||||
// if (_textures.length > 1) {
|
||||
// throw Exception("Multiple textures not yet supported");
|
||||
// } else if (_textures.length == 1) {
|
||||
// if (_textures.first.height == physicalHeight &&
|
||||
// _textures.first.width == physicalWidth) {
|
||||
// return _textures.first;
|
||||
// } else {
|
||||
// await _viewer!.setRendering(false);
|
||||
// await _swapChain?.destroy();
|
||||
// await destroyTexture(_textures.first);
|
||||
// _textures.clear();
|
||||
// }
|
||||
// }
|
||||
|
||||
// _creatingTexture = true;
|
||||
|
||||
// var result = await _channel.invokeMethod("createTexture",
|
||||
// [physicalWidth, physicalHeight, offsetLeft, offsetLeft]);
|
||||
|
||||
// if (result == null || (result[0] == -1)) {
|
||||
// throw Exception("Failed to create texture");
|
||||
// }
|
||||
// final flutterTextureId = result[0] as int?;
|
||||
// final hardwareTextureId = result[1] as int?;
|
||||
// final surfaceAddress = result[2] as int?;
|
||||
|
||||
// _logger.info(
|
||||
// "Created texture with flutter texture id ${flutterTextureId}, hardwareTextureId $hardwareTextureId and surfaceAddress $surfaceAddress");
|
||||
|
||||
// final texture = ThermionFlutterTexture(flutterTextureId, hardwareTextureId,
|
||||
// physicalWidth, physicalHeight, surfaceAddress);
|
||||
|
||||
// await _viewer?.createSwapChain(physicalWidth, physicalHeight,
|
||||
// surface: texture.surfaceAddress == null
|
||||
// ? nullptr
|
||||
// : Pointer<Void>.fromAddress(texture.surfaceAddress!));
|
||||
|
||||
// if (texture.hardwareTextureId != null) {
|
||||
// if (_renderTarget != null) {
|
||||
// await _renderTarget!.destroy();
|
||||
// }
|
||||
// // ignore: unused_local_variable
|
||||
// _renderTarget = await _viewer?.createRenderTarget(
|
||||
// physicalWidth, physicalHeight, texture.hardwareTextureId!);
|
||||
// }
|
||||
|
||||
// await _viewer?.updateViewportAndCameraProjection(
|
||||
// physicalWidth.toDouble(), physicalHeight.toDouble());
|
||||
// _creatingTexture = false;
|
||||
// _textures.add(texture);
|
||||
// return texture;
|
||||
}
|
||||
|
||||
///
|
||||
/// Called by [ThermionWidget] to resize a texture. Don't call this yourself.
|
||||
///
|
||||
@override
|
||||
Future resizeWindow(
|
||||
int width,
|
||||
int height,
|
||||
int offsetLeft,
|
||||
int offsetTop,
|
||||
) async {
|
||||
throw Exception("Not supported on iOS");
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export 'thermion_flutter_android.dart';
|
||||
export 'thermion_flutter_windows.dart';
|
||||
export 'thermion_flutter_texture_backed_platform.dart';
|
||||
|
||||
@@ -14,11 +14,11 @@ import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dar
|
||||
///
|
||||
abstract class ThermionFlutterMethodChannelInterface
|
||||
extends ThermionFlutterPlatform {
|
||||
final _channel = const MethodChannel("dev.thermion.flutter/event");
|
||||
|
||||
final channel = const MethodChannel("dev.thermion.flutter/event");
|
||||
final _logger = Logger("ThermionFlutterMethodChannelInterface");
|
||||
|
||||
ThermionViewerFFI? viewer;
|
||||
SwapChain? _swapChain;
|
||||
|
||||
Future<ThermionViewer> createViewer({ThermionFlutterOptions? options}) async {
|
||||
if (viewer != null) {
|
||||
@@ -27,25 +27,18 @@ abstract class ThermionFlutterMethodChannelInterface
|
||||
}
|
||||
|
||||
var resourceLoader = Pointer<Void>.fromAddress(
|
||||
await _channel.invokeMethod("getResourceLoaderWrapper"));
|
||||
await channel.invokeMethod("getResourceLoaderWrapper"));
|
||||
|
||||
if (resourceLoader == nullptr) {
|
||||
throw Exception("Failed to get resource loader");
|
||||
}
|
||||
|
||||
// var renderCallbackResult = await _channel.invokeMethod("getRenderCallback");
|
||||
var renderCallback = nullptr;
|
||||
// Pointer<NativeFunction<Void Function(Pointer<Void>)>>.fromAddress(
|
||||
// renderCallbackResult[0]);
|
||||
var renderCallbackOwner = nullptr;
|
||||
// Pointer<Void>.fromAddress(renderCallbackResult[1]);
|
||||
|
||||
var driverPlatform = await _channel.invokeMethod("getDriverPlatform");
|
||||
var driverPlatform = await channel.invokeMethod("getDriverPlatform");
|
||||
var driverPtr = driverPlatform == null
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(driverPlatform);
|
||||
|
||||
var sharedContext = await _channel.invokeMethod("getSharedContext");
|
||||
var sharedContext = await channel.invokeMethod("getSharedContext");
|
||||
|
||||
var sharedContextPtr = sharedContext == null
|
||||
? nullptr
|
||||
@@ -53,8 +46,6 @@ abstract class ThermionFlutterMethodChannelInterface
|
||||
|
||||
viewer = ThermionViewerFFI(
|
||||
resourceLoader: resourceLoader,
|
||||
renderCallback: renderCallback,
|
||||
renderCallbackOwner: renderCallbackOwner,
|
||||
driver: driverPtr,
|
||||
sharedContext: sharedContextPtr,
|
||||
uberArchivePath: options?.uberarchivePath);
|
||||
@@ -69,9 +60,7 @@ abstract class MethodChannelFlutterTexture extends ThermionFlutterTexture {
|
||||
|
||||
MethodChannelFlutterTexture(this.channel);
|
||||
|
||||
Future destroy() async {
|
||||
await channel.invokeMethod("destroyTexture", hardwareId);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get flutterId;
|
||||
@@ -85,7 +74,5 @@ abstract class MethodChannelFlutterTexture extends ThermionFlutterTexture {
|
||||
@override
|
||||
int get width;
|
||||
|
||||
Future markFrameAvailable() async {
|
||||
await channel.invokeMethod("markTextureFrameAvailable", this.flutterId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart' as t;
|
||||
import 'package:thermion_flutter_ffi/thermion_flutter_method_channel_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
|
||||
import 'platform_texture.dart';
|
||||
|
||||
///
|
||||
/// An implementation of [ThermionFlutterPlatform] that uses
|
||||
/// Flutter platform channels to create a rendering context,
|
||||
/// resource loaders, and surface/render target(s).
|
||||
///
|
||||
class ThermionFlutterTextureBackedPlatform extends ThermionFlutterMethodChannelInterface {
|
||||
final _channel = const MethodChannel("dev.thermion.flutter/event");
|
||||
class ThermionFlutterTextureBackedPlatform
|
||||
extends ThermionFlutterMethodChannelInterface {
|
||||
final _logger = Logger("ThermionFlutterTextureBackedPlatform");
|
||||
|
||||
static SwapChain? _swapChain;
|
||||
@@ -32,16 +36,19 @@ class ThermionFlutterTextureBackedPlatform extends ThermionFlutterMethodChannelI
|
||||
if (_swapChain != null) {
|
||||
throw Exception("Only a single swapchain can be created");
|
||||
}
|
||||
|
||||
// this implementation renders directly into a texture/render target
|
||||
// we still need to create a (headless) swapchain, but the actual dimensions
|
||||
// we still need to create a (headless) swapchain, but the actual dimensions
|
||||
// don't matter
|
||||
_swapChain = await viewer.createSwapChain(1, 1);
|
||||
if (Platform.isMacOS || Platform.isIOS) {
|
||||
_swapChain = await viewer.createHeadlessSwapChain(1, 1);
|
||||
}
|
||||
return viewer;
|
||||
}
|
||||
|
||||
// On desktop platforms, textures are always created
|
||||
Future<ThermionFlutterTexture?> createTexture(int width, int height) async {
|
||||
var texture = ThermionFlutterTexture(_channel);
|
||||
Future<ThermionFlutterTexture?> createTexture(t.View view, int width, int height) async {
|
||||
var texture = FlutterPlatformTexture(channel, viewer!, view, (Platform.isMacOS || Platform.isIOS)? _swapChain : null);
|
||||
await texture.resize(width, height, 0, 0);
|
||||
return texture;
|
||||
}
|
||||
@@ -54,119 +61,3 @@ class ThermionFlutterTextureBackedPlatform extends ThermionFlutterMethodChannelI
|
||||
}
|
||||
}
|
||||
|
||||
class TextureCacheEntry {
|
||||
final int flutterId;
|
||||
final int hardwareId;
|
||||
final DateTime creationTime;
|
||||
DateTime? removalTime;
|
||||
bool inUse;
|
||||
|
||||
TextureCacheEntry(this.flutterId, this.hardwareId, {this.removalTime, this.inUse = true})
|
||||
: creationTime = DateTime.now();
|
||||
}
|
||||
|
||||
|
||||
class ThermionFlutterTexture extends MethodChannelFlutterTexture {
|
||||
final _logger = Logger("ThermionFlutterTexture");
|
||||
|
||||
int flutterId = -1;
|
||||
int hardwareId = -1;
|
||||
int width = -1;
|
||||
int height = -1;
|
||||
|
||||
static final Map<String, List<TextureCacheEntry>> _textureCache = {};
|
||||
|
||||
ThermionFlutterTexture(super.channel);
|
||||
|
||||
@override
|
||||
Future<void> resize(
|
||||
int newWidth, int newHeight, int newLeft, int newTop) async {
|
||||
if (newWidth == this.width &&
|
||||
newHeight == this.height &&
|
||||
newLeft == 0 &&
|
||||
newTop == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.width = newWidth;
|
||||
this.height = newHeight;
|
||||
|
||||
// Clean up old textures
|
||||
await _cleanupOldTextures();
|
||||
|
||||
final cacheKey = '${width}x$height';
|
||||
final availableTextures =
|
||||
_textureCache[cacheKey]?.where((entry) => !entry.inUse) ?? [];
|
||||
if (availableTextures.isNotEmpty) {
|
||||
final cachedTexture = availableTextures.first;
|
||||
flutterId = cachedTexture.flutterId;
|
||||
hardwareId = cachedTexture.hardwareId;
|
||||
cachedTexture.inUse = true;
|
||||
_logger.info(
|
||||
"Using cached texture: flutter id $flutterId, hardware id $hardwareId");
|
||||
} else {
|
||||
var result =
|
||||
await channel.invokeMethod("createTexture", [width, height, 0, 0]);
|
||||
if (result == null || (result[0] == -1)) {
|
||||
throw Exception("Failed to create texture");
|
||||
}
|
||||
flutterId = result[0] as int;
|
||||
hardwareId = result[1] as int;
|
||||
|
||||
final newEntry = TextureCacheEntry(flutterId, hardwareId, inUse: true);
|
||||
_textureCache.putIfAbsent(cacheKey, () => []).add(newEntry);
|
||||
_logger.info(
|
||||
"Created new texture: flutter id $flutterId, hardware id $hardwareId");
|
||||
}
|
||||
|
||||
// Mark old texture as not in use
|
||||
if (this.width != -1 && this.height != -1) {
|
||||
final oldCacheKey = '${this.width}x${this.height}';
|
||||
final oldEntry = _textureCache[oldCacheKey]?.firstWhere(
|
||||
(entry) => entry.flutterId == this.flutterId,
|
||||
orElse: () => TextureCacheEntry(-1, -1),
|
||||
);
|
||||
if (oldEntry != null && oldEntry.flutterId != -1) {
|
||||
oldEntry.inUse = false;
|
||||
oldEntry.removalTime = DateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future _cleanupOldTextures() async {
|
||||
final now = DateTime.now();
|
||||
final entriesToRemove = <String, List<TextureCacheEntry>>{};
|
||||
|
||||
for (var entry in _textureCache.entries) {
|
||||
final expiredTextures = entry.value.where((texture) {
|
||||
return !texture.inUse &&
|
||||
texture.removalTime != null &&
|
||||
now.difference(texture.removalTime!).inSeconds > 5;
|
||||
}).toList();
|
||||
|
||||
if (expiredTextures.isNotEmpty) {
|
||||
entriesToRemove[entry.key] = expiredTextures;
|
||||
}
|
||||
}
|
||||
|
||||
for (var entry in entriesToRemove.entries) {
|
||||
for (var texture in entry.value) {
|
||||
await _destroyTexture(texture.flutterId, texture.hardwareId);
|
||||
_logger.info("Destroying texture: ${texture.flutterId}");
|
||||
_textureCache[entry.key]?.remove(texture);
|
||||
}
|
||||
if (_textureCache[entry.key]?.isEmpty ?? false) {
|
||||
_textureCache.remove(entry.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _destroyTexture(int flutterId, int? hardwareId) async {
|
||||
try {
|
||||
await channel.invokeMethod("destroyTexture", [flutterId, hardwareId]);
|
||||
_logger.info("Destroyed old texture: flutter id $flutterId, hardware id $hardwareId");
|
||||
} catch (e) {
|
||||
_logger.severe("Failed to destroy texture: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,9 @@ class ThermionFlutterWindows
|
||||
///
|
||||
/// Not supported on Windows. Throws an exception.
|
||||
///
|
||||
Future<ThermionFlutterTexture?> createTexture(int width, int height) async {
|
||||
throw Exception("Texture not supported on Windows");
|
||||
@override
|
||||
Future<ThermionFlutterTexture?> createTexture(View view, int width, int height) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
bool _resizing = false;
|
||||
@@ -94,4 +95,6 @@ class ThermionFlutterWindows
|
||||
// _resizing = false;
|
||||
// return newTexture;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ flutter:
|
||||
ios:
|
||||
dartPluginClass: ThermionFlutterTextureBackedPlatform
|
||||
android:
|
||||
dartPluginClass: ThermionFlutterAndroid
|
||||
dartPluginClass: ThermionFlutterTextureBackedPlatform
|
||||
macos:
|
||||
dartPluginClass: ThermionFlutterTextureBackedPlatform
|
||||
windows:
|
||||
|
||||
Reference in New Issue
Block a user