refactoring
This commit is contained in:
@@ -44,9 +44,17 @@ namespace thermion
|
||||
uint64_t TSWAP_CHAIN_CONFIG_APPLE_CVPIXELBUFFER = filament::backend::SWAP_CHAIN_CONFIG_APPLE_CVPIXELBUFFER;
|
||||
uint64_t TSWAP_CHAIN_CONFIG_HAS_STENCIL_BUFFER = filament::backend::SWAP_CHAIN_CONFIG_HAS_STENCIL_BUFFER;
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE TEngine *Engine_create(TBackend backend)
|
||||
EMSCRIPTEN_KEEPALIVE TEngine *Engine_create(
|
||||
TBackend backend,
|
||||
void* platform,
|
||||
void* sharedContext,
|
||||
uint8_t stereoscopicEyeCount,
|
||||
bool disableHandleUseAfterFreeCheck)
|
||||
{
|
||||
auto *engine = filament::Engine::create(static_cast<filament::Engine::Backend>(backend));
|
||||
filament::Engine::Config config;
|
||||
config.stereoscopicEyeCount = stereoscopicEyeCount;
|
||||
config.disableHandleUseAfterFreeCheck = disableHandleUseAfterFreeCheck;
|
||||
auto *engine = filament::Engine::create(static_cast<filament::Engine::Backend>(backend), platform, sharedContext, &config);
|
||||
return reinterpret_cast<TEngine *>(engine);
|
||||
}
|
||||
|
||||
@@ -71,6 +79,12 @@ namespace thermion
|
||||
return reinterpret_cast<TSwapChain *>(swapChain);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Engine_destroySwapChain(TEngine *tEngine, TSwapChain *tSwapChain) {
|
||||
auto *engine = reinterpret_cast<Engine *>(tEngine);
|
||||
auto *swapChain = reinterpret_cast<SwapChain *>(tSwapChain);
|
||||
engine->destroy(swapChain);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE TView *Engine_createView(TEngine *tEngine)
|
||||
{
|
||||
auto *engine = reinterpret_cast<Engine *>(tEngine);
|
||||
@@ -279,12 +293,21 @@ namespace thermion
|
||||
EMSCRIPTEN_KEEPALIVE void Engine_destroySkybox(TEngine *tEngine, TSkybox *tSkybox) {
|
||||
auto *engine = reinterpret_cast<filament::Engine *>(tEngine);
|
||||
auto *skybox = reinterpret_cast<filament::Skybox *>(tSkybox);
|
||||
if(skybox->getTexture()) {
|
||||
engine->destroy(skybox->getTexture());
|
||||
}
|
||||
engine->destroy(skybox);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Engine_destroyIndirectLight(TEngine *tEngine, TIndirectLight *tIndirectLight) {
|
||||
auto *engine = reinterpret_cast<filament::Engine *>(tEngine);
|
||||
auto *indirectLight = reinterpret_cast<filament::IndirectLight *>(tIndirectLight);
|
||||
if(indirectLight->getReflectionsTexture()) {
|
||||
engine->destroy(indirectLight->getReflectionsTexture());
|
||||
}
|
||||
if(indirectLight->getIrradianceTexture()) {
|
||||
engine->destroy(indirectLight->getIrradianceTexture());
|
||||
}
|
||||
engine->destroy(indirectLight);
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,12 @@ EMSCRIPTEN_KEEPALIVE TMaterialInstance *GltfAssetLoader_getMaterialInstance(TRen
|
||||
return reinterpret_cast<TMaterialInstance*>(mi);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE TMaterialProvider *GltfAssetLoader_getMaterialProvider(TGltfAssetLoader *tAssetLoader) {
|
||||
auto *assetLoader = reinterpret_cast<gltfio::AssetLoader>(tAssetLoader);
|
||||
auto materialProvider = assetLoader->getMaterialProvider();
|
||||
return reinterpret_cast<TMaterialProvider *>(&materialProvider);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
}
|
||||
|
||||
41
thermion_dart/native/src/c_api/TIndirectLight.cpp
Normal file
41
thermion_dart/native/src/c_api/TIndirectLight.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#include "c_api/TScene.h"
|
||||
|
||||
#include <filament/Engine.h>
|
||||
#include <filament/Fence.h>
|
||||
#include <filament/IndirectLight.h>
|
||||
#include <filament/Material.h>
|
||||
#include <filament/Scene.h>
|
||||
#include <filament/Skybox.h>
|
||||
#include <filament/Texture.h>
|
||||
#include <filament/TextureSampler.h>
|
||||
#include <filament/TransformManager.h>
|
||||
#include <filament/View.h>
|
||||
|
||||
#include <gltfio/FilamentAsset.h>
|
||||
#include <gltfio/FilamentInstance.h>
|
||||
|
||||
#include "Log.hpp"
|
||||
|
||||
#ifdef __cplusplus
|
||||
namespace thermion
|
||||
{
|
||||
extern "C"
|
||||
{
|
||||
using namespace filament;
|
||||
#endif
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void IndirectLight_setRotation(TIndirectLight *tIndirectLight, double3x3 rotation)
|
||||
{
|
||||
auto *indirectLight = reinterpret_cast<filament::IndirectLight *>(tIndirectLight);
|
||||
const filament::math::mat3f fRotation {
|
||||
filament::math::float3 { rotation.col1.x, rotation.col1.y, rotation.col1.z },
|
||||
filament::math::float3 { rotation.col2.x, rotation.col2.y, rotation.col2.z },
|
||||
filament::math::float3 { rotation.col3.x, rotation.col3.y, rotation.col3.z },
|
||||
};
|
||||
indirectLight->setRotation(fRotation);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -34,6 +34,16 @@ namespace thermion
|
||||
return reinterpret_cast<TRenderTarget *>(rt);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void RenderTarget_destroy(
|
||||
TEngine *tEngine,
|
||||
TRenderTarget *tRenderTarget
|
||||
) {
|
||||
auto engine = reinterpret_cast<filament::Engine *>(tEngine);
|
||||
auto *renderTarget = reinterpret_cast<filament::RenderTarget *>(tRenderTarget);
|
||||
engine->destroy(renderTarget);
|
||||
}
|
||||
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
38
thermion_dart/native/src/c_api/TRenderTicker.cpp
Normal file
38
thermion_dart/native/src/c_api/TRenderTicker.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifdef _WIN32
|
||||
#include "ThermionWin32.h"
|
||||
#endif
|
||||
|
||||
#include <thread>
|
||||
#include <functional>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/emscripten.h>
|
||||
#endif
|
||||
|
||||
#include "filament/LightManager.h"
|
||||
#include "Log.hpp"
|
||||
|
||||
using namespace thermion;
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#include "c_api/TRenderTicker.hpp"
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE TRenderTicker *RenderTicker_create(TRenderer *tRenderer, TSceneManager *tSceneManager) {
|
||||
auto *renderer = reinterpret_cast<filament::Renderer *>(tRenderer);
|
||||
auto *sceneManager = reinterpret_cast<thermion::SceneManager *>(tSceneManager);
|
||||
auto *renderTicker = new RenderTicker(renderer, sceneManager);
|
||||
return reinterpret_cast<TRenderTicker *>(renderTicker);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void RenderTicker_render(TRenderTicker *tRenderTicker, uint64_t frameTimeInNanos) {
|
||||
auto *renderTicker = reinterpret_cast<RenderTicker *>
|
||||
renderTicker->render(frameTimeInNanos);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void RenderTicker_setRenderable(TRenderTicker *tRenderTicker, TSwapChain *swapChain, TView **views, uint8_t numViews) {
|
||||
auto *renderTicker = reinterpret_cast<RenderTicker *>
|
||||
renderTicker->setRenderable(swapChain, views, numViews);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -61,6 +61,22 @@ EMSCRIPTEN_KEEPALIVE void Renderer_renderStandaloneView(TRenderer *tRenderer, TV
|
||||
renderer->renderStandaloneView(view);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Renderer_setFrameRateOptions(
|
||||
TRenderer *tRenderer,
|
||||
float headRoomRatio,
|
||||
float scaleRate,
|
||||
uint8_t history,
|
||||
uint8_t interval
|
||||
) {
|
||||
auto *renderer = reinterpret_cast<filament::Renderer *>(tRenderer);
|
||||
filament::Renderer::FrameRateOptions fro;
|
||||
fro.headRoomRatio = headRoomRatio;
|
||||
fro.scaleRate = scaleRate;
|
||||
fro.interval = interval;
|
||||
fro.interval = interval;
|
||||
renderer->setFrameRateOptions(fro);
|
||||
}
|
||||
|
||||
class CaptureCallbackHandler : public filament::backend::CallbackHandler
|
||||
{
|
||||
void post(void *user, Callback callback)
|
||||
|
||||
@@ -1,415 +0,0 @@
|
||||
#ifdef _WIN32
|
||||
#include "ThermionWin32.h"
|
||||
#endif
|
||||
|
||||
#include <thread>
|
||||
#include <functional>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/emscripten.h>
|
||||
#endif
|
||||
|
||||
#include "filament/LightManager.h"
|
||||
#include "ResourceBuffer.hpp"
|
||||
#include "FilamentViewer.hpp"
|
||||
#include "Log.hpp"
|
||||
#include "ThreadPool.hpp"
|
||||
|
||||
using namespace thermion;
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
#include "c_api/ThermionDartApi.h"
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE TViewer *Viewer_create(const void *context, const void *const loader, void *const platform, const char *uberArchivePath)
|
||||
{
|
||||
const auto *loaderImpl = new ResourceLoaderWrapperImpl((ResourceLoaderWrapper *)loader);
|
||||
auto viewer = new FilamentViewer(context, loaderImpl, platform, uberArchivePath);
|
||||
return reinterpret_cast<TViewer *>(viewer);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE TEngine *Viewer_getEngine(TViewer *viewer)
|
||||
{
|
||||
auto *engine = reinterpret_cast<FilamentViewer *>(viewer)->getEngine();
|
||||
return reinterpret_cast<TEngine *>(engine);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE TRenderer *Viewer_getRenderer(TViewer *tViewer) {
|
||||
auto *viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
return reinterpret_cast<TRenderer *>(viewer->getRenderer());
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE TRenderTarget *Viewer_createRenderTarget(TViewer *tViewer, intptr_t colorTexture, intptr_t depthTexture, uint32_t width, uint32_t height)
|
||||
{
|
||||
auto viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
auto renderTarget = viewer->createRenderTarget(colorTexture, depthTexture, width, height);
|
||||
return reinterpret_cast<TRenderTarget *>(renderTarget);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_destroyRenderTarget(TViewer *tViewer, TRenderTarget *tRenderTarget)
|
||||
{
|
||||
auto viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
auto renderTarget = reinterpret_cast<RenderTarget *>(tRenderTarget);
|
||||
viewer->destroyRenderTarget(renderTarget);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_destroy(TViewer *tViewer)
|
||||
{
|
||||
auto *viewer = reinterpret_cast<FilamentViewer*>(tViewer);
|
||||
delete viewer;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void set_background_color(TViewer *viewer, const float r, const float g, const float b, const float a)
|
||||
{
|
||||
((FilamentViewer *)viewer)->setBackgroundColor(r, g, b, a);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void clear_background_image(TViewer *viewer)
|
||||
{
|
||||
((FilamentViewer *)viewer)->clearBackgroundImage();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void set_background_image(TViewer *viewer, const char *path, bool fillHeight)
|
||||
{
|
||||
((FilamentViewer *)viewer)->setBackgroundImage(path, fillHeight, 100, 100);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void set_background_image_position(TViewer *viewer, float x, float y, bool clamp)
|
||||
{
|
||||
((FilamentViewer *)viewer)->setBackgroundImagePosition(x, y, clamp, 100, 100);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_loadSkybox(TViewer *viewer, const char *skyboxPath)
|
||||
{
|
||||
((FilamentViewer *)viewer)->loadSkybox(skyboxPath);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_removeSkybox(TViewer *viewer)
|
||||
{
|
||||
((FilamentViewer *)viewer)->removeSkybox();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void create_ibl(TViewer *viewer, float r, float g, float b, float intensity)
|
||||
{
|
||||
((FilamentViewer *)viewer)->createIbl(r, g, b, intensity);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_loadIbl(TViewer *viewer, const char *iblPath, float intensity)
|
||||
{
|
||||
((FilamentViewer *)viewer)->loadIbl(iblPath, intensity);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_removeIbl(TViewer *viewer)
|
||||
{
|
||||
((FilamentViewer *)viewer)->removeIbl();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void rotate_ibl(TViewer *viewer, float *rotationMatrix)
|
||||
{
|
||||
math::mat3f matrix(rotationMatrix[0], rotationMatrix[1],
|
||||
rotationMatrix[2],
|
||||
rotationMatrix[3],
|
||||
rotationMatrix[4],
|
||||
rotationMatrix[5],
|
||||
rotationMatrix[6],
|
||||
rotationMatrix[7],
|
||||
rotationMatrix[8]);
|
||||
|
||||
((FilamentViewer *)viewer)->rotateIbl(matrix);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int get_instance_count(TSceneManager *sceneManager, EntityId entityId)
|
||||
{
|
||||
return ((SceneManager *)sceneManager)->getInstanceCount(entityId);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void get_instances(TSceneManager *sceneManager, EntityId entityId, EntityId *out)
|
||||
{
|
||||
return ((SceneManager *)sceneManager)->getInstances(entityId, out);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_setMainCamera(TViewer *tViewer, TView *tView)
|
||||
{
|
||||
auto *viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
auto *view = reinterpret_cast<View *>(tView);
|
||||
viewer->setMainCamera(view);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE EntityId Viewer_getMainCamera(TViewer *viewer)
|
||||
{
|
||||
return ((FilamentViewer *)viewer)->getMainCamera();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE float get_camera_fov(TCamera *camera, bool horizontal)
|
||||
{
|
||||
auto cam = reinterpret_cast<filament::Camera *>(camera);
|
||||
return cam->getFieldOfViewInDegrees(horizontal ? Camera::Fov::HORIZONTAL : Camera::Fov::VERTICAL);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE double get_camera_focal_length(TCamera *const camera)
|
||||
{
|
||||
auto cam = reinterpret_cast<filament::Camera *>(camera);
|
||||
return cam->getFocalLength();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void set_camera_projection_from_fov(TCamera *camera, double fovInDegrees, double aspect, double near, double far, bool horizontal)
|
||||
{
|
||||
auto cam = reinterpret_cast<filament::Camera *>(camera);
|
||||
cam->setProjection(fovInDegrees, aspect, near, far, horizontal ? Camera::Fov::HORIZONTAL : Camera::Fov::VERTICAL);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE double4x4 get_camera_model_matrix(TCamera *camera)
|
||||
{
|
||||
const auto &mat = reinterpret_cast<filament::Camera *>(camera)->getModelMatrix();
|
||||
return convert_mat4_to_double4x4(mat);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE double4x4 get_camera_view_matrix(TCamera *camera)
|
||||
{
|
||||
const auto &mat = reinterpret_cast<filament::Camera *>(camera)->getViewMatrix();
|
||||
return convert_mat4_to_double4x4(mat);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE double4x4 get_camera_projection_matrix(TCamera *camera)
|
||||
{
|
||||
const auto &mat = reinterpret_cast<filament::Camera *>(camera)->getProjectionMatrix();
|
||||
return convert_mat4_to_double4x4(mat);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE double4x4 get_camera_culling_projection_matrix(TCamera *camera)
|
||||
{
|
||||
const auto &mat = reinterpret_cast<filament::Camera *>(camera)->getCullingProjectionMatrix();
|
||||
return convert_mat4_to_double4x4(mat);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void set_camera_projection_matrix(TCamera *camera, double4x4 matrix, double near, double far)
|
||||
{
|
||||
auto cam = reinterpret_cast<filament::Camera *>(camera);
|
||||
const auto &mat = convert_double4x4_to_mat4(matrix);
|
||||
cam->setCustomProjection(mat, near, far);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Camera_setLensProjection(TCamera *camera, double near, double far, double aspect, double focalLength)
|
||||
{
|
||||
auto cam = reinterpret_cast<filament::Camera *>(camera);
|
||||
cam->setLensProjection(focalLength, aspect, near, far);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Camera_setModelMatrix(TCamera *camera, double4x4 matrix)
|
||||
{
|
||||
auto cam = reinterpret_cast<filament::Camera *>(camera);
|
||||
cam->setModelMatrix(convert_double4x4_to_mat4(matrix));
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE double get_camera_near(TCamera *camera)
|
||||
{
|
||||
auto cam = reinterpret_cast<filament::Camera *>(camera);
|
||||
return cam->getNear();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE double get_camera_culling_far(TCamera *camera)
|
||||
{
|
||||
auto cam = reinterpret_cast<filament::Camera *>(camera);
|
||||
return cam->getCullingFar();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const double *const get_camera_frustum(TCamera *camera)
|
||||
{
|
||||
|
||||
const auto frustum = reinterpret_cast<filament::Camera *>(camera)->getFrustum();
|
||||
|
||||
const math::float4 *planes = frustum.getNormalizedPlanes();
|
||||
double *array = (double *)calloc(24, sizeof(double));
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
auto plane = planes[i];
|
||||
array[i * 4] = double(plane.x);
|
||||
array[i * 4 + 1] = double(plane.y);
|
||||
array[i * 4 + 2] = double(plane.z);
|
||||
array[i * 4 + 3] = double(plane.w);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void set_camera_focus_distance(TCamera *camera, float distance)
|
||||
{
|
||||
auto *cam = reinterpret_cast<filament::Camera *>(camera);
|
||||
cam->setFocusDistance(distance);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void set_camera_exposure(TCamera *camera, float aperture, float shutterSpeed, float sensitivity)
|
||||
{
|
||||
auto *cam = reinterpret_cast<filament::Camera *>(camera);
|
||||
cam->setExposure(aperture, shutterSpeed, sensitivity);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void set_camera_model_matrix(TCamera *camera, double4x4 matrix)
|
||||
{
|
||||
auto *cam = reinterpret_cast<filament::Camera *>(camera);
|
||||
const filament::math::mat4 &mat = convert_double4x4_to_mat4(matrix);
|
||||
cam->setModelMatrix(mat);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_render(
|
||||
TViewer *tViewer)
|
||||
{
|
||||
auto viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
viewer->render(0);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_setViewRenderable(TViewer *tViewer, TSwapChain *tSwapChain, TView *tView, bool renderable)
|
||||
{
|
||||
auto viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
auto swapChain = reinterpret_cast<SwapChain *>(tSwapChain);
|
||||
auto *view = reinterpret_cast<View *>(tView);
|
||||
viewer->setRenderable(view, swapChain, renderable);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_capture(
|
||||
TViewer *tViewer,
|
||||
TView *tView,
|
||||
TSwapChain *tSwapChain,
|
||||
uint8_t *pixelBuffer,
|
||||
bool useFence,
|
||||
void (*callback)(void))
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
useFence = true;
|
||||
#endif
|
||||
auto swapChain = reinterpret_cast<SwapChain *>(tSwapChain);
|
||||
auto viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
auto *view = reinterpret_cast<View *>(tView);
|
||||
viewer->capture(view, pixelBuffer, useFence, swapChain, callback);
|
||||
};
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_captureRenderTarget(
|
||||
TViewer *tViewer,
|
||||
TView *tView,
|
||||
TSwapChain *tSwapChain,
|
||||
TRenderTarget *tRenderTarget,
|
||||
uint8_t *pixelBuffer,
|
||||
bool useFence,
|
||||
void (*callback)(void))
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
useFence = true;
|
||||
#endif
|
||||
auto swapChain = reinterpret_cast<SwapChain *>(tSwapChain);
|
||||
auto renderTarget = reinterpret_cast<RenderTarget *>(tRenderTarget);
|
||||
auto viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
auto *view = reinterpret_cast<View *>(tView);
|
||||
viewer->capture(view, pixelBuffer, useFence, swapChain, renderTarget, callback);
|
||||
};
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void set_frame_interval(
|
||||
TViewer *viewer,
|
||||
float frameInterval)
|
||||
{
|
||||
((FilamentViewer *)viewer)->setFrameInterval(frameInterval);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_destroySwapChain(TViewer *tViewer, TSwapChain *tSwapChain)
|
||||
{
|
||||
auto viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
auto swapChain = reinterpret_cast<SwapChain *>(tSwapChain);
|
||||
viewer->destroySwapChain(swapChain);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE TSwapChain *Viewer_createHeadlessSwapChain(TViewer *tViewer, uint32_t width, uint32_t height)
|
||||
{
|
||||
auto viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
auto swapChain = viewer->createSwapChain(width, height);
|
||||
return reinterpret_cast<TSwapChain *>(swapChain);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE TSwapChain *Viewer_createSwapChain(TViewer *tViewer, const void *const window)
|
||||
{
|
||||
auto viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
auto swapChain = viewer->createSwapChain(window);
|
||||
return reinterpret_cast<TSwapChain *>(swapChain);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE TSwapChain *Viewer_getSwapChainAt(TViewer *tViewer, int index)
|
||||
{
|
||||
auto viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
auto swapChain = viewer->getSwapChainAt(index);
|
||||
return reinterpret_cast<TSwapChain *>(swapChain);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE TView *Viewer_createView(TViewer *tViewer)
|
||||
{
|
||||
auto viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
auto view = viewer->createView();
|
||||
return reinterpret_cast<TView *>(view);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE TView *Viewer_getViewAt(TViewer *tViewer, int32_t index)
|
||||
{
|
||||
auto viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
auto view = viewer->getViewAt(index);
|
||||
return reinterpret_cast<TView *>(view);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE TSceneManager *Viewer_getSceneManager(TViewer *tViewer)
|
||||
{
|
||||
auto *viewer = reinterpret_cast<FilamentViewer *>(tViewer);
|
||||
auto *sceneManager = viewer->getSceneManager();
|
||||
return reinterpret_cast<TSceneManager *>(sceneManager);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void queue_position_update_from_viewport_coords(TSceneManager *sceneManager, TView *tView, EntityId entity, float viewportX, float viewportY)
|
||||
{
|
||||
auto *view = reinterpret_cast<View *>(tView);
|
||||
((SceneManager *)sceneManager)->queueRelativePositionUpdateFromViewportVector(view, entity, viewportX, viewportY);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void ios_dummy()
|
||||
{
|
||||
Log("Dummy called");
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void thermion_filament_free(void *ptr)
|
||||
{
|
||||
free(ptr);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void add_collision_component(TSceneManager *sceneManager, EntityId entityId, void (*onCollisionCallback)(const EntityId entityId1, const EntityId entityId2), bool affectsCollidingTransform)
|
||||
{
|
||||
((SceneManager *)sceneManager)->addCollisionComponent(entityId, onCollisionCallback, affectsCollidingTransform);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void remove_collision_component(TSceneManager *sceneManager, EntityId entityId)
|
||||
{
|
||||
((SceneManager *)sceneManager)->removeCollisionComponent(entityId);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void test_collisions(TSceneManager *sceneManager, EntityId entity)
|
||||
{
|
||||
((SceneManager *)sceneManager)->testCollisions(entity);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE Aabb2 get_bounding_box(TSceneManager *sceneManager, TView *tView, EntityId entity)
|
||||
{
|
||||
auto view = reinterpret_cast<View *>(tView);
|
||||
return ((SceneManager *)sceneManager)->getScreenSpaceBoundingBox(view, entity);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void get_bounding_box_to_out(TSceneManager *sceneManager, TView *tView, EntityId entity, float *minX, float *minY, float *maxX, float *maxY)
|
||||
{
|
||||
auto view = reinterpret_cast<View *>(tView);
|
||||
auto box = ((SceneManager *)sceneManager)->getScreenSpaceBoundingBox(view, entity);
|
||||
*minX = box.minX;
|
||||
*minY = box.minY;
|
||||
*maxX = box.maxX;
|
||||
*maxY = box.maxY;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void thermion_flutter_free(void *ptr)
|
||||
{
|
||||
free(ptr);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <filament/LightManager.h>
|
||||
|
||||
#include "c_api/APIBoundaryTypes.h"
|
||||
|
||||
#include "c_api/TAnimationManager.h"
|
||||
#include "c_api/TEngine.h"
|
||||
#include "c_api/TGltfAssetLoader.h"
|
||||
@@ -19,7 +18,8 @@
|
||||
#include "c_api/TView.h"
|
||||
#include "c_api/ThermionDartRenderThreadApi.h"
|
||||
|
||||
#include "FilamentViewer.hpp"
|
||||
#include "RenderTicker.hpp"
|
||||
#include "rendering/RenderLoop.hpp"
|
||||
#include "Log.hpp"
|
||||
|
||||
#include "ThreadPool.hpp"
|
||||
@@ -28,165 +28,6 @@ using namespace thermion;
|
||||
using namespace std::chrono_literals;
|
||||
#include <time.h>
|
||||
|
||||
class RenderLoop
|
||||
{
|
||||
public:
|
||||
explicit RenderLoop()
|
||||
{
|
||||
srand(time(NULL));
|
||||
t = new std::thread([this]()
|
||||
{ start(); });
|
||||
}
|
||||
|
||||
~RenderLoop()
|
||||
{
|
||||
TRACE("Destroying RenderLoop");
|
||||
_stop = true;
|
||||
_cv.notify_one();
|
||||
TRACE("Joining RenderLoop thread..");
|
||||
t->join();
|
||||
TRACE("RenderLoop destructor complete");
|
||||
}
|
||||
|
||||
void start()
|
||||
{
|
||||
while (!_stop)
|
||||
{
|
||||
iter();
|
||||
}
|
||||
}
|
||||
|
||||
void destroyViewer() {
|
||||
std::packaged_task<void()> lambda([=]() mutable
|
||||
{
|
||||
if(viewer) {
|
||||
Viewer_destroy(viewer);
|
||||
}
|
||||
viewer = nullptr;
|
||||
_renderCallback = nullptr;
|
||||
_renderCallbackOwner = nullptr;
|
||||
|
||||
});
|
||||
auto fut = add_task(lambda);
|
||||
fut.wait();
|
||||
}
|
||||
|
||||
void createViewer(
|
||||
void *const context,
|
||||
void *const platform,
|
||||
const char *uberArchivePath,
|
||||
const void *const loader,
|
||||
void (*renderCallback)(void *),
|
||||
void *const owner,
|
||||
void (*callback)(TViewer *))
|
||||
{
|
||||
_renderCallback = renderCallback;
|
||||
_renderCallbackOwner = owner;
|
||||
std::packaged_task<void()> lambda([=]() mutable
|
||||
{
|
||||
if(viewer) {
|
||||
Viewer_destroy(viewer);
|
||||
}
|
||||
viewer = Viewer_create(context, loader, platform, uberArchivePath);
|
||||
callback(viewer); });
|
||||
add_task(lambda);
|
||||
}
|
||||
|
||||
void requestFrame(void (*callback)())
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_mutex);
|
||||
this->_requestFrameRenderCallback = callback;
|
||||
_cv.notify_one();
|
||||
}
|
||||
|
||||
void iter()
|
||||
{
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_mutex);
|
||||
if (_requestFrameRenderCallback)
|
||||
{
|
||||
doRender();
|
||||
lock.unlock();
|
||||
this->_requestFrameRenderCallback();
|
||||
this->_requestFrameRenderCallback = nullptr;
|
||||
|
||||
// Calculate and print FPS
|
||||
auto currentTime = std::chrono::high_resolution_clock::now();
|
||||
float deltaTime = std::chrono::duration<float, std::chrono::seconds::period>(currentTime - _lastFrameTime).count();
|
||||
_lastFrameTime = currentTime;
|
||||
|
||||
_frameCount++;
|
||||
_accumulatedTime += deltaTime;
|
||||
|
||||
if (_accumulatedTime >= 1.0f) // Update FPS every second
|
||||
{
|
||||
_fps = _frameCount / _accumulatedTime;
|
||||
// std::cout << "FPS: " << _fps << std::endl;
|
||||
_frameCount = 0;
|
||||
_accumulatedTime = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::unique_lock<std::mutex> taskLock(_taskMutex);
|
||||
|
||||
if (!_tasks.empty())
|
||||
{
|
||||
auto task = std::move(_tasks.front());
|
||||
_tasks.pop_front();
|
||||
taskLock.unlock();
|
||||
task();
|
||||
taskLock.lock();
|
||||
}
|
||||
|
||||
_cv.wait_for(taskLock, std::chrono::microseconds(2000), [this]
|
||||
{ return !_tasks.empty() || _stop; });
|
||||
}
|
||||
|
||||
void doRender()
|
||||
{
|
||||
Viewer_render(viewer);
|
||||
if (_renderCallback)
|
||||
{
|
||||
_renderCallback(_renderCallbackOwner);
|
||||
}
|
||||
}
|
||||
|
||||
void setFrameIntervalInMilliseconds(float frameIntervalInMilliseconds)
|
||||
{
|
||||
_frameIntervalInMicroseconds = static_cast<int>(1000.0f * frameIntervalInMilliseconds);
|
||||
}
|
||||
|
||||
template <class Rt>
|
||||
auto add_task(std::packaged_task<Rt()> &pt) -> std::future<Rt>
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_taskMutex);
|
||||
auto ret = pt.get_future();
|
||||
_tasks.push_back([pt = std::make_shared<std::packaged_task<Rt()>>(
|
||||
std::move(pt))]
|
||||
{ (*pt)(); });
|
||||
_cv.notify_one();
|
||||
return ret;
|
||||
}
|
||||
|
||||
TViewer *viewer = std::nullptr_t();
|
||||
|
||||
private:
|
||||
void (*_requestFrameRenderCallback)() = nullptr;
|
||||
bool _stop = false;
|
||||
int _frameIntervalInMicroseconds = 1000000 / 60;
|
||||
std::mutex _mutex;
|
||||
std::mutex _taskMutex;
|
||||
std::condition_variable _cv;
|
||||
void (*_renderCallback)(void *const) = nullptr;
|
||||
void *_renderCallbackOwner = nullptr;
|
||||
std::deque<std::function<void()>> _tasks;
|
||||
std::chrono::high_resolution_clock::time_point _lastFrameTime;
|
||||
int _frameCount = 0;
|
||||
float _accumulatedTime = 0.0f;
|
||||
float _fps = 0.0f;
|
||||
std::thread *t = nullptr;
|
||||
};
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
@@ -205,145 +46,21 @@ extern "C"
|
||||
TRACE("RenderLoop_destroy");
|
||||
if (_rl)
|
||||
{
|
||||
_rl->destroyViewer();
|
||||
_rl = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_createOnRenderThread(
|
||||
void *const context, void *const platform, const char *uberArchivePath,
|
||||
const void *const loader,
|
||||
void (*renderCallback)(void *const renderCallbackOwner),
|
||||
void *const renderCallbackOwner,
|
||||
void (*callback)(TViewer *))
|
||||
{
|
||||
TRACE("Viewer_createOnRenderThread");
|
||||
_rl->createViewer(
|
||||
context,
|
||||
platform,
|
||||
uberArchivePath,
|
||||
loader,
|
||||
renderCallback,
|
||||
renderCallbackOwner,
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_destroyOnRenderThread(TViewer *viewer)
|
||||
{
|
||||
TRACE("Viewer_destroyOnRenderThread");
|
||||
if (!_rl)
|
||||
{
|
||||
Log("Warning - cannot destroy viewer, no RenderLoop has been created");
|
||||
} else {
|
||||
_rl->destroyViewer();
|
||||
}
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_createViewRenderThread(TViewer *viewer, void (*onComplete)(TView *tView)) {
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void RenderTicker_renderRenderThread(TRenderTicker *tRenderTicker, , uint64_t frameTimeInNanos, void (*onComplete)()) {
|
||||
std::packaged_task<void()> lambda(
|
||||
[=]() mutable
|
||||
{
|
||||
auto *view = Viewer_createView(viewer);
|
||||
onComplete(view);
|
||||
RenderTicker_render(tRenderTicker, frameTimeInNanos);
|
||||
onComplete();
|
||||
});
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_createHeadlessSwapChainRenderThread(TViewer *viewer,
|
||||
uint32_t width,
|
||||
uint32_t height,
|
||||
void (*onComplete)(TSwapChain *))
|
||||
{
|
||||
std::packaged_task<void()> lambda(
|
||||
[=]() mutable
|
||||
{
|
||||
auto *swapChain = Viewer_createHeadlessSwapChain(viewer, width, height);
|
||||
onComplete(swapChain);
|
||||
});
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_createSwapChainRenderThread(TViewer *viewer,
|
||||
void *const surface,
|
||||
void (*onComplete)(TSwapChain *))
|
||||
{
|
||||
std::packaged_task<void()> lambda(
|
||||
[=]() mutable
|
||||
{
|
||||
auto *swapChain = Viewer_createSwapChain(viewer, surface);
|
||||
onComplete(swapChain);
|
||||
});
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_destroySwapChainRenderThread(TViewer *viewer, TSwapChain *swapChain, void (*onComplete)())
|
||||
{
|
||||
std::packaged_task<void()> lambda(
|
||||
[=]() mutable
|
||||
{
|
||||
Viewer_destroySwapChain(viewer, swapChain);
|
||||
onComplete();
|
||||
});
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_requestFrameRenderThread(TViewer *viewer, void (*onComplete)())
|
||||
{
|
||||
if (!_rl)
|
||||
{
|
||||
Log("No render loop!"); // PANIC?
|
||||
}
|
||||
else
|
||||
{
|
||||
_rl->requestFrame(onComplete);
|
||||
}
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_loadIblRenderThread(TViewer *viewer, const char *iblPath, float intensity, void (*onComplete)())
|
||||
{
|
||||
std::packaged_task<void()> lambda(
|
||||
[=]() mutable
|
||||
{
|
||||
Viewer_loadIbl(viewer, iblPath, intensity);
|
||||
onComplete();
|
||||
});
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_removeIblRenderThread(TViewer *viewer, void (*onComplete)())
|
||||
{
|
||||
std::packaged_task<void()> lambda(
|
||||
[=]() mutable
|
||||
{
|
||||
Viewer_removeIbl(viewer);
|
||||
onComplete();
|
||||
});
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_createRenderTargetRenderThread(TViewer *viewer, intptr_t colorTexture, intptr_t depthTexture, uint32_t width, uint32_t height, void (*onComplete)(TRenderTarget *))
|
||||
{
|
||||
std::packaged_task<void()> lambda(
|
||||
[=]() mutable
|
||||
{
|
||||
auto renderTarget = Viewer_createRenderTarget(viewer, colorTexture, depthTexture, width, height);
|
||||
onComplete(renderTarget);
|
||||
});
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_destroyRenderTargetRenderThread(TViewer *tViewer, TRenderTarget *tRenderTarget, void (*onComplete)())
|
||||
{
|
||||
std::packaged_task<void()> lambda(
|
||||
[=]() mutable
|
||||
{
|
||||
Viewer_destroyRenderTarget(tViewer, tRenderTarget);
|
||||
onComplete();
|
||||
});
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Engine_createRenderThread(TBackend backend, void (*onComplete)(TEngine *)) {
|
||||
std::packaged_task<void()> lambda(
|
||||
[=]() mutable
|
||||
@@ -383,6 +100,16 @@ extern "C"
|
||||
});
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Engine_destroySwapChainRenderThread(TEngine *tEngine, TSwapChain *tSwapChain, void (*onComplete)()) {
|
||||
std::packaged_task<void()> lambda(
|
||||
[=]() mutable
|
||||
{
|
||||
Engine_destroySwapChain(tEngine);
|
||||
onComplete();
|
||||
});
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Engine_createCameraRenderThread(TEngine* tEngine, void (*onComplete)(TCamera *)) {
|
||||
std::packaged_task<void()> lambda(
|
||||
@@ -446,6 +173,7 @@ extern "C"
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Engine_destroyMaterialRenderThread(TEngine *tEngine, TMaterial *tMaterial, void (*onComplete)())
|
||||
{
|
||||
std::packaged_task<void()> lambda(
|
||||
@@ -592,15 +320,6 @@ extern "C"
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void
|
||||
set_frame_interval_render_thread(TViewer *viewer, float frameIntervalInMilliseconds)
|
||||
{
|
||||
_rl->setFrameIntervalInMilliseconds(frameIntervalInMilliseconds);
|
||||
std::packaged_task<void()> lambda([=]() mutable
|
||||
{ ((FilamentViewer *)viewer)->setFrameInterval(frameIntervalInMilliseconds); });
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_renderRenderThread(TViewer *viewer, TView *tView, TSwapChain *tSwapChain)
|
||||
{
|
||||
std::packaged_task<void()> lambda([=]() mutable
|
||||
@@ -608,30 +327,6 @@ extern "C"
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_captureRenderThread(TViewer *viewer, TView *view, TSwapChain *tSwapChain, uint8_t *pixelBuffer, bool useFence, void (*onComplete)())
|
||||
{
|
||||
std::packaged_task<void()> lambda([=]() mutable
|
||||
{ Viewer_capture(viewer, view, tSwapChain, pixelBuffer, useFence, onComplete); });
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Viewer_captureRenderTargetRenderThread(TViewer *viewer, TView *view, TSwapChain *tSwapChain, TRenderTarget *tRenderTarget, uint8_t *pixelBuffer, bool useFence, void (*onComplete)())
|
||||
{
|
||||
std::packaged_task<void()> lambda([=]() mutable
|
||||
{ Viewer_captureRenderTarget(viewer, view, tSwapChain, tRenderTarget, pixelBuffer, useFence, onComplete); });
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void
|
||||
set_background_color_render_thread(TViewer *viewer, const float r, const float g,
|
||||
const float b, const float a)
|
||||
{
|
||||
std::packaged_task<void()> lambda(
|
||||
[=]() mutable
|
||||
{ set_background_color(viewer, r, g, b, a); });
|
||||
auto fut = _rl->add_task(lambda);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void SceneManager_createGridRenderThread(TSceneManager *tSceneManager, TMaterial *tMaterial, void (*callback)(TSceneAsset *))
|
||||
{
|
||||
std::packaged_task<void()> lambda([=]() mutable
|
||||
|
||||
Reference in New Issue
Block a user