Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_461fe3ff06374ecba693831148697ba6
34 KiB
Games on gex: Half-Life, Peggle Deluxe and Unity games
Stream G of the pewasm to gex merge. The goal is to run both games in gex's JIT
tier, first natively (gexc) and then in the browser (web/nt.html). Nothing
here is specific to one game: every fix lives in gex as general x86, Linux, NT,
or GL behaviour, and each one has its own test under tests/.
State as of 2026-10-03 (evening):
native (gexc) |
browser (nt.html, real GPU) |
|
|---|---|---|
| Half-Life (i386 Linux, GoldSrc) | plays c1a0, 16-29 fps | renders c1a0 (needs -nomsaa), 7-9 fps |
| Peggle Deluxe (i386 Windows, PopCap) | plays Stage One: name entry, shots, scoring; 7-12 fps | plays Stage One with the same scripted input; 3-5 fps |
Screenshots: work/games/screens/ (halflife-c1a0-{native,browser}.png,
peggle-stage1-{native,browser}.png).
Measurement scripts
Both games are workloads for the other streams. Each script prints ok lines
(time to the first frame or the map, time to gameplay, frames/s over
--measure seconds) or FAIL:
tests/games/peggle.sh [--browser] [--measure S] [--shot DIR] [-- GEXC-FLAGS]
tests/games/halflife.sh [--browser] [--map M] [--measure S] [--shot DIR] [-- GEXC-FLAGS]
- Native runs use
GEXC(defaulttarget/x86/release/gexc) in the JIT tier (--no-cgen); anything after--replaces those flags, so the same run measures--cgenor--fjit. Games run onCORES(default 5,11). --browserrunsbrowser-peggle.mjs/browser-halflife.mjs.GEX_WEBGPU_SOFTWARE=0picks the real GPU; SwiftShader is the default. They needweb/public/gexbuilt (GEX_ARCH=x86 shims/browser/build.sh,web/build-mesa-wasm.sh, and for Half-Lifeweb/build-xwayland-wasm.sh).- Peggle's input is
tests/games/peggle-input.txt:GEX_NT_INPUT, timed by GL presents, so the run is the same natively and in the page, from a profile-less start (click to play, the name "GEX", Stage One, its instructions, then shots). Gameplay is measured from its# gameplay Nframe. The native script runs on a linked copy of the prefix with an emptyProgramData(where Peggle keeps its profile). - Half-Life's workload is
+map c1a0, the tram ride, which plays the same way with no input. Its frames are counted from--wayland-outfile names (GEX_PPM_EVERY=10writes every 10th frame, named by its real index). - Defaults for Half-Life's libraries are
work/games/deps/{gl-i386,steam-i386, hl-fallback}and the game copywork/games/halflife; Peggle's iswork/games/peggle.
Measured on this machine with the scripts (JIT tier, shared cores):
| time to first frame | time to gameplay / map | gameplay fps | |
|---|---|---|---|
| Peggle native | 173 s | 332 s | 7.1 (120 s) |
| Peggle browser, real GPU | 43 s | 487 s | 3.4 (150 s) |
| Half-Life native | 429 s | 16.0 (60 s) | |
Half-Life browser, real GPU (-nomsaa) |
88 s | 7.2 (60 s) |
Native numbers include writing every 10th frame and the GL readback for it, and the cores were shared with other streams' builds; compare runs made the same way. Half-Life's native load (429 s) is far slower than the page's (88 s) and has not been looked at.
Half-Life
Native
You need:
- A Steam install of Half-Life. Use a copy (for example
work/games/halflife), because the game writes into its own directory. - gex's guest GL for i386:
GEX_CC="gcc -m32" guest/gl/build.sh(gives you libGL.so.1, libEGL, and libGLESv2 on gex-gl). - gex-steam for i386:
guest/steam/build.sh(libsteam_api with no Steam client, which provides an auth ticket and the private vtable slots that hw.so calls). - CEF's GTK2 dependencies from the Steam runtime
(
~/.local/share/Steam/ubuntu12_32/steam-runtime/usr/lib/i386-linux-gnu), linked into one directory (GEX_HL_FALLBACK). chromehtml.so (CEF) is loaded every time and needs an X display. Natively, the host's X display provides it.
cd work/games/halflife
gexc run --no-cgen --gl --wayland $XDG_RUNTIME_DIR/hlwl --wayland-size 800x600 \
--env LD_LIBRARY_PATH=$STEAM_I386:$GL_I386:$PWD:$FALLBACK \
--env SDL_VIDEODRIVER=wayland --env SDL_AUDIODRIVER=dummy --env HOME=$HLHOME \
./hl_linux -novid +map c1a0
Run games on cores 5 and 11 (taskset -c 5,11). Add --wayland-out DIR to
write every presented frame as a PPM. Screenshots:
work/games/screens/halflife-{menu,c1a0}-native.png.
Browser
TMPDIR=/tmp/gexg HL_DIR=... GEX_GL_I386=... GEX_STEAM_I386=... GEX_HL_FALLBACK=... \
node tests/games/browser-halflife.mjs --shot DIR --hl-args -nomsaa [--map c1a0]
TMPDIR has to be a short path. Otherwise Chromium's socket path is too long.
GEX_WEBGPU_SOFTWARE=0 selects the real GPU. The script bundles the game,
gex's libraries, the GTK2 fallback, and the host files that Half-Life opens
(tests/games/halflife-system-files.txt, collected with a native strace) into
web/public/gex/nt/halflife.tar. Delete the tar to rebuild it. The script
opens nt.html with exe=/hl/hl_linux&gl=1&glbackend=wgpu&hostx=1 and waits
for the map, then reports presents/s.
What made it this far:
hostx=1gives the page an X server (Xwayland as wasm,web/build-xwayland-wasm.sh). CEF/GTK get their X display from it, so no CEF stubs are needed./dev/shmis one filesystem shared by all processes in the browser shim (CEF's processes use shm_open across process boundaries).- Stale translations are now retired after each syscall in the browser shim (dlclose followed by dlopen at the same address made libudev jump into the middle of an instruction).
nt.htmlresolves the bundle's hard links and accepts Linux ELF programs.- EGL's default context version is now 1.0 with the compatibility profile. Before, it was 4.4 core, and HL could not build its main framebuffer.
?mesaenv=K=V,...passes environment variables to Mesa in the GL worker (for exampleMESA_DEBUG=incomplete_fbo).
It renders c1a0 at about 9 frames/s on the real GPU (-nomsaa).
The black window came from the bundle's hard links. nt.html turned each
hard link into a copy, and glibc's loader identifies a loaded library by
device and inode. So libGL.so.1 and libEGL.so.1 (one file on the host)
loaded as two libraries, each with its own gex-gl ring and its own GL device.
HL created its framebuffer's texture through one and attached it through the
other, which left the framebuffer incomplete. Hard links are now real links
in the VFS (gex_add_hardlink). tests/games/src/sdl-fbo-blit.c reproduces
HL's present path on its own, and info prints the GL version, the
framebuffer queries and the extension list.
Still open:
- MSAA: without
-nomsaa, the main framebuffer is incomplete (multisample FBO) on gex_wgpu. - V8 out of memory. One real-GPU run ran the renderer out of heap. Each guest thread is a Worker with its own copy of the dispatch tables, and they grow to millions of entries (table 4 reached 4.5M). Function-indexed tables (merge plan §9) remove these per-Worker tables; that work belongs to §9, not to this stream.
- SwiftShader: the WebGPU device is lost ("destroyed") during the run.
Peggle Deluxe
The game directory is Peggle Deluxe/. Make a run copy (work/games/peggle)
and extract the real executable from the DRM wrapper with
tests/games/popcap-unwrap.py Peggle.exe popcapgame1.exe. Stage the Wine prefix
with tools/nt-stage-wine.sh. Then run:
cd work/games/peggle
GEX_NT_STRICT=1 GEX_NT_INPUT=... gexc run --no-cgen --wine-prefix $PREFIX --gl --wayland-out DIR popcapgame1.exe
or tests/games/peggle.sh (above). The window is 1920x1080 with Peggle's
800x600 scaled to 1440x1080 in the middle; GEX_NT_INPUT coordinates are
client coordinates of that window.
What made it work:
- bass.dll's self-check (tier-1 SMC guard fix), HWND_BROADCAST messages.
- MMX lifted: j2k-codec decodes the JPEG 2000 images (logo, title screen).
- SetWindowPos on another thread's window runs on the owning thread (Wine's WM_WINE_SETWINDOWPOS). In the page, the compositor's Configure is handled by whichever thread peeks first, which for Peggle is a helper thread; before, that sent WM_WINDOWPOSCHANGING across threads and stopped the process.
- gex's
GEX_*settings reach the kernel in a page (nt::Config::settings,Process::setting), soGEX_NT_INPUTscripts work in the browser, andnt.html'senv=keeps commas inside a value.
Not needed so far: the GDI text WIP (crates/gex-kernel/src/nt/gdi/font.rs,
parked commit). Peggle draws all its text itself.
Open:
- Frame rate. Natively 7-12 fps in the JIT tier, in the page 3-5 fps, where
Peggle wants 100. Not profiled yet; the main thread is guest time
(
GEX_NT_STATS' time split in the page). - The page's window has a caption, so a 1920x1080 client area does not fit a 1920x1080 page (scrollbars). Input is injected at client coordinates, so the script is not affected.
Unity games (stream U)
Muck (Linux x86-64, Unity 2019.4.19f1), Hollow Knight (Windows x64, Unity 6000.0.61f1) and Rain World (Windows x64, Unity 2020.3.45f1). All three are Mono (MonoBleedingEdge), not IL2CPP. Run them in place from the Steam install, read-only; write prefixes, homes and logs elsewhere.
State as of 2026-10-04: Muck reaches its main menu natively (--fjit --mem64, status below); Rain World and Hollow Knight have not been run
since. Every one of them needs more address space than a wasm32 guest has
(below), which --mem64 provides.
Running
All three need a 64-bit guest memory: gexc run --mem64 (docs/mem64.md), or
nothing at all after the first run, which records the image when it runs out
of address space.
-
Muck (natively): guest GL for x86-64 (
GEX_CC=gcc guest/gl/build.sh DIR) and an X display: Xwayland run inside the guest, a client of gex's own compositor (no Xvfb, no host socket bridge). A small script started under gexc runsXwayland :N -geometry 800x600 -shm -ac -nolisten tcpwith noLD_LIBRARY_PATH(gex-gl's libGL is not Xwayland's), waits for/tmp/.X11-unix/XN, thenDISPLAY=:N execs the game (-ac: the socket exists only in the guest's namespace):GEX_FJIT_CACHE=1 gexc run --fjit --mem64 --gl --wayland $XDG_RUNTIME_DIR/x \ --wayland-size 800x600 --wayland-out DIR --env LD_LIBRARY_PATH=GLDIR \ --env HOME=... --env MONO_DEBUG=explicit-null-checks \ /bin/sh inguest.sh 71 XWLOG ./Muck.x86_64 -logFile LOG \ -screen-width 800 -screen-height 600 -screen-fullscreen 0Xwayland's lock and socket node land in the host's /tmp; remove only your display's afterwards. (A copy of the script: work/unity-resume/inguest.sh.) Unity 2019.4's SDL is X11-only (GLX through gex-gl's libGL). Its window's frames are presented by gex-gl on the host GPU and written to DIR (
GEX_PPM_EVERY=N,GEX_FPS=1). -
Hollow Knight / Rain World: a staged prefix (
tools/nt-stage-wine.sh),gexc run --fjit --mem64 --wine-prefix P --gl --wayland-out DIR game.exe. Hollow Knight's build has no OpenGL renderer (-force-glcorefails with "OpenGL Core was not built"), so it needs D3D11 on wined3d. Rain World:--env WINEDLLOVERRIDES=winhttp=bkeeps BepInEx's doorstop proxy (winhttp.dllin the game directory) from loading, without touching the install. -
Mono's null checks. Mono turns a fault on a null object reference into NullReferenceException, and gex has no page protection in the low region, so the fault does not happen. The run scripts set
MONO_DEBUG=explicit-null-checks(Mono configuration, not a game change). The general fix is low-page protection in gex: a fault on an access below the guest's floor, which other runtimes that rely on null faults (JVMs, .NET) need as well. -
Start-up time. Under
--fjitthe first run spends almost all of its time translating code it has not seen (Cranelift about 60%, recovery about 15%): Muck took over 15 minutes of wall time to reach Mono's assembly load, which takes 0.1 s natively.GEX_FJIT_CACHE=1keeps compiled translations on disk (about 150 KB each, about 1 GB for Muck's start-up) so later runs load them; AOT units for UnityPlayer.so and libmonobdwgc are the larger lever. -
What Unity needed from the kernel:
sched_getparamand friends (Mono aborts whenpthread_getschedparamfails),/proc/stat, and priority-inheritance futexes (FUTEX_LOCK_PI/UNLOCK_PI; without them aPTHREAD_PRIO_INHERITmutex never woke its waiter and the player deadlocked with every thread asleep). On Windows: crypt32's Unix side, rsaenh's registration,NtNotifyChangeKey,NtRenameKey, in-process named pipes (CreatePipe),SystemSupportedProcessorArchitectures. -
Steam. Without a Steam client, Muck's Host Lobby does nothing (its game is a Steam lobby; there is no offline mode), so Muck's gameplay needs an x86-64 gex-steam with lobby and loopback P2P. Hollow Knight stops on its title screen natively without Steam; Rain World gets through EOS errors.
-
Status (2026-10-04, later): Muck reaches its main menu natively under
--fjit --mem64(Host Lobby, Join Lobby, Settings, Achievements, About, Quit over the 3D island; work/games/screens/muck-menu-gex-fjit-mem64.png) at 17-23 fps at 800x600 on two cores (cores 1,7; a GL capture was being written, and there is no pewasm Unity run to pair it with). From a cold translation cache the menu took about 100 minutes of wall time; most of it is translation. What it took, each with a test: tier-1's self-modification guard read code bytes at an i32 address in an M64 module; the function JIT translated a JIT's writable-and-executable code without guards (it now leaves it to tier-1); tier-1 failed a miss whose page another thread's guard retired mid-translation (it now translates again); a worker thread'sexit_groupor fatal error left the process hanging; and gex-gl'sglMapBufferRangeshadow uploaded zeros over bytes not written or not flushed. Muck logs a NullReferenceException after "Initializing packets." (no Steam) and goes on. -
Slow menu frames look tiled. While a frame takes seconds, Muck's menu draws its scene tiled (8x11, later 23x30 copies). The GL capture shows that this is Muck's own data: its full-screen quad's texcoords are 0.5 +/- 11.5 (and 15.3 vertically, 4:3), sampled with REPEAT, and
_MainTex_STis (1,1,0,0). It looks like a per-frame zoom that is unstable at Unity's 0.333 s maximum deltaTime, and the menu draws correctly once frames are fast. -
Mono's code under the function JIT now runs on tier-1, each block checking its bytes on entry (
Block::guard). This is correct, but managed code gets no function-tier speed. What would recover it: a function-level byte check for code in writable-and-executable pages, with direct transfers into code not yet seen leaving through dispatch, or a store-side check of a code-page bitmap. -
Rain World (2026-10-04, paused for a reboot): run natively as
rw.shin work/unity-resume/rw/ does (--fjit --mem64 --wine-prefix P --gl,--env WINEDLLOVERRIDES=winhttp=b, 800x600, a copy of the staged prefix and a hard-linked copy of F's scratchpad/fjit/cgcache-peg units). Its build has no OpenGL Core shaders ("Forced GfxDevice 'OpenGL Core' was not built from editor"), so it runs D3D11 on wined3d, as Hollow Knight must: "Direct3D 11.0 [level 11.1]", Mono's reload completes (430-500 s cold), Rewired's input initializes, the first scene loads ("UnloadTime") and the window presents its first (black) frame. Fixed on the way, each with a test in tests/nt: NtSuspendThread/NtGetContextThread for Boehm's stop-the-world (it aborted with a message box), WINEDLLOVERRIDES (BepInEx's winhttp.dll doorstop loaded regardless and MonoMod died), the display configuration queries (Unity allocated 0xffffffff * 72 bytes), raw input, display device names, per-monitor DPI; and a regression of mine (a Windows worker thread's ExitThread ended the process). Where it stops: run r7 died about 10 minutes after the scene load of an unhandled illegal instruction in Mono's JIT code (0xa3ea00a, no module). gexc now logs the bytes (gex-nt: illegal instruction at); run r8 was stopped by the reboot before reaching that point (presents=1, 36 min in). Resume: rerunrw.sh r9 7200 --fjit --mem64(withRENDER=andCGDIR=.../cgpeg; about 40 minutes cold), read the illegal-instruction line, decode the bytes, fix the decoder or the trap's handling. Still unimplemented and seen: NtCreateUserProcess (the crash handler), NtSetInformationThread class 1000, NtUserSetWindowsHookEx, secur32's Unix side. -
Knobs:
GEX_FPS=1(presents per second),GEX_PPM_EVERY=N,--wayland-out null,GEX_STRACE_STACK=N(stack words at each syscall under--strace),GEX_NT_TRACE(logs each NtAllocateVirtualMemory request),GEX_FJIT_CACHE=1,GEX_DUMP_BAD_WASM=DIR(keep a translation the engine rejects as DIR/PC.wasm).
Address space: why none fits in wasm32
Unity's memory manager (MemoryManager::VirtualAllocator, named from Muck's
UnityPlayer_s.debug) gives every allocator block its own 256 MiB-aligned
slot and looks blocks up by address >> 28, so two blocks never share a
slot whatever their size. Baselib reserves each one by mapping 512 MiB - 4 KiB
PROT_NONE and trimming. A gex x86-64 guest has 2 GiB for mappings
(MMAP_TOP, the dispatch directory's reach), and the NT allocator 2 GiB.
Slots used, measured natively (Muck on Linux with an mmap backtrace shim,
the Windows games under host Wine 11.15 with WINEDEBUG=+virtual):
| game | slots at startup | what they are |
|---|---|---|
| Muck | 19 at the menu | 4 job temp allocators (ThreadsafeLinearAllocator), 1 bucket allocator, 1 main-thread temp (TLSAllocator), 9 dynamic-heap pools (main, per-thread, gfx, typetree, file system, ...), 4 large allocations (gfx command buffer, async upload queue, a texture, FMOD samples) |
| Hollow Knight | 17 at the title screen | (no symbols; same Baselib pattern) |
| Rain World | 16 after engine init | (no symbols; same pattern) |
Every allocation larger than a heap block takes a slot of its own, so the count grows with what a level loads. Launch knobs tried:
- Muck:
-job-worker-count 0/1,-force-gfx-st, both together, even-nographics: 19 every time. Unity 2019.4 has nomemorysetup-*keys. - Hollow Knight (Unity 6 reads
-memorysetup-KEY VALUEfrom the command line; theKEY=VALUEform is ignored): every heap block size at 256 MiB takes it from 17 to 13 at the first screen; 1 MiB blocks leave it at 17;-job-worker-count 1leaves it at 17. - Rain World (2020.3): only
-job-worker-count, no memorysetup keys.
Raising MMAP_TOP does not get there: up to 3 GiB costs a larger directory
(it must end below the non-PIE base 0x400000, which allows exactly 3 GiB) and,
for Windows guests, moving the host-call slots and trampolines at
0x30_0000-0x40_0000 that a larger directory runs into; past 3 GiB the
directory has to leave the low region, and in the browser the NT shim's heap
above 0x8000_0000 has to move. 3.75 GiB gives at most 14 slots, fewer once
images, stacks and the 512 MiB transient reservation are placed. So all
three need a 64-bit guest memory (docs/mem64.md).
Games matrix (stream G, 2026-10-05): untested non-.NET games
tests/games/matrix.sh (games listed in tests/games/matrix.txt) runs each
game natively under gexc --no-cgen --gl with a time limit, from a copy of
its directory (hard links, small files copied, so the originals are never
written) on a hard-linked copy of the staged prefix, and prints
slug arch stage frames secs end reason, keeping first.png/last.png.
Correctness only: no timing on gooner.
. ~/gex-sandbox/scratch/env.sh
guest/steam/build-win.sh ~/gex-sandbox/scratch/G/steam-win # gex-steam (below)
GEX_CC=gcc guest/gl/build.sh ~/gex-sandbox/scratch/G/gl-x64 # Linux x86-64 games
TMPDIR=~/gex-sandbox/scratch/G/tmp GEX_STEAM_WIN=~/gex-sandbox/scratch/G/steam-win \
GEX_GL_X64=~/gex-sandbox/scratch/G/gl-x64 tests/games/matrix.sh --time 300 --only undertale,katana-zero --out DIR
Run a copy of matrix.sh for long batches (editing the script under a running bash breaks it at the end of its loop). Never run a game in place: the first triage did, and wrote steam_appid.txt, logs and configs into games/ (removed again).
gex-steam for Windows (guest/steam/build-win.sh)
Without a Steam client the game's steam_api.dll fails, and GameMaker games
then quit or wait ("Steam must be running to play this game"). gex-steam's
Windows build replaces steam_api.dll/steam_api64.dll (matrix.sh swaps it in
the copy; the original is kept as .orig): clang for the MSVC ABI
(thiscall, callee-popped arguments, sret for aggregates, MSVC overload
order), every interface version (242, generated by gen_versions.py from
per-version headers: ~/src/gbe_fork/sdk/steam, a sparse clone of
github.com/Detanup01/gbe_fork, plus ~/src/Steamworks.NET/CodeGen/steam/steam_api.json;
neither is in the tree). Pre-1.37 accessors (SteamUser() ...) use the
versions found in the .orig DLL. Undertale and Katana ZERO get through
Steam init with it. GEX_STEAM_TRACE=1 (GAME_ENV) is meant to log
lookups but writes to the GUI process's stderr handle, which shows
nothing yet; use OutputDebugString next time.
Scope (user, 2026-10-05)
gex's NT layer is legacy: stream W's native Wine replaces it, and no more
NT-layer work is done. Stream G covers i386 Windows games on W's native
Wine (wt/wine-slim's gexc with a copy of scratch/W/pfx-gui-slim), Linux
builds, and browser runs of what reaches gameplay. x64 Windows games wait
for 64-bit native Wine. Isaac: Rebirth is SteamStub-wrapped (it relaunches
through steam://run): parked for the user. Antichamber's UDK.exe has a
managed entry point: it needs a CLR.
Where the games got on the NT layer (2026-10-05)
With gex-steam and the input scripts in tests/games/*-input.txt, natively, on master's translated-Wine path plus NT fixes that are now parked (below):
| game | arch | reached | notes |
|---|---|---|---|
| Undertale | i386 | gameplay: name entry, walking in the Ruins | menu frame 650, gameplay 1500 |
| Retrocycles | i386 | gameplay: a light-cycle match | menu 200, gameplay 1000; also on W's native Wine |
| Katana ZERO | i386 | menu | GameMaker's menu ignores clicks: keyboard script |
| N++ | i386 | first screen, garbled background | glClearBufferfv on the default framebuffer did nothing on gooner's llvmpipe pbuffer |
| Momodora | i386 | data loaded, no frame in 900 s | |
| 5D Chess | x64 | gameplay (a move) | waits for 64-bit native Wine |
| Geometry Dash | x64 | menu | waits for 64-bit native Wine |
| DELTARUNE, Will You Snail | x64 | not mapped | fixed base 0x140000000, no relocations: needs code above 4 GiB (docs/mem64.md) |
Screens: work/games/screens/ (undertale-ruins, retrocycles-gameplay,
katana-zero-menu, 5d-chess-board, geometry-dash-menu; -native.png).
What native Wine must provide (from these games)
Each of these stopped a game on the NT layer. The NT-layer versions, with tests in tests/nt, are parked on merge/games2 (not for master); native Wine needs the same behaviour:
- Window hooks (
SetWindowsHookEx,CallNextHookEx,CallMsgFilter): Katana ZERO, Spelunky 2, Hyper Light Drifter set a low-level keyboard hook. W's native-Wine path also stops here today (Katana ZERO,NtUserSetWindowsHookEx). Test: tests/nt/src/hooks.c. - GL uploads larger than 8 MiB (one ring command): Undertale's 2048x2048 texture pages (16 MiB), Retrocycles (8 MiB). W's path still hits "a GL command of 8388664 bytes" with Retrocycles. Test: glbig.c.
- COM classes registered (what wineboot's DllRegisterServer does): DirectInput8 for SDL_Init (N++, OneShot), DirectSound/MMDevice for SDL_mixer (5D Chess), WIC. W's path: Retrocycles' dsound gets REGDB_E_CLASSNOTREG. Test: com.c; a generator from the DLLs' WINE_REGISTRY resources is tools/gen_nt_classes.py on merge/games2.
- No audio device and no schannel without stopping: Wine's audio drivers (winepulse.drv ...) and secur32's Unix side absent, the program falls back to silence or no TLS.
- OpenGL 3.3 core-profile contexts (wglCreateContextAttribsARB with the core bit): 5D Chess (SDL2), Cruelty Squad (Godot 3). Test: glcore.c.
- I/O completion ports (Spelunky 2). Test: iocp.c.
\??\unix\...paths (Haxe: /etc/.haxepath; Dicey Dungeons). Test: unixpath.c.- GetKeyNameText (Spelunky 2), SetThreadExecutionState, SPI_GET/SETFOREGROUNDLOCKTIMEOUT and MOUSETRAILS (OneShot), VirtualLock (Spelunky 2): tests keyname.c, sysmisc.c.
- A synchronous RegNotifyChangeKeyValue that waits (Geometry Dash's watcher thread).
- NtAreMappedFilesTheSame (Geometry Dash), NtSetContextThread on another thread (Mosa Lina's HashLink GC), NtCreateUserProcess for crash handlers (Cruelty Squad): seen, not done.
On native Wine (master c5620656 + this branch, prefix a copy of scratch/W/pfx-gui-slim), 2026-10-06
| game | reached | stops on (repros for W in scratch/G/wrepro) |
|---|---|---|
| Undertale | gameplay (the Ruins), but the intro picture and the player's sprite are missing | 16 MiB texture uploads fail (glbig) |
| Retrocycles | gameplay | 8 MiB uploads (glbig), dsound's MMDevice class (com) |
| Momodora | 1300 black frames | uploads (glbig), wined3d draw-quad GL_INVALID_OPERATION, glGetShaderSource/glGetAttachedShaders |
| Katana ZERO | no frame | NtUserSetWindowsHookEx (hooks) |
| N++ | no frame | SDL_Init's DirectInput8 class (com) |
Linux builds (no Wine): Retrocycles reaches its language menu on Xwayland inside the guest (SDL 1.2 and libpng12 from Debian packages in LINUX_LIBS); Baba Is You gets past Steam (gex-steam Linux) and D-Bus autolaunch, creates its GL window, no frame yet. Input for Linux games needs scripted Wayland input (GEX_WL_INPUT, on stream U's branch).
Linux builds: scripted input, Steam, X11 (2026-10-06)
- Input:
GEX_WL_INPUT(master), timed in seconds from the Nth frame; matrix.sh passes a Linux game's input file (*.wl) as it. tests/games/retrocycles-linux.wl takes Retrocycles through its language menu and First Setup to the welcome screen. Withframes 1the clock started on Xwayland's first frame, long before the game's menu. - Steam: gex-steam for Linux (
guest/steam/build.sh OUT clang++ [-m32]; GCC 12 lacks-nostdlib++), swapped in by matrix.sh with GEX_STEAM_LINUX64. Smoke test: tests/games/steam-smoke.sh (i386 and x86-64, on the host). - X11: Baba Is You's static SDL2 has no Wayland, and SDL 1.2 has none, so
both run as
linux-x11on Xwayland inside the guest. Xwayland runs xkbcomp (translated) to compile its keymap, which takes minutes on a loaded gooner; X clients wait for it. - Baba Is You (Chowdren) needed: gex-steam with an older stats interface
(STEAMUSERSTATS 012), no D-Bus autolaunch (libdbus ran dbus-launch, which
waited on the display),
--lazy(eager translation of the 9 MB executable is 216 MB of wasm). It creates its GL window and stops after its renderer string, waiting on an X reply. - Retrocycles (Linux): gameplay natively (language menu, First Setup,
welcome, matches; tests/games/retrocycles-linux.wl, menu 650, gameplay
6000). gexc had died of SIGSEGV in the host's Mesa when the first match
started: the native GL host passed a glDrawElements' client-side index
pointer (guest memory) to the host's GL as an offset. Fixed in gex-gl's
native host (
b7556e59, with a test). - In the page (tests/games/browser-linux.mjs, hostx=1): Retrocycles starts, its X windows appear and Mesa draws (hundreds of thousands of draws), but the canvas stays black: a GLX window's frames on an X11 window do not reach the page's compositor yet (Half-Life's GL is on a Wayland surface; only its CEF uses X). Next: how the page shows a GL surface made for an X window (gexgl eglCreateWindowSurface with an X window id under hostx).
- Baba Is You: from its game directory (
@bin64/Chowdren) it finds its assets. It requires six GL extensions by name and showed "OpenGL extension 'GL_ARB_shader_objects' not supported." in an SDL message box (a forked child whose X font request failed, so it only hung; the lineRenderer: R - V -is meant to end empty). The guest GL withheld the extension for one missing command, glDeleteObjectARB; with it Baba renders. Run fullscreen (its default), the frames are cropped: SDL believes the window is the screen's size while the GL drawable keeps the window's 854x480 (no X window manager in the guest to carry the fullscreen request). It keeps its settings in ~/.local/share/Baba_Is_You from getpwuid(), not $HOME. - Xwayland in the guest:
gexc run --bindputs tests/games/xkbcomp-cache.sh over /usr/bin/xkbcomp (matrix.sh does it for linux-x11 games), so the keymap is compiled once (/tmp/gex-xkm-cache-UID) and later runs copy it. - GLX: contexts are what glXCreateContextAttribsARB asked for (tests/games/glx-context.sh: 3.2 core, 2.1 compatibility, ES 2.0 after SDL's throwaway context).
- The page, resolved (2026-10-06): Retrocycles renders in the page, windowed and fullscreen (work/games/screens/retrocycles-linux-language-browser.png). The black canvas was SDL 1.2's fullscreen: it maps an override-redirect window over the screen and reparents its GL window into it, so the unpainted window covered the one the frames were attached to. The guest GL now attaches to the top-level window and follows reparenting (tests/games/src/glx-quad.c shows it in the page). A correctness run on edger's GPU (one run, no timing) showed the compositor repainting normally, so there was no starvation; on gooner's SwiftShader the GL bridge still times out after a screen change (a batch over 30 s).
- Baba Is You: gameplay natively (2026-10-06; tests/games/baba.wl,
gameplay 4000): through its credits screen into the first level, where
Baba walks right, pushes the rocks and wins; the world map follows.
matrix.txt's
seed=seed/baba@~/.local/share/Baba_Is_Youmounts a fresh copy of tests/games/seed/baba (fullscreen=0) there for each run; nothing reaches the real home. What it took:- The "intro screen" is the credits screen (IGF, Nordic Game Jam, MP2), and it waits for a click; then a fresh save loads ~3,900 sprite PNGs on the main thread and goes straight into the first level (no title menu).
- Chowdren reads the mouse and keys as state once per game frame, so the
script's
clickandtap(press and release 0.1 s apart) fell inside one slow frame and were missed. baba.wl holds every press for a second or more (button 272 1/key right 1...0). - No key worked at first because the seed said
controlinit=1, which tells Baba its key bindings already exist; with none in the file it had no bindings at all. The seed now leaves controlinit out, so Baba writes its defaults. - X focus is not the problem for Baba (SDL focuses its own window), but matrix.sh now starts tests/games/xfocus.c (stream U's) for every linux-x11 game, since some need it (NOFOCUS=1 leaves it out).
- Runs that "stuck" before any frame were Xwayland waiting on xkbcomp: every process started under gex is translated afresh (a short one is seconds of Cranelift; on a loaded gooner, minutes), and the xkbcomp cache's cache hit was six processes (id, mkdir, cat, cmp, rm, cp). It now uses shell builtins and one cp (tests/games/xkbcomp-cache-test.sh).
- A gexc thread panic, "overflow when subtracting durations", in a timed
futex wait on a loaded machine (two clock readings around the deadline):
fixed in gex-native's perform_block (
82480885, test time_left_tests). --fjitwith GEX_FJIT_CACHE=1 caches Cranelift's output only; the analysis and wasm emission still run in every process. No tier-share line: Baba never exits on its own, and the counts print at exit.
- Retrocycles in the page on edger's GPU (2026-10-06, one correctness run,
no timing): first frame, the input script, past present 6000 (gameplay)
and on to the script's arrow keys. The compositor's
litreadback of its own output changed with each scene (3,127 to 104,775 sampled pixels), so the page drew the game; the harness's screenshots stayed black on the real GPU (Chrome's screenshot misses the WebGPU canvas there; on gooner's SwiftShader the same screenshots show the game). A readback-based screenshot in browser-linux.mjs would fix the pictures. - Process start under native gexc: the module cache already covers runtime
translations (a second run of
sh -c catadds no entries), so wasmtime's own cache would add nothing. Within one run, though, each new process's translations come out as different wasm and miss: about 60 identical short processes (scratch/G/hang/loop20.sh) left 1,008 new entries. - Earlier, the page (browser-linux.mjs; own ports 5291/9431): Retrocycles' GL frames were attached to SDL 1.2's child window, which no compositor window has; they now name its top-level window. The run still shows black: Mesa keeps drawing (about 25 draws a second under SwiftShader on a loaded gooner, immediate-mode draws, each with its own buffer map), the compositor worker that hosts Mesa repaints only a handful of frames (its animation loop does not get a turn while the GL ring is served), and after a screen change one batch takes over the bridge's 30 s limit. Next steps: a real GPU (edger) to separate slowness from the compositor starving, and letting the compositor repaint between GL batches.
Resume (stream G)
Gate branch merge/games2-gate (PE loader fixes, gex-steam for Windows,
matrix and scripts, this doc). The NT-layer work stays on merge/games2.
Scratch ~/gex-sandbox/scratch/G: launch.sh NAME TIME SLUGS [args] runs a
private copy of matrix.sh (env: GEXC, GEX_WINE_PREFIX), sheet.py makes
contact sheets. Native Wine: GEXC=~/gex-sandbox/wt/wine-slim/target/x86/release/gexc,
prefix a copy of scratch/W/pfx-gui-slim (never W's own). Repros for
missing pieces on that path go to W through the coordinator.
Tools that came out of this
GEX_TRACE_INSNS+tools/x86-difftrace: differential instruction tracing between gex and native (gdb), which found the bass.dll SMC bug.GEXGL_TRACE_GENERATED: logs generated compat-GL commands.tests/gl-replay.mjs(softpipe) andweb/public/mesa-wgpu-replay.html(gex_wgpu) replay the same capture. Comparing the two separates a driver bug from a transport or presentation bug.