This commit is contained in:
Nick Fisher
2024-10-23 01:20:22 +11:00
427 changed files with 74032 additions and 151261 deletions

View File

@@ -1,3 +1,92 @@
## 0.2.1-dev.7
- Update a dependency to the latest release.
## 0.2.1-dev.6
- Update a dependency to the latest release.
## 0.2.1-dev.5
- Update a dependency to the latest release.
## 0.2.1-dev.4
- Update a dependency to the latest release.
## 0.2.1-dev.3
- Update a dependency to the latest release.
## 0.2.1-dev.2
- Update a dependency to the latest release.
## 0.2.1-dev.1
- Update a dependency to the latest release.
## 0.2.1-dev.0
- Update a dependency to the latest release.
## 0.2.1-dev.0.0.0
- y
## 0.2.0
- Graduate package to a stable release. See pre-releases prior to this version for changelog entries.
## 0.2.0-dev.8.0.0
> Note: This release has breaking changes.
- **REFACTOR**: continual refactor to support multiple render targets.
- **FEAT**: support multiple ThermionWidget on Android.
- **FEAT**: use imported texture on iOS.
- **FEAT**: working implementation of multiple widgets on macos.
- **BREAKING** **REFACTOR**: refactor to support multiple Views/Render Targets.
- **BREAKING** **FEAT**: big refactor to support multiple swapchains.
## 0.2.0-dev.7.0
- Update a dependency to the latest release.
## 0.2.0-dev.6.0
- Update a dependency to the latest release.
## 0.2.0-dev.5.0
- Update a dependency to the latest release.
## 0.2.0-dev.4.0
- Update a dependency to the latest release.
## 0.2.0-dev.1.0
> Note: This release has breaking changes.
- **FEAT**: (flutter) move DPR calculation to resizeTexture and add createViewerWithOptions method to ThermionFlutterFFI.
- **BREAKING** **FIX**: (flutter) pass pixelRatio to createTexture.
## 0.1.0+12
- **FIX**: add logging dependency.
- **FIX**: web/JS bool checks need to compare to int.
- **FIX**: add logging dependency.
- **FIX**: web/JS bool checks need to compare to int.
## 0.1.0+11
- **FIX**: add logging dependency.
## 0.1.0+10
- **FIX**: web/JS bool checks need to compare to int.
## 0.1.0+9
- Update a dependency to the latest release.

View File

@@ -0,0 +1,122 @@
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;
bool destroyed = false;
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 {
_logger.info(
"Resizing texture to $newWidth x $newHeight (offset $newLeft, $newTop)");
if (newWidth == this.width &&
newHeight == this.height &&
newLeft == 0 &&
newTop == 0) {
_logger.info("Existing texture matches requested dimensions");
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;
_logger.info("Created texture ${flutterId} / ${hardwareId}");
if (destroySwapChainOnResize) {
if (swapChain != null) {
await viewer.destroySwapChain(swapChain!);
}
swapChain = await viewer.createSwapChain(result[2]);
await view.setRenderable(true, swapChain!);
} else if (hardwareId != _lastHardwareId) {
if (_renderTarget != null) {
await viewer.destroyRenderTarget(_renderTarget!);
}
_renderTarget =
await viewer.createRenderTarget(width, height, hardwareId);
await view.setRenderTarget(_renderTarget!);
await view.setRenderable(true, swapChain!);
if (_lastFlutterId != -1 && _lastHardwareId != -1) {
await _destroyTexture(_lastFlutterId, _lastHardwareId);
_lastFlutterId = -1;
_lastHardwareId = -1;
}
}
}
Future<void> _destroyTexture(
int flutterTextureId, int hardwareTextureId) async {
try {
await channel.invokeMethod(
"destroyTexture", [flutterTextureId, hardwareTextureId]);
_logger.info("Destroyed texture $flutterTextureId / $hardwareTextureId");
} catch (e) {
_logger.severe("Failed to destroy texture: $e");
}
}
bool destroying = false;
Future destroy() async {
if (destroyed || destroying) {
return;
}
destroying = true;
await view.setRenderTarget(null);
if (_renderTarget != null) {
await viewer.destroyRenderTarget(_renderTarget!);
_renderTarget = null;
}
if (destroySwapChainOnResize && swapChain != null) {
await viewer.destroySwapChain(swapChain!);
swapChain = null;
}
await _destroyTexture(flutterId, hardwareId);
flutterId = -1;
hardwareId = -1;
destroying = false;
destroyed = true;
}
Future markFrameAvailable() async {
await channel.invokeMethod("markTextureFrameAvailable", this.flutterId);
}
}

View File

@@ -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();
}

View File

@@ -1,234 +0,0 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'dart:ffi';
import 'package:thermion_dart/thermion_dart.dart';
import 'package:thermion_dart/thermion_dart/thermion_viewer_ffi.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 a Flutter platform
/// channel to create a rendering context, resource loaders, and
/// render target(s).
///
class ThermionFlutterFFI extends ThermionFlutterPlatform {
final _channel = const MethodChannel("dev.thermion.flutter/event");
final _logger = Logger("ThermionFlutterFFI");
ThermionViewerFFI? _viewer;
ThermionFlutterFFI._() {}
static void registerWith() {
ThermionFlutterPlatform.instance = ThermionFlutterFFI._();
}
final _textures = <ThermionFlutterTexture>{};
Future<ThermionViewer> createViewer({String? uberArchivePath}) async {
var resourceLoader = Pointer<Void>.fromAddress(
await _channel.invokeMethod("getResourceLoaderWrapper"));
if (resourceLoader == nullptr) {
throw Exception("Failed to get resource loader");
}
var renderCallbackResult = await _channel.invokeMethod("getRenderCallback");
var renderCallback =
Pointer<NativeFunction<Void Function(Pointer<Void>)>>.fromAddress(
renderCallbackResult[0]);
var renderCallbackOwner =
Pointer<Void>.fromAddress(renderCallbackResult[1]);
var driverPlatform = await _channel.invokeMethod("getDriverPlatform");
var driverPtr = driverPlatform == null
? nullptr
: Pointer<Void>.fromAddress(driverPlatform);
var sharedContext = await _channel.invokeMethod("getSharedContext");
var sharedContextPtr = sharedContext == null
? nullptr
: Pointer<Void>.fromAddress(sharedContext);
_viewer = ThermionViewerFFI(
resourceLoader: resourceLoader,
renderCallback: renderCallback,
renderCallbackOwner: renderCallbackOwner,
driver: driverPtr,
sharedContext: sharedContextPtr,
uberArchivePath: uberArchivePath);
await _viewer!.initialized;
return _viewer!;
}
bool _creatingTexture = false;
bool _destroyingTexture = false;
Future _waitForTextureCreationToComplete() async {
var iter = 0;
while (_creatingTexture || _destroyingTexture) {
await Future.delayed(Duration(milliseconds: 50));
iter++;
if (iter > 10) {
throw Exception(
"Previous call to createTexture failed to complete within 500ms");
}
}
}
///
/// Create a backing surface for rendering.
/// This is called by [ThermionWidget]; 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, int offsetLeft, int offsetRight) async {
// when a ThermionWidget is inserted, disposed then immediately reinserted
// into the widget hierarchy (e.g. rebuilding due to setState(() {}) being called in an ancestor widget)
// the first call to createTexture may not have completed before the second.
// add a loop here to wait (max 500ms) for the first call to complete
await _waitForTextureCreationToComplete();
// 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 == height && _textures.first.width == width) {
return _textures.first;
} else {
await _viewer!.setRendering(false);
await _viewer!.destroySwapChain();
await destroyTexture(_textures.first);
_textures.clear();
}
}
_creatingTexture = true;
var result = await _channel
.invokeMethod("createTexture", [width, height, 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");
_viewer?.viewportDimensions = (width.toDouble(), height.toDouble());
final texture = ThermionFlutterTexture(
flutterTextureId, hardwareTextureId, width, height, surfaceAddress);
await _viewer?.createSwapChain(width.toDouble(), height.toDouble(),
surface: texture.surfaceAddress == null
? nullptr
: Pointer<Void>.fromAddress(texture.surfaceAddress!));
if (texture.hardwareTextureId != null) {
// ignore: unused_local_variable
var renderTarget = await _viewer?.createRenderTarget(
width.toDouble(), height.toDouble(), texture.hardwareTextureId!);
}
await _viewer?.updateViewportAndCameraProjection(
width.toDouble(), height.toDouble());
_viewer?.render();
_creatingTexture = false;
_textures.add(texture);
return texture;
}
///
/// Destroy a texture and clean up the texture cache (if applicable).
///
Future destroyTexture(ThermionFlutterTexture texture) async {
if (_creatingTexture || _destroyingTexture) {
throw Exception(
"Cannot destroy texture while concurrent call to createTexture/destroyTexture has not completed");
}
_destroyingTexture = true;
_textures.remove(texture);
await _channel.invokeMethod("destroyTexture", texture.flutterTextureId);
_destroyingTexture = false;
}
bool _resizing = false;
///
/// Called by [ThermionWidget] to resize a texture. Don't call this yourself.
///
@override
Future<ThermionFlutterTexture?> resizeTexture(ThermionFlutterTexture texture,
int width, int height, int offsetLeft, int offsetRight) async {
if (_resizing) {
throw Exception("Resize underway");
}
if ((width - _viewer!.viewportDimensions.$1).abs() < 0.001 ||
(height - _viewer!.viewportDimensions.$2).abs() < 0.001) {
return texture;
}
_resizing = true;
bool wasRendering = _viewer!.rendering;
await _viewer!.setRendering(false);
await _viewer!.destroySwapChain();
await destroyTexture(texture);
var result = await _channel
.invokeMethod("createTexture", [width, height, offsetLeft, offsetLeft]);
if (result == null || result[0] == -1) {
throw Exception("Failed to create texture");
}
_viewer!.viewportDimensions = (width.toDouble(), height.toDouble());
var newTexture =
ThermionFlutterTexture(result[0], result[1], width, height, result[2]);
await _viewer!.createSwapChain(width.toDouble(), height.toDouble(),
surface: newTexture.surfaceAddress == null
? nullptr
: Pointer<Void>.fromAddress(newTexture.surfaceAddress!));
if (newTexture.hardwareTextureId != null) {
// ignore: unused_local_variable
var renderTarget = await _viewer!.createRenderTarget(
width.toDouble(), height.toDouble(), newTexture.hardwareTextureId!);
}
await _viewer!
.updateViewportAndCameraProjection(width.toDouble(), height.toDouble());
_viewer!.viewportDimensions = (width.toDouble(), height.toDouble());
if (wasRendering) {
await _viewer!.setRendering(true);
}
_textures.add(newTexture);
_resizing = false;
return newTexture;
}
}

View File

@@ -0,0 +1,77 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'dart:ffi';
import 'package:thermion_dart/thermion_dart.dart';
import 'package:thermion_dart/src/viewer/src/ffi/thermion_viewer_ffi.dart';
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
import 'package:logging/logging.dart';
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
///
/// An abstract implementation of [ThermionFlutterPlatform] that uses
/// Flutter platform channels to create a rendering context,
/// resource loaders, and surface/render target(s).
///
abstract class ThermionFlutterMethodChannelInterface
extends ThermionFlutterPlatform {
final channel = const MethodChannel("dev.thermion.flutter/event");
final _logger = Logger("ThermionFlutterMethodChannelInterface");
ThermionViewerFFI? viewer;
Future<ThermionViewer> createViewer({ThermionFlutterOptions? options}) async {
if (viewer != null) {
throw Exception(
"Only one ThermionViewer can be created at any given time; ensure you have called [dispose] on the previous instance before constructing a new instance.");
}
var resourceLoader = Pointer<Void>.fromAddress(
await channel.invokeMethod("getResourceLoaderWrapper"));
if (resourceLoader == nullptr) {
throw Exception("Failed to get resource loader");
}
var driverPlatform = await channel.invokeMethod("getDriverPlatform");
var driverPtr = driverPlatform == null
? nullptr
: Pointer<Void>.fromAddress(driverPlatform);
var sharedContext = await channel.invokeMethod("getSharedContext");
var sharedContextPtr = sharedContext == null
? nullptr
: Pointer<Void>.fromAddress(sharedContext);
viewer = ThermionViewerFFI(
resourceLoader: resourceLoader,
driver: driverPtr,
sharedContext: sharedContextPtr,
uberArchivePath: options?.uberarchivePath);
await viewer!.initialized;
viewer!.onDispose(() async {
this.viewer = null;
});
return viewer!;
}
}
abstract class MethodChannelFlutterTexture extends ThermionFlutterTexture {
final MethodChannel channel;
MethodChannelFlutterTexture(this.channel);
@override
int get flutterId;
@override
int get hardwareId;
@override
int get height;
@override
int get width;
}

View File

@@ -0,0 +1,71 @@
import 'dart:async';
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:logging/logging.dart';
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
import 'package:thermion_flutter_platform_interface/thermion_flutter_window.dart';
import 'platform_texture.dart';
///
/// An implementation of [ThermionFlutterPlatform] that uses
/// Flutter platform channels to create a rendering context,
/// resource loaders, and a texture that will be used as a render target
/// for a headless swapchain.
///
class ThermionFlutterTextureBackedPlatform
extends ThermionFlutterMethodChannelInterface {
final _logger = Logger("ThermionFlutterTextureBackedPlatform");
static SwapChain? _swapChain;
ThermionFlutterTextureBackedPlatform._();
static ThermionFlutterTextureBackedPlatform? instance;
static void registerWith() {
instance ??= ThermionFlutterTextureBackedPlatform._();
ThermionFlutterPlatform.instance = instance!;
}
@override
Future<ThermionViewer> createViewer({ThermionFlutterOptions? options}) async {
var viewer = await super.createViewer(options: options);
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
// don't matter
if (Platform.isMacOS || Platform.isIOS) {
_swapChain = await viewer.createHeadlessSwapChain(1, 1);
}
viewer.onDispose(() async {
_swapChain = null;
});
return viewer;
}
// On desktop platforms, textures are always created
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;
}
@override
Future<ThermionFlutterWindow> createWindow(
int width, int height, int offsetLeft, int offsetTop) {
// TODO: implement createWindow
throw UnimplementedError();
}
}

View File

@@ -0,0 +1,132 @@
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';
import 'package:thermion_flutter_platform_interface/thermion_flutter_window.dart';
///
/// A Windows-only implementation of [ThermionFlutterPlatform] that uses
/// a Flutter platform channel to create a rendering context,
/// resource loader and a native HWND that will be sit behind the running
/// Flutter application.
///
class ThermionFlutterWindows
extends ThermionFlutterMethodChannelInterface {
final _channel = const MethodChannel("dev.thermion.flutter/event");
final _logger = Logger("ThermionFlutterWindows");
ThermionViewer? _viewer;
SwapChain? _swapChain;
ThermionFlutterWindows._() {}
static void registerWith() {
ThermionFlutterPlatform.instance = ThermionFlutterWindows._();
}
@override
Future<ThermionViewer> createViewer({ThermionFlutterOptions? options}) async {
if(_viewer != null) {
throw Exception("Only one viewer should be instantiated over the life of the app");
}
_viewer = await super.createViewer(options: options);
return _viewer!;
}
///
/// Not supported on Windows. Throws an exception.
///
@override
Future<ThermionFlutterTexture?> createTexture(View view, int width, int height) {
throw UnimplementedError();
}
@override
Future<ThermionFlutterWindow> createWindow(int width, int height, int offsetLeft, int offsetTop) async {
var result = await _channel
.invokeMethod("createWindow", [width, height, offsetLeft, offsetLeft]);
if (result == null || result[2] == -1) {
throw Exception("Failed to create window");
}
var window =
ThermionFlutterWindowImpl(result[2], _channel, viewer!);
await window.resize(width, height, offsetLeft, offsetTop);
var view = await _viewer!.getViewAt(0);
await view.updateViewport(width, height);
_swapChain = await _viewer!.createSwapChain(window.handle);
await view.setRenderable(true, _swapChain!);
return window;
}
}
class ThermionFlutterWindowImpl extends ThermionFlutterWindow {
final ThermionViewer viewer;
final int handle;
int height = 0;
int width = 0;
int offsetLeft = 0;
int offsetTop = 0;
final MethodChannel _channel;
ThermionFlutterWindowImpl(this.handle, this._channel, this.viewer);
@override
Future destroy() async {
await _channel
.invokeMethod("destroyWindow", this.handle);
}
@override
Future markFrameAvailable() {
// TODO: implement markFrameAvailable
throw UnimplementedError();
}
bool _resizing = false;
///
/// Called by [ThermionWidget] to resize the window. Don't call this yourself.
///
@override
Future resize(
int width, int height, int offsetLeft, int offsetTop) async {
if (_resizing) {
throw Exception("Resize underway");
}
if (width == this.width && height == this.height && this.offsetLeft == offsetLeft && this.offsetTop == offsetTop) {
return;
}
this.width = width;
this.height = height;
this.offsetLeft = offsetLeft;
this.offsetTop = offsetTop;
_resizing = true;
await _channel
.invokeMethod("resizeWindow", [width, height, offsetLeft, offsetTop]);
_resizing = false;
}
}

View File

@@ -1,7 +1,7 @@
name: thermion_flutter_ffi
description: An FFI interface for the thermion_flutter plugin (all platforms except web).
repository: https://github.com/nmfisher/thermion_flutter/thermion_flutter
version: 0.1.0+9
version: 0.2.1-dev.7
environment:
sdk: ">=3.3.0 <4.0.0"
@@ -11,21 +11,28 @@ flutter:
implements: thermion_flutter_platform_interface
platforms:
ios:
dartPluginClass: ThermionFlutterFFI
dartPluginClass: ThermionFlutterTextureBackedPlatform
android:
dartPluginClass: ThermionFlutterFFI
dartPluginClass: ThermionFlutterTextureBackedPlatform
macos:
dartPluginClass: ThermionFlutterFFI
dartPluginClass: ThermionFlutterTextureBackedPlatform
windows:
dartPluginClass: ThermionFlutterFFI
dartPluginClass: ThermionFlutterWindows
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.1.0
thermion_flutter_platform_interface: ^0.1.0+9
thermion_dart: ^0.1.1+5
thermion_flutter_platform_interface: ^0.2.1-dev.7
thermion_dart: ^0.2.1-dev.0.0.8
logging: ^1.2.0
dependency_overrides:
thermion_dart:
path: ../../thermion_dart
thermion_flutter_platform_interface:
path: ../thermion_flutter_platform_interface
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^5.0.0