feat(rendering): contain incremental render scene
Pin Arch behind acdream-owned identities, deltas, queries, generation gates, and memory diagnostics. The new five-archetype store is unbound and non-drawing so static publication can be shadowed without changing production behavior. Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
This commit is contained in:
parent
f9b68f8f2a
commit
dbd8318417
7 changed files with 1698 additions and 2 deletions
405
tests/AcDream.App.Tests/Rendering/ArchRenderSceneTests.cs
Normal file
405
tests/AcDream.App.Tests/Rendering/ArchRenderSceneTests.cs
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Scene.Arch;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class ArchRenderSceneTests
|
||||
{
|
||||
[Fact]
|
||||
public void Apply_RegisterChannelUpdatesAndUnregister_PreservesTypedRecord()
|
||||
{
|
||||
RenderSceneGeneration generation = Generation(1);
|
||||
using var scene = new ArchRenderScene(generation);
|
||||
RenderProjectionRecord original = Record(
|
||||
id: 10,
|
||||
incarnation: 3,
|
||||
RenderProjectionClass.LiveDynamicRoot,
|
||||
x: 1,
|
||||
bucket: 0xAABB);
|
||||
|
||||
RenderDeltaApplyResult register = scene.Apply(
|
||||
[RenderProjectionDelta.Register(generation, 1, original)]);
|
||||
|
||||
Assert.Equal(1, register.Applied);
|
||||
Assert.Equal(1, register.Registered);
|
||||
Assert.Equal(
|
||||
new RenderProjectionCounts(1, 0, 0, 1, 0, 0),
|
||||
scene.Counts);
|
||||
|
||||
RenderProjectionRecord transformed = original with
|
||||
{
|
||||
PreviousTransform = new PreviousRenderTransform(
|
||||
original.Transform.LocalToWorld),
|
||||
Transform = new RenderTransform(
|
||||
Matrix4x4.CreateTranslation(4, 5, 6)),
|
||||
Bounds = new RenderWorldBounds(
|
||||
new Vector3(3, 4, 5),
|
||||
new Vector3(5, 6, 7)),
|
||||
SortKey = new RenderSortKey(99),
|
||||
};
|
||||
RenderProjectionRecord appearance = transformed with
|
||||
{
|
||||
MeshSet = new RenderMeshSet(Asset(80), 4, 7),
|
||||
Material = new RenderMaterialVariant(12, 13, 0.5f),
|
||||
DegradeState = new RenderDegradeState(2, 8),
|
||||
};
|
||||
RenderProjectionRecord flags = appearance with
|
||||
{
|
||||
Flags = RenderProjectionFlags.Draw
|
||||
| RenderProjectionFlags.Translucent,
|
||||
};
|
||||
RenderProjectionRecord rebucketed = flags with
|
||||
{
|
||||
Residency = new RenderSpatialResidency(
|
||||
Bucket(0xCCDD),
|
||||
0xCCDD0000,
|
||||
0xCCDD0102),
|
||||
};
|
||||
|
||||
RenderDeltaApplyResult updates = scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Update(
|
||||
RenderProjectionDeltaKind.UpdateTransform,
|
||||
generation,
|
||||
2,
|
||||
transformed),
|
||||
RenderProjectionDelta.Update(
|
||||
RenderProjectionDeltaKind.UpdateAppearance,
|
||||
generation,
|
||||
3,
|
||||
appearance),
|
||||
RenderProjectionDelta.Update(
|
||||
RenderProjectionDeltaKind.UpdateFlags,
|
||||
generation,
|
||||
4,
|
||||
flags),
|
||||
RenderProjectionDelta.Update(
|
||||
RenderProjectionDeltaKind.Rebucket,
|
||||
generation,
|
||||
5,
|
||||
rebucketed),
|
||||
]);
|
||||
|
||||
Assert.Equal(4, updates.Applied);
|
||||
Assert.Equal(4, updates.Updated);
|
||||
Assert.True(scene.OpenQuery().TryGet(original.Id, out var stored));
|
||||
Assert.Equal(rebucketed.Transform, stored.Transform);
|
||||
Assert.Equal(rebucketed.Bounds, stored.Bounds);
|
||||
Assert.Equal(rebucketed.MeshSet, stored.MeshSet);
|
||||
Assert.Equal(rebucketed.Material, stored.Material);
|
||||
Assert.Equal(rebucketed.Flags, stored.Flags);
|
||||
Assert.Equal(rebucketed.Residency, stored.Residency);
|
||||
Assert.Equal(
|
||||
RenderDirtyMask.All,
|
||||
stored.DirtyMask);
|
||||
|
||||
RenderDeltaApplyResult unregister = scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Unregister(
|
||||
generation,
|
||||
6,
|
||||
original.Id,
|
||||
original.OwnerIncarnation),
|
||||
]);
|
||||
|
||||
Assert.Equal(1, unregister.Applied);
|
||||
Assert.Equal(1, unregister.Unregistered);
|
||||
Assert.Equal(default, scene.Counts);
|
||||
Assert.False(scene.OpenQuery().TryGet(original.Id, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_NewerIncarnationReplacesAndOldCallbacksCannotRemoveIt()
|
||||
{
|
||||
RenderSceneGeneration generation = Generation(4);
|
||||
using var scene = new ArchRenderScene(generation);
|
||||
RenderProjectionRecord first = Record(
|
||||
44,
|
||||
10,
|
||||
RenderProjectionClass.OutdoorStatic);
|
||||
RenderProjectionRecord replacement = Record(
|
||||
44,
|
||||
11,
|
||||
RenderProjectionClass.ActiveAnimatedStatic,
|
||||
x: 9);
|
||||
|
||||
scene.Apply([RenderProjectionDelta.Register(generation, 1, first)]);
|
||||
RenderDeltaApplyResult replaced = scene.Apply(
|
||||
[RenderProjectionDelta.Register(generation, 2, replacement)]);
|
||||
RenderDeltaApplyResult staleRegister = scene.Apply(
|
||||
[RenderProjectionDelta.Register(generation, 3, first)]);
|
||||
RenderDeltaApplyResult staleDelete = scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Unregister(
|
||||
generation,
|
||||
4,
|
||||
first.Id,
|
||||
first.OwnerIncarnation),
|
||||
]);
|
||||
|
||||
Assert.Equal(1, replaced.Replaced);
|
||||
Assert.Equal(1, replaced.Registered);
|
||||
Assert.Equal(1, staleRegister.RejectedStaleIncarnation);
|
||||
Assert.Equal(1, staleDelete.RejectedStaleIncarnation);
|
||||
Assert.Equal(
|
||||
new RenderProjectionCounts(1, 0, 0, 0, 1, 0),
|
||||
scene.Counts);
|
||||
Assert.True(scene.OpenQuery().TryGet(first.Id, out var current));
|
||||
Assert.Equal(replacement.OwnerIncarnation, current.OwnerIncarnation);
|
||||
Assert.Equal(replacement.Transform, current.Transform);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_RejectsWrongGenerationOutOfOrderAndMissingDeltas()
|
||||
{
|
||||
RenderSceneGeneration generation = Generation(7);
|
||||
using var scene = new ArchRenderScene(generation);
|
||||
RenderProjectionRecord record = Record(
|
||||
70,
|
||||
1,
|
||||
RenderProjectionClass.IndoorCellStatic);
|
||||
|
||||
RenderDeltaApplyResult result = scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(Generation(6), 1, record),
|
||||
RenderProjectionDelta.Register(generation, 2, record),
|
||||
RenderProjectionDelta.Update(
|
||||
RenderProjectionDeltaKind.UpdateFlags,
|
||||
generation,
|
||||
2,
|
||||
record),
|
||||
RenderProjectionDelta.Unregister(
|
||||
generation,
|
||||
3,
|
||||
Projection(999),
|
||||
Incarnation(1)),
|
||||
]);
|
||||
|
||||
Assert.Equal(1, result.Applied);
|
||||
Assert.Equal(1, result.RejectedGeneration);
|
||||
Assert.Equal(1, result.RejectedOutOfOrderSequence);
|
||||
Assert.Equal(1, result.RejectedMissing);
|
||||
Assert.Equal(1, scene.Counts.Total);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_AdvancesGenerationReclaimsRecordsAndInvalidatesBorrowedQuery()
|
||||
{
|
||||
RenderSceneGeneration firstGeneration = Generation(1);
|
||||
using var scene = new ArchRenderScene(firstGeneration);
|
||||
RenderProjectionRecord record = Record(
|
||||
1,
|
||||
1,
|
||||
RenderProjectionClass.EquippedChild);
|
||||
scene.Apply(
|
||||
[RenderProjectionDelta.Register(firstGeneration, 1, record)]);
|
||||
RenderSceneQuery borrowed = scene.OpenQuery();
|
||||
|
||||
scene.Clear(Generation(2));
|
||||
|
||||
Assert.Equal(Generation(2), scene.Generation);
|
||||
Assert.Equal(default, scene.Counts);
|
||||
Assert.Equal(0, scene.Memory.EntityCount);
|
||||
Assert.Throws<InvalidOperationException>(() => _ = borrowed.Counts);
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => scene.Clear(Generation(2)));
|
||||
|
||||
RenderProjectionRecord next = record with
|
||||
{
|
||||
OwnerIncarnation = Incarnation(2),
|
||||
};
|
||||
RenderDeltaApplyResult applied = scene.Apply(
|
||||
[RenderProjectionDelta.Register(Generation(2), 1, next)]);
|
||||
Assert.Equal(1, applied.Registered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiveProjectionClasses_UseNarrowArchetypesAndReportRetainedMemory()
|
||||
{
|
||||
RenderSceneGeneration generation = Generation(9);
|
||||
using var scene = new ArchRenderScene(generation);
|
||||
RenderProjectionClass[] classes =
|
||||
[
|
||||
RenderProjectionClass.OutdoorStatic,
|
||||
RenderProjectionClass.IndoorCellStatic,
|
||||
RenderProjectionClass.LiveDynamicRoot,
|
||||
RenderProjectionClass.ActiveAnimatedStatic,
|
||||
RenderProjectionClass.EquippedChild,
|
||||
];
|
||||
var deltas = new RenderProjectionDelta[classes.Length];
|
||||
for (int i = 0; i < classes.Length; i++)
|
||||
{
|
||||
RenderProjectionRecord record = Record(
|
||||
(ulong)i + 1,
|
||||
1,
|
||||
classes[i]);
|
||||
deltas[i] = RenderProjectionDelta.Register(
|
||||
generation,
|
||||
(ulong)i + 1,
|
||||
record);
|
||||
}
|
||||
|
||||
scene.Apply(deltas);
|
||||
RenderSceneMemoryAccounting memory = scene.Memory;
|
||||
|
||||
Assert.Equal(5, scene.Counts.Total);
|
||||
Assert.Equal(5, memory.EntityCount);
|
||||
Assert.Equal(5, memory.ArchetypeCount);
|
||||
Assert.Equal(5, memory.AllocatedChunkCount);
|
||||
Assert.True(memory.ArchEntityCapacity >= memory.EntityCount);
|
||||
Assert.True(memory.EstimatedChunkPayloadBytes > 0);
|
||||
Assert.True(memory.ProjectionLookupCapacity >= memory.EntityCount);
|
||||
Assert.True(memory.EstimatedProjectionLookupBytes > 0);
|
||||
Assert.Equal(
|
||||
memory.EstimatedChunkPayloadBytes
|
||||
+ memory.EstimatedProjectionLookupBytes,
|
||||
memory.TotalEstimatedBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DynamicSynchronization_UpdatesOnlyMatchingLiveIncarnation()
|
||||
{
|
||||
RenderSceneGeneration generation = Generation(10);
|
||||
using var scene = new ArchRenderScene(generation);
|
||||
RenderProjectionRecord record = Record(
|
||||
100,
|
||||
5,
|
||||
RenderProjectionClass.LiveDynamicRoot);
|
||||
scene.Apply([RenderProjectionDelta.Register(generation, 1, record)]);
|
||||
|
||||
DynamicProjectionUpdate stale = new(
|
||||
record.Id,
|
||||
Incarnation(4),
|
||||
new RenderTransform(Matrix4x4.CreateTranslation(100, 0, 0)),
|
||||
record.Bounds);
|
||||
DynamicProjectionUpdate current = new(
|
||||
record.Id,
|
||||
record.OwnerIncarnation,
|
||||
new RenderTransform(Matrix4x4.CreateTranslation(8, 9, 10)),
|
||||
new RenderWorldBounds(
|
||||
new Vector3(7, 8, 9),
|
||||
new Vector3(9, 10, 11)));
|
||||
scene.SynchronizeDynamicSources(
|
||||
new DynamicProjectionSyncInput(generation, [stale, current]));
|
||||
|
||||
Assert.True(scene.OpenQuery().TryGet(record.Id, out var stored));
|
||||
Assert.Equal(record.Transform.LocalToWorld, stored.PreviousTransform.LocalToWorld);
|
||||
Assert.Equal(current.Transform, stored.Transform);
|
||||
Assert.Equal(current.Bounds, stored.Bounds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Digest_IsStableAcrossRegistrationOrderAndBufferReuse()
|
||||
{
|
||||
RenderSceneGeneration generation = Generation(12);
|
||||
using var first = new ArchRenderScene(generation);
|
||||
using var second = new ArchRenderScene(generation);
|
||||
RenderProjectionRecord a = Record(
|
||||
1,
|
||||
1,
|
||||
RenderProjectionClass.OutdoorStatic);
|
||||
RenderProjectionRecord b = Record(
|
||||
2,
|
||||
1,
|
||||
RenderProjectionClass.LiveDynamicRoot);
|
||||
|
||||
first.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(generation, 1, a),
|
||||
RenderProjectionDelta.Register(generation, 2, b),
|
||||
]);
|
||||
second.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(generation, 1, b),
|
||||
RenderProjectionDelta.Register(generation, 2, a),
|
||||
]);
|
||||
var buffer = new RenderSceneDigestBuffer();
|
||||
|
||||
RenderSceneDigest firstDigest = first.BuildDigest(buffer);
|
||||
int retainedCapacity = buffer.Capacity;
|
||||
RenderSceneDigest repeatedDigest = first.BuildDigest(buffer);
|
||||
RenderSceneDigest secondDigest =
|
||||
second.BuildDigest(new RenderSceneDigestBuffer());
|
||||
|
||||
Assert.Equal(firstDigest, repeatedDigest);
|
||||
Assert.Equal(firstDigest, secondDigest);
|
||||
Assert.Equal(retainedCapacity, buffer.Capacity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mutation_FromAnotherThread_IsRejected()
|
||||
{
|
||||
RenderSceneGeneration generation = Generation(15);
|
||||
using var scene = new ArchRenderScene(generation);
|
||||
RenderProjectionRecord record = Record(
|
||||
15,
|
||||
1,
|
||||
RenderProjectionClass.LiveDynamicRoot);
|
||||
|
||||
Exception? captured = null;
|
||||
var mutationThread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
scene.Apply(
|
||||
[RenderProjectionDelta.Register(generation, 1, record)]);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
captured = error;
|
||||
}
|
||||
});
|
||||
mutationThread.Start();
|
||||
mutationThread.Join();
|
||||
|
||||
InvalidOperationException error =
|
||||
Assert.IsType<InvalidOperationException>(captured);
|
||||
Assert.Contains("owning update thread", error.Message);
|
||||
Assert.Equal(0, scene.Counts.Total);
|
||||
}
|
||||
|
||||
private static RenderProjectionRecord Record(
|
||||
ulong id,
|
||||
ulong incarnation,
|
||||
RenderProjectionClass projectionClass,
|
||||
float x = 0,
|
||||
ulong bucket = 1)
|
||||
{
|
||||
Matrix4x4 transform = Matrix4x4.CreateTranslation(x, 2, 3);
|
||||
return new RenderProjectionRecord(
|
||||
Projection(id),
|
||||
projectionClass,
|
||||
Incarnation(incarnation),
|
||||
new RenderTransform(transform),
|
||||
new PreviousRenderTransform(transform),
|
||||
new RenderMeshSet(Asset(id + 100), 2, 1),
|
||||
new RenderMaterialVariant(id + 200, id + 300, 1),
|
||||
new RenderSpatialResidency(
|
||||
Bucket(bucket),
|
||||
(uint)(bucket << 16),
|
||||
(uint)((bucket << 16) | 0x101)),
|
||||
new RenderWorldBounds(
|
||||
new Vector3(x - 1, 1, 2),
|
||||
new Vector3(x + 1, 3, 4)),
|
||||
RenderProjectionFlags.Draw | RenderProjectionFlags.Selectable,
|
||||
new RenderDegradeState(0, 1),
|
||||
new RenderSortKey(id + 400),
|
||||
RenderDirtyMask.All);
|
||||
}
|
||||
|
||||
private static RenderProjectionId Projection(ulong value) =>
|
||||
RenderProjectionId.FromRaw(value);
|
||||
|
||||
private static RenderOwnerIncarnation Incarnation(ulong value) =>
|
||||
RenderOwnerIncarnation.FromRaw(value);
|
||||
|
||||
private static RenderSceneGeneration Generation(ulong value) =>
|
||||
RenderSceneGeneration.FromRaw(value);
|
||||
|
||||
private static RenderSpatialBucket Bucket(ulong value) =>
|
||||
RenderSpatialBucket.FromRaw(value);
|
||||
|
||||
private static RenderAssetHandle Asset(ulong value) =>
|
||||
RenderAssetHandle.FromRaw(value);
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class RenderSceneArchitectureTests
|
||||
{
|
||||
[Fact]
|
||||
public void ArchDependency_IsPinnedOnlyInAppAndNamespacesStayContained()
|
||||
{
|
||||
string root = FindRepoRoot();
|
||||
string appRoot = Path.Combine(root, "src", "AcDream.App");
|
||||
string allowedRoot = Path.GetFullPath(
|
||||
Path.Combine(appRoot, "Rendering", "Scene", "Arch"));
|
||||
string[] appSources =
|
||||
Directory.GetFiles(appRoot, "*.cs", SearchOption.AllDirectories);
|
||||
|
||||
string[] namespaceViolations = appSources
|
||||
.Where(path => Regex.IsMatch(
|
||||
File.ReadAllText(path),
|
||||
@"(?:\busing\s+Arch(?:\.|;)|\bArch\.Core\b)"))
|
||||
.Where(path => !IsUnder(path, allowedRoot))
|
||||
.Select(Path.GetFullPath)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
string[] packageOwners = Directory
|
||||
.GetFiles(Path.Combine(root, "src"), "*.csproj", SearchOption.AllDirectories)
|
||||
.Where(HasDirectArchReference)
|
||||
.Select(Path.GetFullPath)
|
||||
.ToArray();
|
||||
|
||||
Assert.Empty(namespaceViolations);
|
||||
Assert.Equal(
|
||||
[Path.GetFullPath(Path.Combine(appRoot, "AcDream.App.csproj"))],
|
||||
packageOwners);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArchTypes_DoNotCrossAcdreamOwnedSceneBoundary()
|
||||
{
|
||||
Type[] appTypes = typeof(IRenderScene).Assembly.GetTypes();
|
||||
var violations = new List<string>();
|
||||
|
||||
foreach (Type type in appTypes)
|
||||
{
|
||||
if (type.Namespace?.StartsWith(
|
||||
"AcDream.App.Rendering.Scene.Arch",
|
||||
StringComparison.Ordinal)
|
||||
== true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (FieldInfo field in type.GetFields(
|
||||
BindingFlags.Instance
|
||||
| BindingFlags.Static
|
||||
| BindingFlags.Public
|
||||
| BindingFlags.NonPublic
|
||||
| BindingFlags.DeclaredOnly))
|
||||
{
|
||||
if (ContainsArchType(field.FieldType))
|
||||
violations.Add($"{type.FullName} field {field.Name}");
|
||||
}
|
||||
|
||||
foreach (PropertyInfo property in type.GetProperties(
|
||||
BindingFlags.Instance
|
||||
| BindingFlags.Static
|
||||
| BindingFlags.Public
|
||||
| BindingFlags.NonPublic
|
||||
| BindingFlags.DeclaredOnly))
|
||||
{
|
||||
if (ContainsArchType(property.PropertyType))
|
||||
violations.Add($"{type.FullName} property {property.Name}");
|
||||
}
|
||||
|
||||
foreach (MethodBase method in type
|
||||
.GetMethods(
|
||||
BindingFlags.Instance
|
||||
| BindingFlags.Static
|
||||
| BindingFlags.Public
|
||||
| BindingFlags.NonPublic
|
||||
| BindingFlags.DeclaredOnly)
|
||||
.Cast<MethodBase>()
|
||||
.Concat(type.GetConstructors(
|
||||
BindingFlags.Instance
|
||||
| BindingFlags.Public
|
||||
| BindingFlags.NonPublic)))
|
||||
{
|
||||
if (method.GetParameters()
|
||||
.Any(parameter => ContainsArchType(parameter.ParameterType)))
|
||||
{
|
||||
violations.Add($"{type.FullName} method {method.Name}");
|
||||
}
|
||||
|
||||
if (method is MethodInfo info
|
||||
&& ContainsArchType(info.ReturnType))
|
||||
{
|
||||
violations.Add($"{type.FullName} return {method.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Empty(violations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrimitiveIdentityExtraction_StaysInsideArchAdapter()
|
||||
{
|
||||
string root = FindRepoRoot();
|
||||
string appRoot = Path.Combine(root, "src", "AcDream.App");
|
||||
string allowedRoot = Path.GetFullPath(
|
||||
Path.Combine(appRoot, "Rendering", "Scene", "Arch"));
|
||||
string contracts = Path.GetFullPath(
|
||||
Path.Combine(
|
||||
appRoot,
|
||||
"Rendering",
|
||||
"Scene",
|
||||
"RenderSceneContracts.cs"));
|
||||
|
||||
string[] violations = Directory
|
||||
.GetFiles(appRoot, "*.cs", SearchOption.AllDirectories)
|
||||
.Where(path => Regex.IsMatch(
|
||||
File.ReadAllText(path),
|
||||
@"\.\s*RawValue\b"))
|
||||
.Where(path =>
|
||||
!IsUnder(path, allowedRoot)
|
||||
&& !Path.GetFullPath(path).Equals(
|
||||
contracts,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
.Select(Path.GetFullPath)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
Assert.Empty(violations);
|
||||
}
|
||||
|
||||
private static bool HasDirectArchReference(string projectPath)
|
||||
{
|
||||
XDocument project = XDocument.Load(projectPath);
|
||||
return project
|
||||
.Descendants("PackageReference")
|
||||
.Any(element => string.Equals(
|
||||
(string?)element.Attribute("Include"),
|
||||
"Arch",
|
||||
StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static bool ContainsArchType(Type type)
|
||||
{
|
||||
if (type.IsByRef || type.IsArray || type.IsPointer)
|
||||
return ContainsArchType(type.GetElementType()!);
|
||||
|
||||
if (type.Namespace?.StartsWith("Arch", StringComparison.Ordinal) == true)
|
||||
return true;
|
||||
|
||||
return type.IsGenericType
|
||||
&& type.GetGenericArguments().Any(ContainsArchType);
|
||||
}
|
||||
|
||||
private static bool IsUnder(string path, string root)
|
||||
{
|
||||
string relative = Path.GetRelativePath(root, Path.GetFullPath(path));
|
||||
return relative != ".."
|
||||
&& !relative.StartsWith(
|
||||
$"..{Path.DirectorySeparatorChar}",
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
string? dir = AppContext.BaseDirectory;
|
||||
while (dir is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(dir, "AcDream.slnx")))
|
||||
return dir;
|
||||
dir = Directory.GetParent(dir)?.FullName;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Could not locate AcDream.slnx.");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue