WallpaperApplication.cpp 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648
  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/Model/Property.h"
  15. #include "WallpaperEngine/Data/Model/Wallpaper.h"
  16. #include "WallpaperEngine/Debugging/CallStack.h"
  17. #include "WallpaperEngine/FileSystem/Adapters/MediaCover.h"
  18. #include "WallpaperEngine/Media/DBusMediaSource.h"
  19. #include "WallpaperEngine/WebBrowser/CEF/BrowserClient.h"
  20. #include "WallpaperEngine/WebBrowser/CEF/SharedMemoryRenderHandler.h"
  21. #include "WallpaperEngine/WebBrowser/IPC/WebHostSharedMemory.h"
  22. #include "include/cef_browser.h"
  23. #if DEMOMODE
  24. #include "recording.h"
  25. #endif /* DEMOMODE */
  26. #include <algorithm>
  27. #include <cctype>
  28. #include <climits>
  29. #include <csignal>
  30. #include <fstream>
  31. #include <numeric>
  32. #include <string_view>
  33. #include <unistd.h>
  34. #define STB_IMAGE_WRITE_IMPLEMENTATION
  35. #include <stb_image_write.h>
  36. #include <thread>
  37. #define FULLSCREEN_CHECK_WAIT_TIME 250
  38. float g_Time;
  39. float g_TimeLast;
  40. /** Unscaled wall-clock time, unaffected by --speed */
  41. float g_RealTime;
  42. float g_Daytime;
  43. using namespace WallpaperEngine::Assets;
  44. using namespace WallpaperEngine::Application;
  45. using namespace WallpaperEngine::Data::Model;
  46. using namespace WallpaperEngine::FileSystem;
  47. void CustomGLDebugCallback (
  48. GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam
  49. ) {
  50. if (severity != GL_DEBUG_SEVERITY_HIGH) {
  51. return;
  52. }
  53. sLog.error ("OpenGL error: ", message, ", type: ", type, ", id: ", id);
  54. std::vector<WallpaperEngine::Debugging::CallStack::CallInfo> callInfo;
  55. WallpaperEngine::Debugging::CallStack::GetCalls (callInfo);
  56. for (std::vector<WallpaperEngine::Debugging::CallStack::CallInfo>::size_type i = 0; i < callInfo.size (); ++i) {
  57. fprintf (
  58. stderr, "[%3lu] %15lu: %s in %s\n", callInfo.size () - i, callInfo[i].offset, callInfo[i].function.c_str (),
  59. callInfo[i].module.c_str ()
  60. );
  61. }
  62. }
  63. bool WallpaperApplication::isCefSubprocess () const {
  64. for (int i = 1; i < this->m_context.getArgc (); i++) {
  65. if (std::string_view (this->m_context.getArgv ()[i]).starts_with ("--type=")) {
  66. return true;
  67. }
  68. }
  69. return false;
  70. }
  71. WallpaperApplication::WallpaperApplication (ApplicationContext& context) : m_context (context) {
  72. this->initializeSubsystems ();
  73. try {
  74. this->loadBackgrounds ();
  75. } catch (const std::exception& e) {
  76. if (!this->isCefSubprocess ()) {
  77. throw;
  78. }
  79. // CEF re-execs this binary for its own subprocesses with a reconstructed argv that doesn't
  80. // always resolve to a loadable background. Must still reach setupBrowser() below regardless
  81. // - that's what calls CefExecuteProcess() to hand off into Chromium's subprocess entrypoint;
  82. // skipping it here leaves CEF's IPC handshake incomplete, which reads as a crashed subprocess
  83. // and gets retried, turning one web wallpaper into several stray processes.
  84. sLog.error ("Skipping background load for CEF subprocess re-exec: ", e.what ());
  85. }
  86. this->setupProperties ();
  87. this->listObjects ();
  88. this->setupBrowser ();
  89. this->initializePlaylists ();
  90. }
  91. void WallpaperApplication::initializeSubsystems () {
  92. m_mediaSource = std::make_unique<WallpaperEngine::Media::DBusMediaSource> (std::chrono::milliseconds (2000));
  93. }
  94. AssetLocatorUniquePtr WallpaperApplication::setupAssetLocator (const std::string& bg) const {
  95. auto container = std::make_unique<Container> ();
  96. const std::filesystem::path path = bg;
  97. container->registerAdapterFactory (std::make_unique<MediaCoverFactory> (*this->m_mediaSource));
  98. container->mount ("$mediaThumbnail", "$mediaThumbnail");
  99. container->mount (path, "/");
  100. try {
  101. container->mount (path / "scene.pkg", "/");
  102. } catch (std::runtime_error&) { }
  103. try {
  104. container->mount (path / "gifscene.pkg", "/");
  105. } catch (std::runtime_error&) { }
  106. try {
  107. container->mount (this->m_context.settings.general.assets, "/");
  108. } catch (std::runtime_error&) {
  109. sLog.exception ("Cannot find a valid assets folder, resolved to ", this->m_context.settings.general.assets);
  110. }
  111. try {
  112. container->mount (std::filesystem::current_path (), "/");
  113. } catch (std::runtime_error&) { }
  114. auto& vfs = container->getVFS ();
  115. // Had to get a little creative with the effects to achieve the same bloom effect without any custom code:
  116. // these virtual files are loaded by an image in the scene that takes the current _rt_FullFrameBuffer and
  117. // applies the bloom effect to render it out to the screen.
  118. vfs.add (
  119. "effects/wpenginelinux/bloomeffect.json",
  120. { { "name", "camerabloom_wpengine_linux" },
  121. { "group", "wpengine_linux_camera" },
  122. { "dependencies", JSON::array () },
  123. {
  124. "passes",
  125. JSON::array (
  126. { { { "material", "materials/util/downsample_quarter_bloom.json" },
  127. { "target", "_rt_4FrameBuffer" },
  128. { "bind", JSON::array ({ { { "name", "_rt_FullFrameBuffer" }, { "index", 0 } } }) } },
  129. { { "material", "materials/util/downsample_eighth_blur_v.json" },
  130. { "target", "_rt_8FrameBuffer" },
  131. { "bind", JSON::array ({ { { "name", "_rt_4FrameBuffer" }, { "index", 0 } } }) } },
  132. { { "material", "materials/util/blur_h_bloom.json" },
  133. { "target", "_rt_Bloom" },
  134. { "bind", JSON::array ({ { { "name", "_rt_8FrameBuffer" }, { "index", 0 } } }) } },
  135. { { "material", "materials/util/combine.json" },
  136. { "target", "_rt_FullFrameBuffer" },
  137. { "bind",
  138. JSON::array (
  139. { { { "name", "_rt_imageLayerComposite_-1_a" }, { "index", 0 } },
  140. { { "name", "_rt_Bloom" }, { "index", 1 } } }
  141. ) } } }
  142. ),
  143. } }
  144. );
  145. // Wastes a render pass on an image element that exists only to host the bloom material above
  146. vfs.add ("models/wpenginelinux.json", { { "material", "materials/wpenginelinux.json" } });
  147. vfs.add (
  148. "materials/wpenginelinux.json",
  149. { { "passes",
  150. JSON::array (
  151. { { { "blending", "normal" },
  152. { "cullmode", "nocull" },
  153. { "depthtest", "disabled" },
  154. { "depthwrite", "disabled" },
  155. { "shader", "genericimage2" },
  156. { "textures", JSON::array ({ "_rt_FullFrameBuffer" }) } } }
  157. ) } }
  158. );
  159. vfs.add (
  160. "shaders/commands/copy.frag",
  161. "uniform sampler2D g_Texture0;\n"
  162. "in vec2 v_TexCoord;\n"
  163. "void main () {\n"
  164. "out_FragColor = texture (g_Texture0, v_TexCoord);\n"
  165. "}"
  166. );
  167. vfs.add (
  168. "shaders/commands/copy.vert",
  169. "in vec3 a_Position;\n"
  170. "in vec2 a_TexCoord;\n"
  171. "out vec2 v_TexCoord;\n"
  172. "void main () {\n"
  173. "gl_Position = vec4 (a_Position, 1.0);\n"
  174. "v_TexCoord = a_TexCoord;\n"
  175. "}"
  176. );
  177. return std::make_unique<AssetLocator> (std::move (container));
  178. }
  179. void WallpaperApplication::loadBackgrounds () {
  180. if (this->m_context.settings.render.mode == ApplicationContext::NORMAL_WINDOW
  181. || this->m_context.settings.render.mode == ApplicationContext::EXPLICIT_WINDOW) {
  182. auto path = this->m_context.settings.general.defaultBackground;
  183. if (this->m_context.settings.general.defaultPlaylist.has_value ()
  184. && !this->m_context.settings.general.defaultPlaylist->items.empty ()) {
  185. path = this->m_context.settings.general.defaultPlaylist->items.front ();
  186. }
  187. this->m_backgrounds["default"] = this->loadBackground (path);
  188. return;
  189. }
  190. for (const auto& [screen, path] : this->m_context.settings.general.screenBackgrounds) {
  191. // skip span group synthetic keys here, they're handled below
  192. if (screen.rfind ("span:", 0) == 0) {
  193. continue;
  194. }
  195. // screens with no path should use the default
  196. if (path.empty ()) {
  197. this->m_backgrounds[screen] = this->loadBackground (this->m_context.settings.general.defaultBackground);
  198. } else {
  199. this->m_backgrounds[screen] = this->loadBackground (path);
  200. }
  201. }
  202. // Load one background per span group
  203. for (const auto& spanGroup : this->m_context.settings.general.spanGroups) {
  204. if (spanGroup.screens.empty ()) {
  205. continue;
  206. }
  207. std::filesystem::path bgPath = spanGroup.background;
  208. if (bgPath.empty ()) {
  209. bgPath = this->m_context.settings.general.defaultBackground;
  210. }
  211. // use the first screen's name as the group key for the loaded project
  212. const std::string groupKey = "span:" + spanGroup.screens.front ();
  213. this->m_backgrounds[groupKey] = this->loadBackground (bgPath);
  214. }
  215. }
  216. ProjectUniquePtr WallpaperApplication::loadBackground (const std::string& bg) {
  217. auto container = this->setupAssetLocator (bg);
  218. auto json = WallpaperEngine::Data::JSON::JSON::parse (container->readString ("project.json"));
  219. // when a background is loaded, reset the screenshot variables
  220. // this allows taking screenshots after a background changes
  221. // useful for playlists
  222. if (this->m_context.settings.screenshot.take) {
  223. this->m_nextFrameScreenshot = this->m_context.settings.screenshot.delay;
  224. if (this->m_videoDriver != nullptr) {
  225. this->m_nextFrameScreenshot += this->m_videoDriver->getFrameCounter ();
  226. }
  227. this->m_screenShotTaken = false;
  228. }
  229. return WallpaperEngine::Data::Parsers::ProjectParser::parse (json, std::move (container));
  230. }
  231. std::vector<std::size_t>
  232. WallpaperApplication::buildPlaylistOrder (const ApplicationContext::PlaylistDefinition& definition) {
  233. std::vector<std::size_t> order (definition.items.size ());
  234. std::iota (order.begin (), order.end (), 0);
  235. if (definition.settings.order == "random") {
  236. std::shuffle (order.begin (), order.end (), this->m_playlistRng);
  237. }
  238. return order;
  239. }
  240. void WallpaperApplication::initializePlaylists () {
  241. const bool hasDefaultPlaylist = this->m_context.settings.general.defaultPlaylist.has_value ();
  242. const bool hasScreenPlaylists = !this->m_context.settings.general.screenPlaylists.empty ();
  243. if (!hasDefaultPlaylist && !hasScreenPlaylists) {
  244. return;
  245. }
  246. const auto now = std::chrono::steady_clock::now ();
  247. auto registerPlaylist = [this, now] (
  248. const std::string& key, const ApplicationContext::PlaylistDefinition& playlist,
  249. std::optional<std::filesystem::path> currentPath
  250. ) {
  251. if (playlist.items.empty ()) {
  252. return;
  253. }
  254. ActivePlaylist state;
  255. state.definition = playlist;
  256. state.order = this->buildPlaylistOrder (playlist);
  257. if (state.order.empty ()) {
  258. return;
  259. }
  260. if (currentPath.has_value ()) {
  261. state.orderIndex = 0;
  262. for (std::size_t i = 0; i < state.order.size (); i++) {
  263. if (playlist.items[state.order[i]] == currentPath.value ()) {
  264. state.orderIndex = i;
  265. break;
  266. }
  267. }
  268. }
  269. const uint32_t delayMinutes = std::max<uint32_t> (1, state.definition.settings.delayMinutes);
  270. state.nextSwitch = now + std::chrono::minutes (delayMinutes);
  271. state.lastUpdate = now;
  272. this->m_activePlaylists.insert_or_assign (key, std::move (state));
  273. };
  274. if (hasDefaultPlaylist
  275. && (this->m_context.settings.render.mode == ApplicationContext::NORMAL_WINDOW
  276. || this->m_context.settings.render.mode == ApplicationContext::EXPLICIT_WINDOW)) {
  277. const auto& playlist = this->m_context.settings.general.defaultPlaylist.value ();
  278. const auto currentPath = playlist.items.empty ()
  279. ? std::optional<std::filesystem::path> { this->m_context.settings.general.defaultBackground }
  280. : std::optional<std::filesystem::path> { playlist.items.front () };
  281. registerPlaylist ("default", playlist, currentPath);
  282. }
  283. for (const auto& [screen, playlist] : this->m_context.settings.general.screenPlaylists) {
  284. const auto current = this->m_context.settings.general.screenBackgrounds.find (screen);
  285. const auto currentPath = current != this->m_context.settings.general.screenBackgrounds.end ()
  286. ? std::optional<std::filesystem::path> { current->second }
  287. : std::nullopt;
  288. registerPlaylist (screen, playlist, currentPath);
  289. }
  290. }
  291. bool WallpaperApplication::makeAnyViewportCurrent () const {
  292. if (!this->m_renderContext) {
  293. return false;
  294. }
  295. const auto& viewports = this->m_renderContext->getOutput ().getViewports ();
  296. if (viewports.empty ()) {
  297. return false;
  298. }
  299. viewports.begin ()->second->makeCurrent ();
  300. return true;
  301. }
  302. bool WallpaperApplication::preflightWallpaper (const std::string& path) {
  303. try {
  304. // avoid mutating state, just ensure project.json parses
  305. auto container = this->setupAssetLocator (path);
  306. const auto json = WallpaperEngine::Data::JSON::JSON::parse (container->readString ("project.json"));
  307. if (!json.contains ("type") || !json.contains ("file")) {
  308. sLog.error ("Preflight failed for ", path, ": missing required fields");
  309. return false;
  310. }
  311. return true;
  312. } catch (const std::exception& e) {
  313. sLog.error ("Preflight failed for ", path, ": ", e.what ());
  314. return false;
  315. }
  316. }
  317. bool WallpaperApplication::selectNextCandidate (ActivePlaylist& playlist, std::size_t& outOrderIndex) {
  318. if (playlist.order.empty ()) {
  319. return false;
  320. }
  321. std::size_t attempts = 0;
  322. std::size_t candidateOrderIndex = outOrderIndex;
  323. while (attempts < playlist.order.size ()) {
  324. const auto candidateIndex = playlist.order[candidateOrderIndex];
  325. if (!playlist.failedIndices.contains (candidateIndex)) {
  326. outOrderIndex = candidateOrderIndex;
  327. return true;
  328. }
  329. attempts++;
  330. candidateOrderIndex = (candidateOrderIndex + 1) % playlist.order.size ();
  331. }
  332. return false;
  333. }
  334. void WallpaperApplication::advancePlaylist (
  335. const std::string& screen, ActivePlaylist& playlist, const std::chrono::steady_clock::time_point& now
  336. ) {
  337. if (playlist.order.empty ()) {
  338. return;
  339. }
  340. playlist.orderIndex = (playlist.orderIndex + 1) % playlist.order.size ();
  341. if (playlist.orderIndex == 0 && playlist.definition.settings.order == "random") {
  342. std::shuffle (playlist.order.begin (), playlist.order.end (), this->m_playlistRng);
  343. }
  344. std::size_t candidateOrderIndex = playlist.orderIndex;
  345. if (!this->selectNextCandidate (playlist, candidateOrderIndex)) {
  346. sLog.error ("All playlist items failed for ", screen, ", keeping current wallpaper");
  347. const uint32_t delayMinutes = std::max<uint32_t> (1, playlist.definition.settings.delayMinutes);
  348. playlist.nextSwitch = now + std::chrono::minutes (delayMinutes);
  349. return;
  350. }
  351. const auto candidateIndex = playlist.order[candidateOrderIndex];
  352. const auto& candidatePath = playlist.definition.items[candidateIndex];
  353. if (!this->preflightWallpaper (candidatePath.string ())) {
  354. playlist.failedIndices.insert (candidateIndex);
  355. if (!this->selectNextCandidate (playlist, candidateOrderIndex)) {
  356. sLog.error ("All playlist items failed for ", screen, ", keeping current wallpaper");
  357. const uint32_t delayMinutes = std::max<uint32_t> (1, playlist.definition.settings.delayMinutes);
  358. playlist.nextSwitch = now + std::chrono::minutes (delayMinutes);
  359. return;
  360. }
  361. }
  362. playlist.orderIndex = candidateOrderIndex;
  363. const auto& nextPath = playlist.definition.items[playlist.order[playlist.orderIndex]];
  364. bool loaded = false;
  365. try {
  366. if (!this->makeAnyViewportCurrent ()) {
  367. sLog.error ("Cannot switch playlist on ", screen, ": no active viewport");
  368. throw std::runtime_error ("No viewport available");
  369. }
  370. auto project = this->loadBackground (nextPath.string ());
  371. this->setupPropertiesForProject (*project);
  372. this->m_backgrounds[screen] = std::move (project);
  373. const auto scalingIt = this->m_context.settings.general.screenScalings.find (screen);
  374. const auto clampIt = this->m_context.settings.general.screenClamps.find (screen);
  375. const auto scaling = scalingIt != this->m_context.settings.general.screenScalings.end ()
  376. ? scalingIt->second
  377. : this->m_context.settings.render.window.scalingMode;
  378. const auto clamp = clampIt != this->m_context.settings.general.screenClamps.end ()
  379. ? clampIt->second
  380. : this->m_context.settings.render.window.clamp;
  381. if (this->m_renderContext) {
  382. auto wallpaper = WallpaperEngine::Render::CWallpaper::fromWallpaper (
  383. *this->m_backgrounds[screen]->wallpaper, *this->m_renderContext, *this->m_audioContext, nextPath,
  384. scaling, clamp, this->resolveScreenRenderSize (screen)
  385. );
  386. wallpaper->setZoom (this->resolveScreenZoom (screen));
  387. wallpaper->setCornerColor (this->resolveScreenCornerColor (screen));
  388. this->m_renderContext->setWallpaper (screen, std::move (wallpaper));
  389. }
  390. this->m_context.settings.general.screenBackgrounds[screen] = nextPath;
  391. loaded = true;
  392. } catch (const std::exception& e) {
  393. sLog.error ("Failed to advance playlist on ", screen, ": ", e.what ());
  394. }
  395. if (!loaded) {
  396. playlist.failedIndices.insert (playlist.order[playlist.orderIndex]);
  397. // Keep current position; next timer tick will retry advancement
  398. sLog.error ("Failed to load wallpaper for ", screen, ", will retry on next cycle");
  399. }
  400. const uint32_t delayMinutes = std::max<uint32_t> (1, playlist.definition.settings.delayMinutes);
  401. playlist.nextSwitch = now + std::chrono::minutes (delayMinutes);
  402. }
  403. void WallpaperApplication::updatePlaylists () {
  404. if (this->m_activePlaylists.empty ()) {
  405. return;
  406. }
  407. const auto now = std::chrono::steady_clock::now ();
  408. for (auto& [screen, playlist] : this->m_activePlaylists) {
  409. playlist.lastUpdate = now;
  410. if (playlist.definition.settings.mode != "timer") {
  411. continue;
  412. }
  413. if (playlist.definition.items.size () <= 1) {
  414. continue;
  415. }
  416. if (now < playlist.nextSwitch) {
  417. continue;
  418. }
  419. this->advancePlaylist (screen, playlist, now);
  420. }
  421. }
  422. namespace {
  423. /** Everything a control-file request can carry; any field left unset means "don't touch this" */
  424. struct HotswapRequest {
  425. std::optional<std::string> path;
  426. /** True once a "layers=1" line was seen, meaning disabledObjects/enabledObjects below are a full replacement */
  427. bool layersProvided = false;
  428. std::vector<std::string> disabledObjects;
  429. std::vector<std::string> enabledObjects;
  430. /** True once at least one "property=name=value" line was seen */
  431. bool propertiesProvided = false;
  432. std::map<std::string, std::string> properties;
  433. std::optional<int> volume;
  434. /** "on"/"off"/"toggle" (also "1"/"0"/"true"/"false" for on/off) */
  435. std::optional<std::string> xray;
  436. /** "stretch"/"fit"/"fill"/"center"/"default" */
  437. std::optional<std::string> scaling;
  438. /** floating-point zoom factor layered on top of scaling, e.g. "1.5" */
  439. std::optional<std::string> zoom;
  440. /** "on"/"off"/"toggle" (also "1"/"0"/"true"/"false" for on/off) */
  441. std::optional<std::string> disableParallax;
  442. /** hex RGB/RGBA color, e.g. "000000" or "#1a1a1aff" */
  443. std::optional<std::string> cornerColor;
  444. /** floating-point playback speed multiplier, e.g. "0.5" */
  445. std::optional<std::string> speed;
  446. };
  447. std::string trimHotswapToken (const std::string& value) {
  448. std::size_t begin = 0;
  449. std::size_t end = value.size ();
  450. while (begin < end && std::isspace (static_cast<unsigned char> (value[begin]))) {
  451. begin++;
  452. }
  453. while (end > begin && std::isspace (static_cast<unsigned char> (value[end - 1]))) {
  454. end--;
  455. }
  456. return value.substr (begin, end - begin);
  457. }
  458. /**
  459. * Parses the control file. Supports the original bare-path-on-one-line format for backwards
  460. * compatibility, plus key=value lines (path/layers/disable-object/enable-object/volume/xray/scaling/zoom/
  461. * disable-parallax/corner-color/speed/property) so a single request can carry more than just the
  462. * background path. "property=name=value" (repeatable) carries --set-property-equivalent overrides.
  463. */
  464. HotswapRequest parseHotswapRequest (std::istream& file) {
  465. HotswapRequest request;
  466. std::string rawLine;
  467. while (std::getline (file, rawLine)) {
  468. const std::string line = trimHotswapToken (rawLine);
  469. if (line.empty ()) {
  470. continue;
  471. }
  472. const auto separator = line.find ('=');
  473. if (separator == std::string::npos) {
  474. // legacy format: the whole line is the new background path
  475. if (!request.path.has_value ()) {
  476. request.path = line;
  477. }
  478. continue;
  479. }
  480. const std::string key = trimHotswapToken (line.substr (0, separator));
  481. const std::string value = trimHotswapToken (line.substr (separator + 1));
  482. if (key == "path") {
  483. request.path = value;
  484. } else if (key == "layers") {
  485. request.layersProvided = true;
  486. } else if (key == "disable-object") {
  487. request.layersProvided = true;
  488. request.disabledObjects.push_back (value);
  489. } else if (key == "enable-object") {
  490. request.layersProvided = true;
  491. request.enabledObjects.push_back (value);
  492. } else if (key == "volume") {
  493. try {
  494. request.volume = std::stoi (value);
  495. } catch (const std::exception&) {
  496. sLog.error ("Hotswap: ignoring invalid volume value: ", value);
  497. }
  498. } else if (key == "xray") {
  499. request.xray = value;
  500. } else if (key == "scaling") {
  501. request.scaling = value;
  502. } else if (key == "zoom") {
  503. request.zoom = value;
  504. } else if (key == "disable-parallax") {
  505. request.disableParallax = value;
  506. } else if (key == "corner-color") {
  507. request.cornerColor = value;
  508. } else if (key == "speed") {
  509. request.speed = value;
  510. } else if (key == "property") {
  511. const auto propSeparator = value.find ('=');
  512. if (propSeparator == std::string::npos) {
  513. sLog.error ("Hotswap: ignoring malformed property line: ", value);
  514. } else {
  515. request.propertiesProvided = true;
  516. request.properties[value.substr (0, propSeparator)] = value.substr (propSeparator + 1);
  517. }
  518. } else {
  519. sLog.error ("Hotswap: ignoring unknown control file key: ", key);
  520. }
  521. }
  522. return request;
  523. }
  524. } // namespace
  525. void WallpaperApplication::checkHotswapRequest () {
  526. if (!this->m_hotswapRequested.exchange (false)) {
  527. return;
  528. }
  529. const char* runtimeDir = getenv ("XDG_RUNTIME_DIR");
  530. const auto controlFile = std::filesystem::path (runtimeDir != nullptr ? runtimeDir : "/tmp") / "lwe-control";
  531. std::ifstream file (controlFile);
  532. if (!file.is_open ()) {
  533. sLog.error ("Hotswap requested but control file could not be read: ", controlFile.string ());
  534. return;
  535. }
  536. const auto request = parseHotswapRequest (file);
  537. if (!request.path.has_value () && !request.layersProvided && !request.volume.has_value ()
  538. && !request.xray.has_value () && !request.scaling.has_value () && !request.zoom.has_value ()
  539. && !request.disableParallax.has_value () && !request.cornerColor.has_value ()
  540. && !request.speed.has_value () && !request.propertiesProvided) {
  541. sLog.error ("Hotswap requested but control file was empty");
  542. return;
  543. }
  544. if (request.volume.has_value ()) {
  545. this->applyVolumeHotswap (*request.volume);
  546. }
  547. if (request.xray.has_value ()) {
  548. this->applyXrayHotswap (*request.xray);
  549. }
  550. if (request.scaling.has_value ()) {
  551. this->applyScalingHotswap (*request.scaling);
  552. }
  553. if (request.zoom.has_value ()) {
  554. this->applyZoomHotswap (*request.zoom);
  555. }
  556. if (request.disableParallax.has_value ()) {
  557. this->applyParallaxHotswap (*request.disableParallax);
  558. }
  559. if (request.cornerColor.has_value ()) {
  560. this->applyCornerColorHotswap (*request.cornerColor);
  561. }
  562. if (request.speed.has_value ()) {
  563. this->applySpeedHotswap (*request.speed);
  564. }
  565. if (request.layersProvided) {
  566. this->m_context.settings.general.disabledObjects = request.disabledObjects;
  567. this->m_context.settings.general.enabledObjects = request.enabledObjects;
  568. }
  569. if (request.propertiesProvided) {
  570. for (const auto& [name, value] : request.properties) {
  571. this->m_context.settings.general.properties[name] = value;
  572. }
  573. }
  574. // volume/xray/speed-only requests are pure live setters with nothing to reload. Properties,
  575. // like layers, are baked into the scene graph at parse time, so they need the same reload.
  576. if (!request.path.has_value () && !request.layersProvided && !request.propertiesProvided) {
  577. return;
  578. }
  579. if (request.path.has_value () && !this->preflightWallpaper (*request.path)) {
  580. sLog.error ("Hotswap failed, invalid wallpaper at ", *request.path);
  581. return;
  582. }
  583. if (!this->makeAnyViewportCurrent ()) {
  584. sLog.error ("Hotswap failed, no active viewport");
  585. return;
  586. }
  587. if (request.path.has_value ()) {
  588. sLog.out ("Hotswapping wallpaper to ", *request.path);
  589. } else {
  590. sLog.out ("Hotswapping wallpaper layers");
  591. }
  592. for (auto& [screen, background] : this->m_backgrounds) {
  593. const std::string targetPath = request.path.value_or (this->resolveScreenBackgroundPath (screen));
  594. try {
  595. auto project = this->loadBackground (targetPath);
  596. this->setupPropertiesForProject (*project);
  597. background = std::move (project);
  598. const auto scalingIt = this->m_context.settings.general.screenScalings.find (screen);
  599. const auto clampIt = this->m_context.settings.general.screenClamps.find (screen);
  600. const auto scaling = scalingIt != this->m_context.settings.general.screenScalings.end ()
  601. ? scalingIt->second
  602. : this->m_context.settings.render.window.scalingMode;
  603. const auto clamp = clampIt != this->m_context.settings.general.screenClamps.end ()
  604. ? clampIt->second
  605. : this->m_context.settings.render.window.clamp;
  606. if (this->m_renderContext) {
  607. auto wallpaper = WallpaperEngine::Render::CWallpaper::fromWallpaper (
  608. *background->wallpaper, *this->m_renderContext, *this->m_audioContext, targetPath, scaling, clamp,
  609. this->resolveScreenRenderSize (screen)
  610. );
  611. wallpaper->setZoom (this->resolveScreenZoom (screen));
  612. wallpaper->setCornerColor (this->resolveScreenCornerColor (screen));
  613. this->m_renderContext->setWallpaper (screen, std::move (wallpaper));
  614. }
  615. if (request.path.has_value ()) {
  616. if (screen.starts_with ("span:")) {
  617. for (auto& spanGroup : this->m_context.settings.general.spanGroups) {
  618. if (!spanGroup.screens.empty () && "span:" + spanGroup.screens.front () == screen) {
  619. spanGroup.background = *request.path;
  620. }
  621. }
  622. } else {
  623. this->m_context.settings.general.screenBackgrounds[screen] = *request.path;
  624. }
  625. }
  626. } catch (const std::exception& e) {
  627. sLog.error ("Hotswap failed for screen ", screen, ": ", e.what ());
  628. }
  629. }
  630. if (request.path.has_value ()) {
  631. this->m_context.settings.general.defaultBackground = *request.path;
  632. }
  633. }
  634. std::string WallpaperApplication::resolveScreenBackgroundPath (const std::string& screen) const {
  635. if (screen.starts_with ("span:")) {
  636. for (const auto& spanGroup : this->m_context.settings.general.spanGroups) {
  637. if (!spanGroup.screens.empty () && "span:" + spanGroup.screens.front () == screen) {
  638. return spanGroup.background.empty () ? this->m_context.settings.general.defaultBackground.string ()
  639. : spanGroup.background.string ();
  640. }
  641. }
  642. return this->m_context.settings.general.defaultBackground.string ();
  643. }
  644. const auto it = this->m_context.settings.general.screenBackgrounds.find (screen);
  645. if (it != this->m_context.settings.general.screenBackgrounds.end () && !it->second.empty ()) {
  646. return it->second.string ();
  647. }
  648. return this->m_context.settings.general.defaultBackground.string ();
  649. }
  650. float WallpaperApplication::resolveScreenZoom (const std::string& screen) const {
  651. const auto it = this->m_context.settings.general.screenZooms.find (screen);
  652. return it != this->m_context.settings.general.screenZooms.end () ? it->second
  653. : this->m_context.settings.render.window.zoom;
  654. }
  655. glm::vec4 WallpaperApplication::resolveScreenCornerColor (const std::string& screen) const {
  656. const auto it = this->m_context.settings.general.screenCornerColors.find (screen);
  657. return it != this->m_context.settings.general.screenCornerColors.end ()
  658. ? it->second
  659. : this->m_context.settings.render.window.cornerColor;
  660. }
  661. glm::ivec2 WallpaperApplication::resolveScreenRenderSize (const std::string& screen) const {
  662. const auto& viewports = this->m_renderContext->getOutput ().getViewports ();
  663. const auto it = viewports.find (screen);
  664. if (it != viewports.end ()) {
  665. return { it->second->viewport.z, it->second->viewport.w };
  666. }
  667. return { this->m_renderContext->getOutput ().getFullWidth (), this->m_renderContext->getOutput ().getFullHeight () };
  668. }
  669. void WallpaperApplication::applyVolumeHotswap (int volume) {
  670. volume = std::max (0, std::min (volume, 128));
  671. this->m_context.settings.audio.volume = volume;
  672. this->m_context.state.audio.volume = volume;
  673. if (!this->m_renderContext) {
  674. return;
  675. }
  676. const double scaledVolume = this->m_context.settings.audio.enabled ? volume * 100.0 / 128.0 : 0.0;
  677. for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
  678. if (wallpaper->is<WallpaperEngine::Render::Wallpapers::CVideo> ()) {
  679. wallpaper->as<WallpaperEngine::Render::Wallpapers::CVideo> ()->setVolume (scaledVolume);
  680. }
  681. }
  682. sLog.out ("Hotswap: applied volume ", volume, " live");
  683. }
  684. void WallpaperApplication::applyXrayHotswap (const std::string& value) {
  685. bool newState;
  686. if (value == "toggle") {
  687. newState = !this->m_context.state.xray.fullReveal;
  688. } else if (value == "on" || value == "1" || value == "true") {
  689. newState = true;
  690. } else if (value == "off" || value == "0" || value == "false") {
  691. newState = false;
  692. } else {
  693. sLog.error ("Hotswap: ignoring invalid xray value: ", value);
  694. return;
  695. }
  696. this->m_context.state.xray.fullReveal = newState;
  697. sLog.out ("Hotswap: full xray ", newState ? "enabled" : "disabled", " live");
  698. }
  699. void WallpaperApplication::applyScalingHotswap (const std::string& value) {
  700. const auto mode = WallpaperEngine::Render::WallpaperState::parseScalingMode (value);
  701. if (!mode.has_value ()) {
  702. sLog.error ("Hotswap: ignoring invalid scaling value: ", value);
  703. return;
  704. }
  705. this->m_context.settings.render.window.scalingMode = *mode;
  706. for (auto& [screen, scaling] : this->m_context.settings.general.screenScalings) {
  707. scaling = *mode;
  708. }
  709. for (auto& spanGroup : this->m_context.settings.general.spanGroups) {
  710. spanGroup.scaling = *mode;
  711. }
  712. if (this->m_renderContext) {
  713. for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
  714. wallpaper->setScalingMode (*mode);
  715. }
  716. }
  717. sLog.out ("Hotswap: applied scaling mode ", value, " live");
  718. }
  719. void WallpaperApplication::applyZoomHotswap (const std::string& value) {
  720. float zoom;
  721. try {
  722. zoom = std::stof (value);
  723. } catch (const std::exception&) {
  724. sLog.error ("Hotswap: ignoring invalid zoom value: ", value);
  725. return;
  726. }
  727. this->m_context.settings.render.window.zoom = zoom;
  728. for (auto& [screen, screenZoom] : this->m_context.settings.general.screenZooms) {
  729. screenZoom = zoom;
  730. }
  731. for (auto& spanGroup : this->m_context.settings.general.spanGroups) {
  732. spanGroup.zoom = zoom;
  733. }
  734. if (this->m_renderContext) {
  735. for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
  736. wallpaper->setZoom (zoom);
  737. }
  738. }
  739. sLog.out ("Hotswap: applied zoom ", zoom, " live");
  740. }
  741. void WallpaperApplication::applyParallaxHotswap (const std::string& value) {
  742. bool newState;
  743. if (value == "toggle") {
  744. newState = !this->m_context.settings.mouse.disableparallax;
  745. } else if (value == "on" || value == "1" || value == "true") {
  746. newState = true;
  747. } else if (value == "off" || value == "0" || value == "false") {
  748. newState = false;
  749. } else {
  750. sLog.error ("Hotswap: ignoring invalid disable-parallax value: ", value);
  751. return;
  752. }
  753. this->m_context.settings.mouse.disableparallax = newState;
  754. sLog.out ("Hotswap: parallax ", newState ? "force-disabled" : "enabled", " live");
  755. }
  756. void WallpaperApplication::applyCornerColorHotswap (const std::string& value) {
  757. const auto color = WallpaperEngine::Render::CFBO::parseColor (value);
  758. if (!color.has_value ()) {
  759. sLog.error ("Hotswap: ignoring invalid corner color: ", value);
  760. return;
  761. }
  762. this->m_context.settings.render.window.cornerColor = *color;
  763. for (auto& [screen, screenColor] : this->m_context.settings.general.screenCornerColors) {
  764. screenColor = *color;
  765. }
  766. for (auto& spanGroup : this->m_context.settings.general.spanGroups) {
  767. spanGroup.cornerColor = *color;
  768. }
  769. if (this->m_renderContext) {
  770. for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
  771. wallpaper->setCornerColor (*color);
  772. }
  773. }
  774. sLog.out ("Hotswap: applied corner color ", value, " live");
  775. }
  776. void WallpaperApplication::applySpeedHotswap (const std::string& value) {
  777. float speed;
  778. try {
  779. speed = std::stof (value);
  780. } catch (const std::exception&) {
  781. sLog.error ("Hotswap: ignoring invalid speed value: ", value);
  782. return;
  783. }
  784. if (speed <= 0.0f) {
  785. sLog.error ("Hotswap: ignoring non-positive speed value: ", value);
  786. return;
  787. }
  788. this->m_context.settings.render.playbackSpeed = speed;
  789. // Particles/effects/scripts read settings.render.playbackSpeed directly every frame, but
  790. // video wallpapers are driven by mpv's own clock and need the change pushed explicitly.
  791. if (this->m_renderContext) {
  792. for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
  793. if (wallpaper->is<WallpaperEngine::Render::Wallpapers::CVideo> ()) {
  794. wallpaper->as<WallpaperEngine::Render::Wallpapers::CVideo> ()->setSpeed (speed);
  795. }
  796. }
  797. }
  798. sLog.out ("Hotswap: applied speed ", speed, " live");
  799. }
  800. void WallpaperApplication::setupPropertiesForProject (const Project& project) {
  801. for (const auto& [key, cur] : project.properties) {
  802. auto override = this->m_context.settings.general.properties.find (key);
  803. if (override != this->m_context.settings.general.properties.end ()) {
  804. sLog.out ("Applying override value for ", key);
  805. cur->update (override->second, DynamicValue::UpdateSource::User);
  806. }
  807. if (this->m_context.settings.general.onlyListProperties) {
  808. sLog.out (cur->dump ());
  809. }
  810. }
  811. }
  812. void WallpaperApplication::setupProperties () {
  813. for (const auto& [background, info] : this->m_backgrounds) {
  814. this->setupPropertiesForProject (*info);
  815. }
  816. }
  817. void WallpaperApplication::listObjectsForProject (const std::string& background, const Project& project) const {
  818. if (!project.wallpaper->is<Scene> ()) {
  819. return;
  820. }
  821. const auto scene = project.wallpaper->as<Scene> ();
  822. sLog.out ("Objects for ", background, ":");
  823. for (const auto& object : scene->objects) {
  824. std::string type = "unknown";
  825. if (object->is<Image> ()) {
  826. type = "image";
  827. } else if (object->is<Particle> ()) {
  828. type = "particle";
  829. } else if (object->is<Text> ()) {
  830. type = "text";
  831. } else if (object->is<Sound> ()) {
  832. type = "sound";
  833. }
  834. sLog.out (" ", object->id, " - ", object->name, " (", type, ")");
  835. }
  836. }
  837. void WallpaperApplication::listObjects () const {
  838. if (!this->m_context.settings.general.onlyListObjects) {
  839. return;
  840. }
  841. for (const auto& [background, info] : this->m_backgrounds) {
  842. this->listObjectsForProject (background, *info);
  843. }
  844. }
  845. void WallpaperApplication::setupBrowser () {
  846. // The main engine process never hosts CEF directly - CEF only supports one
  847. // CefInitialize()/CefShutdown() pair per process, so a process that might later need to stop
  848. // and restart hosting a web wallpaper can't safely do it in place. Only two roles reach here:
  849. // CEF's own subprocess re-execs (--type=zygote/gpu-process/renderer/utility), which must
  850. // always reach WebBrowserContext even if loadBackgrounds() found nothing to load, and this
  851. // process's own --web-host role.
  852. if ((!this->isCefSubprocess () && !this->m_context.settings.general.webHost) || this->m_browserContext) {
  853. return;
  854. }
  855. this->m_browserContext = std::make_unique<WebBrowser::WebBrowserContext> (*this);
  856. }
  857. void WallpaperApplication::runWebHost () {
  858. using namespace WallpaperEngine::WebBrowser;
  859. using namespace WallpaperEngine::WebBrowser::IPC;
  860. const auto backgroundIt = this->m_backgrounds.find ("default");
  861. if (backgroundIt == this->m_backgrounds.end () || !backgroundIt->second->wallpaper->is<Web> ()) {
  862. sLog.error ("--web-host requires a single Web wallpaper background, none found");
  863. return;
  864. }
  865. if (!this->m_browserContext) {
  866. sLog.error ("--web-host: CEF was not initialized (setupBrowser() found no Web project?)");
  867. return;
  868. }
  869. const Project& project = *backgroundIt->second;
  870. const Web& web = *project.wallpaper->as<Web> ();
  871. const int width = static_cast<int> (this->m_context.settings.general.webHostWidth);
  872. const int height = static_cast<int> (this->m_context.settings.general.webHostHeight);
  873. if (width <= 0 || height <= 0) {
  874. sLog.error ("--web-host: invalid --web-host-width/--web-host-height");
  875. return;
  876. }
  877. auto* shm = attachSharedMemory (this->m_context.settings.general.webHostShm, width, height);
  878. if (shm == nullptr) {
  879. sLog.error ("--web-host: failed to attach shared memory ", this->m_context.settings.general.webHostShm);
  880. return;
  881. }
  882. CefWindowInfo windowInfo;
  883. windowInfo.SetAsWindowless (0);
  884. CefBrowserSettings browserSettings;
  885. browserSettings.windowless_frame_rate = std::max (60, this->m_context.settings.render.maximumFPS);
  886. const CefRefPtr<CEF::SharedMemoryRenderHandler> renderHandler = new CEF::SharedMemoryRenderHandler (shm);
  887. const CefRefPtr<CEF::BrowserClient> client = new CEF::BrowserClient (renderHandler, project.properties);
  888. // the "w" prefix + workshop id host must match what the scheme handler factory expects to resolve
  889. const std::string htmlURL = std::string (WPENGINE_SCHEME) + "://w" + project.workshopId + "/" + web.filename;
  890. sLog.out ("--web-host: creating browser for ", htmlURL, " (pid ", static_cast<long> (getpid ()), ")");
  891. const CefRefPtr<CefBrowser> browser
  892. = CefBrowserHost::CreateBrowserSync (windowInfo, client, htmlURL, browserSettings, nullptr, nullptr);
  893. if (!browser) {
  894. sLog.error ("--web-host: failed to create the CEF browser");
  895. shm->helperFailed.store (true, std::memory_order_release);
  896. closeSharedMemory (shm, this->m_context.settings.general.webHostShm, width, height, false);
  897. return;
  898. }
  899. shm->helperReady.store (true, std::memory_order_release);
  900. Input::MouseClickStatus lastLeft = Input::Released;
  901. Input::MouseClickStatus lastRight = Input::Released;
  902. uint32_t lastDesiredWidth = shm->desiredWidth.load (std::memory_order_relaxed);
  903. uint32_t lastDesiredHeight = shm->desiredHeight.load (std::memory_order_relaxed);
  904. while (!shm->quitRequested.load (std::memory_order_acquire)) {
  905. CefDoMessageLoopWork ();
  906. const uint32_t desiredWidth = shm->desiredWidth.load (std::memory_order_relaxed);
  907. const uint32_t desiredHeight = shm->desiredHeight.load (std::memory_order_relaxed);
  908. if (desiredWidth != lastDesiredWidth || desiredHeight != lastDesiredHeight) {
  909. lastDesiredWidth = desiredWidth;
  910. lastDesiredHeight = desiredHeight;
  911. browser->GetHost ()->WasResized ();
  912. }
  913. CefMouseEvent evt;
  914. evt.x = static_cast<int> (shm->mouseX.load (std::memory_order_relaxed));
  915. evt.y = static_cast<int> (shm->mouseY.load (std::memory_order_relaxed));
  916. browser->GetHost ()->SendMouseMoveEvent (evt, false);
  917. const auto left = static_cast<Input::MouseClickStatus> (shm->leftClick.load (std::memory_order_relaxed));
  918. const auto right = static_cast<Input::MouseClickStatus> (shm->rightClick.load (std::memory_order_relaxed));
  919. if (left != lastLeft) {
  920. browser->GetHost ()->SendMouseClickEvent (
  921. evt, CefBrowserHost::MouseButtonType::MBT_LEFT, left == Input::Released, 1
  922. );
  923. lastLeft = left;
  924. }
  925. if (right != lastRight) {
  926. browser->GetHost ()->SendMouseClickEvent (
  927. evt, CefBrowserHost::MouseButtonType::MBT_RIGHT, right == Input::Released, 1
  928. );
  929. lastRight = right;
  930. }
  931. std::this_thread::sleep_for (std::chrono::milliseconds (4));
  932. }
  933. browser->GetHost ()->CloseBrowser (true);
  934. // CloseBrowser() only requests an async close - wait briefly for it to finish before
  935. // CefShutdown() below tears everything down out from under it
  936. for (int i = 0; i < 500 && !client->isClosed (); i++) {
  937. CefDoMessageLoopWork ();
  938. }
  939. closeSharedMemory (shm, this->m_context.settings.general.webHostShm, width, height, false);
  940. }
  941. void WallpaperApplication::takeScreenshot (const std::filesystem::path& filename) const {
  942. const int width = this->m_renderContext->getOutput ().getFullWidth ();
  943. const int height = this->m_renderContext->getOutput ().getFullHeight ();
  944. const bool vflip = this->m_renderContext->getOutput ().renderVFlip ();
  945. const auto& wallpapers = this->m_renderContext->getWallpapers ();
  946. struct ViewportCapture {
  947. uint8_t* buffer;
  948. int readWidth;
  949. int readHeight;
  950. int vpWidth;
  951. int vpHeight;
  952. int xoffset;
  953. float ustart, uend, vstart, vend;
  954. };
  955. std::vector<ViewportCapture> captures;
  956. int currentXOffset = 0;
  957. for (const auto& [screen, viewport] : this->m_renderContext->getOutput ().getViewports ()) {
  958. viewport->makeCurrent ();
  959. const auto wallpaperIt = wallpapers.find (screen);
  960. if (wallpaperIt == wallpapers.end ()) {
  961. sLog.error ("Cannot find wallpaper for screen ", screen);
  962. continue;
  963. }
  964. const auto& wallpaper = wallpaperIt->second;
  965. const int vpWidth = viewport->viewport.z - viewport->viewport.x;
  966. const int vpHeight = viewport->viewport.w - viewport->viewport.y;
  967. // bind the wallpaper's FBO to read from it directly
  968. // this is more reliable than the default framebuffer on some drivers (NVIDIA/Wayland)
  969. glBindFramebuffer (GL_FRAMEBUFFER, wallpaper->getWallpaperFramebuffer ());
  970. // ensure rendering is complete before reading
  971. glFinish ();
  972. const int readWidth = wallpaper->getWidth ();
  973. const int readHeight = wallpaper->getHeight ();
  974. const auto bufferSize = readWidth * readHeight * 3;
  975. auto* buffer = new uint8_t[bufferSize];
  976. glPixelStorei (GL_PACK_ALIGNMENT, 1);
  977. if (GLEW_VERSION_4_5) {
  978. glReadnPixels (0, 0, readWidth, readHeight, GL_RGB, GL_UNSIGNED_BYTE, bufferSize, buffer);
  979. } else {
  980. glReadPixels (0, 0, readWidth, readHeight, GL_RGB, GL_UNSIGNED_BYTE, buffer);
  981. }
  982. glBindFramebuffer (GL_FRAMEBUFFER, 0);
  983. if (const GLenum error = glGetError (); error != GL_NO_ERROR) {
  984. sLog.error ("Cannot obtain pixel data for screen ", screen, ". OpenGL error: ", error);
  985. delete[] buffer;
  986. continue;
  987. }
  988. // Get the UV coordinates which define the visible portion based on scaling mode
  989. const auto [ustart, uend, vstart, vend] = wallpaper->getState ().getTextureUVs ();
  990. captures.push_back (
  991. { buffer, readWidth, readHeight, vpWidth, vpHeight, currentXOffset, ustart, uend, vstart, vend }
  992. );
  993. if (viewport->single) {
  994. currentXOffset += vpWidth;
  995. }
  996. }
  997. const auto extension = filename.extension ();
  998. const std::string extStr = extension.string ();
  999. // Offload pixel processing and saving to a background thread to avoid hitches
  1000. std::thread ([captures, width, height, vflip, extStr, filename] () {
  1001. auto* bitmap = new uint8_t[width * height * 3] { 0 };
  1002. for (const auto& capture : captures) {
  1003. // copy pixels to bitmap, sampling from the UV-defined region
  1004. for (int y = 0; y < capture.vpHeight; y++) {
  1005. for (int x = 0; x < capture.vpWidth; x++) {
  1006. // interpolate within the UV range to get source coordinates
  1007. const float u
  1008. = capture.ustart + (static_cast<float> (x) / capture.vpWidth) * (capture.uend - capture.ustart);
  1009. const float v = capture.vstart
  1010. + (static_cast<float> (y) / capture.vpHeight) * (capture.vend - capture.vstart);
  1011. // convert UV to pixel coordinates in the source buffer
  1012. const int srcX = std::clamp (static_cast<int> (u * capture.readWidth), 0, capture.readWidth - 1);
  1013. const int srcY = std::clamp (static_cast<int> (v * capture.readHeight), 0, capture.readHeight - 1);
  1014. const int srcIdx = (srcY * capture.readWidth + srcX) * 3;
  1015. const int xfinal = x + capture.xoffset;
  1016. // FBO content is not flipped like default framebuffer, so invert vflip logic
  1017. const int yfinal = vflip ? y : (capture.vpHeight - y - 1);
  1018. if (yfinal >= 0 && yfinal < height && xfinal >= 0 && xfinal < width) {
  1019. bitmap[yfinal * width * 3 + xfinal * 3] = capture.buffer[srcIdx];
  1020. bitmap[yfinal * width * 3 + xfinal * 3 + 1] = capture.buffer[srcIdx + 1];
  1021. bitmap[yfinal * width * 3 + xfinal * 3 + 2] = capture.buffer[srcIdx + 2];
  1022. }
  1023. }
  1024. }
  1025. delete[] capture.buffer;
  1026. }
  1027. if (extStr == ".bmp") {
  1028. stbi_write_bmp (filename.c_str (), width, height, 3, bitmap);
  1029. } else if (extStr == ".png") {
  1030. stbi_write_png (filename.c_str (), width, height, 3, bitmap, width * 3);
  1031. } else if (extStr == ".jpg" || extStr == ".jpeg") {
  1032. stbi_write_jpg (filename.c_str (), width, height, 3, bitmap, 100);
  1033. }
  1034. delete[] bitmap;
  1035. }).detach ();
  1036. }
  1037. void WallpaperApplication::setupOutput () {
  1038. const char* XDG_SESSION_TYPE = getenv ("XDG_SESSION_TYPE");
  1039. if (!XDG_SESSION_TYPE) {
  1040. sLog.exception (
  1041. "Cannot read environment variable XDG_SESSION_TYPE, window server detection failed. Please ensure proper "
  1042. "values are set"
  1043. );
  1044. }
  1045. sLog.debug ("Checking for window servers: ");
  1046. for (const auto& windowServer : sVideoFactories.getRegisteredDrivers ()) {
  1047. sLog.debug ("\t", windowServer);
  1048. }
  1049. this->m_videoDriver = sVideoFactories.createVideoDriver (
  1050. this->m_context.settings.render.mode, XDG_SESSION_TYPE, this->m_context, *this
  1051. );
  1052. this->m_fullScreenDetector
  1053. = sVideoFactories.createFullscreenDetector (XDG_SESSION_TYPE, this->m_context, *this->m_videoDriver);
  1054. }
  1055. void WallpaperApplication::setupAudio () {
  1056. // ensure audioprocessing is required by any background, and we have it enabled
  1057. const bool audioProcessingRequired = std::ranges::any_of (
  1058. this->m_backgrounds, [] (const std::pair<const std::string, ProjectUniquePtr>& pair) -> bool {
  1059. return pair.second->supportsAudioProcessing;
  1060. }
  1061. );
  1062. if (audioProcessingRequired && this->m_context.settings.audio.audioprocessing) {
  1063. this->m_audioRecorder
  1064. = std::make_unique<WallpaperEngine::Audio::Drivers::Recorders::PulseAudioPlaybackRecorder> ();
  1065. } else {
  1066. this->m_audioRecorder = std::make_unique<WallpaperEngine::Audio::Drivers::Recorders::PlaybackRecorder> ();
  1067. }
  1068. if (this->m_context.settings.audio.automute) {
  1069. m_audioDetector = std::make_unique<WallpaperEngine::Audio::Drivers::Detectors::PulseAudioPlayingDetector> (
  1070. this->m_context, *this->m_fullScreenDetector
  1071. );
  1072. } else {
  1073. m_audioDetector = std::make_unique<WallpaperEngine::Audio::Drivers::Detectors::AudioPlayingDetector> (
  1074. this->m_context, *this->m_fullScreenDetector
  1075. );
  1076. }
  1077. m_audioDriver = std::make_unique<WallpaperEngine::Audio::Drivers::SDLAudioDriver> (
  1078. this->m_context, *this->m_audioDetector, *this->m_audioRecorder
  1079. );
  1080. m_audioContext = std::make_unique<WallpaperEngine::Audio::AudioContext> (*m_audioDriver);
  1081. }
  1082. void WallpaperApplication::prepareOutputs () {
  1083. m_renderContext
  1084. = std::make_unique<WallpaperEngine::Render::RenderContext> (*m_videoDriver, *this, *this->m_mediaSource);
  1085. // set all the specific wallpapers required (skip span group synthetic keys)
  1086. for (const auto& [background, info] : this->m_backgrounds) {
  1087. if (background.rfind ("span:", 0) == 0) {
  1088. continue;
  1089. }
  1090. const auto scalingIt = this->m_context.settings.general.screenScalings.find (background);
  1091. const auto clampIt = this->m_context.settings.general.screenClamps.find (background);
  1092. const auto scaling = scalingIt != this->m_context.settings.general.screenScalings.end ()
  1093. ? scalingIt->second
  1094. : this->m_context.settings.render.window.scalingMode;
  1095. const auto clamp = clampIt != this->m_context.settings.general.screenClamps.end ()
  1096. ? clampIt->second
  1097. : this->m_context.settings.render.window.clamp;
  1098. auto wallpaper = WallpaperEngine::Render::CWallpaper::fromWallpaper (
  1099. *info->wallpaper, *m_renderContext, *m_audioContext, this->resolveScreenBackgroundPath (background),
  1100. scaling, clamp, this->resolveScreenRenderSize (background)
  1101. );
  1102. wallpaper->setZoom (this->resolveScreenZoom (background));
  1103. wallpaper->setCornerColor (this->resolveScreenCornerColor (background));
  1104. m_renderContext->setWallpaper (background, std::move (wallpaper));
  1105. }
  1106. // Set up span groups: one shared wallpaper per group, registered for each viewport
  1107. for (const auto& spanGroup : this->m_context.settings.general.spanGroups) {
  1108. if (spanGroup.screens.empty ()) {
  1109. continue;
  1110. }
  1111. const std::string groupKey = "span:" + spanGroup.screens.front ();
  1112. const auto bgIt = this->m_backgrounds.find (groupKey);
  1113. if (bgIt == this->m_backgrounds.end ()) {
  1114. continue;
  1115. }
  1116. // Compute the bounding box of all viewports in this span group
  1117. const auto& viewports = m_renderContext->getOutput ().getViewports ();
  1118. int minX = INT_MAX, minY = INT_MAX, maxX = INT_MIN, maxY = INT_MIN;
  1119. bool anyFound = false;
  1120. for (const auto& screenName : spanGroup.screens) {
  1121. const auto vpIt = viewports.find (screenName);
  1122. if (vpIt == viewports.end ()) {
  1123. sLog.error ("Span group screen not found: ", screenName);
  1124. continue;
  1125. }
  1126. anyFound = true;
  1127. const auto& vp = vpIt->second;
  1128. const int x = vp->globalPosition.x;
  1129. const int y = vp->globalPosition.y;
  1130. const int w = vp->logicalSize.x;
  1131. const int h = vp->logicalSize.y;
  1132. sLog.debug (
  1133. "SPAN DEBUG prepareOutputs: screen '", screenName, "' globalPos=(", x, ",", y, ") logicalSize=", w, "x",
  1134. h
  1135. );
  1136. minX = std::min (minX, x);
  1137. minY = std::min (minY, y);
  1138. maxX = std::max (maxX, x + w);
  1139. maxY = std::max (maxY, y + h);
  1140. }
  1141. if (!anyFound) {
  1142. sLog.error ("No viewports found for span group, skipping");
  1143. continue;
  1144. }
  1145. sLog.debug (
  1146. "SPAN DEBUG prepareOutputs: bounding box=(", minX, ",", minY, ",", maxX - minX, ",", maxY - minY, ")"
  1147. );
  1148. WallpaperEngine::Render::CWallpaper::SpanInfo spanInfo;
  1149. spanInfo.totalBounds = { minX, minY, maxX - minX, maxY - minY };
  1150. // Create one shared wallpaper with the span group's scaling mode
  1151. auto sharedWallpaper = WallpaperEngine::Render::CWallpaper::fromWallpaper (
  1152. *bgIt->second->wallpaper, *m_renderContext, *m_audioContext, this->resolveScreenBackgroundPath (groupKey),
  1153. spanGroup.scaling, spanGroup.clamp, glm::ivec2 { maxX - minX, maxY - minY }
  1154. );
  1155. sharedWallpaper->setZoom (spanGroup.zoom);
  1156. sharedWallpaper->setCornerColor (spanGroup.cornerColor);
  1157. // Convert to shared_ptr so it can be registered for multiple viewports
  1158. std::shared_ptr<WallpaperEngine::Render::CWallpaper> shared (std::move (sharedWallpaper));
  1159. shared->setSpanInfo (spanInfo);
  1160. // Register the same wallpaper for each screen in the span group
  1161. for (const auto& screenName : spanGroup.screens) {
  1162. m_renderContext->setWallpaper (screenName, shared);
  1163. }
  1164. }
  1165. }
  1166. void WallpaperApplication::setupOpenGLDebugging () {
  1167. #if !NDEBUG
  1168. glDebugMessageCallback (CustomGLDebugCallback, nullptr);
  1169. glEnable (GL_DEBUG_OUTPUT_SYNCHRONOUS);
  1170. #endif
  1171. }
  1172. void WallpaperApplication::setup () {
  1173. this->setupOutput ();
  1174. // we_manager launches us with LD_PRELOAD forcing the system's libEGL, because CEF's own
  1175. // bundled libEGL sits next to us on LD_LIBRARY_PATH and would otherwise shadow it and break
  1176. // our own Wayland/EGL output. That preload has already done its job for our own process by
  1177. // this point (setupOutput() above just created our GL context with it) - but it stays in the
  1178. // environment we inherit, and CEF's own subprocesses (spawned lazily, e.g. the GPU process
  1179. // the moment a web wallpaper's WebGL content first needs one) would inherit it too, forcing
  1180. // Mesa's system EGL onto CEF's ANGLE stack instead of the bundled EGL it actually expects.
  1181. // That silently breaks WebGL - no crash, no error, just a canvas that never paints anything.
  1182. // Unsetting it here only affects processes we fork+exec after this point; it doesn't undo
  1183. // anything already loaded into our own process by the dynamic linker at our own startup.
  1184. unsetenv ("LD_PRELOAD");
  1185. this->setupAudio ();
  1186. this->prepareOutputs ();
  1187. this->setupOpenGLDebugging ();
  1188. if (this->m_context.settings.general.dumpStructure) {
  1189. auto prettyPrinter = Data::Dumpers::StringPrinter ();
  1190. for (const auto& [background, info] : this->m_renderContext->getWallpapers ()) {
  1191. prettyPrinter.printWallpaper (info->getWallpaperData ());
  1192. }
  1193. std::cout << prettyPrinter.str () << std::endl;
  1194. }
  1195. #if DEMOMODE
  1196. // ensure only one background is running so everything can be properly caught
  1197. if (this->m_renderContext->getWallpapers ().size () > 1) {
  1198. sLog.exception ("Demo mode only supports one background");
  1199. }
  1200. int width = this->m_renderContext->getWallpapers ().begin ()->second->getWidth ();
  1201. int height = this->m_renderContext->getWallpapers ().begin ()->second->getHeight ();
  1202. std::vector<uint8_t> pixels (width * height * 3);
  1203. bool initialized = false;
  1204. int frame = 0;
  1205. #endif /* DEMOMODE */
  1206. }
  1207. void WallpaperApplication::render () {
  1208. static time_t seconds;
  1209. static struct tm* timeinfo;
  1210. static float rawTimeLast = 0.0f;
  1211. if (this->m_isPaused) {
  1212. usleep (FULLSCREEN_CHECK_WAIT_TIME);
  1213. if (this->m_fullScreenDetector->anythingFullscreen () && this->m_context.state.general.keepRunning) {
  1214. return;
  1215. }
  1216. m_renderContext->setPause (false);
  1217. // account for paused duration in playlist timers
  1218. const auto pausedNow = std::chrono::steady_clock::now ();
  1219. const auto pausedDuration = pausedNow - this->m_pauseStart;
  1220. for (auto& [_, playlist] : this->m_activePlaylists) {
  1221. if (!playlist.definition.settings.updateOnPause) {
  1222. playlist.nextSwitch += pausedDuration;
  1223. playlist.lastUpdate += pausedDuration;
  1224. }
  1225. }
  1226. this->m_isPaused = false;
  1227. } else {
  1228. time (&seconds);
  1229. timeinfo = localtime (&seconds);
  1230. g_Daytime = static_cast<float> ((timeinfo->tm_hour * 60) + timeinfo->tm_min) / (24.0f * 60.0f);
  1231. const float rawTimeNow = m_videoDriver->getRenderTime ();
  1232. const float rawDelta = rawTimeNow - rawTimeLast;
  1233. rawTimeLast = rawTimeNow;
  1234. g_TimeLast = g_Time;
  1235. g_Time += rawDelta * this->m_context.settings.render.playbackSpeed;
  1236. g_RealTime = rawTimeNow;
  1237. m_audioDriver->update ();
  1238. m_mediaSource->update ();
  1239. m_videoDriver->getInputContext ().update ();
  1240. m_videoDriver->dispatchEventQueue ();
  1241. if (m_videoDriver->closeRequested ()) {
  1242. sLog.out ("Stop requested by driver");
  1243. this->m_context.state.general.keepRunning = false;
  1244. }
  1245. #if DEMOMODE
  1246. // wait for a full render cycle before actually starting
  1247. // this gives some extra time for video and web decoders to set themselves up
  1248. // because of size changes
  1249. if (m_videoDriver->getFrameCounter () > (uint32_t)this->m_context.settings.render.maximumFPS) {
  1250. if (!initialized) {
  1251. width = this->m_renderContext->getWallpapers ().begin ()->second->getWidth ();
  1252. height = this->m_renderContext->getWallpapers ().begin ()->second->getHeight ();
  1253. pixels.reserve (width * height * 3);
  1254. init_encoder ("output.webm", width, height);
  1255. initialized = true;
  1256. }
  1257. glBindFramebuffer (
  1258. GL_FRAMEBUFFER, this->m_renderContext->getWallpapers ().begin ()->second->getWallpaperFramebuffer ()
  1259. );
  1260. glPixelStorei (GL_PACK_ALIGNMENT, 1);
  1261. glReadPixels (0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels.data ());
  1262. write_video_frame (pixels.data ());
  1263. frame++;
  1264. if (frame >= FRAME_COUNT) {
  1265. this->m_context.state.general.keepRunning = false;
  1266. }
  1267. }
  1268. #endif /* DEMOMODE */
  1269. if (this->m_fullScreenDetector->anythingFullscreen () && this->m_context.state.general.keepRunning) {
  1270. this->m_isPaused = true;
  1271. this->m_pauseStart = std::chrono::steady_clock::now ();
  1272. m_renderContext->setPause (true);
  1273. return;
  1274. }
  1275. }
  1276. this->checkHotswapRequest ();
  1277. this->updatePlaylists ();
  1278. if (!this->m_context.settings.screenshot.take || this->m_screenShotTaken == true) {
  1279. return;
  1280. }
  1281. if (this->m_videoDriver->getFrameCounter () < this->m_nextFrameScreenshot) {
  1282. return;
  1283. }
  1284. this->takeScreenshot (this->m_context.settings.screenshot.path);
  1285. this->m_screenShotTaken = true;
  1286. }
  1287. void WallpaperApplication::cleanup () {
  1288. sLog.out ("Stopping");
  1289. #if DEMOMODE
  1290. close_encoder ();
  1291. #endif /* DEMOMODE */
  1292. SDL_Quit ();
  1293. }
  1294. void WallpaperApplication::show () {
  1295. setup ();
  1296. while (this->m_context.state.general.keepRunning) {
  1297. render ();
  1298. }
  1299. cleanup ();
  1300. }
  1301. void WallpaperApplication::update (Render::Drivers::Output::OutputViewport* viewport) {
  1302. m_renderContext->render (viewport);
  1303. }
  1304. void WallpaperApplication::signal (int signal) {
  1305. if (signal == SIGUSR1) {
  1306. // keep the handler itself trivial, the actual reload happens on the render thread
  1307. this->m_hotswapRequested = true;
  1308. return;
  1309. }
  1310. sLog.out ("Stop requested by signal ", signal);
  1311. this->m_context.state.general.keepRunning = false;
  1312. }
  1313. const std::map<std::string, ProjectUniquePtr>& WallpaperApplication::getBackgrounds () const {
  1314. return this->m_backgrounds;
  1315. }
  1316. ApplicationContext& WallpaperApplication::getContext () const { return this->m_context; }
  1317. const WallpaperEngine::Render::Drivers::Output::Output& WallpaperApplication::getOutput () const {
  1318. return this->m_renderContext->getOutput ();
  1319. }
  1320. void WallpaperApplication::setDestinationFramebuffer (GLuint framebuffer) {
  1321. this->m_destinationFramebuffer = framebuffer;
  1322. for (const auto& [screen, wallpaper] : this->m_renderContext->getWallpapers ()) {
  1323. wallpaper->setDestinationFramebuffer (framebuffer);
  1324. };
  1325. }
  1326. GLuint WallpaperApplication::getDestinationFramebuffer () const { return this->m_destinationFramebuffer; }