Files
MaxsTechReview 9f7f6d821d perf(epic): keep the hidden launcher from loading its store and Fab pages, and compress it while games run (#1764)
* perf(epic): keep the hidden launcher from loading its store and Fab pages, and compress it while games run

The hidden Epic Games Launcher that stays up for sign-in while a game runs pre-warmed its store and Fab pages on a display nobody sees. Under FEX each page used about one core and grew past a gigabyte, crashing and reloading in a loop. The hidden instance now skips the store pre-warm, gets a blank page in place of Fab, and is paged out to zRAM with the Steam client once a game has focus. The visible launcher is unchanged.

* test(zram): keep the scan fixture PIDs above the kernel PID range
2026-10-29 10:01:25 -04:00

242 lines
13 KiB
C

#if !defined(__x86_64__)
#error webkitinput is built for x86_64 guests only
#endif
typedef struct _WebKitSettings WebKitSettings;
typedef struct _WebKitUserContentManager WebKitUserContentManager;
typedef struct _WebKitUserScript WebKitUserScript;
extern void g_object_set(void *object, const char *first_property, ...) __attribute__((weak));
extern void *g_object_get_data(void *object, const char *key) __attribute__((weak));
extern void g_object_set_data(void *object, const char *key, void *data) __attribute__((weak));
extern void *dlsym(void *handle, const char *symbol) __attribute__((weak));
extern unsigned gtk_key_snooper_install(int (*snooper)(void *, void *, void *), void *data) __attribute__((weak));
extern void *gtk_widget_get_toplevel(void *widget) __attribute__((weak));
extern void gtk_container_forall(void *container, void (*callback)(void *, void *), void *data) __attribute__((weak));
extern unsigned long gtk_container_get_type(void) __attribute__((weak));
extern unsigned long webkit_web_view_get_type(void) __attribute__((weak));
extern int g_type_check_instance_is_a(void *instance, unsigned long type) __attribute__((weak));
extern const char *webkit_web_view_get_uri(void *view) __attribute__((weak));
extern void webkit_web_view_run_javascript(void *view, const char *script, void *cancellable, void *callback,
void *data) __attribute__((weak));
extern WebKitUserScript *webkit_user_script_new(const char *source, int frames, int time,
const char *const *allow, const char *const *block) __attribute__((weak));
extern void webkit_user_script_unref(WebKitUserScript *script) __attribute__((weak));
extern const char *g_getenv(const char *name) __attribute__((weak));
static const char NAV_SCRIPT[] =
"(()=>{let open=-1e9,arrowAt=-1e9,ringed=null,ringOutline='',ringOffset='',settleTimer=0,settling=false,redirecting=false;"
"const shown=()=>[...document.querySelectorAll('[role=dialog],[role=alertdialog],[aria-modal=true],[role=menu]')]"
".some(e=>e.getClientRects().length);"
"const hidden=e=>{const r=e.getBoundingClientRect();if(!r.width||!r.height)return true;"
"for(let n=e;n&&n.nodeType===1;n=n.parentElement){const s=getComputedStyle(n);"
"if(s.visibility==='hidden'||+s.opacity<0.1)return true;}return false;};"
"const windowButton=e=>/^(Minimize|Maximize|Restore|Close) window$/.test(e.getAttribute('aria-label')||'');"
"const editing=e=>!!e&&e.nodeType===1&&(e.isContentEditable||"
"e.matches('input:not([type=checkbox]):not([type=radio]):not([type=button]):not([type=submit]),textarea,select'));"
"const mainNav=()=>[...document.querySelectorAll('nav')].filter(n=>n.querySelectorAll('a[href]').length>1&&!hidden(n))"
".sort((a,b)=>a.getBoundingClientRect().y-b.getBoundingClientRect().y)[0];"
"const first=nav=>{const below=nav?nav.getBoundingClientRect().bottom:0,all=[];"
"for(const e of document.querySelectorAll('a[href],button,input,[role=button],[tabindex]')){"
"if(e.tabIndex<0||(nav&&nav.contains(e)))continue;const r=e.getBoundingClientRect();"
"if(r.top<below||r.bottom>innerHeight||windowButton(e)||hidden(e))continue;all.push({e,r});}"
"const big=all.filter(c=>c.r.width*c.r.height>=12000),list=big.length?big:all;"
"const key=c=>Math.round(c.r.top/8)*1e5+c.r.left;"
"list.sort((a,b)=>key(a)-key(b));return list.length?list[0].e:null;};"
"const big=e=>{const r=e.getBoundingClientRect();return r.width*r.height>=12000;};"
"const toward=(from,key,nav,small)=>{const r=from.getBoundingClientRect(),cx=r.left+r.width/2,cy=r.top+r.height/2;let best=null,score=1e9;"
"for(const c of document.querySelectorAll('a[href],button,[role=button],[tabindex]')){if(c===from||c.tabIndex<0||(nav&&nav.contains(c))||from.contains(c)||c.contains(from))continue;"
"const q=c.getBoundingClientRect();if(!small&&q.width*q.height<12000)continue;const qx=q.left+q.width/2,qy=q.top+q.height/2;let along,across;"
"if(key==='ArrowDown'){along=q.top-r.bottom;across=qx-cx;}else if(key==='ArrowUp'){along=r.top-q.bottom;across=qx-cx;}"
"else if(key==='ArrowRight'){along=q.left-r.right;across=qy-cy;}else{along=r.left-q.right;across=qy-cy;}"
"if(along<-4)continue;const d=Math.max(along,0)+2*Math.abs(across);if(d<score&&!windowButton(c)&&!hidden(c)){score=d;best=c;}}return best;};"
"const settle=()=>{const a=document.activeElement;if(editing(a)||shown())return;const nav=mainNav(),list=[];"
"for(const c of document.querySelectorAll('a[href],button,[role=button],[tabindex]')){if(c.tabIndex<0||(nav&&nav.contains(c)))continue;"
"const r=c.getBoundingClientRect();if(r.width*r.height>=12000&&r.top>=0&&r.left>=0&&r.bottom<=innerHeight&&r.right<=innerWidth)list.push({c,r});}"
"const key=x=>Math.round(x.r.top/8)*1e5+x.r.left;list.sort((x,y)=>key(x)-key(y));"
"for(const {c,r} of list){const hit=document.elementFromPoint(r.left+r.width/2,r.top+8);"
"if(hit&&(hit===c||c.contains(hit))&&!hidden(c)){arrowAt=performance.now();c.focus({preventScroll:true});return;}}};"
"const arm=()=>{clearTimeout(settleTimer);settleTimer=setTimeout(()=>{settling=false;settle();},200);};"
"addEventListener('scroll',()=>{if(settling)arm();},true);"
"const unring=()=>{if(ringed){ringed.style.outline=ringOutline;ringed.style.outlineOffset=ringOffset;ringed=null;}};"
"addEventListener('keydown',e=>{const k=e.key||'';if(k.startsWith('Arrow')){arrowAt=e.timeStamp;const a=document.activeElement;"
"if(!editing(a)&&!shown()){const nav=mainNav();"
"if(!a||a===document.body||hidden(a)||(k==='ArrowDown'&&nav&&nav.contains(a))){const f=first(nav);"
"if(f){e.preventDefault();e.stopImmediatePropagation();f.focus();f.scrollIntoView({block:'nearest'});}}"
"else if(big(a)&&!e.ctrlKey&&!e.altKey){let t=toward(a,k,nav);"
"if(!t&&(k==='ArrowUp'||k==='ArrowDown'))t=toward(a,k,nav,true);"
"if(t){e.preventDefault();e.stopImmediatePropagation();t.focus({preventScroll:true});t.scrollIntoView({block:'nearest',inline:'nearest'});}}}}"
"if(k==='Escape')open=shown()?e.timeStamp:-1e9;"
"if(e.code==='F13'){e.preventDefault();e.stopImmediatePropagation();settling=true;arm();}},true);"
"addEventListener('keydown',e=>{if(e.key!=='BrowserBack')return;e.preventDefault();e.stopImmediatePropagation();"
"const a=document.activeElement;if(editing(a)){a.blur();return;}"
"if(e.timeStamp-open>500)history.back();},true);"
"window.droiddeckTab=step=>{"
"const nav=mainNav();if(!nav)return;"
"const tabs=[...nav.querySelectorAll('a[href]')].filter(a=>!hidden(a)),part=p=>p.split('/')[1]||'';"
"const at=tabs.findIndex(a=>part(new URL(a.href).pathname)===part(location.pathname));"
"const to=tabs[Math.min(tabs.length-1,Math.max(0,at+step))];"
"if(at>=0&&to!==tabs[at]){to.click();to.blur();}};"
"const redirect=n=>{redirecting=true;try{n.focus();}finally{redirecting=false;}};"
"addEventListener('focusout',unring,true);"
"addEventListener('focusin',e=>{const t=e.target;if(!(t instanceof Element)||e.timeStamp-arrowAt>1000)return;"
"const back=e.relatedTarget instanceof Element&&e.relatedTarget.focus?e.relatedTarget:null;"
"if(!redirecting&&windowButton(t)){if(back)redirect(back);return;}"
"if(!redirecting&&hidden(t)){let a=t.parentElement;"
"while(a&&!(a.matches('a[href],button,[role=button],[tabindex]')&&!hidden(a)))a=a.parentElement;"
"if(a)redirect(a);else if(back)redirect(back);return;}"
"unring();ringed=(t.matches('input,textarea')&&t.closest('label'))||t;"
"ringOutline=ringed.style.outline;ringOffset=ringed.style.outlineOffset;"
"ringed.style.outline='3px solid #fff';ringed.style.outlineOffset='2px';},true);})();";
typedef struct {
int type;
void *window;
signed char send_event;
unsigned time;
unsigned state;
unsigned keyval;
} KeyEvent;
static void *launcher_view;
static void find_launcher_view(void *widget, void *data)
{
(void)data;
if (launcher_view)
return;
if (g_type_check_instance_is_a(widget, webkit_web_view_get_type())) {
const char *uri = webkit_web_view_get_uri(widget);
const char *prefix = "tauri:";
while (uri && *prefix && *uri == *prefix) {
uri++;
prefix++;
}
if (uri && !*prefix)
launcher_view = widget;
} else if (g_type_check_instance_is_a(widget, gtk_container_get_type())) {
gtk_container_forall(widget, find_launcher_view, 0);
}
}
static int switch_tab(void *grab, void *event_data, void *data)
{
KeyEvent *event = event_data;
(void)data;
if (!grab || event->type != 8 || !(event->state & 4) || (event->keyval != 0xff55 && event->keyval != 0xff56))
return 0;
launcher_view = 0;
find_launcher_view(gtk_widget_get_toplevel(grab), 0);
if (!launcher_view)
return 0;
webkit_web_view_run_javascript(launcher_view, event->keyval == 0xff56 ? "window.droiddeckTab&&droiddeckTab(1)"
: "window.droiddeckTab&&droiddeckTab(-1)",
0, 0, 0);
return 1;
}
static void watch_tab_keys(void)
{
static int watching;
if (watching || !gtk_key_snooper_install || !gtk_widget_get_toplevel || !gtk_container_forall ||
!gtk_container_get_type || !webkit_web_view_get_type || !g_type_check_instance_is_a ||
!webkit_web_view_get_uri || !webkit_web_view_run_javascript)
return;
watching = 1;
gtk_key_snooper_install(switch_tab, 0);
}
static void set(WebKitSettings *settings, const char *setter, const char *property, int value)
{
static int setting;
if (setting) {
void (*next)(WebKitSettings *, int) = dlsym ? (void (*)(WebKitSettings *, int))dlsym((void *)-1l, setter) : 0;
if (next)
next(settings, value);
return;
}
if (!g_object_set)
return;
setting = 1;
g_object_set(settings, property, value, "enable-smooth-scrolling", 0, "enable-spatial-navigation", 1, (const char *)0);
setting = 0;
}
#define SETTER(name, property) \
__attribute__((visibility("default"))) void webkit_settings_set_##name(WebKitSettings *settings, int value) \
{ \
set(settings, "webkit_settings_set_" #name, property, value); \
}
SETTER(enable_javascript, "enable-javascript")
SETTER(enable_developer_extras, "enable-developer-extras")
SETTER(enable_webgl, "enable-webgl")
SETTER(enable_webaudio, "enable-webaudio")
SETTER(enable_page_cache, "enable-page-cache")
SETTER(javascript_can_access_clipboard, "javascript-can-access-clipboard")
SETTER(javascript_can_open_windows_automatically, "javascript-can-open-windows-automatically")
SETTER(enable_back_forward_navigation_gestures, "enable-back-forward-navigation-gestures")
__attribute__((visibility("default"))) void webkit_user_content_manager_add_script(WebKitUserContentManager *manager,
WebKitUserScript *script)
{
static void (*next)(WebKitUserContentManager *, WebKitUserScript *);
if (!next && dlsym)
next = (void (*)(WebKitUserContentManager *, WebKitUserScript *))dlsym((void *)-1l, "webkit_user_content_manager_add_script");
if (!next)
return;
watch_tab_keys();
if (g_object_get_data && g_object_set_data && webkit_user_script_new && webkit_user_script_unref &&
!g_object_get_data(manager, "droiddeck-nav")) {
g_object_set_data(manager, "droiddeck-nav", (void *)1);
WebKitUserScript *nav = webkit_user_script_new(NAV_SCRIPT, 1, 0, 0, 0);
if (nav) {
next(manager, nav);
webkit_user_script_unref(nav);
}
}
next(manager, script);
}
static int starts(const char *text, const char *prefix)
{
while (*prefix)
if (*text++ != *prefix++)
return 0;
return 1;
}
static int fab_uri(const char *uri)
{
if (!uri)
return 0;
if (starts(uri, "https://"))
uri += 8;
else if (starts(uri, "http://"))
uri += 7;
else
return 0;
if (starts(uri, "www."))
uri += 4;
if (!starts(uri, "fab.com"))
return 0;
uri += 7;
return !*uri || *uri == '/' || *uri == '?' || *uri == '#' || *uri == ':';
}
static int hidden_launcher(void)
{
const char *hidden = g_getenv ? g_getenv("DROIDDECK_EPIC_HIDDEN") : 0;
return hidden && hidden[0] == '1' && !hidden[1];
}
__attribute__((visibility("default"))) void webkit_web_view_load_uri(void *view, const char *uri)
{
static void (*next)(void *, const char *);
if (!next && dlsym)
next = (void (*)(void *, const char *))dlsym((void *)-1l, "webkit_web_view_load_uri");
if (!next)
return;
next(view, hidden_launcher() && fab_uri(uri) ? "about:blank" : uri);
}