EngineObject.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  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/Render/Wallpapers/CScene.h"
  7. #include <ranges>
  8. using namespace WallpaperEngine::Scripting;
  9. extern float g_Time;
  10. extern float g_TimeLast;
  11. extern float g_Daytime;
  12. static uint32_t EngineInstanceId = 0;
  13. std::map<uint32_t, EngineObject&> engineInstances;
  14. JSValue engine_set_value (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) { return JS_EXCEPTION; }
  15. JSValue engine_open_user_shortcut (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
  16. return JS_UNDEFINED;
  17. }
  18. JSValue engine_get_frametime (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
  19. return JS_NewFloat64 (ctx, g_Time - g_TimeLast);
  20. }
  21. JSValue engine_get_runtime (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
  22. return JS_NewFloat64 (ctx, g_Time);
  23. }
  24. JSValue engine_get_daytime (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
  25. return JS_NewFloat64 (ctx, g_Daytime);
  26. }
  27. JSValue engine_stop_interval (
  28. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  29. ) {
  30. if (argc != 1) {
  31. return JS_EXCEPTION;
  32. }
  33. const auto it = engineInstances.find (magic);
  34. if (it == engineInstances.end ()) {
  35. return JS_EXCEPTION;
  36. }
  37. int id = 0;
  38. JS_ToInt32 (ctx, &id, argv[0]);
  39. it->second.clearInterval (id);
  40. return JS_UNDEFINED;
  41. }
  42. JSValue engine_stop_timeout (
  43. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  44. ) {
  45. if (argc != 1) {
  46. return JS_EXCEPTION;
  47. }
  48. const auto it = engineInstances.find (magic);
  49. if (it == engineInstances.end ()) {
  50. return JS_EXCEPTION;
  51. }
  52. int id = 0;
  53. JS_ToInt32 (ctx, &id, argv[0]);
  54. it->second.clearTimeout (id);
  55. return JS_UNDEFINED;
  56. }
  57. JSValue engine_set_interval (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
  58. if (argc < 1) {
  59. return JS_EXCEPTION;
  60. }
  61. int delay = 0;
  62. if (argc > 1) {
  63. JS_ToInt32 (ctx, &delay, argv[1]);
  64. }
  65. JSValue function = argv[0];
  66. if (!JS_IsFunction (ctx, function)) {
  67. return JS_EXCEPTION;
  68. }
  69. const auto it = engineInstances.find (magic);
  70. if (it == engineInstances.end ()) {
  71. return JS_EXCEPTION;
  72. }
  73. int id = it->second.reserveNextIntervalId (function, delay);
  74. JSValue args[] = { JS_NewInt32 (ctx, id) };
  75. return JS_NewCFunctionData (ctx, engine_stop_interval, 2, magic, 1, args);
  76. }
  77. // Backs the "average"/"left"/"right" getters on the object returned by registerAudioBuffers().
  78. // The recorder only ever produces one (mono) spectrum - see PulseAudioPlaybackRecorder - so all
  79. // three read the same data, matching how CPass already binds it to both the Left and Right
  80. // g_AudioSpectrum shader uniforms.
  81. JSValue audio_buffer_get_values (
  82. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  83. ) {
  84. int engineInstanceId = 0;
  85. int resolution = 32;
  86. JS_ToInt32 (ctx, &engineInstanceId, func_data[0]);
  87. JS_ToInt32 (ctx, &resolution, func_data[1]);
  88. JSValue result = JS_NewArray (ctx);
  89. const auto it = engineInstances.find (engineInstanceId);
  90. if (it == engineInstances.end ()) {
  91. sLog.error ("registerAudioBuffers: no EngineObject found for instance ", engineInstanceId, " - returning zeros");
  92. return result;
  93. }
  94. const auto& recorder = it->second.getScene ().getAudioContext ().getDriver ().getRecorder ();
  95. const float* data = recorder.audio32;
  96. if (resolution == 16) {
  97. data = recorder.audio16;
  98. } else if (resolution == 64) {
  99. data = recorder.audio64;
  100. }
  101. static int diagnosticCounter = 0;
  102. if (++diagnosticCounter >= 500) {
  103. diagnosticCounter = 0;
  104. sLog.debug ("registerAudioBuffers: average[0..3] = ", data[0], ", ", data[1], ", ", data[2], ", ", data[3]);
  105. }
  106. for (int i = 0; i < resolution; i++) {
  107. JS_SetPropertyUint32 (ctx, result, i, JS_NewFloat64 (ctx, data[i]));
  108. }
  109. return result;
  110. }
  111. // engine.registerAudioBuffers(resolution): resolution must be 16, 32 or 64 (falls back to 32
  112. // otherwise), matching the AUDIO_RESOLUTION_* constants below. Returns an object whose
  113. // average/left/right properties are re-read from the live FFT spectrum every access, so scripts
  114. // that poll them from an update() callback see current values each frame.
  115. JSValue engine_register_audio_buffers (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
  116. int resolution = 32;
  117. if (argc > 0) {
  118. JS_ToInt32 (ctx, &resolution, argv[0]);
  119. }
  120. if (resolution != 16 && resolution != 32 && resolution != 64) {
  121. resolution = 32;
  122. }
  123. JSValue result = JS_NewObject (ctx);
  124. static constexpr const char* properties[] = { "average", "left", "right" };
  125. for (const char* property : properties) {
  126. JSValue closureData[] = { JS_NewInt32 (ctx, magic), JS_NewInt32 (ctx, resolution) };
  127. JS_DefinePropertyGetSet (
  128. ctx, result, JS_NewAtom (ctx, property),
  129. JS_NewCFunctionData (ctx, audio_buffer_get_values, 0, 0, 2, closureData),
  130. JS_NewCFunction (ctx, engine_set_value, "set", 1), JS_PROP_ENUMERABLE
  131. );
  132. }
  133. return result;
  134. }
  135. JSValue engine_set_timeout (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
  136. if (argc < 1) {
  137. return JS_EXCEPTION;
  138. }
  139. int delay = 0;
  140. if (argc > 1) {
  141. JS_ToInt32 (ctx, &delay, argv[1]);
  142. }
  143. JSValue function = argv[0];
  144. if (!JS_IsFunction (ctx, function)) {
  145. return JS_EXCEPTION;
  146. }
  147. const auto it = engineInstances.find (magic);
  148. if (it == engineInstances.end ()) {
  149. return JS_EXCEPTION;
  150. }
  151. int id = it->second.reserveNextTimeoutId (function, delay);
  152. JSValue args[] = { JS_NewInt32 (ctx, id) };
  153. return JS_NewCFunctionData (ctx, engine_stop_timeout, 2, magic, 1, args);
  154. }
  155. EngineObject::EngineObject (ScriptEngine& engine, Render::Wallpapers::CScene& scene) :
  156. m_scene (scene), m_engine (engine), m_instanceId (++EngineInstanceId), m_classId (0) {
  157. // required so setInterval/setTimeout/registerAudioBuffers's magic-encoded instance id can find
  158. // their way back to this object from the free-standing JS callback functions above
  159. engineInstances.emplace (this->m_instanceId, *this);
  160. this->m_definition = { .class_name = "IEngine" };
  161. JS_NewClassID (this->m_engine.getRuntime (), &this->m_classId);
  162. JS_NewClass (this->m_engine.getRuntime (), this->m_classId, &this->m_definition);
  163. this->m_instance = JS_NewObjectClass (this->m_engine.getContext (), this->m_classId);
  164. JS_DupValue (this->m_engine.getContext (), this->m_instance);
  165. // set properties
  166. JS_SetOpaque (this->m_instance, this);
  167. JS_DefinePropertyGetSet (
  168. this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "frametime"),
  169. JS_NewCFunction (this->m_engine.getContext (), engine_get_frametime, "get", 0),
  170. JS_NewCFunction (this->m_engine.getContext (), engine_set_value, "set", 1), JS_PROP_ENUMERABLE
  171. );
  172. JS_DefinePropertyGetSet (
  173. this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "runtime"),
  174. JS_NewCFunction (this->m_engine.getContext (), engine_get_runtime, "get", 0),
  175. JS_NewCFunction (this->m_engine.getContext (), engine_set_value, "set", 1), JS_PROP_ENUMERABLE
  176. );
  177. JS_DefinePropertyGetSet (
  178. this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "timeOfDay"),
  179. JS_NewCFunction (this->m_engine.getContext (), engine_get_daytime, "get", 0),
  180. JS_NewCFunction (this->m_engine.getContext (), engine_set_value, "set", 1), JS_PROP_ENUMERABLE
  181. );
  182. JS_DefinePropertyValueStr (
  183. this->m_engine.getContext (), this->m_instance, "AUDIO_RESOLUTION_16",
  184. JS_NewInt32 (this->m_engine.getContext (), 16), JS_PROP_ENUMERABLE
  185. );
  186. JS_DefinePropertyValueStr (
  187. this->m_engine.getContext (), this->m_instance, "AUDIO_RESOLUTION_32",
  188. JS_NewInt32 (this->m_engine.getContext (), 32), JS_PROP_ENUMERABLE
  189. );
  190. JS_DefinePropertyValueStr (
  191. this->m_engine.getContext (), this->m_instance, "AUDIO_RESOLUTION_64",
  192. JS_NewInt32 (this->m_engine.getContext (), 64), JS_PROP_ENUMERABLE
  193. );
  194. JS_DefinePropertyValueStr (
  195. this->m_engine.getContext (), this->m_instance, "setInterval",
  196. JS_NewCFunctionMagic (
  197. this->m_engine.getContext (), engine_set_interval, "setInterval", 2, JS_CFUNC_generic_magic,
  198. this->m_instanceId
  199. ),
  200. JS_PROP_ENUMERABLE
  201. );
  202. JS_DefinePropertyValueStr (
  203. this->m_engine.getContext (), this->m_instance, "setTimeout",
  204. JS_NewCFunctionMagic (
  205. this->m_engine.getContext (), engine_set_timeout, "setTimeout", 2, JS_CFUNC_generic_magic,
  206. this->m_instanceId
  207. ),
  208. JS_PROP_ENUMERABLE
  209. );
  210. JS_DefinePropertyValueStr (
  211. this->m_engine.getContext (), this->m_instance, "openUserShortcut",
  212. JS_NewCFunction (this->m_engine.getContext (), engine_open_user_shortcut, "openUserShortcut", 0),
  213. JS_PROP_ENUMERABLE
  214. );
  215. JS_DefinePropertyValueStr (
  216. this->m_engine.getContext (), this->m_instance, "registerAudioBuffers",
  217. JS_NewCFunctionMagic (
  218. this->m_engine.getContext (), engine_register_audio_buffers, "registerAudioBuffers", 1,
  219. JS_CFUNC_generic_magic, this->m_instanceId
  220. ),
  221. JS_PROP_ENUMERABLE
  222. );
  223. // TODO: ADD THE REST OF THE DEFINITION!
  224. }
  225. EngineObject::~EngineObject () {
  226. // clear all the timeouts and intervals
  227. for (const auto& [id, timeout] : this->m_timeouts) {
  228. JS_FreeValue (this->m_engine.getContext (), timeout.callback);
  229. }
  230. for (const auto& [id, interval] : this->m_intervals) {
  231. JS_FreeValue (this->m_engine.getContext (), interval.callback);
  232. }
  233. engineInstances.erase (this->m_instanceId);
  234. this->m_intervals.clear ();
  235. this->m_timeouts.clear ();
  236. JS_FreeValue (this->m_engine.getContext (), this->m_instance);
  237. }
  238. uint32_t EngineObject::reserveNextTimeoutId (JSValue function, uint64_t duration) {
  239. const auto id = ++this->m_nextTimeoutId;
  240. this->m_timeouts[id] = Timeout { .callback = function,
  241. .duration = std::chrono::milliseconds (duration),
  242. .next = std::chrono::steady_clock::now () + std::chrono::milliseconds (duration) };
  243. return id;
  244. }
  245. uint32_t EngineObject::reserveNextIntervalId (JSValue function, uint64_t duration) {
  246. const auto id = ++this->m_nextIntervalId;
  247. this->m_intervals[id]
  248. = Timeout { .callback = function,
  249. .duration = std::chrono::milliseconds (duration),
  250. .next = std::chrono::steady_clock::now () + std::chrono::milliseconds (duration) };
  251. return id;
  252. }
  253. void EngineObject::clearInterval (uint32_t id) {
  254. const auto it = this->m_intervals.find (id);
  255. if (it == this->m_intervals.end ()) {
  256. return;
  257. }
  258. JS_FreeValue (this->getEngine ().getContext (), it->second.callback);
  259. this->m_intervals.erase (id);
  260. }
  261. void EngineObject::clearTimeout (uint32_t id) {
  262. const auto it = this->m_timeouts.find (id);
  263. if (it == this->m_timeouts.end ()) {
  264. return;
  265. }
  266. JS_FreeValue (this->getEngine ().getContext (), it->second.callback);
  267. this->m_timeouts.erase (id);
  268. }
  269. void EngineObject::tick () {
  270. const auto now = std::chrono::steady_clock::now ();
  271. // check any interval and run them if needed
  272. for (auto& timeout : this->m_intervals | std::views::values) {
  273. if (timeout.next > now) {
  274. continue;
  275. }
  276. timeout.next = now + timeout.duration;
  277. JS_Call (this->m_engine.getContext (), timeout.callback, JS_NULL, 0, nullptr);
  278. }
  279. std::vector<uint32_t> removeTimeouts;
  280. // check any timeout and run them if needed
  281. for (auto& [id, timeout] : this->m_timeouts) {
  282. if (timeout.next > now) {
  283. continue;
  284. }
  285. JS_Call (this->m_engine.getContext (), timeout.callback, JS_NULL, 0, nullptr);
  286. JS_FreeValue (this->m_engine.getContext (), timeout.callback);
  287. removeTimeouts.push_back (id);
  288. }
  289. for (auto id : removeTimeouts) {
  290. this->m_timeouts.erase (id);
  291. }
  292. }