using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Wb;
using AcDream.Core.Meshing;
using AcDream.Core.World;
using DatReaderWriter.Enums;
namespace AcDream.App.Tests.Rendering.Wb;
public sealed class DirectionalShadowPreparedDrawTests
{
[Theory]
[InlineData(TranslucencyKind.Opaque, true, DirectionalShadowCasterMaterial.Opaque)]
[InlineData(TranslucencyKind.ClipMap, true, DirectionalShadowCasterMaterial.AlphaCutout)]
[InlineData(TranslucencyKind.AlphaBlend, false, DirectionalShadowCasterMaterial.Opaque)]
[InlineData(TranslucencyKind.Additive, false, DirectionalShadowCasterMaterial.Opaque)]
[InlineData(TranslucencyKind.InvAlpha, false, DirectionalShadowCasterMaterial.Opaque)]
internal void MaterialPolicy_PreservesCutoutsAndExcludesTrueTransparency(
TranslucencyKind source,
bool expectedAccepted,
DirectionalShadowCasterMaterial expectedMaterial)
{
bool accepted = DirectionalShadowPreparedDraws.TryClassifyMaterial(
source,
out DirectionalShadowCasterMaterial material);
Assert.Equal(expectedAccepted, accepted);
if (accepted)
Assert.Equal(expectedMaterial, material);
}
[Theory]
[InlineData(0f, false)]
[InlineData(0.0001f, true)]
[InlineData(1f, true)]
[InlineData(float.NaN, true)]
internal void FadePolicy_OnlyExactOpaquePartsCast(
float translucency,
bool excluded)
{
Assert.Equal(
excluded,
DirectionalShadowPreparedDraws.FadeExcludesCaster(translucency));
}
[Fact]
public void Complete_GroupsCommandsAndRetainsExactCurrentTransforms()
{
var product = new DirectionalShadowPreparedDraws();
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(4);
Assert.True(product.TryBegin(generation, 7, estimatedInstances: 3));
Matrix4x4 first = Matrix4x4.CreateRotationZ(0.25f)
* Matrix4x4.CreateTranslation(10f, 20f, 30f);
Matrix4x4 second = Matrix4x4.CreateScale(1.25f)
* Matrix4x4.CreateTranslation(-2f, 3f, 7f);
Matrix4x4 leaves = Matrix4x4.CreateRotationX(0.5f)
* Matrix4x4.CreateTranslation(100f, 200f, 12f);
product.Add(
firstIndex: 40,
baseVertex: 3,
indexCount: 18,
GpuTextureSlot.Unassigned,
textureLayer: 0,
CullMode.CounterClockwise,
DirectionalShadowCasterMaterial.Opaque,
in first);
product.Add(
firstIndex: 40,
baseVertex: 3,
indexCount: 18,
GpuTextureSlot.Unassigned,
textureLayer: 0,
CullMode.CounterClockwise,
DirectionalShadowCasterMaterial.Opaque,
in second);
product.Add(
firstIndex: 90,
baseVertex: 11,
indexCount: 24,
new GpuTextureSlot(17),
textureLayer: 6,
CullMode.None,
DirectionalShadowCasterMaterial.AlphaCutout,
in leaves);
var inputStats = new DirectionalShadowPreparationStats(
SourceCasters: 2,
SourceMeshRefs: 3,
SourceParts: 3,
SourceBatches: 5,
PreparedInstances: 0,
PreparedOpaqueCommands: 0,
PreparedAlphaCutoutCommands: 0,
RejectedTransparentBatches: 2,
RejectedFadedParts: 1,
MissingMeshes: 0,
UnresolvedAlphaCutoutTextures: 0);
product.Complete(generation, 7, in inputStats);
Assert.Equal(3, product.Transforms.Length);
Assert.Equal(2, product.Commands.Length);
Assert.Equal(1, product.OpaqueCommandCount);
Assert.Equal(1, product.AlphaCutoutCommandCount);
Assert.Single(product.OpaqueCommands.ToArray());
Assert.Single(product.AlphaCutoutCommands.ToArray());
Assert.Single(product.OpaqueBatches.ToArray());
Assert.Single(product.AlphaCutoutBatches.ToArray());
Assert.Single(product.OpaqueRuns.ToArray());
Assert.Single(product.AlphaCutoutRuns.ToArray());
Assert.Equal(0, product.OpaqueRuns[0].StartCommand);
Assert.Equal(1, product.AlphaCutoutRuns[0].StartCommand);
Assert.Equal(2u, product.Commands[0].InstanceCount);
Assert.Equal(0u, product.Commands[0].BaseInstance);
Assert.Equal(1u, product.Commands[1].InstanceCount);
Assert.Equal(2u, product.Commands[1].BaseInstance);
Assert.Equal(DirectionalShadowCasterMaterial.Opaque, product.Batches[0].Material);
Assert.False(product.Batches[0].TextureSlot.IsAssigned);
Assert.Equal(DirectionalShadowCasterMaterial.AlphaCutout, product.Batches[1].Material);
Assert.Equal(17u, product.Batches[1].TextureSlot.Index);
Assert.Equal(6u, product.Batches[1].TextureLayer);
Assert.Contains(first, product.Transforms.ToArray());
Assert.Contains(second, product.Transforms.ToArray());
Assert.Equal(leaves, product.Transforms[2]);
Assert.Equal(3, product.Stats.PreparedInstances);
Assert.Equal(2, product.Stats.RejectedTransparentBatches);
Assert.Equal(1, product.Stats.RejectedFadedParts);
}
///
/// Campaign VM VM6 review fix round (A2): two instances of the IDENTICAL
/// mesh subset (same index range, texture, material, cull mode) — one a
/// procedural-scenery entity (top nibble 0x8), one a
/// LandblockStaticEntityIdAllocator entity (top nibble 0xC) sharing the
/// SAME GfxObj — classify to different FoliageWindClassification flags
/// (0x2 cutout vs 0x0) via the identical Classify call the world
/// receiver uses, and must land as two SEPARATE prepared caster
/// batches/commands rather than coalescing into one. This is the
/// caster-side half of "casters and receivers agree by construction":
/// DirectionalShadowDrawKey (the caster's sort/group key, unlike the
/// pre-fix receiver GroupKey) already includes FoliageFlags, so this
/// pins that it stays correct.
///
[Fact]
public void SameSubsetDifferentFoliageClassificationNeverCoalescesIntoOneCasterBatch()
{
const uint sceneryEntityId = 0x80010203u; // ProceduralSceneryIdAllocator
const uint landblockStaticEntityId = 0xC0010203u; // LandblockStaticEntityIdAllocator
uint sceneryFlags = FoliageWindClassification.Classify(
sceneryEntityId,
isExcluded: false,
TranslucencyKind.ClipMap,
meshHasCutoutSubset: true);
uint landblockStaticFlags = FoliageWindClassification.Classify(
landblockStaticEntityId,
isExcluded: false,
TranslucencyKind.ClipMap,
meshHasCutoutSubset: true);
Assert.Equal(FoliageWindClassification.CutoutFoliageFlag, sceneryFlags);
Assert.Equal(0u, landblockStaticFlags);
var product = new DirectionalShadowPreparedDraws();
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
Assert.True(product.TryBegin(generation, 1, estimatedInstances: 2));
Matrix4x4 sceneryTransform = Matrix4x4.CreateTranslation(1f, 2f, 3f);
Matrix4x4 landblockStaticTransform = Matrix4x4.CreateTranslation(9f, 8f, 7f);
GpuTextureSlot sharedTexture = new(42);
product.Add(
firstIndex: 100,
baseVertex: 5,
indexCount: 12,
sharedTexture,
textureLayer: 0,
CullMode.CounterClockwise,
DirectionalShadowCasterMaterial.AlphaCutout,
in sceneryTransform,
sceneryFlags);
product.Add(
firstIndex: 100,
baseVertex: 5,
indexCount: 12,
sharedTexture,
textureLayer: 0,
CullMode.CounterClockwise,
DirectionalShadowCasterMaterial.AlphaCutout,
in landblockStaticTransform,
landblockStaticFlags);
product.Complete(
generation,
1,
new DirectionalShadowPreparationStats(
SourceCasters: 2,
SourceMeshRefs: 2,
SourceParts: 2,
SourceBatches: 2,
PreparedInstances: 0,
PreparedOpaqueCommands: 0,
PreparedAlphaCutoutCommands: 0,
RejectedTransparentBatches: 0,
RejectedFadedParts: 0,
MissingMeshes: 0,
UnresolvedAlphaCutoutTextures: 0));
// Two distinct commands/batches, NOT one command with InstanceCount=2
// — the identical geometry/texture/material would have coalesced
// pre-fix, since only entity-level classification (which the caster
// key did not carry before A2) tells them apart.
Assert.Equal(2, product.Commands.Length);
Assert.Equal(2, product.Batches.Length);
Assert.All(product.Commands.ToArray(), command => Assert.Equal(1u, command.InstanceCount));
int sceneryIndex = product.Batches.ToArray()
.ToList()
.FindIndex(batch => batch.FoliageFlags == FoliageWindClassification.CutoutFoliageFlag);
int landblockStaticIndex = product.Batches.ToArray()
.ToList()
.FindIndex(batch => batch.FoliageFlags == 0u);
Assert.True(sceneryIndex >= 0, "expected one prepared batch carrying the cutout foliage flag");
Assert.True(landblockStaticIndex >= 0, "expected one prepared batch carrying zero foliage flags");
Assert.NotEqual(sceneryIndex, landblockStaticIndex);
Assert.Equal(
sceneryTransform,
product.Transforms[(int)product.Commands[sceneryIndex].BaseInstance]);
Assert.Equal(
landblockStaticTransform,
product.Transforms[(int)product.Commands[landblockStaticIndex].BaseInstance]);
}
[Fact]
public void SameCasterBuild_ReplaysWithoutReclassificationOrStorageGrowth()
{
var product = new DirectionalShadowPreparedDraws();
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(8);
Matrix4x4 exactMeshRefTransform = new(
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16);
Assert.True(product.TryBegin(generation, 20, estimatedInstances: 1));
product.Add(
1,
2,
3,
GpuTextureSlot.Unassigned,
0,
CullMode.Clockwise,
DirectionalShadowCasterMaterial.Opaque,
in exactMeshRefTransform);
DirectionalShadowPreparationStats stats = default;
product.Complete(generation, 20, in stats);
long retained = product.RetainedScratchBytes;
ulong buildSequence = product.BuildSequence;
Assert.False(product.TryBegin(generation, 20, estimatedInstances: 100));
Assert.Equal(buildSequence, product.BuildSequence);
Assert.Equal(retained, product.RetainedScratchBytes);
Assert.Equal(exactMeshRefTransform, product.Transforms[0]);
Assert.Single(product.Commands.ToArray());
}
[Fact]
public void StableTopology_ComposesSlimPoseWithoutMutatingCasterOrAllocating()
{
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(10);
Matrix4x4 originalRoot = Matrix4x4.CreateTranslation(1f, 2f, 3f);
Matrix4x4 originalPart = Matrix4x4.CreateTranslation(4f, 5f, 6f);
Matrix4x4 setupPart = Matrix4x4.CreateRotationX(0.25f)
* Matrix4x4.CreateTranslation(7f, 8f, 9f);
DirectionalShadowCaster[] casters =
[
new DirectionalShadowCaster(
Projection(originalRoot, originalPart),
DirectionalShadowCasterKind.LiveDynamic),
];
var product = new DirectionalShadowPreparedDraws();
Assert.True(product.TryBegin(
generation,
casterBuildSequence: 12,
estimatedInstances: 2,
renderDataAvailabilityVersion: 5,
translucencyFadeRevision: 7));
product.MapCasterIdentity(
0,
casters[0].Projection.Id,
casters[0].Projection.ProjectionClass);
DirectionalShadowTransformSource direct =
DirectionalShadowTransformSource.Dynamic(
casterIndex: 0,
meshIndex: 0,
isSetupPart: false,
in setupPart);
DirectionalShadowTransformSource setup =
DirectionalShadowTransformSource.Dynamic(
casterIndex: 0,
meshIndex: 0,
isSetupPart: true,
in setupPart);
Matrix4x4 originalDirect = originalPart * originalRoot;
Matrix4x4 originalSetup = setupPart * originalPart * originalRoot;
product.Add(
10,
0,
3,
GpuTextureSlot.Unassigned,
0,
CullMode.Clockwise,
DirectionalShadowCasterMaterial.Opaque,
in originalDirect,
in direct);
product.Add(
20,
0,
3,
GpuTextureSlot.Unassigned,
0,
CullMode.Clockwise,
DirectionalShadowCasterMaterial.Opaque,
in originalSetup,
in setup);
DirectionalShadowPreparationStats stats = default;
product.Complete(
generation,
12,
in stats,
renderDataAvailabilityVersion: 5,
translucencyFadeRevision: 7);
ulong topologyBuild = product.BuildSequence;
float exactRootX = BitConverter.Int32BitsToSingle(0x41234567);
float exactPartY = BitConverter.Int32BitsToSingle(0x40ABCDEF);
Matrix4x4 currentRoot = Matrix4x4.CreateRotationZ(0.15f)
* Matrix4x4.CreateTranslation(exactRootX, 12f, 13f);
Matrix4x4 currentPart = Matrix4x4.CreateRotationY(0.35f)
* Matrix4x4.CreateTranslation(14f, exactPartY, 16f);
RenderProjectionRecord current = Projection(currentRoot, currentPart);
DirectionalShadowTransformSnapshot snapshot =
DirectionalShadowTransformSnapshot.Capture(in current);
DirectionalShadowChangedPose[] changed = [new(0, snapshot)];
product.RefreshDynamicTransforms(changed);
AssertMatrixBitsEqual(currentPart * currentRoot, product.Transforms[0]);
AssertMatrixBitsEqual(
setupPart * currentPart * currentRoot,
product.Transforms[1]);
Assert.Equal(topologyBuild, product.BuildSequence);
Assert.Equal(2, product.LastDynamicTransformRefreshCount);
Assert.False(product.RequiresTopologyBuild(generation, 12, 5, 7));
AssertMatrixBitsEqual(
originalRoot,
casters[0].Projection.Transform.LocalToWorld);
AssertMatrixBitsEqual(
originalPart,
casters[0].Projection.EntityPayload.MeshRefs[0].PartTransform);
product.RefreshDynamicTransforms(
ReadOnlySpan.Empty);
Assert.Empty(product.DynamicTransformSlots.ToArray());
Assert.Equal(0, product.LastDynamicTransformRefreshCount);
AssertMatrixBitsEqual(currentPart * currentRoot, product.Transforms[0]);
AssertMatrixBitsEqual(
setupPart * currentPart * currentRoot,
product.Transforms[1]);
product.RefreshDynamicTransforms(changed, denseRefresh: true);
Assert.True(product.LastDynamicTransformRefreshWasDense);
Assert.Equal([0, 1], product.DynamicTransformSlots.ToArray());
product.RefreshDynamicTransforms(changed);
long before = GC.GetAllocatedBytesForCurrentThread();
for (int iteration = 0; iteration < 256; iteration++)
product.RefreshDynamicTransforms(changed);
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(0, allocated);
RenderProjectionRecord wrongId = current with
{
Id = RenderProjectionId.FromRaw(99),
};
DirectionalShadowTransformSnapshot wrongSnapshot =
DirectionalShadowTransformSnapshot.Capture(in wrongId);
DirectionalShadowChangedPose[] wrongIdentity = [new(0, wrongSnapshot)];
InvalidOperationException identityFailure = Assert.Throws<
InvalidOperationException>(
() => product.RefreshDynamicTransforms(wrongIdentity));
Assert.Contains("does not match", identityFailure.Message);
DirectionalShadowChangedPose[] staleSlot = [new(1, snapshot)];
InvalidOperationException slotFailure = Assert.Throws<
InvalidOperationException>(
() => product.RefreshDynamicTransforms(staleSlot));
Assert.Contains("stale or unmapped caster index", slotFailure.Message);
}
[Fact]
public void StableTopology_MapsChangedCasterToOnlyItsRetainedTransformSlots()
{
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(12);
Matrix4x4 firstRoot = Matrix4x4.CreateTranslation(1f, 2f, 3f);
Matrix4x4 secondRoot = Matrix4x4.CreateTranslation(4f, 5f, 6f);
Matrix4x4 firstPart = Matrix4x4.CreateTranslation(7f, 8f, 9f);
Matrix4x4 secondPart = Matrix4x4.CreateTranslation(10f, 11f, 12f);
DirectionalShadowCaster[] casters =
[
new DirectionalShadowCaster(
Projection(firstRoot, firstPart),
DirectionalShadowCasterKind.LiveDynamic),
new DirectionalShadowCaster(
Projection(secondRoot, secondPart) with
{
Id = RenderProjectionId.FromRaw(2),
},
DirectionalShadowCasterKind.EquippedChild),
];
var product = new DirectionalShadowPreparedDraws();
Assert.True(product.TryBegin(generation, 13, estimatedInstances: 2));
product.MapCasterIdentity(
0,
casters[0].Projection.Id,
casters[0].Projection.ProjectionClass);
product.MapCasterIdentity(
1,
casters[1].Projection.Id,
casters[1].Projection.ProjectionClass);
Matrix4x4 setupPart = Matrix4x4.Identity;
DirectionalShadowTransformSource firstSource =
DirectionalShadowTransformSource.Dynamic(
0,
0,
false,
in setupPart);
DirectionalShadowTransformSource secondSource =
DirectionalShadowTransformSource.Dynamic(
1,
0,
false,
in setupPart);
Matrix4x4 firstWorld = firstPart * firstRoot;
Matrix4x4 secondWorld = secondPart * secondRoot;
product.Add(
1, 0, 3, GpuTextureSlot.Unassigned, 0,
CullMode.Clockwise,
DirectionalShadowCasterMaterial.Opaque,
in firstWorld,
in firstSource);
product.Add(
2, 0, 3, GpuTextureSlot.Unassigned, 0,
CullMode.Clockwise,
DirectionalShadowCasterMaterial.Opaque,
in secondWorld,
in secondSource);
DirectionalShadowPreparationStats stats = default;
product.Complete(generation, 13, in stats);
Matrix4x4 movedRoot = Matrix4x4.CreateTranslation(40f, 50f, 60f);
Matrix4x4 movedPart = Matrix4x4.CreateTranslation(70f, 80f, 90f);
casters[1] = casters[1] with
{
Projection = Projection(movedRoot, movedPart) with
{
Id = RenderProjectionId.FromRaw(2),
},
};
product.RefreshDynamicTransforms(casters, [1]);
Assert.Equal([0, 1], product.AllDynamicTransformSlots.ToArray());
Assert.Equal([1], product.DynamicTransformSlots.ToArray());
AssertMatrixBitsEqual(firstWorld, product.Transforms[0]);
AssertMatrixBitsEqual(movedPart * movedRoot, product.Transforms[1]);
Assert.Equal(1, product.LastDynamicTransformRefreshCount);
product.RefreshDenseDynamicTransforms(casters);
Assert.True(product.LastDynamicTransformRefreshWasDense);
Assert.Equal([0, 1], product.DynamicTransformSlots.ToArray());
Assert.Equal(2, product.LastDynamicTransformRefreshCount);
AssertMatrixBitsEqual(firstWorld, product.Transforms[0]);
AssertMatrixBitsEqual(movedPart * movedRoot, product.Transforms[1]);
Matrix4x4 denseFirstRoot = Matrix4x4.CreateTranslation(100f, 101f, 102f);
Matrix4x4 denseFirstPart = Matrix4x4.CreateTranslation(103f, 104f, 105f);
casters[0] = casters[0] with
{
Projection = Projection(denseFirstRoot, denseFirstPart),
};
RenderProjectionRecord firstProjection = casters[0].Projection;
RenderProjectionRecord secondProjection = casters[1].Projection;
DirectionalShadowTransformSnapshot firstPose =
DirectionalShadowTransformSnapshot.Capture(in firstProjection);
DirectionalShadowTransformSnapshot secondPose =
DirectionalShadowTransformSnapshot.Capture(in secondProjection);
DirectionalShadowChangedPose[] reversedDenseChanges =
[
new(1, secondPose),
new(0, firstPose),
];
product.RefreshDynamicTransforms(reversedDenseChanges, denseRefresh: true);
Assert.True(product.LastDynamicTransformRefreshWasDense);
Assert.Equal([0, 1], product.DynamicTransformSlots.ToArray());
Assert.Equal(2, product.LastDynamicTransformRefreshCount);
AssertMatrixBitsEqual(
denseFirstPart * denseFirstRoot,
product.Transforms[0]);
AssertMatrixBitsEqual(movedPart * movedRoot, product.Transforms[1]);
long beforeDense = GC.GetAllocatedBytesForCurrentThread();
for (int iteration = 0; iteration < 256; iteration++)
{
product.RefreshDynamicTransforms(
reversedDenseChanges,
denseRefresh: true);
}
long denseAllocated =
GC.GetAllocatedBytesForCurrentThread() - beforeDense;
Assert.Equal(0, denseAllocated);
Assert.True(product.TryBegin(generation, 14, estimatedInstances: 0));
product.Complete(generation, 14, in stats);
product.RefreshDynamicTransforms(casters, [0]);
Assert.Empty(product.DynamicTransformSlots.ToArray());
Assert.Equal(0, product.LastDynamicTransformRefreshCount);
}
[Fact]
public void ResourceAvailabilityFadeAndPendingTextureInvalidateRetainedTopology()
{
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(11);
var product = new DirectionalShadowPreparedDraws();
Assert.True(product.TryBegin(generation, 2, 0, 20, 30));
var pending = new DirectionalShadowPreparationStats(
SourceCasters: 1,
SourceMeshRefs: 1,
SourceParts: 1,
SourceBatches: 1,
PreparedInstances: 0,
PreparedOpaqueCommands: 0,
PreparedAlphaCutoutCommands: 0,
RejectedTransparentBatches: 0,
RejectedFadedParts: 0,
MissingMeshes: 1,
UnresolvedAlphaCutoutTextures: 0);
product.Complete(generation, 2, in pending, 20, 30);
Assert.False(product.RequiresTopologyBuild(generation, 2, 20, 30));
Assert.True(product.RequiresTopologyBuild(generation, 2, 21, 30));
Assert.True(product.RequiresTopologyBuild(generation, 2, 20, 31));
Assert.True(product.TryBegin(generation, 2, 0, 21, 30));
pending = pending with { UnresolvedAlphaCutoutTextures = 1 };
product.Complete(generation, 2, in pending, 21, 30);
Assert.True(product.RequiresTopologyBuild(generation, 2, 21, 30));
}
[Fact]
public void SetupComposition_UsesPublishedMeshRefTransformWithoutAnotherPose()
{
Matrix4x4 root = Matrix4x4.CreateRotationZ(0.1f)
* Matrix4x4.CreateTranslation(50f, 60f, 70f);
Matrix4x4 currentMeshRef = Matrix4x4.CreateRotationY(0.2f)
* Matrix4x4.CreateTranslation(4f, 5f, 6f);
Matrix4x4 authoredSetupPart = Matrix4x4.CreateRotationX(0.3f)
* Matrix4x4.CreateTranslation(1f, 2f, 3f);
Matrix4x4 actual = WbDrawDispatcher.ComposePartWorldMatrix(
root,
currentMeshRef,
authoredSetupPart);
Assert.Equal(authoredSetupPart * currentMeshRef * root, actual);
}
private static RenderProjectionRecord Projection(
in Matrix4x4 root,
in Matrix4x4 part) =>
new RenderProjectionRecord() with
{
Id = RenderProjectionId.FromRaw(1),
ProjectionClass = RenderProjectionClass.LiveDynamicRoot,
Transform = new RenderTransform(root),
EntityPayload = new RenderEntityPayload(
[new MeshRef(0x01000001, part)],
PaletteOverride: null,
IsBuildingShell: false),
};
private static void AssertMatrixBitsEqual(
Matrix4x4 expected,
Matrix4x4 actual)
{
ReadOnlySpan expectedBits = MemoryMarshal.AsBytes(
MemoryMarshal.CreateReadOnlySpan(ref expected, 1));
ReadOnlySpan actualBits = MemoryMarshal.AsBytes(
MemoryMarshal.CreateReadOnlySpan(ref actual, 1));
Assert.True(expectedBits.SequenceEqual(actualBits));
}
}