diff --git a/README.md b/README.md index 69ac0a01..72a3c9e5 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ This package is currently only tested on Flutter >= `3.16.0-0.2.pre`, so you wil flutter channel beta flutter upgrade ``` +Earlier versions have specific issues that will prevent them from working on Windows/MacOS! Next, clone this repository and pull the latest binaries from Git LFS: @@ -358,6 +359,7 @@ Separately, we also force the Filament gltfio library to load assets via in-memo ``` git checkout flutter-filament-windows mkdir out && cd out +"C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" --build . --target gltf_viewer --config Debug ``` Building notes: diff --git a/lib/filament_controller.dart b/lib/filament_controller.dart index f3f9417e..7d462327 100644 --- a/lib/filament_controller.dart +++ b/lib/filament_controller.dart @@ -17,11 +17,11 @@ class TextureDetails { } abstract class FilamentController { - // the current target size of the viewport, in logical pixels - ui.Size size = ui.Size.zero; Future get isReadyForScene; + TextureDetails? get textureDetails; + /// /// The result(s) of calling [pick] (see below). /// This may be a broadcast stream, so you should ensure you have subscribed to this stream before calling [pick]. @@ -29,6 +29,11 @@ abstract class FilamentController { /// Stream get pickResult; + /// + /// Whether the controller is currently rendering at [framerate]. + /// + bool get rendering; + /// /// Set to true to continuously render the scene at the framerate specified by [setFrameRate] (60 fps by default). /// @@ -63,7 +68,7 @@ abstract class FilamentController { /// - /// Destroys the backing texture. You probably want to call [destroy] instead of this; this is exposed mostly for lifecycle changes which are handled by FilamentWidget. + /// Destroys the specified backing texture. You probably want to call [destroy] instead of this; this is exposed mostly for lifecycle changes which are handled by FilamentWidget. /// Future destroyTexture(); @@ -79,13 +84,13 @@ abstract class FilamentController { /// 5) The FilamentWidget will replace the empty Container with a Texture widget /// If you need to wait until a FilamentViewer has been created, [await] the [isReadyForScene] Future. /// - Future createViewer(int width, int height); + void createViewer(int width, int height); /// /// Resize the viewport & backing texture. /// This is called by FilamentWidget; you shouldn't need to invoke this manually. /// - Future resize(int width, int height, {double scaleFactor = 1.0}); + Future resize(int width, int height, {double scaleFactor = 1.0}); /// /// Set the background image to [path] (which should have a file extension .png, .jpg, or .ktx). diff --git a/lib/filament_controller_ffi.dart b/lib/filament_controller_ffi.dart index ca342f03..9cf6dbe1 100644 --- a/lib/filament_controller_ffi.dart +++ b/lib/filament_controller_ffi.dart @@ -16,8 +16,6 @@ class FilamentControllerFFI extends FilamentController { double _pixelRatio = 1.0; - int? _textureId; - Completer _isReadyForScene = Completer(); Future get isReadyForScene => _isReadyForScene.future; @@ -27,13 +25,15 @@ class FilamentControllerFFI extends FilamentController { Pointer? _viewer; - bool _resizing = false; final String? uberArchivePath; Stream get pickResult => _pickResultController.stream; final _pickResultController = StreamController.broadcast(); + @override + TextureDetails? textureDetails; + /// /// This controller uses platform channels to bridge Dart with the C/C++ code for the Filament API. /// Setting up the context/texture (since this is platform-specific) and the render ticker are platform-specific; all other methods are passed through by the platform channel to the methods specified in PolyvoxFilamentApi.h. @@ -52,10 +52,11 @@ class FilamentControllerFFI extends FilamentController { } bool _rendering = false; + bool get rendering => _rendering; @override Future setRendering(bool render) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _rendering = render; @@ -64,7 +65,7 @@ class FilamentControllerFFI extends FilamentController { @override Future render() async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.render_ffi(_viewer!); @@ -89,7 +90,7 @@ class FilamentControllerFFI extends FilamentController { @override Future destroyViewer() async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } var viewer = _viewer; @@ -103,25 +104,24 @@ class FilamentControllerFFI extends FilamentController { @override Future destroyTexture() async { - if (_textureId != null) { - throw Exception("No texture available"); + if(textureDetails != null) { + await _channel.invokeMethod("destroyTexture", textureDetails!.textureId); } - print("Destroying texture"); - // we need to flush all references to the previous texture ID before calling destroy, otherwise the Texture widget will attempt to render a non-existent texture and crash. - // however, this is not a synchronous stream, so we need to ensure the Texture widget has been removed from the hierarchy before destroying - _textureId = null; - await _channel.invokeMethod("destroyTexture", _textureId!); print("Texture destroyed"); } /// /// Called by `FilamentWidget`. You do not need to call this yourself. /// - Future createViewer(int width, int height) async { + void createViewer(int width, int height) async { if (_viewer != null) { throw Exception( "Viewer already exists, make sure you call destroyViewer first"); } + if(textureDetails != null) { + throw Exception( + "Texture already exists, make sure you call destroyTexture first"); + } if (_isReadyForScene.isCompleted) { throw Exception( "Do not call createViewer when a viewer has already been created without calling destroyViewer"); @@ -132,14 +132,14 @@ class FilamentControllerFFI extends FilamentController { throw Exception("Failed to get resource loader"); } - size = ui.Size(width * _pixelRatio, height * _pixelRatio); + var size = ui.Size(width * _pixelRatio, height * _pixelRatio); print("Creating viewer with size $size"); var textures = await _channel.invokeMethod("createTexture", [size.width, size.height]); var flutterTextureId = textures[0]; - _textureId = flutterTextureId; + // void* on iOS (pointer to pixel buffer), void* on Android (pointer to native window), null on Windows/macOS var surfaceAddress = textures[1] as int? ?? 0; @@ -193,7 +193,7 @@ class FilamentControllerFFI extends FilamentController { _assetManager = _lib.get_asset_manager(_viewer!); _isReadyForScene.complete(true); - return TextureDetails(textureId: _textureId!, width: width, height: height); + textureDetails = TextureDetails(textureId: flutterTextureId!, width: width, height: height); } /// @@ -259,33 +259,38 @@ class FilamentControllerFFI extends FilamentController { /// # Given we don't do this on other platforms, I'm OK to stick with the existing solution for the time being. /// ############################################################################ /// - - /// - /// - /// Other options: - /// 1) never destroy the texture, simply allocate a large (4k?) texture and crop as needed - /// 2) double-buffering? + @override - Future resize(int width, int height, - {double scaleFactor = 1.0}) async { - if (_textureId == null) { - throw Exception("No texture created, ignoring call to resize."); - } - var textureId = _textureId; - _textureId = null; + Future resize(int width, int height, {double scaleFactor = 1.0}) async { + // we defer to the FilamentWidget to ensure that every call to [resize] is synchronized + // so this exception should never be thrown (right?) + if(textureDetails == null) { + throw Exception("Resize currently underway, ignoring"); + } + + var _textureDetails = textureDetails; + + textureDetails = null; + _lib.set_rendering_ffi(_viewer!, false); - if (_viewer != null) { - _lib.destroy_swap_chain_ffi(_viewer!); + if(_textureDetails != null) { + if (_viewer != null) { + _lib.destroy_swap_chain_ffi(_viewer!); + } + + await _channel.invokeMethod("destroyTexture", _textureDetails!.textureId); + print("Destroyed texture ${_textureDetails!.textureId}"); } - await _channel.invokeMethod("destroyTexture", textureId); + var newSize = ui.Size(width * _pixelRatio, height * _pixelRatio); - size = ui.Size(width * _pixelRatio, height * _pixelRatio); + print("Size after pixel ratio : $width x $height "); - var textures = - await _channel.invokeMethod("createTexture", [size.width, size.height]); + + var textures = await _channel + .invokeMethod("createTexture", [newSize.width, newSize.height]); // void* on iOS (pointer to pixel buffer), void* on Android (pointer to native window), null on Windows/macOS var surfaceAddress = textures[1] as int? ?? 0; @@ -296,28 +301,27 @@ class FilamentControllerFFI extends FilamentController { _lib.create_swap_chain_ffi( _viewer!, Pointer.fromAddress(surfaceAddress), - size.width.toInt(), - size.height.toInt()); + newSize.width.toInt(), + newSize.height.toInt()); if (nativeTexture != 0) { assert(surfaceAddress == 0); print("Creating render target from native texture $nativeTexture"); _lib.create_render_target_ffi( - _viewer!, nativeTexture, size.width.toInt(), size.height.toInt()); + _viewer!, nativeTexture, newSize.width.toInt(), newSize.height.toInt()); } _lib.update_viewport_and_camera_projection_ffi( - _viewer!, size.width.toInt(), size.height.toInt(), 1.0); + _viewer!, newSize.width.toInt(), newSize.height.toInt(), 1.0); await setRendering(_rendering); - - _textureId = textures[0]; - - return TextureDetails(textureId: _textureId!, width: width, height: height); + textureDetails = TextureDetails(textureId: textures[0]!, width: width, height: height); } + + @override Future clearBackgroundImage() async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.clear_background_image_ffi(_viewer!); @@ -325,7 +329,7 @@ class FilamentControllerFFI extends FilamentController { @override Future setBackgroundImage(String path, {bool fillHeight = false}) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.set_background_image_ffi( @@ -334,7 +338,7 @@ class FilamentControllerFFI extends FilamentController { @override Future setBackgroundColor(Color color) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.set_background_color_ffi( @@ -348,7 +352,7 @@ class FilamentControllerFFI extends FilamentController { @override Future setBackgroundImagePosition(double x, double y, {bool clamp = false}) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.set_background_image_position_ffi(_viewer!, x, y, clamp); @@ -356,7 +360,7 @@ class FilamentControllerFFI extends FilamentController { @override Future loadSkybox(String skyboxPath) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.load_skybox_ffi(_viewer!, skyboxPath.toNativeUtf8().cast()); @@ -364,7 +368,7 @@ class FilamentControllerFFI extends FilamentController { @override Future loadIbl(String lightingPath, {double intensity = 30000}) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.load_ibl_ffi( @@ -373,7 +377,7 @@ class FilamentControllerFFI extends FilamentController { @override Future removeSkybox() async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.remove_skybox_ffi(_viewer!); @@ -381,7 +385,7 @@ class FilamentControllerFFI extends FilamentController { @override Future removeIbl() async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.remove_ibl_ffi(_viewer!); @@ -399,7 +403,7 @@ class FilamentControllerFFI extends FilamentController { double dirY, double dirZ, bool castShadows) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } var entity = _lib.add_light_ffi(_viewer!, type, colour, intensity, posX, @@ -409,7 +413,7 @@ class FilamentControllerFFI extends FilamentController { @override Future removeLight(FilamentEntity light) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.remove_light_ffi(_viewer!, light); @@ -417,7 +421,7 @@ class FilamentControllerFFI extends FilamentController { @override Future clearLights() async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.clear_lights_ffi(_viewer!); @@ -425,7 +429,7 @@ class FilamentControllerFFI extends FilamentController { @override Future loadGlb(String path, {bool unlit = false}) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } if (unlit) { @@ -446,7 +450,7 @@ class FilamentControllerFFI extends FilamentController { throw Exception( "loadGltf has a race condition on Windows which is likely to crash your program. If you really want to try, pass force=true to loadGltf"); } - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } var asset = _lib.load_gltf_ffi( @@ -461,7 +465,7 @@ class FilamentControllerFFI extends FilamentController { @override Future panStart(double x, double y) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.grab_begin(_viewer!, x * _pixelRatio, y * _pixelRatio, true); @@ -469,7 +473,7 @@ class FilamentControllerFFI extends FilamentController { @override Future panUpdate(double x, double y) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.grab_update(_viewer!, x * _pixelRatio, y * _pixelRatio); @@ -477,7 +481,7 @@ class FilamentControllerFFI extends FilamentController { @override Future panEnd() async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.grab_end(_viewer!); @@ -485,7 +489,7 @@ class FilamentControllerFFI extends FilamentController { @override Future rotateStart(double x, double y) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.grab_begin(_viewer!, x * _pixelRatio, y * _pixelRatio, false); @@ -493,7 +497,7 @@ class FilamentControllerFFI extends FilamentController { @override Future rotateUpdate(double x, double y) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.grab_update(_viewer!, x * _pixelRatio, y * _pixelRatio); @@ -501,7 +505,7 @@ class FilamentControllerFFI extends FilamentController { @override Future rotateEnd() async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.grab_end(_viewer!); @@ -510,7 +514,7 @@ class FilamentControllerFFI extends FilamentController { @override Future setMorphTargetWeights( FilamentEntity asset, String meshName, List weights) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } var weightsPtr = calloc(weights.length); @@ -526,7 +530,7 @@ class FilamentControllerFFI extends FilamentController { @override Future> getMorphTargetNames( FilamentEntity asset, String meshName) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } var names = []; @@ -544,7 +548,7 @@ class FilamentControllerFFI extends FilamentController { @override Future> getAnimationNames(FilamentEntity asset) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } var animationCount = _lib.get_animation_count(_assetManager!, asset); @@ -561,7 +565,7 @@ class FilamentControllerFFI extends FilamentController { @override Future getAnimationDuration( FilamentEntity asset, int animationIndex) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } var duration = @@ -573,7 +577,7 @@ class FilamentControllerFFI extends FilamentController { @override Future setMorphAnimationData( FilamentEntity asset, MorphAnimationData animation) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } @@ -603,7 +607,7 @@ class FilamentControllerFFI extends FilamentController { @override Future setBoneAnimation( FilamentEntity asset, BoneAnimationData animation) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } // var data = calloc(animation.frameData.length); @@ -640,7 +644,7 @@ class FilamentControllerFFI extends FilamentController { @override Future removeAsset(FilamentEntity asset) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.remove_asset_ffi(_viewer!, asset); @@ -648,7 +652,7 @@ class FilamentControllerFFI extends FilamentController { @override Future clearAssets() async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.clear_assets_ffi(_viewer!); @@ -656,7 +660,7 @@ class FilamentControllerFFI extends FilamentController { @override Future zoomBegin() async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.scroll_begin(_viewer!); @@ -664,7 +668,7 @@ class FilamentControllerFFI extends FilamentController { @override Future zoomUpdate(double x, double y, double z) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.scroll_update(_viewer!, x, y, z); @@ -672,7 +676,7 @@ class FilamentControllerFFI extends FilamentController { @override Future zoomEnd() async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.scroll_end(_viewer!); @@ -684,7 +688,7 @@ class FilamentControllerFFI extends FilamentController { bool reverse = false, bool replaceActive = true, double crossfade = 0.0}) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.play_animation_ffi( @@ -693,14 +697,14 @@ class FilamentControllerFFI extends FilamentController { Future setAnimationFrame( FilamentEntity asset, int index, int animationFrame) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.set_animation_frame(_assetManager!, asset, index, animationFrame); } Future stopAnimation(FilamentEntity asset, int animationIndex) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.stop_animation(_assetManager!, asset, animationIndex); @@ -708,7 +712,7 @@ class FilamentControllerFFI extends FilamentController { @override Future setCamera(FilamentEntity asset, String? name) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } var result = _lib.set_camera( @@ -720,7 +724,7 @@ class FilamentControllerFFI extends FilamentController { @override Future setToneMapping(ToneMapper mapper) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } @@ -729,7 +733,7 @@ class FilamentControllerFFI extends FilamentController { @override Future setPostProcessing(bool enabled) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } @@ -738,35 +742,35 @@ class FilamentControllerFFI extends FilamentController { @override Future setBloom(double bloom) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.set_bloom_ffi(_viewer!, bloom); } Future setCameraFocalLength(double focalLength) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.set_camera_focal_length(_viewer!, focalLength); } Future setCameraFocusDistance(double focusDistance) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.set_camera_focus_distance(_viewer!, focusDistance); } Future setCameraPosition(double x, double y, double z) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.set_camera_position(_viewer!, x, y, z); } Future moveCameraToAsset(FilamentEntity asset) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.move_camera_to_asset(_viewer!, asset); @@ -774,7 +778,7 @@ class FilamentControllerFFI extends FilamentController { @override Future setViewFrustumCulling(bool enabled) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.set_view_frustum_culling(_viewer!, enabled); @@ -782,21 +786,21 @@ class FilamentControllerFFI extends FilamentController { Future setCameraExposure( double aperture, double shutterSpeed, double sensitivity) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.set_camera_exposure(_viewer!, aperture, shutterSpeed, sensitivity); } Future setCameraRotation(double rads, double x, double y, double z) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.set_camera_rotation(_viewer!, rads, x, y, z); } Future setCameraModelMatrix(List matrix) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } assert(matrix.length == 16); @@ -810,7 +814,7 @@ class FilamentControllerFFI extends FilamentController { Future setMaterialColor(FilamentEntity asset, String meshName, int materialIndex, Color color) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } var result = _lib.set_material_color( @@ -828,21 +832,21 @@ class FilamentControllerFFI extends FilamentController { } Future transformToUnitCube(FilamentEntity asset) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.transform_to_unit_cube(_assetManager!, asset); } Future setPosition(FilamentEntity asset, double x, double y, double z) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.set_position(_assetManager!, asset, x, y, z); } Future setScale(FilamentEntity asset, double scale) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.set_scale(_assetManager!, asset, scale); @@ -850,14 +854,14 @@ class FilamentControllerFFI extends FilamentController { Future setRotation( FilamentEntity asset, double rads, double x, double y, double z) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } _lib.set_rotation(_assetManager!, asset, rads, x, y, z); } Future hide(FilamentEntity asset, String meshName) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } if (_lib.hide_mesh( @@ -866,7 +870,7 @@ class FilamentControllerFFI extends FilamentController { } Future reveal(FilamentEntity asset, String meshName) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } if (_lib.reveal_mesh( @@ -885,13 +889,13 @@ class FilamentControllerFFI extends FilamentController { } void pick(int x, int y) async { - if (_viewer == null || _resizing) { + if (_viewer == null) { throw Exception("No viewer available, ignoring"); } final outPtr = calloc(1); outPtr.value = 0; - _lib.pick_ffi(_viewer!, x, size.height.toInt() - y, outPtr); + _lib.pick_ffi(_viewer!, x, textureDetails!.height - y, outPtr); int wait = 0; while (outPtr.value == 0) { await Future.delayed(Duration(milliseconds: 50)); diff --git a/lib/filament_controller_method_channel.dart b/lib/filament_controller_method_channel.dart index 66c1e957..d5d1a7ff 100644 --- a/lib/filament_controller_method_channel.dart +++ b/lib/filament_controller_method_channel.dart @@ -670,4 +670,12 @@ class FilamentControllerMethodChannel extends FilamentController { // TODO: implement getNameForEntity throw UnimplementedError(); } + + @override + // TODO: implement textureDetails + TextureDetails? get textureDetails => throw UnimplementedError(); + + @override + // TODO: implement rendering + bool get rendering => throw UnimplementedError(); } diff --git a/lib/widgets/filament_widget.dart b/lib/widgets/filament_widget.dart index 8d9364d2..39b57080 100644 --- a/lib/widgets/filament_widget.dart +++ b/lib/widgets/filament_widget.dart @@ -9,7 +9,7 @@ import 'package:polyvox_filament/filament_controller.dart'; import 'dart:async'; -typedef ResizeCallback = void Function(Size oldSize, Size newSize); +typedef ResizeCallback = void Function(Size newSize); class ResizeObserver extends SingleChildRenderObjectWidget { final ResizeCallback onResized; @@ -36,14 +36,14 @@ class _RenderResizeObserver extends RenderProxyBox { required this.onLayoutChangedCallback, }) : super(child); - late var _oldSize = size; + Size _oldSize = Size.zero; @override - void performLayout() { + void performLayout() async { super.performLayout(); - if (size != _oldSize) { - onLayoutChangedCallback(_oldSize, size); - _oldSize = size; + if (size.width != _oldSize.width || size.height != _oldSize.height) { + onLayoutChangedCallback(size); + _oldSize = Size(size.width, size.height); } } } @@ -65,62 +65,11 @@ class FilamentWidget extends StatefulWidget { } class _FilamentWidgetState extends State { - TextureDetails? _textureDetails; - - late final AppLifecycleListener _listener; - AppLifecycleState? _lastState; - - String? _error; - int? _width; int? _height; - void _handleStateChange(AppLifecycleState state) async { - switch (state) { - case AppLifecycleState.detached: - print("Detached"); - _textureDetails = null; - - await widget.controller.destroyViewer(); - await widget.controller.destroyTexture(); - break; - case AppLifecycleState.hidden: - print("Hidden"); - if (Platform.isIOS) { - _textureDetails = null; - await widget.controller.destroyViewer(); - await widget.controller.destroyTexture(); - } - break; - case AppLifecycleState.inactive: - print("Inactive"); - break; - case AppLifecycleState.paused: - print("Paused"); - break; - case AppLifecycleState.resumed: - print("Resumed"); - if (!Platform.isWindows) { - if (_textureDetails == null) { - var size = ((context.findRenderObject()) as RenderBox).size; - print("Size after resuming : $size"); - _height = size.height.ceil(); - _width = size.width.ceil(); - await widget.controller.createViewer(_width!, _height!); - print("Created viewer Size after resuming"); - } - } - break; - } - _lastState = state; - } - @override void initState() { - _listener = AppLifecycleListener( - onStateChange: _handleStateChange, - ); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { // when attaching a debugger via Android Studio on startup, this can delay presentation of the widget // (meaning the widget may attempt to create a viewer with size 0x0). @@ -131,26 +80,166 @@ class _FilamentWidgetState extends State { var size = ((context.findRenderObject()) as RenderBox).size; _width = size.width.ceil(); _height = size.height.ceil(); - try { - _textureDetails = - await widget.controller.createViewer(_width!, _height!); - } catch (err) { - setState(() { - _error = err.toString(); - }); + setState(() {}); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (_width == null || _height == null) { + return widget.initial ?? Container(color: Colors.red); + } + + return ResizeObserver( + onResized: (newSize) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + setState(() { + _width = newSize.width.ceil(); + _height = newSize.height.ceil(); + }); + }); + }, + child: _SizedFilamentWidget( + initial: widget.initial, + width: _width!, + height: _height!, + controller: widget.controller, + )); + } +} + +class _SizedFilamentWidget extends StatefulWidget { + final int width; + final int height; + final Widget? initial; + final FilamentController controller; + + const _SizedFilamentWidget( + {super.key, + required this.width, + required this.height, + this.initial, + required this.controller}); + + @override + State createState() => _SizedFilamentWidgetState(); +} + +class _SizedFilamentWidgetState extends State<_SizedFilamentWidget> { + String? _error; + + late final AppLifecycleListener _appLifecycleListener; + AppLifecycleState? _lastState; + + @override + void initState() { + _appLifecycleListener = AppLifecycleListener( + onStateChange: _handleStateChange, + ); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + // when attaching a debugger via Android Studio on startup, this can delay presentation of the widget + // (meaning the widget may attempt to create a viewer with size 0x0). + // we just add a small delay here which should avoid this + if (!kReleaseMode) { + await Future.delayed(Duration(seconds: 2)); } + try { + widget.controller.createViewer(widget.width, widget.height); + } catch (err) { + _error = err.toString(); + } + setState(() {}); }); super.initState(); } + Timer? _resizeTimer; + bool _resizing = false; + + Future _resize() { + final completer = Completer(); + _resizeTimer?.cancel(); + + _resizeTimer = Timer(Duration(milliseconds: 200), () async { + var size = ((context.findRenderObject()) as RenderBox).size; + var width = size.width.ceil(); + var height = size.height.ceil(); + while (_resizing) { + await Future.delayed(Duration(milliseconds: 20)); + } + _resizing = true; + await widget.controller.resize(width, height); + _resizeTimer = null; + setState(() {}); + _resizing = false; + completer.complete(); + }); + return completer.future; + } + + @override + void didUpdateWidget(_SizedFilamentWidget oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.height != widget.height || oldWidget.width != widget.width) { + _resize(); + } + } + @override void dispose() { - _listener.dispose(); + _appLifecycleListener.dispose(); super.dispose(); } - Timer? _resizeTimer; + bool _wasRenderingOnInactive = false; + + void _handleStateChange(AppLifecycleState state) async { + switch (state) { + case AppLifecycleState.detached: + print("Detached"); + + if (widget.controller.textureDetails != null) { + await widget.controller.destroyViewer(); + await widget.controller.destroyTexture(); + } + break; + case AppLifecycleState.hidden: + print("Hidden"); + if (Platform.isIOS && widget.controller.textureDetails != null) { + await widget.controller.destroyViewer(); + await widget.controller.destroyTexture(); + } + break; + case AppLifecycleState.inactive: + print("Inactive"); + // on Windows in particular, restoring a window after minimizing stalls the renderer (and the whole application) for a considerable length of time. + // disabling rendering on minimize seems to fix the issue (so I wonder if there's some kind of command buffer that's filling up while the window is minimized). + _wasRenderingOnInactive = widget.controller.rendering; + await widget.controller.setRendering(false); + break; + case AppLifecycleState.paused: + print("Paused"); + break; + case AppLifecycleState.resumed: + print("Resumed"); + if (!Platform.isWindows) { + if (widget.controller.textureDetails == null) { + var size = ((context.findRenderObject()) as RenderBox).size; + widget.controller + .createViewer(size.width.ceil(), size.height.ceil()); + } + } else { + await _resize(); + } + await widget.controller.setRendering(_wasRenderingOnInactive); + break; + } + _lastState = state; + } @override Widget build(BuildContext context) { @@ -164,51 +253,26 @@ class _FilamentWidgetState extends State { ])); } - // if no texture ID is available, display the [initial] widget (solid red by default) - late Widget content; - - if (_textureDetails == null || - _textureDetails!.height != _height || - _textureDetails!.width != _width) { - content = widget.initial ?? Container(color: Colors.red); - } else { - content = Texture( - key: ObjectKey("texture_${_textureDetails!.textureId}"), - textureId: _textureDetails!.textureId, - filterQuality: FilterQuality.none, - freeze: false, - ); + if (widget.controller.textureDetails == null || _resizeTimer != null) { + return widget.initial ?? Container(color: Colors.red); } - // see [FilamentControllerFFI.resize] for an explanation of how we deal with resizing - return ResizeObserver( - onResized: (Size oldSize, Size newSize) async { - _resizeTimer?.cancel(); + var texture = Texture( + key: ObjectKey("texture_${widget.controller.textureDetails!.textureId}"), + textureId: widget.controller.textureDetails!.textureId, + filterQuality: FilterQuality.none, + freeze: false, + ); - _resizeTimer = Timer(const Duration(milliseconds: 50), () async { - var newWidth = newSize.width.ceil(); - var newHeight = newSize.height.ceil(); - try { - _textureDetails = - await widget.controller.resize(newWidth, newHeight); - setState(() { - _width = newWidth; - _height = newHeight; - }); - } catch (err) { - print(err); - } - }); - }, - child: Stack(children: [ - Positioned.fill( - child: Platform.isLinux || Platform.isWindows - ? Transform( - alignment: Alignment.center, - transform: Matrix4.rotationX( - pi), // TODO - this rotation is due to OpenGL texture coordinate working in a different space from Flutter, can we move this to the C++ side somewhere? - child: content) - : content) - ])); + return Stack(children: [ + Positioned.fill( + child: Platform.isLinux || Platform.isWindows + ? Transform( + alignment: Alignment.center, + transform: Matrix4.rotationX( + pi), // TODO - this rotation is due to OpenGL texture coordinate working in a different space from Flutter, can we move this to the C++ side somewhere? + child: texture) + : texture) + ]); } } diff --git a/pubspec.yaml b/pubspec.yaml index c5bfcd23..01a1b39e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,8 +4,8 @@ version: 0.0.1 homepage: environment: - sdk: ">=3.0.0 <3.11.0" - flutter: ">=1.20.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.16.0-0.2.pre" dependencies: flutter: diff --git a/windows/opengl_texture_buffer.cpp b/windows/opengl_texture_buffer.cpp index 0e3cf1d4..68446311 100644 --- a/windows/opengl_texture_buffer.cpp +++ b/windows/opengl_texture_buffer.cpp @@ -70,12 +70,14 @@ OpenGLTextureBuffer::OpenGLTextureBuffer( std::make_unique(flutter::PixelBufferTexture( [=](size_t width, size_t height) -> const FlutterDesktopPixelBuffer * { + if (width != this->_width || height != this->_height) { - std::cout << "Front-end widget has been resized, you need to " - "teardown/rebuild the swapchain. This pixel buffer " - "will be discarded." - << std::endl; - return nullptr; + if(!this->logged) { + std::cout << "Front-end widget expects " << width << "x" << height << " but this is " << this->_width << "x" << this->_height + << std::endl; + this->logged = true; + } + return nullptr; } uint8_t *data = (uint8_t *)pixelData.get();