render_we.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. #!/usr/bin/env python3
  2. """Real Wallpaper Engine reference frames under Wine, a fresh WE instance per item.
  3. render_we.py [ids...] (default: every item in the scan.py database)
  4. render_we.py <dir|scene.json|scene.pkg> ... test scenes, frames go to $REFS/<dir name>
  5. Writes $REFS/<id>/f*.png (default ~/.local/share/we_live/refs; full :98 screen, window at +4+30) + we.log.
  6. Items that already have frames are skipped unless FORCE=1. Instead of fixed sleeps it waits until the window
  7. isn't a flat colour any more (the first rendered frame), then SETTLE seconds, then FRAMES frames INTERVAL
  8. seconds apart (env overrides). -control openWallpaper sent to a running instance doesn't switch wallpapers under
  9. Wine (it only brings the UI window back), hence one instance per item.
  10. Audio is pointed at dead sockets, see README.md. Setup of the Wine prefix: README.md.
  11. """
  12. import os, subprocess, sys, time
  13. import numpy as np
  14. import mss
  15. from PIL import Image
  16. from Xlib import display as xdisplay
  17. S = os.environ.get('WE_LIVE_DIR', os.path.expanduser('~/.local/share/we_live'))
  18. HERE = os.path.dirname(os.path.abspath(__file__)) + '/wine'
  19. W = os.environ.get('LWE_WORKSHOP_DIR', '/workspace/SteamLibrary/steamapps/workshop/content/431960')
  20. LIB = os.environ.get('LWE_LIBRARY_DIR', os.path.expanduser('~/.local/share/lwe-library'))
  21. REFS = os.environ.get('REFS', S + '/refs')
  22. D = ':98'
  23. FRAMES = int(os.environ.get('FRAMES', 5))
  24. INTERVAL = float(os.environ.get('INTERVAL', 1.0))
  25. SETTLE = float(os.environ.get('SETTLE', 4.0))
  26. TIMEOUT = float(os.environ.get('SWITCH_TIMEOUT', 30))
  27. RETRIES = int(os.environ.get('RETRIES', 3))
  28. # Wine must only see the isolated :98, not the desktop session (wayland-0, D-Bus) in the shared runtime dir
  29. env = {k: v for k, v in os.environ.items() if k not in ('WAYLAND_DISPLAY', 'WAYLAND_SOCKET', 'DBUS_SESSION_BUS_ADDRESS')}
  30. env = dict(env, XDG_RUNTIME_DIR=S + '/run', WINEPREFIX=S + '/wineprefix', DISPLAY=D, WINEDEBUG='-all', DXVK_LOG_PATH=S,
  31. PULSE_SERVER=f'unix:{S}/run/no-pulse', PIPEWIRE_REMOTE=f'{S}/run/no-pipewire', ALSA_CONFIG_PATH=S + '/run/asound-null.conf')
  32. WINE = '/usr/lib/wine/wine64'
  33. we_proc = None
  34. log = None
  35. def winpath(p):
  36. return 'Z:' + p.replace('/', '\\')
  37. def grab(m):
  38. s = m.grab({'left': 0, 'top': 0, 'width': 1920, 'height': 1080})
  39. return Image.frombytes('RGB', s.size, s.rgb)
  40. def small(img):
  41. return np.asarray(img.crop((4, 30, 1920, 1080)).resize((240, 131))).astype(np.float32)
  42. def hide_ui():
  43. d = xdisplay.Display(D)
  44. root = d.screen().root
  45. def walk(w):
  46. # windows can disappear while walking the tree
  47. try:
  48. children = w.query_tree().children
  49. except Exception:
  50. return
  51. for c in children:
  52. yield c
  53. yield from walk(c)
  54. for w in walk(root):
  55. try:
  56. if w.get_wm_name() == 'Wallpaper UI':
  57. w.unmap()
  58. except Exception:
  59. pass
  60. try:
  61. top = root.query_tree().children
  62. except Exception:
  63. top = []
  64. for w in top:
  65. try:
  66. g = w.get_geometry()
  67. if w.get_attributes().map_state == 2 and min(g.width, g.height) <= 20 and (g.width, g.height) != (1, 1):
  68. w.unmap()
  69. except Exception:
  70. pass
  71. d.sync()
  72. d.close()
  73. def start_we(first):
  74. global we_proc, log
  75. subprocess.run(['/usr/lib/wine/wineserver', '-k'], env=env, stderr=subprocess.DEVNULL)
  76. time.sleep(1)
  77. log = open(S + '/we_run_last.log', 'w')
  78. we_proc = subprocess.Popen([WINE, 'wallpaper64.exe', '-control', 'openWallpaper', '-file', winpath(first),
  79. '-monitor', '0', '-playInWindow', '-silent'], cwd=S + '/we28', env=env,
  80. stdout=log, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, start_new_session=True)
  81. def wait_rendered(m, t0, timeout):
  82. """until the window isn't a flat colour any more, or timeout"""
  83. while time.time() - t0 < timeout:
  84. hide_ui()
  85. if small(grab(m)).std() > 3:
  86. return True
  87. time.sleep(0.3)
  88. return False
  89. def item_file(id_, info):
  90. if info.get('type') == 'scene':
  91. # "file" is scene.json, or gifscene.json for gif scenes, packed into the .pkg of the same name
  92. name = info.get('file') or 'scene.json'
  93. pkg = os.path.join(W, id_, os.path.splitext(name)[0] + '.pkg')
  94. return pkg if os.path.exists(pkg) else os.path.join(W, id_, name)
  95. return os.path.join(W, id_, 'project.json')
  96. def main():
  97. import json, glob
  98. db = json.load(open(LIB + '/features.json'))
  99. ids = sys.argv[1:] or sorted(db)
  100. if not os.environ.get('FORCE'):
  101. ids = [i for i in ids if os.path.exists(i) or not glob.glob(f'{REFS}/{i}/f*.png')]
  102. out_root = REFS
  103. files = []
  104. for n, i in enumerate(ids):
  105. if os.path.exists(i):
  106. path = os.path.abspath(i)
  107. files.append(path + '/scene.json' if os.path.isdir(path) else path)
  108. ids[n] = os.path.basename(path if os.path.isdir(path) else os.path.dirname(path))
  109. else:
  110. files.append(item_file(i, db.get(i, {}).get('info', {})))
  111. subprocess.run(['bash', HERE + '/we_display.sh'], stdout=subprocess.DEVNULL, check=True)
  112. with mss.MSS(display=D) as m:
  113. for n, f in enumerate(files):
  114. id_ = ids[n]
  115. out = os.path.join(out_root, id_)
  116. os.makedirs(out, exist_ok=True)
  117. for old in os.listdir(out):
  118. os.remove(os.path.join(out, old))
  119. t0 = time.time()
  120. # scenes with video textures sometimes never get past a flat first frame under Wine, a new
  121. # instance usually does
  122. for attempt in range(RETRIES):
  123. start = time.time()
  124. start_we(f)
  125. # the killed instance's window must be gone before "not flat" means anything
  126. while time.time() - start < 10 and small(grab(m)).std() > 3:
  127. time.sleep(0.2)
  128. ok = wait_rendered(m, start, TIMEOUT)
  129. if ok:
  130. break
  131. time.sleep(SETTLE)
  132. hide_ui()
  133. t1 = time.time()
  134. last = None
  135. for i in range(FRAMES):
  136. last = grab(m)
  137. last.save('%s/f%03d_%.1f.png' % (out, i, time.time() - t1))
  138. time.sleep(INTERVAL)
  139. log.flush()
  140. try:
  141. with open(S + '/we_run_last.log', errors='replace') as src, open(out + '/we.log', 'w') as dst:
  142. dst.write(src.read()[-20000:])
  143. except OSError:
  144. pass
  145. print(f'we {id_} {FRAMES} frames{"" if ok else " (still flat)"} {time.time() - t0:.1f}s', flush=True)
  146. subprocess.run(['/usr/lib/wine/wineserver', '-k'], env=env, stderr=subprocess.DEVNULL)
  147. if __name__ == '__main__':
  148. main()