unity3d Unity:如果在之前没有更新的FixedUpdate中执行,则在具有字符串属性的ref结构之间转换将失败

v09wglhw  于 2023-06-30  发布在  其他
关注(0)|答案(2)|浏览(154)

我的目标是存储帧的快照数据,用于网络协调。数据是使用一种特殊的集合类型存储的,该集合类型只接受非托管数据,但非托管数据本身可以有多种形式。负责转录数据以存储的类使用泛型ref结构,而集合使用非泛型ref结构。
我的问题是,如果被转换的ref结构包含字符串字段(这可能扩展到所有引用类型),则在FixedUpdate期间发生的转换不会正确工作,而该更新之前没有更新。
如果您通过控制台应用程序进行转换,则没有问题。
如果您在第一次更新调用之后进行转换,并且更新的频率与fixedupdate相同或更高,则不会出现问题。
如果使用不包含字符串字段的ref结构进行转换,则不会出现问题。
此问题仅在使用KeyedRefStructs时,在更新之前没有更新的已修复更新日期上发生。
如果您从PlayerLoop系统中删除FixedUpdateLoop,并在手动模拟物理时使用此代码,也没有问题。
下面是示例代码,不是实际实现。它旨在成为当您在Unity编辑器中点击play时重新创建问题所需的最小代码量。

using System;
using System.ComponentModel;
 
 
public readonly ref struct RefStruct
{
    public RefStruct(Span<byte> span)
    {
        Span = span;
    }
 
    public readonly Span<byte> Span;
}
public readonly ref struct RefStruct<T> where T : unmanaged
{
    public RefStruct(T state)
    {
        State = state;
    }
 
    public readonly T State;
 
    public static implicit operator T(RefStruct<T> snapshot)
    {
        return snapshot.State;
    }
 
    public static implicit operator RefStruct(RefStruct<T> snapshot)
    {
        var state = snapshot.State;
        Span<byte> span = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref state, 1));
        return new RefStruct(span);
    }
 
    public static implicit operator RefStruct<T>(RefStruct snapshot)
    {
        var span = snapshot.Span;
        T state = MemoryMarshal.Cast<byte, T>(span)[default];
        return new RefStruct<T>(state);
    }
}
using System;
using System.ComponentModel;
 
public readonly ref struct KeyedRefStruct
{
    public KeyedRefStruct((int, string) key, Span<byte> span)
    {
        Key = key;
        Span = span;
    }
 
    public readonly (int, string) Key;
    public readonly Span<byte> Span;
}
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
 
public readonly ref struct KeyedRefStruct<T> where T : unmanaged
{
    public KeyedRefStruct((int, string) key, T state)
    {
        Key = key;
        State = state;
    }
 
    public readonly (int, string) Key;
    public readonly T State;
 
    public static implicit operator T(KeyedRefStruct<T> snapshot)
    {
        return snapshot.State;
    }
 
    public static implicit operator KeyedRefStruct(KeyedRefStruct<T> snapshot)
    {
        var state = snapshot.State;
        Span<byte> span = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref state, 1));
        return new KeyedRefStruct(snapshot.Key, span);
    }
 
    public static implicit operator KeyedRefStruct<T>(KeyedRefStruct snapshot)
    {
        var span = snapshot.Span;
        T state = MemoryMarshal.Cast<byte, T>(span)[default];
        return new KeyedRefStruct<T>(snapshot.Key, state);
    }
}
using System;
using UnityEngine;
 
public class RefStructConverter : MonoBehaviour
{
    public float testValue = 5f;
    public bool InspectRefSpan;
 
    int frame = default;
 
    private void FixedUpdate()
    {
        if (frame <= 2)
        {
            RefStructTest(testValue);
            KeyedRefStructTest(testValue);
        }
 
        frame++;
    }
 
    void RefStructTest(float value)
    {
        float originalValue = value;
        RefStruct<float> originalRefStruct = new RefStruct<float>(originalValue);
        RefStruct castedRefStruct = originalRefStruct;
        string inspectionResults = InspectRefSpan ? " bytes: " + ContentsToString(castedRefStruct.Span) : default;
        RefStruct<float> recastedRefStruct = castedRefStruct;
        float castedValue = recastedRefStruct;
        Debug.Log($"Frame {frame} RefStruct Expected value: {originalValue} Actual value: {castedValue} {inspectionResults}");
    }
 
    void KeyedRefStructTest(float value)
    {
        float originalValue = value;
        KeyedRefStruct<float> originalRefStruct = new KeyedRefStruct<float>(default, originalValue);
        KeyedRefStruct castedRefStruct = originalRefStruct;
        string inspectionResults = InspectRefSpan ? " bytes: " + ContentsToString(castedRefStruct.Span) : default;
        KeyedRefStruct<float> recastedRefStruct = castedRefStruct;
        float castedValue = recastedRefStruct;
        Debug.Log($"Frame {frame} KeyedRefStruct Expected value: {originalValue} Actual value: {castedValue} {inspectionResults}");
    }
 
    string ContentsToString<T>(Span<T> span)
    {
        string str = string.Empty;
 
        str += "[";
 
        for (int i = default; i < span.Length; i++)
        {
            str += $"{span[i]}";
 
            if (i < span.Length - 1)
            {
                str += $", ";
            }
            else
            {
                str += "]";
            }
        }
 
        return $"{span.Length} element(s): {str}";
    }
}
Output:
Frame 0 RefStruct Expected value: 5 Actual value: 5
Frame 0 KeyedRefStruct Expected value: 5 Actual value: 0
Frame 1 RefStruct Expected value: 5 Actual value: 5
Frame 1 KeyedRefStruct Expected value: 5 Actual value: 5
Frame 2 RefStruct Expected value: 5 Actual value: 5
Frame 2 KeyedRefStruct Expected value: 5 Actual value: 5

以下是我在Unity论坛上发表的关于这个问题的帖子:https://forum.unity.com/threads/casting-ref-structs-with-string-properties-fails-if-done-in-the-first-fixedupdate-call.1454446/
一位发帖者指出,这个问题可能是由于RefStructT和RefStruct之间的隐式转换,其中在堆栈上的值上创建了一个跨度。这是有意义的,但这些错误从来没有发生RefStructT和RefStruct,只有KeyedRefStructT和KeyedRefStruct,特别是在FixedUpdates期间没有先前的更新。
任何见解将不胜感激,谢谢。
我在一个针对.net 2.1标准的控制台应用程序中测试了这段代码,我的unity项目也是针对这一标准的,只要我在发布模式下运行,控制台应用程序就能正常工作。

cyvaqqii

cyvaqqii1#

答案很简单:在转换操作符中,state是一个局部变量,当操作符函数结束时,它就会消失。但是您直接使用它来创建跨度。
您需要使用in参数和Unsafe.AsRef

public static implicit operator RefStruct(in RefStruct<T> snapshot)
{
    ref var state = ref Unsafe.AsRef(in snapshot.State);
    Span<byte> span = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref state, 1));
    return new RefStruct(span);
}

public static implicit operator KeyedRefStruct(in KeyedRefStruct<T> snapshot)
{
    ref var state = ref Unsafe.AsRef(in snapshot.State);
    Span<byte> span = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref state, 1));
    return new KeyedRefStruct(snapshot.Key, span);
}

请注意,即使这样,整个设计是高度危险Span<byte>结构体必须具有与您正在转换的结构体相同或更短的生存期,因此这些<T>结构体不得存储在堆栈上。这是非常难以实现的,因为编译器经常将值放在堆栈上。
老实说,直接对你拥有的任何值使用这些转换函数会更安全,这至少会减轻一些生存期问题。也许你可以使用Memory<T>,这会更安全,尽管你的确切用例还不清楚。

**TL;DR;**您不能随意使用Span<T>。它是一种高度受限的类型,使用Unsafe处理它可能会使您暴露于非托管堆栈的变幻莫测和GC/Jitter生存期问题。

ckocjqey

ckocjqey2#

这个问题确实是由于在堆栈上分配的变量上创建了一个跨度而引起的。
解决方案:

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
 
public readonly ref struct KeyedRefStruct<T> where T : unmanaged
{
    public KeyedRefStruct((int, string) key, ref T state)
    {
        Key = key;
        Span = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref state, 1));
    }
 
    public KeyedRefStruct((int, string) key, Span<byte> span)
    {
        Key = key;
        Span = span;
    }
 
    public readonly (int, string) Key;
    public readonly Span<byte> Span;
 
    readonly T State => MemoryMarshal.Cast<byte, T>(Span)[default];
 
    public static implicit operator T(KeyedRefStruct<T> snapshot) => snapshot.State;
    public static implicit operator KeyedRefStruct(KeyedRefStruct<T> snapshot) => new KeyedRefStruct(snapshot.Key, snapshot.Span);
    public static implicit operator KeyedRefStruct<T>(KeyedRefStruct snapshot) => new KeyedRefStruct<T>(snapshot.Key, snapshot.Span);
}

输出:
帧0 RefStruct预期值:5实际值:5
帧0 KeyedRefStruct预期值:5实际值:5
帧1 RefStruct预期值:5实际值:5
帧1 KeyedRefStruct预期值:5实际值:5
帧2 RefStruct预期值:5实际值:5
帧2 KeyedRefStruct预期值:5实际值:5

相关问题