WallpaperApplication.cpp 70 KB

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