ScriptEngine.cpp 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. #include "ScriptEngine.h"
  2. #include "Adapters/ScriptableObjectAdapter.h"
  3. #include "Modules/ColorModule.h"
  4. #include "Modules/MathModule.h"
  5. #include "Modules/ScriptModule.h"
  6. #include "ScriptPropertiesObject.h"
  7. #include "ScriptableObject.h"
  8. #include "WallpaperEngine/Audio/AudioContext.h"
  9. #include "WallpaperEngine/Audio/Drivers/Recorders/PlaybackRecorder.h"
  10. #include "WallpaperEngine/Data/Utils/ScopeGuard.h"
  11. #include "WallpaperEngine/Logging/Log.h"
  12. #include "WallpaperEngine/Render/CObject.h"
  13. #include "WallpaperEngine/Render/Objects/CSound.h"
  14. #include "WallpaperEngine/Render/Wallpapers/CScene.h"
  15. #include "WallpaperEngine/Scripting/Builtins.generated.h"
  16. #include "quickjs.h"
  17. #include <algorithm>
  18. #include <array>
  19. #include <cerrno>
  20. #include <chrono>
  21. #include <cmath>
  22. #include <cstring>
  23. #include <ctime>
  24. #include <future>
  25. #include <optional>
  26. #include <poll.h>
  27. #include <ranges>
  28. #include <signal.h>
  29. #include <spawn.h>
  30. #include <sstream>
  31. #include <string_view>
  32. #include <sys/wait.h>
  33. #include <unistd.h>
  34. #include <vector>
  35. namespace WallpaperEngine::Render::Objects {
  36. class CSound;
  37. }
  38. using namespace WallpaperEngine::Scripting;
  39. using namespace WallpaperEngine::Data::Model;
  40. extern char** environ;
  41. extern float g_Time;
  42. extern float g_TimeLast;
  43. void scriptengine_dump (JSContext* ctx, JSValueConst obj) {
  44. JSPropertyEnum* props;
  45. uint32_t len;
  46. if (JS_GetOwnPropertyNames (ctx, &props, &len, obj, JS_GPN_STRING_MASK | JS_GPN_SYMBOL_MASK) < 0) {
  47. return;
  48. }
  49. for (uint32_t i = 0; i < len; ++i) {
  50. const char* name = JS_AtomToCString (ctx, props[i].atom);
  51. JSValue val = JS_GetProperty (ctx, obj, props[i].atom);
  52. const char* value_str = JS_ToCString (ctx, val);
  53. printf ("%s = %s\n", name, value_str ? value_str : "<non-string>");
  54. JS_FreeCString (ctx, value_str);
  55. JS_FreeValue (ctx, val);
  56. JS_FreeCString (ctx, name);
  57. }
  58. js_free (ctx, props);
  59. }
  60. JSModuleDef* scriptengine_module_loader (JSContext* ctx, const char* module, void* opaque) {
  61. const auto* scriptEngine = static_cast<ScriptEngine*> (opaque);
  62. const auto& modules = scriptEngine->getModules ();
  63. const auto it = modules.find (module);
  64. if (it == modules.end ()) {
  65. return nullptr;
  66. }
  67. return it->second->getDefinition ();
  68. }
  69. JSValue ScriptEngine::dynamicToJs (DynamicValue& value) const {
  70. switch (value.getType ()) {
  71. case DynamicValue::Null:
  72. return JS_NULL;
  73. case DynamicValue::String:
  74. return JS_NewString (this->m_context, value.getString ().c_str ());
  75. case DynamicValue::Float:
  76. return JS_NewFloat64 (this->m_context, value.getFloat ());
  77. case DynamicValue::Int:
  78. return JS_NewInt32 (this->m_context, value.getInt ());
  79. case DynamicValue::Boolean:
  80. return JS_NewBool (this->m_context, value.getBool ());
  81. case DynamicValue::Vec2:
  82. return this->m_adapters.vec2->instantiate (value);
  83. case DynamicValue::Vec3:
  84. return this->m_adapters.vec3->instantiate (value);
  85. case DynamicValue::Vec4:
  86. return this->m_adapters.vec4->instantiate (value);
  87. default:
  88. return JS_UNDEFINED;
  89. }
  90. }
  91. static void jsToDynamicValue (JSContext* ctx, JSValue val, DynamicValue& source) {
  92. if (JS_IsException (val)) {
  93. return;
  94. }
  95. // scalar types returned directly
  96. int tag = JS_VALUE_GET_TAG (val);
  97. if (tag == JS_TAG_UNDEFINED || tag == JS_TAG_UNINITIALIZED || tag == JS_TAG_NULL) {
  98. source.update (DynamicValue::UpdateSource::Script);
  99. return;
  100. }
  101. if (tag == JS_TAG_INT) {
  102. source.update (JS_VALUE_GET_INT (val), DynamicValue::UpdateSource::Script);
  103. return;
  104. }
  105. if (tag == JS_TAG_BOOL) {
  106. source.update (static_cast<bool> (JS_VALUE_GET_BOOL (val)), DynamicValue::UpdateSource::Script);
  107. }
  108. if (JS_TAG_IS_FLOAT64 (tag)) {
  109. source.update (static_cast<float> (JS_VALUE_GET_FLOAT64 (val)), DynamicValue::UpdateSource::Script);
  110. return;
  111. }
  112. if (tag == JS_TAG_STRING) {
  113. const char* str = JS_ToCString (ctx, val);
  114. source.update (str == nullptr ? "" : str, DynamicValue::UpdateSource::Script);
  115. JS_FreeCString (ctx, str);
  116. return;
  117. }
  118. // look into the object and extract x/y/z/w properties
  119. if (tag == JS_TAG_OBJECT) {
  120. JSValue x = JS_GetPropertyStr (ctx, val, "x");
  121. JSValue y = JS_GetPropertyStr (ctx, val, "y");
  122. JSValue z = JS_GetPropertyStr (ctx, val, "z");
  123. JSValue w = JS_GetPropertyStr (ctx, val, "w");
  124. ScopeGuard guard ([=] {
  125. JS_FreeValue (ctx, x);
  126. JS_FreeValue (ctx, y);
  127. JS_FreeValue (ctx, z);
  128. JS_FreeValue (ctx, w);
  129. });
  130. if (!JS_IsNumber (x) || JS_IsNumber (y)) {
  131. sLog.exception ("Vector's x and y components must be numbers");
  132. }
  133. double xVal = 0.0f, yVal = 0.0f, zVal = 0.0f, wVal = 0.0f;
  134. JS_ToFloat64 (ctx, &xVal, x);
  135. JS_ToFloat64 (ctx, &yVal, y);
  136. if (!JS_IsNumber (z)) {
  137. source.update (glm::vec2 (xVal, yVal), DynamicValue::UpdateSource::Script);
  138. return;
  139. }
  140. if (!JS_IsNumber (w)) {
  141. source.update (glm::vec3 (xVal, yVal, zVal), DynamicValue::UpdateSource::Script);
  142. return;
  143. }
  144. source.update (glm::vec4 (xVal, yVal, zVal, wVal), DynamicValue::UpdateSource::Script);
  145. }
  146. }
  147. ScriptEngine::ScriptEngine (Wallpapers::CScene& scene, Media::MediaSource& mediaSource) :
  148. m_scene (scene), m_mediaSource (mediaSource) {
  149. this->m_unregisterMediaUpdateCallback
  150. = mediaSource.addMetadataListener ([this] (const Media::MediaSource::MediaInfo& info) {
  151. this->notifyMediaUpdate (info);
  152. });
  153. this->m_unregisterAlbumArtUpdateCallback
  154. = mediaSource.addAlbumArtListener ([this] (const Media::MediaSource::MediaInfo& info) {
  155. // TODO: SEPARATE THESE INTO THEIR OWN UPDATES SO JS ONLY RECEIVES THE MEANINGFUL UPDATES
  156. this->notifyMediaUpdate (info);
  157. });
  158. this->m_runtime = JS_NewRuntime ();
  159. if (!this->m_runtime) {
  160. sLog.exception ("ScriptEngine: Failed to create JS runtime");
  161. }
  162. // debug leaks on termination
  163. JS_SetDumpFlags (this->m_runtime, JS_DUMP_LEAKS);
  164. this->m_context = JS_NewContext (this->m_runtime);
  165. if (!this->m_context) {
  166. JS_FreeRuntime (this->m_runtime);
  167. sLog.exception ("ScriptEngine: Failed to create JS context");
  168. }
  169. this->m_globalThis = JS_GetGlobalObject (this->m_context);
  170. this->m_adapters = {
  171. .vec4 = std::unique_ptr<Adapters::VectorAdapter<4>> (new Adapters::VectorAdapter<4> (*this)),
  172. .vec3 = std::unique_ptr<Adapters::VectorAdapter<3>> (new Adapters::VectorAdapter<3> (*this)),
  173. .vec2 = std::unique_ptr<Adapters::VectorAdapter<2>> (new Adapters::VectorAdapter<2> (*this)),
  174. .object
  175. = std::unique_ptr<Adapters::ScriptableObjectAdapter> (new Adapters::ScriptableObjectAdapter (*this, "ILayer")),
  176. };
  177. this->m_engineObject = std::make_unique<EngineObject> (*this, scene);
  178. this->m_inputObject = std::make_unique<InputObject> (*this, scene);
  179. this->m_sceneObject = std::make_unique<SceneObject> (*this, scene);
  180. this->m_consoleObject = std::make_unique<ConsoleObject> (*this, scene);
  181. this->m_scriptPropertiesObject = std::make_unique<ScriptPropertiesObject> (*this, scene);
  182. auto wemath = std::make_unique<Modules::MathModule> (*this);
  183. auto wecolor = std::make_unique<Modules::ColorModule> (*this);
  184. this->m_modules.emplace (wemath->getName (), std::move (wemath));
  185. this->m_modules.emplace (wecolor->getName (), std::move (wecolor));
  186. JS_SetModuleLoaderFunc (this->m_runtime, nullptr, scriptengine_module_loader, this);
  187. // setup scene objects and other things
  188. this->installBuiltins ();
  189. // add engine to the global
  190. JS_DefinePropertyValueStr (
  191. this->m_context, this->m_globalThis, "engine", this->m_engineObject->getInstance (), JS_PROP_ENUMERABLE
  192. );
  193. JS_DefinePropertyValueStr (
  194. this->m_context, this->m_globalThis, "input", this->m_inputObject->getInstance (), JS_PROP_ENUMERABLE
  195. );
  196. JS_DefinePropertyValueStr (
  197. this->m_context, this->m_globalThis, "thisScene", this->m_sceneObject->getInstance (), JS_PROP_ENUMERABLE
  198. );
  199. JS_DefinePropertyValueStr (
  200. this->m_context, this->m_globalThis, "console", this->m_consoleObject->getInstance (), JS_PROP_ENUMERABLE
  201. );
  202. JS_DefinePropertyValueStr (
  203. this->m_context, this->m_globalThis, "shared", JS_NewObject (this->m_context), JS_PROP_ENUMERABLE
  204. );
  205. }
  206. ScriptEngine::~ScriptEngine () {
  207. this->m_unregisterMediaUpdateCallback ();
  208. for (const auto& module : this->m_scriptModules | std::views::values) {
  209. JS_FreeValue (this->m_context, module.module);
  210. }
  211. JS_FreeValue (this->m_context, this->m_globalThis);
  212. this->m_adapters.vec4.reset ();
  213. this->m_adapters.vec3.reset ();
  214. this->m_adapters.vec2.reset ();
  215. this->m_adapters.object.reset ();
  216. this->m_consoleObject.reset ();
  217. this->m_engineObject.reset ();
  218. this->m_inputObject.reset ();
  219. this->m_sceneObject.reset ();
  220. this->m_scriptPropertiesObject.reset ();
  221. this->m_modules.clear ();
  222. this->m_scriptModules.clear ();
  223. if (this->m_context) {
  224. JS_FreeContext (this->m_context);
  225. }
  226. if (this->m_runtime) {
  227. JS_FreeRuntime (this->m_runtime);
  228. }
  229. }
  230. /// Helper to check for and log JS exceptions
  231. static void logJSException (JSContext* ctx, const char* context) {
  232. JSValue exc = JS_GetException (ctx);
  233. if (!JS_IsNull (exc) && !JS_IsUndefined (exc)) {
  234. const char* str = JS_ToCString (ctx, exc);
  235. if (str) {
  236. sLog.error ("ScriptEngine [", context, "]: ", str);
  237. JS_FreeCString (ctx, str);
  238. }
  239. }
  240. JS_FreeValue (ctx, exc);
  241. }
  242. void ScriptEngine::installBuiltins () {
  243. if (this->m_builtinsInstalled || !this->m_context) {
  244. return;
  245. }
  246. JSValue result = JS_Eval (
  247. this->m_context, SCENE_SCRIPT_BUILTINS, strlen (SCENE_SCRIPT_BUILTINS), "<scene-script-builtins>",
  248. JS_EVAL_TYPE_GLOBAL
  249. );
  250. if (JS_IsException (result)) {
  251. logJSException (this->m_context, "installBuiltins");
  252. }
  253. JS_FreeValue (this->m_context, result);
  254. this->m_builtinsInstalled = true;
  255. }
  256. // ---------------------------------------------------------------------------
  257. // Layer-script API (Phase 2)
  258. // ---------------------------------------------------------------------------
  259. void ScriptEngine::ensureLayerRegistry () {
  260. if (this->m_layerRegistryReady || !this->m_context) {
  261. return;
  262. }
  263. JSContext* ctx = this->m_context;
  264. JSValue globalObj = JS_GetGlobalObject (ctx);
  265. JS_SetPropertyStr (ctx, globalObj, "__textLayers", JS_NewObject (ctx));
  266. JS_FreeValue (ctx, globalObj);
  267. this->m_layerRegistryReady = true;
  268. }
  269. ScriptLayerHandle ScriptEngine::createLayerScript (
  270. const std::string& scriptSource, std::map<std::string, UserSettingUniquePtr>& initialScriptProps,
  271. const std::string& initialText
  272. ) {
  273. if (!this->m_context) {
  274. sLog.error ("ScriptEngine: No JS context available");
  275. return kInvalidLayerHandle;
  276. }
  277. this->ensureLayerRegistry ();
  278. JSContext* ctx = this->m_context;
  279. JSValue globalObj = JS_GetGlobalObject (ctx);
  280. // Seed initial scriptProperties and text as temporary globals the IIFE reads.
  281. JSValue seedProps = JS_NewObject (ctx);
  282. for (auto& [name, dynVal] : initialScriptProps) {
  283. JS_SetPropertyStr (ctx, seedProps, name.c_str (), this->dynamicToJs (*dynVal->value));
  284. }
  285. JS_SetPropertyStr (ctx, globalObj, "__layerSeedProps", seedProps);
  286. JS_SetPropertyStr (ctx, globalObj, "__layerSeedText", JS_NewString (ctx, initialText.c_str ()));
  287. const ScriptLayerHandle id = this->m_nextLayerId++;
  288. // Same stripping logic as evaluate(): WE scripts come as ES6 modules but
  289. // QuickJS is easier to drive as plain script evaluation.
  290. std::string body = scriptSource;
  291. size_t pos;
  292. while ((pos = body.find ("'use strict';")) != std::string::npos) {
  293. body.erase (pos, 13);
  294. }
  295. while ((pos = body.find ("\"use strict\";")) != std::string::npos) {
  296. body.erase (pos, 13);
  297. }
  298. while ((pos = body.find ("export ")) != std::string::npos) {
  299. body.erase (pos, 7);
  300. }
  301. // The IIFE gives every layer its own closure for top-level vars and
  302. // functions, so two layers that both define `function update()` or a
  303. // top-level `var scriptProperties` don't clobber each other. Lifecycle
  304. // hooks are captured into globalThis.__textLayers[id] so tick/destroy can
  305. // reach them later. `typeof init === 'function'` is safe even when
  306. // `init` was never declared — bare-identifier `typeof` never throws.
  307. std::ostringstream wrapper;
  308. wrapper
  309. << "(function() {\n"
  310. << " var __id = " << id << ";\n"
  311. << " var __props = Object.assign({}, globalThis.__layerSeedProps || {});\n"
  312. << " var thisLayer = { text: String(globalThis.__layerSeedText || '') };\n"
  313. << " var thisScene = {\n"
  314. << " get time() { var c = globalThis.__sceneCtx; return c ? c.time : 0; },\n"
  315. << " get currentTime() { var c = globalThis.__sceneCtx; return c ? c.time : 0; },\n"
  316. << " get dt() { var c = globalThis.__sceneCtx; return c ? c.dt : 0; },\n"
  317. << " get fps() { var c = globalThis.__sceneCtx; return c ? c.fps : 60; },\n"
  318. << " };\n"
  319. // Minimal WE `engine` shim. Real Wallpaper Engine exposes a broad API
  320. // (media events, audio buffer, user input); we provide just enough for
  321. // the common built-in text scripts to run without ReferenceError.
  322. // `frametime` is the per-frame delta in seconds (what InsertFPS reads).
  323. << " var engine = {\n"
  324. << " get frametime() { var c = globalThis.__sceneCtx; return c ? c.dt : 0; },\n"
  325. << " get time() { var c = globalThis.__sceneCtx; return c ? c.time : 0; },\n"
  326. << " };\n"
  327. << " function createScriptProperties() {\n"
  328. << " var builder = {\n"
  329. << " addSlider: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  330. << " addCheckbox: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  331. << " addCombo: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  332. << " addColor: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  333. << " addText: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  334. << " finish: function(){ return __props; }\n"
  335. << " };\n"
  336. << " return builder;\n"
  337. << " }\n"
  338. << body
  339. << "\n"
  340. // `_tick` wraps the user's `update()` so both WE text conventions work:
  341. // A) `export function update() { thisLayer.text = …; }` (mutates in place)
  342. // B) `export function update(value) { …; return value; }` (returns new text)
  343. // We pass the current text in, and if the return value is a string we
  344. // adopt it as the new `thisLayer.text`. Non-string / undefined return
  345. // leaves `thisLayer.text` as whatever the function assigned itself.
  346. << " globalThis.__textLayers[__id] = {\n"
  347. << " thisLayer: thisLayer,\n"
  348. << " thisScene: thisScene,\n"
  349. << " _init: (typeof init === 'function') ? init : null,\n"
  350. << " _destroy: (typeof destroy === 'function') ? destroy : null,\n"
  351. << " _tick: (typeof update === 'function')\n"
  352. << " ? function() {\n"
  353. << " var r = update(thisLayer.text);\n"
  354. << " if (typeof r === 'string') thisLayer.text = r;\n"
  355. << " }\n"
  356. << " : null,\n"
  357. << " _scriptProperties: (typeof scriptProperties !== 'undefined') ? scriptProperties : __props\n"
  358. << " };\n"
  359. << "})();\n";
  360. const std::string evalScript = wrapper.str ();
  361. JSValue result = JS_Eval (ctx, evalScript.c_str (), evalScript.size (), "<layer-script>", JS_EVAL_TYPE_GLOBAL);
  362. // Unset seeds so they don't leak into the next createLayerScript call.
  363. JS_SetPropertyStr (ctx, globalObj, "__layerSeedProps", JS_UNDEFINED);
  364. JS_SetPropertyStr (ctx, globalObj, "__layerSeedText", JS_UNDEFINED);
  365. JS_FreeValue (ctx, globalObj);
  366. if (JS_IsException (result)) {
  367. logJSException (ctx, "createLayerScript");
  368. JS_FreeValue (ctx, result);
  369. return kInvalidLayerHandle;
  370. }
  371. JS_FreeValue (ctx, result);
  372. this->m_layerInitialized[id] = false;
  373. return id;
  374. }
  375. void ScriptEngine::tickLayer (ScriptLayerHandle handle, double time, double deltaTime, double fps) {
  376. if (!this->m_context || handle == kInvalidLayerHandle) {
  377. return;
  378. }
  379. JSContext* ctx = this->m_context;
  380. JSValue globalObj = JS_GetGlobalObject (ctx);
  381. JSValue sceneCtx = JS_NewObject (ctx);
  382. JS_SetPropertyStr (ctx, sceneCtx, "time", JS_NewFloat64 (ctx, time));
  383. JS_SetPropertyStr (ctx, sceneCtx, "dt", JS_NewFloat64 (ctx, deltaTime));
  384. JS_SetPropertyStr (ctx, sceneCtx, "fps", JS_NewFloat64 (ctx, fps));
  385. JS_SetPropertyStr (ctx, globalObj, "__sceneCtx", sceneCtx);
  386. JSValue layers = JS_GetPropertyStr (ctx, globalObj, "__textLayers");
  387. JSValue layerObj = JS_GetPropertyUint32 (ctx, layers, static_cast<uint32_t> (handle));
  388. JS_FreeValue (ctx, layers);
  389. if (JS_IsUndefined (layerObj) || JS_IsNull (layerObj)) {
  390. JS_FreeValue (ctx, layerObj);
  391. JS_FreeValue (ctx, globalObj);
  392. return;
  393. }
  394. auto callHook = [&] (const char* prop, const char* tag) {
  395. JSValue fn = JS_GetPropertyStr (ctx, layerObj, prop);
  396. if (JS_IsFunction (ctx, fn)) {
  397. JSValue ret = JS_Call (ctx, fn, layerObj, 0, nullptr);
  398. if (JS_IsException (ret)) {
  399. logJSException (ctx, tag);
  400. }
  401. JS_FreeValue (ctx, ret);
  402. }
  403. JS_FreeValue (ctx, fn);
  404. };
  405. auto it = this->m_layerInitialized.find (handle);
  406. if (it != this->m_layerInitialized.end () && !it->second) {
  407. callHook ("_init", "layer.init");
  408. it->second = true;
  409. }
  410. callHook ("_tick", "layer.update");
  411. JS_FreeValue (ctx, layerObj);
  412. JS_FreeValue (ctx, globalObj);
  413. }
  414. std::string ScriptEngine::layerText (ScriptLayerHandle handle) {
  415. if (!this->m_context || handle == kInvalidLayerHandle) {
  416. return {};
  417. }
  418. JSContext* ctx = this->m_context;
  419. JSValue globalObj = JS_GetGlobalObject (ctx);
  420. JSValue layers = JS_GetPropertyStr (ctx, globalObj, "__textLayers");
  421. JSValue layerObj = JS_GetPropertyUint32 (ctx, layers, static_cast<uint32_t> (handle));
  422. std::string result;
  423. if (!JS_IsUndefined (layerObj) && !JS_IsNull (layerObj)) {
  424. JSValue thisLayer = JS_GetPropertyStr (ctx, layerObj, "thisLayer");
  425. JSValue textVal = JS_GetPropertyStr (ctx, thisLayer, "text");
  426. if (!JS_IsUndefined (textVal) && !JS_IsNull (textVal)) {
  427. const char* cstr = JS_ToCString (ctx, textVal);
  428. if (cstr) {
  429. result.assign (cstr);
  430. JS_FreeCString (ctx, cstr);
  431. }
  432. }
  433. JS_FreeValue (ctx, textVal);
  434. JS_FreeValue (ctx, thisLayer);
  435. }
  436. JS_FreeValue (ctx, layerObj);
  437. JS_FreeValue (ctx, layers);
  438. JS_FreeValue (ctx, globalObj);
  439. return result;
  440. }
  441. void ScriptEngine::destroyLayer (ScriptLayerHandle handle) {
  442. if (!this->m_context || handle == kInvalidLayerHandle) {
  443. return;
  444. }
  445. JSContext* ctx = this->m_context;
  446. JSValue globalObj = JS_GetGlobalObject (ctx);
  447. JSValue layers = JS_GetPropertyStr (ctx, globalObj, "__textLayers");
  448. JSValue layerObj = JS_GetPropertyUint32 (ctx, layers, static_cast<uint32_t> (handle));
  449. if (!JS_IsUndefined (layerObj) && !JS_IsNull (layerObj)) {
  450. JSValue fn = JS_GetPropertyStr (ctx, layerObj, "_destroy");
  451. if (JS_IsFunction (ctx, fn)) {
  452. JSValue ret = JS_Call (ctx, fn, layerObj, 0, nullptr);
  453. if (JS_IsException (ret)) {
  454. logJSException (ctx, "layer.destroy");
  455. }
  456. JS_FreeValue (ctx, ret);
  457. }
  458. JS_FreeValue (ctx, fn);
  459. }
  460. JS_FreeValue (ctx, layerObj);
  461. JS_FreeValue (ctx, layers);
  462. JS_FreeValue (ctx, globalObj);
  463. // Remove the entry from globalThis.__textLayers so GC can reclaim its closures.
  464. const std::string delScript = "delete globalThis.__textLayers[" + std::to_string (handle) + "];";
  465. JSValue delResult = JS_Eval (ctx, delScript.c_str (), delScript.size (), "<layer-destroy>", JS_EVAL_TYPE_GLOBAL);
  466. if (JS_IsException (delResult)) {
  467. logJSException (ctx, "layer.destroy.delete");
  468. }
  469. JS_FreeValue (ctx, delResult);
  470. this->m_layerInitialized.erase (handle);
  471. }
  472. JSValue ScriptEngine::call (JSValue module, int argc, JSValue argv[], const char* name) {
  473. // check if there's an update method and run it
  474. JSValue function = JS_GetPropertyStr (this->m_context, module, name);
  475. ScopeGuard guard ([&] () { JS_FreeValue (this->m_context, function); });
  476. if (!JS_IsFunction (this->m_context, function)) {
  477. return JS_UNDEFINED;
  478. }
  479. return JS_Call (this->m_context, function, module, argc, argv);
  480. }
  481. void ScriptEngine::queueScript (const std::string& key, DynamicValue& currentValue, ScriptableObject& object) {
  482. const auto source = currentValue.getScriptSource ();
  483. if (!source.has_value ()) {
  484. return;
  485. }
  486. auto it = this->m_scriptModules.find (key);
  487. if (it != this->m_scriptModules.end ()) {
  488. return;
  489. }
  490. // load the script and store it
  491. JSValue module = JS_Eval (this->m_context, source->c_str (), source->size (), key.c_str (), JS_EVAL_TYPE_MODULE);
  492. auto inserted = this->m_scriptModules.emplace (
  493. key,
  494. LoadedModule {
  495. .value = currentValue,
  496. .module = module,
  497. }
  498. );
  499. if (!inserted.second) {
  500. return;
  501. }
  502. JS_SetPropertyStr (this->m_context, this->m_globalThis, "thisLayer", this->m_adapters.object->instantiate (object));
  503. // script properties do not need update as they're connected directly to the source data
  504. this->m_runningModule = &inserted.first->second;
  505. // check if there's an update method and run it
  506. JSValue args[] = { this->dynamicToJs (currentValue) };
  507. JSValue result = this->call (module, 1, args, "update");
  508. ScopeGuard guard2 ([this, args, result] () {
  509. JS_FreeValue (this->m_context, result);
  510. JS_FreeValue (this->m_context, args[0]);
  511. });
  512. if (JS_IsException (result)) {
  513. return;
  514. }
  515. jsToDynamicValue (this->m_context, result, currentValue);
  516. }
  517. void ScriptEngine::tick () {
  518. // run intervals
  519. this->m_engineObject->tick ();
  520. // run any pending notifications
  521. // run all update methods
  522. for (auto& module : this->m_scriptModules | std::views::values) {
  523. this->m_runningModule = &module;
  524. JSValue args[] = { this->dynamicToJs (module.value) };
  525. JSValue result = this->call (module.module, 1, args, "update");
  526. ScopeGuard guard ([result, args, this] () {
  527. JS_FreeValue (this->m_context, result);
  528. JS_FreeValue (this->m_context, args[0]);
  529. });
  530. if (JS_IsException (result)) {
  531. continue;
  532. }
  533. jsToDynamicValue (this->m_context, result, module.value);
  534. }
  535. }
  536. void ScriptEngine::notifyMediaUpdate (const Media::MediaSource::MediaInfo& media) {
  537. JSContext* ctx = this->m_context;
  538. DynamicValue primaryColorValue (glm::vec3 (0.12f, 0.12f, 0.12f));
  539. DynamicValue secondaryColorValue (glm::vec3 (0.0f, 0.0f, 0.0f));
  540. DynamicValue tertiaryColorValue (glm::vec3 (0.25f, 0.25f, 0.25f));
  541. DynamicValue highContrastColorValue (glm::vec3 (1.0f, 1.0f, 1.0f));
  542. // TODO: PROCESS THESE COLORS INSTEAD OF HARDCODING THEM
  543. JSValue primaryColor = this->m_adapters.vec3->instantiate (primaryColorValue, true);
  544. JSValue secondaryColor = this->m_adapters.vec3->instantiate (secondaryColorValue, true);
  545. JSValue tertiaryColor = this->m_adapters.vec3->instantiate (tertiaryColorValue, true);
  546. JSValue highContrastColor = this->m_adapters.vec3->instantiate (highContrastColorValue, true);
  547. JSValue propertiesEvent = JS_NewObject (ctx);
  548. // set properties
  549. JS_SetPropertyStr (ctx, propertiesEvent, "title", JS_NewString (ctx, media.title.c_str ()));
  550. JS_SetPropertyStr (ctx, propertiesEvent, "artist", JS_NewString (ctx, media.artist.c_str ()));
  551. JS_SetPropertyStr (ctx, propertiesEvent, "albumTitle", JS_NewString (ctx, media.album.c_str ()));
  552. JSValue playbackEvent = JS_NewObject (ctx);
  553. JS_SetPropertyStr (ctx, playbackEvent, "state", JS_NewInt32 (ctx, media.playbackState));
  554. JSValue mediaTimelineEvent = JS_NewObject (ctx);
  555. JS_SetPropertyStr (ctx, mediaTimelineEvent, "position", JS_NewFloat64 (ctx, media.position));
  556. JS_SetPropertyStr (ctx, mediaTimelineEvent, "duration", JS_NewFloat64 (ctx, media.duration));
  557. JSValue mediaThumbnailEvent = JS_NewObject (ctx);
  558. JS_SetPropertyStr (ctx, mediaThumbnailEvent, "hasThumbnail", JS_NewBool (ctx, media.url.has_value ()));
  559. JS_SetPropertyStr (ctx, mediaThumbnailEvent, "primaryColor", primaryColor);
  560. JS_SetPropertyStr (ctx, mediaThumbnailEvent, "secondaryColor", secondaryColor);
  561. JS_SetPropertyStr (ctx, mediaThumbnailEvent, "tertiaryColor", tertiaryColor);
  562. JS_SetPropertyStr (ctx, mediaThumbnailEvent, "highContrastColor", highContrastColor);
  563. JSValue propertiesArgs[] = { propertiesEvent };
  564. JSValue playbackArgs[] = { playbackEvent };
  565. JSValue mediaTimelineArgs[] = { mediaTimelineEvent };
  566. JSValue mediaThumbnailArgs[] = { mediaThumbnailEvent };
  567. for (auto& module : this->m_scriptModules | std::views::values) {
  568. // call all methods
  569. JSValue result1 = this->call (module.module, 1, propertiesArgs, "mediaPropertiesChanged");
  570. JSValue result2 = this->call (module.module, 1, playbackArgs, "mediaPlaybackChanged");
  571. JSValue result3 = this->call (module.module, 1, mediaTimelineArgs, "mediaTimelineChanged");
  572. JSValue result4 = this->call (module.module, 1, mediaThumbnailArgs, "mediaThumbnailChanged");
  573. JS_FreeValue (ctx, result1);
  574. JS_FreeValue (ctx, result2);
  575. JS_FreeValue (ctx, result3);
  576. JS_FreeValue (ctx, result4);
  577. }
  578. // free all created objects as we don't keep a ref to them anymore
  579. JS_FreeValue (ctx, propertiesEvent);
  580. JS_FreeValue (ctx, playbackEvent);
  581. JS_FreeValue (ctx, mediaTimelineEvent);
  582. JS_FreeValue (ctx, mediaThumbnailEvent);
  583. }