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) { } } }