WallpaperApplication.cpp 79 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281
  1. #include "WallpaperApplication.h"
  2. #include <cstdlib>
  3. #include "Steam/FileSystem/FileSystem.h"
  4. #include "WallpaperEngine/Application/ApplicationState.h"
  5. #include "WallpaperEngine/Assets/AssetLoadException.h"
  6. #include "WallpaperEngine/Audio/Drivers/Detectors/PulseAudioPlayingDetector.h"
  7. #include "WallpaperEngine/FileSystem/Container.h"
  8. #include "WallpaperEngine/Logging/Log.h"
  9. #include "WallpaperEngine/Render/Drivers/VideoFactories.h"
  10. #include "WallpaperEngine/Render/RenderContext.h"
  11. #include "WallpaperEngine/Render/Wallpapers/CVideo.h"
  12. #include "WallpaperEngine/Data/Dumpers/StringPrinter.h"
  13. #include "WallpaperEngine/Data/Parsers/ProjectParser.h"
  14. #include "WallpaperEngine/Data/Utils/AudioSensitivity.h"
  15. #include "WallpaperEngine/Data/Model/Property.h"
  16. #include "WallpaperEngine/Data/Model/Wallpaper.h"
  17. #include "WallpaperEngine/Debugging/CallStack.h"
  18. #include "WallpaperEngine/FileSystem/Adapters/MediaCover.h"
  19. #include "WallpaperEngine/Media/DBusMediaSource.h"
  20. #include "WallpaperEngine/WebBrowser/CEF/BrowserClient.h"
  21. #include "WallpaperEngine/WebBrowser/CEF/PageBridge.h"
  22. #include "WallpaperEngine/WebBrowser/CEF/SharedMemoryRenderHandler.h"
  23. #include "WallpaperEngine/WebBrowser/IPC/WebHostSharedMemory.h"
  24. #include "include/cef_browser.h"
  25. #if DEMOMODE
  26. #include "recording.h"
  27. #endif /* DEMOMODE */
  28. #include <algorithm>
  29. #include <cctype>
  30. #include <chrono>
  31. #include <climits>
  32. #include <csignal>
  33. #include <ctime>
  34. #include <fstream>
  35. #include <numeric>
  36. #include <string_view>
  37. #include <unistd.h>
  38. #define STB_IMAGE_WRITE_IMPLEMENTATION
  39. #include <stb_image_write.h>
  40. #include <thread>
  41. #define FULLSCREEN_CHECK_WAIT_TIME 250
  42. float g_Time;
  43. float g_TimeLast;
  44. /** Unscaled wall-clock time, unaffected by --speed */
  45. float g_RealTime;
  46. float g_Daytime;
  47. using namespace WallpaperEngine::Assets;
  48. using namespace WallpaperEngine::Application;
  49. using namespace WallpaperEngine::Data::Model;
  50. using namespace WallpaperEngine::FileSystem;
  51. void CustomGLDebugCallback (
  52. GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam
  53. ) {
  54. // Widened from HIGH-only to catch MEDIUM/LOW too, while chasing the reload-corruption bug
  55. // (bloom-enabled scenes going black after an in-process wallpaper reload) - HIGH-only produced
  56. // nothing even with the driver's debug output confirmed reachable, so the failure isn't a GL
  57. // API validity error at all; it's a logic bug somewhere in scene reconstruction. Safe to narrow
  58. // back to HIGH-only once that's found, but low-severity output costs little in the meantime.
  59. if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) {
  60. return;
  61. }
  62. // libmpv's own internal renderer (not our code - shows up as libmpv.so/libgallium.so frames in
  63. // the call stack below) reuses a GL_STATIC_DRAW buffer with glBufferSubData every frame during
  64. // video playback, which the driver flags as a performance hint, not a correctness problem. It
  65. // fires on every single frame of every video wallpaper, drowning out anything else in this
  66. // (already widened) severity range.
  67. if (type == GL_DEBUG_TYPE_PERFORMANCE) {
  68. return;
  69. }
  70. sLog.error ("OpenGL error: ", message, ", type: ", type, ", id: ", id, ", severity: ", severity);
  71. std::vector<WallpaperEngine::Debugging::CallStack::CallInfo> callInfo;
  72. WallpaperEngine::Debugging::CallStack::GetCalls (callInfo);
  73. for (std::vector<WallpaperEngine::Debugging::CallStack::CallInfo>::size_type i = 0; i < callInfo.size (); ++i) {
  74. fprintf (
  75. stderr, "[%3lu] %15lu: %s in %s\n", callInfo.size () - i, callInfo[i].offset, callInfo[i].function.c_str (),
  76. callInfo[i].module.c_str ()
  77. );
  78. }
  79. }
  80. bool WallpaperApplication::isCefSubprocess () const {
  81. for (int i = 1; i < this->m_context.getArgc (); i++) {
  82. if (std::string_view (this->m_context.getArgv ()[i]).starts_with ("--type=")) {
  83. return true;
  84. }
  85. }
  86. return false;
  87. }
  88. WallpaperApplication::WallpaperApplication (ApplicationContext& context) : m_context (context) {
  89. this->initializeSubsystems ();
  90. try {
  91. this->loadBackgrounds ();
  92. } catch (const std::exception& e) {
  93. if (!this->isCefSubprocess ()) {
  94. throw;
  95. }
  96. // CEF re-execs this binary for its own subprocesses with a reconstructed argv that doesn't
  97. // always resolve to a loadable background. Must still reach setupBrowser() below regardless
  98. // - that's what calls CefExecuteProcess() to hand off into Chromium's subprocess entrypoint;
  99. // skipping it here leaves CEF's IPC handshake incomplete, which reads as a crashed subprocess
  100. // and gets retried, turning one web wallpaper into several stray processes.
  101. sLog.error ("Skipping background load for CEF subprocess re-exec: ", e.what ());
  102. }
  103. this->setupProperties ();
  104. this->setupAudioSensitivity ();
  105. this->setupSoundVolume ();
  106. this->listObjects ();
  107. this->listEffects ();
  108. this->listAudioObjects ();
  109. this->setupBrowser ();
  110. this->initializePlaylists ();
  111. }
  112. void WallpaperApplication::initializeSubsystems () {
  113. m_mediaSource = std::make_unique<WallpaperEngine::Media::DBusMediaSource> (std::chrono::milliseconds (2000));
  114. }
  115. AssetLocatorUniquePtr
  116. WallpaperApplication::setupAssetLocator (const std::string& bg, const std::filesystem::path& overlay) const {
  117. auto container = std::make_unique<Container> ();
  118. const std::filesystem::path path = bg;
  119. container->registerAdapterFactory (std::make_unique<MediaCoverFactory> (*this->m_mediaSource));
  120. container->mount ("$mediaThumbnail", "$mediaThumbnail");
  121. container->mount (path, "/");
  122. try {
  123. container->mount (path / "scene.pkg", "/");
  124. } catch (std::runtime_error&) { }
  125. try {
  126. container->mount (path / "gifscene.pkg", "/");
  127. } catch (std::runtime_error&) { }
  128. if (!overlay.empty ()) {
  129. container->mount (overlay, "/");
  130. }
  131. try {
  132. container->mount (this->m_context.settings.general.assets, "/");
  133. } catch (std::runtime_error&) {
  134. sLog.exception ("Cannot find a valid assets folder, resolved to ", this->m_context.settings.general.assets);
  135. }
  136. try {
  137. container->mount (std::filesystem::current_path (), "/");
  138. } catch (std::runtime_error&) { }
  139. auto& vfs = container->getVFS ();
  140. // Had to get a little creative with the effects to achieve the same bloom effect without any custom code:
  141. // these virtual files are loaded by an image in the scene that takes the current _rt_FullFrameBuffer and
  142. // applies the bloom effect to render it out to the screen.
  143. vfs.add (
  144. "effects/wpenginelinux/bloomeffect.json",
  145. { { "name", "camerabloom_wpengine_linux" },
  146. { "group", "wpengine_linux_camera" },
  147. { "dependencies", JSON::array () },
  148. {
  149. "passes",
  150. JSON::array (
  151. { { { "material", "materials/util/downsample_quarter_bloom.json" },
  152. { "target", "_rt_4FrameBuffer" },
  153. { "bind", JSON::array ({ { { "name", "_rt_FullFrameBuffer" }, { "index", 0 } } }) } },
  154. { { "material", "materials/util/downsample_eighth_blur_v.json" },
  155. { "target", "_rt_8FrameBuffer" },
  156. { "bind", JSON::array ({ { { "name", "_rt_4FrameBuffer" }, { "index", 0 } } }) } },
  157. { { "material", "materials/util/blur_h_bloom.json" },
  158. { "target", "_rt_Bloom" },
  159. { "bind", JSON::array ({ { { "name", "_rt_8FrameBuffer" }, { "index", 0 } } }) } },
  160. { { "material", "materials/util/combine.json" },
  161. { "target", "_rt_FullFrameBuffer" },
  162. { "bind",
  163. JSON::array (
  164. { { { "name", "_rt_imageLayerComposite_-1_a" }, { "index", 0 } },
  165. { { "name", "_rt_Bloom" }, { "index", 1 } } }
  166. ) } } }
  167. ),
  168. } }
  169. );
  170. // Wastes a render pass on an image element that exists only to host the bloom material above
  171. vfs.add ("models/wpenginelinux.json", { { "material", "materials/wpenginelinux.json" } });
  172. vfs.add (
  173. "materials/wpenginelinux.json",
  174. { { "passes",
  175. JSON::array (
  176. { { { "blending", "normal" },
  177. { "cullmode", "nocull" },
  178. { "depthtest", "disabled" },
  179. { "depthwrite", "disabled" },
  180. { "shader", "genericimage2" },
  181. { "textures", JSON::array ({ "_rt_FullFrameBuffer" }) } } }
  182. ) } }
  183. );
  184. vfs.add (
  185. "shaders/commands/copy.frag",
  186. "uniform sampler2D g_Texture0;\n"
  187. "in vec2 v_TexCoord;\n"
  188. "void main () {\n"
  189. "out_FragColor = texture (g_Texture0, v_TexCoord);\n"
  190. "}"
  191. );
  192. vfs.add (
  193. "shaders/commands/copy.vert",
  194. "in vec3 a_Position;\n"
  195. "in vec2 a_TexCoord;\n"
  196. "out vec2 v_TexCoord;\n"
  197. "void main () {\n"
  198. "gl_Position = vec4 (a_Position, 1.0);\n"
  199. "v_TexCoord = a_TexCoord;\n"
  200. "}"
  201. );
  202. return std::make_unique<AssetLocator> (std::move (container));
  203. }
  204. void WallpaperApplication::loadBackgrounds () {
  205. if (this->m_context.settings.render.mode == ApplicationContext::NORMAL_WINDOW
  206. || this->m_context.settings.render.mode == ApplicationContext::EXPLICIT_WINDOW) {
  207. auto path = this->m_context.settings.general.defaultBackground;
  208. if (this->m_context.settings.general.defaultPlaylist.has_value ()
  209. && !this->m_context.settings.general.defaultPlaylist->items.empty ()) {
  210. path = this->m_context.settings.general.defaultPlaylist->items.front ();
  211. }
  212. this->m_backgrounds["default"] = this->loadBackground (path);
  213. return;
  214. }
  215. for (const auto& [screen, path] : this->m_context.settings.general.screenBackgrounds) {
  216. // skip span group synthetic keys here, they're handled below
  217. if (screen.rfind ("span:", 0) == 0) {
  218. continue;
  219. }
  220. if (path.empty ()) {
  221. this->m_backgrounds[screen] = this->loadBackground (this->m_context.settings.general.defaultBackground);
  222. } else {
  223. this->m_backgrounds[screen] = this->loadBackground (path);
  224. }
  225. }
  226. for (const auto& spanGroup : this->m_context.settings.general.spanGroups) {
  227. if (spanGroup.screens.empty ()) {
  228. continue;
  229. }
  230. std::filesystem::path bgPath = spanGroup.background;
  231. if (bgPath.empty ()) {
  232. bgPath = this->m_context.settings.general.defaultBackground;
  233. }
  234. // "span:" + first screen's name is the convention used as the group key
  235. const std::string groupKey = "span:" + spanGroup.screens.front ();
  236. this->m_backgrounds[groupKey] = this->loadBackground (bgPath);
  237. }
  238. }
  239. WallpaperApplication::ProjectSource WallpaperApplication::openProjectSource (const std::string& path) const {
  240. auto container = this->setupAssetLocator (path);
  241. auto json = WallpaperEngine::Data::JSON::JSON::parse (container->readString ("project.json"));
  242. if (json.contains ("file") || !json.contains ("dependency")) {
  243. return { std::move (container), std::move (json), std::nullopt };
  244. }
  245. const auto dependency = json.at ("dependency");
  246. const auto dependencyId = dependency.is_string () ? dependency.get<std::string> () : dependency.dump ();
  247. std::filesystem::path presetDir = path;
  248. if (!presetDir.has_filename ()) {
  249. presetDir = presetDir.parent_path ();
  250. }
  251. const auto baseDir = presetDir.parent_path () / dependencyId;
  252. if (!std::filesystem::exists (baseDir / "project.json")) {
  253. sLog.exception ("Preset ", path, " requires wallpaper ", dependencyId, " which is not installed");
  254. }
  255. auto baseContainer = this->setupAssetLocator (baseDir.string (), presetDir);
  256. auto baseJson = WallpaperEngine::Data::JSON::JSON::parse (baseContainer->readString ("project.json"));
  257. return { std::move (baseContainer), std::move (baseJson), json.optional ("preset") };
  258. }
  259. void WallpaperApplication::applyPreset (
  260. const Project& project, const WallpaperEngine::Data::JSON::JSON& preset, const std::filesystem::path& presetDir
  261. ) {
  262. for (const auto& entry : preset.items ()) {
  263. const auto property = project.properties.find (entry.key ());
  264. // wec_* and friends are Wallpaper Engine's own per-preset settings, not wallpaper properties
  265. if (property == project.properties.end () || entry.value ().is_null ()) {
  266. continue;
  267. }
  268. const auto& value = entry.value ();
  269. std::string text;
  270. if (value.is_string ()) {
  271. text = value.get<std::string> ();
  272. } else if (value.is_boolean ()) {
  273. text = value.get<bool> () ? "true" : "false";
  274. } else {
  275. text = value.dump ();
  276. }
  277. // file and directory properties hold paths relative to the preset folder, pages expect an absolute one
  278. if (const auto* file = dynamic_cast<const PropertyFile*> (property->second.get ());
  279. file != nullptr && !text.empty () && std::filesystem::path (text).is_relative ()) {
  280. const auto candidate = presetDir / text;
  281. if (file->isDirectory () ? std::filesystem::is_directory (candidate)
  282. : std::filesystem::is_regular_file (candidate)) {
  283. text = candidate.string ();
  284. }
  285. }
  286. if (auto* combo = dynamic_cast<PropertyCombo*> (property->second.get ()); combo != nullptr) {
  287. combo->allowValue (text);
  288. }
  289. try {
  290. property->second->update (text, DynamicValue::UpdateSource::User);
  291. } catch (const std::exception& e) {
  292. sLog.error ("Cannot apply preset value for ", entry.key (), ": ", e.what ());
  293. }
  294. }
  295. }
  296. ProjectUniquePtr WallpaperApplication::loadBackground (const std::string& bg) {
  297. auto [container, json, preset] = this->openProjectSource (bg);
  298. // reset screenshot state so a background change (e.g. playlist advance) can screenshot again
  299. if (this->m_context.settings.screenshot.take) {
  300. this->m_nextFrameScreenshot = this->m_context.settings.screenshot.delay;
  301. if (this->m_videoDriver != nullptr) {
  302. this->m_nextFrameScreenshot += this->m_videoDriver->getFrameCounter ();
  303. }
  304. this->m_screenShotTaken = false;
  305. }
  306. auto project = WallpaperEngine::Data::Parsers::ProjectParser::parse (json, std::move (container));
  307. if (preset.has_value ()) {
  308. std::filesystem::path presetDir = bg;
  309. if (!presetDir.has_filename ()) {
  310. presetDir = presetDir.parent_path ();
  311. }
  312. applyPreset (*project, *preset, presetDir);
  313. }
  314. return project;
  315. }
  316. std::vector<std::size_t>
  317. WallpaperApplication::buildPlaylistOrder (const ApplicationContext::PlaylistDefinition& definition) {
  318. std::vector<std::size_t> order (definition.items.size ());
  319. std::iota (order.begin (), order.end (), 0);
  320. if (definition.settings.order == "random") {
  321. std::shuffle (order.begin (), order.end (), this->m_playlistRng);
  322. }
  323. return order;
  324. }
  325. void WallpaperApplication::initializePlaylists () {
  326. const bool hasDefaultPlaylist = this->m_context.settings.general.defaultPlaylist.has_value ();
  327. const bool hasScreenPlaylists = !this->m_context.settings.general.screenPlaylists.empty ();
  328. if (!hasDefaultPlaylist && !hasScreenPlaylists) {
  329. return;
  330. }
  331. const auto now = std::chrono::steady_clock::now ();
  332. auto registerPlaylist = [this, now] (
  333. const std::string& key, const ApplicationContext::PlaylistDefinition& playlist,
  334. std::optional<std::filesystem::path> currentPath
  335. ) {
  336. if (playlist.items.empty ()) {
  337. return;
  338. }
  339. ActivePlaylist state;
  340. state.definition = playlist;
  341. state.order = this->buildPlaylistOrder (playlist);
  342. if (state.order.empty ()) {
  343. return;
  344. }
  345. if (currentPath.has_value ()) {
  346. state.orderIndex = 0;
  347. for (std::size_t i = 0; i < state.order.size (); i++) {
  348. if (playlist.items[state.order[i]] == currentPath.value ()) {
  349. state.orderIndex = i;
  350. break;
  351. }
  352. }
  353. }
  354. const uint32_t delayMinutes = std::max<uint32_t> (1, state.definition.settings.delayMinutes);
  355. state.nextSwitch = now + std::chrono::minutes (delayMinutes);
  356. state.lastUpdate = now;
  357. this->m_activePlaylists.insert_or_assign (key, std::move (state));
  358. };
  359. if (hasDefaultPlaylist
  360. && (this->m_context.settings.render.mode == ApplicationContext::NORMAL_WINDOW
  361. || this->m_context.settings.render.mode == ApplicationContext::EXPLICIT_WINDOW)) {
  362. const auto& playlist = this->m_context.settings.general.defaultPlaylist.value ();
  363. const auto currentPath = playlist.items.empty ()
  364. ? std::optional<std::filesystem::path> { this->m_context.settings.general.defaultBackground }
  365. : std::optional<std::filesystem::path> { playlist.items.front () };
  366. registerPlaylist ("default", playlist, currentPath);
  367. }
  368. for (const auto& [screen, playlist] : this->m_context.settings.general.screenPlaylists) {
  369. const auto current = this->m_context.settings.general.screenBackgrounds.find (screen);
  370. const auto currentPath = current != this->m_context.settings.general.screenBackgrounds.end ()
  371. ? std::optional<std::filesystem::path> { current->second }
  372. : std::nullopt;
  373. registerPlaylist (screen, playlist, currentPath);
  374. }
  375. }
  376. bool WallpaperApplication::makeAnyViewportCurrent () const {
  377. if (!this->m_renderContext) {
  378. return false;
  379. }
  380. const auto& viewports = this->m_renderContext->getOutput ().getViewports ();
  381. if (viewports.empty ()) {
  382. return false;
  383. }
  384. viewports.begin ()->second->makeCurrent ();
  385. return true;
  386. }
  387. bool WallpaperApplication::preflightWallpaper (const std::string& path) {
  388. try {
  389. // avoid mutating state, just ensure project.json parses
  390. const auto source = this->openProjectSource (path);
  391. const auto& json = source.json;
  392. if (!json.contains ("type") || !json.contains ("file")) {
  393. sLog.error ("Preflight failed for ", path, ": missing required fields");
  394. return false;
  395. }
  396. return true;
  397. } catch (const std::exception& e) {
  398. sLog.error ("Preflight failed for ", path, ": ", e.what ());
  399. return false;
  400. }
  401. }
  402. bool WallpaperApplication::selectNextCandidate (ActivePlaylist& playlist, std::size_t& outOrderIndex) {
  403. if (playlist.order.empty ()) {
  404. return false;
  405. }
  406. std::size_t attempts = 0;
  407. std::size_t candidateOrderIndex = outOrderIndex;
  408. while (attempts < playlist.order.size ()) {
  409. const auto candidateIndex = playlist.order[candidateOrderIndex];
  410. if (!playlist.failedIndices.contains (candidateIndex)) {
  411. outOrderIndex = candidateOrderIndex;
  412. return true;
  413. }
  414. attempts++;
  415. candidateOrderIndex = (candidateOrderIndex + 1) % playlist.order.size ();
  416. }
  417. return false;
  418. }
  419. void WallpaperApplication::advancePlaylist (
  420. const std::string& screen, ActivePlaylist& playlist, const std::chrono::steady_clock::time_point& now
  421. ) {
  422. if (playlist.order.empty ()) {
  423. return;
  424. }
  425. playlist.orderIndex = (playlist.orderIndex + 1) % playlist.order.size ();
  426. if (playlist.orderIndex == 0 && playlist.definition.settings.order == "random") {
  427. std::shuffle (playlist.order.begin (), playlist.order.end (), this->m_playlistRng);
  428. }
  429. std::size_t candidateOrderIndex = playlist.orderIndex;
  430. if (!this->selectNextCandidate (playlist, candidateOrderIndex)) {
  431. sLog.error ("All playlist items failed for ", screen, ", keeping current wallpaper");
  432. const uint32_t delayMinutes = std::max<uint32_t> (1, playlist.definition.settings.delayMinutes);
  433. playlist.nextSwitch = now + std::chrono::minutes (delayMinutes);
  434. return;
  435. }
  436. const auto candidateIndex = playlist.order[candidateOrderIndex];
  437. const auto& candidatePath = playlist.definition.items[candidateIndex];
  438. if (!this->preflightWallpaper (candidatePath.string ())) {
  439. playlist.failedIndices.insert (candidateIndex);
  440. if (!this->selectNextCandidate (playlist, candidateOrderIndex)) {
  441. sLog.error ("All playlist items failed for ", screen, ", keeping current wallpaper");
  442. const uint32_t delayMinutes = std::max<uint32_t> (1, playlist.definition.settings.delayMinutes);
  443. playlist.nextSwitch = now + std::chrono::minutes (delayMinutes);
  444. return;
  445. }
  446. }
  447. playlist.orderIndex = candidateOrderIndex;
  448. const auto& nextPath = playlist.definition.items[playlist.order[playlist.orderIndex]];
  449. bool loaded = false;
  450. try {
  451. if (!this->makeAnyViewportCurrent ()) {
  452. sLog.error ("Cannot switch playlist on ", screen, ": no active viewport");
  453. throw std::runtime_error ("No viewport available");
  454. }
  455. auto project = this->loadBackground (nextPath.string ());
  456. this->setupPropertiesForProject (*project);
  457. // the outgoing project must outlive setWallpaper(), the old wallpaper keeps a reference into it
  458. auto outgoing = std::move (this->m_backgrounds[screen]);
  459. this->m_backgrounds[screen] = std::move (project);
  460. try {
  461. const auto scalingIt = this->m_context.settings.general.screenScalings.find (screen);
  462. const auto clampIt = this->m_context.settings.general.screenClamps.find (screen);
  463. const auto scaling = scalingIt != this->m_context.settings.general.screenScalings.end ()
  464. ? scalingIt->second
  465. : this->m_context.settings.render.window.scalingMode;
  466. const auto clamp = clampIt != this->m_context.settings.general.screenClamps.end ()
  467. ? clampIt->second
  468. : this->m_context.settings.render.window.clamp;
  469. if (this->m_renderContext) {
  470. auto wallpaper = WallpaperEngine::Render::CWallpaper::fromWallpaper (
  471. *this->m_backgrounds[screen]->wallpaper, *this->m_renderContext, *this->m_audioContext, nextPath,
  472. scaling, clamp, this->resolveScreenRenderSize (screen)
  473. );
  474. wallpaper->setZoom (this->resolveScreenZoom (screen));
  475. const auto offset = this->resolveScreenOffset (screen);
  476. wallpaper->setOffset (offset.x, offset.y);
  477. wallpaper->setCornerColor (this->resolveScreenCornerColor (screen));
  478. this->m_renderContext->setWallpaper (screen, std::move (wallpaper));
  479. this->applyAudioPolicy ();
  480. }
  481. } catch (...) {
  482. project = std::move (this->m_backgrounds[screen]);
  483. this->m_backgrounds[screen] = std::move (outgoing);
  484. if (this->m_renderContext) {
  485. this->m_renderContext->pruneTextures ();
  486. }
  487. throw;
  488. }
  489. this->m_context.settings.general.screenBackgrounds[screen] = nextPath;
  490. loaded = true;
  491. } catch (const std::exception& e) {
  492. sLog.error ("Failed to advance playlist on ", screen, ": ", e.what ());
  493. }
  494. if (!loaded) {
  495. playlist.failedIndices.insert (playlist.order[playlist.orderIndex]);
  496. // Keep current position; next timer tick will retry advancement
  497. sLog.error ("Failed to load wallpaper for ", screen, ", will retry on next cycle");
  498. }
  499. const uint32_t delayMinutes = std::max<uint32_t> (1, playlist.definition.settings.delayMinutes);
  500. playlist.nextSwitch = now + std::chrono::minutes (delayMinutes);
  501. }
  502. void WallpaperApplication::updatePlaylists () {
  503. if (this->m_activePlaylists.empty ()) {
  504. return;
  505. }
  506. const auto now = std::chrono::steady_clock::now ();
  507. for (auto& [screen, playlist] : this->m_activePlaylists) {
  508. playlist.lastUpdate = now;
  509. if (playlist.definition.settings.mode != "timer") {
  510. continue;
  511. }
  512. if (playlist.definition.items.size () <= 1) {
  513. continue;
  514. }
  515. if (now < playlist.nextSwitch) {
  516. continue;
  517. }
  518. this->advancePlaylist (screen, playlist, now);
  519. }
  520. }
  521. namespace {
  522. /** Everything a control-file request can carry; any field left unset means "don't touch this" */
  523. struct HotswapRequest {
  524. std::optional<std::string> path;
  525. /** True once a "layers=1" line was seen, meaning disabledObjects/enabledObjects below are a full replacement */
  526. bool layersProvided = false;
  527. std::vector<std::string> disabledObjects;
  528. std::vector<std::string> enabledObjects;
  529. /** True once at least one "property=name=value" line was seen */
  530. bool propertiesProvided = false;
  531. std::map<std::string, std::string> properties;
  532. std::optional<int> volume;
  533. /** frame rate limit, see --fps */
  534. std::optional<int> fps;
  535. /** "on"/"off"/"toggle" (also "1"/"0"/"true"/"false" for on/off) */
  536. std::optional<std::string> xray;
  537. /** "stretch"/"fit"/"fill"/"center"/"default" */
  538. std::optional<std::string> scaling;
  539. /** floating-point zoom factor layered on top of scaling, e.g. "1.5" */
  540. std::optional<std::string> zoom;
  541. /** "X,Y" offset re-center, each axis in [-1, 1], e.g. "0.5,-1" */
  542. std::optional<std::string> offset;
  543. /** "on"/"off"/"toggle" (also "1"/"0"/"true"/"false" for on/off) */
  544. std::optional<std::string> disableParallax;
  545. /** "on"/"off" (also "1"/"0"/"true"/"false"), see --expand-canvas. Baked in at scene build, so it reloads */
  546. std::optional<std::string> expandCanvas;
  547. /** hex RGB/RGBA color, e.g. "000000" or "#1a1a1aff" */
  548. std::optional<std::string> cornerColor;
  549. /** floating-point playback speed multiplier, e.g. "0.5" */
  550. std::optional<std::string> speed;
  551. /** screen name to restrict audio to, or "" to clear the restriction, see --audio-screen */
  552. std::optional<std::string> audioScreen;
  553. /** 0-128, see --ambient-volume */
  554. std::optional<std::string> ambientVolume;
  555. /** True once at least one "audio-sensitivity=id=multiplier" line was seen */
  556. bool audioSensitivityProvided = false;
  557. std::map<std::string, std::string> audioSensitivity;
  558. /** True once at least one "sound-volume=id=volume" line was seen */
  559. bool soundVolumeProvided = false;
  560. std::map<std::string, std::string> soundVolume;
  561. };
  562. std::string trimHotswapToken (const std::string& value) {
  563. std::size_t begin = 0;
  564. std::size_t end = value.size ();
  565. while (begin < end && std::isspace (static_cast<unsigned char> (value[begin]))) {
  566. begin++;
  567. }
  568. while (end > begin && std::isspace (static_cast<unsigned char> (value[end - 1]))) {
  569. end--;
  570. }
  571. return value.substr (begin, end - begin);
  572. }
  573. /**
  574. * Parses the control file. Supports the original bare-path-on-one-line format for backwards
  575. * compatibility, plus key=value lines (path/layers/disable-object/enable-object/volume/xray/scaling/zoom/
  576. * offset/disable-parallax/corner-color/speed/audio-screen/ambient-volume/property) so a single request can
  577. * carry more than just the background path. "property=name=value" (repeatable) carries
  578. * --set-property-equivalent overrides.
  579. */
  580. HotswapRequest parseHotswapRequest (std::istream& file) {
  581. HotswapRequest request;
  582. std::string rawLine;
  583. while (std::getline (file, rawLine)) {
  584. const std::string line = trimHotswapToken (rawLine);
  585. if (line.empty ()) {
  586. continue;
  587. }
  588. const auto separator = line.find ('=');
  589. if (separator == std::string::npos) {
  590. // legacy format: the whole line is the new background path
  591. if (!request.path.has_value ()) {
  592. request.path = line;
  593. }
  594. continue;
  595. }
  596. const std::string key = trimHotswapToken (line.substr (0, separator));
  597. const std::string value = trimHotswapToken (line.substr (separator + 1));
  598. if (key == "path") {
  599. request.path = value;
  600. } else if (key == "layers") {
  601. request.layersProvided = true;
  602. } else if (key == "disable-object") {
  603. request.layersProvided = true;
  604. request.disabledObjects.push_back (value);
  605. } else if (key == "enable-object") {
  606. request.layersProvided = true;
  607. request.enabledObjects.push_back (value);
  608. } else if (key == "volume") {
  609. try {
  610. request.volume = std::stoi (value);
  611. } catch (const std::exception&) {
  612. sLog.error ("Hotswap: ignoring invalid volume value: ", value);
  613. }
  614. } else if (key == "fps") {
  615. try {
  616. request.fps = std::stoi (value);
  617. } catch (const std::exception&) {
  618. sLog.error ("Hotswap: ignoring invalid fps value: ", value);
  619. }
  620. } else if (key == "xray") {
  621. request.xray = value;
  622. } else if (key == "scaling") {
  623. request.scaling = value;
  624. } else if (key == "zoom") {
  625. request.zoom = value;
  626. } else if (key == "offset") {
  627. request.offset = value;
  628. } else if (key == "disable-parallax") {
  629. request.disableParallax = value;
  630. } else if (key == "expand-canvas") {
  631. request.expandCanvas = value;
  632. } else if (key == "corner-color") {
  633. request.cornerColor = value;
  634. } else if (key == "speed") {
  635. request.speed = value;
  636. } else if (key == "audio-screen") {
  637. request.audioScreen = value;
  638. } else if (key == "ambient-volume") {
  639. request.ambientVolume = value;
  640. } else if (key == "property") {
  641. const auto propSeparator = value.find ('=');
  642. if (propSeparator == std::string::npos) {
  643. sLog.error ("Hotswap: ignoring malformed property line: ", value);
  644. } else {
  645. request.propertiesProvided = true;
  646. request.properties[value.substr (0, propSeparator)] = value.substr (propSeparator + 1);
  647. }
  648. } else if (key == "audio-sensitivity") {
  649. const auto sensSeparator = value.find ('=');
  650. if (sensSeparator == std::string::npos) {
  651. sLog.error ("Hotswap: ignoring malformed audio-sensitivity line: ", value);
  652. } else {
  653. request.audioSensitivityProvided = true;
  654. request.audioSensitivity[value.substr (0, sensSeparator)] = value.substr (sensSeparator + 1);
  655. }
  656. } else if (key == "sound-volume") {
  657. const auto volSeparator = value.find ('=');
  658. if (volSeparator == std::string::npos) {
  659. sLog.error ("Hotswap: ignoring malformed sound-volume line: ", value);
  660. } else {
  661. request.soundVolumeProvided = true;
  662. request.soundVolume[value.substr (0, volSeparator)] = value.substr (volSeparator + 1);
  663. }
  664. } else {
  665. sLog.error ("Hotswap: ignoring unknown control file key: ", key);
  666. }
  667. }
  668. return request;
  669. }
  670. } // namespace
  671. void WallpaperApplication::checkHotswapRequest () {
  672. if (!this->m_hotswapRequested.exchange (false)) {
  673. return;
  674. }
  675. const char* runtimeDir = getenv ("XDG_RUNTIME_DIR");
  676. const auto controlFile = std::filesystem::path (runtimeDir != nullptr ? runtimeDir : "/tmp") / "lwe-control";
  677. std::ifstream file (controlFile);
  678. if (!file.is_open ()) {
  679. sLog.error ("Hotswap requested but control file could not be read: ", controlFile.string ());
  680. return;
  681. }
  682. const auto request = parseHotswapRequest (file);
  683. if (!request.path.has_value () && !request.layersProvided && !request.volume.has_value ()
  684. && !request.xray.has_value () && !request.scaling.has_value () && !request.zoom.has_value ()
  685. && !request.offset.has_value () && !request.disableParallax.has_value () && !request.expandCanvas.has_value ()
  686. && !request.cornerColor.has_value ()
  687. && !request.speed.has_value () && !request.audioScreen.has_value () && !request.ambientVolume.has_value ()
  688. && !request.propertiesProvided && !request.audioSensitivityProvided && !request.soundVolumeProvided) {
  689. sLog.error ("Hotswap requested but control file was empty");
  690. return;
  691. }
  692. // overrides belong to the wallpaper they were set for, object ids and generic property names
  693. // ("newproperty1") collide across wallpapers, so a new wallpaper starts without the old ones and
  694. // only gets whatever this same request carries for it
  695. if (request.path.has_value ()) {
  696. auto& general = this->m_context.settings.general;
  697. general.disabledObjects.clear ();
  698. general.enabledObjects.clear ();
  699. general.disabledEffects.clear ();
  700. general.enabledEffects.clear ();
  701. general.properties.clear ();
  702. general.audioSensitivity.clear ();
  703. general.soundVolume.clear ();
  704. }
  705. if (request.volume.has_value ()) {
  706. this->applyVolumeHotswap (*request.volume);
  707. }
  708. if (request.fps.has_value ()) {
  709. this->applyFpsHotswap (*request.fps);
  710. }
  711. if (request.xray.has_value ()) {
  712. this->applyXrayHotswap (*request.xray);
  713. }
  714. if (request.scaling.has_value ()) {
  715. this->applyScalingHotswap (*request.scaling);
  716. }
  717. if (request.zoom.has_value ()) {
  718. this->applyZoomHotswap (*request.zoom);
  719. }
  720. if (request.offset.has_value ()) {
  721. this->applyOffsetHotswap (*request.offset);
  722. }
  723. if (request.disableParallax.has_value ()) {
  724. this->applyParallaxHotswap (*request.disableParallax);
  725. }
  726. if (request.cornerColor.has_value ()) {
  727. this->applyCornerColorHotswap (*request.cornerColor);
  728. }
  729. if (request.speed.has_value ()) {
  730. this->applySpeedHotswap (*request.speed);
  731. }
  732. if (request.audioScreen.has_value ()) {
  733. this->applyAudioScreenHotswap (*request.audioScreen);
  734. }
  735. if (request.ambientVolume.has_value ()) {
  736. this->applyAmbientVolumeHotswap (*request.ambientVolume);
  737. }
  738. if (request.soundVolumeProvided) {
  739. this->applySoundVolumeHotswap (request.soundVolume);
  740. }
  741. bool expandCanvasChanged = false;
  742. if (request.expandCanvas.has_value ()) {
  743. const auto& value = *request.expandCanvas;
  744. const bool enable = value == "on" || value == "1" || value == "true";
  745. if (!enable && value != "off" && value != "0" && value != "false") {
  746. sLog.error ("Hotswap: ignoring invalid expand-canvas value: ", value);
  747. } else {
  748. expandCanvasChanged = this->m_context.settings.general.expandCanvas != enable;
  749. this->m_context.settings.general.expandCanvas = enable;
  750. }
  751. }
  752. if (request.layersProvided) {
  753. this->m_context.settings.general.disabledObjects = request.disabledObjects;
  754. this->m_context.settings.general.enabledObjects = request.enabledObjects;
  755. }
  756. if (request.propertiesProvided) {
  757. for (const auto& [name, value] : request.properties) {
  758. this->m_context.settings.general.properties[name] = value;
  759. }
  760. }
  761. if (request.audioSensitivityProvided) {
  762. for (const auto& [target, value] : request.audioSensitivity) {
  763. try {
  764. this->m_context.settings.general.audioSensitivity[target] = std::stof (value);
  765. } catch (const std::exception&) {
  766. sLog.error ("Hotswap: ignoring invalid audio-sensitivity value: ", value);
  767. }
  768. }
  769. }
  770. // volume/xray/speed-only requests are pure live setters with nothing to reload. Properties
  771. // and audio sensitivity, like layers, are baked into the scene graph at parse time, so they
  772. // need the same reload, as does the expanded canvas (it sizes the scene's framebuffers).
  773. if (!request.path.has_value () && !request.layersProvided && !request.propertiesProvided
  774. && !request.audioSensitivityProvided && !expandCanvasChanged) {
  775. return;
  776. }
  777. if (request.path.has_value () && !this->preflightWallpaper (*request.path)) {
  778. sLog.error ("Hotswap failed, invalid wallpaper at ", *request.path);
  779. return;
  780. }
  781. if (!this->makeAnyViewportCurrent ()) {
  782. sLog.error ("Hotswap failed, no active viewport");
  783. return;
  784. }
  785. if (request.path.has_value ()) {
  786. sLog.out ("Hotswapping wallpaper to ", *request.path);
  787. } else {
  788. sLog.out ("Hotswapping wallpaper layers");
  789. }
  790. // the outgoing project must outlive setWallpaper(), the old wallpaper keeps a reference into it
  791. for (auto& [screen, background] : this->m_backgrounds) {
  792. const std::string targetPath = request.path.value_or (this->resolveScreenBackgroundPath (screen));
  793. try {
  794. auto project = this->loadBackground (targetPath);
  795. this->setupPropertiesForProject (*project);
  796. this->setupAudioSensitivityForProject (*project);
  797. this->setupSoundVolumeForProject (*project);
  798. auto outgoing = std::move (background);
  799. background = std::move (project);
  800. try {
  801. const auto scalingIt = this->m_context.settings.general.screenScalings.find (screen);
  802. const auto clampIt = this->m_context.settings.general.screenClamps.find (screen);
  803. const auto scaling = scalingIt != this->m_context.settings.general.screenScalings.end ()
  804. ? scalingIt->second
  805. : this->m_context.settings.render.window.scalingMode;
  806. const auto clamp = clampIt != this->m_context.settings.general.screenClamps.end ()
  807. ? clampIt->second
  808. : this->m_context.settings.render.window.clamp;
  809. if (this->m_renderContext) {
  810. auto wallpaper = WallpaperEngine::Render::CWallpaper::fromWallpaper (
  811. *background->wallpaper, *this->m_renderContext, *this->m_audioContext, targetPath, scaling, clamp,
  812. this->resolveScreenRenderSize (screen)
  813. );
  814. wallpaper->setZoom (this->resolveScreenZoom (screen));
  815. const auto offset = this->resolveScreenOffset (screen);
  816. wallpaper->setOffset (offset.x, offset.y);
  817. wallpaper->setCornerColor (this->resolveScreenCornerColor (screen));
  818. this->m_renderContext->setWallpaper (screen, std::move (wallpaper));
  819. }
  820. } catch (...) {
  821. project = std::move (background);
  822. background = std::move (outgoing);
  823. if (this->m_renderContext) {
  824. this->m_renderContext->pruneTextures ();
  825. }
  826. throw;
  827. }
  828. if (request.path.has_value ()) {
  829. if (screen.starts_with ("span:")) {
  830. for (auto& spanGroup : this->m_context.settings.general.spanGroups) {
  831. if (!spanGroup.screens.empty () && "span:" + spanGroup.screens.front () == screen) {
  832. spanGroup.background = *request.path;
  833. }
  834. }
  835. } else {
  836. this->m_context.settings.general.screenBackgrounds[screen] = *request.path;
  837. }
  838. }
  839. } catch (const std::exception& e) {
  840. sLog.error ("Hotswap failed for screen ", screen, ": ", e.what ());
  841. }
  842. }
  843. this->applyAudioPolicy ();
  844. if (request.path.has_value ()) {
  845. this->m_context.settings.general.defaultBackground = *request.path;
  846. }
  847. }
  848. std::string WallpaperApplication::resolveScreenBackgroundPath (const std::string& screen) const {
  849. if (screen.starts_with ("span:")) {
  850. for (const auto& spanGroup : this->m_context.settings.general.spanGroups) {
  851. if (!spanGroup.screens.empty () && "span:" + spanGroup.screens.front () == screen) {
  852. return spanGroup.background.empty () ? this->m_context.settings.general.defaultBackground.string ()
  853. : spanGroup.background.string ();
  854. }
  855. }
  856. return this->m_context.settings.general.defaultBackground.string ();
  857. }
  858. const auto it = this->m_context.settings.general.screenBackgrounds.find (screen);
  859. if (it != this->m_context.settings.general.screenBackgrounds.end () && !it->second.empty ()) {
  860. return it->second.string ();
  861. }
  862. return this->m_context.settings.general.defaultBackground.string ();
  863. }
  864. float WallpaperApplication::resolveScreenZoom (const std::string& screen) const {
  865. const auto it = this->m_context.settings.general.screenZooms.find (screen);
  866. return it != this->m_context.settings.general.screenZooms.end () ? it->second
  867. : this->m_context.settings.render.window.zoom;
  868. }
  869. glm::vec2 WallpaperApplication::resolveScreenOffset (const std::string& screen) const {
  870. const auto it = this->m_context.settings.general.screenOffsets.find (screen);
  871. return it != this->m_context.settings.general.screenOffsets.end ()
  872. ? it->second
  873. : this->m_context.settings.render.window.offset;
  874. }
  875. glm::vec4 WallpaperApplication::resolveScreenCornerColor (const std::string& screen) const {
  876. const auto it = this->m_context.settings.general.screenCornerColors.find (screen);
  877. return it != this->m_context.settings.general.screenCornerColors.end ()
  878. ? it->second
  879. : this->m_context.settings.render.window.cornerColor;
  880. }
  881. glm::ivec2 WallpaperApplication::resolveScreenRenderSize (const std::string& screen) const {
  882. const auto& viewports = this->m_renderContext->getOutput ().getViewports ();
  883. const auto it = viewports.find (screen);
  884. if (it != viewports.end ()) {
  885. return { it->second->viewport.z, it->second->viewport.w };
  886. }
  887. return { this->m_renderContext->getOutput ().getFullWidth (), this->m_renderContext->getOutput ().getFullHeight () };
  888. }
  889. void WallpaperApplication::applyVolumeHotswap (int volume) {
  890. volume = std::max (0, std::min (volume, 128));
  891. this->m_context.settings.audio.volume = volume;
  892. this->m_context.state.audio.volume = volume;
  893. if (!this->m_renderContext) {
  894. return;
  895. }
  896. const double scaledVolume = this->m_context.settings.audio.enabled ? volume * 100.0 / 128.0 : 0.0;
  897. for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
  898. if (wallpaper->is<WallpaperEngine::Render::Wallpapers::CVideo> ()) {
  899. wallpaper->as<WallpaperEngine::Render::Wallpapers::CVideo> ()->setVolume (scaledVolume);
  900. }
  901. }
  902. sLog.out ("Hotswap: applied volume ", volume, " live");
  903. }
  904. void WallpaperApplication::applyFpsHotswap (int fps) {
  905. fps = std::max (1, fps);
  906. this->m_context.settings.render.maximumFPS = fps;
  907. sLog.out ("Hotswap: applied fps limit ", fps, " live");
  908. }
  909. void WallpaperApplication::applyXrayHotswap (const std::string& value) {
  910. bool newState;
  911. if (value == "toggle") {
  912. newState = !this->m_context.state.xray.fullReveal;
  913. } else if (value == "on" || value == "1" || value == "true") {
  914. newState = true;
  915. } else if (value == "off" || value == "0" || value == "false") {
  916. newState = false;
  917. } else {
  918. sLog.error ("Hotswap: ignoring invalid xray value: ", value);
  919. return;
  920. }
  921. this->m_context.state.xray.fullReveal = newState;
  922. sLog.out ("Hotswap: full xray ", newState ? "enabled" : "disabled", " live");
  923. }
  924. void WallpaperApplication::applyScalingHotswap (const std::string& value) {
  925. const auto mode = WallpaperEngine::Render::WallpaperState::parseScalingMode (value);
  926. if (!mode.has_value ()) {
  927. sLog.error ("Hotswap: ignoring invalid scaling value: ", value);
  928. return;
  929. }
  930. this->m_context.settings.render.window.scalingMode = *mode;
  931. for (auto& [screen, scaling] : this->m_context.settings.general.screenScalings) {
  932. scaling = *mode;
  933. }
  934. for (auto& spanGroup : this->m_context.settings.general.spanGroups) {
  935. spanGroup.scaling = *mode;
  936. }
  937. if (this->m_renderContext) {
  938. for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
  939. wallpaper->setScalingMode (*mode);
  940. }
  941. }
  942. sLog.out ("Hotswap: applied scaling mode ", value, " live");
  943. }
  944. void WallpaperApplication::applyZoomHotswap (const std::string& value) {
  945. float zoom;
  946. try {
  947. zoom = std::stof (value);
  948. } catch (const std::exception&) {
  949. sLog.error ("Hotswap: ignoring invalid zoom value: ", value);
  950. return;
  951. }
  952. this->m_context.settings.render.window.zoom = zoom;
  953. for (auto& [screen, screenZoom] : this->m_context.settings.general.screenZooms) {
  954. screenZoom = zoom;
  955. }
  956. for (auto& spanGroup : this->m_context.settings.general.spanGroups) {
  957. spanGroup.zoom = zoom;
  958. }
  959. if (this->m_renderContext) {
  960. for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
  961. wallpaper->setZoom (zoom);
  962. }
  963. }
  964. sLog.out ("Hotswap: applied zoom ", zoom, " live");
  965. }
  966. void WallpaperApplication::applyOffsetHotswap (const std::string& value) {
  967. const auto comma = value.find (',');
  968. if (comma == std::string::npos) {
  969. sLog.error ("Hotswap: ignoring malformed offset value (expected X,Y): ", value);
  970. return;
  971. }
  972. glm::vec2 offset;
  973. try {
  974. offset.x = std::stof (value.substr (0, comma));
  975. offset.y = std::stof (value.substr (comma + 1));
  976. } catch (const std::exception&) {
  977. sLog.error ("Hotswap: ignoring invalid offset value: ", value);
  978. return;
  979. }
  980. this->m_context.settings.render.window.offset = offset;
  981. for (auto& [screen, screenOffset] : this->m_context.settings.general.screenOffsets) {
  982. screenOffset = offset;
  983. }
  984. for (auto& spanGroup : this->m_context.settings.general.spanGroups) {
  985. spanGroup.offset = offset;
  986. }
  987. if (this->m_renderContext) {
  988. for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
  989. wallpaper->setOffset (offset.x, offset.y);
  990. }
  991. }
  992. sLog.out ("Hotswap: applied offset ", offset.x, ",", offset.y, " live");
  993. }
  994. void WallpaperApplication::applyParallaxHotswap (const std::string& value) {
  995. bool newState;
  996. if (value == "toggle") {
  997. newState = !this->m_context.settings.mouse.disableparallax;
  998. } else if (value == "on" || value == "1" || value == "true") {
  999. newState = true;
  1000. } else if (value == "off" || value == "0" || value == "false") {
  1001. newState = false;
  1002. } else {
  1003. sLog.error ("Hotswap: ignoring invalid disable-parallax value: ", value);
  1004. return;
  1005. }
  1006. this->m_context.settings.mouse.disableparallax = newState;
  1007. sLog.out ("Hotswap: parallax ", newState ? "force-disabled" : "enabled", " live");
  1008. }
  1009. void WallpaperApplication::applyCornerColorHotswap (const std::string& value) {
  1010. const auto color = WallpaperEngine::Render::CFBO::parseColor (value);
  1011. if (!color.has_value ()) {
  1012. sLog.error ("Hotswap: ignoring invalid corner color: ", value);
  1013. return;
  1014. }
  1015. this->m_context.settings.render.window.cornerColor = *color;
  1016. for (auto& [screen, screenColor] : this->m_context.settings.general.screenCornerColors) {
  1017. screenColor = *color;
  1018. }
  1019. for (auto& spanGroup : this->m_context.settings.general.spanGroups) {
  1020. spanGroup.cornerColor = *color;
  1021. }
  1022. if (this->m_renderContext) {
  1023. for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
  1024. wallpaper->setCornerColor (*color);
  1025. }
  1026. }
  1027. sLog.out ("Hotswap: applied corner color ", value, " live");
  1028. }
  1029. void WallpaperApplication::applySpeedHotswap (const std::string& value) {
  1030. float speed;
  1031. try {
  1032. speed = std::stof (value);
  1033. } catch (const std::exception&) {
  1034. sLog.error ("Hotswap: ignoring invalid speed value: ", value);
  1035. return;
  1036. }
  1037. if (speed <= 0.0f) {
  1038. sLog.error ("Hotswap: ignoring non-positive speed value: ", value);
  1039. return;
  1040. }
  1041. this->m_context.settings.render.playbackSpeed = speed;
  1042. // Particles/effects/scripts read settings.render.playbackSpeed directly every frame, but
  1043. // video wallpapers are driven by mpv's own clock and need the change pushed explicitly.
  1044. if (this->m_renderContext) {
  1045. for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
  1046. if (wallpaper->is<WallpaperEngine::Render::Wallpapers::CVideo> ()) {
  1047. wallpaper->as<WallpaperEngine::Render::Wallpapers::CVideo> ()->setSpeed (speed);
  1048. }
  1049. }
  1050. }
  1051. sLog.out ("Hotswap: applied speed ", speed, " live");
  1052. }
  1053. void WallpaperApplication::applyAudioScreenHotswap (const std::string& value) {
  1054. this->m_context.settings.audio.audioScreen = value.empty () ? std::nullopt : std::optional<std::string> (value);
  1055. this->applyAudioPolicy ();
  1056. sLog.out ("Hotswap: applied audio screen '", value, "' live");
  1057. }
  1058. void WallpaperApplication::applyAmbientVolumeHotswap (const std::string& value) {
  1059. int volume;
  1060. try {
  1061. volume = std::stoi (value);
  1062. } catch (const std::exception&) {
  1063. sLog.error ("Hotswap: ignoring invalid ambient volume value: ", value);
  1064. return;
  1065. }
  1066. this->m_context.settings.audio.ambientVolume = std::max (0, std::min (volume, 128));
  1067. this->applyAudioPolicy ();
  1068. sLog.out ("Hotswap: applied ambient volume ", volume, " live");
  1069. }
  1070. void WallpaperApplication::applyAudioPolicy () {
  1071. if (!this->m_renderContext) {
  1072. return;
  1073. }
  1074. const auto& audioScreen = this->m_context.settings.audio.audioScreen;
  1075. const auto& ambientVolume = this->m_context.settings.audio.ambientVolume;
  1076. std::map<WallpaperEngine::Render::CWallpaper*, bool> wantMuted;
  1077. for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
  1078. const bool screenMuted = audioScreen.has_value () && screen != *audioScreen;
  1079. auto [it, inserted] = wantMuted.try_emplace (wallpaper.get (), screenMuted);
  1080. if (!inserted) {
  1081. it->second = it->second && screenMuted;
  1082. }
  1083. }
  1084. for (const auto& [wallpaper, muted] : wantMuted) {
  1085. wallpaper->setAudioPolicy (muted, ambientVolume);
  1086. }
  1087. }
  1088. void WallpaperApplication::setupPropertiesForProject (const Project& project) {
  1089. for (const auto& [key, cur] : project.properties) {
  1090. auto override = this->m_context.settings.general.properties.find (key);
  1091. if (override != this->m_context.settings.general.properties.end ()) {
  1092. sLog.out ("Applying override value for ", key);
  1093. cur->update (override->second, DynamicValue::UpdateSource::User);
  1094. }
  1095. if (this->m_context.settings.general.onlyListProperties) {
  1096. sLog.out (cur->dump ());
  1097. }
  1098. }
  1099. }
  1100. void WallpaperApplication::setupProperties () {
  1101. for (const auto& [background, info] : this->m_backgrounds) {
  1102. this->setupPropertiesForProject (*info);
  1103. }
  1104. }
  1105. void WallpaperApplication::listObjectsForProject (const std::string& background, const Project& project) const {
  1106. if (!project.wallpaper->is<Scene> ()) {
  1107. return;
  1108. }
  1109. const auto scene = project.wallpaper->as<Scene> ();
  1110. sLog.out ("Objects for ", background, ":");
  1111. for (const auto& object : scene->objects) {
  1112. std::string type = "unknown";
  1113. if (object->is<Image> ()) {
  1114. type = "image";
  1115. } else if (object->is<Particle> ()) {
  1116. type = "particle";
  1117. } else if (object->is<Text> ()) {
  1118. type = "text";
  1119. } else if (object->is<Sound> ()) {
  1120. type = "sound";
  1121. }
  1122. sLog.out (" ", object->id, " - ", object->name, " (", type, ")");
  1123. }
  1124. }
  1125. void WallpaperApplication::listObjects () const {
  1126. if (!this->m_context.settings.general.onlyListObjects) {
  1127. return;
  1128. }
  1129. for (const auto& [background, info] : this->m_backgrounds) {
  1130. this->listObjectsForProject (background, *info);
  1131. }
  1132. }
  1133. void WallpaperApplication::listEffectsForProject (const std::string& background, const Project& project) const {
  1134. if (!project.wallpaper->is<Scene> ()) {
  1135. return;
  1136. }
  1137. const auto scene = project.wallpaper->as<Scene> ();
  1138. sLog.out ("Effects for ", background, ":");
  1139. for (const auto& object : scene->objects) {
  1140. if (!object->is<Image> ()) {
  1141. continue;
  1142. }
  1143. const auto* image = object->as<Image> ();
  1144. for (const auto& effect : image->effects) {
  1145. sLog.out (
  1146. " ", effect->id, " - ", effect->name, " (object=", object->id, " objectName=\"", object->name,
  1147. "\" group=\"", effect->effect->group, "\")"
  1148. );
  1149. }
  1150. }
  1151. }
  1152. void WallpaperApplication::listEffects () const {
  1153. if (!this->m_context.settings.general.onlyListEffects) {
  1154. return;
  1155. }
  1156. for (const auto& [background, info] : this->m_backgrounds) {
  1157. this->listEffectsForProject (background, *info);
  1158. }
  1159. }
  1160. namespace {
  1161. struct AudioReactiveProperty {
  1162. std::string name;
  1163. DynamicValue* value;
  1164. };
  1165. void considerAudioReactive (
  1166. std::vector<AudioReactiveProperty>& result, const std::string& name, const UserSettingUniquePtr& setting
  1167. ) {
  1168. if (!setting || !setting->value) {
  1169. return;
  1170. }
  1171. const auto& source = setting->value->getScriptSource ();
  1172. if (source.has_value () && source->find ("registerAudioBuffers") != std::string::npos) {
  1173. result.push_back ({ name, setting->value.get () });
  1174. }
  1175. }
  1176. // Mirrors exactly the set of fields ScriptableObject/CImage/CText/CParticle register with the
  1177. // script engine (see Scripting/ScriptableObject.cpp, Render/Objects/CImage.cpp, CText.cpp,
  1178. // CParticle.cpp) - "origin" is always the base object's own field (never overridden per-type),
  1179. // while scale/angles/visible fall back to the generic group* fields only for object types that
  1180. // don't provide their own (Sound, Light, plain groups).
  1181. std::vector<AudioReactiveProperty> collectAudioReactiveProperties (const Object& object) {
  1182. std::vector<AudioReactiveProperty> result;
  1183. considerAudioReactive (result, "origin", object.origin);
  1184. if (object.is<Image> ()) {
  1185. const auto* image = object.as<Image> ();
  1186. considerAudioReactive (result, "scale", image->scale);
  1187. considerAudioReactive (result, "angles", image->angles);
  1188. considerAudioReactive (result, "visible", image->visible);
  1189. considerAudioReactive (result, "alpha", image->alpha);
  1190. considerAudioReactive (result, "color", image->color);
  1191. considerAudioReactive (result, "parallaxDepth", image->parallaxDepth);
  1192. } else if (object.is<Text> ()) {
  1193. const auto* text = object.as<Text> ();
  1194. considerAudioReactive (result, "scale", text->scale);
  1195. considerAudioReactive (result, "color", text->color);
  1196. considerAudioReactive (result, "alpha", text->alpha);
  1197. considerAudioReactive (result, "visible", text->visible);
  1198. considerAudioReactive (result, "pointSize", text->pointSize);
  1199. considerAudioReactive (result, "text", text->text);
  1200. considerAudioReactive (result, "parallaxDepth", text->parallaxDepth);
  1201. } else if (object.is<Particle> ()) {
  1202. const auto* particle = object.as<Particle> ();
  1203. considerAudioReactive (result, "scale", particle->scale);
  1204. considerAudioReactive (result, "angles", particle->angles);
  1205. considerAudioReactive (result, "visible", particle->visible);
  1206. considerAudioReactive (result, "parallaxDepth", particle->parallaxDepth);
  1207. } else {
  1208. considerAudioReactive (result, "scale", object.groupScale);
  1209. considerAudioReactive (result, "angles", object.groupAngles);
  1210. considerAudioReactive (result, "visible", object.groupVisible);
  1211. }
  1212. return result;
  1213. }
  1214. float readScriptPropertyFloat (DynamicValue& value, const std::string& key) {
  1215. auto& properties = value.getProperties ();
  1216. const auto it = properties.find (key);
  1217. return it != properties.end () && it->second && it->second->value ? it->second->value->getFloat () : 0.0f;
  1218. }
  1219. } // namespace
  1220. void WallpaperApplication::listAudioObjectsForProject (const std::string& background, const Project& project) const {
  1221. if (!project.wallpaper->is<Scene> ()) {
  1222. return;
  1223. }
  1224. const auto scene = project.wallpaper->as<Scene> ();
  1225. sLog.out ("Audio-reactive objects for ", background, ":");
  1226. for (const auto& object : scene->objects) {
  1227. for (const auto& reactive : collectAudioReactiveProperties (*object)) {
  1228. sLog.out (
  1229. " ", object->id, " - ", object->name, " (", reactive.name, "): minvalue=",
  1230. readScriptPropertyFloat (*reactive.value, "minvalue"), " maxvalue=",
  1231. readScriptPropertyFloat (*reactive.value, "maxvalue"), " frequency=",
  1232. readScriptPropertyFloat (*reactive.value, "frequency"), " smoothing=",
  1233. readScriptPropertyFloat (*reactive.value, "smoothing")
  1234. );
  1235. }
  1236. }
  1237. }
  1238. void WallpaperApplication::listAudioObjects () const {
  1239. if (!this->m_context.settings.general.onlyListAudioObjects) {
  1240. return;
  1241. }
  1242. for (const auto& [background, info] : this->m_backgrounds) {
  1243. this->listAudioObjectsForProject (background, *info);
  1244. }
  1245. }
  1246. void WallpaperApplication::setupAudioSensitivityForProject (const Project& project) const {
  1247. if (!project.wallpaper->is<Scene> ()) {
  1248. return;
  1249. }
  1250. const auto scene = project.wallpaper->as<Scene> ();
  1251. for (const auto& object : scene->objects) {
  1252. const auto sensitivity = this->m_context.resolveAudioSensitivity (object->id, object->name);
  1253. if (!sensitivity.has_value ()) {
  1254. continue;
  1255. }
  1256. for (const auto& reactive : collectAudioReactiveProperties (*object)) {
  1257. auto& scriptProps = reactive.value->getProperties ();
  1258. const auto minIt = scriptProps.find ("minvalue");
  1259. const auto maxIt = scriptProps.find ("maxvalue");
  1260. if (minIt == scriptProps.end () || maxIt == scriptProps.end () || !minIt->second->value
  1261. || !maxIt->second->value) {
  1262. continue;
  1263. }
  1264. const auto [newMin, newMax] = WallpaperEngine::Data::Utils::scaleAudioRange (
  1265. minIt->second->value->getFloat (), maxIt->second->value->getFloat (), sensitivity.value ()
  1266. );
  1267. minIt->second->value->update (newMin, DynamicValue::UpdateSource::User);
  1268. maxIt->second->value->update (newMax, DynamicValue::UpdateSource::User);
  1269. sLog.debug (
  1270. "Applying audio sensitivity ", sensitivity.value (), " to ", object->id, " - ", object->name, " (",
  1271. reactive.name, ")"
  1272. );
  1273. }
  1274. }
  1275. }
  1276. void WallpaperApplication::setupAudioSensitivity () {
  1277. for (const auto& [background, info] : this->m_backgrounds) {
  1278. this->setupAudioSensitivityForProject (*info);
  1279. }
  1280. }
  1281. void WallpaperApplication::setupSoundVolumeForProject (const Project& project) const {
  1282. if (!project.wallpaper->is<Scene> ()) {
  1283. return;
  1284. }
  1285. const auto scene = project.wallpaper->as<Scene> ();
  1286. for (const auto& object : scene->objects) {
  1287. if (!object->is<Sound> ()) {
  1288. continue;
  1289. }
  1290. const auto* sound = object->as<Sound> ();
  1291. const auto volume = this->m_context.resolveSoundVolume (object->id, object->name);
  1292. if (!volume.has_value () || !sound->volume || !sound->volume->value) {
  1293. continue;
  1294. }
  1295. sound->volume->value->update (std::clamp (volume.value (), 0.0f, 1.0f), DynamicValue::UpdateSource::User);
  1296. sLog.debug ("Applying sound volume ", volume.value (), " to ", object->id, " - ", object->name);
  1297. }
  1298. }
  1299. void WallpaperApplication::setupSoundVolume () {
  1300. for (const auto& [background, info] : this->m_backgrounds) {
  1301. this->setupSoundVolumeForProject (*info);
  1302. }
  1303. }
  1304. void WallpaperApplication::applySoundVolumeHotswap (const std::map<std::string, std::string>& targets) {
  1305. for (const auto& [target, value] : targets) {
  1306. try {
  1307. this->m_context.settings.general.soundVolume[target] = std::stof (value);
  1308. } catch (const std::exception&) {
  1309. sLog.error ("Hotswap: ignoring invalid sound-volume value: ", value);
  1310. }
  1311. }
  1312. // Sound objects already read their own DynamicValue live every frame (see
  1313. // CSound::applyEffectiveVolume), so re-resolving and pushing the new value straight into the
  1314. // currently loaded projects' live objects is enough - no reload needed, unlike properties/
  1315. // audio-sensitivity which are baked into the scene graph at parse time.
  1316. for (const auto& [background, info] : this->m_backgrounds) {
  1317. this->setupSoundVolumeForProject (*info);
  1318. }
  1319. sLog.out ("Hotswap: applied sound volume live");
  1320. }
  1321. void WallpaperApplication::setupBrowser () {
  1322. // The main engine process never hosts CEF directly - CEF only supports one
  1323. // CefInitialize()/CefShutdown() pair per process, so a process that might later need to stop
  1324. // and restart hosting a web wallpaper can't safely do it in place. Only two roles reach here:
  1325. // CEF's own subprocess re-execs (--type=zygote/gpu-process/renderer/utility), which must
  1326. // always reach WebBrowserContext even if loadBackgrounds() found nothing to load, and this
  1327. // process's own --web-host role.
  1328. if ((!this->isCefSubprocess () && !this->m_context.settings.general.webHost) || this->m_browserContext) {
  1329. return;
  1330. }
  1331. this->m_browserContext = std::make_unique<WebBrowser::WebBrowserContext> (*this);
  1332. }
  1333. void WallpaperApplication::runWebHost () {
  1334. using namespace WallpaperEngine::WebBrowser;
  1335. using namespace WallpaperEngine::WebBrowser::IPC;
  1336. const auto backgroundIt = this->m_backgrounds.find ("default");
  1337. if (backgroundIt == this->m_backgrounds.end () || !backgroundIt->second->wallpaper->is<Web> ()) {
  1338. sLog.error ("--web-host requires a single Web wallpaper background, none found");
  1339. return;
  1340. }
  1341. if (!this->m_browserContext) {
  1342. sLog.error ("--web-host: CEF was not initialized (setupBrowser() found no Web project?)");
  1343. return;
  1344. }
  1345. const Project& project = *backgroundIt->second;
  1346. const Web& web = *project.wallpaper->as<Web> ();
  1347. const int width = static_cast<int> (this->m_context.settings.general.webHostWidth);
  1348. const int height = static_cast<int> (this->m_context.settings.general.webHostHeight);
  1349. if (width <= 0 || height <= 0) {
  1350. sLog.error ("--web-host: invalid --web-host-width/--web-host-height");
  1351. return;
  1352. }
  1353. auto* shm = attachSharedMemory (this->m_context.settings.general.webHostShm, width, height);
  1354. if (shm == nullptr) {
  1355. sLog.error ("--web-host: failed to attach shared memory ", this->m_context.settings.general.webHostShm);
  1356. return;
  1357. }
  1358. CefWindowInfo windowInfo;
  1359. windowInfo.SetAsWindowless (0);
  1360. // frames are produced when the main process asks for them (see the loop below), so they line up with what it
  1361. // presents instead of drifting against a free running 60Hz timer
  1362. windowInfo.external_begin_frame_enabled = true;
  1363. CefBrowserSettings browserSettings;
  1364. // only a ceiling while external begin frames are on, the main process decides when a frame is produced
  1365. browserSettings.windowless_frame_rate = std::max (60, this->m_context.settings.render.maximumFPS);
  1366. const CefRefPtr<CEF::SharedMemoryRenderHandler> renderHandler = new CEF::SharedMemoryRenderHandler (shm);
  1367. const CefRefPtr<CEF::BrowserClient> client = new CEF::BrowserClient (renderHandler, project.properties);
  1368. // the "w" prefix + workshop id host must match what the scheme handler factory expects to resolve
  1369. const std::string htmlURL = std::string (WPENGINE_SCHEME) + "://w" + project.workshopId + "/" + web.filename;
  1370. sLog.out ("--web-host: creating browser for ", htmlURL, " (pid ", static_cast<long> (getpid ()), ")");
  1371. const CefRefPtr<CefBrowser> browser
  1372. = CefBrowserHost::CreateBrowserSync (windowInfo, client, htmlURL, browserSettings, nullptr, nullptr);
  1373. if (!browser) {
  1374. sLog.error ("--web-host: failed to create the CEF browser");
  1375. shm->helperFailed.store (true, std::memory_order_release);
  1376. closeSharedMemory (shm, this->m_context.settings.general.webHostShm, width, height, false);
  1377. return;
  1378. }
  1379. // a windowless browser is treated as backgrounded by default, which freezes the page's own timers and rAF
  1380. browser->GetHost ()->WasHidden (false);
  1381. shm->helperReady.store (true, std::memory_order_release);
  1382. Input::MouseClickStatus lastLeft = Input::Released;
  1383. Input::MouseClickStatus lastRight = Input::Released;
  1384. uint32_t lastFrameRequest = shm->frameRequestSeq.load (std::memory_order_acquire);
  1385. auto lastBeginFrame = std::chrono::steady_clock::now ();
  1386. int lastMouseX = -1;
  1387. int lastMouseY = -1;
  1388. uint32_t lastDesiredWidth = shm->desiredWidth.load (std::memory_order_relaxed);
  1389. uint32_t lastDesiredHeight = shm->desiredHeight.load (std::memory_order_relaxed);
  1390. bool lastAudioMuted = shm->audioMuted.load (std::memory_order_relaxed);
  1391. browser->GetHost ()->SetAudioMuted (lastAudioMuted);
  1392. CEF::PageBridge bridge (browser, *shm, project.properties, *client);
  1393. while (!shm->quitRequested.load (std::memory_order_acquire)) {
  1394. // The heartbeat keeps the page alive (timers, videos, first paint) when the main process isn't rendering, for
  1395. // example while the surface is fully covered and the compositor stops sending frame callbacks.
  1396. const auto now = std::chrono::steady_clock::now ();
  1397. const uint32_t frameRequest = shm->frameRequestSeq.load (std::memory_order_acquire);
  1398. if (frameRequest != lastFrameRequest || now - lastBeginFrame > std::chrono::milliseconds (100)) {
  1399. lastFrameRequest = frameRequest;
  1400. lastBeginFrame = now;
  1401. browser->GetHost ()->SendExternalBeginFrame ();
  1402. }
  1403. CefDoMessageLoopWork ();
  1404. const uint32_t desiredWidth = shm->desiredWidth.load (std::memory_order_relaxed);
  1405. const uint32_t desiredHeight = shm->desiredHeight.load (std::memory_order_relaxed);
  1406. if (desiredWidth != lastDesiredWidth || desiredHeight != lastDesiredHeight) {
  1407. lastDesiredWidth = desiredWidth;
  1408. lastDesiredHeight = desiredHeight;
  1409. browser->GetHost ()->WasResized ();
  1410. }
  1411. const bool desiredAudioMuted = shm->audioMuted.load (std::memory_order_relaxed);
  1412. if (desiredAudioMuted != lastAudioMuted) {
  1413. lastAudioMuted = desiredAudioMuted;
  1414. browser->GetHost ()->SetAudioMuted (lastAudioMuted);
  1415. }
  1416. bridge.update ();
  1417. CefMouseEvent evt;
  1418. evt.x = static_cast<int> (shm->mouseX.load (std::memory_order_relaxed));
  1419. evt.y = static_cast<int> (shm->mouseY.load (std::memory_order_relaxed));
  1420. // every event wakes the page's input handling, so only tell it when the pointer actually moved
  1421. if (evt.x != lastMouseX || evt.y != lastMouseY) {
  1422. lastMouseX = evt.x;
  1423. lastMouseY = evt.y;
  1424. browser->GetHost ()->SendMouseMoveEvent (evt, false);
  1425. }
  1426. const auto left = static_cast<Input::MouseClickStatus> (shm->leftClick.load (std::memory_order_relaxed));
  1427. const auto right = static_cast<Input::MouseClickStatus> (shm->rightClick.load (std::memory_order_relaxed));
  1428. if (left != lastLeft) {
  1429. browser->GetHost ()->SendMouseClickEvent (
  1430. evt, CefBrowserHost::MouseButtonType::MBT_LEFT, left == Input::Released, 1
  1431. );
  1432. lastLeft = left;
  1433. }
  1434. if (right != lastRight) {
  1435. browser->GetHost ()->SendMouseClickEvent (
  1436. evt, CefBrowserHost::MouseButtonType::MBT_RIGHT, right == Input::Released, 1
  1437. );
  1438. lastRight = right;
  1439. }
  1440. // Short, this is also how often input and audio get a chance to be handed to the page and how soon a painted
  1441. // frame gets picked up. A frame request from the main process cuts the wait short.
  1442. shm->waitForFrameRequest (lastFrameRequest, 1);
  1443. }
  1444. browser->GetHost ()->CloseBrowser (true);
  1445. // CloseBrowser() only requests an async close - wait briefly for it to finish before
  1446. // CefShutdown() below tears everything down out from under it
  1447. for (int i = 0; i < 500 && !client->isClosed (); i++) {
  1448. CefDoMessageLoopWork ();
  1449. }
  1450. closeSharedMemory (shm, this->m_context.settings.general.webHostShm, width, height, false);
  1451. }
  1452. void WallpaperApplication::takeScreenshot (const std::filesystem::path& filename) const {
  1453. const int width = this->m_renderContext->getOutput ().getFullWidth ();
  1454. const int height = this->m_renderContext->getOutput ().getFullHeight ();
  1455. const bool vflip = this->m_renderContext->getOutput ().renderVFlip ();
  1456. const auto& wallpapers = this->m_renderContext->getWallpapers ();
  1457. struct ViewportCapture {
  1458. uint8_t* buffer;
  1459. int readWidth;
  1460. int readHeight;
  1461. int vpWidth;
  1462. int vpHeight;
  1463. int xoffset;
  1464. float ustart, uend, vstart, vend;
  1465. };
  1466. std::vector<ViewportCapture> captures;
  1467. int currentXOffset = 0;
  1468. for (const auto& [screen, viewport] : this->m_renderContext->getOutput ().getViewports ()) {
  1469. viewport->makeCurrent ();
  1470. const auto wallpaperIt = wallpapers.find (screen);
  1471. if (wallpaperIt == wallpapers.end ()) {
  1472. sLog.error ("Cannot find wallpaper for screen ", screen);
  1473. continue;
  1474. }
  1475. const auto& wallpaper = wallpaperIt->second;
  1476. const int vpWidth = viewport->viewport.z - viewport->viewport.x;
  1477. const int vpHeight = viewport->viewport.w - viewport->viewport.y;
  1478. // bind the wallpaper's FBO to read from it directly
  1479. // this is more reliable than the default framebuffer on some drivers (NVIDIA/Wayland)
  1480. glBindFramebuffer (GL_FRAMEBUFFER, wallpaper->getWallpaperFramebuffer ());
  1481. glFinish ();
  1482. const int readWidth = wallpaper->getCanvasWidth ();
  1483. const int readHeight = wallpaper->getCanvasHeight ();
  1484. const auto bufferSize = readWidth * readHeight * 3;
  1485. auto* buffer = new uint8_t[bufferSize];
  1486. glPixelStorei (GL_PACK_ALIGNMENT, 1);
  1487. if (GLEW_VERSION_4_5) {
  1488. glReadnPixels (0, 0, readWidth, readHeight, GL_RGB, GL_UNSIGNED_BYTE, bufferSize, buffer);
  1489. } else {
  1490. glReadPixels (0, 0, readWidth, readHeight, GL_RGB, GL_UNSIGNED_BYTE, buffer);
  1491. }
  1492. glBindFramebuffer (GL_FRAMEBUFFER, 0);
  1493. if (const GLenum error = glGetError (); error != GL_NO_ERROR) {
  1494. sLog.error ("Cannot obtain pixel data for screen ", screen, ". OpenGL error: ", error);
  1495. delete[] buffer;
  1496. continue;
  1497. }
  1498. // Get the UV coordinates which define the visible portion based on scaling mode
  1499. const auto [ustart, uend, vstart, vend] = wallpaper->getState ().getTextureUVs ();
  1500. captures.push_back (
  1501. { buffer, readWidth, readHeight, vpWidth, vpHeight, currentXOffset, ustart, uend, vstart, vend }
  1502. );
  1503. if (viewport->single) {
  1504. currentXOffset += vpWidth;
  1505. }
  1506. }
  1507. const auto extension = filename.extension ();
  1508. const std::string extStr = extension.string ();
  1509. // Offload pixel processing and saving to a background thread to avoid hitches
  1510. std::thread ([captures, width, height, vflip, extStr, filename] () {
  1511. auto* bitmap = new uint8_t[width * height * 3] { 0 };
  1512. for (const auto& capture : captures) {
  1513. // sample the bitmap from the UV-defined visible region
  1514. for (int y = 0; y < capture.vpHeight; y++) {
  1515. for (int x = 0; x < capture.vpWidth; x++) {
  1516. const float u
  1517. = capture.ustart + (static_cast<float> (x) / capture.vpWidth) * (capture.uend - capture.ustart);
  1518. const float v = capture.vstart
  1519. + (static_cast<float> (y) / capture.vpHeight) * (capture.vend - capture.vstart);
  1520. const int srcX = std::clamp (static_cast<int> (u * capture.readWidth), 0, capture.readWidth - 1);
  1521. const int srcY = std::clamp (static_cast<int> (v * capture.readHeight), 0, capture.readHeight - 1);
  1522. const int srcIdx = (srcY * capture.readWidth + srcX) * 3;
  1523. const int xfinal = x + capture.xoffset;
  1524. // FBO content is not flipped like default framebuffer, so invert vflip logic
  1525. const int yfinal = vflip ? y : (capture.vpHeight - y - 1);
  1526. if (yfinal >= 0 && yfinal < height && xfinal >= 0 && xfinal < width) {
  1527. bitmap[yfinal * width * 3 + xfinal * 3] = capture.buffer[srcIdx];
  1528. bitmap[yfinal * width * 3 + xfinal * 3 + 1] = capture.buffer[srcIdx + 1];
  1529. bitmap[yfinal * width * 3 + xfinal * 3 + 2] = capture.buffer[srcIdx + 2];
  1530. }
  1531. }
  1532. }
  1533. delete[] capture.buffer;
  1534. }
  1535. if (extStr == ".bmp") {
  1536. stbi_write_bmp (filename.c_str (), width, height, 3, bitmap);
  1537. } else if (extStr == ".png") {
  1538. stbi_write_png (filename.c_str (), width, height, 3, bitmap, width * 3);
  1539. } else if (extStr == ".jpg" || extStr == ".jpeg") {
  1540. stbi_write_jpg (filename.c_str (), width, height, 3, bitmap, 100);
  1541. }
  1542. delete[] bitmap;
  1543. }).detach ();
  1544. }
  1545. void WallpaperApplication::setupOutput () {
  1546. const char* XDG_SESSION_TYPE = getenv ("XDG_SESSION_TYPE");
  1547. if (!XDG_SESSION_TYPE) {
  1548. sLog.exception (
  1549. "Cannot read environment variable XDG_SESSION_TYPE, window server detection failed. Please ensure proper "
  1550. "values are set"
  1551. );
  1552. }
  1553. sLog.debug ("Checking for window servers: ");
  1554. for (const auto& windowServer : sVideoFactories.getRegisteredDrivers ()) {
  1555. sLog.debug ("\t", windowServer);
  1556. }
  1557. this->m_videoDriver = sVideoFactories.createVideoDriver (
  1558. this->m_context.settings.render.mode, XDG_SESSION_TYPE, this->m_context, *this
  1559. );
  1560. this->m_fullScreenDetector
  1561. = sVideoFactories.createFullscreenDetector (XDG_SESSION_TYPE, this->m_context, *this->m_videoDriver);
  1562. }
  1563. void WallpaperApplication::setupAudio () {
  1564. const bool audioProcessingRequired = std::ranges::any_of (
  1565. this->m_backgrounds, [] (const std::pair<const std::string, ProjectUniquePtr>& pair) -> bool {
  1566. return pair.second->supportsAudioProcessing;
  1567. }
  1568. );
  1569. if (audioProcessingRequired && this->m_context.settings.audio.audioprocessing) {
  1570. this->m_audioRecorder
  1571. = std::make_unique<WallpaperEngine::Audio::Drivers::Recorders::PulseAudioPlaybackRecorder> ();
  1572. } else {
  1573. this->m_audioRecorder = std::make_unique<WallpaperEngine::Audio::Drivers::Recorders::PlaybackRecorder> ();
  1574. }
  1575. if (this->m_context.settings.audio.automute) {
  1576. m_audioDetector = std::make_unique<WallpaperEngine::Audio::Drivers::Detectors::PulseAudioPlayingDetector> (
  1577. this->m_context, *this->m_fullScreenDetector
  1578. );
  1579. } else {
  1580. m_audioDetector = std::make_unique<WallpaperEngine::Audio::Drivers::Detectors::AudioPlayingDetector> (
  1581. this->m_context, *this->m_fullScreenDetector
  1582. );
  1583. }
  1584. m_audioDriver = std::make_unique<WallpaperEngine::Audio::Drivers::SDLAudioDriver> (
  1585. this->m_context, *this->m_audioDetector, *this->m_audioRecorder
  1586. );
  1587. m_audioContext = std::make_unique<WallpaperEngine::Audio::AudioContext> (*m_audioDriver);
  1588. }
  1589. void WallpaperApplication::prepareOutputs () {
  1590. m_renderContext
  1591. = std::make_unique<WallpaperEngine::Render::RenderContext> (*m_videoDriver, *this, *this->m_mediaSource);
  1592. // set all the specific wallpapers required (skip span group synthetic keys)
  1593. for (const auto& [background, info] : this->m_backgrounds) {
  1594. if (background.rfind ("span:", 0) == 0) {
  1595. continue;
  1596. }
  1597. const auto scalingIt = this->m_context.settings.general.screenScalings.find (background);
  1598. const auto clampIt = this->m_context.settings.general.screenClamps.find (background);
  1599. const auto scaling = scalingIt != this->m_context.settings.general.screenScalings.end ()
  1600. ? scalingIt->second
  1601. : this->m_context.settings.render.window.scalingMode;
  1602. const auto clamp = clampIt != this->m_context.settings.general.screenClamps.end ()
  1603. ? clampIt->second
  1604. : this->m_context.settings.render.window.clamp;
  1605. auto wallpaper = WallpaperEngine::Render::CWallpaper::fromWallpaper (
  1606. *info->wallpaper, *m_renderContext, *m_audioContext, this->resolveScreenBackgroundPath (background),
  1607. scaling, clamp, this->resolveScreenRenderSize (background)
  1608. );
  1609. wallpaper->setZoom (this->resolveScreenZoom (background));
  1610. const auto offset = this->resolveScreenOffset (background);
  1611. wallpaper->setOffset (offset.x, offset.y);
  1612. wallpaper->setCornerColor (this->resolveScreenCornerColor (background));
  1613. m_renderContext->setWallpaper (background, std::move (wallpaper));
  1614. }
  1615. // Set up span groups: one shared wallpaper per group, registered for each viewport
  1616. for (const auto& spanGroup : this->m_context.settings.general.spanGroups) {
  1617. if (spanGroup.screens.empty ()) {
  1618. continue;
  1619. }
  1620. const std::string groupKey = "span:" + spanGroup.screens.front ();
  1621. const auto bgIt = this->m_backgrounds.find (groupKey);
  1622. if (bgIt == this->m_backgrounds.end ()) {
  1623. continue;
  1624. }
  1625. // bounding box of all viewports in this span group
  1626. const auto& viewports = m_renderContext->getOutput ().getViewports ();
  1627. int minX = INT_MAX, minY = INT_MAX, maxX = INT_MIN, maxY = INT_MIN;
  1628. bool anyFound = false;
  1629. for (const auto& screenName : spanGroup.screens) {
  1630. const auto vpIt = viewports.find (screenName);
  1631. if (vpIt == viewports.end ()) {
  1632. sLog.error ("Span group screen not found: ", screenName);
  1633. continue;
  1634. }
  1635. anyFound = true;
  1636. const auto& vp = vpIt->second;
  1637. const int x = vp->globalPosition.x;
  1638. const int y = vp->globalPosition.y;
  1639. const int w = vp->logicalSize.x;
  1640. const int h = vp->logicalSize.y;
  1641. sLog.debug (
  1642. "SPAN DEBUG prepareOutputs: screen '", screenName, "' globalPos=(", x, ",", y, ") logicalSize=", w, "x",
  1643. h
  1644. );
  1645. minX = std::min (minX, x);
  1646. minY = std::min (minY, y);
  1647. maxX = std::max (maxX, x + w);
  1648. maxY = std::max (maxY, y + h);
  1649. }
  1650. if (!anyFound) {
  1651. sLog.error ("No viewports found for span group, skipping");
  1652. continue;
  1653. }
  1654. sLog.debug (
  1655. "SPAN DEBUG prepareOutputs: bounding box=(", minX, ",", minY, ",", maxX - minX, ",", maxY - minY, ")"
  1656. );
  1657. WallpaperEngine::Render::CWallpaper::SpanInfo spanInfo;
  1658. spanInfo.totalBounds = { minX, minY, maxX - minX, maxY - minY };
  1659. auto sharedWallpaper = WallpaperEngine::Render::CWallpaper::fromWallpaper (
  1660. *bgIt->second->wallpaper, *m_renderContext, *m_audioContext, this->resolveScreenBackgroundPath (groupKey),
  1661. spanGroup.scaling, spanGroup.clamp, glm::ivec2 { maxX - minX, maxY - minY }
  1662. );
  1663. sharedWallpaper->setZoom (spanGroup.zoom);
  1664. sharedWallpaper->setOffset (spanGroup.offset.x, spanGroup.offset.y);
  1665. sharedWallpaper->setCornerColor (spanGroup.cornerColor);
  1666. // Convert to shared_ptr so it can be registered for multiple viewports
  1667. std::shared_ptr<WallpaperEngine::Render::CWallpaper> shared (std::move (sharedWallpaper));
  1668. shared->setSpanInfo (spanInfo);
  1669. for (const auto& screenName : spanGroup.screens) {
  1670. m_renderContext->setWallpaper (screenName, shared);
  1671. }
  1672. }
  1673. this->applyAudioPolicy ();
  1674. }
  1675. void WallpaperApplication::setupOpenGLDebugging () {
  1676. // Not gated behind NDEBUG: GL_DEBUG_OUTPUT has near-zero cost when nothing goes wrong, and a
  1677. // Release build silently swallowing driver errors is exactly what's masking the in-process
  1678. // reload corruption bug - see CustomGLDebugCallback() above and the "layer toggle blackens the
  1679. // wallpaper" investigation.
  1680. glDebugMessageCallback (CustomGLDebugCallback, nullptr);
  1681. glEnable (GL_DEBUG_OUTPUT_SYNCHRONOUS);
  1682. }
  1683. void WallpaperApplication::setup () {
  1684. this->setupOutput ();
  1685. // we_manager launches us with LD_PRELOAD forcing the system's libEGL, because CEF's own
  1686. // bundled libEGL sits next to us on LD_LIBRARY_PATH and would otherwise shadow it and break
  1687. // our own Wayland/EGL output. That preload has already done its job for our own process by
  1688. // this point (setupOutput() above just created our GL context with it) - but it stays in the
  1689. // environment we inherit, and CEF's own subprocesses (spawned lazily, e.g. the GPU process
  1690. // the moment a web wallpaper's WebGL content first needs one) would inherit it too, forcing
  1691. // Mesa's system EGL onto CEF's ANGLE stack instead of the bundled EGL it actually expects.
  1692. // That silently breaks WebGL - no crash, no error, just a canvas that never paints anything.
  1693. // Unsetting it here only affects processes we fork+exec after this point; it doesn't undo
  1694. // anything already loaded into our own process by the dynamic linker at our own startup.
  1695. unsetenv ("LD_PRELOAD");
  1696. this->setupAudio ();
  1697. this->prepareOutputs ();
  1698. this->setupOpenGLDebugging ();
  1699. if (this->m_context.settings.general.dumpStructure) {
  1700. auto prettyPrinter = Data::Dumpers::StringPrinter ();
  1701. for (const auto& [background, info] : this->m_renderContext->getWallpapers ()) {
  1702. prettyPrinter.printWallpaper (info->getWallpaperData ());
  1703. }
  1704. std::cout << prettyPrinter.str () << std::endl;
  1705. }
  1706. #if DEMOMODE
  1707. // ensure only one background is running so everything can be properly caught
  1708. if (this->m_renderContext->getWallpapers ().size () > 1) {
  1709. sLog.exception ("Demo mode only supports one background");
  1710. }
  1711. int width = this->m_renderContext->getWallpapers ().begin ()->second->getCanvasWidth ();
  1712. int height = this->m_renderContext->getWallpapers ().begin ()->second->getCanvasHeight ();
  1713. std::vector<uint8_t> pixels (width * height * 3);
  1714. bool initialized = false;
  1715. int frame = 0;
  1716. #endif /* DEMOMODE */
  1717. }
  1718. void WallpaperApplication::render () {
  1719. static time_t seconds;
  1720. static struct tm* timeinfo;
  1721. static float rawTimeLast = 0.0f;
  1722. if (this->m_isPaused) {
  1723. usleep (FULLSCREEN_CHECK_WAIT_TIME);
  1724. if (this->m_fullScreenDetector->anythingFullscreen () && this->m_context.state.general.keepRunning) {
  1725. return;
  1726. }
  1727. m_renderContext->setPause (false);
  1728. // account for paused duration in playlist timers
  1729. const auto pausedNow = std::chrono::steady_clock::now ();
  1730. const auto pausedDuration = pausedNow - this->m_pauseStart;
  1731. for (auto& [_, playlist] : this->m_activePlaylists) {
  1732. if (!playlist.definition.settings.updateOnPause) {
  1733. playlist.nextSwitch += pausedDuration;
  1734. playlist.lastUpdate += pausedDuration;
  1735. }
  1736. }
  1737. this->m_isPaused = false;
  1738. } else {
  1739. time (&seconds);
  1740. timeinfo = localtime (&seconds);
  1741. g_Daytime = static_cast<float> ((timeinfo->tm_hour * 60) + timeinfo->tm_min) / (24.0f * 60.0f);
  1742. const float rawTimeNow = m_videoDriver->getRenderTime ();
  1743. const float rawDelta = rawTimeNow - rawTimeLast;
  1744. rawTimeLast = rawTimeNow;
  1745. g_TimeLast = g_Time;
  1746. if (!this->m_context.settings.render.freezeAnimations) {
  1747. g_Time += rawDelta * this->m_context.settings.render.playbackSpeed;
  1748. }
  1749. g_RealTime = rawTimeNow;
  1750. m_audioDriver->update ();
  1751. m_mediaSource->update ();
  1752. m_videoDriver->getInputContext ().update ();
  1753. m_videoDriver->dispatchEventQueue ();
  1754. if (m_videoDriver->closeRequested ()) {
  1755. sLog.out ("Stop requested by driver");
  1756. this->m_context.state.general.keepRunning = false;
  1757. }
  1758. #if DEMOMODE
  1759. // wait a full render cycle before starting, giving video/web decoders time to set up
  1760. if (m_videoDriver->getFrameCounter () > (uint32_t)this->m_context.settings.render.maximumFPS) {
  1761. if (!initialized) {
  1762. width = this->m_renderContext->getWallpapers ().begin ()->second->getCanvasWidth ();
  1763. height = this->m_renderContext->getWallpapers ().begin ()->second->getCanvasHeight ();
  1764. pixels.reserve (width * height * 3);
  1765. init_encoder ("output.webm", width, height);
  1766. initialized = true;
  1767. }
  1768. glBindFramebuffer (
  1769. GL_FRAMEBUFFER, this->m_renderContext->getWallpapers ().begin ()->second->getWallpaperFramebuffer ()
  1770. );
  1771. glPixelStorei (GL_PACK_ALIGNMENT, 1);
  1772. glReadPixels (0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels.data ());
  1773. write_video_frame (pixels.data ());
  1774. frame++;
  1775. if (frame >= FRAME_COUNT) {
  1776. this->m_context.state.general.keepRunning = false;
  1777. }
  1778. }
  1779. #endif /* DEMOMODE */
  1780. if (this->m_fullScreenDetector->anythingFullscreen () && this->m_context.state.general.keepRunning) {
  1781. this->m_isPaused = true;
  1782. this->m_pauseStart = std::chrono::steady_clock::now ();
  1783. m_renderContext->setPause (true);
  1784. return;
  1785. }
  1786. }
  1787. this->checkHotswapRequest ();
  1788. this->updatePlaylists ();
  1789. if (!this->m_context.settings.screenshot.take || this->m_screenShotTaken == true) {
  1790. return;
  1791. }
  1792. if (this->m_videoDriver->getFrameCounter () < this->m_nextFrameScreenshot) {
  1793. return;
  1794. }
  1795. this->takeScreenshot (this->m_context.settings.screenshot.path);
  1796. this->m_screenShotTaken = true;
  1797. }
  1798. void WallpaperApplication::cleanup () {
  1799. sLog.out ("Stopping");
  1800. #if DEMOMODE
  1801. close_encoder ();
  1802. #endif /* DEMOMODE */
  1803. SDL_Quit ();
  1804. }
  1805. void WallpaperApplication::show () {
  1806. setup ();
  1807. while (this->m_context.state.general.keepRunning) {
  1808. render ();
  1809. }
  1810. cleanup ();
  1811. }
  1812. void WallpaperApplication::update (Render::Drivers::Output::OutputViewport* viewport) {
  1813. m_renderContext->render (viewport);
  1814. }
  1815. void WallpaperApplication::signal (int signal) {
  1816. if (signal == SIGUSR1) {
  1817. // keep the handler itself trivial, the actual reload happens on the render thread
  1818. this->m_hotswapRequested = true;
  1819. return;
  1820. }
  1821. sLog.out ("Stop requested by signal ", signal);
  1822. this->m_context.state.general.keepRunning = false;
  1823. }
  1824. const std::map<std::string, ProjectUniquePtr>& WallpaperApplication::getBackgrounds () const {
  1825. return this->m_backgrounds;
  1826. }
  1827. ApplicationContext& WallpaperApplication::getContext () const { return this->m_context; }
  1828. const WallpaperEngine::Render::Drivers::Output::Output& WallpaperApplication::getOutput () const {
  1829. return this->m_renderContext->getOutput ();
  1830. }
  1831. void WallpaperApplication::setDestinationFramebuffer (GLuint framebuffer) {
  1832. this->m_destinationFramebuffer = framebuffer;
  1833. for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
  1834. wallpaper->setDestinationFramebuffer (framebuffer);
  1835. };
  1836. }
  1837. GLuint WallpaperApplication::getDestinationFramebuffer () const { return this->m_destinationFramebuffer; }