mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
Provider liveness workaround: a reported Component keeps its GameObject and the GameObject's components (stand-in wrappers), pending Unity's flag 0x2
Unity's GC marks a component's GameObject, and a GameObject's components, only when the object's GC entry has flag 0x2; objects kept through script data do not get it here, so Hollow Knight's loading-spinner prefab lost its GameObject (then its components) under a live component. For each reported UnityEngine.Component the walk now also reports, through stand-ins Unity's callback reads like wrappers (m_CachedPtr at +0x10), the component's GameObject ([native+0x20]) and that GameObject's components ([go+0x20], count [go+0x30]). With it HK instantiates the spinner and its intro asks for Menu_Title. GEXMONO_LV_NOGO=1 turns it off; the walk itself stays opt-in (GEXMONO_LIVENESS=1). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_461fe3ff06374ecba693831148697ba6
This commit is contained in:
1 parent
58793de09e
commit
6b119e1583
1 file changed
+83
-5
@@ -429,6 +429,7 @@ U(mono_unity_free_captured_memory_snapshot) { ENTER; RETV; }
|
||||
* objects meanwhile. */
|
||||
typedef struct {
|
||||
MonoClass *filter;
|
||||
GPtrArray *fakes; /* stand-in wrappers for GameObjects (lv_keep_gameobject) */
|
||||
guint32 max;
|
||||
uint64_t cb, data;
|
||||
void **arr;
|
||||
@@ -448,6 +449,7 @@ lv_new (MonoClass *filter, guint32 max, uint64_t cb, uint64_t data)
|
||||
s->arr = g_new0 (void *, s->max);
|
||||
s->seen = g_hash_table_new (NULL, NULL);
|
||||
s->stack = g_ptr_array_new ();
|
||||
s->fakes = g_ptr_array_new ();
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -504,6 +506,9 @@ lv_free (Liveness *s)
|
||||
return;
|
||||
g_hash_table_destroy (s->seen);
|
||||
g_ptr_array_free (s->stack, TRUE);
|
||||
for (guint i = 0; i < s->fakes->len; i++)
|
||||
g_free (g_ptr_array_index (s->fakes, i));
|
||||
g_ptr_array_free (s->fakes, TRUE);
|
||||
g_free (s->arr);
|
||||
g_free (s);
|
||||
}
|
||||
@@ -515,6 +520,7 @@ lv_free (Liveness *s)
|
||||
* next pass. */
|
||||
static int lv_env (const char *name);
|
||||
static GHashTable *lv_pass_seen;
|
||||
static GHashTable *lv_pass_go; /* GameObjects already reported this pass (lv_keep_gameobject) */
|
||||
static pthread_mutex_t lv_pass_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static void
|
||||
@@ -523,6 +529,8 @@ lv_pass_reset (void)
|
||||
pthread_mutex_lock (&lv_pass_lock);
|
||||
if (lv_pass_seen)
|
||||
g_hash_table_remove_all (lv_pass_seen);
|
||||
if (lv_pass_go)
|
||||
g_hash_table_remove_all (lv_pass_go);
|
||||
pthread_mutex_unlock (&lv_pass_lock);
|
||||
}
|
||||
|
||||
@@ -605,14 +613,75 @@ lv_fields (Liveness *s, MonoClass *k, char *base, gboolean boxed)
|
||||
}
|
||||
}
|
||||
|
||||
/* Workaround, pending the real rule (docs/unity-dotnet.md §12 item 16-17): Unity's GC marks a
|
||||
* component's GameObject from the component only when the component's GC entry has a flag (0x2)
|
||||
* that a component kept alive through script data does not get here, so the GameObject was
|
||||
* unloaded under a live component (Hollow Knight's loading-spinner prefab). For each reported
|
||||
* UnityEngine.Component the walk also reports its GameObject, through a stand-in for a managed
|
||||
* wrapper: Unity's callback reads only the wrapper's m_CachedPtr (+0x10) and the native object's
|
||||
* instance ID (+8); a Component's native GameObject pointer is at +0x20.
|
||||
* GEXMONO_LV_NOGO=1 turns it off. */
|
||||
static MonoClass *lv_component_class;
|
||||
|
||||
static void
|
||||
lv_report (Liveness *s, MonoObject *o)
|
||||
{
|
||||
s->arr [s->n++] = o;
|
||||
if (s->n >= s->max)
|
||||
lv_flush (s);
|
||||
}
|
||||
|
||||
static void
|
||||
lv_keep_gameobject (Liveness *s, MonoObject *o, MonoClass *k)
|
||||
{
|
||||
static int off = -1;
|
||||
if (off < 0)
|
||||
off = lv_env ("GEXMONO_LV_NOGO");
|
||||
if (off || !lv_component_class || !mono_class_is_subclass_of_internal (k, lv_component_class, FALSE))
|
||||
return;
|
||||
uint8_t *native = *(uint8_t **)((char *)o + MONO_ABI_SIZEOF (MonoObject));
|
||||
if ((uintptr_t)native < 0x10000 || (uintptr_t)native >= ((uintptr_t)1 << 34))
|
||||
return;
|
||||
uint8_t *go = *(uint8_t **)(native + 0x20);
|
||||
if ((uintptr_t)go < 0x10000 || (uintptr_t)go >= ((uintptr_t)1 << 34))
|
||||
return;
|
||||
pthread_mutex_lock (&lv_pass_lock);
|
||||
if (!lv_pass_go)
|
||||
lv_pass_go = g_hash_table_new (NULL, NULL);
|
||||
gboolean seen = g_hash_table_contains (lv_pass_go, go);
|
||||
if (!seen)
|
||||
g_hash_table_add (lv_pass_go, go);
|
||||
pthread_mutex_unlock (&lv_pass_lock);
|
||||
if (seen)
|
||||
return;
|
||||
uintptr_t *fake = g_new0 (uintptr_t, 3);
|
||||
fake [2] = (uintptr_t)go; /* where m_CachedPtr sits after the 16-byte header */
|
||||
g_ptr_array_add (s->fakes, fake);
|
||||
lv_report (s, (MonoObject *)fake);
|
||||
/* And the GameObject's components (its component array: {type, component} pairs at
|
||||
* [go+0x20], count at [go+0x30]), which Unity does not pull in from the GameObject either. */
|
||||
uintptr_t *comps = *(uintptr_t **)(go + 0x20);
|
||||
uintptr_t count = *(uintptr_t *)(go + 0x30);
|
||||
if ((uintptr_t)comps < 0x10000 || (uintptr_t)comps >= ((uintptr_t)1 << 34) || count > 256)
|
||||
return;
|
||||
for (uintptr_t i = 0; i < count; i++) {
|
||||
uintptr_t comp = comps [2 * i + 1];
|
||||
if (comp < 0x10000 || comp >= ((uintptr_t)1 << 34))
|
||||
continue;
|
||||
uintptr_t *f = g_new0 (uintptr_t, 3);
|
||||
f [2] = comp;
|
||||
g_ptr_array_add (s->fakes, f);
|
||||
lv_report (s, (MonoObject *)f);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
lv_object_walk (Liveness *s, MonoObject *o, gboolean report)
|
||||
{
|
||||
MonoClass *k = mono_object_class (o);
|
||||
if (report && s->filter && mono_class_is_subclass_of_internal (k, s->filter, FALSE)) {
|
||||
s->arr [s->n++] = o;
|
||||
if (s->n >= s->max)
|
||||
lv_flush (s);
|
||||
lv_report (s, o);
|
||||
lv_keep_gameobject (s, o, k);
|
||||
}
|
||||
if (m_class_get_rank (k)) {
|
||||
MonoArray *a = (MonoArray *)o;
|
||||
@@ -673,11 +742,20 @@ void sgen_gc_unlock (void);
|
||||
static int lv_world_stopped;
|
||||
|
||||
/* allocate_struct (filter, max_count, callback, userdata, reallocate): Unity 2021+. */
|
||||
U(mono_unity_liveness_allocate_struct) { ENTER; RET (lv_new ((MonoClass *)P (0), (guint32)A (1), A (2), A (3))); }
|
||||
static void
|
||||
lv_resolve_component (MonoClass *filter)
|
||||
{
|
||||
if (lv_component_class || !filter)
|
||||
return;
|
||||
ERROR_DECL (err);
|
||||
lv_component_class = mono_class_from_name_checked (m_class_get_image (filter), "UnityEngine", "Component", err);
|
||||
mono_error_cleanup (err);
|
||||
}
|
||||
U(mono_unity_liveness_allocate_struct) { ENTER; lv_resolve_component ((MonoClass *)P (0)); RET (lv_new ((MonoClass *)P (0), (guint32)A (1), A (2), A (3))); }
|
||||
U(mono_unity_liveness_free_struct) { ENTER; lv_free ((Liveness *)P (0)); RETV; }
|
||||
U(mono_unity_liveness_finalize) { ENTER; if (P (0)) lv_flush ((Liveness *)P (0)); RETV; }
|
||||
/* calculation_begin (filter, max_count, callback, userdata, world_start, world_stop): the older form. */
|
||||
U(mono_unity_liveness_calculation_begin) { ENTER; lv_pass_reset (); RET (lv_new ((MonoClass *)P (0), (guint32)A (1), A (2), A (3))); }
|
||||
U(mono_unity_liveness_calculation_begin) { ENTER; lv_pass_reset (); lv_resolve_component ((MonoClass *)P (0)); RET (lv_new ((MonoClass *)P (0), (guint32)A (1), A (2), A (3))); }
|
||||
U(mono_unity_liveness_calculation_end) { ENTER; if (P (0)) lv_flush ((Liveness *)P (0)); lv_free ((Liveness *)P (0)); RETV; }
|
||||
U(mono_unity_liveness_calculation_from_root)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user