using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Wb;
using AcDream.App.Tests.Rendering.Gpu;
using AcDream.Content;
using Chorizite.Core.Render.Enums;
using Microsoft.Extensions.Logging.Abstractions;
using CullMode = DatReaderWriter.Enums.CullMode;
namespace AcDream.App.Tests.Rendering;
///
/// S4-c2 final EnvCell production-dispatch pins: exact retained per-batch mask,
/// one token per (cell,list), separate list-filtered replay sources, immediate
/// detail turns, interleaving, and warmed allocation behavior.
///
public sealed class EnvCellAlphaDrawSourceTests
{
///
/// Canonical F4180104 surface 08000BFF's pure Base1ClipMap mask 0x08
/// reaches CLIP while alpha-family 0x02 reaches ALPHA. Mutation check:
/// hardcoding MaskAlphaFamily in the production batch classifier makes
/// the first assertion report Alpha instead of Clip.
[Fact]
public void RetainedPerBatchMask_RoutesClipAndAlphaExactly()
{
var clip = new ObjectRenderBatch { IsTransparent = true, RetailSurfaceMask = 0x08 };
var alpha = new ObjectRenderBatch { IsTransparent = true, RetailSurfaceMask = 0x02 };
Assert.Equal(
EnvCellTransparentRoute.Clip,
EnvCellRenderer.RouteTransparentBatch(clip, detailSurfaceActive: false));
Assert.Equal(
EnvCellTransparentRoute.Alpha,
EnvCellRenderer.RouteTransparentBatch(alpha, detailSurfaceActive: false));
}
///
/// A mixed cell creates one token in each list and neither set draws before
/// the real queue drain. CLIP drains first and receives only the CLIP
/// replay filter; ALPHA receives only ALPHA. Mutation checks: inverting the
/// detail-off immediate predicate makes Assert.Empty(log) fail;
/// removing the replay filter makes either route assertion fail.
[Fact]
public void MixedCell_DefersOneTokenPerList_AndReplayIsListFiltered()
{
using var fixture = new ProductionEnvCellFixture(
detailSurfaceActive: false,
0x08,
0x02);
EnvCellTransparentRoute routes = fixture.Renderer.GetTransparentRoutes(
ProductionEnvCellFixture.CellId,
detailSurfaceActive: false);
fixture.Queue.BeginFrame();
RetailPViewPassExecutor.DispatchTransparentCellShell(
ProductionEnvCellFixture.CellId,
routes,
detailSurfaceActive: false,
fixture.Queue,
fixture.ClipSource,
fixture.AlphaSource,
fixture.ImmediateSink);
Assert.Equal(EnvCellTransparentRoute.Clip | EnvCellTransparentRoute.Alpha, routes);
Assert.Empty(fixture.Device.Calls.OfType());
Assert.Equal(1, fixture.Queue.ClipCount);
Assert.Equal(1, fixture.Queue.AlphaCount);
fixture.Queue.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
GpuRecordedMultiDrawIndirect[] draws =
[.. fixture.Device.Calls.OfType()];
Assert.Equal(2, draws.Length);
Assert.All(draws, static draw => Assert.Equal(1u, draw.DrawCount));
fixture.Queue.EndFrame();
}
[Fact]
public void DetailOn_DrawsAtCellTurnWithDetail_AndQueuesNothing()
{
using var fixture = new ProductionEnvCellFixture(
detailSurfaceActive: true,
0x08);
EnvCellTransparentRoute routes = fixture.Renderer.GetTransparentRoutes(
ProductionEnvCellFixture.CellId,
detailSurfaceActive: true);
fixture.Queue.BeginFrame();
RetailPViewPassExecutor.DispatchTransparentCellShell(
ProductionEnvCellFixture.CellId,
routes,
detailSurfaceActive: true,
fixture.Queue,
fixture.ClipSource,
fixture.AlphaSource,
fixture.ImmediateSink);
Assert.Equal(EnvCellTransparentRoute.Immediate, routes);
Assert.Equal(1, fixture.ImmediateSink.DrawCount);
Assert.True(fixture.ImmediateSink.LastDetailSurfaceActive);
Assert.Equal(2, fixture.Device.Calls.OfType().Count());
Assert.Equal(0, fixture.Queue.PendingCount);
fixture.Queue.EndFrame();
Assert.Equal(1, fixture.ImmediateSink.DrawCount);
}
/// Two cell tokens from the SAME ,
/// with an unrelated source's entry appended between them, must still
/// produce TWO separate single-cell draw calls around the interposed
/// entry — 's "only adjacent
/// same-source entries batch" invariant (the queue never groups across
/// another entry, which is what keeps compositing order exact). Mutation
/// check: an implementation that draws every prepared cell id in one
/// call regardless of the requested (first, count) range would
/// print both cell ids together on EACH of the two
/// DrawPreparedAlphaBatch invocations instead of once each — the
/// exact sequence assertion below fails against that mutation.
[Fact]
public void ParticleAppendedBetweenTwoCellTokens_KeepsItsPositionInTheCombinedDrain()
{
var log = new List();
var clipSource = Source(EnvCellTransparentRoute.Clip, log);
var cellSource = Source(EnvCellTransparentRoute.Alpha, log);
var particleSource = new RecordingSource("particle", log);
var queue = new RetailAlphaQueue();
queue.BeginFrame();
RetailPViewPassExecutor.DispatchTransparentCellShell(
0x100u, EnvCellTransparentRoute.Alpha, false, queue,
clipSource, cellSource, (_, _, _) => throw new InvalidOperationException());
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, particleSource, 7, false));
RetailPViewPassExecutor.DispatchTransparentCellShell(
0x200u, EnvCellTransparentRoute.Alpha, false, queue,
clipSource, cellSource, (_, _, _) => throw new InvalidOperationException());
queue.EndFrame();
Assert.Equal(new[] { "Alpha:00000100", "particle:7", "Alpha:00000200" }, log);
}
[Fact]
public void ProductionDispatch_WarmedImmediateAndFilteredSourcesDoNotAllocate()
{
var sink = new NoAllocRouteSink();
var clipSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
sink.Render,
EnvCellTransparentRoute.Clip);
var alphaSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
sink.Render,
EnvCellTransparentRoute.Alpha);
RetailPViewPassExecutor.RenderImmediateEnvCellRoute immediate = sink.DrawImmediate;
var queue = new RetailAlphaQueue();
for (int i = 0; i < 64; i++)
RunMixed(queue, clipSource, alphaSource, immediate);
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 1_000; i++)
RunMixed(queue, clipSource, alphaSource, immediate);
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(0, allocated);
}
private static void RunMixed(
RetailAlphaQueue queue,
RetailPViewPassExecutor.EnvCellAlphaDrawSource clipSource,
RetailPViewPassExecutor.EnvCellAlphaDrawSource alphaSource,
RetailPViewPassExecutor.RenderImmediateEnvCellRoute immediate)
{
queue.BeginFrame();
RetailPViewPassExecutor.DispatchTransparentCellShell(
0xF4180104u,
EnvCellTransparentRoute.Immediate
| EnvCellTransparentRoute.Clip
| EnvCellTransparentRoute.Alpha,
detailSurfaceActive: false,
queue,
clipSource,
alphaSource,
immediate);
queue.EndFrame();
}
private static RetailPViewPassExecutor.EnvCellAlphaDrawSource Source(
EnvCellTransparentRoute expectedRoute,
List log) =>
new(
(cells, route, detail) =>
{
Assert.Equal(expectedRoute, route);
Assert.False(detail);
for (int i = 0; i < cells.Count; i++)
log.Add($"{route}:{cells[i]:X8}");
},
expectedRoute);
///
/// Recording-GPU fixture for the production EnvCell scan, dispatch and
/// filtered replay path. The two retained masks enter through the real
/// Content upload boundary, not through hand-built App batches.
///
private sealed class ProductionEnvCellFixture : IDisposable
{
public const uint CellId = 0xF4180104u;
private const ulong MeshId = 0x2_F4180104UL;
private readonly GpuDeviceFrameLifetime _frames;
private readonly VulkanWorldPassScope _scope;
private readonly ObjectMeshManager _meshManager;
private readonly IGpuPassEncoder _pass;
private readonly IDisposable _publication;
public ProductionEnvCellFixture(
bool detailSurfaceActive,
params byte[] retainedMasks)
{
Device = new RecordingGpuDevice();
_frames = new GpuDeviceFrameLifetime(Device);
_scope = new VulkanWorldPassScope(sampleCount: 1);
_meshManager = new ObjectMeshManager(
new VulkanMeshPipelineDevice(Device.Retirement),
Device,
new NullPreparedAssetSource(),
NullLogger.Instance);
var mesh = new ObjectMeshData
{
ObjectId = MeshId,
Vertices =
[
new VertexPositionNormalTexture { Position = new Vector3(0, 0, 0) },
new VertexPositionNormalTexture { Position = new Vector3(1, 0, 0) },
new VertexPositionNormalTexture { Position = new Vector3(0, 1, 0) },
],
};
var batches = new List(retainedMasks.Length);
for (int i = 0; i < retainedMasks.Length; i++)
{
batches.Add(new TextureBatchData
{
Key = new TextureKey { SurfaceId = 0x08000BFFu + (uint)i },
TextureData = new byte[8 * 8 * 4],
Indices = [0, 1, 2],
IsTransparent = true,
Translucency = AcDream.Core.Meshing.TranslucencyKind.AlphaBlend,
RetailSurfaceMask = retainedMasks[i],
CullMode = CullMode.Clockwise,
IsCellShell = true,
SourceSurfaceIndex = i,
});
}
mesh.TextureBatches[(8, 8, TextureFormat.RGBA8)] = batches;
Assert.NotNull(_meshManager.UploadMeshData(mesh));
Renderer = new EnvCellRenderer(
Device,
_frames,
_scope,
_meshManager,
new WbFrustum(),
detailSurfaceActive
? new TerrainAtlas.RetailDetailTextureBinding(
new GpuTextureSlot(99),
Tiling: 2f,
SurfaceTextureId: 1,
RenderSurfaceId: 2,
Width: 4,
Height: 4)
: default,
detailSurfaceActive ? DetailOn : DetailOff);
typeof(EnvCellRenderer)
.GetField("_activeSnapshot", System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.Instance)!
.SetValue(Renderer, new EnvCellVisibilitySnapshot
{
BatchedByCell = new Dictionary>>
{
[CellId] = new Dictionary>
{
[MeshId] =
[
new InstanceData
{
Transform = Matrix4x4.Identity,
CellId = CellId,
},
],
},
},
});
Queue = new RetailAlphaQueue();
ClipSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
Renderer.RenderTransparentOrdered,
EnvCellTransparentRoute.Clip);
AlphaSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
Renderer.RenderTransparentOrdered,
EnvCellTransparentRoute.Alpha);
ImmediateSink = new ProductionImmediateSink(Renderer);
_frames.BeginFrame();
IGpuFrame frame = _frames.CurrentFrame!;
_pass = frame.BeginPass(
GpuPassDescription.BackbufferClear(
"s4-c2-envcell-production",
Vector4.Zero,
sampleCount: 1));
_publication = _scope.Publish(_pass);
Renderer.BeginFrame(frameSlot: 0);
Device.Clear();
}
public RecordingGpuDevice Device { get; }
public EnvCellRenderer Renderer { get; }
public RetailAlphaQueue Queue { get; }
public RetailPViewPassExecutor.EnvCellAlphaDrawSource ClipSource { get; }
public RetailPViewPassExecutor.EnvCellAlphaDrawSource AlphaSource { get; }
public ProductionImmediateSink ImmediateSink { get; }
private static bool DetailOn() => true;
private static bool DetailOff() => false;
public void Dispose()
{
_publication.Dispose();
_pass.Dispose();
Renderer.Dispose();
_meshManager.Dispose();
Device.Dispose();
}
}
private sealed class ProductionImmediateSink(EnvCellRenderer renderer) :
IEnvCellImmediateDrawSink
{
public int DrawCount { get; private set; }
public bool LastDetailSurfaceActive { get; private set; }
public void DrawImmediate(
uint cellId,
EnvCellTransparentRoute route,
bool detailSurfaceActive)
{
DrawCount++;
LastDetailSurfaceActive = detailSurfaceActive;
renderer.RenderTransparentOrdered([cellId], route, detailSurfaceActive);
}
}
private sealed class NullPreparedAssetSource : IPreparedAssetSource
{
public PreparedAssetSourceStats Stats => default;
public CacheStats DecodedTextureCacheStats => default;
public PreparedAssetPresence Probe(
AcDream.Content.Pak.PakAssetType type,
uint sourceFileId) =>
PreparedAssetPresence.Missing;
public PreparedAssetReadResult Read(
in PreparedAssetRequest request,
CancellationToken cancellationToken = default) =>
PreparedAssetReadResult.Missing;
public void Dispose()
{
}
}
private sealed class RecordingSource(string name, List log) : IRetailAlphaDrawSource
{
private int[] _prepared = [];
public void PrepareAlphaDraws(ReadOnlySpan tokens) => _prepared = tokens.ToArray();
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
{
for (int i = 0; i < drawCount; i++)
log.Add($"{name}:{_prepared[firstPreparedDraw + i]}");
}
public void ResetAlphaSubmissions()
{
}
}
private sealed class NoAllocRouteSink
{
public void Render(
IReadOnlyList cells,
EnvCellTransparentRoute route,
bool detailSurfaceActive)
{
}
public void DrawImmediate(
uint cellId,
EnvCellTransparentRoute route,
bool detailSurfaceActive)
{
}
}
}