Loading some Plugins causes FrooxEngine to explode
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
- FrooxEngine shouldn't crash (while initializing)
- 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)
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.
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() { }
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;
}
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>() { }
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
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)
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 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);
}
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
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));
}
}
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.
// 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);
}
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.
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.