Resonite-Issues icon indicating copy to clipboard operation
Resonite-Issues copied to clipboard

Loading some Plugins causes FrooxEngine to explode

Open ColinTimBarndt opened this issue 3 months ago • 11 comments

Describe the bug?

When a Plugin is loaded which contains a custom Component class extending StaticAssetProvider, the FrooxEngine process crashes. The Renderer keeps running and is eating the GPU.

Relevant Exception
❌11:13:26 PM.530 (FPS: 0):	Unhandled exception when running the engine:
System.ArgumentException: Member 'FrooxEngine.Sync`1' is declared in another module and needs to be imported
   at Mono.Cecil.MetadataBuilder.LookupToken(IMetadataTokenProvider provider)
   at Mono.Cecil.SignatureWriter.MakeTypeDefOrRefCodedRID(TypeReference type)
   at Mono.Cecil.SignatureWriter.WriteTypeSignature(TypeReference type)
   at Mono.Cecil.MetadataBuilder.GetFieldSignature(FieldReference field)
   at Mono.Cecil.MetadataBuilder.GetMemberRefSignature(MemberReference member)
   at Mono.Cecil.MetadataBuilder.CreateMemberRefRow(MemberReference member)
   at Mono.Cecil.MetadataBuilder.GetMemberRefToken(MemberReference member)
   at Mono.Cecil.MetadataBuilder.LookupToken(IMetadataTokenProvider provider)
   at Mono.Cecil.Cil.CodeWriter.WriteOperand(Instruction instruction)
   at Mono.Cecil.Cil.CodeWriter.WriteInstructions()
   at Mono.Cecil.Cil.CodeWriter.WriteResolvedMethodBody(MethodDefinition method)
   at Mono.Cecil.Cil.CodeWriter.WriteMethodBody(MethodDefinition method)
   at Mono.Cecil.MetadataBuilder.AddMethod(MethodDefinition method)
   at Mono.Cecil.MetadataBuilder.AddMethods(TypeDefinition type)
   at Mono.Cecil.MetadataBuilder.AddType(TypeDefinition type)
   at Mono.Cecil.MetadataBuilder.AddTypes()
   at Mono.Cecil.MetadataBuilder.BuildTypes()
   at Mono.Cecil.MetadataBuilder.BuildModule()
   at Mono.Cecil.MetadataBuilder.BuildMetadata()
   at Mono.Cecil.ModuleWriter.<>c.<BuildMetadata>b__2_0(MetadataBuilder builder, MetadataReader _)
   at Mono.Cecil.ModuleDefinition.Read[TItem,TRet](TItem item, Func`3 read)
   at Mono.Cecil.ModuleWriter.BuildMetadata(ModuleDefinition module, MetadataBuilder metadata)
   at Mono.Cecil.ModuleWriter.Write(ModuleDefinition module, Disposable`1 stream, WriterParameters parameters)
   at Mono.Cecil.ModuleWriter.WriteModule(ModuleDefinition module, Disposable`1 stream, WriterParameters parameters)
   at Mono.Cecil.ModuleDefinition.Write(Stream stream, WriterParameters parameters)
   at Mono.Cecil.ModuleDefinition.Write(WriterParameters parameters)
   at Mono.Cecil.AssemblyDefinition.Write(WriterParameters parameters)
   at FrooxEngine.Weaver.AssemblyPostProcessor.Process(String path, String& versionNumber, String frooxEngineModuleRoot) in D:\Workspace\Everion\FrooxEngine\FrooxEngine.Weaver\AssemblyPostProcessor.cs:line 502
   at FrooxEngine.Weaver.AssemblyPostProcessor.Process(String path, String frooxEngineModuleRoot) in D:\Workspace\Everion\FrooxEngine\FrooxEngine.Weaver\AssemblyPostProcessor.cs:line 46
   at FrooxEngine.Engine.ProcessStartupCommands(LaunchOptions options)
   at FrooxEngine.Engine.Initialize(String appPath, Boolean useRenderer, LaunchOptions options, ISystemInfo systemInfo, IEngineInitProgress progress)
   at Renderite.Host.GraphicalClientRunner.Run(LaunchOptions options) in D:\Workspace\Everion\FrooxEngine\GraphicalClient\GraphicalClientRunner.cs:line 115
   at Program.<Main>$(String[] args) in D:\Workspace\Everion\FrooxEngine\GraphicalClient\Program.cs:line 51

To Reproduce

Load such a plugin by specifying -LoadAssembly Plugins/Plugin.Wasm.dll in your Steam launch arguments (assuming that the plugin is at that location).

Reproduction Item/World

The below ZIP file contains the plugin DLL built from https://github.com/ColinTimBarndt/resonite-wasm-experiments/tree/main/Plugin.Wasm. Plugin.zip

Expected behavior

  1. FrooxEngine shouldn't crash (while initializing)
  2. If it crashes, the Renderer should be stopped as well

Screenshots

nothing to see here

Resonite Version Number

2025.9.12.1173

What Platforms does this occur on?

Linux

What headset if any do you use?

Desktop

Log Files

desktop-colin - 2025.9.12.1173 - 2025-09-21 23_13_26.log

Additional Context

I'm trying to implement WebAssembly for FrooxEngine as a Plugin to test how it could be done. This requires me to implement WebAssembly assets and a StaticWebAssembly component which extends StaticAssetProvider<WebAssemblyModule, DummyMetadata, SingleVariantDescriptor>.

Reporters

Colin The Cat (colin.cat)

ColinTimBarndt avatar Sep 21 '25 21:09 ColinTimBarndt

Hello! Here are the results of the automated log parsing:

Version OS CPU GPU VRAM RAM Headset Plug-ins/Mods Renderer Clean Exit
Beta 2025.9.12.1173 Steam Runtime AMD Ryzen 7 9800X3D 8-Core Processor Navi 10 [Radeon RX 5600 OEM/5600 XT / 5700/5700 XT] (rev c1) 31.20 GB 62.40 GB no None

This message has been auto-generated using logscanner.

github-actions[bot] avatar Sep 21 '25 21:09 github-actions[bot]

More details

The issue appears to only happen when the class is extending a foreign class with generic type parameters.

These work:

[Category(["**TEST**"])]
public class StaticTestThing() : StaticBinary() { }

[Category(["**TEST**"])]
public class TypeTestThing() : TypeField() { }
Image

This one also surprisingly works. I wanted to test if the generic class has to be from a separate assembly.

[Category(["**TEST**"])]
public class TestThing() : OtherTestThing<int>() { }

[Category(["**TEST**"])]
[GenericTypes(GenericTypesAttribute.Group.EnginePrimitives)]
public class OtherTestThing<T>() : Component where T : unmanaged
{
    public readonly Sync<T> Bwah;
}
Image

Any of these do not work:

[Category(["**TEST**"])]
public class ValueTestThing() : ValueField<int>() { }

[Category(["**TEST**"])]
public class RefTestThing() : ReferenceField<Slot>() { }

[Category(["**TEST**"])]
public class TestThing() : ValueDriver<int>() { }

ColinTimBarndt avatar Sep 21 '25 23:09 ColinTimBarndt

I am in the process of forking Mono.Cecil to add debug information, and it's helping me find the issue. Apparently, the type for the sync member is not being resolved:

[Category(["**TEST**"])]
public class TestThing() : ValueUserOverride<int> { }
❌2:14:14 AM.552 (FPS: 0):	Unhandled exception when running the engine:
System.ArgumentException: Member 'FrooxEngine.FieldDrive`1' (Mono.Cecil.TypeDefinition) is declared in another module 'FrooxEngine.dll' and needs to be imported (expected Plugin.Wasm.dll)
   at Mono.Cecil.MetadataBuilder.LookupToken(IMetadataTokenProvider provider) in [...]/Mono.Cecil/AssemblyWriter.cs:line 2283
   at Mono.Cecil.SignatureWriter.MakeTypeDefOrRefCodedRID(TypeReference type) in [...]/Mono.Cecil/AssemblyWriter.cs:line 2761
   at Mono.Cecil.SignatureWriter.WriteTypeSignature(TypeReference type) in [...]/Mono.Cecil/AssemblyWriter.cs:line 2794
   at Mono.Cecil.MetadataBuilder.GetFieldSignature(FieldReference field) in [...]/Mono.Cecil/AssemblyWriter.cs:line 2143
   at Mono.Cecil.MetadataBuilder.GetMemberRefSignature(MemberReference member) in [...]/Mono.Cecil/AssemblyWriter.cs:line 2158
   at Mono.Cecil.MetadataBuilder.CreateMemberRefRow(MemberReference member) in [...]/Mono.Cecil/AssemblyWriter.cs:line 2040
   at Mono.Cecil.MetadataBuilder.GetMemberRefToken(MemberReference member) in [...]/Mono.Cecil/AssemblyWriter.cs:line 2029
   at Mono.Cecil.MetadataBuilder.LookupToken(IMetadataTokenProvider provider) in [...]/Mono.Cecil/AssemblyWriter.cs:line 2301
   at Mono.Cecil.Cil.CodeWriter.WriteOperand(Instruction instruction) in [...]/Mono.Cecil.Cil/CodeWriter.cs:line 270
   at Mono.Cecil.Cil.CodeWriter.WriteInstructions() in [...]/Mono.Cecil.Cil/CodeWriter.cs:line 180
   at Mono.Cecil.Cil.CodeWriter.WriteResolvedMethodBody(MethodDefinition method) in [...]/Mono.Cecil.Cil/CodeWriter.cs:line 111
   at Mono.Cecil.Cil.CodeWriter.WriteMethodBody(MethodDefinition method) in [...]/Mono.Cecil.Cil/CodeWriter.cs:line 54
   at Mono.Cecil.MetadataBuilder.AddMethod(MethodDefinition method) in [...]/Mono.Cecil/AssemblyWriter.cs:line 1683
   at Mono.Cecil.MetadataBuilder.AddMethods(TypeDefinition type) in [...]/Mono.Cecil/AssemblyWriter.cs:line 1676
   at Mono.Cecil.MetadataBuilder.AddType(TypeDefinition type) in [...]/Mono.Cecil/AssemblyWriter.cs:line 1473
   at Mono.Cecil.MetadataBuilder.AddTypes() in [...]/Mono.Cecil/AssemblyWriter.cs:line 1445
   at Mono.Cecil.MetadataBuilder.BuildTypes() in [...]/Mono.Cecil/AssemblyWriter.cs:line 1294
   at Mono.Cecil.MetadataBuilder.BuildModule() in [...]/Mono.Cecil/AssemblyWriter.cs:line 1064
   at Mono.Cecil.MetadataBuilder.BuildMetadata() in [...]/Mono.Cecil/AssemblyWriter.cs:line 1034
   at Mono.Cecil.ModuleWriter.<>c.<BuildMetadata>b__2_0(MetadataBuilder builder, MetadataReader _) in [...]/Mono.Cecil/AssemblyWriter.cs:line 147
   at Mono.Cecil.ModuleDefinition.Read[TItem,TRet](TItem item, Func`3 read) in [...]/Mono.Cecil/ModuleDefinition.cs:line 982
   at Mono.Cecil.ModuleWriter.BuildMetadata(ModuleDefinition module, MetadataBuilder metadata) in [...]/Mono.Cecil/AssemblyWriter.cs:line 146
   at Mono.Cecil.ModuleWriter.Write(ModuleDefinition module, Disposable`1 stream, WriterParameters parameters) in [...]/Mono.Cecil/AssemblyWriter.cs:line 119
   at Mono.Cecil.ModuleWriter.WriteModule(ModuleDefinition module, Disposable`1 stream, WriterParameters parameters) in [...]/Mono.Cecil/AssemblyWriter.cs:line 78
   at Mono.Cecil.ModuleDefinition.Write(Stream stream, WriterParameters parameters) in [...]/Mono.Cecil/ModuleDefinition.cs:line 1198
   at Mono.Cecil.ModuleDefinition.Write(WriterParameters parameters) in [...]/Mono.Cecil/ModuleDefinition.cs:line 1184
   at Mono.Cecil.AssemblyDefinition.Write(WriterParameters parameters) in [...]/Mono.Cecil/AssemblyDefinition.cs:line 171
   at FrooxEngine.Weaver.AssemblyPostProcessor.Process(String path, String& versionNumber, String frooxEngineModuleRoot) in D:\Workspace\Everion\FrooxEngine\FrooxEngine.Weaver\AssemblyPostProcessor.cs:line 502
   at FrooxEngine.Weaver.AssemblyPostProcessor.Process(String path, String frooxEngineModuleRoot) in D:\Workspace\Everion\FrooxEngine\FrooxEngine.Weaver\AssemblyPostProcessor.cs:line 46
   at FrooxEngine.Engine.ProcessStartupCommands(LaunchOptions options)
   at FrooxEngine.Engine.Initialize(String appPath, Boolean useRenderer, LaunchOptions options, ISystemInfo systemInfo, IEngineInitProgress progress)
   at Renderite.Host.GraphicalClientRunner.Run(LaunchOptions options) in D:\Workspace\Everion\FrooxEngine\GraphicalClient\GraphicalClientRunner.cs:line 115
   at Program.<Main>$(String[] args) in D:\Workspace\Everion\FrooxEngine\GraphicalClient\Program.cs:line 51

ColinTimBarndt avatar Sep 22 '25 00:09 ColinTimBarndt

I was able to further narrow it down. The problem lies within the generated "GetSyncMember" method. When the assembly is generated, it attempts to get a field reference. If the type of that reference has a generic parameter, the whole type is not correctly imported. I've modified Cecil to produce a lot of debug info, see below:

[DEBUG] [Mono.Cecil] WriteResolvedMethodBody('FrooxEngine.ISyncMember Plugin.Wasm.TestThing::GetSyncMember(System.Int32)')
[DEBUG] [Mono.Cecil] Instruction: IL_0000: ldarg.1
[DEBUG] [Mono.Cecil] Instruction: IL_0001: switch IL_002f,IL_0036,IL_003d,IL_0044,IL_004b,IL_0052,IL_0059,IL_0060,IL_0067
[DEBUG] [Mono.Cecil] Instruction: IL_002a: br IL_006e
...
[DEBUG] [Mono.Cecil] Instruction: IL_0060: ldarg.0
[DEBUG] [Mono.Cecil] Instruction: IL_0061: ldfld FrooxEngine.SyncBag`1<FrooxEngine.ValueOverrideBase`1/Override<T>> FrooxEngine.ValueOverrideBase`1<System.Int32>::_overrides
[DEBUG] [Mono.Cecil] Field Ref Type Module: Plugin.Wasm.dll <-- CORRECTLY IMPORTED
[DEBUG] [Mono.Cecil] LookupToken(FrooxEngine.SyncBag`1<FrooxEngine.ValueOverrideBase`1/Override<T>> FrooxEngine.ValueOverrideBase`1<System.Int32>::_overrides)
[DEBUG] [Mono.Cecil] LookupToken(FrooxEngine.ValueOverrideBase`1)
[DEBUG] [Mono.Cecil] LookupToken(FrooxEngine.SyncBag`1)
[DEBUG] [Mono.Cecil] LookupToken(FrooxEngine.ValueOverrideBase`1/Override)
[DEBUG] [Mono.Cecil] Instruction: IL_0066: ret
[DEBUG] [Mono.Cecil] Instruction: IL_0067: ldarg.0
[DEBUG] [Mono.Cecil] Instruction: IL_0068: ldfld FrooxEngine.FieldDrive`1<T> FrooxEngine.ValueUserOverride`1<System.Int32>::Target
[DEBUG] [Mono.Cecil] Field Ref Type Module: FrooxEngine.dll <-- HERE IS THE PROBLEM
[DEBUG] [Mono.Cecil] LookupToken(FrooxEngine.FieldDrive`1<T> FrooxEngine.ValueUserOverride`1<System.Int32>::Target)
[DEBUG] [Mono.Cecil] LookupToken(FrooxEngine.ValueUserOverride`1)
[DEBUG] [Mono.Cecil] LookupToken(FrooxEngine.FieldDrive`1)

ColinTimBarndt avatar Sep 22 '25 01:09 ColinTimBarndt

A workaround is to add [module: Description("FROOXENGINE_WEAVED")] to your plugin

This will stop the weaver from trying to process it.

However you will need to manually do all the things the weaver normally does, like adding the following methods to all components:

protected override void InitializeSyncMembers()

public override ISyncMember GetSyncMember(int index)

public static T __New()

Nytra avatar Sep 22 '25 01:09 Nytra

@Nytra hold on, I'm very close to finding a bug in Cecil :D

So, when importing the reference to the field, the type of the field is supposed to also be imported. However, when this specific field is imported, somehow the field itself is already imported, causing the code to assume that the type was imported as well:

[DEBUG] [Mono.Cecil] ImportReference<FieldReference>(FrooxEngine.FieldDrive`1<T> FrooxEngine.ValueUserOverride`1<System.Int32>::Target) 'Plugin.Wasm.dll' Typ 'FrooxEngine.dll'
public FieldReference ImportReference (FieldReference field, IGenericParameterProvider context)
{
	Console.WriteLine(string.Format("[DEBUG] [Mono.Cecil] ImportReference<FieldReference>({0}) '{1}' Typ '{2}'", field, field.Module, field.FieldType.Module));
	Mixin.CheckField (field);

	if (field.Module == this)
		return field; // short circuit

	CheckContext (context, this);


	return MetadataImporter.ImportReference (field, context);
}

ColinTimBarndt avatar Sep 22 '25 02:09 ColinTimBarndt

I correct myself, this might be a FrooxEngine bug. The below code is producing that bogus FieldReference.

public static FieldReference GetGenericFieldReference(this FieldDefinition field, TypeReference onType = null)
{
	if (!field.DeclaringType.HasGenericParameters)
	{
		return field;
	}
	GenericInstanceType declaringType = new GenericInstanceType(field.DeclaringType);
	foreach (GenericParameter p in field.DeclaringType.GenericParameters)
	{
		declaringType.GenericArguments.Add(p);
	}
	return new FieldReference(field.Name, field.FieldType, onType ?? declaringType);
}

cc @Nytra

ColinTimBarndt avatar Sep 22 '25 02:09 ColinTimBarndt

Ahh, I think I see why the code was written the way it is. It's assuming that it will either be called on FrooxEngine.dll (referencing fields within FrooxEngine.dll) or called on something External.dll (referencing fields within External.dll). If the field is defined inside of a base class within FrooxEngine.dll (on a class inside of External.dll), it just assumes that the class must be in FrooxEngine.dll and thus generating the wrong FieldReference. At least that's what I think is happening here:

// worker2 is the class of the component
foreach (WorkerFieldData fieldInfo in workerFields)
{
    if (ShouldProcessField(fieldInfo.field, worker2, null))
    {
        FieldReference fieldRef = fieldInfo.field.GetGenericFieldReference((fieldInfo.declaringType.FullName == worker2.FullName) ? null : fieldInfo.declaringType);
        if (fieldRef.FieldType.IsGenericParameter)
        {
            fieldRef = module.ImportReference(fieldRef, worker2);
        }
        else if (module.Name != "FrooxEngine.dll")
        {
            fieldRef = module.ImportReference(fieldRef, module.ImportReference(fieldInfo.declaringType));
        }
        Instruction jumpTarget = il.Create(OpCodes.Ldarg_0);
        jumpTargets.Add(jumpTarget);
        instructions.Add(jumpTarget);
        instructions.Add(il.Create(OpCodes.Ldfld, fieldRef));
        instructions.Add(il.Create(OpCodes.Ret));
    }
}

ColinTimBarndt avatar Sep 22 '25 02:09 ColinTimBarndt

I fixed the bug by patching Cecil. But I'm not sure if this is Cecil's fault by relying on a wrong invariant (that if the field's class type is imported, the field type must be imported too), or that Resonite accidentally broke that invariant.

Image
// ModuleDefinition.cs
public FieldReference ImportReference (FieldReference field, IGenericParameterProvider context)
{
	Mixin.CheckField (field);

	if (field.Module == this) {
+		if (field.FieldType.Module == this)
+			return field;
+		return new FieldReference(field.Name, MetadataImporter.ImportReference(field.FieldType, context), field.DeclaringType);
	}

	CheckContext (context, this);


	return MetadataImporter.ImportReference (field, context);
}

ColinTimBarndt avatar Sep 22 '25 02:09 ColinTimBarndt

I fixed the bug by patching Cecil. But I'm not sure if this is Cecil's fault by relying on a wrong invariant (that if the field's class type is imported, the field type must be imported too), or that Resonite accidentally broke that invariant.

I would say that's a Cecil bug - seems like an invariant that shouldn't be assumed. Especially considering that all the system types would fall under this case too.

Banane9 avatar Sep 22 '25 06:09 Banane9

The above pull request to Cecil will fix this issue! If you want to try it out, you can either use my pre-built patch release or build it yourself by cloning https://github.com/ColinTimBarndt/mono-cecil/tree/fix/import-reference.

ColinTimBarndt avatar Sep 22 '25 14:09 ColinTimBarndt