fixes for window resizing on Windows
This commit is contained in:
@@ -155,9 +155,6 @@ class PolyvoxFilamentPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, Lo
|
|||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
Log.e("polyvox_filament", call.method, null)
|
Log.e("polyvox_filament", call.method, null)
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"getSharedContext" -> {
|
|
||||||
result.success(null)
|
|
||||||
}
|
|
||||||
"createTexture" -> {
|
"createTexture" -> {
|
||||||
if(_surfaceTextureEntry != null) {
|
if(_surfaceTextureEntry != null) {
|
||||||
result.error("TEXTURE_EXISTS", "Texture already exist. Make sure you call destroyTexture first", null)
|
result.error("TEXTURE_EXISTS", "Texture already exist. Make sure you call destroyTexture first", null)
|
||||||
@@ -184,7 +181,7 @@ class PolyvoxFilamentPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, Lo
|
|||||||
|
|
||||||
val nativeWindow = _lib.get_native_window_from_surface(_surface!! as Object, JNIEnv.CURRENT)
|
val nativeWindow = _lib.get_native_window_from_surface(_surface!! as Object, JNIEnv.CURRENT)
|
||||||
|
|
||||||
val resultList = listOf(_surfaceTextureEntry!!.id(), Pointer.nativeValue(nativeWindow), null )
|
val resultList = listOf(_surfaceTextureEntry!!.id(), Pointer.nativeValue(nativeWindow), null, null )
|
||||||
|
|
||||||
result.success(resultList)
|
result.success(resultList)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,8 +166,6 @@ public class SwiftPolyvoxFilamentPlugin: NSObject, FlutterPlugin, FlutterTexture
|
|||||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
let methodName = call.method;
|
let methodName = call.method;
|
||||||
switch methodName {
|
switch methodName {
|
||||||
case "getSharedContext":
|
|
||||||
result(nil)
|
|
||||||
case "getResourceLoaderWrapper":
|
case "getResourceLoaderWrapper":
|
||||||
let resourceLoaderWrapper = make_resource_loader(loadResource, freeResource, Unmanaged.passUnretained(self).toOpaque())
|
let resourceLoaderWrapper = make_resource_loader(loadResource, freeResource, Unmanaged.passUnretained(self).toOpaque())
|
||||||
result(unsafeBitCast(resourceLoaderWrapper, to:Int64.self))
|
result(unsafeBitCast(resourceLoaderWrapper, to:Int64.self))
|
||||||
@@ -180,7 +178,7 @@ public class SwiftPolyvoxFilamentPlugin: NSObject, FlutterPlugin, FlutterTexture
|
|||||||
createPixelBuffer(width:Int(args[0]), height:Int(args[1]))
|
createPixelBuffer(width:Int(args[0]), height:Int(args[1]))
|
||||||
let pixelBufferPtr = unsafeBitCast(pixelBuffer!, to:UnsafeRawPointer.self)
|
let pixelBufferPtr = unsafeBitCast(pixelBuffer!, to:UnsafeRawPointer.self)
|
||||||
let pixelBufferAddress = Int(bitPattern:pixelBufferPtr);
|
let pixelBufferAddress = Int(bitPattern:pixelBufferPtr);
|
||||||
result([self.flutterTextureId, pixelBufferAddress, nil])
|
result([self.flutterTextureId, pixelBufferAddress, nil, nil])
|
||||||
case "destroyTexture":
|
case "destroyTexture":
|
||||||
if(self.flutterTextureId != nil) {
|
if(self.flutterTextureId != nil) {
|
||||||
self.registry.unregisterTexture(self.flutterTextureId!)
|
self.registry.unregisterTexture(self.flutterTextureId!)
|
||||||
|
|||||||
@@ -257,16 +257,18 @@ FilamentAsset* AssetManager::getAssetByEntityId(EntityId entityId) {
|
|||||||
|
|
||||||
void AssetManager::updateAnimations() {
|
void AssetManager::updateAnimations() {
|
||||||
|
|
||||||
auto now = high_resolution_clock::now();
|
|
||||||
|
|
||||||
RenderableManager &rm = _engine->getRenderableManager();
|
RenderableManager &rm = _engine->getRenderableManager();
|
||||||
|
|
||||||
for (auto& asset : _assets) {
|
for (auto& asset : _assets) {
|
||||||
|
|
||||||
|
|
||||||
std::vector<int> completed;
|
std::vector<int> completed;
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for(auto& anim : asset.mAnimations) {
|
for(auto& anim : asset.mAnimations) {
|
||||||
|
|
||||||
|
auto now = high_resolution_clock::now();
|
||||||
|
|
||||||
auto elapsed = float(std::chrono::duration_cast<std::chrono::milliseconds>(now - anim.mStart).count()) / 1000.0f;
|
auto elapsed = float(std::chrono::duration_cast<std::chrono::milliseconds>(now - anim.mStart).count()) / 1000.0f;
|
||||||
|
|
||||||
if(anim.mLoop || elapsed < anim.mDuration) {
|
if(anim.mLoop || elapsed < anim.mDuration) {
|
||||||
|
|||||||
@@ -991,22 +991,22 @@ namespace polyvox
|
|||||||
cam.lookAt(eye, target, upward);
|
cam.lookAt(eye, target, upward);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - this was an experiment but probably useful to keep for debugging
|
// // TODO - this was an experiment but probably useful to keep for debugging
|
||||||
// if pixelBuffer is provided, we will copy the framebuffer into the pixelBuffer.
|
// // if pixelBuffer is provided, we will copy the framebuffer into the pixelBuffer.
|
||||||
if (pixelBuffer)
|
// if (pixelBuffer)
|
||||||
{
|
// {
|
||||||
auto pbd = Texture::PixelBufferDescriptor(
|
// auto pbd = Texture::PixelBufferDescriptor(
|
||||||
pixelBuffer, size_t(1024 * 768 * 4),
|
// pixelBuffer, size_t(1024 * 768 * 4),
|
||||||
Texture::Format::RGBA,
|
// Texture::Format::RGBA,
|
||||||
Texture::Type::BYTE, nullptr, callback, data);
|
// Texture::Type::BYTE, nullptr, callback, data);
|
||||||
|
|
||||||
_renderer->beginFrame(_swapChain, 0);
|
// _renderer->beginFrame(_swapChain, 0);
|
||||||
_renderer->render(_view);
|
// _renderer->render(_view);
|
||||||
_renderer->readPixels(0, 0, 1024, 768, std::move(pbd));
|
// _renderer->readPixels(0, 0, 1024, 768, std::move(pbd));
|
||||||
_renderer->endFrame();
|
// _renderer->endFrame();
|
||||||
}
|
// }
|
||||||
else
|
// else
|
||||||
{
|
// {
|
||||||
// Render the scene, unless the renderer wants to skip the frame.
|
// Render the scene, unless the renderer wants to skip the frame.
|
||||||
if (_renderer->beginFrame(_swapChain, frameTimeInNanos))
|
if (_renderer->beginFrame(_swapChain, frameTimeInNanos))
|
||||||
{
|
{
|
||||||
@@ -1015,9 +1015,10 @@ namespace polyvox
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// std::cout << "Skipped" << std::endl;
|
||||||
// skipped frame
|
// skipped frame
|
||||||
}
|
}
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
void FilamentViewer::updateViewportAndCameraProjection(
|
void FilamentViewer::updateViewportAndCameraProjection(
|
||||||
|
|||||||
@@ -105,13 +105,13 @@ abstract class FilamentController {
|
|||||||
/// 5) The FilamentWidget will replace the empty Container with a Texture widget
|
/// 5) The FilamentWidget will replace the empty Container with a Texture widget
|
||||||
/// If you need to wait until a FilamentViewer has been created, listen to the [viewer] stream.
|
/// If you need to wait until a FilamentViewer has been created, listen to the [viewer] stream.
|
||||||
///
|
///
|
||||||
Future createViewer(int width, int height);
|
Future createViewer(Rect rect);
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Resize the viewport & backing texture.
|
/// Resize the viewport & backing texture.
|
||||||
/// This is called by FilamentWidget; you shouldn't need to invoke this manually.
|
/// 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(Rect rect);
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Set the background image to [path] (which should have a file extension .png, .jpg, or .ktx).
|
/// Set the background image to [path] (which should have a file extension .png, .jpg, or .ktx).
|
||||||
|
|||||||
@@ -9,16 +9,17 @@ import 'package:polyvox_filament/filament_controller.dart';
|
|||||||
|
|
||||||
import 'package:polyvox_filament/animations/animation_data.dart';
|
import 'package:polyvox_filament/animations/animation_data.dart';
|
||||||
import 'package:polyvox_filament/generated_bindings.dart';
|
import 'package:polyvox_filament/generated_bindings.dart';
|
||||||
|
import 'package:polyvox_filament/rendering_surface.dart';
|
||||||
|
|
||||||
// ignore: constant_identifier_names
|
// ignore: constant_identifier_names
|
||||||
const FilamentEntity _FILAMENT_ASSET_ERROR = 0;
|
const FilamentEntity _FILAMENT_ASSET_ERROR = 0;
|
||||||
|
|
||||||
class FilamentControllerFFI extends FilamentController {
|
class FilamentControllerFFI extends FilamentController {
|
||||||
|
|
||||||
final _channel = const MethodChannel("app.polyvox.filament/event");
|
final _channel = const MethodChannel("app.polyvox.filament/event");
|
||||||
|
|
||||||
|
bool _usesBackingWindow = false;
|
||||||
@override
|
@override
|
||||||
bool get requiresTextureWidget => !Platform.isWindows;
|
bool get requiresTextureWidget => !_usesBackingWindow;
|
||||||
|
|
||||||
double _pixelRatio = 1.0;
|
double _pixelRatio = 1.0;
|
||||||
|
|
||||||
@@ -48,17 +49,22 @@ class FilamentControllerFFI extends FilamentController {
|
|||||||
/// 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.
|
/// 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.
|
||||||
///
|
///
|
||||||
FilamentControllerFFI({this.uberArchivePath}) {
|
FilamentControllerFFI({this.uberArchivePath}) {
|
||||||
|
// on some platforms, we ignore the resize event raised by the Flutter RenderObserver
|
||||||
|
// in favour of a window-level event passed via the method channel.
|
||||||
|
// (this is because there is no apparent way to exactly synchronize resizing a Flutter widget and resizing a pixel buffer, so we need
|
||||||
|
// to handle the latter first and rebuild the swapchain appropriately).
|
||||||
_channel.setMethodCallHandler((call) async {
|
_channel.setMethodCallHandler((call) async {
|
||||||
if(call.arguments[0] == _resizingWidth && call.arguments[1] == _resizingHeight) {
|
if (call.arguments[0] == _resizingWidth &&
|
||||||
|
call.arguments[1] == _resizingHeight) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_resizeTimer?.cancel();
|
_resizeTimer?.cancel();
|
||||||
_resizingWidth = call.arguments[0];
|
_resizingWidth = call.arguments[0];
|
||||||
_resizingHeight = call.arguments[1];
|
_resizingHeight = call.arguments[1];
|
||||||
_resizeTimer = Timer(const Duration(milliseconds: 500), () async {
|
_resizeTimer = Timer(const Duration(milliseconds: 500), () async {
|
||||||
await resize(_resizingWidth!, _resizingHeight!);
|
await resize(Offset.zero &
|
||||||
|
ui.Size(_resizingWidth!.toDouble(), _resizingHeight!.toDouble()));
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
late DynamicLibrary dl;
|
late DynamicLibrary dl;
|
||||||
if (Platform.isIOS || Platform.isMacOS || Platform.isWindows) {
|
if (Platform.isIOS || Platform.isMacOS || Platform.isWindows) {
|
||||||
@@ -67,6 +73,12 @@ class FilamentControllerFFI extends FilamentController {
|
|||||||
dl = DynamicLibrary.open("libpolyvox_filament_android.so");
|
dl = DynamicLibrary.open("libpolyvox_filament_android.so");
|
||||||
}
|
}
|
||||||
_lib = NativeLibrary(dl);
|
_lib = NativeLibrary(dl);
|
||||||
|
if(Platform.isWindows) {
|
||||||
|
_channel.invokeMethod("usesBackingWindow").then((result) {
|
||||||
|
_usesBackingWindow = result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _rendering = false;
|
bool _rendering = false;
|
||||||
@@ -123,19 +135,21 @@ class FilamentControllerFFI extends FilamentController {
|
|||||||
@override
|
@override
|
||||||
Future destroyTexture() async {
|
Future destroyTexture() async {
|
||||||
if (textureDetails.value != null) {
|
if (textureDetails.value != null) {
|
||||||
await _channel.invokeMethod("destroyTexture", textureDetails.value!.textureId);
|
await _channel.invokeMethod(
|
||||||
|
"destroyTexture", textureDetails.value!.textureId);
|
||||||
}
|
}
|
||||||
print("Texture destroyed");
|
print("Texture destroyed");
|
||||||
}
|
}
|
||||||
|
|
||||||
Pointer<Void> _driver = nullptr.cast<Void>();
|
Pointer<Void> _driver = nullptr.cast<Void>();
|
||||||
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Called by `FilamentWidget`. You do not need to call this yourself.
|
/// Called by `FilamentWidget`. You do not need to call this yourself.
|
||||||
///
|
///
|
||||||
@override
|
@override
|
||||||
Future createViewer(int width, int height) async {
|
Future createViewer(Rect rect) async {
|
||||||
|
double width = rect.width;
|
||||||
|
double height = rect.height;
|
||||||
if (_viewer != null) {
|
if (_viewer != null) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
"Viewer already exists, make sure you call destroyViewer first");
|
"Viewer already exists, make sure you call destroyViewer first");
|
||||||
@@ -155,18 +169,6 @@ class FilamentControllerFFI extends FilamentController {
|
|||||||
|
|
||||||
print("Creating viewer with size $size");
|
print("Creating viewer with size $size");
|
||||||
|
|
||||||
var textures =
|
|
||||||
await _channel.invokeMethod("createTexture", [size.width, size.height]);
|
|
||||||
var flutterTextureId = textures[0];
|
|
||||||
|
|
||||||
// void* on iOS (pointer to pixel buffer), Android (pointer to native window), null on macOS/Windows
|
|
||||||
var surfaceAddress = textures[1] as int? ?? 0;
|
|
||||||
|
|
||||||
// null on iOS/Android, void* on MacOS (pointer to metal texture), GLuid on Windows/Linux
|
|
||||||
var nativeTexture = textures[2] as int? ?? 0;
|
|
||||||
|
|
||||||
print("Using flutterTextureId $flutterTextureId, surface $surfaceAddress and nativeTexture $nativeTexture");
|
|
||||||
|
|
||||||
if (Platform.isWindows && requiresTextureWidget) {
|
if (Platform.isWindows && requiresTextureWidget) {
|
||||||
_driver = Pointer<Void>.fromAddress(
|
_driver = Pointer<Void>.fromAddress(
|
||||||
await _channel.invokeMethod("getDriverPlatform"));
|
await _channel.invokeMethod("getDriverPlatform"));
|
||||||
@@ -179,11 +181,10 @@ class FilamentControllerFFI extends FilamentController {
|
|||||||
var renderCallbackOwner =
|
var renderCallbackOwner =
|
||||||
Pointer<Void>.fromAddress(renderCallbackResult[1]);
|
Pointer<Void>.fromAddress(renderCallbackResult[1]);
|
||||||
|
|
||||||
var sharedContext = await _channel.invokeMethod("getSharedContext");
|
var renderingSurface = await _createRenderingSurface(rect);
|
||||||
print("Got shared context : $sharedContext");
|
|
||||||
|
|
||||||
_viewer = _lib.create_filament_viewer_ffi(
|
_viewer = _lib.create_filament_viewer_ffi(
|
||||||
Pointer<Void>.fromAddress(sharedContext ?? 0),
|
Pointer<Void>.fromAddress(renderingSurface.sharedContext ?? 0),
|
||||||
_driver,
|
_driver,
|
||||||
uberArchivePath?.toNativeUtf8().cast<Char>() ?? nullptr,
|
uberArchivePath?.toNativeUtf8().cast<Char>() ?? nullptr,
|
||||||
loader,
|
loader,
|
||||||
@@ -194,50 +195,57 @@ class FilamentControllerFFI extends FilamentController {
|
|||||||
throw Exception("Failed to create viewer. Check logs for details");
|
throw Exception("Failed to create viewer. Check logs for details");
|
||||||
}
|
}
|
||||||
|
|
||||||
_lib.create_swap_chain_ffi(
|
|
||||||
_viewer!,
|
|
||||||
Pointer<Void>.fromAddress(surfaceAddress),
|
|
||||||
size.width.toInt(),
|
|
||||||
size.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());
|
|
||||||
}
|
|
||||||
|
|
||||||
_lib.update_viewport_and_camera_projection_ffi(
|
|
||||||
_viewer!, size.width.toInt(), size.height.toInt(), 1.0);
|
|
||||||
|
|
||||||
_assetManager = _lib.get_asset_manager(_viewer!);
|
_assetManager = _lib.get_asset_manager(_viewer!);
|
||||||
|
|
||||||
|
_lib.create_swap_chain_ffi(_viewer!, renderingSurface.surface,
|
||||||
|
rect.width.toInt(), rect.height.toInt());
|
||||||
|
if (renderingSurface.textureHandle != 0) {
|
||||||
|
print(
|
||||||
|
"Creating render target from native texture ${renderingSurface.textureHandle}");
|
||||||
|
_lib.create_render_target_ffi(_viewer!, renderingSurface.textureHandle,
|
||||||
|
rect.width.toInt(), rect.height.toInt());
|
||||||
|
}
|
||||||
|
|
||||||
textureDetails.value = TextureDetails(
|
textureDetails.value = TextureDetails(
|
||||||
textureId: flutterTextureId!, width: width, height: height);
|
textureId: renderingSurface.flutterTextureId!,
|
||||||
|
width: rect.width.toInt(),
|
||||||
|
height: rect.height.toInt());
|
||||||
|
|
||||||
|
_lib.update_viewport_and_camera_projection_ffi(
|
||||||
|
_viewer!, rect.width.toInt(), rect.height.toInt(), 1.0);
|
||||||
|
|
||||||
_hasViewerController.add(true);
|
_hasViewerController.add(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<RenderingSurface> _createRenderingSurface(Rect rect) async {
|
||||||
|
return RenderingSurface.from(await _channel.invokeMethod(
|
||||||
|
"createTexture",
|
||||||
|
[rect.width, rect.height, _pixelRatio, rect.left, rect.top]));
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// When a FilamentWidget is resized, it will call [resize]. This method will tear down/recreate the swapchain and propagate a new texture ID back to the FilamentWidget.
|
/// When a FilamentWidget is resized, it will call the [resize] method below, which will tear down/recreate the swapchain.
|
||||||
/// For "once-off" resizes, this is fine.
|
/// For "once-off" resizes, this is fine; however, this can be problematic for consecutive resizes
|
||||||
/// However, this can be problematic for consecutive resizes (e.g. dragging to expand/contract the parent window on desktop, or animating the size of the FilamentWidget itself).
|
/// (e.g. dragging to expand/contract the parent window on desktop, or animating the size of the FilamentWidget itself).
|
||||||
/// It is too expensive to recreate the swapchain multiple times per second.
|
/// It is too expensive to recreate the swapchain multiple times per second.
|
||||||
/// We therefore add a timer to FilamentWidget so that the call to [resize] is delayed (e.g. 50ms).
|
/// We therefore add a timer to FilamentWidget so that the call to [resize] is delayed (e.g. 500ms).
|
||||||
/// Any subsequent resizes before the delay window elapses will cancel the earlier call.
|
/// Any subsequent resizes before the delay window elapses will cancel the earlier call.
|
||||||
///
|
///
|
||||||
/// The overall process looks like this:
|
/// The overall process looks like this:
|
||||||
/// 1) the window is resized
|
/// 1) the window is resized
|
||||||
/// 2) (Windows only) PixelBufferTexture is requested to provide a new pixel buffer with a new size, and we return an empty texture
|
/// 2) (Windows only) the Flutter engine requests PixelBufferTexture to provide a new pixel buffer with a new size (we return an empty texture, blanking the Texture widget)
|
||||||
/// 3) After Xms, [resize] is invoked
|
/// 3) After Xms, [resize] is invoked
|
||||||
/// 4) the viewer is instructed to stop rendering (synchronous)
|
/// 4) the viewer is instructed to stop rendering (synchronous)
|
||||||
/// 5) the existing Filament swapchain is destroyed (synchronous)
|
/// 5) the existing Filament swapchain is destroyed (synchronous)
|
||||||
/// 6) the Flutter texture is unregistered
|
/// 6) (where a Texture widget is used), the Flutter texture is unregistered
|
||||||
/// a) this is asynchronous, but
|
/// a) this is asynchronous, but
|
||||||
/// b) *** SEE NOTE BELOW ON WINDOWS *** by passing the method channel result through to the callback, we make this synchronous from the Flutter side,
|
/// b) *** SEE NOTE BELOW ON WINDOWS *** by passing the method channel result through to the callback, we make this synchronous from the Flutter side,
|
||||||
// c) in this async callback, the glTexture is destroyed
|
/// c) in this async callback, the glTexture is destroyed
|
||||||
/// 7) a new Flutter/OpenGL texture is created (synchronous)
|
/// 7) (where a backing window is used), the window is resized
|
||||||
|
/// 7) (where a Texture widget is used), a new Flutter/OpenGL texture is created (synchronous)
|
||||||
/// 8) a new swapchain is created (synchronous)
|
/// 8) a new swapchain is created (synchronous)
|
||||||
/// 9) if the viewer was rendering prior to the resize, the viewer is instructed to recommence rendering
|
/// 9) if the viewer was rendering prior to the resize, the viewer is instructed to recommence rendering
|
||||||
/// 10) the new texture ID is pushed to the FilamentWidget
|
/// 10) (where a Texture widget is used) the new texture ID is pushed to the FilamentWidget
|
||||||
/// 11) the FilamentWidget updates the Texture widget with the new texture.
|
/// 11) the FilamentWidget updates the Texture widget with the new texture.
|
||||||
///
|
///
|
||||||
/// #### (Windows-only) ############################################################
|
/// #### (Windows-only) ############################################################
|
||||||
@@ -279,60 +287,68 @@ 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.
|
/// # Given we don't do this on other platforms, I'm OK to stick with the existing solution for the time being.
|
||||||
/// ############################################################################
|
/// ############################################################################
|
||||||
///
|
///
|
||||||
|
bool _resizing = false;
|
||||||
@override
|
@override
|
||||||
Future resize(int width, int height, {double scaleFactor = 1.0}) async {
|
Future resize(Rect rect) async {
|
||||||
|
|
||||||
if(Platform.isWindows) {
|
if (_viewer == null) {
|
||||||
return;
|
throw Exception("Cannot resize without active viewer");
|
||||||
}
|
}
|
||||||
// we defer to the FilamentWidget to ensure that every call to [resize] is synchronized
|
|
||||||
// so this exception should never be thrown (right?)
|
if (_resizing) {
|
||||||
if (textureDetails.value == null) {
|
|
||||||
throw Exception("Resize currently underway, ignoring");
|
throw Exception("Resize currently underway, ignoring");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_resizing = true;
|
||||||
|
|
||||||
_lib.set_rendering_ffi(_viewer!, false);
|
_lib.set_rendering_ffi(_viewer!, false);
|
||||||
|
|
||||||
if (textureDetails.value != null) {
|
if(!_usesBackingWindow) {
|
||||||
if (_viewer != null) {
|
_lib.destroy_swap_chain_ffi(_viewer!);
|
||||||
_lib.destroy_swap_chain_ffi(_viewer!);
|
}
|
||||||
|
|
||||||
|
if (requiresTextureWidget) {
|
||||||
|
if(textureDetails.value != null) {
|
||||||
|
await _channel.invokeMethod(
|
||||||
|
"destroyTexture", textureDetails.value!.textureId);
|
||||||
}
|
}
|
||||||
await _channel.invokeMethod("destroyTexture", textureDetails.value!.textureId);
|
} else if(Platform.isWindows) {
|
||||||
print("Destroyed texture ${textureDetails.value!.textureId}");
|
print("Resizing window with rect $rect");
|
||||||
|
await _channel.invokeMethod(
|
||||||
|
"resizeWindow", [rect.width, rect.height, _pixelRatio, rect.left, rect.top]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var newSize = ui.Size(width * _pixelRatio, height * _pixelRatio);
|
var renderingSurface = await _createRenderingSurface(rect);
|
||||||
|
|
||||||
print("Size after pixel ratio : $width x $height ");
|
if (_viewer!.address == 0) {
|
||||||
|
throw Exception("Failed to create viewer. Check logs for details");
|
||||||
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;
|
|
||||||
|
|
||||||
// null on iOS/Android, void* on MacOS (pointer to metal texture), GLuid on Windows/Linux
|
|
||||||
var nativeTexture = textures[2] as int? ?? 0;
|
|
||||||
|
|
||||||
_lib.create_swap_chain_ffi(
|
|
||||||
_viewer!,
|
|
||||||
Pointer<Void>.fromAddress(surfaceAddress),
|
|
||||||
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,
|
|
||||||
newSize.width.toInt(), newSize.height.toInt());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_assetManager = _lib.get_asset_manager(_viewer!);
|
||||||
|
|
||||||
|
if(!_usesBackingWindow) {
|
||||||
|
_lib.create_swap_chain_ffi(_viewer!, renderingSurface.surface,
|
||||||
|
rect.width.toInt(), rect.height.toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderingSurface.textureHandle != 0) {
|
||||||
|
print(
|
||||||
|
"Creating render target from native texture ${renderingSurface.textureHandle}");
|
||||||
|
_lib.create_render_target_ffi(_viewer!, renderingSurface.textureHandle,
|
||||||
|
rect.width.toInt(), rect.height.toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
textureDetails.value = TextureDetails(
|
||||||
|
textureId: renderingSurface.flutterTextureId!,
|
||||||
|
width: rect.width.toInt(),
|
||||||
|
height: rect.height.toInt());
|
||||||
|
|
||||||
_lib.update_viewport_and_camera_projection_ffi(
|
_lib.update_viewport_and_camera_projection_ffi(
|
||||||
_viewer!, newSize.width.toInt(), newSize.height.toInt(), 1.0);
|
_viewer!, rect.width.toInt(), rect.height.toInt(), 1.0);
|
||||||
|
|
||||||
await setRendering(_rendering);
|
await setRendering(_rendering);
|
||||||
textureDetails.value =
|
|
||||||
TextureDetails(textureId: textures[0]!, width: width, height: height);
|
_resizing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
37
lib/rendering_surface.dart
Normal file
37
lib/rendering_surface.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'dart:ffi';
|
||||||
|
|
||||||
|
class RenderingSurface {
|
||||||
|
final int flutterTextureId;
|
||||||
|
final Pointer<Void> surface;
|
||||||
|
final int textureHandle;
|
||||||
|
final int sharedContext;
|
||||||
|
|
||||||
|
factory RenderingSurface.from(dynamic platformMessage) {
|
||||||
|
var flutterTextureId = platformMessage[0];
|
||||||
|
|
||||||
|
// void* on iOS (pointer to pixel buffer), Android (pointer to native window), null on macOS/Windows
|
||||||
|
var surfaceAddress = platformMessage[1] as int? ?? 0;
|
||||||
|
|
||||||
|
// null on iOS/Android, void* on MacOS (pointer to metal texture), GLuid on Windows/Linux
|
||||||
|
var nativeTexture = platformMessage[2] as int? ?? 0;
|
||||||
|
|
||||||
|
if(nativeTexture != 0) {
|
||||||
|
assert(surfaceAddress == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sharedContext = platformMessage[3] as int? ?? 0;
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Using flutterTextureId $flutterTextureId, surface $surfaceAddress nativeTexture $nativeTexture and sharedContext $sharedContext");
|
||||||
|
return RenderingSurface(
|
||||||
|
sharedContext: sharedContext,
|
||||||
|
flutterTextureId: flutterTextureId,
|
||||||
|
surface: Pointer<Void>.fromAddress(surfaceAddress),
|
||||||
|
textureHandle: nativeTexture);
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderingSurface({required this.sharedContext,
|
||||||
|
required this.flutterTextureId,
|
||||||
|
required this.surface,
|
||||||
|
required this.textureHandle});
|
||||||
|
}
|
||||||
@@ -87,9 +87,6 @@ class _FilamentWidgetState extends State<FilamentWidget> {
|
|||||||
|
|
||||||
return ResizeObserver(
|
return ResizeObserver(
|
||||||
onResized: (newSize) {
|
onResized: (newSize) {
|
||||||
if (!Platform.isWindows) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_width = newSize.width.ceil();
|
_width = newSize.width.ceil();
|
||||||
@@ -129,6 +126,13 @@ class _SizedFilamentWidgetState extends State<_SizedFilamentWidget> {
|
|||||||
|
|
||||||
late final AppLifecycleListener _appLifecycleListener;
|
late final AppLifecycleListener _appLifecycleListener;
|
||||||
|
|
||||||
|
Rect get _rect {
|
||||||
|
final renderBox =(context.findRenderObject()) as RenderBox;
|
||||||
|
final size = renderBox.size;
|
||||||
|
final translation = renderBox.getTransformTo(null).getTranslation();
|
||||||
|
return Rect.fromLTWH(translation.x, translation.y, size.width, size.height);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_appLifecycleListener = AppLifecycleListener(
|
_appLifecycleListener = AppLifecycleListener(
|
||||||
@@ -137,7 +141,7 @@ class _SizedFilamentWidgetState extends State<_SizedFilamentWidget> {
|
|||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||||
try {
|
try {
|
||||||
await widget.controller.createViewer(widget.width, widget.height);
|
await widget.controller.createViewer(_rect);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
_error = err.toString();
|
_error = err.toString();
|
||||||
}
|
}
|
||||||
@@ -151,6 +155,7 @@ class _SizedFilamentWidgetState extends State<_SizedFilamentWidget> {
|
|||||||
bool _resizing = false;
|
bool _resizing = false;
|
||||||
|
|
||||||
Future _resize() {
|
Future _resize() {
|
||||||
|
print("Resizing widget");
|
||||||
final completer = Completer();
|
final completer = Completer();
|
||||||
// resizing the window can be sluggish (particular in debug mode), exacerbated when simultaneously recreating the swapchain and resize the window.
|
// resizing the window can be sluggish (particular in debug mode), exacerbated when simultaneously recreating the swapchain and resize the window.
|
||||||
// to address this, whenever the widget is resized, we set a timer for Xms in the future.
|
// to address this, whenever the widget is resized, we set a timer for Xms in the future.
|
||||||
@@ -164,14 +169,13 @@ class _SizedFilamentWidgetState extends State<_SizedFilamentWidget> {
|
|||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var size = ((context.findRenderObject()) as RenderBox).size;
|
|
||||||
var width = size.width.ceil();
|
|
||||||
var height = size.height.ceil();
|
|
||||||
while (_resizing) {
|
while (_resizing) {
|
||||||
await Future.delayed(const Duration(milliseconds: 20));
|
await Future.delayed(const Duration(milliseconds: 20));
|
||||||
}
|
}
|
||||||
|
|
||||||
_resizing = true;
|
_resizing = true;
|
||||||
await widget.controller.resize(width, height);
|
|
||||||
|
await widget.controller.resize(_rect);
|
||||||
_resizeTimer = null;
|
_resizeTimer = null;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
_resizing = false;
|
_resizing = false;
|
||||||
|
|||||||
@@ -111,8 +111,6 @@ public class SwiftPolyvoxFilamentPlugin: NSObject, FlutterPlugin, FlutterTexture
|
|||||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
let methodName = call.method;
|
let methodName = call.method;
|
||||||
switch methodName {
|
switch methodName {
|
||||||
case "getSharedContext":
|
|
||||||
result(nil)
|
|
||||||
case "getResourceLoaderWrapper":
|
case "getResourceLoaderWrapper":
|
||||||
let resourceLoaderWrapper = make_resource_loader(loadResource, freeResource, Unmanaged.passUnretained(self).toOpaque())
|
let resourceLoaderWrapper = make_resource_loader(loadResource, freeResource, Unmanaged.passUnretained(self).toOpaque())
|
||||||
result(unsafeBitCast(resourceLoaderWrapper, to:Int64.self))
|
result(unsafeBitCast(resourceLoaderWrapper, to:Int64.self))
|
||||||
@@ -154,7 +152,7 @@ public class SwiftPolyvoxFilamentPlugin: NSObject, FlutterPlugin, FlutterTexture
|
|||||||
let metalTexturePtr = Unmanaged.passUnretained(metalTexture!).toOpaque()
|
let metalTexturePtr = Unmanaged.passUnretained(metalTexture!).toOpaque()
|
||||||
let metalTextureAddress = Int(bitPattern:metalTexturePtr)
|
let metalTextureAddress = Int(bitPattern:metalTexturePtr)
|
||||||
|
|
||||||
result([self.flutterTextureId as Any, nil, metalTextureAddress])
|
result([self.flutterTextureId as Any, nil, metalTextureAddress, nil])
|
||||||
case "destroyTexture":
|
case "destroyTexture":
|
||||||
if(self.flutterTextureId != nil) {
|
if(self.flutterTextureId != nil) {
|
||||||
self.registry.unregisterTexture(self.flutterTextureId!)
|
self.registry.unregisterTexture(self.flutterTextureId!)
|
||||||
|
|||||||
@@ -132,35 +132,11 @@ void SetWindowComposition(HWND window, int32_t accent_state,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
LRESULT NativeViewSubclassProc(HWND window, UINT message, WPARAM wparam,
|
|
||||||
LPARAM lparam, UINT_PTR subclass_id,
|
|
||||||
DWORD_PTR ref_data) noexcept {
|
|
||||||
switch (message) {
|
|
||||||
case WM_ERASEBKGND: {
|
|
||||||
// Prevent erasing of |window| when it is unfocused and minimized or
|
|
||||||
// moved out of screen etc.
|
|
||||||
return 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case WM_SIZE: {
|
|
||||||
// Prevent unnecessary maxmize, minimize or restore messages for |window|.
|
|
||||||
return 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return ::DefSubclassProc(window, message, wparam, lparam);
|
|
||||||
}
|
|
||||||
|
|
||||||
LRESULT CALLBACK FilamentWindowProc(HWND const window, UINT const message,
|
LRESULT CALLBACK FilamentWindowProc(HWND const window, UINT const message,
|
||||||
WPARAM const wparam,
|
WPARAM const wparam,
|
||||||
LPARAM const lparam) noexcept {
|
LPARAM const lparam) noexcept {
|
||||||
// std::cout << "FILAMENT WINDOW EVENT " << message << std::endl;
|
|
||||||
|
|
||||||
switch (message) {
|
switch (message) {
|
||||||
case WM_MOUSEMOVE: {
|
case WM_MOUSEMOVE: {
|
||||||
std::cout << "FILAMENT MOUSE MOVE" << std::endl;
|
|
||||||
TRACKMOUSEEVENT event;
|
TRACKMOUSEEVENT event;
|
||||||
event.cbSize = sizeof(event);
|
event.cbSize = sizeof(event);
|
||||||
event.hwndTrack = window;
|
event.hwndTrack = window;
|
||||||
@@ -168,10 +144,8 @@ LRESULT CALLBACK FilamentWindowProc(HWND const window, UINT const message,
|
|||||||
event.dwHoverTime = 200;
|
event.dwHoverTime = 200;
|
||||||
auto user_data = ::GetWindowLongPtr(window, GWLP_USERDATA);
|
auto user_data = ::GetWindowLongPtr(window, GWLP_USERDATA);
|
||||||
if (user_data) {
|
if (user_data) {
|
||||||
std::cout << "setting foreground in filamentwindwoproc" << std::endl;
|
|
||||||
HWND flutterRootWindow = reinterpret_cast<HWND>(user_data);
|
HWND flutterRootWindow = reinterpret_cast<HWND>(user_data);
|
||||||
::SetForegroundWindow(flutterRootWindow);
|
::SetForegroundWindow(flutterRootWindow);
|
||||||
// NativeViewCore::GetInstance()->SetHitTestBehavior(0);
|
|
||||||
LONG ex_style = ::GetWindowLong(flutterRootWindow, GWL_EXSTYLE);
|
LONG ex_style = ::GetWindowLong(flutterRootWindow, GWL_EXSTYLE);
|
||||||
ex_style &= ~(WS_EX_TRANSPARENT | WS_EX_LAYERED);
|
ex_style &= ~(WS_EX_TRANSPARENT | WS_EX_LAYERED);
|
||||||
::SetWindowLong(flutterRootWindow, GWL_EXSTYLE, ex_style);
|
::SetWindowLong(flutterRootWindow, GWL_EXSTYLE, ex_style);
|
||||||
@@ -179,7 +153,6 @@ LRESULT CALLBACK FilamentWindowProc(HWND const window, UINT const message,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case WM_ERASEBKGND: {
|
case WM_ERASEBKGND: {
|
||||||
std::cout << "FILAMENT ERASE BKGND" << std::endl;
|
|
||||||
// Prevent erasing of |window| when it is unfocused and minimized or
|
// Prevent erasing of |window| when it is unfocused and minimized or
|
||||||
// moved out of screen etc.
|
// moved out of screen etc.
|
||||||
break;
|
break;
|
||||||
@@ -189,11 +162,9 @@ LRESULT CALLBACK FilamentWindowProc(HWND const window, UINT const message,
|
|||||||
case WM_MOVING:
|
case WM_MOVING:
|
||||||
case WM_ACTIVATE:
|
case WM_ACTIVATE:
|
||||||
case WM_WINDOWPOSCHANGED: {
|
case WM_WINDOWPOSCHANGED: {
|
||||||
// std::cout << "FILAMENT POS CHANGED" << std::endl;
|
|
||||||
// NativeViewCore::GetInstance()->SetHitTestBehavior(0);
|
// NativeViewCore::GetInstance()->SetHitTestBehavior(0);
|
||||||
auto user_data = ::GetWindowLongPtr(window, GWLP_USERDATA);
|
auto user_data = ::GetWindowLongPtr(window, GWLP_USERDATA);
|
||||||
if (user_data) {
|
if (user_data) {
|
||||||
std::cout << "setting foreground in filamentwindwoproc" << std::endl;
|
|
||||||
HWND flutterRootWindow = reinterpret_cast<HWND>(user_data);
|
HWND flutterRootWindow = reinterpret_cast<HWND>(user_data);
|
||||||
::SetForegroundWindow(flutterRootWindow);
|
::SetForegroundWindow(flutterRootWindow);
|
||||||
// NativeViewCore::GetInstance()->SetHitTestBehavior(0);
|
// NativeViewCore::GetInstance()->SetHitTestBehavior(0);
|
||||||
@@ -210,7 +181,10 @@ LRESULT CALLBACK FilamentWindowProc(HWND const window, UINT const message,
|
|||||||
}
|
}
|
||||||
|
|
||||||
BackingWindow::BackingWindow(flutter::PluginRegistrarWindows *pluginRegistrar,
|
BackingWindow::BackingWindow(flutter::PluginRegistrarWindows *pluginRegistrar,
|
||||||
int initialWidth, int initialHeight) {
|
int width,
|
||||||
|
int height,
|
||||||
|
int left,
|
||||||
|
int top) : _width(width), _height(height), _left(left), _top(top) {
|
||||||
// a Flutter application actually has two windows - the innner window contains the FlutterView.
|
// a Flutter application actually has two windows - the innner window contains the FlutterView.
|
||||||
// although we will use the outer window for various events, we always position things relative to the inner window.
|
// although we will use the outer window for various events, we always position things relative to the inner window.
|
||||||
_flutterViewWindow = pluginRegistrar->GetView()->GetNativeWindow();
|
_flutterViewWindow = pluginRegistrar->GetView()->GetNativeWindow();
|
||||||
@@ -218,9 +192,6 @@ BackingWindow::BackingWindow(flutter::PluginRegistrarWindows *pluginRegistrar,
|
|||||||
|
|
||||||
RECT flutterChildRect;
|
RECT flutterChildRect;
|
||||||
::GetWindowRect(_flutterViewWindow, &flutterChildRect);
|
::GetWindowRect(_flutterViewWindow, &flutterChildRect);
|
||||||
// ::GetClientRect(flutterWindow, &flutterChildRect);
|
|
||||||
|
|
||||||
std::cout << "child rect " << flutterChildRect.left << " " << flutterChildRect.top << " " << flutterChildRect.right << " " << flutterChildRect.bottom << std::endl;
|
|
||||||
|
|
||||||
// set composition to allow transparency
|
// set composition to allow transparency
|
||||||
SetWindowComposition(_flutterRootWindow, 6, 0);
|
SetWindowComposition(_flutterRootWindow, 6, 0);
|
||||||
@@ -230,26 +201,21 @@ BackingWindow::BackingWindow(flutter::PluginRegistrarWindows *pluginRegistrar,
|
|||||||
UINT message,
|
UINT message,
|
||||||
WPARAM wparam,
|
WPARAM wparam,
|
||||||
LPARAM lparam) {
|
LPARAM lparam) {
|
||||||
// std::cout << "TOP LEVEL EVENT " << message << std::endl;
|
|
||||||
switch (message) {
|
switch (message) {
|
||||||
case WM_MOUSEMOVE: {
|
case WM_MOUSEMOVE: {
|
||||||
// std::cout << "FLUTTER MOUSE MOVE" << std::endl;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case WM_ACTIVATE: {
|
case WM_ACTIVATE: {
|
||||||
std::cout << "WM_ACTIVATE" << std::endl;
|
|
||||||
RECT rootWindowRect;
|
RECT rootWindowRect;
|
||||||
::GetWindowRect(_flutterViewWindow, &rootWindowRect);
|
::GetWindowRect(_flutterViewWindow, &rootWindowRect);
|
||||||
// Position |native_view| such that it's z order is behind |window_| &
|
// Position |native_view| such that it's z order is behind |window_| &
|
||||||
// redraw aswell.
|
// redraw aswell.
|
||||||
::SetWindowPos(_windowHandle, _flutterRootWindow, rootWindowRect.left,
|
::SetWindowPos(_windowHandle, _flutterRootWindow, rootWindowRect.left + _left,
|
||||||
rootWindowRect.top, rootWindowRect.right - rootWindowRect.left,
|
rootWindowRect.top + _top, _width,
|
||||||
rootWindowRect.bottom - rootWindowRect.top, SWP_NOACTIVATE);
|
_height, SWP_NOACTIVATE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case WM_SIZE: {
|
case WM_SIZE: {
|
||||||
std::cout << "WM_SIZE" << std::endl;
|
|
||||||
|
|
||||||
// Handle Windows's minimize & maximize animations properly.
|
// Handle Windows's minimize & maximize animations properly.
|
||||||
// Since |SetWindowPos| & other Win32 APIs on |native_view_container_|
|
// Since |SetWindowPos| & other Win32 APIs on |native_view_container_|
|
||||||
// do not re-produce the same DWM animations like actual user
|
// do not re-produce the same DWM animations like actual user
|
||||||
@@ -289,6 +255,12 @@ BackingWindow::BackingWindow(flutter::PluginRegistrarWindows *pluginRegistrar,
|
|||||||
if (wparam != SIZE_MINIMIZED) {
|
if (wparam != SIZE_MINIMIZED) {
|
||||||
::ShowWindow(_windowHandle, SW_SHOWNOACTIVATE);
|
::ShowWindow(_windowHandle, SW_SHOWNOACTIVATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RECT flutterViewRect;
|
||||||
|
::GetWindowRect(_flutterViewWindow, &flutterViewRect);
|
||||||
|
::SetWindowPos(_windowHandle, _flutterRootWindow, flutterViewRect.left + _left,
|
||||||
|
flutterViewRect.top + _top, _width, _height,
|
||||||
|
SWP_NOACTIVATE);
|
||||||
},
|
},
|
||||||
last_thread_time_)
|
last_thread_time_)
|
||||||
.detach();
|
.detach();
|
||||||
@@ -302,12 +274,11 @@ BackingWindow::BackingWindow(flutter::PluginRegistrarWindows *pluginRegistrar,
|
|||||||
case WM_WINDOWPOSCHANGED: {
|
case WM_WINDOWPOSCHANGED: {
|
||||||
RECT rootWindowRect;
|
RECT rootWindowRect;
|
||||||
::GetWindowRect(_flutterViewWindow, &rootWindowRect);
|
::GetWindowRect(_flutterViewWindow, &rootWindowRect);
|
||||||
// std::cout << "FLUTTER WINDOWPOSCHANGED TO " << rootWindowRect.left << " " << rootWindowRect.top << " " << rootWindowRect.right << " " << rootWindowRect.bottom << std::endl;
|
|
||||||
if (rootWindowRect.right - rootWindowRect.left > 0 &&
|
if (rootWindowRect.right - rootWindowRect.left > 0 &&
|
||||||
rootWindowRect.bottom - rootWindowRect.top > 0) {
|
rootWindowRect.bottom - rootWindowRect.top > 0) {
|
||||||
::SetWindowPos(_windowHandle, _flutterRootWindow, rootWindowRect.left,
|
::SetWindowPos(_windowHandle, _flutterRootWindow, rootWindowRect.left + _left,
|
||||||
rootWindowRect.top, rootWindowRect.right - rootWindowRect.left,
|
rootWindowRect.top + _top, _width,
|
||||||
rootWindowRect.bottom - rootWindowRect.top, SWP_NOACTIVATE);
|
_height, SWP_NOACTIVATE);
|
||||||
// |window_| is minimized.
|
// |window_| is minimized.
|
||||||
if (rootWindowRect.left < 0 && rootWindowRect.top < 0 &&
|
if (rootWindowRect.left < 0 && rootWindowRect.top < 0 &&
|
||||||
rootWindowRect.right < 0 && rootWindowRect.bottom < 0) {
|
rootWindowRect.right < 0 && rootWindowRect.bottom < 0) {
|
||||||
@@ -343,7 +314,7 @@ BackingWindow::BackingWindow(flutter::PluginRegistrarWindows *pluginRegistrar,
|
|||||||
window_class.hbrBackground = ::CreateSolidBrush(0);
|
window_class.hbrBackground = ::CreateSolidBrush(0);
|
||||||
::RegisterClassExW(&window_class);
|
::RegisterClassExW(&window_class);
|
||||||
_windowHandle = ::CreateWindow(kClassName, kWindowName, WS_OVERLAPPEDWINDOW,
|
_windowHandle = ::CreateWindow(kClassName, kWindowName, WS_OVERLAPPEDWINDOW,
|
||||||
0, 0, initialWidth, initialHeight, nullptr,
|
0, 0, _width, _height, nullptr,
|
||||||
nullptr, GetModuleHandle(nullptr), nullptr);
|
nullptr, GetModuleHandle(nullptr), nullptr);
|
||||||
|
|
||||||
// Disable DWM animations
|
// Disable DWM animations
|
||||||
@@ -352,22 +323,19 @@ BackingWindow::BackingWindow(flutter::PluginRegistrarWindows *pluginRegistrar,
|
|||||||
&disable_window_transitions,
|
&disable_window_transitions,
|
||||||
sizeof(disable_window_transitions));
|
sizeof(disable_window_transitions));
|
||||||
|
|
||||||
::SetWindowSubclass(_windowHandle, NativeViewSubclassProc, 69420,
|
auto style = ::GetWindowLong(_windowHandle, GWL_STYLE);
|
||||||
NULL); // what does this do?
|
|
||||||
|
|
||||||
auto style = ::GetWindowLongPtr(_windowHandle, GWL_STYLE);
|
|
||||||
style &= ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX |
|
style &= ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX |
|
||||||
WS_EX_APPWINDOW);
|
WS_EX_APPWINDOW);
|
||||||
::SetWindowLongPtr(_windowHandle, GWL_STYLE, style);
|
::SetWindowLong(_windowHandle, GWL_STYLE, style);
|
||||||
|
|
||||||
RECT flutterViewRect;
|
|
||||||
::GetClientRect(_flutterViewWindow, &flutterViewRect);
|
|
||||||
|
|
||||||
::SetWindowLongPtr(_windowHandle, GWLP_USERDATA,
|
::SetWindowLongPtr(_windowHandle, GWLP_USERDATA,
|
||||||
reinterpret_cast<LONG>(_flutterRootWindow));
|
reinterpret_cast<LONG>(_flutterRootWindow));
|
||||||
|
|
||||||
::SetWindowPos(_windowHandle, _flutterRootWindow, flutterViewRect.left,
|
RECT flutterViewRect;
|
||||||
flutterViewRect.top, flutterViewRect.right - flutterViewRect.left, flutterViewRect.bottom - flutterViewRect.top,
|
::GetWindowRect(_flutterViewWindow, &flutterViewRect);
|
||||||
|
|
||||||
|
::SetWindowPos(_windowHandle, _flutterRootWindow, flutterViewRect.left + _left,
|
||||||
|
flutterViewRect.top + _top, _width, _height,
|
||||||
SWP_NOACTIVATE);
|
SWP_NOACTIVATE);
|
||||||
|
|
||||||
// remove taskbar entry for the window we created
|
// remove taskbar entry for the window we created
|
||||||
@@ -378,8 +346,28 @@ BackingWindow::BackingWindow(flutter::PluginRegistrarWindows *pluginRegistrar,
|
|||||||
taskbar->Release();
|
taskbar->Release();
|
||||||
|
|
||||||
::ShowWindow(_windowHandle, SW_SHOW);
|
::ShowWindow(_windowHandle, SW_SHOW);
|
||||||
::ShowWindow(_flutterRootWindow, SW_SHOW);
|
::ShowWindow(_flutterViewWindow, SW_SHOW);
|
||||||
::SetFocus(_flutterRootWindow);
|
::SetForegroundWindow(_flutterViewWindow);
|
||||||
|
::SetFocus(_flutterViewWindow);
|
||||||
|
LONG ex_style = ::GetWindowLong(_flutterRootWindow, GWL_EXSTYLE);
|
||||||
|
ex_style &= ~(WS_EX_TRANSPARENT | WS_EX_LAYERED);
|
||||||
|
::SetWindowLong(_flutterRootWindow, GWL_EXSTYLE, ex_style);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BackingWindow::Resize(int width, int height, int left, int top) {
|
||||||
|
_width = width;
|
||||||
|
_height = height;
|
||||||
|
_left = left;
|
||||||
|
_top = top;
|
||||||
|
RECT flutterViewRect;
|
||||||
|
::ShowWindow(_windowHandle, SW_HIDE);
|
||||||
|
::GetWindowRect(_flutterViewWindow, &flutterViewRect);
|
||||||
|
std::cout << "Resizing to " << _width << " x " << _height << " with LT" << _left << " " << _top << " flutter view rect" << flutterViewRect.left << " " << flutterViewRect.top << " " << flutterViewRect.right << " " << flutterViewRect.bottom << std::endl;
|
||||||
|
|
||||||
|
::SetWindowPos(_windowHandle, _flutterRootWindow, flutterViewRect.left + _left,
|
||||||
|
flutterViewRect.top + _top, _width, _height,
|
||||||
|
SWP_NOACTIVATE);
|
||||||
|
::ShowWindow(_windowHandle, SW_SHOWNOACTIVATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
HWND BackingWindow::GetHandle() { return _windowHandle; }
|
HWND BackingWindow::GetHandle() { return _windowHandle; }
|
||||||
|
|||||||
@@ -11,13 +11,20 @@ class BackingWindow {
|
|||||||
public:
|
public:
|
||||||
BackingWindow(
|
BackingWindow(
|
||||||
flutter::PluginRegistrarWindows *pluginRegistrar,
|
flutter::PluginRegistrarWindows *pluginRegistrar,
|
||||||
int initialWidth,
|
int width,
|
||||||
int initialHeight);
|
int height,
|
||||||
|
int left,
|
||||||
|
int top);
|
||||||
HWND GetHandle();
|
HWND GetHandle();
|
||||||
|
void Resize(int width, int height, int left, int top);
|
||||||
private:
|
private:
|
||||||
HWND _windowHandle;
|
HWND _windowHandle;
|
||||||
HWND _flutterRootWindow;
|
HWND _flutterRootWindow;
|
||||||
HWND _flutterViewWindow;
|
HWND _flutterViewWindow;
|
||||||
|
uint32_t _width = 0;
|
||||||
|
uint32_t _height = 0;
|
||||||
|
uint32_t _left = 0;
|
||||||
|
uint32_t _top = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,11 +137,19 @@ EGLContext::EGLContext(flutter::PluginRegistrarWindows* pluginRegistrar, flutter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EGLContext::CreateTexture(
|
EGLContext::CreateRenderingSurface(
|
||||||
uint32_t width, uint32_t height,
|
uint32_t width, uint32_t height,
|
||||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result,
|
||||||
|
uint32_t left, uint32_t top
|
||||||
|
) {
|
||||||
importGLESExtensionsEntryPoints();
|
importGLESExtensionsEntryPoints();
|
||||||
|
|
||||||
|
if(left != 0 || top != 0) {
|
||||||
|
result->Error("ERROR",
|
||||||
|
"Rendering with EGL uses a Texture render target/Flutter widget and does not need a window offset.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (_active.get()) {
|
if (_active.get()) {
|
||||||
result->Error("ERROR",
|
result->Error("ERROR",
|
||||||
"Texture already exists. You must call destroyTexture before "
|
"Texture already exists. You must call destroyTexture before "
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ namespace polyvox_filament {
|
|||||||
class EGLContext : public FlutterRenderingContext {
|
class EGLContext : public FlutterRenderingContext {
|
||||||
public:
|
public:
|
||||||
EGLContext(flutter::PluginRegistrarWindows* pluginRegistrar, flutter::TextureRegistrar* textureRegistrar);
|
EGLContext(flutter::PluginRegistrarWindows* pluginRegistrar, flutter::TextureRegistrar* textureRegistrar);
|
||||||
void CreateTexture(
|
|
||||||
uint32_t width, uint32_t height,
|
|
||||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
|
||||||
private:
|
private:
|
||||||
EGLContext _context = NULL;
|
EGLContext _context = NULL;
|
||||||
EGLConfig _eglConfig = NULL;
|
EGLConfig _eglConfig = NULL;
|
||||||
|
|||||||
@@ -12,17 +12,14 @@ namespace polyvox_filament {
|
|||||||
|
|
||||||
class FlutterRenderContext {
|
class FlutterRenderContext {
|
||||||
public:
|
public:
|
||||||
|
void CreateRenderingSurface(uint32_t width, uint32_t height, std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result, uint32_t left, uint32_t top );
|
||||||
|
|
||||||
void DestroyTexture(std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
void DestroyTexture(std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||||
if (!_active) {
|
if (!_active) {
|
||||||
result->Success("Texture has already been detroyed, ignoring");
|
result->Success("Texture has already been detroyed, ignoring");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (_active->flutterTextureId != *flutterTextureId) {
|
|
||||||
// result->Error("TEXTURE_MISMATCH", "Specified texture ID is not active");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
auto sh = std::make_shared<
|
auto sh = std::make_shared<
|
||||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>>(
|
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>>(
|
||||||
std::move(result));
|
std::move(result));
|
||||||
|
|||||||
@@ -167,8 +167,17 @@ void PolyvoxFilamentPlugin::CreateTexture(
|
|||||||
const auto *args =
|
const auto *args =
|
||||||
std::get_if<flutter::EncodableList>(methodCall.arguments());
|
std::get_if<flutter::EncodableList>(methodCall.arguments());
|
||||||
|
|
||||||
const auto width = (uint32_t)round(*(std::get_if<double>(&(args->at(0)))));
|
double dWidth = *(std::get_if<double>(&(args->at(0))));
|
||||||
const auto height = (uint32_t)round(*(std::get_if<double>(&(args->at(1)))));
|
double dHeight = *(std::get_if<double>(&(args->at(1))));
|
||||||
|
double pixelRatio = *(std::get_if<double>(&(args->at(2))));
|
||||||
|
double dLeft = *(std::get_if<double>(&(args->at(3))));
|
||||||
|
double dTop = *(std::get_if<double>(&(args->at(4))));
|
||||||
|
auto width = (uint32_t)round(dWidth * pixelRatio);
|
||||||
|
auto height = (uint32_t)round(dHeight * pixelRatio);
|
||||||
|
auto left = (uint32_t)round(dLeft * pixelRatio);
|
||||||
|
auto top = (uint32_t)round(dTop * pixelRatio);
|
||||||
|
|
||||||
|
std::cout << "Using " << width << "x" << height << " (pixel ratio " << pixelRatio << ")" << std::endl;
|
||||||
|
|
||||||
// create a single shared context for the life of the application
|
// create a single shared context for the life of the application
|
||||||
// this will be used to create a backing texture and passed to Filament
|
// this will be used to create a backing texture and passed to Filament
|
||||||
@@ -179,7 +188,7 @@ void PolyvoxFilamentPlugin::CreateTexture(
|
|||||||
_context = std::make_unique<WGLContext>(_pluginRegistrar, _textureRegistrar);
|
_context = std::make_unique<WGLContext>(_pluginRegistrar, _textureRegistrar);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
_context->CreateTexture(width, height, std::move(result));
|
_context->CreateRenderingSurface(width, height, std::move(result), left, top);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PolyvoxFilamentPlugin::DestroyTexture(
|
void PolyvoxFilamentPlugin::DestroyTexture(
|
||||||
@@ -207,7 +216,7 @@ void PolyvoxFilamentPlugin::HandleMethodCall(
|
|||||||
const flutter::MethodCall<flutter::EncodableValue> &methodCall,
|
const flutter::MethodCall<flutter::EncodableValue> &methodCall,
|
||||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||||
|
|
||||||
if (methodCall.method_name() == "useBackingWindow") {
|
if (methodCall.method_name() == "usesBackingWindow") {
|
||||||
result->Success(flutter::EncodableValue(
|
result->Success(flutter::EncodableValue(
|
||||||
#ifdef WGL_USE_BACKING_WINDOW
|
#ifdef WGL_USE_BACKING_WINDOW
|
||||||
true
|
true
|
||||||
@@ -215,12 +224,28 @@ void PolyvoxFilamentPlugin::HandleMethodCall(
|
|||||||
false
|
false
|
||||||
#endif
|
#endif
|
||||||
));
|
));
|
||||||
} else if (methodCall.method_name() == "getSharedContext") {
|
|
||||||
result->Success(flutter::EncodableValue((int64_t)_context->sharedContext));
|
|
||||||
} else if (methodCall.method_name() == "getResourceLoaderWrapper") {
|
} else if (methodCall.method_name() == "getResourceLoaderWrapper") {
|
||||||
const ResourceLoaderWrapper *const resourceLoader =
|
const ResourceLoaderWrapper *const resourceLoader =
|
||||||
new ResourceLoaderWrapper(_loadResource, _freeResource, this);
|
new ResourceLoaderWrapper(_loadResource, _freeResource, this);
|
||||||
result->Success(flutter::EncodableValue((int64_t)resourceLoader));
|
result->Success(flutter::EncodableValue((int64_t)resourceLoader));
|
||||||
|
} else if (methodCall.method_name() == "resizeWindow") {
|
||||||
|
#if WGL_USE_BACKING_WINDOW
|
||||||
|
const auto *args =
|
||||||
|
std::get_if<flutter::EncodableList>(methodCall.arguments());
|
||||||
|
double dWidth = *(std::get_if<double>(&(args->at(0))));
|
||||||
|
double dHeight = *(std::get_if<double>(&(args->at(1))));
|
||||||
|
double pixelRatio = *(std::get_if<double>(&(args->at(2))));
|
||||||
|
double dLeft = *(std::get_if<double>(&(args->at(3))));
|
||||||
|
double dTop = *(std::get_if<double>(&(args->at(4))));
|
||||||
|
auto width = (uint32_t)round(dWidth * pixelRatio);
|
||||||
|
auto height = (uint32_t)round(dHeight * pixelRatio);
|
||||||
|
auto left = (uint32_t)round(dLeft * pixelRatio);
|
||||||
|
auto top = (uint32_t)round(dTop * pixelRatio);
|
||||||
|
_context->ResizeRenderingSurface(width, height, left, top);
|
||||||
|
result->Success();
|
||||||
|
#else
|
||||||
|
result->Error("ERROR", "resizeWindow is only available when using a backing window");
|
||||||
|
#endif
|
||||||
} else if (methodCall.method_name() == "createTexture") {
|
} else if (methodCall.method_name() == "createTexture") {
|
||||||
CreateTexture(methodCall, std::move(result));
|
CreateTexture(methodCall, std::move(result));
|
||||||
} else if (methodCall.method_name() == "destroyTexture") {
|
} else if (methodCall.method_name() == "destroyTexture") {
|
||||||
|
|||||||
@@ -96,24 +96,37 @@ WGLContext::WGLContext(flutter::PluginRegistrarWindows *pluginRegistrar,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WGLContext::CreateTexture(
|
void WGLContext::ResizeRenderingSurface(uint32_t width, uint32_t height, uint32_t left, uint32_t top) {
|
||||||
|
_backingWindow->Resize(width, height, left, top);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WGLContext::CreateRenderingSurface(
|
||||||
uint32_t width, uint32_t height,
|
uint32_t width, uint32_t height,
|
||||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result, uint32_t left, uint32_t top) {
|
||||||
|
|
||||||
#if WGL_USE_BACKING_WINDOW
|
#if WGL_USE_BACKING_WINDOW
|
||||||
_backingWindow = std::make_unique<BackingWindow>(
|
if(!_backingWindow) {
|
||||||
_pluginRegistrar, static_cast<int>(width), static_cast<int>(height));
|
_backingWindow = std::make_unique<BackingWindow>(
|
||||||
|
_pluginRegistrar, static_cast<int>(width), static_cast<int>(height), static_cast<int>(left), static_cast<int>(top));
|
||||||
|
} else {
|
||||||
|
ResizeRenderingSurface(width, height, left, top);
|
||||||
|
}
|
||||||
std::vector<flutter::EncodableValue> resultList;
|
std::vector<flutter::EncodableValue> resultList;
|
||||||
resultList.push_back(flutter::EncodableValue((int64_t) nullptr));
|
resultList.push_back(flutter::EncodableValue((int64_t) nullptr));
|
||||||
resultList.push_back(
|
resultList.push_back(
|
||||||
flutter::EncodableValue((int64_t)_backingWindow->GetHandle()));
|
flutter::EncodableValue((int64_t)_backingWindow->GetHandle()));
|
||||||
resultList.push_back(flutter::EncodableValue((int64_t) nullptr));
|
resultList.push_back(flutter::EncodableValue((int64_t) nullptr));
|
||||||
|
resultList.push_back(flutter::EncodableValue((int64_t)sharedContext));
|
||||||
result->Success(resultList);
|
result->Success(resultList);
|
||||||
#else
|
#else
|
||||||
if (_active.get()) {
|
if(left != 0 || top != 0) {
|
||||||
|
result->Error("ERROR",
|
||||||
|
"When WGL_USE_BACKING_WINDOW is false, rendering with WGL uses a Texture render target/Flutter widget and does not need a window offset.");
|
||||||
|
} else if (_active.get()) {
|
||||||
result->Error("ERROR",
|
result->Error("ERROR",
|
||||||
"Texture already exists. You must call destroyTexture before "
|
"Texture already exists. You must call destroyTexture before "
|
||||||
"attempting to create a new one.");
|
"attempting to create a new one.");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
_active = std::make_unique<OpenGLTextureBuffer>(
|
_active = std::make_unique<OpenGLTextureBuffer>(
|
||||||
_pluginRegistrar, _textureRegistrar, std::move(result), width, height,
|
_pluginRegistrar, _textureRegistrar, std::move(result), width, height,
|
||||||
@@ -124,9 +137,10 @@ void WGLContext::CreateTexture(
|
|||||||
resultList.push_back(flutter::EncodableValue((int64_t) nullptr));
|
resultList.push_back(flutter::EncodableValue((int64_t) nullptr));
|
||||||
resultList.push_back(flutter::EncodableValue((int64_t) nullptr));
|
resultList.push_back(flutter::EncodableValue((int64_t) nullptr));
|
||||||
resultList.push_back(flutter::EncodableValue((int64_t) nullptr));
|
resultList.push_back(flutter::EncodableValue((int64_t) nullptr));
|
||||||
|
resultList.push_back(flutter::EncodableValue((int64_t)sharedContext));
|
||||||
result->Success(resultList);
|
result->Success(resultList);
|
||||||
} else {
|
} else {
|
||||||
result->Error("FOO", "ERROR", nullptr);
|
result->Error("NO_FLUTTER_TEXTURE", "Unknown error registering texture with Flutter.", nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -12,9 +12,15 @@ namespace polyvox_filament {
|
|||||||
class WGLContext : public FlutterRenderContext {
|
class WGLContext : public FlutterRenderContext {
|
||||||
public:
|
public:
|
||||||
WGLContext(flutter::PluginRegistrarWindows* pluginRegistrar, flutter::TextureRegistrar* textureRegistrar);
|
WGLContext(flutter::PluginRegistrarWindows* pluginRegistrar, flutter::TextureRegistrar* textureRegistrar);
|
||||||
void CreateTexture(uint32_t width, uint32_t height, std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
|
||||||
void* GetSharedContext();
|
void* GetSharedContext();
|
||||||
|
void CreateRenderingSurface(
|
||||||
|
uint32_t width, uint32_t height,
|
||||||
|
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result, uint32_t left, uint32_t top);
|
||||||
|
void ResizeRenderingSurface(
|
||||||
|
uint32_t width, uint32_t height, uint32_t left, uint32_t top
|
||||||
|
);
|
||||||
private:
|
private:
|
||||||
|
|
||||||
flutter::PluginRegistrarWindows* _pluginRegistrar = nullptr;
|
flutter::PluginRegistrarWindows* _pluginRegistrar = nullptr;
|
||||||
flutter::TextureRegistrar* _textureRegistrar = nullptr;
|
flutter::TextureRegistrar* _textureRegistrar = nullptr;
|
||||||
HGLRC _context = NULL;
|
HGLRC _context = NULL;
|
||||||
|
|||||||
Reference in New Issue
Block a user