mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
Merge merge/unity c784b5c6: Hollow Knight GC hang and STATE_BLOCKING crash fixed (musl-wasm64 3f1c1f08 provider TLS), embed test wasm JIT leg
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_b6478c5570464e3aa53c29026c16b36e
This commit is contained in:
commit
c562065698
16 files changed
+856
-64
No files matched your search
@@ -2063,6 +2063,36 @@ impl Client {
|
||||
self.surfaces.get(&surface).map(|s| s.x11).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Whether input can be delivered: the client has a pointer and a
|
||||
/// keyboard, and a surface to give them to.
|
||||
pub fn input_ready(&self) -> bool {
|
||||
self.pointer.is_some() && self.keyboard.is_some() && self.pointer_target.or(self.focus_surface).is_some()
|
||||
}
|
||||
|
||||
/// Where pointer input would go now, and the surfaces there are: for a
|
||||
/// host explaining scripted input (`GEX_WL_INPUT`).
|
||||
pub fn describe_input(&self) -> String {
|
||||
let mut ids: Vec<_> = self.surfaces.keys().copied().collect();
|
||||
ids.sort();
|
||||
let list: Vec<String> = ids
|
||||
.iter()
|
||||
.map(|id| {
|
||||
let s = &self.surfaces[id];
|
||||
format!("{id}:{:?}{}{}{}", s.role, if s.x11 { " x11" } else { "" },
|
||||
if s.committed_buffer.is_some() { " drawn" } else { "" },
|
||||
s.external.map(|(w, h)| format!(" ext {w}x{h}")).unwrap_or_default())
|
||||
})
|
||||
.collect();
|
||||
format!(
|
||||
"pointer {:?} on {:?}, target {:?}, focus {:?}; surfaces [{}]",
|
||||
self.pointer.is_some(),
|
||||
self.pointer_on,
|
||||
self.pointer_target,
|
||||
self.focus_surface,
|
||||
list.join(", ")
|
||||
)
|
||||
}
|
||||
|
||||
/// Move the pointer to a position on a particular surface, entering it (and
|
||||
/// leaving whatever it was on) if the surface changed.
|
||||
///
|
||||
@@ -2157,10 +2187,17 @@ impl Client {
|
||||
|
||||
/// Pointer moved to surface-local `(x, y)` in pixels.
|
||||
pub fn pointer_motion(&mut self, x: i32, y: i32, time: u32) {
|
||||
let Some(ptr) = self.pointer else { return };
|
||||
let Some(ptr) = self.pointer else {
|
||||
trace_out("[wl] pointer motion before the client asked for a pointer");
|
||||
return;
|
||||
};
|
||||
// Before the enter, so the enter it may trigger carries this position.
|
||||
self.pointer_at = (x, y);
|
||||
if !self.ensure_pointer_enter() {
|
||||
trace_out(&format!(
|
||||
"[wl] pointer motion dropped: no surface to enter (target {:?}, focus {:?})",
|
||||
self.pointer_target, self.focus_surface
|
||||
));
|
||||
return;
|
||||
}
|
||||
// motion(time, surface_x, surface_y)
|
||||
|
||||
+182
-14
@@ -695,17 +695,185 @@ Not yet:
|
||||
natively under gexc as well (wasmtime compiles the published
|
||||
modules), so native runs get the same tier.
|
||||
- Unity's remaining internal calls as they show up; then Stardew/Celeste.
|
||||
- **Resume (after the move; HEAD 1db5c816, `master` merged in):**
|
||||
0. The coordgate on this branch still lacks its browser NT step (it was
|
||||
stopped while E was timing), and nothing after 49a5773b has been
|
||||
gated. Run it before the libprov changes go to master.
|
||||
1. Build: `tools/unity-dotnet/mono/build.sh OUT`, then
|
||||
`tools/unity-dotnet/sysroot` via its `sysroot` step. musl comes from
|
||||
`MUSL_SRC`, which defaults to `../musl-wasm64` beside the worktree
|
||||
(remote `mw`, branch `gex-wasm64`).
|
||||
2. Find which lookup returns NULL. Candidates: `get_attr` on an
|
||||
attribute in UnityEngine.CoreModule, a field name Unity's Mono had
|
||||
that .NET's types lack, or a type whose lookup resolves through a
|
||||
facade's forwarders.
|
||||
3. Then on to the menu with graphics (the old `muck.sh` path: in-guest
|
||||
Xwayland on gex's compositor), and to Unity's own internal calls.
|
||||
- **Scripted input (2026-10-05):** `GEX_WL_INPUT=FILE` feeds the native
|
||||
compositor timed `move`/`click`/`button`/`key`/`tap` events, timed from
|
||||
its `frames N`th frame (`frames 0`: from its start), held until a client
|
||||
has a seat and a surface. Checked end to end on worldmachine with
|
||||
`tests/x11/xinput-echo.c` on the in-guest (rootful) Xwayland: motion, a
|
||||
click and keys arrive. Muck's menu still ignores the clicks (unknown
|
||||
why; Muck is set aside, see the coordinator's direction).
|
||||
- **Hollow Knight (Unity 6000.0.61, linux-x64 Mono; 2026-10-05):** batch
|
||||
mode loads the first scene natively with --fjit
|
||||
(`muck-dotnet.sh` with `MUCK=<HK dir> EXE=hollow_knight.x86_64
|
||||
UNITY_RELAUNCH=1 STEAM_APPID=367520`). Needed: a PIE rebuild of the
|
||||
player's small main (`tests/games/unity-launcher.c`; Unity 6 links it at
|
||||
0x200000, inside gex's reserved low region); `mono_dllmap_insert`
|
||||
ignored; GC handles as the GC's raw values in runtime, CoreLib and
|
||||
provider (Unity 6 tests a handle's bit 0 and its managed code calls
|
||||
`GCHandle.FromIntPtr` on raw values); dead-handle reads tolerated; the
|
||||
framework's x86-64 crypto and compression natives beside the BCL (HK's
|
||||
localisation decrypts with AES). Also: guest signal handlers run at a
|
||||
provider's syscalls (`__gex_signal_frames` in gexprov64.c).
|
||||
- **Hollow Knight, parked 2026-10-05 (shutdown):** with graphics on
|
||||
worldmachine (`scratch/unity/wm/muck.sh` with `GAMEDIR=<HK>
|
||||
EXE=hollow_knight.x86_64 UNITY_RELAUNCH=1 STEAM_APPID=367520`; HK is
|
||||
copied to `~/src/noita-wasm/games-linux/Hollow Knight` there) two
|
||||
problems, after GL 4.6 comes up and physics initialises:
|
||||
1. **With --fjit only:** `free(): invalid pointer` (deterministic), from
|
||||
Unity's allocator's fallback `free` (UnityPlayer vaddr ~0xf8c0af, called
|
||||
via 0xf8a0c0/0xf9559d). Not a `free` the program calls through the PLT
|
||||
(an LD_PRELOAD watcher saw none), so glibc-internal or a corrupted
|
||||
Unity block. Without --fjit it does not happen: a function-JIT
|
||||
translation bug is the first suspect. Next: bisect with the fjit off
|
||||
for UnityPlayer ranges, or `GEX_DUMP_ABORT=1` (now works for x86-64)
|
||||
and map the frames.
|
||||
2. **Without --fjit (and in batch mode with it):** a hang after
|
||||
"Unloading 5 Unused Serialized files". `GEX_DUMP_AT=640` shows the
|
||||
main thread inside the provider's `mono_gc_collect` (Unity's
|
||||
UnloadUnusedAssets), sleeping in a loop (strace: only
|
||||
clock_nanosleep); threads 47/48 in libc. Next: the same dump with the
|
||||
wasm stacks (`GEX_DUMP_AT` now prints `WasmBacktrace` for each thread;
|
||||
run with `GEX_WASM_BACKTRACE=1`) to see what the collection waits for
|
||||
-- likely sgen's suspend of a thread attached to Mono (Unity has ~70)
|
||||
that never acknowledges, or the provider stop-the-world.
|
||||
Then the keyboard script to gameplay (`GEX_WL_INPUT`, works on X
|
||||
clients) and interpreter vs JIT on worldmachine.
|
||||
- **Hollow Knight after the restart (gooner, 2026-10-05; runs
|
||||
`scratch/unity/runs/hk20`-`hk30`, batch mode, no --fjit):**
|
||||
1. **The hang is fixed.** The `GEX_WASM_BACKTRACE` dump put the main
|
||||
thread in sgen's `unified_suspend_stop_world` restart loop (`g_usleep`):
|
||||
one thread stayed "in a critical location" because its saved SP lay
|
||||
outside its registered stack. `GEXMONO_TRACE_THREADS=1` (patch 0003)
|
||||
and `GEX_PROVIDER_STACKS=1` showed that SP was on thread 70's provider
|
||||
stack while the MonoThreadInfo was thread 28's: musl's
|
||||
`__gex_provider_attach` gave attached threads no thread-specific data,
|
||||
so every attached thread's `pthread_getspecific` read the words at
|
||||
address 0 and Mono's thread-info key handed thread 70 thread 28's info
|
||||
(musl-wasm64 3f1c1f08; that commit also makes a provider-created
|
||||
thread report gex's stack). The stop-the-world now names such a thread
|
||||
once (`gex STW: thread ... still critical`).
|
||||
2. **Next crash fixed (6407a1ed):** "Cannot transition thread from
|
||||
STATE_BLOCKING with DO_BLOCKING": Unity calls the embedding API
|
||||
(`mono_gc_get_used_size`, `mono_runtime_invoke`) from guest code a
|
||||
P/Invoke called, i.e. inside the interpreter's GC-safe region, and the
|
||||
old one-stack crossing hooks left the thread Blocking. Now calls into
|
||||
guest code and entries from it are separate pairs (gexprov64.c
|
||||
`gexp_hook_call_guest`/`_call_return`, `gexp_hook_from_guest`/
|
||||
`_to_guest`, `_mark`/`_unwind` for a raise through guest frames).
|
||||
3. HK then unloads 153 assets (the GC that hung) and runs its first
|
||||
scene's scripts (no UIManager in batch mode, TextMeshPro font
|
||||
warnings). `provider_stack` now reports the stack's real base (it was
|
||||
derived from the current SP).
|
||||
4. The --fjit `free(): invalid pointer` did not reproduce on gooner
|
||||
(hk21: --fjit with graphics, llvmpipe, reached the same unload hang);
|
||||
it was seen on worldmachine only. `GEX_FJIT_RANGE`/`GEX_FJIT_SKIP`
|
||||
(hex `LO-HI,...`) restrict the function JIT by entry address for the
|
||||
bisection.
|
||||
5. **Black frames with graphics (gooner, llvmpipe; hk31-hk32):** with
|
||||
--fjit HK creates its GL 4.5 device, loads a second scene and the
|
||||
compositor gets ~30 presents/s, but every captured frame
|
||||
(`f-*.ppm`, 185 of them over 25 min) is all black. Not the Wayland
|
||||
backend: `xwayland-inguest.sh` now unsets `WAYLAND_DISPLAY` for the
|
||||
program (Unity 6 and SDL could otherwise pick gex's compositor), still
|
||||
black. Unknown whether Unity draws nothing (shaders?) or the blit to
|
||||
the window is lost. hk33 (`GEXGL_TRACE_GENERATED=1
|
||||
GEXGL_SUBMIT_TRACE=1`, periodic tallies every 5000 submits in
|
||||
out.txt) was stopped at the reboot before it got far: rerun it, map
|
||||
opcodes with `guest/gl/gexgl.h`, and check for draws vs clears/blits.
|
||||
6. **The --fjit `free(): invalid pointer` bisection (worldmachine only;
|
||||
its glibc is Arch 2.44, gooner's Debian 2.36):** script
|
||||
`scratch/unity/wm/fjtry.sh NAME SKIP [RANGE]` (run through
|
||||
`BENCH_HOST=worldmachine scratch/bench/bench.sh -- bash
|
||||
scratch/unity/wm/fjtry.sh ...`, give each a distinct `XNUM=`; prints
|
||||
BAD/GOOD/NONE/STALL; it installs `wm/gexc.new` if present). Addresses
|
||||
on worldmachine: libc 0x7d748000-0x7d948000, UnityPlayer
|
||||
0x7e041000-0x7fe00000. Results: full --fjit BAD (hkw1, hkw2);
|
||||
RANGE=libc only GOOD (b3, reached "Unloading 5"); RANGE=UnityPlayer
|
||||
only, libc+UnityPlayer, SKIP=UnityPlayer, SKIP=libc: all stall
|
||||
before the GL device (b1, b2, b4-b8; b7/b8 after the VMA-scan fix in
|
||||
`job`). So something besides libc and UnityPlayer is involved, and
|
||||
jitting a subset while other code stays on tier-1 stalls (itself
|
||||
worth a look: a tier-1/fjit interplay bug). b9-b11 (libc + the small
|
||||
libs 0x7d948000-0x7e041000; libc + above UnityPlayer; everything below
|
||||
0x7d948000) were killed by the reboot: rerun them first.
|
||||
7. Next after these: GEX_WL_INPUT keyboard script into gameplay, then
|
||||
interpreter vs JIT on worldmachine (stage with `scratch/unity/wm`:
|
||||
copy target/dbg-x86/release/gexc, dnc/provider/mono.wasm,
|
||||
dnc/lib/System.Private.CoreLib.dll, tests/games/*.sh; HK lives in
|
||||
`~/src/noita-wasm/games-linux/Hollow Knight` there), then the next
|
||||
GAMES-CORPUS.md games.
|
||||
**Build and run recipe (gooner):** `. scratch/env.sh`; gexc:
|
||||
`cargo build --release -p gexc --features gexc/x86 --target-dir
|
||||
target/dbg-x86`; provider: `tools/unity-dotnet/mono/build.sh
|
||||
~/gex-sandbox/scratch/unity/dnc sysroot runtime provider` (sysroot when
|
||||
musl-wasm64 changed; it builds wt/musl-wasm64, branch gex-wasm64, pushed
|
||||
to mw at 3f1c1f08); HK batch: `MUCK="$HOME/gex-sandbox/games-linux/Hollow
|
||||
Knight" EXE=hollow_knight.x86_64 UNITY_RELAUNCH=1 STEAM_APPID=367520
|
||||
GEXC=$PWD/target/dbg-x86/release/gexc sh tests/games/muck-dotnet.sh
|
||||
~/gex-sandbox/scratch/unity/dnc RUNDIR SECS [--fjit]`; add
|
||||
`GFX=$HOME/gex-sandbox/scratch/unity/gl64 XNUM=N EVERY=300` for
|
||||
graphics. Regression checks used: `tests/unity-dotnet/embed/run.sh
|
||||
~/gex-sandbox/scratch/unity/dnc target/dbg-x86/release/gexc` (PASS) and
|
||||
Muck batch with --fjit (main loop runs).
|
||||
Working-tree note: `dnc/src` has every patch staged (`git add -A`) so
|
||||
`git diff` shows only new edits (how 0003 was made). Run `git -C
|
||||
scratch/unity/dnc/src reset -q` before build.sh's `src` step.
|
||||
- **Resume (after the 2026-10-05 evening reboot; HEAD 8bfbae51, master
|
||||
e3917bac merged):**
|
||||
0. Gate head 8bfbae51 offered to the coordinator (the branch gate has
|
||||
never run on anything after d69c1cd6/3718d0ca). Extra legs that prove
|
||||
this stream's work, all checked green on 8bfbae51 on gooner:
|
||||
`tests/unity-dotnet/embed/run.sh ~/gex-sandbox/scratch/unity/dnc
|
||||
target/dbg-x86/release/gexc` (interpreter, --fjit, and since 8bfbae51
|
||||
the wasm JIT at threshold 1, plus the GC-roots control);
|
||||
`tests/unity-dotnet/prov64/run.sh
|
||||
~/gex-sandbox/scratch/unity/dnc/sysroot/sysroot GEXC` (note the doubled
|
||||
`sysroot`); Muck batch with --fjit (`muck-dotnet.sh ... 600 --fjit`,
|
||||
player.log reaches "Initializing packets"); HK batch (reaches its first
|
||||
scene's scripts: "Couldn't find a UIManager"). Logs in
|
||||
`scratch/unity/logs/`, runs `scratch/unity/runs/mk2`, `hk34`.
|
||||
1. The fork clone `dnc/src` has a local commit `2a05cbbc5` (patch 0001)
|
||||
on top of 6812ea26, so build.sh's `src` step fails ("local changes
|
||||
would be overwritten"). The working tree was verified identical to
|
||||
6812ea26 + patches/0001-0003 (tree hashes equal), so build with the
|
||||
steps `sysroot runtime provider` (skip `src`).
|
||||
2. **HK black frames (gooner, runs hk35-hk37, hk40):** not a GL
|
||||
translation problem: Unity never presents. `GEX_GL_CAPTURE=FILE` (host
|
||||
env) recordings (`scratch/unity/gltr.py FILE hist|frame N|draws`) of
|
||||
hk36 and hk37 are byte-identical in size (70,337,112 bytes) and hold
|
||||
no PRESENT, no draw, one CLEAR, no shader: the stream stops after a
|
||||
TEX_STORAGE_2D during the first scene's load. `--strace` (hk37): the
|
||||
render thread (guest tid 68, same ring as the main thread) made 4
|
||||
submits and then waits in futex/clock_gettime, idle, while the main
|
||||
thread keeps running scripts. So the player loop stops sending
|
||||
frames. Suspects: (a) the window never gets focus or visibility in the
|
||||
rootful in-guest Xwayland with no window manager, and HK (unlike
|
||||
Muck) does not run in the background; hk40 (`GEX_WL_INPUT` pointer
|
||||
moves into the window every 7 s) was to test this but died first, see
|
||||
3; (b) the game logic: "Couldn't find a Game Manager" in both batch
|
||||
and graphics runs. `GEXMONO_TRACE=1` (hk38) shows Unity's
|
||||
FindObjectOfType loop testing every object against GameManager's class
|
||||
with `mono_class_is_subclass_of` and finding none. A control on
|
||||
Unity's own Mono (run `hkst`, no provider) is running to tell whether
|
||||
that message is normal for this scene. Also seen: SDL "window backend
|
||||
is (null)" and "Video subsystem has not been initialized" early on,
|
||||
then a window at 800x600 anyway (same in hk31/hk32).
|
||||
3. **--fjit memory corruption is intermittent, and not only on
|
||||
worldmachine:** hk40 on gooner died at startup with `*** stack
|
||||
smashing detected ***` (same flags as hk36, which ran). On worldmachine:
|
||||
b9 (RANGE libc + small libs) GOOD; b10 (libc + above UnityPlayer) and
|
||||
b11 (everything below 0x7d948000) stalled before player.log existed;
|
||||
b12 (full --fjit, `GEX_DUMP_ABORT=1`) BAD, dump in
|
||||
`scratch/unity/wm/res/b12-out.txt`: main thread during physics init,
|
||||
return addresses UnityPlayer+0x8920af/+0x8900c0 (vaddr 0xf8c0af, Unity's
|
||||
allocator after `pthread_getspecific`) -> libc free; b13 (SKIP the
|
||||
allocator 0x7e8d0000-0x7e8de000) failed differently: thread 49
|
||||
SIGSEGV at pc 0 after 4 min. On gooner, batch HK with RANGE=UnityPlayer
|
||||
only (hkr1) and full --fjit (hkr0) both reach the first scene, so the
|
||||
worldmachine "stalls" with partial ranges are not a deterministic
|
||||
tier-1/fjit interplay bug. Conclusion so far: a nondeterministic
|
||||
corruption under --fjit (canary, malloc chunk, a null call target),
|
||||
more likely a race or a per-thread state slip (TLS/fs base?) than one
|
||||
mistranslated function; range bisection cannot pin it. Next: repeat
|
||||
identical runs to measure the failure rate, then look at thread
|
||||
creation/fs-base and publication races in the function JIT.
|
||||
@@ -62,6 +62,8 @@ pub(crate) struct Store {
|
||||
/// The stack pointer this thread's stateful providers share
|
||||
/// (`libprov::stack`), made at the first one's instantiation.
|
||||
pub provider_sp: Option<wasmtime::Global>,
|
||||
/// The lowest address of that stack (`libprov::STACK` bytes).
|
||||
pub provider_stack_lo: u64,
|
||||
/// The wasm exception tag of `gex.provider_throw` (wasm64 providers; [`crate::libprov::tag`]).
|
||||
pub provider_tag: Option<wasmtime::Tag>,
|
||||
/// How far through the map's log this table is.
|
||||
@@ -227,7 +229,7 @@ pub(crate) fn ensure(mut cx: impl AsContextMut<Data = HostState>) -> Result<()>
|
||||
table.set(&mut c, gex_abi::ftab::FALLBACK as u64, wasmtime::Ref::Func(Some(fallback)))?;
|
||||
let jit = c.data_mut().jit.as_mut().unwrap();
|
||||
jit.instances.push(inst);
|
||||
jit.ftab = Some(Store { table, resolve, bridge, root, provider_sp: None, provider_tag: None, replayed: 0 });
|
||||
jit.ftab = Some(Store { table, resolve, bridge, root, provider_sp: None, provider_stack_lo: 0, provider_tag: None, replayed: 0 });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -651,8 +653,27 @@ struct Job {
|
||||
cache: Option<(std::path::PathBuf, String)>,
|
||||
}
|
||||
|
||||
/// `GEX_FJIT_RANGE=LO-HI[,LO-HI...]` (hex guest addresses): the function JIT
|
||||
/// takes only functions entered in those ranges; `GEX_FJIT_SKIP=` the same
|
||||
/// form, never those. For bisecting a translation bug down to one function.
|
||||
fn bisect_allows(addr: u64) -> bool {
|
||||
type Ranges = Option<Vec<(u64, u64)>>;
|
||||
fn parse(var: &str) -> Ranges {
|
||||
let v = std::env::var(var).ok()?;
|
||||
let hex = |s: &str| u64::from_str_radix(s.trim().trim_start_matches("0x"), 16).ok();
|
||||
Some(v.split(',').filter_map(|r| { let (a, b) = r.split_once('-')?; Some((hex(a)?, hex(b)?)) }).collect())
|
||||
}
|
||||
static R: std::sync::OnceLock<(Ranges, Ranges)> = std::sync::OnceLock::new();
|
||||
let (only, skip) = R.get_or_init(|| (parse("GEX_FJIT_RANGE"), parse("GEX_FJIT_SKIP")));
|
||||
let inr = |rs: &Vec<(u64, u64)>| rs.iter().any(|&(s, e)| addr >= s && addr < e);
|
||||
only.as_ref().is_none_or(inr) && !skip.as_ref().is_some_and(inr)
|
||||
}
|
||||
|
||||
fn job(caller: &mut Caller<'_, HostState>, sh: &Arc<Mutex<JitShared>>, memory: &gex_mem::GuestMemory, addr: u64) -> Option<Job> {
|
||||
gex_kernel::ftab::translator()?;
|
||||
if !bisect_allows(addr) {
|
||||
return None;
|
||||
}
|
||||
let (exec, ro, wx): (Vec<(u64, u64)>, Vec<(u64, u64)>, Vec<(u64, u64)>) = caller.data_mut().with_kernel(|k| {
|
||||
let v = k.mem.vmas();
|
||||
(
|
||||
|
||||
+57
-7
@@ -847,6 +847,7 @@ fn run_program_with(
|
||||
std::sync::atomic::Ordering::Relaxed,
|
||||
);
|
||||
JIT_ON.store(opts.jit && !opts.aot_only, std::sync::atomic::Ordering::Relaxed);
|
||||
std::sync::LazyLock::force(&DUMP_START);
|
||||
DUMP_ABORT.store(
|
||||
std::env::var_os("GEX_DUMP_ABORT").is_some(),
|
||||
std::sync::atomic::Ordering::Relaxed,
|
||||
@@ -1697,6 +1698,20 @@ fn run_thread_loop(
|
||||
// GEX_TRACE_SEGV=1: where (with GEX_WASM_BACKTRACE=1, the wasm frames).
|
||||
if std::env::var_os("GEX_TRACE_SEGV").is_some() {
|
||||
eprintln!("gex: guest memory access out of bounds: {e:?}");
|
||||
// The context's PC is the last one stored: the faulting block's entry, or near it.
|
||||
if let Some(memory) = store.data().memory.clone() {
|
||||
let at = read_u64(memory.bytes(), (ctx_addr + ctx::PC as u64) as usize);
|
||||
eprintln!("gex: thread {} context pc {at:#x}", store.data().thread.tid);
|
||||
let maps = store.data_mut().with_kernel(|k| k.mem.proc_maps());
|
||||
for l in maps.lines() {
|
||||
let mut r = l.split(|c| c == '-' || c == ' ');
|
||||
let (Some(a), Some(b)) = (r.next(), r.next()) else { continue };
|
||||
let (Ok(a), Ok(b)) = (u64::from_str_radix(a, 16), u64::from_str_radix(b, 16)) else { continue };
|
||||
if (a..b).contains(&at) {
|
||||
eprintln!("gex: in {l}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if action.handler == 0 {
|
||||
return Ok(Stopped::Exited(Exit::Signal {
|
||||
@@ -2451,6 +2466,10 @@ static JIT_ON: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::ne
|
||||
/// unwinds nothing, so the state must be captured at the syscall or it is gone
|
||||
/// (e.g. which function's canary failed, from the stack's return addresses).
|
||||
static DUMP_ABORT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
static DUMP_AT: std::sync::LazyLock<Option<f64>> =
|
||||
std::sync::LazyLock::new(|| std::env::var("GEX_DUMP_AT").ok().and_then(|v| v.parse().ok()));
|
||||
static DUMP_START: std::sync::LazyLock<std::time::Instant> = std::sync::LazyLock::new(std::time::Instant::now);
|
||||
static DUMPED: std::sync::LazyLock<Mutex<std::collections::HashSet<i32>>> = std::sync::LazyLock::new(Default::default);
|
||||
|
||||
/// A watch on a range of guest memory: `GEX_WATCH=<hex addr>[+<hex len>]`. A
|
||||
/// software watchpoint: every safepoint and syscall compares the bytes and
|
||||
@@ -2537,7 +2556,7 @@ fn dump_guest(gm: &mut GuestMem<'_>, ctx_addr: u64, tid: i32, vmas: &[gex_kernel
|
||||
|gm: &mut GuestMem<'_>, off: usize| gm.read_u64(ctx_addr + off as u64).unwrap_or(0);
|
||||
let pc = read(gm, ctx::PC);
|
||||
let sp = read(gm, ctx::SP);
|
||||
eprintln!("gex: guest abort in thread {tid}");
|
||||
eprintln!("gex: guest state of thread {tid}");
|
||||
eprintln!(" pc {pc:#018x} {}", describe(pc));
|
||||
eprintln!(" sp {sp:#018x} {}", describe(sp));
|
||||
for row in 0..8 {
|
||||
@@ -2901,6 +2920,11 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
|
||||
.ok_or_else(|| anyhow!("guest memory unavailable"))?;
|
||||
// Profiling: count the call against its function (S6).
|
||||
profile::syscall_enter(&mut caller, &GuestMem::new(memory.bytes()), pc as u64);
|
||||
let dump_bt = DUMP_AT.filter(|&at| DUMP_START.elapsed().as_secs_f64() >= at).and_then(|_| {
|
||||
let tid = caller.data().thread.tid;
|
||||
(!DUMPED.lock().unwrap_or_else(|e| e.into_inner()).contains(&tid))
|
||||
.then(|| format!("{:?}", wasmtime::WasmBacktrace::force_capture(&caller)))
|
||||
});
|
||||
let data = memory.bytes();
|
||||
let state = caller.data_mut();
|
||||
// A thread that loops on a blocking syscall -- a condition
|
||||
@@ -2932,15 +2956,41 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
|
||||
// A guest that is about to kill itself is the last moment its
|
||||
// state means anything. `abort` unwinds nothing, so by the time
|
||||
// the process is gone there is no way to ask where it was.
|
||||
// `GEX_DUMP_AT=SECONDS`: each thread's state once, at its first syscall after that long -- where a
|
||||
// run that has stopped making progress is (a thread waiting in a loop of sleeps shows up; one
|
||||
// blocked for good does not).
|
||||
if let Some(at) = *DUMP_AT {
|
||||
if DUMP_START.elapsed().as_secs_f64() >= at
|
||||
&& DUMPED.lock().unwrap_or_else(|e| e.into_inner()).insert(state.thread.tid)
|
||||
{
|
||||
let ctx_addr = state.thread.ctx_addr;
|
||||
let tid = state.thread.tid;
|
||||
let vmas = state.with_kernel(|k| k.mem.vmas().to_vec());
|
||||
eprintln!("gex: GEX_DUMP_AT: thread {tid} at syscall {nr}");
|
||||
dump_guest(&mut gm, ctx_addr, tid, &vmas);
|
||||
if let Some(bt) = &dump_bt {
|
||||
eprintln!(" wasm: {bt}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if DUMP_ABORT.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
let signo = match nr {
|
||||
gex_kernel::syscalls::nr::KILL | gex_kernel::syscalls::nr::TKILL => {
|
||||
gm.read_u64(state.thread.ctx_addr + ctx::x(1) as u64)
|
||||
// x86-64 numbers and argument registers (rsi = x(6), rdx = x(2)) when the guest is x86-64.
|
||||
let signo = if gex_abi::X86 {
|
||||
match gm.read_u64(state.thread.ctx_addr + ctx::x(0) as u64).unwrap_or(!0) {
|
||||
62 | 200 => gm.read_u64(state.thread.ctx_addr + ctx::x(6) as u64),
|
||||
234 => gm.read_u64(state.thread.ctx_addr + ctx::x(2) as u64),
|
||||
_ => Err(gex_kernel::Errno::EINVAL),
|
||||
}
|
||||
gex_kernel::syscalls::nr::TGKILL => {
|
||||
gm.read_u64(state.thread.ctx_addr + ctx::x(2) as u64)
|
||||
} else {
|
||||
match nr {
|
||||
gex_kernel::syscalls::nr::KILL | gex_kernel::syscalls::nr::TKILL => {
|
||||
gm.read_u64(state.thread.ctx_addr + ctx::x(1) as u64)
|
||||
}
|
||||
gex_kernel::syscalls::nr::TGKILL => {
|
||||
gm.read_u64(state.thread.ctx_addr + ctx::x(2) as u64)
|
||||
}
|
||||
_ => Err(gex_kernel::Errno::EINVAL),
|
||||
}
|
||||
_ => Err(gex_kernel::Errno::EINVAL),
|
||||
};
|
||||
if signo == Ok(6) {
|
||||
let ctx_addr = state.thread.ctx_addr;
|
||||
|
||||
@@ -110,6 +110,9 @@ pub(crate) fn stack(cx: &mut wasmtime::StoreContextMut<'_, HostState>) -> Result
|
||||
let name = format!("[provider stack, thread {tid}]");
|
||||
let base = if LIBS.get().map_or(true, |v| v.is_empty()) { alloc_top(cx.data_mut(), STACK, &name)? } else { alloc(cx.data_mut(), STACK, &name)? };
|
||||
let top = base + STACK - 16;
|
||||
if std::env::var_os("GEX_PROVIDER_STACKS").is_some() {
|
||||
eprintln!("gex: provider stack for thread {tid}: [{base:#x}, {:#x})", base + STACK);
|
||||
}
|
||||
// Pointer-sized: i64 for a wasm64 provider beside a 64-bit guest memory.
|
||||
let g = if gex_abi::mem64() {
|
||||
wasmtime::Global::new(&mut *cx, wasmtime::GlobalType::new(wasmtime::ValType::I64, wasmtime::Mutability::Var), Val::I64(top as i64))?
|
||||
@@ -118,6 +121,7 @@ pub(crate) fn stack(cx: &mut wasmtime::StoreContextMut<'_, HostState>) -> Result
|
||||
};
|
||||
if let Some(s) = cx.data_mut().jit.as_mut().and_then(|j| j.ftab.as_mut()) {
|
||||
s.provider_sp = Some(g.clone());
|
||||
s.provider_stack_lo = base;
|
||||
}
|
||||
Ok(g)
|
||||
}
|
||||
@@ -522,17 +526,14 @@ fn register64(linker: &mut Linker<HostState>) -> Result<()> {
|
||||
Ok(())
|
||||
})()) })?;
|
||||
// This thread's provider C stack (`stack`): its lowest address; it is
|
||||
// `STACK` bytes. musl reports it as the thread's stack.
|
||||
// `STACK` bytes. musl reports it as the thread's stack. (Not derived from
|
||||
// the stack pointer's current value, which is below the top by whatever
|
||||
// the thread has on its stack when it asks.)
|
||||
linker.func_wrap(IMPORT_MODULE, "provider_stack", |mut caller: Caller<'_, HostState>| -> wasmtime::Result<i64> { crate::host((|| -> anyhow::Result<i64> {
|
||||
use wasmtime::AsContextMut;
|
||||
let mut cx = caller.as_context_mut();
|
||||
let g = stack(&mut cx)?;
|
||||
let top = match g.get(&mut cx) {
|
||||
Val::I64(v) => v as u64,
|
||||
Val::I32(v) => v as u32 as u64,
|
||||
_ => 0,
|
||||
};
|
||||
Ok((top + 16 - STACK) as i64)
|
||||
stack(&mut cx)?;
|
||||
Ok(cx.data().jit.as_ref().and_then(|j| j.ftab.as_ref()).map_or(0, |s| s.provider_stack_lo) as i64)
|
||||
})()) })?;
|
||||
// Constants of gex's ABI a provider calling guest code needs.
|
||||
linker.func_wrap(IMPORT_MODULE, "provider_abi", |_caller: Caller<'_, HostState>, which: i32| -> i64 {
|
||||
|
||||
@@ -50,6 +50,104 @@ struct Inner {
|
||||
/// The GPU compositor, when one could be started. Optional: a machine with
|
||||
/// no graphics adapter still runs, compositing on the host instead.
|
||||
gpu: Mutex<Option<gex_gpu::Compositor>>,
|
||||
/// `GEX_WL_INPUT`: scripted input, timed from the first frame.
|
||||
input: Mutex<Option<Input>>,
|
||||
}
|
||||
|
||||
/// One scripted input event (`GEX_WL_INPUT`, [`Input::load`]).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Ev {
|
||||
Motion(i32, i32),
|
||||
Button(u32, bool),
|
||||
Key(u32, bool),
|
||||
}
|
||||
|
||||
/// A script of input events at times (seconds) after the compositor's Nth
|
||||
/// frame (`frames N`, default 1: a game's first frame is often a splash long
|
||||
/// before its menu; 0: from the compositor's start), delivered to the clients by [`Wayland::pump`] as they fall due:
|
||||
/// what an automated game test needs to get past menus. One event per line,
|
||||
/// `#` comments:
|
||||
///
|
||||
/// ```text
|
||||
/// frames 300 # time from the 300th frame
|
||||
/// 30 click 100 328 # motion, then BTN_LEFT down and (0.1 s later) up
|
||||
/// 31.5 move 400 300
|
||||
/// 32 button 272 1 # a Linux BTN_* code, 1 down / 0 up
|
||||
/// 40 tap enter # key down, and up 0.1 s later
|
||||
/// 41 key z 1 # a key name or Linux KEY_* code, 1 down / 0 up
|
||||
/// ```
|
||||
///
|
||||
/// Coordinates are the focused surface's pixels.
|
||||
struct Input {
|
||||
events: Vec<(f64, Ev, String)>,
|
||||
next: usize,
|
||||
start: Option<std::time::Instant>,
|
||||
/// Frames to see before the clock starts, and how many have been.
|
||||
after: u64,
|
||||
seen: u64,
|
||||
}
|
||||
|
||||
/// Linux `KEY_*` codes for the names a script may use (else a number).
|
||||
fn key_code(name: &str) -> Option<u32> {
|
||||
let k = match name.to_ascii_lowercase().as_str() {
|
||||
"esc" | "escape" => 1,
|
||||
"1" => 2, "2" => 3, "3" => 4, "4" => 5, "5" => 6, "6" => 7, "7" => 8, "8" => 9, "9" => 10, "0" => 11,
|
||||
"backspace" => 14, "tab" => 15,
|
||||
"q" => 16, "w" => 17, "e" => 18, "r" => 19, "t" => 20, "y" => 21, "u" => 22, "i" => 23, "o" => 24, "p" => 25,
|
||||
"enter" | "return" => 28, "ctrl" | "leftctrl" => 29,
|
||||
"a" => 30, "s" => 31, "d" => 32, "f" => 33, "g" => 34, "h" => 35, "j" => 36, "k" => 37, "l" => 38,
|
||||
"shift" | "leftshift" => 42,
|
||||
"z" => 44, "x" => 45, "c" => 46, "v" => 47, "b" => 48, "n" => 49, "m" => 50,
|
||||
"alt" | "leftalt" => 56, "space" => 57,
|
||||
"f1" => 59, "f2" => 60, "f3" => 61, "f4" => 62, "f5" => 63,
|
||||
"up" => 103, "left" => 105, "right" => 106, "down" => 108,
|
||||
n => return n.parse().ok(),
|
||||
};
|
||||
Some(k)
|
||||
}
|
||||
|
||||
impl Input {
|
||||
fn load(path: &str) -> Result<Input, String> {
|
||||
let text = std::fs::read_to_string(path).map_err(|e| format!("{path}: {e}"))?;
|
||||
let mut events = Vec::new();
|
||||
let mut after = 1;
|
||||
for (n, line) in text.lines().enumerate() {
|
||||
let line = line.split('#').next().unwrap_or("").trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let w: Vec<&str> = line.split_whitespace().collect();
|
||||
let bad = || format!("{path}:{}: cannot read `{line}`", n + 1);
|
||||
if w[0] == "frames" {
|
||||
after = w.get(1).and_then(|v| v.parse().ok()).ok_or_else(bad)?;
|
||||
continue;
|
||||
}
|
||||
let t: f64 = w[0].parse().map_err(|_| bad())?;
|
||||
let num = |i: usize| -> Result<i32, String> { w.get(i).and_then(|v| v.parse().ok()).ok_or_else(bad) };
|
||||
let key = |i: usize| -> Result<u32, String> { w.get(i).and_then(|v| key_code(v)).ok_or_else(bad) };
|
||||
let down = |i: usize| -> Result<bool, String> { Ok(num(i)? != 0) };
|
||||
let l = line.to_string();
|
||||
match w.get(1).copied() {
|
||||
Some("move") => events.push((t, Ev::Motion(num(2)?, num(3)?), l)),
|
||||
Some("click") => {
|
||||
events.push((t, Ev::Motion(num(2)?, num(3)?), l.clone()));
|
||||
events.push((t, Ev::Button(0x110, true), l.clone()));
|
||||
events.push((t + 0.1, Ev::Button(0x110, false), l));
|
||||
}
|
||||
Some("button") => events.push((t, Ev::Button(num(2)? as u32, down(3)?), l)),
|
||||
Some("key") => events.push((t, Ev::Key(key(2)?, down(3)?), l)),
|
||||
Some("tap") => {
|
||||
events.push((t, Ev::Key(key(2)?, true), l.clone()));
|
||||
events.push((t + 0.1, Ev::Key(key(2)?, false), l));
|
||||
}
|
||||
_ => return Err(bad()),
|
||||
}
|
||||
}
|
||||
events.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
// `frames 0`: from the compositor's start.
|
||||
let start = if after == 0 { Some(std::time::Instant::now()) } else { None };
|
||||
Ok(Input { events, next: 0, start, after, seen: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Wayland {
|
||||
@@ -60,14 +158,53 @@ impl std::fmt::Debug for Wayland {
|
||||
|
||||
impl Wayland {
|
||||
pub fn new(width: i32, height: i32) -> Wayland {
|
||||
let input = match std::env::var("GEX_WL_INPUT") {
|
||||
Ok(p) => match Input::load(&p) {
|
||||
Ok(i) => Some(i),
|
||||
Err(e) => {
|
||||
eprintln!("[gex] GEX_WL_INPUT: {e}");
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(_) => None,
|
||||
};
|
||||
Wayland {
|
||||
inner: Arc::new(Inner {
|
||||
size: Mutex::new((width, height)),
|
||||
input: Mutex::new(input),
|
||||
..Default::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame was shown (a client's commit, or GL's own present): the
|
||||
/// input script's clock starts at its `frames N`th.
|
||||
fn note_frame(&self) {
|
||||
if let Some(inp) = self.inner.input.lock().unwrap().as_mut() {
|
||||
inp.seen += 1;
|
||||
if inp.seen >= inp.after && inp.start.is_none() {
|
||||
eprintln!("[gex] input: the script's clock starts at frame {}", inp.seen);
|
||||
inp.start = Some(std::time::Instant::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The scripted events now due (`GEX_WL_INPUT`), with their time in ms.
|
||||
fn due_input(&self) -> Vec<(Ev, u32)> {
|
||||
let mut g = self.inner.input.lock().unwrap();
|
||||
let Some(inp) = g.as_mut() else { return Vec::new() };
|
||||
let Some(start) = inp.start else { return Vec::new() };
|
||||
let now = start.elapsed().as_secs_f64();
|
||||
let mut due = Vec::new();
|
||||
while inp.next < inp.events.len() && inp.events[inp.next].0 <= now {
|
||||
let (t, ev, line) = &inp.events[inp.next];
|
||||
eprintln!("[gex] input at {now:.1} s: {ev:?} ({line})");
|
||||
due.push((*ev, (t * 1000.0) as u32));
|
||||
inp.next += 1;
|
||||
}
|
||||
due
|
||||
}
|
||||
|
||||
/// Composite on the GPU rather than handing single frames to the host.
|
||||
/// Returns the adapter, or the reason there is none; either way the
|
||||
/// compositor keeps working.
|
||||
@@ -159,6 +296,7 @@ impl Wayland {
|
||||
/// Write a frame drawn somewhere other than a client buffer (a GL
|
||||
/// client's host surface): RGBA rows, top first.
|
||||
pub fn present_external(&self, width: i32, height: i32, rgba: &[u8]) {
|
||||
self.note_frame();
|
||||
let Some(prefix) = self.inner.out.lock().unwrap().clone() else {
|
||||
return;
|
||||
};
|
||||
@@ -183,6 +321,9 @@ impl Wayland {
|
||||
/// kernel locates a buffer's pixels through its shared mappings; guest
|
||||
/// memory is where they are.
|
||||
pub fn pump(&self, kernel: &Kernel, mem: &GuestMem<'_>) {
|
||||
// Scripted input waits for a client that can take it (a seat and a surface).
|
||||
let ready = self.inner.sessions.lock().unwrap().iter().any(|s| s.client.input_ready());
|
||||
let due = if ready { self.due_input() } else { Vec::new() };
|
||||
let mut sessions = self.inner.sessions.lock().unwrap();
|
||||
let mut produced = Vec::new();
|
||||
sessions.retain_mut(|s| {
|
||||
@@ -212,6 +353,17 @@ impl Wayland {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !due.is_empty() {
|
||||
eprintln!("[gex] input: {}", s.client.describe_input());
|
||||
}
|
||||
let take = s.client.input_ready();
|
||||
for &(ev, ms) in due.iter().filter(|_| take) {
|
||||
match ev {
|
||||
Ev::Motion(x, y) => s.client.pointer_motion(x, y, ms),
|
||||
Ev::Button(b, down) => s.client.pointer_button(b, down, ms),
|
||||
Ev::Key(k, down) => s.client.keyboard_key(k, down, ms),
|
||||
}
|
||||
}
|
||||
s.out.extend_from_slice(&s.client.take_outgoing());
|
||||
// A `wl_keyboard.keymap` event owes a descriptor for its `h`
|
||||
// argument: an in-memory file holding the keymap, staged on the
|
||||
@@ -238,6 +390,7 @@ impl Wayland {
|
||||
});
|
||||
drop(sessions);
|
||||
if !produced.is_empty() {
|
||||
self.note_frame();
|
||||
// One screen per batch of commits, not one per surface.
|
||||
self.save(&produced);
|
||||
self.inner.frames.lock().unwrap().extend(produced);
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
# lib/System.Private.CoreLib.dll). The BCL directory (CoreLib plus .NET 10's
|
||||
# managed shared framework) is made in RUNDIR. Env: MUCK (the Linux install,
|
||||
# default ~/gex-sandbox/games-linux/Muck), GEXC, CORES (optional taskset
|
||||
# list), DOTNET_FW (the .NET 10 shared framework directory).
|
||||
# list), EXE (another Unity game's executable in MUCK, e.g. hollow_knight.x86_64),
|
||||
# UNITY_RELAUNCH=1 (run a PIE rebuild of that executable: unity-launcher.c; with it,
|
||||
# STEAM_APPID=N writes steam_appid.txt beside it), DOTNET_FW (the .NET 10 shared framework directory).
|
||||
#
|
||||
# Default: batch mode, no graphics. GFX=GLDIR: with graphics, GL through
|
||||
# gex-gl (GLDIR holds guest/gl/build.sh's x86-64 build) and X from an
|
||||
@@ -19,17 +21,31 @@ HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
OUT=$(realpath "$1"); RUN=$(realpath -m "$2"); SECS=${3:-1800}
|
||||
shift 2; [ $# -gt 0 ] && shift
|
||||
MUCK=${MUCK:-$HOME/gex-sandbox/games-linux/Muck}
|
||||
EXE=${EXE:-Muck.x86_64}
|
||||
GEXC=${GEXC:-$HERE/../../target/x86/release/gexc}
|
||||
FW=${DOTNET_FW:-$(dirname "$(dotnet --list-runtimes | awk '/Microsoft.NETCore.App 10\./ {v=$2; p=$3} END {gsub(/[\[\]]/, "", p); print p "/" v "/x"}')")}
|
||||
mkdir -p "$RUN/bcl" "$RUN/home"
|
||||
cp "$FW"/*.dll "$RUN/bcl/"
|
||||
cp "$OUT/lib/System.Private.CoreLib.dll" "$RUN/bcl/"
|
||||
# The framework's x86-64 natives that have no wasm64 build in the provider: P/Invokes to them go to
|
||||
# the guest (the provider's dl fallback), which loads them beside the BCL with the host's OpenSSL/zlib.
|
||||
cp "$FW"/libSystem.Security.Cryptography.Native.OpenSsl.so "$FW"/libSystem.IO.Compression.Native.so "$RUN/bcl/"
|
||||
if [ -n "${UNITY_RELAUNCH:-}" ]; then
|
||||
# The game's executable rebuilt PIE (unity-launcher.c) among links to its files.
|
||||
G="$RUN/game"; mkdir -p "$G"
|
||||
for f in "$MUCK"/*; do [ "$(basename "$f")" = "$EXE" ] || ln -sfn "$f" "$G/"; done
|
||||
cc -O1 -fPIE -pie -o "$G/$EXE" "$HERE/unity-launcher.c" -L"$MUCK" -l:UnityPlayer.so -Wl,-rpath,'$ORIGIN'
|
||||
# STEAM_APPID: Steamworks' development file, so a game started outside Steam runs instead of
|
||||
# asking Steam to restart it.
|
||||
[ -n "${STEAM_APPID:-}" ] && echo "$STEAM_APPID" > "$G/steam_appid.txt"
|
||||
MUCK=$G
|
||||
fi
|
||||
cd "$MUCK"
|
||||
export GEX_WASM_DLLS=libmonobdwgc-2.0.so="$OUT/provider/mono.wasm"
|
||||
if [ -z "${GFX:-}" ]; then
|
||||
timeout -s KILL "$SECS" ${CORES:+taskset -c "$CORES"} \
|
||||
"$GEXC" run --mem64 "$@" --env HOME="$RUN/home" --env GEXMONO_ASSEMBLIES="$RUN/bcl" \
|
||||
./Muck.x86_64 -batchmode -nographics -logFile "$RUN/player.log" > "$RUN/out.txt" 2>&1 || echo "exit $?"
|
||||
./$EXE -batchmode -nographics -logFile "$RUN/player.log" > "$RUN/out.txt" 2>&1 || echo "exit $?"
|
||||
else
|
||||
N=${XNUM:-71}
|
||||
[ -e /tmp/.X$N-lock ] && { echo "display :$N is taken (XNUM)"; exit 1; }
|
||||
@@ -37,7 +53,7 @@ else
|
||||
"$GEXC" run --mem64 "$@" --gl --wayland "${XDG_RUNTIME_DIR:-/tmp}/muckwl-$$" --wayland-size 800x600 --wayland-out "$RUN/f" \
|
||||
--env LD_LIBRARY_PATH="$(realpath "$GFX")" --env HOME="$RUN/home" --env GEXMONO_ASSEMBLIES="$RUN/bcl" \
|
||||
/bin/sh "$HERE/xwayland-inguest.sh" $N "$RUN/xwayland.log" \
|
||||
./Muck.x86_64 -logFile "$RUN/player.log" -screen-width 800 -screen-height 600 -screen-fullscreen 0 \
|
||||
./$EXE -logFile "$RUN/player.log" -screen-width 800 -screen-height 600 -screen-fullscreen 0 \
|
||||
> "$RUN/out.txt" 2>&1 || echo "exit $?"
|
||||
# Xwayland's lock and socket node are host files; remove only this display's.
|
||||
rm -f /tmp/.X$N-lock /tmp/.X11-unix/X$N
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/* A Unity Linux player's executable is only `main` calling UnityPlayer.so's
|
||||
* PlayerMain. Some (Unity 6's, e.g. Hollow Knight's) are linked at 0x200000,
|
||||
* inside gex's reserved low region; this is the same program built PIE.
|
||||
* muck-dotnet.sh UNITY_RELAUNCH=1 builds it under the game's executable name
|
||||
* in a directory of links to the game's files, since the player finds its
|
||||
* data from its own path (<name>_Data). */
|
||||
int _Z10PlayerMainiPPc (int argc, char **argv); /* PlayerMain(int, char**) */
|
||||
|
||||
int
|
||||
main (int argc, char **argv)
|
||||
{
|
||||
return _Z10PlayerMainiPPc (argc, argv);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Runs inside gex: Xwayland on gex's compositor, then the program on it.
|
||||
# usage: inguest.sh DISPLAYNUM LOG prog args...
|
||||
# Xwayland gets no LD_LIBRARY_PATH: the program's (gex-gl's libGL) is not its.
|
||||
# Xwayland gets no LD_LIBRARY_PATH or LD_PRELOAD: the program's (gex-gl's libGL, a diagnostic) are not its.
|
||||
n=$1; log=$2; shift 2
|
||||
env -u LD_LIBRARY_PATH Xwayland :$n -shm -ac -nolisten tcp > "$log" 2>&1 &
|
||||
env -u LD_LIBRARY_PATH -u LD_PRELOAD Xwayland :$n -shm -ac -nolisten tcp > "$log" 2>&1 &
|
||||
xw=$!
|
||||
i=0
|
||||
while [ ! -S /tmp/.X11-unix/X$n ]; do
|
||||
@@ -12,4 +12,5 @@ while [ ! -S /tmp/.X11-unix/X$n ]; do
|
||||
sleep 1; i=$((i+1))
|
||||
done
|
||||
echo "[inguest] X :$n up after $i s"
|
||||
DISPLAY=:$n exec "$@"
|
||||
# The program uses X: a Wayland-capable one (Unity 6, SDL) would otherwise pick gex's compositor directly.
|
||||
DISPLAY=:$n exec env -u WAYLAND_DISPLAY "$@"
|
||||
@@ -22,12 +22,22 @@ cp "$FW/System.Runtime.dll" "$FW/System.Collections.dll" "$FW/System.Runtime.Int
|
||||
cd "$T"
|
||||
GEX_WASM_DLLS=libmonobdwgc-2.0.so="$OUT/provider/mono.wasm" timeout 300 "$GEXC" run --mem64 \
|
||||
--env GEXMONO_ASSEMBLIES="$T/lib" ./embed ./libmonobdwgc-2.0.so "$T/asm/t.dll" | tee out.txt
|
||||
if ! { grep -q "^Compute(50) = 50002$" out.txt && grep -q "^Hello = hello gex from t ptr=8$" out.txt && grep -q "^Guest = twice 42; concat guest strings; mix 323.25; scale 2,4,6; big 11,25,52,3; caught from the guest$" out.txt && grep -q "^Back = callback 806.50; unmanaged 1015$" out.txt && grep -q "^Work(200) = 24983091$" out.txt && grep -q "^held across collections: 64 of 64 intact$" out.txt && grep -q "^worker thread: 0 bad of 640$" out.txt && grep -q "^gc ok$" out.txt; }; then echo FAIL; exit 1; fi
|
||||
PATTERNS=("Compute(50) = 50002" "Hello = hello gex from t ptr=8"
|
||||
"Guest = twice 42; concat guest strings; mix 323.25; scale 2,4,6; big 11,25,52,3; caught from the guest"
|
||||
"Back = callback 806.50; unmanaged 1015" "Work(200) = 24983091"
|
||||
"held across collections: 64 of 64 intact" "worker thread: 0 bad of 640" "gc ok")
|
||||
for p in "${PATTERNS[@]}"; do grep -a -q -x -F "$p" out.txt || { echo "FAIL: no '$p'"; exit 1; }; done
|
||||
# The same under the function JIT, which must leave the replaced library's own code alone: it once
|
||||
# followed the PLT into the stub ("x86 Mono stub ... called"), as it followed Muck's into Boehm's GC.
|
||||
GEX_WASM_DLLS=libmonobdwgc-2.0.so="$OUT/provider/mono.wasm" timeout 300 "$GEXC" run --mem64 --fjit \
|
||||
--env GEXMONO_ASSEMBLIES="$T/lib" ./embed ./libmonobdwgc-2.0.so "$T/asm/t.dll" > fjit.txt 2>&1 || true
|
||||
if ! grep -q "^gc ok$" fjit.txt; then echo "FAIL (--fjit)"; tail -3 fjit.txt; exit 1; fi
|
||||
# The fork's wasm JIT (GEXMONO_WASM_JIT=1) at threshold 1: every method it accepts runs as a
|
||||
# wasm64 module compiled by the host, and the results must be the interpreter's.
|
||||
GEX_WASM_DLLS=libmonobdwgc-2.0.so="$OUT/provider/mono.wasm" timeout 600 "$GEXC" run --mem64 \
|
||||
--env GEXMONO_WASM_JIT=1 --env MONO_WASM_JIT_THRESHOLD=1 \
|
||||
--env GEXMONO_ASSEMBLIES="$T/lib" ./embed ./libmonobdwgc-2.0.so "$T/asm/t.dll" > wasmjit.txt 2>&1 || true
|
||||
for p in "${PATTERNS[@]}"; do grep -a -q -x -F "$p" wasmjit.txt || { echo "FAIL (wasm JIT): no '$p'"; exit 1; }; done
|
||||
# The control: without scanning the guest's frames, objects held only there die.
|
||||
GEX_WASM_DLLS=libmonobdwgc-2.0.so="$OUT/provider/mono.wasm" timeout 300 "$GEXC" run --mem64 --env GEXP_NO_GUEST_ROOTS=1 \
|
||||
--env GEXMONO_ASSEMBLIES="$T/lib" ./embed ./libmonobdwgc-2.0.so "$T/asm/t.dll" > control.txt 2>&1 || true
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/* An X window that prints the input it gets, one line per event, unbuffered:
|
||||
* the check that scripted input (GEX_WL_INPUT) reaches an X client through the
|
||||
* in-guest Xwayland.
|
||||
* xinput-echo [SECONDS] exits after SECONDS (default 60) */
|
||||
#include <X11/Xlib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
|
||||
int
|
||||
main (int argc, char **argv)
|
||||
{
|
||||
int secs = argc > 1 ? atoi (argv [1]) : 60;
|
||||
setvbuf (stdout, NULL, _IONBF, 0);
|
||||
printf ("xinput-echo: opening the display\n");
|
||||
Display *d = XOpenDisplay (NULL);
|
||||
if (!d) { printf ("no display\n"); return 1; }
|
||||
Window w = XCreateSimpleWindow (d, DefaultRootWindow (d), 0, 0, 800, 600, 0, 0, 0x336699);
|
||||
XSelectInput (d, w, KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | EnterWindowMask | ExposureMask | FocusChangeMask);
|
||||
XMapWindow (d, w);
|
||||
XFlush (d);
|
||||
printf ("xinput-echo: window mapped\n");
|
||||
time_t end = time (NULL) + secs;
|
||||
setvbuf (stdout, NULL, _IONBF, 0);
|
||||
int motions = 0;
|
||||
time_t beat = time (NULL);
|
||||
while (time (NULL) < end) {
|
||||
if (time (NULL) >= beat + 5) { beat = time (NULL); printf ("alive, %d motions\n", motions); }
|
||||
while (XPending (d)) {
|
||||
XEvent e;
|
||||
XNextEvent (d, &e);
|
||||
switch (e.type) {
|
||||
case MotionNotify: if (motions++ % 20 == 0) printf ("motion %d %d\n", e.xmotion.x, e.xmotion.y); break;
|
||||
case ButtonPress: printf ("button press %u at %d %d\n", e.xbutton.button, e.xbutton.x, e.xbutton.y); break;
|
||||
case ButtonRelease: printf ("button release %u\n", e.xbutton.button); break;
|
||||
case KeyPress: printf ("key press %u\n", e.xkey.keycode); break;
|
||||
case KeyRelease: printf ("key release %u\n", e.xkey.keycode); break;
|
||||
case EnterNotify: printf ("enter %d %d\n", e.xcrossing.x, e.xcrossing.y); break;
|
||||
case FocusIn: printf ("focus in\n"); break;
|
||||
case Expose: if (e.xexpose.count == 0) { XClearWindow (d, w); XFlush (d); } break;
|
||||
}
|
||||
}
|
||||
struct timespec ts = { 0, 20000000 };
|
||||
nanosleep (&ts, NULL);
|
||||
}
|
||||
printf ("motions %d\n", motions);
|
||||
return 0;
|
||||
}
|
||||
@@ -138,40 +138,96 @@ dl_symbol (void *handle, const char *name, char **err, void *user_data)
|
||||
* A thread attached to the runtime runs guest code in GC Safe (blocking)
|
||||
* mode, so a collection does not wait for it to poll: gex stops it at its
|
||||
* next safepoint instead (sgen-stw.c, gexp_stop_guest_threads), and its
|
||||
* guest stack and registers are scanned conservatively (sgen-mono.c). Each
|
||||
* crossing into guest code pushes whether it entered the region. */
|
||||
* guest stack and registers are scanned conservatively (sgen-mono.c).
|
||||
*
|
||||
* Calling guest code enters the region unless the thread is already in it
|
||||
* (the interpreter's P/Invoke transition), and its return leaves only what
|
||||
* the call entered. Entering from guest code makes the thread Running for
|
||||
* the call's duration whatever state it was in -- guest code a P/Invoke
|
||||
* called, calling the embedding API back (Unity does: mono_runtime_invoke,
|
||||
* mono_gc_get_used_size), is in the interpreter's GC-safe region -- and the
|
||||
* return to guest code puts it back; a thread that was Running in guest code
|
||||
* (its first entry attached it) is left GC-safe. */
|
||||
|
||||
#include <mono/utils/mono-threads.h>
|
||||
#include <mono/utils/mono-threads-api.h>
|
||||
|
||||
#define GEXMONO_SAFE_MAX 256
|
||||
static __thread gpointer safe_cookie [GEXMONO_SAFE_MAX];
|
||||
static __thread int safe_depth;
|
||||
static __thread gpointer entry_cookie [GEXMONO_SAFE_MAX];
|
||||
static __thread int entry_depth;
|
||||
|
||||
void
|
||||
gexp_hook_to_guest (void)
|
||||
void *
|
||||
gexp_hook_call_guest (void)
|
||||
{
|
||||
gpointer cookie = NULL;
|
||||
MonoThreadInfo *info = mono_thread_info_current_unchecked ();
|
||||
if (info && mono_thread_info_current_state (info) == STATE_RUNNING) {
|
||||
gpointer dummy;
|
||||
cookie = mono_threads_enter_gc_safe_region_unbalanced (&dummy);
|
||||
return mono_threads_enter_gc_safe_region_unbalanced (&dummy);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
gexp_hook_call_return (void *cookie)
|
||||
{
|
||||
if (cookie) {
|
||||
gpointer dummy;
|
||||
mono_threads_exit_gc_safe_region_unbalanced (cookie, &dummy);
|
||||
}
|
||||
if (safe_depth < GEXMONO_SAFE_MAX)
|
||||
safe_cookie [safe_depth] = cookie;
|
||||
safe_depth++;
|
||||
}
|
||||
|
||||
void
|
||||
gexp_hook_from_guest (void)
|
||||
{
|
||||
if (!safe_depth)
|
||||
return; /* the thread's first entry */
|
||||
safe_depth--;
|
||||
gpointer cookie = safe_depth < GEXMONO_SAFE_MAX ? safe_cookie [safe_depth] : NULL;
|
||||
if (cookie) {
|
||||
gpointer cookie = NULL;
|
||||
MonoThreadInfo *info = mono_thread_info_current_unchecked ();
|
||||
if (info && mono_thread_info_current_state (info) != STATE_RUNNING) {
|
||||
gpointer dummy;
|
||||
mono_threads_exit_gc_safe_region_unbalanced (cookie, &dummy);
|
||||
cookie = mono_threads_enter_gc_unsafe_region_unbalanced (&dummy);
|
||||
}
|
||||
if (entry_depth < GEXMONO_SAFE_MAX)
|
||||
entry_cookie [entry_depth] = cookie;
|
||||
entry_depth++;
|
||||
}
|
||||
|
||||
void
|
||||
gexp_hook_to_guest (void)
|
||||
{
|
||||
gpointer cookie = NULL;
|
||||
if (entry_depth > 0) {
|
||||
entry_depth--;
|
||||
cookie = entry_depth < GEXMONO_SAFE_MAX ? entry_cookie [entry_depth] : NULL;
|
||||
}
|
||||
gpointer dummy;
|
||||
if (cookie) {
|
||||
mono_threads_exit_gc_unsafe_region_unbalanced (cookie, &dummy);
|
||||
return;
|
||||
}
|
||||
MonoThreadInfo *info = mono_thread_info_current_unchecked ();
|
||||
if (info && mono_thread_info_current_state (info) == STATE_RUNNING)
|
||||
mono_threads_enter_gc_safe_region_unbalanced (&dummy);
|
||||
}
|
||||
|
||||
/* A raise that abandoned guest frames (gexp_call_guest_catching): the
|
||||
* entries inside them never return, and the thread (Running: it raised) goes
|
||||
* back to the state it called the guest in -- GC-safe when the interpreter's
|
||||
* P/Invoke had entered the region, which it will leave itself. */
|
||||
int
|
||||
gexp_hook_mark (void)
|
||||
{
|
||||
MonoThreadInfo *info = mono_thread_info_current_unchecked ();
|
||||
int safe = info && mono_thread_info_current_state (info) != STATE_RUNNING;
|
||||
return entry_depth * 2 + safe;
|
||||
}
|
||||
|
||||
void
|
||||
gexp_hook_unwind (int mark)
|
||||
{
|
||||
entry_depth = mark >> 1;
|
||||
MonoThreadInfo *info = mono_thread_info_current_unchecked ();
|
||||
if ((mark & 1) && info && mono_thread_info_current_state (info) == STATE_RUNNING) {
|
||||
gpointer dummy;
|
||||
mono_threads_enter_gc_safe_region_unbalanced (&dummy);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
#include "gexmono-adapters.h"
|
||||
#include <mono/metadata/class-internals.h>
|
||||
#include <mono/metadata/object-internals.h>
|
||||
@@ -224,6 +225,54 @@ U(mono_thread_pop_appdomain_ref) { ENTER; RETV; }
|
||||
U(mono_thread_has_sufficient_execution_stack) { ENTER; RET (1); }
|
||||
U(mono_gchandle_is_in_domain) { ENTER; RET (1); }
|
||||
U(mono_gchandle_is_in_domain_v2) { ENTER; RET (1); }
|
||||
/* Unity 6 uses the v2 handle API with Unity's Mono encoding: a v2 handle is the GC's own handle value
|
||||
* (slot << 3 | type + 1), which Unity's libmonobdwgc also returns from the v1 calls. Its scripting layer
|
||||
* tests bit 0: set (normal and weak handles), a GC handle; clear, a pointer to an object reference; and its
|
||||
* managed code takes the same values (GCHandle.FromIntPtr). This tree's runtime and CoreLib use that raw
|
||||
* value as the handle (gc-internals.h, GCHandle.cs), so v1 and v2 calls are the same functions here. */
|
||||
MonoGCHandle mono_gchandle_new_internal (MonoObject *obj, gboolean pinned);
|
||||
MonoGCHandle mono_gchandle_new_weakref_internal (MonoObject *obj, gboolean track_resurrection);
|
||||
MonoObject *mono_gchandle_get_target_internal (MonoGCHandle gchandle);
|
||||
void mono_gchandle_free_internal (MonoGCHandle gchandle);
|
||||
#define UNITY_H(h) ((uintptr_t)(h))
|
||||
#define GC_H(u) ((MonoGCHandle)(uintptr_t)(u))
|
||||
/* Unity reads handles after freeing them (Hollow Knight: get_target on a freed handle), which Unity's
|
||||
* Boehm-backed Mono answers with NULL and SGen asserts on. So the handles given out are remembered, and a
|
||||
* read or free of one that is not live is NULL / nothing. */
|
||||
static GHashTable *live_handles;
|
||||
static pthread_mutex_t live_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static uintptr_t
|
||||
handle_out (MonoGCHandle h)
|
||||
{
|
||||
uintptr_t u = UNITY_H (h);
|
||||
pthread_mutex_lock (&live_lock);
|
||||
if (!live_handles)
|
||||
live_handles = g_hash_table_new (NULL, NULL);
|
||||
g_hash_table_add (live_handles, (gpointer)u);
|
||||
pthread_mutex_unlock (&live_lock);
|
||||
if (gexmono_trace_on)
|
||||
fprintf (stderr, "[gexmono] -> gchandle %#llx\n", (unsigned long long)u);
|
||||
return u;
|
||||
}
|
||||
static gboolean
|
||||
handle_live (uintptr_t u, gboolean drop)
|
||||
{
|
||||
pthread_mutex_lock (&live_lock);
|
||||
gboolean live = live_handles && (drop ? g_hash_table_remove (live_handles, (gpointer)u) : g_hash_table_contains (live_handles, (gpointer)u));
|
||||
pthread_mutex_unlock (&live_lock);
|
||||
if (!live)
|
||||
fprintf (stderr, "[gexmono] gchandle %#llx is not live (%s)\n", (unsigned long long)u, drop ? "free" : "get_target");
|
||||
return live;
|
||||
}
|
||||
U(mono_gchandle_new_v2) { ENTER; RET (handle_out (mono_gchandle_new_internal ((MonoObject *)P (0), (gboolean)A (1)))); }
|
||||
U(mono_gchandle_new_weakref_v2) { ENTER; RET (handle_out (mono_gchandle_new_weakref_internal ((MonoObject *)P (0), (gboolean)A (1)))); }
|
||||
U(mono_gchandle_get_target_v2) { ENTER; RET (handle_live (A (0), FALSE) ? (uintptr_t)mono_gchandle_get_target_internal (GC_H (A (0))) : 0); }
|
||||
U(mono_gchandle_free_v2) { ENTER; if (handle_live (A (0), TRUE)) mono_gchandle_free_internal (GC_H (A (0))); RETV; }
|
||||
/* The v1 calls too, in the same encoding: Unity mixes them (a handle from one, freed or read by the other). */
|
||||
U(mono_gchandle_new) { ENTER; RET ((uint32_t)handle_out (mono_gchandle_new_internal ((MonoObject *)P (0), (gboolean)A (1)))); }
|
||||
U(mono_gchandle_new_weakref) { ENTER; RET ((uint32_t)handle_out (mono_gchandle_new_weakref_internal ((MonoObject *)P (0), (gboolean)A (1)))); }
|
||||
U(mono_gchandle_get_target) { ENTER; RET (handle_live ((uint32_t)A (0), FALSE) ? (uintptr_t)mono_gchandle_get_target_internal (GC_H ((uint32_t)A (0))) : 0); }
|
||||
U(mono_gchandle_free) { ENTER; if (handle_live ((uint32_t)A (0), TRUE)) mono_gchandle_free_internal (GC_H ((uint32_t)A (0))); RETV; }
|
||||
U(mono_pmip) { ENTER; RET (0); }
|
||||
U(mono_debug_lookup_source_location_by_il) { ENTER; RET (0); }
|
||||
|
||||
@@ -340,6 +389,15 @@ set_assemblies_path (const char *unity_path)
|
||||
g_string_free (s, TRUE);
|
||||
}
|
||||
|
||||
/* Unity 6 maps DllImport names (mono_dllmap_insert), which .NET's Mono does not have: noted, and left
|
||||
* to the find-plugin callback and dl fallback, which see the original name. */
|
||||
U(mono_dllmap_insert)
|
||||
{
|
||||
ENTER;
|
||||
fprintf (stderr, "[gexmono] dllmap %s%s%s -> %s%s%s (ignored)\n", P (1) ? (const char *)P (1) : "?", P (2) ? ":" : "",
|
||||
P (2) ? (const char *)P (2) : "", P (3) ? (const char *)P (3) : "?", P (4) ? ":" : "", P (4) ? (const char *)P (4) : "");
|
||||
RETV;
|
||||
}
|
||||
U(mono_set_dirs) { ENTER; set_assemblies_path (P (0)); RETV; }
|
||||
U(mono_set_assemblies_path) { ENTER; set_assemblies_path (P (0)); RETV; }
|
||||
U(mono_set_assemblies_path_null_separated)
|
||||
@@ -395,7 +453,7 @@ __attribute__((used)) gexp_body_t const gexmono_unity_adapters [] = {
|
||||
L(mono_runtime_unhandled_exception_policy_set) L(mono_threads_set_shutting_down)
|
||||
L(mono_thread_suspend_all_other_threads) L(mono_thread_pool_cleanup) L(mono_thread_push_appdomain_ref)
|
||||
L(mono_thread_pop_appdomain_ref) L(mono_thread_has_sufficient_execution_stack) L(mono_gchandle_is_in_domain)
|
||||
L(mono_gchandle_is_in_domain_v2) L(mono_pmip) L(mono_debug_lookup_source_location_by_il)
|
||||
L(mono_gchandle_is_in_domain_v2) L(mono_gchandle_new_v2) L(mono_gchandle_new_weakref_v2) L(mono_gchandle_get_target_v2) L(mono_gchandle_free_v2) L(mono_gchandle_new) L(mono_gchandle_new_weakref) L(mono_gchandle_get_target) L(mono_gchandle_free) L(mono_pmip) L(mono_debug_lookup_source_location_by_il)
|
||||
L(mono_unity_gc_set_mode) L(mono_unity_gc_disable) L(mono_unity_gc_enable) L(mono_unity_gc_is_disabled)
|
||||
L(mono_gc_is_incremental) L(mono_gc_set_incremental) L(mono_gc_collect_a_little) L(mono_gc_get_max_time_slice_ns)
|
||||
L(mono_gc_set_max_time_slice_ns) L(mono_gc_start_incremental_collection) L(mono_gc_set_heap_validate_frequency)
|
||||
@@ -413,7 +471,7 @@ __attribute__((used)) gexp_body_t const gexmono_unity_adapters [] = {
|
||||
L(mono_profiler_create) L(mono_profiler_enable_coverage) L(mono_profiler_set_coverage_filter_callback)
|
||||
L(mono_profiler_get_coverage_data) L(mono_profiler_get_all_coverage_data) L(mono_profiler_reset_coverage)
|
||||
L(mono_profiler_reset_all_coverage) L(mono_assembly_foreach) L(mono_stack_walk) L(mono_stack_walk_no_il)
|
||||
L(mono_trace_set_log_handler) L(mono_set_dirs) L(mono_set_assemblies_path) L(mono_set_assemblies_path_null_separated)
|
||||
L(mono_trace_set_log_handler) L(mono_dllmap_insert) L(mono_set_dirs) L(mono_set_assemblies_path) L(mono_set_assemblies_path_null_separated)
|
||||
L(mono_set_find_plugin_callback) L(mono_dl_fallback_register)
|
||||
L(mono_class_get_field_from_name) L(mono_custom_attrs_from_field) L(unity_mono_method_is_generic) L(unity_mono_method_is_inflated) L(unity_mono_reflection_method_get_method)
|
||||
0
|
||||
|
||||
@@ -1,3 +1,41 @@
|
||||
diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/GCHandle.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/GCHandle.cs
|
||||
index 060143b50..eaed95671 100644
|
||||
--- a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/GCHandle.cs
|
||||
+++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/GCHandle.cs
|
||||
@@ -46,11 +46,13 @@ private GCHandle(object? value, GCHandleType type)
|
||||
|
||||
IntPtr handle = InternalAlloc(value, type);
|
||||
|
||||
+#if !MONO
|
||||
if (type == GCHandleType.Pinned)
|
||||
{
|
||||
// Record if the handle is pinned.
|
||||
handle |= 1;
|
||||
}
|
||||
+#endif
|
||||
|
||||
_handle = handle;
|
||||
}
|
||||
@@ -180,10 +182,19 @@ public static GCHandle FromIntPtr(IntPtr value)
|
||||
public static bool operator !=(GCHandle a, GCHandle b) => a._handle != b._handle;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
+#if MONO
|
||||
+ // gex's Mono: handles are the GC's raw values, as Unity's native code passes them (gc-internals.h);
|
||||
+ // the low three bits are the handle type + 1, so a pinned handle (type 3) ends in 4.
|
||||
+ private static IntPtr GetHandleValue(IntPtr handle) => handle;
|
||||
+
|
||||
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
+ private static bool IsPinned(IntPtr handle) => (handle & 7) == 4;
|
||||
+#else
|
||||
private static IntPtr GetHandleValue(IntPtr handle) => new IntPtr(handle & ~1); // Remove Pin flag
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsPinned(IntPtr handle) => (handle & 1) != 0; // Check Pin flag
|
||||
+#endif
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ThrowIfInvalid(IntPtr handle)
|
||||
diff --git a/src/mono/cmake/config.h.in b/src/mono/cmake/config.h.in
|
||||
index 7d2b81fc9..56a529ae6 100644
|
||||
--- a/src/mono/cmake/config.h.in
|
||||
@@ -13,6 +51,24 @@ index 7d2b81fc9..56a529ae6 100644
|
||||
+#if defined(HOST_BROWSER) || defined(HOST_GEX)
|
||||
+#define WJ_RUNTIME 1
|
||||
+#endif
|
||||
diff --git a/src/mono/mono/metadata/gc-internals.h b/src/mono/mono/metadata/gc-internals.h
|
||||
index f0141675b..632d3eb4f 100644
|
||||
--- a/src/mono/mono/metadata/gc-internals.h
|
||||
+++ b/src/mono/mono/metadata/gc-internals.h
|
||||
@@ -30,8 +30,11 @@
|
||||
* are used to convert between the old int32 representation to a netcore compatible pointer
|
||||
* representation.
|
||||
*/
|
||||
-#define MONO_GC_HANDLE_TO_UINT(ptr) ((guint32)((size_t)(ptr) >> 1))
|
||||
-#define MONO_GC_HANDLE_FROM_UINT(i) ((MonoGCHandle)((size_t)(i) << 1))
|
||||
+/* gex: a handle is the GC's own value, unshifted, as in Unity's Mono, whose native and managed code (UnityPlayer
|
||||
+ * and UnityEngine's GCHandle.FromIntPtr) pass the same raw values both ways. CoreLib's GCHandle is patched to
|
||||
+ * match: no pin bit, a pinned handle is told by its type bits (the GC's type + 1 in the low three bits). */
|
||||
+#define MONO_GC_HANDLE_TO_UINT(ptr) ((guint32)(size_t)(ptr))
|
||||
+#define MONO_GC_HANDLE_FROM_UINT(i) ((MonoGCHandle)(size_t)(i))
|
||||
/*
|
||||
* Return a GC descriptor for an array containing N pointers to memory allocated
|
||||
* by mono_gc_alloc_fixed ().
|
||||
diff --git a/src/mono/mono/metadata/sgen-mono.c b/src/mono/mono/metadata/sgen-mono.c
|
||||
index ed1afb5a7..45839cad6 100644
|
||||
--- a/src/mono/mono/metadata/sgen-mono.c
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
diff --git a/src/mono/mono/metadata/sgen-stw.c b/src/mono/mono/metadata/sgen-stw.c
|
||||
index b7f15460e..2eebcbb12 100644
|
||||
--- a/src/mono/mono/metadata/sgen-stw.c
|
||||
+++ b/src/mono/mono/metadata/sgen-stw.c
|
||||
@@ -390,6 +390,18 @@ unified_suspend_stop_world (MonoThreadInfoFlags flags, unified_suspend_thread_st
|
||||
continue;
|
||||
}
|
||||
|
||||
+#ifdef HOST_GEX
|
||||
+ /* gex: a stop-the-world that keeps restarting a thread still in a critical location: say why, once. */
|
||||
+ if (sleep_duration == 20000) {
|
||||
+ MonoThreadUnwindState *st = mono_thread_info_get_suspend_state (info);
|
||||
+ g_warning ("gex STW: thread %p still critical: platform %d inside %d gc %d domain %p sp %p ip %p stack [%p, %p) state %d",
|
||||
+ mono_thread_info_get_tid (info), mono_threads_platform_in_critical_region (info),
|
||||
+ info->client_info.info.inside_critical_region, info->client_info.in_critical_region,
|
||||
+ st->unwind_data [MONO_UNWIND_DATA_DOMAIN], MONO_CONTEXT_GET_SP (&st->ctx),
|
||||
+ MONO_CONTEXT_GET_IP (&st->ctx), info->client_info.info.stack_start_limit, info->client_info.info.stack_end,
|
||||
+ mono_thread_info_current_state (info));
|
||||
+ }
|
||||
+#endif
|
||||
suspend_count = mono_thread_info_suspend_count (info);
|
||||
if (!(suspend_count == 1))
|
||||
g_error ("[%p] suspend_count = %d, but should be 1", mono_thread_info_get_tid (info), suspend_count);
|
||||
diff --git a/src/mono/mono/utils/mono-threads-gex.c b/src/mono/mono/utils/mono-threads-gex.c
|
||||
index 0036d0da9..8d5dfb7de 100644
|
||||
--- a/src/mono/mono/utils/mono-threads-gex.c
|
||||
+++ b/src/mono/mono/utils/mono-threads-gex.c
|
||||
@@ -28,10 +28,25 @@ mono_threads_suspend_init (void)
|
||||
* of its guest stack. */
|
||||
void gexp_thread_guest_info (guint64 *ctx, guint64 *stack_end);
|
||||
|
||||
+#include <stdlib.h>
|
||||
+#include <stdio.h>
|
||||
+#include <unistd.h>
|
||||
+#include <sys/syscall.h>
|
||||
+
|
||||
void
|
||||
mono_threads_suspend_register (MonoThreadInfo *info)
|
||||
{
|
||||
gexp_thread_guest_info (&info->gex_ctx, &info->gex_stack_end);
|
||||
+ /* GEXMONO_TRACE_THREADS=1: each thread as it registers, with the C stack Mono will check its saved
|
||||
+ * states against. */
|
||||
+ static int trace = -1;
|
||||
+ if (trace < 0)
|
||||
+ trace = getenv ("GEXMONO_TRACE_THREADS") != NULL;
|
||||
+ if (trace) {
|
||||
+ int here;
|
||||
+ fprintf (stderr, "[gexmono] thread %ld (%p) registers: stack [%p, %p), sp %p\n", (long)syscall (SYS_gettid),
|
||||
+ (void *)mono_thread_info_get_tid (info), info->stack_start_limit, info->stack_end, (void *)&here);
|
||||
+ }
|
||||
}
|
||||
|
||||
void
|
||||
@@ -18,28 +18,45 @@ __attribute__((import_module("gex"), import_name("provider_stop_guest"))) void g
|
||||
__attribute__((import_module("gex"), import_name("provider_start_guest"))) void gexp_host_start_guest(void);
|
||||
__attribute__((import_module("gex"), import_name("provider_guest_stack_end"))) uint64_t gexp_host_guest_stack_end(uint64_t sp);
|
||||
|
||||
/* A runtime's hooks at the crossing (a GC-safe region while in guest code). */
|
||||
/* A runtime's hooks at the crossing (a GC-safe region while in guest code).
|
||||
* Entering from guest code and returning to it (an adapter: gexp_enter,
|
||||
* gexp_return) are one pair; calling guest code and its return
|
||||
* (gexp_call_guest) the other, whose cookie the caller keeps. A raise that
|
||||
* abandons guest frames resets the entry pairs to a mark. */
|
||||
static void no_hook(void) {}
|
||||
static void *no_call_hook(void) { return 0; }
|
||||
static void no_call_return(void *cookie) {}
|
||||
static int no_mark(void) { return 0; }
|
||||
static void no_unwind(int mark) {}
|
||||
__attribute__((weak, alias("no_hook"))) void gexp_hook_to_guest(void);
|
||||
__attribute__((weak, alias("no_hook"))) void gexp_hook_from_guest(void);
|
||||
__attribute__((weak, alias("no_call_hook"))) void *gexp_hook_call_guest(void);
|
||||
__attribute__((weak, alias("no_call_return"))) void gexp_hook_call_return(void *cookie);
|
||||
__attribute__((weak, alias("no_mark"))) int gexp_hook_mark(void);
|
||||
__attribute__((weak, alias("no_unwind"))) void gexp_hook_unwind(int mark);
|
||||
|
||||
void gexp_attach_thread(void)
|
||||
{
|
||||
gexp_host_thread_register(&gexp_in_guest, &gexp_stop);
|
||||
}
|
||||
|
||||
static void in_guest(int v)
|
||||
{
|
||||
__atomic_store_n(&gexp_in_guest, v, __ATOMIC_SEQ_CST);
|
||||
if (v && __atomic_load_n(&gexp_stop, __ATOMIC_SEQ_CST))
|
||||
gexp_host_park();
|
||||
}
|
||||
|
||||
void gexp_from_guest(void)
|
||||
{
|
||||
__atomic_store_n(&gexp_in_guest, 0, __ATOMIC_SEQ_CST);
|
||||
in_guest(0);
|
||||
gexp_hook_from_guest();
|
||||
}
|
||||
|
||||
void gexp_to_guest(void)
|
||||
{
|
||||
gexp_hook_to_guest();
|
||||
__atomic_store_n(&gexp_in_guest, 1, __ATOMIC_SEQ_CST);
|
||||
if (__atomic_load_n(&gexp_stop, __ATOMIC_SEQ_CST))
|
||||
gexp_host_park();
|
||||
in_guest(1);
|
||||
}
|
||||
|
||||
void gexp_stop_guest_threads(void)
|
||||
@@ -119,7 +136,8 @@ void gexp_call_guest(uint64_t fn, gexp_call *c)
|
||||
for (unsigned i = 0; i < c->nxmm && i < 8; i++)
|
||||
*(volatile uint64_t *)((uintptr_t)(uint32_t)ctx + GEXP_CTX_V + 16 * i) = c->xmm[i];
|
||||
GEXP_X(ctx, 4) = sp;
|
||||
gexp_to_guest();
|
||||
void *cookie = gexp_hook_call_guest();
|
||||
in_guest(1);
|
||||
/* Run the guest until it returns to `ra`. A function body returns 1 when
|
||||
* its return went to the address its caller predicted; otherwise 0, with
|
||||
* PC where the guest continues (a longjmp inside the callee, a signal, a
|
||||
@@ -139,7 +157,8 @@ void gexp_call_guest(uint64_t fn, gexp_call *c)
|
||||
gexp_ctx = ctx;
|
||||
gexp_depth = depth;
|
||||
__gex_set_ctx((uint32_t)ctx);
|
||||
gexp_from_guest();
|
||||
in_guest(0);
|
||||
gexp_hook_call_return(cookie);
|
||||
GEXP_X(ctx, 4) = rsp0;
|
||||
c->rax = GEXP_X(ctx, 0);
|
||||
c->rdx = GEXP_X(ctx, 2);
|
||||
@@ -177,6 +196,7 @@ int gexp_catching(void)
|
||||
uint64_t gexp_call_guest_catching(uint64_t fn, gexp_call *c)
|
||||
{
|
||||
catching k = { catch_top, fn, c, 0, gexp_ctx, gexp_depth };
|
||||
int mark = gexp_hook_mark();
|
||||
for (int i = 0; i < 16; i++)
|
||||
k.saved[i] = GEXP_X(k.ctx, i);
|
||||
catch_top = &k;
|
||||
@@ -191,6 +211,8 @@ uint64_t gexp_call_guest_catching(uint64_t fn, gexp_call *c)
|
||||
gexp_ctx = k.ctx;
|
||||
gexp_depth = k.depth;
|
||||
__gex_set_ctx((uint32_t)k.ctx);
|
||||
in_guest(0);
|
||||
gexp_hook_unwind(mark);
|
||||
if (code != GEXP_RAISE_CODE)
|
||||
gexp_host_throw(code); /* someone else's: keep going */
|
||||
return k.payload ? k.payload : 1;
|
||||
@@ -274,3 +296,32 @@ int gexp_reverse_return(int ctx, uint64_t ret, const gexp_call *c)
|
||||
*(volatile uint64_t *)((uintptr_t)(uint32_t)ctx + GEXP_CTX_V + 16) = c->xmm1;
|
||||
return gexp_return(ctx, ret);
|
||||
}
|
||||
|
||||
/* ---- signals at a provider's syscalls ----
|
||||
* A signal handled at a syscall's return (musl wasm64's syscall stub): gex-kernel has left an x86-64
|
||||
* rt_sigframe on the guest stack, RSP at it (its first word the restorer's return address), the handler's
|
||||
* arguments in rdi/rsi/rdx and PC at the handler, which is guest code. Run it by the guest ABI, then do
|
||||
* what the restorer does (pop the return address, rt_sigreturn), which puts back the interrupted context,
|
||||
* the syscall's result and PC included; the loop ends when PC is back where the stub was. Overrides musl's
|
||||
* default, which stops. */
|
||||
#define X86_64_RT_SIGRETURN 15
|
||||
__attribute__((import_module("gex"), import_name("syscall"))) void gexp_host_syscall_ctx(int ctx, long long pc);
|
||||
|
||||
void __gex_signal_frames(volatile unsigned long long *x, unsigned long long pc)
|
||||
{
|
||||
/* A thread of the provider's own (not entered from guest code) has no gexp_ctx yet; this is its context. */
|
||||
if (!gexp_ctx)
|
||||
gexp_ctx = (int)(uintptr_t)x;
|
||||
while (x[32] != pc) {
|
||||
uint64_t handler = x[32], frame = x[4];
|
||||
gexp_call c = { 0 };
|
||||
c.ireg[0] = x[7]; /* signal */
|
||||
c.ireg[1] = x[6]; /* &info */
|
||||
c.ireg[2] = x[2]; /* &uc */
|
||||
c.nireg = 3;
|
||||
gexp_call_guest(handler, &c);
|
||||
x[4] = frame + 8;
|
||||
x[0] = X86_64_RT_SIGRETURN;
|
||||
gexp_host_syscall_ctx((int)(uintptr_t)x, 0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user