CWeb.cpp 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. // This code is a modification of the original projects that can be found at
  2. // https://github.com/if1live/cef-gl-example
  3. // https://github.com/andmcgregor/cefgui
  4. #include "CWeb.h"
  5. #include "WallpaperEngine/Application/WallpaperApplication.h"
  6. #include "WallpaperEngine/Logging/Log.h"
  7. #include "WallpaperEngine/Data/Model/Project.h"
  8. #include "WallpaperEngine/Data/Model/Wallpaper.h"
  9. #include <chrono>
  10. #include <csignal>
  11. #include <sys/prctl.h>
  12. #include <sys/syscall.h>
  13. #include <sys/wait.h>
  14. #include <thread>
  15. #include <unistd.h>
  16. using namespace WallpaperEngine::Render;
  17. using namespace WallpaperEngine::Render::Wallpapers;
  18. using namespace WallpaperEngine::WebBrowser::IPC;
  19. namespace {
  20. // Closes every fd but stdio, so the exec'd CEF child doesn't inherit the parent's
  21. // Wayland/EGL/X11/audio connections. Must be async-signal-safe: this runs in the fork()ed child
  22. // of a process that may have other threads running (SDL audio), so only a raw syscall - never
  23. // opendir/readdir, which allocate - is safe to call here before execv().
  24. void closeInheritedFds () {
  25. #if defined(SYS_close_range)
  26. syscall (SYS_close_range, 3u, ~0u, 0u);
  27. #endif
  28. }
  29. } // namespace
  30. CWeb::CWeb (
  31. const Wallpaper& wallpaper, RenderContext& context, AudioContext& audioContext,
  32. const std::filesystem::path& resolvedBackgroundPath, const WallpaperState::TextureUVsScaling& scalingMode,
  33. const uint32_t& clampMode, const glm::ivec2& maxRenderSize
  34. ) : CWallpaper (wallpaper, context, audioContext, scalingMode, clampMode) {
  35. this->setupFramebuffers ();
  36. // capacity for the shared frame buffer, clamped to by setSize() below - deliberately
  37. // maxRenderSize and not Output::getFullWidth/Height() (see fromWallpaper's doc comment), which
  38. // would badly oversize this for the common case of several separate, non-spanned wallpapers.
  39. this->m_shmMaxWidth = static_cast<uint32_t> (std::max (1, maxRenderSize.x));
  40. this->m_shmMaxHeight = static_cast<uint32_t> (std::max (1, maxRenderSize.y));
  41. this->m_shm = createSharedMemory (this->m_shmName, this->m_shmMaxWidth, this->m_shmMaxHeight);
  42. if (this->m_shm == nullptr) {
  43. sLog.exception ("CWeb: failed to create shared memory for the web host process");
  44. }
  45. this->m_shm->desiredWidth.store (static_cast<uint32_t> (this->m_width), std::memory_order_relaxed);
  46. this->m_shm->desiredHeight.store (static_cast<uint32_t> (this->m_height), std::memory_order_relaxed);
  47. this->spawnHost (resolvedBackgroundPath);
  48. }
  49. void CWeb::spawnHost (const std::filesystem::path& resolvedBackgroundPath) {
  50. auto& appContext = this->getContext ().getApp ().getContext ();
  51. const pid_t pid = fork ();
  52. if (pid < 0) {
  53. // sLog.exception() throws, aborting construction before ~CWeb() ever runs - without this,
  54. // the named shm segment createSharedMemory() already made would never be unlinked.
  55. closeSharedMemory (this->m_shm, this->m_shmName, this->m_shmMaxWidth, this->m_shmMaxHeight, true);
  56. sLog.exception ("CWeb: fork() failed while spawning the web host process");
  57. }
  58. if (pid == 0) {
  59. closeInheritedFds ();
  60. // kill this child if the parent dies without telling it to quit over shared memory
  61. prctl (PR_SET_PDEATHSIG, SIGTERM);
  62. std::vector<std::string> args;
  63. args.emplace_back (resolvedBackgroundPath.string ());
  64. args.emplace_back ("--web-host");
  65. args.emplace_back ("--web-host-shm");
  66. args.emplace_back (this->m_shmName);
  67. args.emplace_back ("--web-host-width");
  68. args.emplace_back (std::to_string (this->m_shmMaxWidth));
  69. args.emplace_back ("--web-host-height");
  70. args.emplace_back (std::to_string (this->m_shmMaxHeight));
  71. if (!appContext.settings.general.assets.empty ()) {
  72. args.emplace_back ("--assets-dir");
  73. args.emplace_back (appContext.settings.general.assets.string ());
  74. }
  75. std::vector<char*> argv;
  76. argv.reserve (args.size () + 2);
  77. // cosmetic only (what the child sees as its own argv[0]) - the executable actually run is
  78. // /proc/self/exe below, not this string, so it doesn't matter whether the original argv[0]
  79. // was absolute, relative, or PATH-resolved by the shell that launched us
  80. argv.push_back (appContext.getArgv ()[0]);
  81. for (auto& arg : args) {
  82. argv.push_back (arg.data ());
  83. }
  84. argv.push_back (nullptr);
  85. execv ("/proc/self/exe", argv.data ());
  86. // only reached if execv() itself failed
  87. _exit (127);
  88. }
  89. this->m_hostPid = pid;
  90. }
  91. void CWeb::setSize (const int width, const int height) {
  92. this->m_width = width > 0 ? width : this->m_width;
  93. this->m_height = height > 0 ? height : this->m_height;
  94. if (this->m_width <= 0 || this->m_height <= 0) {
  95. return;
  96. }
  97. // clamp to the capacity decided at construction time (m_shmMaxWidth/Height) - the host process
  98. // notices this changed and calls WasResized() itself, see runWebHost()
  99. this->m_shm->desiredWidth.store (
  100. std::min (static_cast<uint32_t> (this->m_width), this->m_shmMaxWidth), std::memory_order_relaxed
  101. );
  102. this->m_shm->desiredHeight.store (
  103. std::min (static_cast<uint32_t> (this->m_height), this->m_shmMaxHeight), std::memory_order_relaxed
  104. );
  105. }
  106. void CWeb::renderFrame (const glm::ivec4& viewport) {
  107. if (!this->m_helperFailureLogged && this->m_shm->helperFailed.load (std::memory_order_relaxed)) {
  108. sLog.error ("CWeb: host process failed to start, this web wallpaper will not render");
  109. this->m_helperFailureLogged = true;
  110. }
  111. // ensure the viewport matches the window size, and resize if needed
  112. if (viewport.z != this->getWidth () || viewport.w != this->getHeight ()) {
  113. this->setSize (viewport.z, viewport.w);
  114. }
  115. // ensure the virtual mouse position is up to date
  116. this->updateMouse (viewport);
  117. // use the scene's framebuffer by default
  118. glBindFramebuffer (GL_FRAMEBUFFER, this->getWallpaperFramebuffer ());
  119. // ensure we render over the whole framebuffer
  120. glViewport (0, 0, this->getWidth (), this->getHeight ());
  121. // Pull the latest frame the host process painted, if there's one we haven't uploaded yet.
  122. // Seqlock: frameSeq is even and stable while no write is in progress: odd means the host is
  123. // mid-write, and a value that changed across our read means we caught a write in progress and
  124. // may have read a torn frame - either way, just try again on a later call rather than block,
  125. // bounded so a host that's somehow writing every few microseconds can't stall rendering here.
  126. for (int attempt = 0; attempt < 4; attempt++) {
  127. const uint32_t seqBefore = this->m_shm->frameSeq.load (std::memory_order_acquire);
  128. if (seqBefore % 2 != 0 || seqBefore == this->m_lastUploadedSeq) {
  129. break;
  130. }
  131. const uint32_t frameWidth = this->m_shm->frameWidth.load (std::memory_order_relaxed);
  132. const uint32_t frameHeight = this->m_shm->frameHeight.load (std::memory_order_relaxed);
  133. if (frameWidth == 0 || frameHeight == 0) {
  134. break;
  135. }
  136. glBindTexture (GL_TEXTURE_2D, this->getWallpaperTexture ());
  137. // Only reallocate GPU storage (glTexImage2D) when the size actually changed - the common
  138. // case is the same size every frame, and glTexSubImage2D updating pixels in place avoids the
  139. // driver-side realloc/sync that a fresh glTexImage2D call triggers, up to 60 times a second.
  140. if (frameWidth == this->m_uploadedTextureWidth && frameHeight == this->m_uploadedTextureHeight) {
  141. glTexSubImage2D (
  142. GL_TEXTURE_2D, 0, 0, 0, static_cast<GLsizei> (frameWidth), static_cast<GLsizei> (frameHeight),
  143. GL_BGRA_EXT, GL_UNSIGNED_BYTE, this->m_shm->frameBuffer ()
  144. );
  145. } else {
  146. glTexImage2D (
  147. GL_TEXTURE_2D, 0, GL_RGBA, static_cast<GLsizei> (frameWidth), static_cast<GLsizei> (frameHeight), 0,
  148. GL_BGRA_EXT, GL_UNSIGNED_BYTE, this->m_shm->frameBuffer ()
  149. );
  150. this->m_uploadedTextureWidth = frameWidth;
  151. this->m_uploadedTextureHeight = frameHeight;
  152. }
  153. glBindTexture (GL_TEXTURE_2D, 0);
  154. const uint32_t seqAfter = this->m_shm->frameSeq.load (std::memory_order_acquire);
  155. if (seqAfter == seqBefore) {
  156. this->m_lastUploadedSeq = seqAfter;
  157. break;
  158. }
  159. // torn read, the host wrote a new frame while we were copying - retry
  160. }
  161. }
  162. void CWeb::updateMouse (const glm::ivec4& viewport) {
  163. auto& input = this->getContext ().getInputContext ().getMouseInput ();
  164. const glm::dvec2 position = input.position ();
  165. // Convert from OpenGL coordinates (Y=0 at bottom) to CEF coordinates (Y=0 at top). Written
  166. // unconditionally every frame - cheap atomic stores, and the host process itself only forwards
  167. // clicks to CEF when it observes the value actually changed.
  168. const int x = std::clamp (static_cast<int> (position.x - viewport.x), 0, viewport.z);
  169. const int y = viewport.w - std::clamp (static_cast<int> (position.y - viewport.y), 0, viewport.w);
  170. this->m_shm->mouseX.store (static_cast<double> (x), std::memory_order_relaxed);
  171. this->m_shm->mouseY.store (static_cast<double> (y), std::memory_order_relaxed);
  172. this->m_shm->leftClick.store (static_cast<int32_t> (input.leftClick ()), std::memory_order_relaxed);
  173. this->m_shm->rightClick.store (static_cast<int32_t> (input.rightClick ()), std::memory_order_relaxed);
  174. }
  175. CWeb::~CWeb () {
  176. this->m_shm->quitRequested.store (true, std::memory_order_release);
  177. // Give the host process a chance to close its CEF browser and shut down cleanly before falling
  178. // back to killing it outright - same bounded-wait philosophy this used when CEF was embedded
  179. // directly in this process (500 iterations there too, just pumping CEF's own message loop
  180. // instead of polling waitpid - there's no message loop to pump here anymore).
  181. bool exited = false;
  182. for (int i = 0; i < 500; i++) {
  183. int status = 0;
  184. if (waitpid (this->m_hostPid, &status, WNOHANG) == this->m_hostPid) {
  185. exited = true;
  186. break;
  187. }
  188. std::this_thread::sleep_for (std::chrono::milliseconds (10));
  189. }
  190. if (!exited) {
  191. kill (this->m_hostPid, SIGKILL);
  192. waitpid (this->m_hostPid, nullptr, 0);
  193. }
  194. closeSharedMemory (this->m_shm, this->m_shmName, this->m_shmMaxWidth, this->m_shmMaxHeight, true);
  195. }