feat: working implementation of multiple widgets on macos

This commit is contained in:
Nick Fisher
2024-09-30 13:45:57 +08:00
parent 22020d8607
commit fbd54a2a09
21 changed files with 382 additions and 183 deletions

View File

@@ -69,11 +69,15 @@ class DelegateInputHandler implements InputHandler {
}); });
factory DelegateInputHandler.flight(ThermionViewer viewer, factory DelegateInputHandler.flight(ThermionViewer viewer,
{PickDelegate? pickDelegate, bool freeLook=false, double? clampY, ThermionEntity? entity}) => {PickDelegate? pickDelegate,
bool freeLook = false,
double? clampY,
ThermionEntity? entity}) =>
DelegateInputHandler( DelegateInputHandler(
viewer: viewer, viewer: viewer,
pickDelegate: pickDelegate, pickDelegate: pickDelegate,
transformDelegate: FreeFlightInputHandlerDelegate(viewer, clampY:clampY, entity:entity), transformDelegate: FreeFlightInputHandlerDelegate(viewer,
clampY: clampY, entity: entity),
actions: { actions: {
InputType.MMB_HOLD_AND_MOVE: InputAction.ROTATE, InputType.MMB_HOLD_AND_MOVE: InputAction.ROTATE,
InputType.SCROLLWHEEL: InputAction.TRANSLATE, InputType.SCROLLWHEEL: InputAction.TRANSLATE,
@@ -82,8 +86,7 @@ class DelegateInputHandler implements InputHandler {
InputType.KEYDOWN_W: InputAction.TRANSLATE, InputType.KEYDOWN_W: InputAction.TRANSLATE,
InputType.KEYDOWN_S: InputAction.TRANSLATE, InputType.KEYDOWN_S: InputAction.TRANSLATE,
InputType.KEYDOWN_D: InputAction.TRANSLATE, InputType.KEYDOWN_D: InputAction.TRANSLATE,
if(freeLook) if (freeLook) InputType.POINTER_MOVE: InputAction.ROTATE,
InputType.POINTER_MOVE: InputAction.ROTATE,
}); });
bool _processing = false; bool _processing = false;
@@ -160,15 +163,13 @@ class DelegateInputHandler implements InputHandler {
} }
if (isMiddle) { if (isMiddle) {
_inputDeltas[InputType.MMB_HOLD_AND_MOVE] = _inputDeltas[InputType.MMB_HOLD_AND_MOVE] =
(_inputDeltas[InputType.MMB_HOLD_AND_MOVE] ?? Vector3.zero()) + Vector3(delta.x, delta.y, 0.0); (_inputDeltas[InputType.MMB_HOLD_AND_MOVE] ?? Vector3.zero()) +
Vector3(delta.x, delta.y, 0.0);
} else { } else {
_inputDeltas[InputType.LMB_HOLD_AND_MOVE] = _inputDeltas[InputType.LMB_HOLD_AND_MOVE] =
(_inputDeltas[InputType.LMB_HOLD_AND_MOVE] ?? Vector3.zero()) + Vector3(delta.x, delta.y, 0.0); (_inputDeltas[InputType.LMB_HOLD_AND_MOVE] ?? Vector3.zero()) +
Vector3(delta.x, delta.y, 0.0);
} }
// else {
// _inputDeltas[InputType.POINTER_MOVE] =
// (_inputDeltas[InputType.POINTER_MOVE] ?? Vector3.zero()) + delta;
// }
} }
@override @override
@@ -180,7 +181,8 @@ class DelegateInputHandler implements InputHandler {
return; return;
} }
_inputDeltas[InputType.POINTER_MOVE] = _inputDeltas[InputType.POINTER_MOVE] =
(_inputDeltas[InputType.POINTER_MOVE] ?? Vector3.zero()) + Vector3(delta.x, delta.y, 0.0); (_inputDeltas[InputType.POINTER_MOVE] ?? Vector3.zero()) +
Vector3(delta.x, delta.y, 0.0);
} }
@override @override
@@ -191,8 +193,8 @@ class DelegateInputHandler implements InputHandler {
} }
try { try {
_inputDeltas[InputType.SCROLLWHEEL] = _inputDeltas[InputType.SCROLLWHEEL] =
(_inputDeltas[InputType.SCROLLWHEEL] ?? Vector3.zero()) (_inputDeltas[InputType.SCROLLWHEEL] ?? Vector3.zero()) +
+ Vector3(0,0, scrollDelta > 0 ? 1 : -1); Vector3(0, 0, scrollDelta > 0 ? 1 : -1);
} catch (e) { } catch (e) {
_logger.warning("Error during scroll accumulation: $e"); _logger.warning("Error during scroll accumulation: $e");
} }

View File

@@ -39,4 +39,18 @@ class FFIView extends View {
final engine = Viewer_getEngine(viewer); final engine = Viewer_getEngine(viewer);
return FFICamera(View_getCamera(view), engine); return FFICamera(View_getCamera(view), engine);
} }
@override
Future setAntiAliasing(bool msaa, bool fxaa, bool taa) async {
View_setAntiAliasing(view, msaa, fxaa, taa);
}
@override
Future setPostProcessing(bool enabled) async {
View_setPostProcessing(view, enabled);
}
Future setRenderable(bool renderable) async {
Viewer_markViewRenderable(viewer, view, renderable);
}
} }

View File

@@ -65,7 +65,6 @@ external void Viewer_destroySwapChain(
@ffi.Native< @ffi.Native<
ffi.Bool Function( ffi.Bool Function(
ffi.Pointer<TViewer>, ffi.Pointer<TViewer>,
ffi.Pointer<TView>,
ffi.Pointer<TSwapChain>, ffi.Pointer<TSwapChain>,
ffi.Uint64, ffi.Uint64,
ffi.Pointer<ffi.Void>, ffi.Pointer<ffi.Void>,
@@ -76,7 +75,6 @@ external void Viewer_destroySwapChain(
ffi.Pointer<ffi.Void>)>(isLeaf: true) ffi.Pointer<ffi.Void>)>(isLeaf: true)
external bool Viewer_render( external bool Viewer_render(
ffi.Pointer<TViewer> viewer, ffi.Pointer<TViewer> viewer,
ffi.Pointer<TView> view,
ffi.Pointer<TSwapChain> swapChain, ffi.Pointer<TSwapChain> swapChain,
int frameTimeInNanos, int frameTimeInNanos,
ffi.Pointer<ffi.Void> pixelBuffer, ffi.Pointer<ffi.Void> pixelBuffer,
@@ -146,6 +144,15 @@ external ffi.Pointer<TSwapChain> Viewer_getSwapChainAt(
int index, int index,
); );
@ffi.Native<
ffi.Void Function(
ffi.Pointer<TViewer>, ffi.Pointer<TView>, ffi.Bool)>(isLeaf: true)
external void Viewer_markViewRenderable(
ffi.Pointer<TViewer> viewer,
ffi.Pointer<TView> view,
bool renderable,
);
@ffi.Native<ffi.Pointer<TEngine> Function(ffi.Pointer<TViewer>)>(isLeaf: true) @ffi.Native<ffi.Pointer<TEngine> Function(ffi.Pointer<TViewer>)>(isLeaf: true)
external ffi.Pointer<TEngine> Viewer_getEngine( external ffi.Pointer<TEngine> Viewer_getEngine(
ffi.Pointer<TViewer> viewer, ffi.Pointer<TViewer> viewer,
@@ -1451,6 +1458,15 @@ external void Viewer_captureRenderTargetRenderThread(
ffi.Pointer<ffi.NativeFunction<ffi.Void Function()>> onComplete, ffi.Pointer<ffi.NativeFunction<ffi.Void Function()>> onComplete,
); );
@ffi.Native<
ffi.Void Function(ffi.Pointer<TViewer>, ffi.Pointer<TSwapChain>,
ffi.Pointer<ffi.NativeFunction<ffi.Void Function()>>)>(isLeaf: true)
external void Viewer_requestFrameRenderThread(
ffi.Pointer<TViewer> viewer,
ffi.Pointer<TSwapChain> tSwapChain,
ffi.Pointer<ffi.NativeFunction<ffi.Void Function()>> onComplete,
);
@ffi.Native<ffi.Void Function(ffi.Pointer<TViewer>)>(isLeaf: true) @ffi.Native<ffi.Void Function(ffi.Pointer<TViewer>)>(isLeaf: true)
external void destroy_filament_viewer_render_thread( external void destroy_filament_viewer_render_thread(
ffi.Pointer<TViewer> viewer, ffi.Pointer<TViewer> viewer,
@@ -1471,19 +1487,6 @@ external void set_rendering_render_thread(
ffi.Pointer<ffi.NativeFunction<ffi.Void Function()>> onComplete, ffi.Pointer<ffi.NativeFunction<ffi.Void Function()>> onComplete,
); );
@ffi.Native<
ffi.Void Function(
ffi.Pointer<TViewer>,
ffi.Pointer<TView>,
ffi.Pointer<TSwapChain>,
ffi.Pointer<ffi.NativeFunction<ffi.Void Function()>>)>(isLeaf: true)
external void Viewer_requestFrameRenderThread(
ffi.Pointer<TViewer> viewer,
ffi.Pointer<TView> view,
ffi.Pointer<TSwapChain> tSwapChain,
ffi.Pointer<ffi.NativeFunction<ffi.Void Function()>> onComplete,
);
@ffi.Native<ffi.Void Function(ffi.Pointer<TViewer>, ffi.Float)>(isLeaf: true) @ffi.Native<ffi.Void Function(ffi.Pointer<TViewer>, ffi.Float)>(isLeaf: true)
external void set_frame_interval_render_thread( external void set_frame_interval_render_thread(
ffi.Pointer<TViewer> viewer, ffi.Pointer<TViewer> viewer,
@@ -1888,14 +1891,25 @@ external void View_setBloom(
); );
@ffi.Native< @ffi.Native<
ffi.Void Function( ffi.Void Function(ffi.Pointer<TView>, ffi.Pointer<TEngine>,
ffi.Pointer<TView>, ffi.Pointer<TEngine>, ffi.Int32)>(isLeaf: true) ffi.UnsignedInt)>(symbol: "View_setToneMapping", isLeaf: true)
external void View_setToneMapping( external void _View_setToneMapping(
ffi.Pointer<TView> tView, ffi.Pointer<TView> tView,
ffi.Pointer<TEngine> tEngine, ffi.Pointer<TEngine> tEngine,
int toneMapping, int toneMapping,
); );
void View_setToneMapping(
ffi.Pointer<TView> tView,
ffi.Pointer<TEngine> tEngine,
ToneMapping toneMapping,
) =>
_View_setToneMapping(
tView,
tEngine,
toneMapping.value,
);
@ffi.Native< @ffi.Native<
ffi.Void Function( ffi.Void Function(
ffi.Pointer<TView>, ffi.Bool, ffi.Bool, ffi.Bool)>(isLeaf: true) ffi.Pointer<TView>, ffi.Bool, ffi.Bool, ffi.Bool)>(isLeaf: true)
@@ -2224,10 +2238,20 @@ final class TViewport extends ffi.Struct {
external int height; external int height;
} }
abstract class ToneMapping { enum ToneMapping {
static const int ACES = 0; ACES(0),
static const int FILMIC = 1; FILMIC(1),
static const int LINEAR = 2; LINEAR(2);
final int value;
const ToneMapping(this.value);
static ToneMapping fromValue(int value) => switch (value) {
0 => ACES,
1 => FILMIC,
2 => LINEAR,
_ => throw ArgumentError("Unknown value for ToneMapping: $value"),
};
} }
typedef GizmoPickCallback typedef GizmoPickCallback

View File

@@ -2040,10 +2040,8 @@ class ThermionViewerFFI extends ThermionViewer {
}); });
final swapChain = Viewer_getSwapChainAt(_viewer!, 0); final swapChain = Viewer_getSwapChainAt(_viewer!, 0);
final view = Viewer_getViewAt(_viewer!, 0);
Viewer_requestFrameRenderThread( Viewer_requestFrameRenderThread(_viewer!, swapChain, callback.nativeFunction);
_viewer!, view, swapChain, callback.nativeFunction);
await completer.future; await completer.future;
} }

View File

@@ -1,6 +1,7 @@
library shared_types; library shared_types;
export 'swap_chain.dart'; export 'swap_chain.dart';
export 'view.dart';
export 'render_target.dart'; export 'render_target.dart';
export 'camera.dart'; export 'camera.dart';
export 'material.dart'; export 'material.dart';

View File

@@ -15,4 +15,7 @@ abstract class View {
Future setRenderTarget(covariant RenderTarget? renderTarget); Future setRenderTarget(covariant RenderTarget? renderTarget);
Future setCamera(covariant Camera camera); Future setCamera(covariant Camera camera);
Camera getCamera(); Camera getCamera();
Future setPostProcessing(bool enabled);
Future setAntiAliasing(bool msaa, bool fxaa, bool taa);
Future setRenderable(bool renderable);
} }

View File

@@ -19,7 +19,7 @@ extern "C"
typedef struct TView TView; typedef struct TView TView;
typedef struct TGizmo TGizmo; typedef struct TGizmo TGizmo;
typedef struct TScene TScene; typedef struct TScene TScene;
struct TMaterialKey { struct TMaterialKey {
bool doubleSided = 1; bool doubleSided = 1;
bool unlit = 1; bool unlit = 1;

View File

@@ -70,7 +70,6 @@ namespace thermion
bool render( bool render(
uint64_t frameTimeInNanos, uint64_t frameTimeInNanos,
View* view,
SwapChain* swapChain, SwapChain* swapChain,
void *pixelBuffer, void *pixelBuffer,
void (*callback)(void *buf, size_t size, void *data), void (*callback)(void *buf, size_t size, void *data),
@@ -92,6 +91,22 @@ namespace thermion
Renderer *getRenderer(); Renderer *getRenderer();
std::vector<View*> _renderable;
void setRenderable(View* view, bool renderable) {
auto it = std::find(_renderable.begin(), _renderable.end(), view);
if(renderable) {
if(it == _renderable.end()) {
_renderable.push_back(view);
}
} else {
if(it != _renderable.end()) {
_renderable.erase(it);
}
}
}
void setBackgroundColor(const float r, const float g, const float b, const float a); void setBackgroundColor(const float r, const float g, const float b, const float a);
void setBackgroundImage(const char *resourcePath, bool fillHeight, uint32_t width, uint32_t height); void setBackgroundImage(const char *resourcePath, bool fillHeight, uint32_t width, uint32_t height);
void clearBackgroundImage(); void clearBackgroundImage();

View File

@@ -63,7 +63,6 @@ extern "C"
EMSCRIPTEN_KEEPALIVE void Viewer_destroySwapChain(TViewer *viewer, TSwapChain* swapChain); EMSCRIPTEN_KEEPALIVE void Viewer_destroySwapChain(TViewer *viewer, TSwapChain* swapChain);
EMSCRIPTEN_KEEPALIVE bool Viewer_render( EMSCRIPTEN_KEEPALIVE bool Viewer_render(
TViewer *viewer, TViewer *viewer,
TView *view,
TSwapChain *swapChain, TSwapChain *swapChain,
uint64_t frameTimeInNanos, uint64_t frameTimeInNanos,
void *pixelBuffer, void *pixelBuffer,
@@ -85,13 +84,13 @@ extern "C"
EMSCRIPTEN_KEEPALIVE TView* Viewer_createView(TViewer *viewer); EMSCRIPTEN_KEEPALIVE TView* Viewer_createView(TViewer *viewer);
EMSCRIPTEN_KEEPALIVE TView* Viewer_getViewAt(TViewer *viewer, int index); EMSCRIPTEN_KEEPALIVE TView* Viewer_getViewAt(TViewer *viewer, int index);
EMSCRIPTEN_KEEPALIVE void Viewer_setMainCamera(TViewer *tViewer, TView *tView); EMSCRIPTEN_KEEPALIVE void Viewer_setMainCamera(TViewer *tViewer, TView *tView);
EMSCRIPTEN_KEEPALIVE TSwapChain* Viewer_getSwapChainAt(TViewer *tViewer, int index); EMSCRIPTEN_KEEPALIVE TSwapChain* Viewer_getSwapChainAt(TViewer *tViewer, int index);
EMSCRIPTEN_KEEPALIVE void Viewer_markViewRenderable(TViewer *viewer, TView* view, bool renderable);
// Engine // Engine
EMSCRIPTEN_KEEPALIVE TEngine *Viewer_getEngine(TViewer* viewer); EMSCRIPTEN_KEEPALIVE TEngine *Viewer_getEngine(TViewer* viewer);
EMSCRIPTEN_KEEPALIVE TCamera *Engine_getCameraComponent(TEngine* tEngine, EntityId entityId); EMSCRIPTEN_KEEPALIVE TCamera *Engine_getCameraComponent(TEngine* tEngine, EntityId entityId);
EMSCRIPTEN_KEEPALIVE void Engine_setTransform(TEngine* tEngine, EntityId entity, double4x4 transform); EMSCRIPTEN_KEEPALIVE void Engine_setTransform(TEngine* tEngine, EntityId entity, double4x4 transform);
EMSCRIPTEN_KEEPALIVE void clear_background_image(TViewer *viewer); EMSCRIPTEN_KEEPALIVE void clear_background_image(TViewer *viewer);
EMSCRIPTEN_KEEPALIVE void set_background_image(TViewer *viewer, const char *path, bool fillHeight); EMSCRIPTEN_KEEPALIVE void set_background_image(TViewer *viewer, const char *path, bool fillHeight);

View File

@@ -30,12 +30,13 @@ extern "C"
EMSCRIPTEN_KEEPALIVE void Viewer_renderRenderThread(TViewer *viewer, TView* view, TSwapChain* swapChain); EMSCRIPTEN_KEEPALIVE void Viewer_renderRenderThread(TViewer *viewer, TView* view, TSwapChain* swapChain);
EMSCRIPTEN_KEEPALIVE void Viewer_captureRenderThread(TViewer *viewer, TView* view, TSwapChain* swapChain, uint8_t* out, void (*onComplete)()); EMSCRIPTEN_KEEPALIVE void Viewer_captureRenderThread(TViewer *viewer, TView* view, TSwapChain* swapChain, uint8_t* out, void (*onComplete)());
EMSCRIPTEN_KEEPALIVE void Viewer_captureRenderTargetRenderThread(TViewer *viewer, TView* view, TSwapChain* swapChain, TRenderTarget* renderTarget, uint8_t* out, void (*onComplete)()); EMSCRIPTEN_KEEPALIVE void Viewer_captureRenderTargetRenderThread(TViewer *viewer, TView* view, TSwapChain* swapChain, TRenderTarget* renderTarget, uint8_t* out, void (*onComplete)());
EMSCRIPTEN_KEEPALIVE void Viewer_requestFrameRenderThread(TViewer *viewer, TSwapChain* tSwapChain, void(*onComplete)());
EMSCRIPTEN_KEEPALIVE void destroy_filament_viewer_render_thread(TViewer *viewer); EMSCRIPTEN_KEEPALIVE void destroy_filament_viewer_render_thread(TViewer *viewer);
EMSCRIPTEN_KEEPALIVE FilamentRenderCallback make_render_callback_fn_pointer(FilamentRenderCallback); EMSCRIPTEN_KEEPALIVE FilamentRenderCallback make_render_callback_fn_pointer(FilamentRenderCallback);
EMSCRIPTEN_KEEPALIVE void set_rendering_render_thread(TViewer *viewer, bool rendering, void(*onComplete)()); EMSCRIPTEN_KEEPALIVE void set_rendering_render_thread(TViewer *viewer, bool rendering, void(*onComplete)());
EMSCRIPTEN_KEEPALIVE void Viewer_requestFrameRenderThread(TViewer *viewer, TView *view, TSwapChain *tSwapChain, void(*onComplete)());
EMSCRIPTEN_KEEPALIVE void set_frame_interval_render_thread(TViewer *viewer, float frameInterval); EMSCRIPTEN_KEEPALIVE void set_frame_interval_render_thread(TViewer *viewer, float frameInterval);
EMSCRIPTEN_KEEPALIVE void set_background_color_render_thread(TViewer *viewer, const float r, const float g, const float b, const float a); EMSCRIPTEN_KEEPALIVE void set_background_color_render_thread(TViewer *viewer, const float r, const float g, const float b, const float a);
EMSCRIPTEN_KEEPALIVE void clear_background_image_render_thread(TViewer *viewer); EMSCRIPTEN_KEEPALIVE void clear_background_image_render_thread(TViewer *viewer);

View File

@@ -166,6 +166,8 @@ namespace thermion
createView(); createView();
setRenderable(_views[0], true);
const float aperture = _mainCamera->getAperture(); const float aperture = _mainCamera->getAperture();
const float shutterSpeed = _mainCamera->getShutterSpeed(); const float shutterSpeed = _mainCamera->getShutterSpeed();
const float sens = _mainCamera->getSensitivity(); const float sens = _mainCamera->getSensitivity();
@@ -687,6 +689,7 @@ namespace thermion
RenderTarget *FilamentViewer::createRenderTarget(intptr_t texture, uint32_t width, uint32_t height) RenderTarget *FilamentViewer::createRenderTarget(intptr_t texture, uint32_t width, uint32_t height)
{ {
Log("Creating render target with size %d x %d", width, height);
// Create filament textures and render targets (note the color buffer has the import call) // Create filament textures and render targets (note the color buffer has the import call)
auto rtColor = filament::Texture::Builder() auto rtColor = filament::Texture::Builder()
.width(width) .width(width)
@@ -707,6 +710,7 @@ namespace thermion
.texture(RenderTarget::AttachmentPoint::COLOR, rtColor) .texture(RenderTarget::AttachmentPoint::COLOR, rtColor)
.texture(RenderTarget::AttachmentPoint::DEPTH, rtDepth) .texture(RenderTarget::AttachmentPoint::DEPTH, rtDepth)
.build(*_engine); .build(*_engine);
_renderTargets.push_back(rt);
return rt; return rt;
} }
@@ -800,6 +804,9 @@ namespace thermion
_sceneManager->destroyAll(); _sceneManager->destroyAll();
} }
/// @brief
/// @param asset
///
void FilamentViewer::removeEntity(EntityId asset) void FilamentViewer::removeEntity(EntityId asset)
{ {
_renderMutex.lock(); _renderMutex.lock();
@@ -1015,14 +1022,13 @@ namespace thermion
bool FilamentViewer::render( bool FilamentViewer::render(
uint64_t frameTimeInNanos, uint64_t frameTimeInNanos,
View *view,
SwapChain *swapChain, SwapChain *swapChain,
void *pixelBuffer, void *pixelBuffer,
void (*callback)(void *buf, size_t size, void *data), void (*callback)(void *buf, size_t size, void *data),
void *data) void *data)
{ {
if (!view || !swapChain) if (!swapChain)
{ {
return false; return false;
} }
@@ -1050,15 +1056,11 @@ namespace thermion
if (!beginFrame) if (!beginFrame)
{ {
_skippedFrames++; _skippedFrames++;
} } else {
for(auto *view : _renderable) {
if (beginFrame) _renderer->render(view);
{ }
_renderer->render(view);
_frameCount++; _frameCount++;
_renderer->endFrame(); _renderer->endFrame();
} }
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__

View File

@@ -341,7 +341,6 @@ extern "C"
EMSCRIPTEN_KEEPALIVE bool Viewer_render( EMSCRIPTEN_KEEPALIVE bool Viewer_render(
TViewer *tViewer, TViewer *tViewer,
TView *tView,
TSwapChain *tSwapChain, TSwapChain *tSwapChain,
uint64_t frameTimeInNanos, uint64_t frameTimeInNanos,
void *pixelBuffer, void *pixelBuffer,
@@ -355,8 +354,14 @@ extern "C"
swapChain = viewer->getSwapChainAt(0); swapChain = viewer->getSwapChainAt(0);
} }
return viewer->render(frameTimeInNanos, swapChain, pixelBuffer, callback, data);
}
EMSCRIPTEN_KEEPALIVE void Viewer_markViewRenderable(TViewer *tViewer, TView* tView, bool renderable) {
auto viewer = reinterpret_cast<FilamentViewer *>(tViewer);
auto *view = reinterpret_cast<View*>(tView); auto *view = reinterpret_cast<View*>(tView);
return viewer->render(frameTimeInNanos, view, swapChain, pixelBuffer, callback, data); viewer->setRenderable(view, renderable);
} }
EMSCRIPTEN_KEEPALIVE void Viewer_capture( EMSCRIPTEN_KEEPALIVE void Viewer_capture(

View File

@@ -50,7 +50,7 @@ public:
~RenderLoop() ~RenderLoop()
{ {
_stop = true; _stop = true;
target = nullptr; swapChain = nullptr;
_cv.notify_one(); _cv.notify_one();
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
pthread_join(t, NULL); pthread_join(t, NULL);
@@ -82,21 +82,21 @@ public:
} }
} }
void requestFrame(TView* tView, TSwapChain* tSwapChain, void (*callback)()) void requestFrame(TSwapChain* tSwapChain, void (*callback)())
{ {
this->target = tSwapChain; std::unique_lock<std::mutex> lock(_mutex);
this->view = tView; this->swapChain = tSwapChain;
this->_requestFrameRenderCallback = callback; this->_requestFrameRenderCallback = callback;
} }
void iter() void iter()
{ {
std::unique_lock<std::mutex> lock(_mutex); std::unique_lock<std::mutex> lock(_mutex);
if (target) if (swapChain)
{ {
doRender(view, target); doRender(swapChain);
this->_requestFrameRenderCallback(); this->_requestFrameRenderCallback();
target = nullptr; swapChain = nullptr;
// Calculate and print FPS // Calculate and print FPS
auto currentTime = std::chrono::high_resolution_clock::now(); auto currentTime = std::chrono::high_resolution_clock::now();
@@ -126,8 +126,6 @@ public:
_cv.wait_for(lock, std::chrono::microseconds(1000), [this] _cv.wait_for(lock, std::chrono::microseconds(1000), [this]
{ return !_tasks.empty() || _stop; }); { return !_tasks.empty() || _stop; });
if (_stop)
return;
} }
void createViewer(void *const context, void createViewer(void *const context,
@@ -170,14 +168,14 @@ public:
{ {
std::packaged_task<void()> lambda([=]() mutable std::packaged_task<void()> lambda([=]() mutable
{ {
target = nullptr; swapChain = nullptr;
_viewer = nullptr; _viewer = nullptr;
destroy_filament_viewer(reinterpret_cast<TViewer*>(viewer)); }); destroy_filament_viewer(reinterpret_cast<TViewer*>(viewer)); });
auto fut = add_task(lambda); auto fut = add_task(lambda);
fut.wait(); fut.wait();
} }
bool doRender(TView* tView, TSwapChain *tSwapChain) bool doRender(TSwapChain *tSwapChain)
{ {
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
if (emscripten_is_webgl_context_lost(_context) == EM_TRUE) if (emscripten_is_webgl_context_lost(_context) == EM_TRUE)
@@ -188,7 +186,7 @@ public:
return; return;
} }
#endif #endif
auto rendered = Viewer_render(_viewer, tView, tSwapChain, 0, nullptr, nullptr, nullptr); bool rendered = Viewer_render(_viewer, tSwapChain, 0, nullptr, nullptr, nullptr);
if (_renderCallback) if (_renderCallback)
{ {
_renderCallback(_renderCallbackOwner); _renderCallback(_renderCallbackOwner);
@@ -218,8 +216,7 @@ public:
} }
public: public:
TSwapChain *target; TSwapChain *swapChain;
TView *view;
private: private:
void(*_requestFrameRenderCallback)() = nullptr; void(*_requestFrameRenderCallback)() = nullptr;
@@ -307,7 +304,7 @@ extern "C"
} }
EMSCRIPTEN_KEEPALIVE void Viewer_requestFrameRenderThread(TViewer *viewer, TView* view, TSwapChain* tSwapChain, void(*onComplete)()) EMSCRIPTEN_KEEPALIVE void Viewer_requestFrameRenderThread(TViewer *viewer, TSwapChain* tSwapChain, void(*onComplete)())
{ {
if (!_rl) if (!_rl)
{ {
@@ -315,7 +312,7 @@ extern "C"
} }
else else
{ {
_rl->requestFrame(view, tSwapChain, onComplete); _rl->requestFrame(tSwapChain, onComplete);
} }
} }
@@ -331,7 +328,9 @@ extern "C"
EMSCRIPTEN_KEEPALIVE void Viewer_renderRenderThread(TViewer *viewer, TView *tView, TSwapChain *tSwapChain) EMSCRIPTEN_KEEPALIVE void Viewer_renderRenderThread(TViewer *viewer, TView *tView, TSwapChain *tSwapChain)
{ {
std::packaged_task<void()> lambda([=]() mutable std::packaged_task<void()> lambda([=]() mutable
{ _rl->doRender(tView, tSwapChain); }); {
_rl->doRender(tSwapChain);
});
auto fut = _rl->add_task(lambda); auto fut = _rl->add_task(lambda);
} }

View File

@@ -1,7 +1,7 @@
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
typedef ResizeCallback = void Function(Size newSize); typedef ResizeCallback = void Function(Size oldSize, Size newSize);
class ResizeObserver extends SingleChildRenderObjectWidget { class ResizeObserver extends SingleChildRenderObjectWidget {
final ResizeCallback onResized; final ResizeCallback onResized;
@@ -34,7 +34,7 @@ class _RenderResizeObserver extends RenderProxyBox {
void performLayout() async { void performLayout() async {
super.performLayout(); super.performLayout();
if (size.width != _oldSize.width || size.height != _oldSize.height) { if (size.width != _oldSize.width || size.height != _oldSize.height) {
onLayoutChangedCallback(size); onLayoutChangedCallback(_oldSize, size);
_oldSize = Size(size.width, size.height); _oldSize = Size(size.width, size.height);
} }
} }

View File

@@ -124,7 +124,6 @@ class _ThermionListenerWidgetState extends State<ThermionListenerWidget> {
return widget.child ?? Container(); return widget.child ?? Container();
} }
return Stack(children: [ return Stack(children: [
if (widget.child != null) Positioned.fill(child: widget.child!),
if (isDesktop) Positioned.fill(child: _desktop(pixelRatio)), if (isDesktop) Positioned.fill(child: _desktop(pixelRatio)),
if (!isDesktop) Positioned.fill(child: _mobile(pixelRatio)) if (!isDesktop) Positioned.fill(child: _mobile(pixelRatio))
]); ]);

View File

@@ -26,14 +26,21 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
ThermionFlutterTexture? _texture; ThermionFlutterTexture? _texture;
RenderTarget? _renderTarget; RenderTarget? _renderTarget;
static final _views = <t.View>[];
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
_views.remove(widget.view);
_texture?.destroy(); _texture?.destroy();
} }
@override @override
void initState() { void initState() {
if (_views.contains(widget.view)) {
throw Exception("View already embedded in a widget");
}
_views.add(widget.view);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
await widget.viewer.initialized; await widget.viewer.initialized;
@@ -51,9 +58,10 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
await widget.view.setRenderTarget(_renderTarget!); await widget.view.setRenderTarget(_renderTarget!);
await widget.view.updateViewport(width, height); await widget.view.updateViewport(_texture!.width, _texture!.height);
var camera = await widget.view.getCamera(); var camera = await widget.view.getCamera();
await camera.setLensProjection(aspect: width / height); await camera.setLensProjection(
aspect: _texture!.width / _texture!.height);
if (mounted) { if (mounted) {
setState(() {}); setState(() {});
@@ -73,18 +81,29 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
await _renderTarget!.destroy(); await _renderTarget!.destroy();
texture.destroy(); texture.destroy();
} }
_views.clear();
}); });
}); });
_callbackId = _numCallbacks;
_numCallbacks++;
super.initState(); super.initState();
} }
bool _rendering = false; bool _rendering = false;
static int _numCallbacks = 0;
static int _primaryCallback = 0;
late int _callbackId;
int lastRender = 0;
void _requestFrame() { void _requestFrame() {
WidgetsBinding.instance.scheduleFrameCallback((d) async { WidgetsBinding.instance.scheduleFrameCallback((d) async {
if (!_rendering) { if (widget.viewer.rendering && !_rendering) {
_rendering = true; _rendering = true;
await widget.viewer.requestFrame(); if (_callbackId == _primaryCallback) {
await widget.viewer.requestFrame();
lastRender = d.inMilliseconds;
}
await _texture?.markFrameAvailable(); await _texture?.markFrameAvailable();
_rendering = false; _rendering = false;
} }
@@ -92,29 +111,37 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
}); });
} }
bool _resizing = false; final _resizing = <Future>[];
Timer? _resizeTimer; Timer? _resizeTimer;
Future _resize(Size newSize) async { Future _resize(Size oldSize, Size newSize) async {
await Future.wait(_resizing);
_resizeTimer?.cancel(); _resizeTimer?.cancel();
_resizeTimer = Timer(const Duration(milliseconds: 10), () async { _resizeTimer = Timer(const Duration(milliseconds: 100), () async {
if (_resizing || !mounted) { await Future.wait(_resizing);
return;
}
_resizeTimer!.cancel();
_resizing = true;
if (!mounted) { if (!mounted) {
return; return;
} }
if (newSize.width == _texture?.width &&
newSize.height == _texture?.height) {
return;
}
final completer = Completer();
_resizing.add(completer.future);
newSize *= MediaQuery.of(context).devicePixelRatio; newSize *= MediaQuery.of(context).devicePixelRatio;
var newWidth = newSize.width.ceil(); var newWidth = newSize.width.ceil();
var newHeight = newSize.height.ceil(); var newHeight = newSize.height.ceil();
var lastTextureId = _texture?.hardwareId;
await _texture?.resize( await _texture?.resize(
newWidth, newWidth,
newHeight, newHeight,
@@ -122,12 +149,23 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
0, 0,
); );
await widget.view.updateViewport(newWidth, newHeight); if (_texture?.hardwareId != lastTextureId) {
var camera = await widget.view.getCamera(); await _renderTarget?.destroy();
await camera.setLensProjection(aspect: newWidth / newHeight); _renderTarget = await widget.viewer.createRenderTarget(
_texture!.width, _texture!.height, _texture!.hardwareId);
await widget.view.setRenderTarget(_renderTarget!);
}
await widget.view.updateViewport(_texture!.width, _texture!.height);
var camera = await widget.view.getCamera();
await camera.setLensProjection(
aspect: _texture!.width.toDouble() / _texture!.height.toDouble());
if (!mounted) {
return;
}
setState(() {}); setState(() {});
_resizing = false; completer.complete();
_resizing.remove(completer.future);
}); });
} }
@@ -137,30 +175,17 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
return widget.initial ?? Container(color: Colors.red); return widget.initial ?? Container(color: Colors.red);
} }
return Stack(children: [ return ResizeObserver(
Positioned.fill( onResized: _resize,
child: ResizeObserver( child: Stack(children: [
onResized: _resize, Positioned.fill(
child: Stack(children: [ child: Texture(
Positioned.fill( key: ObjectKey("flutter_texture_${_texture!.flutterId}"),
child: Texture( textureId: _texture!.flutterId,
key: ObjectKey("flutter_texture_${_texture!.flutterId}"), filterQuality: FilterQuality.none,
textureId: _texture!.flutterId, freeze: false,
filterQuality: FilterQuality.none, ))
freeze: false, ]));
))
]))),
Align(
alignment: Alignment.bottomLeft,
child: ElevatedButton(
onPressed: () async {
var img =
await widget.viewer.capture(renderTarget: _renderTarget!);
print(img);
},
child: Text("CAPTURE")),
)
]);
} }
} }

View File

@@ -5,8 +5,8 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
var registrar : FlutterPluginRegistrar var registrar : FlutterPluginRegistrar
var registry: FlutterTextureRegistry var registry: FlutterTextureRegistry
var texture: ThermionFlutterTexture? var textures: [Int64: ThermionFlutterTexture] = [:]
var createdAt = Date() var createdAt = Date()
var destroying = false var destroying = false
@@ -72,6 +72,13 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
self.registry = textureRegistry; self.registry = textureRegistry;
self.registrar = registrar self.registrar = registrar
} }
var markTextureFrameAvailable : @convention(c) (UnsafeMutableRawPointer?) -> () = { instancePtr in
let instance:SwiftThermionFlutterPlugin = Unmanaged<SwiftThermionFlutterPlugin>.fromOpaque(instancePtr!).takeUnretainedValue()
for (_, texture) in instance.textures {
instance.registry.textureFrameAvailable(texture.flutterTextureId)
}
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let methodName = call.method; let methodName = call.method;
@@ -86,33 +93,39 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
registry.textureFrameAvailable(flutterTextureId) registry.textureFrameAvailable(flutterTextureId)
result(nil) result(nil)
case "getRenderCallback": case "getRenderCallback":
result(nil) if(renderCallbackHolder.isEmpty) {
renderCallbackHolder.append(unsafeBitCast(markTextureFrameAvailable, to:Int64.self))
renderCallbackHolder.append(unsafeBitCast(Unmanaged.passUnretained(self), to:UInt64.self))
}
result(renderCallbackHolder)
case "getDriverPlatform": case "getDriverPlatform":
result(nil) result(nil)
case "getSharedContext": case "getSharedContext":
result(nil) result(nil)
case "createTexture": case "createTexture":
if(destroying) {
result(nil)
return
}
let args = call.arguments as! [Any] let args = call.arguments as! [Any]
let width = args[0] as! Int64 let width = args[0] as! Int64
let height = args[1] as! Int64 let height = args[1] as! Int64
self.texture = ThermionFlutterTexture(registry: registry, width: width, height: height) let texture = ThermionFlutterTexture(registry: registry, width: width, height: height)
if(self.texture!.texture.metalTextureAddress == -1) { if texture.texture.metalTextureAddress == -1 {
result(nil) result(nil)
} else { } else {
result([self.texture!.flutterTextureId as Any, self.texture!.texture.metalTextureAddress, nil]) textures[texture.flutterTextureId] = texture
result([texture.flutterTextureId, texture.texture.metalTextureAddress, nil])
} }
case "destroyTexture": case "destroyTexture":
self.destroying = true let args = call.arguments as! [Any]
self.texture?.destroy() let flutterTextureId = args[0] as! Int64
self.texture = nil
result(true) if let texture = textures[flutterTextureId] {
self.destroying = false texture.destroy()
textures.removeValue(forKey: flutterTextureId)
result(true)
} else {
result(false)
}
default: default:
result(FlutterMethodNotImplemented) result(FlutterMethodNotImplemented)
} }

View File

@@ -23,7 +23,7 @@ public class ThermionFlutterTexture : NSObject, FlutterTexture {
} }
public func onTextureUnregistered(_ texture:FlutterTexture) { public func onTextureUnregistered(_ texture:FlutterTexture) {
print("Texture unregistered")
} }
public func destroy() { public func destroy() {

View File

@@ -13,47 +13,36 @@ import 'package:logging/logging.dart';
/// ///
class ThermionFlutterMacOS extends ThermionFlutterMethodChannelInterface { class ThermionFlutterMacOS extends ThermionFlutterMethodChannelInterface {
final _channel = const MethodChannel("dev.thermion.flutter/event"); final _channel = const MethodChannel("dev.thermion.flutter/event");
final _logger = Logger("ThermionFlutterFFI"); final _logger = Logger("ThermionFlutterMacOS");
SwapChain? _swapChain; static SwapChain? _swapChain;
ThermionFlutterMacOS._() {} ThermionFlutterMacOS._();
static ThermionFlutterMacOS? instance;
static void registerWith() { static void registerWith() {
ThermionFlutterPlatform.instance = ThermionFlutterMacOS._(); instance ??= ThermionFlutterMacOS._();
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 is the headless swap chain
// since we will be using render targets, the actual dimensions don't matter
_swapChain = await viewer.createSwapChain(1, 1);
return viewer;
} }
// On desktop platforms, textures are always created // On desktop platforms, textures are always created
Future<ThermionFlutterTexture?> createTexture(int width, int height) async { Future<ThermionFlutterTexture?> createTexture(int width, int height) async {
if (_swapChain == null) { var texture = MacOSMethodChannelFlutterTexture(_channel);
// this is the headless swap chain await texture.resize(width, height, 0, 0);
// since we will be using render targets, the actual dimensions don't matter return texture;
_swapChain = await viewer!.createSwapChain(width, height);
}
// Get screen width and height
int screenWidth = width; //1920;
int screenHeight = height; //1080;
if (width > screenWidth || height > screenHeight) {
throw Exception("TODO - unsupported");
}
var result = await _channel
.invokeMethod("createTexture", [screenWidth, screenHeight, 0, 0]);
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");
return MacOSMethodChannelFlutterTexture(_channel, flutterTextureId!,
hardwareTextureId!, screenWidth, screenHeight);
} }
// On MacOS, we currently use textures/render targets, so there's no window to resize // On MacOS, we currently use textures/render targets, so there's no window to resize
@@ -64,14 +53,119 @@ class ThermionFlutterMacOS extends ThermionFlutterMethodChannelInterface {
} }
} }
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 MacOSMethodChannelFlutterTexture extends MethodChannelFlutterTexture { class MacOSMethodChannelFlutterTexture extends MethodChannelFlutterTexture {
MacOSMethodChannelFlutterTexture(super.channel, super.flutterId, final _logger = Logger("MacOSMethodChannelFlutterTexture");
super.hardwareId, super.width, super.height);
int flutterId = -1;
int hardwareId = -1;
int width = -1;
int height = -1;
static final Map<String, List<TextureCacheEntry>> _textureCache = {};
MacOSMethodChannelFlutterTexture(super.channel);
@override @override
Future resize(int width, int height, int left, int top) async { Future<void> resize(
if (width > this.width || height > this.height || left != 0 || top != 0) { int newWidth, int newHeight, int newLeft, int newTop) async {
throw Exception(); 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 MacOS 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");
} }
} }
} }

View File

@@ -18,6 +18,7 @@ abstract class ThermionFlutterMethodChannelInterface
final _logger = Logger("ThermionFlutterMethodChannelInterface"); final _logger = Logger("ThermionFlutterMethodChannelInterface");
ThermionViewerFFI? viewer; ThermionViewerFFI? viewer;
SwapChain? _swapChain;
Future<ThermionViewer> createViewer({ThermionFlutterOptions? options}) async { Future<ThermionViewer> createViewer({ThermionFlutterOptions? options}) async {
if (viewer != null) { if (viewer != null) {
@@ -31,9 +32,13 @@ abstract class ThermionFlutterMethodChannelInterface
if (resourceLoader == nullptr) { if (resourceLoader == nullptr) {
throw Exception("Failed to get resource loader"); throw Exception("Failed to get resource loader");
} }
// var renderCallbackResult = await _channel.invokeMethod("getRenderCallback");
var renderCallback = nullptr; var renderCallback = nullptr;
// Pointer<NativeFunction<Void Function(Pointer<Void>)>>.fromAddress(
// renderCallbackResult[0]);
var renderCallbackOwner = nullptr; var renderCallbackOwner = nullptr;
// Pointer<Void>.fromAddress(renderCallbackResult[1]);
var driverPlatform = await _channel.invokeMethod("getDriverPlatform"); var driverPlatform = await _channel.invokeMethod("getDriverPlatform");
var driverPtr = driverPlatform == null var driverPtr = driverPlatform == null
@@ -54,33 +59,33 @@ abstract class ThermionFlutterMethodChannelInterface
sharedContext: sharedContextPtr, sharedContext: sharedContextPtr,
uberArchivePath: options?.uberarchivePath); uberArchivePath: options?.uberarchivePath);
await viewer!.initialized; await viewer!.initialized;
return viewer!; return viewer!;
} }
} }
abstract class MethodChannelFlutterTexture extends ThermionFlutterTexture { abstract class MethodChannelFlutterTexture extends ThermionFlutterTexture {
final MethodChannel _channel; final MethodChannel channel;
MethodChannelFlutterTexture( MethodChannelFlutterTexture(this.channel);
this._channel, this.flutterId, this.hardwareId, this.width, this.height);
Future destroy() async { Future destroy() async {
await _channel.invokeMethod("destroyTexture", hardwareId); await channel.invokeMethod("destroyTexture", hardwareId);
} }
@override @override
final int flutterId; int get flutterId;
@override @override
final int hardwareId; int get hardwareId;
@override @override
final int height; int get height;
@override @override
final int width; int get width;
Future markFrameAvailable() async { Future markFrameAvailable() async {
await _channel.invokeMethod("markTextureFrameAvailable", this.flutterId); await channel.invokeMethod("markTextureFrameAvailable", this.flutterId);
} }
} }

View File

@@ -41,6 +41,6 @@ abstract class ThermionFlutterPlatform extends PlatformInterface {
/// ///
/// ///
/// ///
Future resizeWindow( Future resizeWindow(
int width, int height, int offsetTop, int offsetRight); int width, int height, int offsetTop, int offsetRight);
} }