EngineObject.cpp 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. #include "EngineObject.h"
  2. #include "ScriptEngine.h"
  3. #include "WallpaperEngine/Logging/Log.h"
  4. #include "WallpaperEngine/Audio/Drivers/AudioDriver.h"
  5. #include "WallpaperEngine/Audio/Drivers/Recorders/PlaybackRecorder.h"
  6. #include "WallpaperEngine/Data/Model/Property.h"
  7. #include "WallpaperEngine/Desktop/UserShortcut.h"
  8. #include "WallpaperEngine/Render/Wallpapers/CScene.h"
  9. #include "WallpaperEngine/Scripting/Adapters/ScriptableObjectAdapter.h"
  10. #include <chrono>
  11. #include <map>
  12. #include <ranges>
  13. using namespace WallpaperEngine::Scripting;
  14. extern float g_Time;
  15. extern float g_TimeLast;
  16. extern float g_Daytime;
  17. static uint32_t EngineInstanceId = 0;
  18. std::map<uint32_t, EngineObject&> engineInstances;
  19. // read-only properties, writes are ignored instead of aborting the calling script
  20. JSValue engine_set_value (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) { return JS_UNDEFINED; }
  21. // rebuilt on every read so scripts always see the current values
  22. JSValue engine_get_user_properties (
  23. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic
  24. ) {
  25. JSValue result = JS_NewObject (ctx);
  26. const auto it = engineInstances.find (magic);
  27. if (it == engineInstances.end ()) {
  28. return result;
  29. }
  30. auto& engine = it->second.getEngine ();
  31. for (const auto& [name, property] : it->second.getScene ().getUserProperties ()) {
  32. JS_SetPropertyStr (ctx, result, name.c_str (), engine.userPropertyToJs (*property));
  33. }
  34. return result;
  35. }
  36. // the name is the usershortcut user property's; only a shortcut the user assigned is ever opened
  37. JSValue engine_open_user_shortcut (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
  38. const auto it = engineInstances.find (magic);
  39. if (argc < 1 || !JS_IsString (argv[0]) || it == engineInstances.end ()) {
  40. return JS_UNDEFINED;
  41. }
  42. const char* name = JS_ToCString (ctx, argv[0]);
  43. const std::string propertyName = name != nullptr ? name : "";
  44. JS_FreeCString (ctx, name);
  45. const auto& properties = it->second.getScene ().getUserProperties ();
  46. const auto property = properties.find (propertyName);
  47. if (property == properties.end () || !property->second->is<WallpaperEngine::Data::Model::PropertyUserShortcut> ()) {
  48. sLog.error ("openUserShortcut: no user shortcut property named ", propertyName);
  49. return JS_UNDEFINED;
  50. }
  51. const auto shortcut = WallpaperEngine::Desktop::UserShortcut::parse (property->second->getString ());
  52. if (!shortcut.has_value ()) {
  53. return JS_UNDEFINED;
  54. }
  55. // a script calling this from update() instead of a click must not start the app every frame
  56. static std::map<std::string, std::chrono::steady_clock::time_point> lastLaunch;
  57. const auto now = std::chrono::steady_clock::now ();
  58. if (const auto last = lastLaunch.find (propertyName);
  59. last != lastLaunch.end () && now - last->second < std::chrono::seconds (1)) {
  60. return JS_UNDEFINED;
  61. }
  62. lastLaunch[propertyName] = now;
  63. sLog.out ("Opening user shortcut ", propertyName, ": ", shortcut->target);
  64. shortcut->launch ();
  65. return JS_UNDEFINED;
  66. }
  67. // scripts only ever hand the result back to layer properties (layer.font = ...), so the path itself is the handle
  68. JSValue engine_register_asset (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
  69. if (argc < 1 || !JS_IsString (argv[0])) {
  70. return JS_UNDEFINED;
  71. }
  72. return JS_DupValue (ctx, argv[0]);
  73. }
  74. // engine.isRunningInEditor() and friends: fixed answers, this is always a plain desktop wallpaper
  75. JSValue engine_query_flag (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
  76. return JS_NewBool (ctx, magic != 0);
  77. }
  78. // the scene's own coordinate space (project width/height), not the monitor resolution
  79. JSValue engine_get_canvas_size (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
  80. const auto it = engineInstances.find (magic);
  81. if (it == engineInstances.end ()) {
  82. return JS_UNDEFINED;
  83. }
  84. const auto& camera = it->second.getScene ().getCamera ();
  85. const DynamicValue size (glm::vec2 (camera.getWidth (), camera.getHeight ()));
  86. return it->second.getEngine ().getAdapters ().vec2->instantiate (const_cast<DynamicValue&> (size), true);
  87. }
  88. glm::vec2 engine_screen_size (EngineObject& engine) {
  89. const auto& screen = engine.getScene ().getScreenSize ();
  90. if (screen.x > 0 && screen.y > 0) {
  91. return glm::vec2 (screen);
  92. }
  93. const auto& camera = engine.getScene ().getCamera ();
  94. return { camera.getWidth (), camera.getHeight () };
  95. }
  96. JSValue engine_get_screen_resolution (
  97. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic
  98. ) {
  99. const auto it = engineInstances.find (magic);
  100. if (it == engineInstances.end ()) {
  101. return JS_UNDEFINED;
  102. }
  103. const DynamicValue size (engine_screen_size (it->second));
  104. return it->second.getEngine ().getAdapters ().vec2->instantiate (const_cast<DynamicValue&> (size), true);
  105. }
  106. // magic packs the instance id with the question: bit 0 set asks for landscape instead of portrait
  107. JSValue engine_query_orientation (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
  108. const auto it = engineInstances.find (magic >> 1);
  109. if (it == engineInstances.end ()) {
  110. return JS_FALSE;
  111. }
  112. const auto size = engine_screen_size (it->second);
  113. return JS_NewBool (ctx, (magic & 1) ? size.x >= size.y : size.y > size.x);
  114. }
  115. // WE's version of this is the callback behind the stop functions setTimeout/setInterval return;
  116. // called straight off engine it has no timer bound to it and never stops anything
  117. JSValue engine_clear_timeout (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
  118. return JS_FALSE;
  119. }
  120. // layers are never destroyed from scripts here, so any layer handle is still valid
  121. JSValue engine_is_object_valid (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
  122. if (argc < 1) {
  123. return JS_FALSE;
  124. }
  125. return JS_NewBool (ctx, WallpaperEngine::Scripting::Adapters::ScriptableObjectAdapter::getObject (argv[0]) != nullptr);
  126. }
  127. JSValue engine_get_frametime (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
  128. return JS_NewFloat64 (ctx, g_Time - g_TimeLast);
  129. }
  130. JSValue engine_get_runtime (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
  131. return JS_NewFloat64 (ctx, g_Time);
  132. }
  133. JSValue engine_get_daytime (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
  134. return JS_NewFloat64 (ctx, g_Daytime);
  135. }
  136. JSValue engine_stop_interval (
  137. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  138. ) {
  139. if (argc != 1) {
  140. return JS_ThrowTypeError (ctx, "engine_stop_interval: wrong number of arguments");
  141. }
  142. const auto it = engineInstances.find (magic);
  143. if (it == engineInstances.end ()) {
  144. return JS_ThrowTypeError (ctx, "engine_stop_interval: engine instance is gone");
  145. }
  146. int id = 0;
  147. JS_ToInt32 (ctx, &id, argv[0]);
  148. it->second.clearInterval (id);
  149. return JS_UNDEFINED;
  150. }
  151. JSValue engine_stop_timeout (
  152. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  153. ) {
  154. if (argc != 1) {
  155. return JS_ThrowTypeError (ctx, "engine_stop_timeout: wrong number of arguments");
  156. }
  157. const auto it = engineInstances.find (magic);
  158. if (it == engineInstances.end ()) {
  159. return JS_ThrowTypeError (ctx, "engine_stop_timeout: engine instance is gone");
  160. }
  161. int id = 0;
  162. JS_ToInt32 (ctx, &id, argv[0]);
  163. it->second.clearTimeout (id);
  164. return JS_UNDEFINED;
  165. }
  166. JSValue engine_set_interval (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
  167. if (argc < 1) {
  168. return JS_ThrowTypeError (ctx, "engine_set_interval: wrong number of arguments");
  169. }
  170. int delay = 0;
  171. if (argc > 1) {
  172. JS_ToInt32 (ctx, &delay, argv[1]);
  173. }
  174. JSValue function = argv[0];
  175. if (!JS_IsFunction (ctx, function)) {
  176. return JS_ThrowTypeError (ctx, "engine_set_interval: expected a function");
  177. }
  178. const auto it = engineInstances.find (magic);
  179. if (it == engineInstances.end ()) {
  180. return JS_ThrowTypeError (ctx, "engine_set_interval: engine instance is gone");
  181. }
  182. int id = it->second.reserveNextIntervalId (function, delay);
  183. JSValue args[] = { JS_NewInt32 (ctx, id) };
  184. return JS_NewCFunctionData (ctx, engine_stop_interval, 2, magic, 1, args);
  185. }
  186. // Backs the "left"/"right"/"average" getters on the object returned by registerAudioBuffers(). WE hands out
  187. // Float32Array views into its [left | right | average] buffer, these copy the same part on every read.
  188. JSValue audio_buffer_get_values (
  189. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  190. ) {
  191. int engineInstanceId = 0;
  192. int resolution = 32;
  193. int section = 0;
  194. JS_ToInt32 (ctx, &engineInstanceId, func_data[0]);
  195. JS_ToInt32 (ctx, &resolution, func_data[1]);
  196. JS_ToInt32 (ctx, &section, func_data[2]);
  197. JSValue result = JS_NewArray (ctx);
  198. const auto it = engineInstances.find (engineInstanceId);
  199. if (it == engineInstances.end ()) {
  200. sLog.error ("registerAudioBuffers: no EngineObject found for instance ", engineInstanceId, " - returning zeros");
  201. return result;
  202. }
  203. const auto& recorder = it->second.getScene ().getAudioContext ().getDriver ().getRecorder ();
  204. const float* data = recorder.audio32;
  205. if (resolution == 16) {
  206. data = recorder.audio16;
  207. } else if (resolution == 64) {
  208. data = recorder.audio64;
  209. }
  210. data += section * resolution;
  211. for (int i = 0; i < resolution; i++) {
  212. JS_SetPropertyUint32 (ctx, result, i, JS_NewFloat64 (ctx, data[i]));
  213. }
  214. return result;
  215. }
  216. // engine.registerAudioBuffers(resolution): resolution must be 16, 32 or 64 (falls back to 32
  217. // otherwise), matching the AUDIO_RESOLUTION_* constants below. Returns an object whose
  218. // left/right/average properties are re-read from the live spectrum every access, so scripts
  219. // that poll them from an update() callback see current values each frame.
  220. JSValue engine_register_audio_buffers (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
  221. int resolution = 32;
  222. if (argc > 0) {
  223. JS_ToInt32 (ctx, &resolution, argv[0]);
  224. }
  225. if (resolution != 16 && resolution != 32 && resolution != 64) {
  226. resolution = 32;
  227. }
  228. JSValue result = JS_NewObject (ctx);
  229. // same order as the sections of the recorder's buffers
  230. static constexpr const char* properties[] = { "left", "right", "average" };
  231. for (int section = 0; section < 3; section++) {
  232. JSValue closureData[] = { JS_NewInt32 (ctx, magic), JS_NewInt32 (ctx, resolution), JS_NewInt32 (ctx, section) };
  233. JS_DefinePropertyGetSet (
  234. ctx, result, JS_NewAtom (ctx, properties[section]),
  235. JS_NewCFunctionData (ctx, audio_buffer_get_values, 0, 0, 3, closureData),
  236. JS_NewCFunction (ctx, engine_set_value, "set", 1), JS_PROP_ENUMERABLE
  237. );
  238. }
  239. return result;
  240. }
  241. JSValue engine_set_timeout (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
  242. if (argc < 1) {
  243. return JS_ThrowTypeError (ctx, "engine_set_timeout: wrong number of arguments");
  244. }
  245. int delay = 0;
  246. if (argc > 1) {
  247. JS_ToInt32 (ctx, &delay, argv[1]);
  248. }
  249. JSValue function = argv[0];
  250. if (!JS_IsFunction (ctx, function)) {
  251. return JS_ThrowTypeError (ctx, "engine_set_timeout: expected a function");
  252. }
  253. const auto it = engineInstances.find (magic);
  254. if (it == engineInstances.end ()) {
  255. return JS_ThrowTypeError (ctx, "engine_set_timeout: engine instance is gone");
  256. }
  257. int id = it->second.reserveNextTimeoutId (function, delay);
  258. JSValue args[] = { JS_NewInt32 (ctx, id) };
  259. return JS_NewCFunctionData (ctx, engine_stop_timeout, 2, magic, 1, args);
  260. }
  261. EngineObject::EngineObject (ScriptEngine& engine, Render::Wallpapers::CScene& scene) :
  262. m_scene (scene), m_engine (engine), m_instanceId (++EngineInstanceId), m_classId (0) {
  263. // required so setInterval/setTimeout/registerAudioBuffers's magic-encoded instance id can find
  264. // their way back to this object from the free-standing JS callback functions above
  265. engineInstances.emplace (this->m_instanceId, *this);
  266. this->m_definition = { .class_name = "IEngine" };
  267. JS_NewClassID (this->m_engine.getRuntime (), &this->m_classId);
  268. JS_NewClass (this->m_engine.getRuntime (), this->m_classId, &this->m_definition);
  269. this->m_instance = JS_NewObjectClass (this->m_engine.getContext (), this->m_classId);
  270. JS_DupValue (this->m_engine.getContext (), this->m_instance);
  271. JS_SetOpaque (this->m_instance, this);
  272. JS_DefinePropertyGetSet (
  273. this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "frametime"),
  274. JS_NewCFunction (this->m_engine.getContext (), engine_get_frametime, "get", 0),
  275. JS_NewCFunction (this->m_engine.getContext (), engine_set_value, "set", 1), JS_PROP_ENUMERABLE
  276. );
  277. JS_DefinePropertyGetSet (
  278. this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "runtime"),
  279. JS_NewCFunction (this->m_engine.getContext (), engine_get_runtime, "get", 0),
  280. JS_NewCFunction (this->m_engine.getContext (), engine_set_value, "set", 1), JS_PROP_ENUMERABLE
  281. );
  282. JS_DefinePropertyGetSet (
  283. this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "timeOfDay"),
  284. JS_NewCFunction (this->m_engine.getContext (), engine_get_daytime, "get", 0),
  285. JS_NewCFunction (this->m_engine.getContext (), engine_set_value, "set", 1), JS_PROP_ENUMERABLE
  286. );
  287. JS_DefinePropertyGetSet (
  288. this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "canvasSize"),
  289. JS_NewCFunctionMagic (
  290. this->m_engine.getContext (), engine_get_canvas_size, "get", 0, JS_CFUNC_generic_magic, this->m_instanceId
  291. ),
  292. JS_NewCFunction (this->m_engine.getContext (), engine_set_value, "set", 1), JS_PROP_ENUMERABLE
  293. );
  294. JS_DefinePropertyGetSet (
  295. this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "userProperties"),
  296. JS_NewCFunctionMagic (
  297. this->m_engine.getContext (), engine_get_user_properties, "get", 0, JS_CFUNC_generic_magic,
  298. this->m_instanceId
  299. ),
  300. JS_NewCFunction (this->m_engine.getContext (), engine_set_value, "set", 1), JS_PROP_ENUMERABLE
  301. );
  302. JS_DefinePropertyValueStr (
  303. this->m_engine.getContext (), this->m_instance, "AUDIO_RESOLUTION_16",
  304. JS_NewInt32 (this->m_engine.getContext (), 16), JS_PROP_ENUMERABLE
  305. );
  306. JS_DefinePropertyValueStr (
  307. this->m_engine.getContext (), this->m_instance, "AUDIO_RESOLUTION_32",
  308. JS_NewInt32 (this->m_engine.getContext (), 32), JS_PROP_ENUMERABLE
  309. );
  310. JS_DefinePropertyValueStr (
  311. this->m_engine.getContext (), this->m_instance, "AUDIO_RESOLUTION_64",
  312. JS_NewInt32 (this->m_engine.getContext (), 64), JS_PROP_ENUMERABLE
  313. );
  314. JS_DefinePropertyValueStr (
  315. this->m_engine.getContext (), this->m_instance, "setInterval",
  316. JS_NewCFunctionMagic (
  317. this->m_engine.getContext (), engine_set_interval, "setInterval", 2, JS_CFUNC_generic_magic,
  318. this->m_instanceId
  319. ),
  320. JS_PROP_ENUMERABLE
  321. );
  322. JS_DefinePropertyValueStr (
  323. this->m_engine.getContext (), this->m_instance, "setTimeout",
  324. JS_NewCFunctionMagic (
  325. this->m_engine.getContext (), engine_set_timeout, "setTimeout", 2, JS_CFUNC_generic_magic,
  326. this->m_instanceId
  327. ),
  328. JS_PROP_ENUMERABLE
  329. );
  330. JS_DefinePropertyValueStr (
  331. this->m_engine.getContext (), this->m_instance, "registerAsset",
  332. JS_NewCFunction (this->m_engine.getContext (), engine_register_asset, "registerAsset", 1), JS_PROP_ENUMERABLE
  333. );
  334. JS_DefinePropertyValueStr (
  335. this->m_engine.getContext (), this->m_instance, "openUserShortcut",
  336. JS_NewCFunctionMagic (
  337. this->m_engine.getContext (), engine_open_user_shortcut, "openUserShortcut", 1, JS_CFUNC_generic_magic,
  338. this->m_instanceId
  339. ),
  340. JS_PROP_ENUMERABLE
  341. );
  342. JS_DefinePropertyValueStr (
  343. this->m_engine.getContext (), this->m_instance, "registerAudioBuffers",
  344. JS_NewCFunctionMagic (
  345. this->m_engine.getContext (), engine_register_audio_buffers, "registerAudioBuffers", 1,
  346. JS_CFUNC_generic_magic, this->m_instanceId
  347. ),
  348. JS_PROP_ENUMERABLE
  349. );
  350. const struct {
  351. const char* name;
  352. int answer;
  353. } flags[] = {
  354. { "isRunningInEditor", 0 }, { "isDesktopDevice", 1 }, { "isMobileDevice", 0 },
  355. { "isWallpaper", 1 }, { "isScreensaver", 0 },
  356. };
  357. for (const auto& flag : flags) {
  358. JS_DefinePropertyValueStr (
  359. this->m_engine.getContext (), this->m_instance, flag.name,
  360. JS_NewCFunctionMagic (
  361. this->m_engine.getContext (), engine_query_flag, flag.name, 0, JS_CFUNC_generic_magic, flag.answer
  362. ),
  363. JS_PROP_ENUMERABLE
  364. );
  365. }
  366. JS_DefinePropertyGetSet (
  367. this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "screenResolution"),
  368. JS_NewCFunctionMagic (
  369. this->m_engine.getContext (), engine_get_screen_resolution, "get", 0, JS_CFUNC_generic_magic,
  370. this->m_instanceId
  371. ),
  372. JS_NewCFunction (this->m_engine.getContext (), engine_set_value, "set", 1), JS_PROP_ENUMERABLE
  373. );
  374. JS_DefinePropertyValueStr (
  375. this->m_engine.getContext (), this->m_instance, "isPortrait",
  376. JS_NewCFunctionMagic (
  377. this->m_engine.getContext (), engine_query_orientation, "isPortrait", 0, JS_CFUNC_generic_magic,
  378. static_cast<int> (this->m_instanceId << 1)
  379. ),
  380. JS_PROP_ENUMERABLE
  381. );
  382. JS_DefinePropertyValueStr (
  383. this->m_engine.getContext (), this->m_instance, "isLandscape",
  384. JS_NewCFunctionMagic (
  385. this->m_engine.getContext (), engine_query_orientation, "isLandscape", 0, JS_CFUNC_generic_magic,
  386. static_cast<int> ((this->m_instanceId << 1) | 1)
  387. ),
  388. JS_PROP_ENUMERABLE
  389. );
  390. JS_DefinePropertyValueStr (
  391. this->m_engine.getContext (), this->m_instance, "clearTimeout",
  392. JS_NewCFunction (this->m_engine.getContext (), engine_clear_timeout, "clearTimeout", 1), JS_PROP_ENUMERABLE
  393. );
  394. JS_DefinePropertyValueStr (
  395. this->m_engine.getContext (), this->m_instance, "isObjectValid",
  396. JS_NewCFunction (this->m_engine.getContext (), engine_is_object_valid, "isObjectValid", 1),
  397. JS_PROP_ENUMERABLE
  398. );
  399. }
  400. EngineObject::~EngineObject () {
  401. for (const auto& [id, timeout] : this->m_timeouts) {
  402. JS_FreeValue (this->m_engine.getContext (), timeout.callback);
  403. }
  404. for (const auto& [id, interval] : this->m_intervals) {
  405. JS_FreeValue (this->m_engine.getContext (), interval.callback);
  406. }
  407. engineInstances.erase (this->m_instanceId);
  408. this->m_intervals.clear ();
  409. this->m_timeouts.clear ();
  410. JS_FreeValue (this->m_engine.getContext (), this->m_instance);
  411. }
  412. uint32_t EngineObject::reserveNextTimeoutId (JSValue function, uint64_t duration) {
  413. const auto id = ++this->m_nextTimeoutId;
  414. this->m_timeouts[id] = Timeout { .callback = JS_DupValue (this->m_engine.getContext (), function),
  415. .duration = std::chrono::milliseconds (duration),
  416. .next = std::chrono::steady_clock::now () + std::chrono::milliseconds (duration) };
  417. return id;
  418. }
  419. uint32_t EngineObject::reserveNextIntervalId (JSValue function, uint64_t duration) {
  420. const auto id = ++this->m_nextIntervalId;
  421. this->m_intervals[id]
  422. = Timeout { .callback = JS_DupValue (this->m_engine.getContext (), function),
  423. .duration = std::chrono::milliseconds (duration),
  424. .next = std::chrono::steady_clock::now () + std::chrono::milliseconds (duration) };
  425. return id;
  426. }
  427. void EngineObject::clearInterval (uint32_t id) {
  428. const auto it = this->m_intervals.find (id);
  429. if (it == this->m_intervals.end ()) {
  430. return;
  431. }
  432. JS_FreeValue (this->getEngine ().getContext (), it->second.callback);
  433. this->m_intervals.erase (id);
  434. }
  435. void EngineObject::clearTimeout (uint32_t id) {
  436. const auto it = this->m_timeouts.find (id);
  437. if (it == this->m_timeouts.end ()) {
  438. return;
  439. }
  440. JS_FreeValue (this->getEngine ().getContext (), it->second.callback);
  441. this->m_timeouts.erase (id);
  442. }
  443. static void callTimerCallback (JSContext* ctx, JSValueConst callback, const char* kind) {
  444. JSValue result = JS_Call (ctx, callback, JS_NULL, 0, nullptr);
  445. if (JS_IsException (result)) {
  446. logJSException (ctx, kind);
  447. }
  448. JS_FreeValue (ctx, result);
  449. }
  450. void EngineObject::tick () {
  451. const auto now = std::chrono::steady_clock::now ();
  452. auto* ctx = this->m_engine.getContext ();
  453. // only ids are collected up front, callbacks may clearInterval()/setTimeout() from inside themselves
  454. std::vector<uint32_t> dueIntervals;
  455. for (const auto& [id, interval] : this->m_intervals) {
  456. if (interval.next <= now) {
  457. dueIntervals.push_back (id);
  458. }
  459. }
  460. for (const auto id : dueIntervals) {
  461. const auto it = this->m_intervals.find (id);
  462. if (it == this->m_intervals.end ()) {
  463. continue;
  464. }
  465. it->second.next = now + it->second.duration;
  466. JSValue callback = JS_DupValue (ctx, it->second.callback);
  467. callTimerCallback (ctx, callback, "setInterval");
  468. JS_FreeValue (ctx, callback);
  469. }
  470. std::vector<uint32_t> dueTimeouts;
  471. for (const auto& [id, timeout] : this->m_timeouts) {
  472. if (timeout.next <= now) {
  473. dueTimeouts.push_back (id);
  474. }
  475. }
  476. for (const auto id : dueTimeouts) {
  477. const auto it = this->m_timeouts.find (id);
  478. if (it == this->m_timeouts.end ()) {
  479. continue;
  480. }
  481. JSValue callback = it->second.callback;
  482. this->m_timeouts.erase (it);
  483. callTimerCallback (ctx, callback, "setTimeout");
  484. JS_FreeValue (ctx, callback);
  485. }
  486. }