feat(render): implement Campaign AR and terrain fidelity

This commit is contained in:
Erik 2026-08-22 13:13:29 +02:00
parent 99cf26e00c
commit 7a5f96ede5
368 changed files with 50611 additions and 950 deletions

View file

@ -0,0 +1,171 @@
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Plugins;
/// <summary>
/// Pre-device render-pack discovery buffer. Registration only retains the
/// immutable declaration and lazy asset source; it deliberately never calls
/// <see cref="IRenderPackAssets.OpenRead"/> or touches the renderer. This lets
/// plugins register before the window/GPU exists without weakening the retail
/// no-op contract.
/// </summary>
internal sealed class BufferedRenderPackRegistry : IRenderPackRegistry, IDisposable
{
private readonly object _sync = new();
private readonly Dictionary<string, Registration> _registrations =
new(StringComparer.OrdinalIgnoreCase);
private long _revision;
private long _nextRegistrationId;
private bool _disposed;
/// <summary>
/// Monotonic catalog generation. Consumers use <see cref="Changed"/> to
/// invalidate their cached view and consume the new snapshot at a safe
/// frame/UI boundary; no renderer path polls the registry per frame.
/// </summary>
internal long Revision
{
get
{
lock (_sync)
return _revision;
}
}
internal event Action<long>? Changed;
internal IReadOnlyList<BufferedRenderPackRegistration> Snapshot()
{
lock (_sync)
{
ObjectDisposedException.ThrowIf(_disposed, this);
return _registrations.Values
.OrderBy(static value => value.Descriptor.Id, StringComparer.OrdinalIgnoreCase)
.Select(static value => new BufferedRenderPackRegistration(
value.Descriptor,
value.Assets,
value.RegistrationId))
.ToArray();
}
}
public IDisposable Register(
RenderPackDescriptor descriptor,
IRenderPackAssets assets)
{
ArgumentNullException.ThrowIfNull(descriptor);
ArgumentNullException.ThrowIfNull(assets);
Registration registration;
long revision;
lock (_sync)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_registrations.ContainsKey(descriptor.Id))
{
throw new InvalidOperationException(
$"A render pack with id '{descriptor.Id}' is already registered.");
}
registration = new Registration(
this,
descriptor,
assets,
checked(++_nextRegistrationId));
_registrations.Add(descriptor.Id, registration);
revision = checked(++_revision);
}
PublishChanged(revision);
return registration;
}
public void Dispose()
{
Registration[] registrations;
long? revision = null;
lock (_sync)
{
if (_disposed)
return;
_disposed = true;
registrations = _registrations.Values.ToArray();
_registrations.Clear();
if (registrations.Length != 0)
revision = checked(++_revision);
}
foreach (Registration registration in registrations)
registration.WithdrawFromOwner();
if (revision is { } changedRevision)
PublishChanged(changedRevision);
}
private void Withdraw(Registration registration)
{
long? revision = null;
lock (_sync)
{
if (_registrations.TryGetValue(
registration.Descriptor.Id,
out Registration? active)
&& ReferenceEquals(active, registration))
{
_registrations.Remove(registration.Descriptor.Id);
revision = checked(++_revision);
}
}
if (revision is { } changedRevision)
PublishChanged(changedRevision);
}
private void PublishChanged(long revision)
{
Delegate[] subscribers = Changed?.GetInvocationList() ?? [];
foreach (Delegate subscriber in subscribers)
{
try { ((Action<long>)subscriber)(revision); }
catch
{
// Registration ownership must not be corrupted by a UI or
// controller observer. The next explicit snapshot still sees
// the authoritative revision and contents.
}
}
}
private sealed class Registration : IDisposable
{
private BufferedRenderPackRegistry? _owner;
internal Registration(
BufferedRenderPackRegistry owner,
RenderPackDescriptor descriptor,
IRenderPackAssets assets,
long registrationId)
{
_owner = owner;
Descriptor = descriptor;
Assets = assets;
RegistrationId = registrationId;
}
internal RenderPackDescriptor Descriptor { get; }
internal IRenderPackAssets Assets { get; }
internal long RegistrationId { get; }
public void Dispose() =>
Interlocked.Exchange(ref _owner, null)?.Withdraw(this);
internal void WithdrawFromOwner() =>
Interlocked.Exchange(ref _owner, null);
}
}
internal sealed record BufferedRenderPackRegistration(
RenderPackDescriptor Descriptor,
IRenderPackAssets Assets,
long RegistrationId);

View file

@ -1,6 +1,7 @@
using AcDream.Core.Plugins;
using AcDream.Platform;
using AcDream.Plugin.Abstractions;
using AcDream.Plugin.Abstractions.Rendering;
using AcDream.Runtime.Session;
namespace AcDream.App.Plugins;
@ -44,7 +45,8 @@ internal sealed class GraphicalPluginSession : IDisposable
IReadOnlyList<string>? allowList,
string sessionId,
IPluginHost host,
SessionStatusWriter statusWriter)
SessionStatusWriter statusWriter,
IRenderPackRegistry? renderPacks = null)
{
ArgumentNullException.ThrowIfNull(paths);
ArgumentException.ThrowIfNullOrWhiteSpace(sessionId);
@ -53,7 +55,11 @@ internal sealed class GraphicalPluginSession : IDisposable
var plugins = new PluginSession(
host,
status => Report(statusWriter, sessionId, status));
status => Report(statusWriter, sessionId, status),
renderPacks,
renderPacks is null
? [PluginKind.Gameplay]
: [PluginKind.Gameplay, PluginKind.RenderPack]);
return new GraphicalPluginSession(
plugins,
[