#!/usr/bin/env python3 """Real Wallpaper Engine reference frames under Wine, a fresh WE instance per item. render_we.py [ids...] (default: every item in the scan.py database) render_we.py ... test scenes, frames go to $REFS/ Writes $REFS//f*.png (default ~/.local/share/we_live/refs; full :98 screen, window at +4+30) + we.log. Items that already have frames are skipped unless FORCE=1. Instead of fixed sleeps it waits until the window isn't a flat colour any more (the first rendered frame), then SETTLE seconds, then FRAMES frames INTERVAL seconds apart (env overrides). -control openWallpaper sent to a running instance doesn't switch wallpapers under Wine (it only brings the UI window back), hence one instance per item. Audio is pointed at dead sockets, see README.md. Setup of the Wine prefix: README.md. """ import os, subprocess, sys, time import numpy as np import mss from PIL import Image from Xlib import display as xdisplay S = os.environ.get('WE_LIVE_DIR', os.path.expanduser('~/.local/share/we_live')) HERE = os.path.dirname(os.path.abspath(__file__)) + '/wine' W = os.environ.get('LWE_WORKSHOP_DIR', '/workspace/SteamLibrary/steamapps/workshop/content/431960') LIB = os.environ.get('LWE_LIBRARY_DIR', os.path.expanduser('~/.local/share/lwe-library')) REFS = os.environ.get('REFS', S + '/refs') D = ':98' FRAMES = int(os.environ.get('FRAMES', 5)) INTERVAL = float(os.environ.get('INTERVAL', 1.0)) SETTLE = float(os.environ.get('SETTLE', 4.0)) TIMEOUT = float(os.environ.get('SWITCH_TIMEOUT', 30)) RETRIES = int(os.environ.get('RETRIES', 3)) # Wine must only see the isolated :98, not the desktop session (wayland-0, D-Bus) in the shared runtime dir env = {k: v for k, v in os.environ.items() if k not in ('WAYLAND_DISPLAY', 'WAYLAND_SOCKET', 'DBUS_SESSION_BUS_ADDRESS')} env = dict(env, XDG_RUNTIME_DIR=S + '/run', WINEPREFIX=S + '/wineprefix', DISPLAY=D, WINEDEBUG='-all', DXVK_LOG_PATH=S, PULSE_SERVER=f'unix:{S}/run/no-pulse', PIPEWIRE_REMOTE=f'{S}/run/no-pipewire', ALSA_CONFIG_PATH=S + '/run/asound-null.conf') WINE = '/usr/lib/wine/wine64' we_proc = None log = None def winpath(p): return 'Z:' + p.replace('/', '\\') def grab(m): s = m.grab({'left': 0, 'top': 0, 'width': 1920, 'height': 1080}) return Image.frombytes('RGB', s.size, s.rgb) def small(img): return np.asarray(img.crop((4, 30, 1920, 1080)).resize((240, 131))).astype(np.float32) def hide_ui(): d = xdisplay.Display(D) root = d.screen().root def walk(w): # windows can disappear while walking the tree try: children = w.query_tree().children except Exception: return for c in children: yield c yield from walk(c) for w in walk(root): try: if w.get_wm_name() == 'Wallpaper UI': w.unmap() except Exception: pass try: top = root.query_tree().children except Exception: top = [] for w in top: try: g = w.get_geometry() if w.get_attributes().map_state == 2 and min(g.width, g.height) <= 20 and (g.width, g.height) != (1, 1): w.unmap() except Exception: pass d.sync() d.close() def start_we(first): global we_proc, log subprocess.run(['/usr/lib/wine/wineserver', '-k'], env=env, stderr=subprocess.DEVNULL) time.sleep(1) log = open(S + '/we_run_last.log', 'w') we_proc = subprocess.Popen([WINE, 'wallpaper64.exe', '-control', 'openWallpaper', '-file', winpath(first), '-monitor', '0', '-playInWindow', '-silent'], cwd=S + '/we28', env=env, stdout=log, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, start_new_session=True) def wait_rendered(m, t0, timeout): """until the window isn't a flat colour any more, or timeout""" while time.time() - t0 < timeout: hide_ui() if small(grab(m)).std() > 3: return True time.sleep(0.3) return False def item_file(id_, info): if info.get('type') == 'scene': # "file" is scene.json, or gifscene.json for gif scenes, packed into the .pkg of the same name name = info.get('file') or 'scene.json' pkg = os.path.join(W, id_, os.path.splitext(name)[0] + '.pkg') return pkg if os.path.exists(pkg) else os.path.join(W, id_, name) return os.path.join(W, id_, 'project.json') def main(): import json, glob db = json.load(open(LIB + '/features.json')) ids = sys.argv[1:] or sorted(db) if not os.environ.get('FORCE'): ids = [i for i in ids if os.path.exists(i) or not glob.glob(f'{REFS}/{i}/f*.png')] out_root = REFS files = [] for n, i in enumerate(ids): if os.path.exists(i): path = os.path.abspath(i) files.append(path + '/scene.json' if os.path.isdir(path) else path) ids[n] = os.path.basename(path if os.path.isdir(path) else os.path.dirname(path)) else: files.append(item_file(i, db.get(i, {}).get('info', {}))) subprocess.run(['bash', HERE + '/we_display.sh'], stdout=subprocess.DEVNULL, check=True) with mss.MSS(display=D) as m: for n, f in enumerate(files): id_ = ids[n] out = os.path.join(out_root, id_) os.makedirs(out, exist_ok=True) for old in os.listdir(out): os.remove(os.path.join(out, old)) t0 = time.time() # scenes with video textures sometimes never get past a flat first frame under Wine, a new # instance usually does for attempt in range(RETRIES): start = time.time() start_we(f) # the killed instance's window must be gone before "not flat" means anything while time.time() - start < 10 and small(grab(m)).std() > 3: time.sleep(0.2) ok = wait_rendered(m, start, TIMEOUT) if ok: break time.sleep(SETTLE) hide_ui() t1 = time.time() last = None for i in range(FRAMES): last = grab(m) last.save('%s/f%03d_%.1f.png' % (out, i, time.time() - t1)) time.sleep(INTERVAL) log.flush() try: with open(S + '/we_run_last.log', errors='replace') as src, open(out + '/we.log', 'w') as dst: dst.write(src.read()[-20000:]) except OSError: pass print(f'we {id_} {FRAMES} frames{"" if ok else " (still flat)"} {time.time() - t0:.1f}s', flush=True) subprocess.run(['/usr/lib/wine/wineserver', '-k'], env=env, stderr=subprocess.DEVNULL) if __name__ == '__main__': main()