Replace the building and EnvCell detail replay with retail's exact single-pass stage result, including authored surface opacity, squared detail alpha, final-alpha clipping, and the original subset pipeline/order. Arm the ordered walk command in place to close #471, delete the replay pipelines/shaders, and advance prepared content to recipe 9. Mutation witnesses (each restored before commit): - X=a*qA: RetailDetailTextureContractTests.BothShaderFamiliesUseTheSharedOnePassSourceAndDebugPrecedesDetailSample line 174, missing materialAlpha * detail.a * detail.a. - X*=base alpha: same test line 175, forbidden baseTexel.a found. - CLIP against base alpha: EnvCellAlphaDrawSourceTests.ClipShaders_UseGreaterEqualForThePerRangeReference line 260, final-X conditional missing. - second detail draw: EnvCellAlphaDrawSourceTests.DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity line 105, Assert.Single saw 2 MDI calls. - straight-alpha substitution: WalkStaticStreamPopulatorTests.ImmediateBuildingDetail_RetainsOriginalFramebufferFamily line 1244, Additive first failed (only wb-mesh-alpha-1x recorded; InvAlpha also failed). - omit ordered arm: OrderPreservingSubmitterTests.PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace line 305, expected (77,3.5), got (0,0). - omit atmospheric combine: RetailDetailTextureContractTests.BothShaderFamiliesUseTheSharedOnePassSourceAndDebugPrecedesDetailSample line 173, atmospheric shared include missing. - drop serialized opacity: ObjectMeshDataSerializerTests.SurfaceOpacity_RoundTripsBitExactlyAndDeterministically line 288, first reported 0.5 bits 1056964608 vs 1065353216. - stale detail arm: ordered adjacency test line 307, expected following ordinary (0,0), got (77,3.5). - per-frame surface map: EnvCellAlphaDrawSourceTests.ProductionWholeLeaf_WarmedScanSubmitRhiAndFilteredReplayDoNotAllocate line 178, expected 0 B, got 147456 B. Verification before commit: shader compiler 23/23; focused App 213/213; Content 75/75; Core Wb 10/10; launcher migration 6/6; Release solution build 0 warnings / 0 errors; git diff --check clean.
702 lines
28 KiB
C#
702 lines
28 KiB
C#
using System.Numerics;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed class EnvCellAlphaDrawSourceTests
|
|
{
|
|
/// <summary>
|
|
/// 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.</summary>
|
|
[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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The real walk leaf executes the preceding opaque pass, dispatch, and
|
|
/// queue drain. The opaque subset draws at the turn; CLIP and ALPHA do not.
|
|
/// Each delayed list then replays exactly its own subset.</summary>
|
|
[Fact]
|
|
public void WholeLeaf_MixedCellDrawsOpaqueAtTurnThenClipAndAlphaAtDrain()
|
|
{
|
|
using var fixture = new ProductionEnvCellFixture(
|
|
detailSurfaceActive: false,
|
|
BatchSpec.Opaque,
|
|
BatchSpec.ClipDds,
|
|
BatchSpec.Alpha);
|
|
|
|
fixture.Queue.BeginFrame();
|
|
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
|
|
|
GpuRecordedMultiDrawIndirect turnDraw = Assert.Single(
|
|
fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
|
Assert.Equal(1u, turnDraw.DrawCount);
|
|
Assert.Equal(["envcell-opaque"], DrawPipelineNames(fixture.Device));
|
|
Assert.Equal(1, fixture.Queue.ClipCount);
|
|
Assert.Equal(1, fixture.Queue.AlphaCount);
|
|
Assert.False(Assert.Single(ClipEntries(fixture.Queue)).OverrideClipmap);
|
|
|
|
fixture.Queue.EndFrame();
|
|
|
|
GpuRecordedMultiDrawIndirect[] draws =
|
|
[.. fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
|
|
Assert.Equal(3, draws.Length);
|
|
Assert.All(draws, static draw => Assert.Equal(1u, draw.DrawCount));
|
|
Assert.Equal(
|
|
["envcell-opaque", "envcell-clip", "envcell-alpha"],
|
|
DrawPipelineNames(fixture.Device));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, "envcell-opaque", 0f)]
|
|
[InlineData(1, "envcell-alpha", 0f)]
|
|
[InlineData(2, "envcell-additive", 0f)]
|
|
[InlineData(3, "envcell-clip", 200f / 255f)]
|
|
[InlineData(4, "envcell-clip", 100f / 255f)]
|
|
public void DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity(
|
|
int specIndex,
|
|
string expectedPipeline,
|
|
float expectedReference)
|
|
{
|
|
BatchSpec spec = specIndex switch
|
|
{
|
|
0 => BatchSpec.Opaque,
|
|
1 => BatchSpec.Alpha,
|
|
2 => BatchSpec.Additive,
|
|
3 => BatchSpec.ClipDds,
|
|
4 => BatchSpec.ClipPaletted,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(specIndex)),
|
|
};
|
|
using var fixture = new ProductionEnvCellFixture(
|
|
detailSurfaceActive: true,
|
|
spec);
|
|
fixture.Queue.BeginFrame();
|
|
|
|
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
|
|
|
Assert.Single(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
|
Assert.Equal(
|
|
[expectedPipeline],
|
|
DrawPipelineNames(fixture.Device));
|
|
Assert.All(
|
|
DrawPushConstants(fixture.Device),
|
|
constants => Assert.Equal(expectedReference, constants.ParamB));
|
|
GpuPushConstants armed = Assert.Single(
|
|
DrawPushConstants(fixture.Device),
|
|
constants => constants.ParamA != 0f);
|
|
Assert.NotEqual(0u, armed.TextureIndexA);
|
|
Assert.Equal(0, fixture.Queue.PendingCount);
|
|
GpuRecordedStorageBind batchBind = fixture.Device.Calls
|
|
.OfType<GpuRecordedStorageBind>()
|
|
.Last(call => call.Binding == GpuBindingModel.StorageBatches);
|
|
ReadOnlySpan<ModernBatchData> gpuBatches = MemoryMarshal.Cast<byte, ModernBatchData>(
|
|
fixture.Device.RingBytes.Slice((int)batchBind.OffsetBytes, (int)batchBind.SizeBytes));
|
|
Assert.Equal(0.25f, Assert.Single(gpuBatches.ToArray()).SurfaceOpacity);
|
|
fixture.Queue.EndFrame();
|
|
Assert.Single(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
|
}
|
|
|
|
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
|
|
/// with an unrelated source's entry appended between them, must still
|
|
/// produce TWO separate single-cell draw calls around the interposed
|
|
/// entry — <see cref="IRetailAlphaDrawSource"/>'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 <c>(first, count)</c> range would
|
|
/// print both cell ids together on EACH of the two
|
|
/// <c>DrawPreparedAlphaBatch</c> invocations instead of once each — the
|
|
/// exact sequence assertion below fails against that mutation.</summary>
|
|
[Fact]
|
|
public void ParticleAppendedBetweenTwoCellTokens_KeepsItsPositionInTheCombinedDrain()
|
|
{
|
|
var log = new List<string>();
|
|
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 ProductionWholeLeaf_WarmedScanSubmitRhiAndFilteredReplayDoNotAllocate()
|
|
{
|
|
using var fixture = new ProductionEnvCellFixture(
|
|
detailSurfaceActive: false,
|
|
BatchSpec.Opaque,
|
|
BatchSpec.ClipDds,
|
|
BatchSpec.Alpha,
|
|
ringCapacityBytes: 64 * 1024 * 1024);
|
|
|
|
fixture.Device.Clear();
|
|
fixture.Device.RecordingEnabled = false;
|
|
long allocated = ZeroAllocationProbe.MeasureWarmed(
|
|
fixture.RunWholeLeafFrame,
|
|
batchSize: 256,
|
|
warmupBatches: 2,
|
|
samples: 4);
|
|
|
|
Assert.Equal(0, allocated);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false, 200f / 255f)]
|
|
[InlineData(true, 100f / 255f)]
|
|
public void WholeLeaf_ClipDrainBindsExactStateAndTextureClassReference(
|
|
bool paletted,
|
|
float expectedReference)
|
|
{
|
|
BatchSpec clip = paletted ? BatchSpec.ClipPaletted : BatchSpec.ClipDds;
|
|
using var fixture = new ProductionEnvCellFixture(
|
|
detailSurfaceActive: false,
|
|
clip);
|
|
|
|
fixture.Queue.BeginFrame();
|
|
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
|
Assert.Empty(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
|
Assert.False(Assert.Single(ClipEntries(fixture.Queue)).OverrideClipmap);
|
|
fixture.Queue.EndFrame();
|
|
|
|
Assert.Equal(["envcell-clip"], DrawPipelineNames(fixture.Device));
|
|
Assert.Equal(
|
|
expectedReference,
|
|
Assert.Single(DrawPushConstants(fixture.Device)).ParamB);
|
|
|
|
GpuPipelineDescription clipPipeline = fixture.Device.CreatedPipelines
|
|
.Single(static pipeline => pipeline.Description.Name == "envcell-clip")
|
|
.Description;
|
|
Assert.Equal(GpuBlendMode.PremultipliedAlpha, clipPipeline.Blend);
|
|
Assert.True(clipPipeline.Depth.Test);
|
|
Assert.True(clipPipeline.Depth.Write);
|
|
Assert.Equal(WorldDepthContract.WorldCompare, clipPipeline.Depth.Compare);
|
|
Assert.False(clipPipeline.AlphaToCoverage);
|
|
|
|
GpuPipelineDescription alphaPipeline = fixture.Device.CreatedPipelines
|
|
.Single(static pipeline => pipeline.Description.Name == "envcell-alpha")
|
|
.Description;
|
|
Assert.Equal(GpuBlendMode.StraightAlpha, alphaPipeline.Blend);
|
|
Assert.True(alphaPipeline.Depth.Test);
|
|
Assert.False(alphaPipeline.Depth.Write);
|
|
}
|
|
|
|
[Fact]
|
|
public void WholeLeaf_PositiveStippleClipMaskUsesClipPipelineAndDdsReference()
|
|
{
|
|
using var fixture = new ProductionEnvCellFixture(
|
|
detailSurfaceActive: false,
|
|
BatchSpec.ClipPositiveStippleDds);
|
|
|
|
fixture.Queue.BeginFrame();
|
|
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
|
|
|
Assert.Empty(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
|
Assert.Equal(1, fixture.Queue.ClipCount);
|
|
Assert.Equal(0, fixture.Queue.AlphaCount);
|
|
Assert.False(Assert.Single(ClipEntries(fixture.Queue)).OverrideClipmap);
|
|
|
|
fixture.Queue.EndFrame();
|
|
|
|
Assert.Equal(["envcell-clip"], DrawPipelineNames(fixture.Device));
|
|
Assert.Equal(
|
|
200f / 255f,
|
|
Assert.Single(DrawPushConstants(fixture.Device)).ParamB);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Vulkan has no fixed-function alpha test. The shader's discard-on-less
|
|
/// spelling is exactly GREATER_EQUAL: equality survives. Changing either
|
|
/// comparison to <= (strict GREATER) fails this named source+manifest pin.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ClipShaders_UseGreaterEqualForThePerRangeReference()
|
|
{
|
|
string root = RepositoryRoot();
|
|
string modern = File.ReadAllText(Path.Combine(
|
|
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_modern.frag"));
|
|
string atmospheric = File.ReadAllText(Path.Combine(
|
|
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_atmospheric.frag"));
|
|
|
|
foreach (string shader in new[] { modern, atmospheric })
|
|
{
|
|
Assert.Contains(
|
|
"if (detailActive ? alpha < alphaCutoff : color.a < alphaCutoff)",
|
|
shader,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("alpha <= alphaCutoff", shader, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("color.a <= alphaCutoff", shader, StringComparison.Ordinal);
|
|
Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", shader, StringComparison.Ordinal);
|
|
Assert.Contains("abs(value - (100.0 / 255.0)) < 0.000001", shader, StringComparison.Ordinal);
|
|
Assert.Contains("abs(value - (200.0 / 255.0)) < 0.000001", shader, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("uParamB > 0.0", shader, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("value - 0.5", shader, StringComparison.Ordinal);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)] // Flush
|
|
[InlineData(1)] // EndFrame
|
|
[InlineData(2)] // AbortFrame
|
|
public void RejectedEnvCellStorm_RollsBackPayloadAndStillResetsFirstUseSource(int completion)
|
|
{
|
|
const int retainedGeometricBound = 4096;
|
|
int rejectedResetCount = 0;
|
|
int rejectedDrawCount = 0;
|
|
var accepted = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
|
static (_, _, _) => { },
|
|
EnvCellTransparentRoute.Clip);
|
|
var rejected = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
|
(_, _, _) => rejectedDrawCount++,
|
|
EnvCellTransparentRoute.Clip,
|
|
() => rejectedResetCount++);
|
|
var unusedAlpha = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
|
static (_, _, _) => { },
|
|
EnvCellTransparentRoute.Alpha);
|
|
var queue = new RetailAlphaQueue();
|
|
RetailPViewPassExecutor.RenderImmediateEnvCellRoute neverImmediate =
|
|
static (_, _, _) => throw new InvalidOperationException();
|
|
|
|
queue.BeginFrame();
|
|
for (int i = 0; i < RetailAlphaQueue.ListCapacity; i++)
|
|
{
|
|
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
|
(uint)i,
|
|
EnvCellTransparentRoute.Clip,
|
|
detailSurfaceActive: false,
|
|
queue,
|
|
accepted,
|
|
unusedAlpha,
|
|
neverImmediate);
|
|
}
|
|
for (int i = 0; i < RetailAlphaQueue.ListCapacity * 3; i++)
|
|
{
|
|
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
|
0xF4180104u,
|
|
EnvCellTransparentRoute.Clip,
|
|
detailSurfaceActive: false,
|
|
queue,
|
|
rejected,
|
|
unusedAlpha,
|
|
neverImmediate);
|
|
}
|
|
|
|
Assert.Equal(RetailAlphaQueue.ListCapacity, queue.ClipCount);
|
|
Assert.Equal(RetailAlphaQueue.ListCapacity, accepted.PendingCount);
|
|
Assert.Equal(0, rejected.PendingCount);
|
|
Assert.InRange(accepted.PendingCapacity, RetailAlphaQueue.ListCapacity, retainedGeometricBound);
|
|
Assert.InRange(rejected.PendingCapacity, 0, retainedGeometricBound);
|
|
|
|
switch (completion)
|
|
{
|
|
case 0:
|
|
queue.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
|
|
queue.AbortFrame();
|
|
break;
|
|
case 1:
|
|
queue.EndFrame();
|
|
break;
|
|
case 2:
|
|
queue.AbortFrame();
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(completion));
|
|
}
|
|
|
|
Assert.Equal(1, rejectedResetCount);
|
|
Assert.Equal(0, rejectedDrawCount);
|
|
Assert.Equal(0, rejected.PendingCount);
|
|
Assert.InRange(accepted.PendingCapacity, 0, retainedGeometricBound);
|
|
Assert.InRange(accepted.PreparedCapacity, 0, retainedGeometricBound);
|
|
Assert.InRange(accepted.DrawCapacity, 0, retainedGeometricBound);
|
|
Assert.InRange(rejected.PendingCapacity, 0, retainedGeometricBound);
|
|
Assert.InRange(rejected.PreparedCapacity, 0, retainedGeometricBound);
|
|
Assert.InRange(rejected.DrawCapacity, 0, retainedGeometricBound);
|
|
}
|
|
|
|
private static RetailPViewPassExecutor.EnvCellAlphaDrawSource Source(
|
|
EnvCellTransparentRoute expectedRoute,
|
|
List<string> 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);
|
|
|
|
private static List<RetailAlphaEntry> ClipEntries(RetailAlphaQueue queue) =>
|
|
(List<RetailAlphaEntry>)typeof(RetailAlphaQueue)
|
|
.GetField("_clip", BindingFlags.NonPublic | BindingFlags.Instance)!
|
|
.GetValue(queue)!;
|
|
|
|
private static string[] DrawPipelineNames(RecordingGpuDevice device)
|
|
{
|
|
var names = new List<string>();
|
|
IReadOnlyList<GpuRecordedCall> calls = device.Calls;
|
|
for (int i = 0; i < calls.Count; i++)
|
|
{
|
|
if (calls[i] is not GpuRecordedMultiDrawIndirect)
|
|
continue;
|
|
for (int prior = i - 1; prior >= 0; prior--)
|
|
{
|
|
if (calls[prior] is GpuRecordedPipelineBind bind)
|
|
{
|
|
names.Add(bind.PipelineName);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return [.. names];
|
|
}
|
|
|
|
private static GpuPushConstants[] DrawPushConstants(RecordingGpuDevice device)
|
|
{
|
|
var constants = new List<GpuPushConstants>();
|
|
IReadOnlyList<GpuRecordedCall> calls = device.Calls;
|
|
for (int i = 0; i < calls.Count; i++)
|
|
{
|
|
if (calls[i] is not GpuRecordedMultiDrawIndirect)
|
|
continue;
|
|
for (int prior = i - 1; prior >= 0; prior--)
|
|
{
|
|
if (calls[prior] is GpuRecordedPushConstants push)
|
|
{
|
|
constants.Add(push.Constants);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return [.. constants];
|
|
}
|
|
|
|
private static string RepositoryRoot()
|
|
{
|
|
DirectoryInfo? cursor = new(AppContext.BaseDirectory);
|
|
while (cursor is not null && !File.Exists(Path.Combine(cursor.FullName, "AcDream.slnx")))
|
|
cursor = cursor.Parent;
|
|
return cursor?.FullName
|
|
?? throw new DirectoryNotFoundException("Could not locate AcDream.slnx.");
|
|
}
|
|
|
|
private readonly record struct BatchSpec(
|
|
byte Mask,
|
|
bool IsTransparent,
|
|
AcDream.Core.Meshing.TranslucencyKind Translucency,
|
|
uint PaletteId)
|
|
{
|
|
internal static BatchSpec Opaque { get; } = new(
|
|
0x00,
|
|
IsTransparent: false,
|
|
AcDream.Core.Meshing.TranslucencyKind.Opaque,
|
|
PaletteId: 0);
|
|
|
|
internal static BatchSpec ClipDds { get; } = new(
|
|
RetailAlphaMeshRouter.MaskClipMap,
|
|
IsTransparent: true,
|
|
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
|
|
PaletteId: 0);
|
|
|
|
internal static BatchSpec ClipPaletted { get; } = new(
|
|
RetailAlphaMeshRouter.MaskClipMap,
|
|
IsTransparent: true,
|
|
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
|
|
PaletteId: 0x04000001u);
|
|
|
|
internal static BatchSpec ClipPositiveStippleDds { get; } = new(
|
|
RetailAlphaMeshRouter.MaskClipMap | RetailAlphaMeshRouter.MaskPositiveStipple,
|
|
IsTransparent: true,
|
|
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
|
|
PaletteId: 0);
|
|
|
|
internal static BatchSpec Alpha { get; } = new(
|
|
RetailAlphaMeshRouter.MaskAlphaFamily,
|
|
IsTransparent: true,
|
|
AcDream.Core.Meshing.TranslucencyKind.AlphaBlend,
|
|
PaletteId: 0);
|
|
|
|
internal static BatchSpec Additive { get; } = new(
|
|
RetailAlphaMeshRouter.MaskAlphaFamily,
|
|
IsTransparent: true,
|
|
AcDream.Core.Meshing.TranslucencyKind.Additive,
|
|
PaletteId: 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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,
|
|
BatchSpec firstBatch,
|
|
BatchSpec secondBatch = default,
|
|
BatchSpec thirdBatch = default,
|
|
int ringCapacityBytes = 8 * 1024 * 1024)
|
|
{
|
|
Device = new RecordingGpuDevice(ringCapacityBytes);
|
|
_frames = new GpuDeviceFrameLifetime(Device);
|
|
_scope = new VulkanWorldPassScope(sampleCount: 1);
|
|
_meshManager = new ObjectMeshManager(
|
|
new VulkanMeshPipelineDevice(Device.Retirement),
|
|
Device,
|
|
new NullPreparedAssetSource(),
|
|
NullLogger<ObjectMeshManager>.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) },
|
|
],
|
|
};
|
|
BatchSpec[] specs = secondBatch == default
|
|
? [firstBatch]
|
|
: thirdBatch == default
|
|
? [firstBatch, secondBatch]
|
|
: [firstBatch, secondBatch, thirdBatch];
|
|
var batches = new List<TextureBatchData>(specs.Length);
|
|
for (int i = 0; i < specs.Length; i++)
|
|
{
|
|
BatchSpec spec = specs[i];
|
|
batches.Add(new TextureBatchData
|
|
{
|
|
Key = new TextureKey
|
|
{
|
|
SurfaceId = 0x08000BFFu + (uint)i,
|
|
PaletteId = spec.PaletteId,
|
|
},
|
|
TextureData = new byte[8 * 8 * 4],
|
|
Indices = [0, 1, 2],
|
|
IsTransparent = spec.IsTransparent,
|
|
IsAdditive = spec.Translucency == AcDream.Core.Meshing.TranslucencyKind.Additive,
|
|
Translucency = spec.Translucency,
|
|
SurfaceOpacity = 0.25f,
|
|
RetailSurfaceMask = spec.Mask,
|
|
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<uint, Dictionary<ulong, List<InstanceData>>>
|
|
{
|
|
[CellId] = new Dictionary<ulong, List<InstanceData>>
|
|
{
|
|
[MeshId] =
|
|
[
|
|
new InstanceData
|
|
{
|
|
Transform = Matrix4x4.Identity,
|
|
CellId = CellId,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
});
|
|
((HashSet<uint>)typeof(EnvCellRenderer)
|
|
.GetField("_transparentCellIds", BindingFlags.NonPublic | BindingFlags.Instance)!
|
|
.GetValue(Renderer)!)
|
|
.Add(CellId);
|
|
|
|
Queue = new RetailAlphaQueue();
|
|
ClipSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
|
Renderer.RenderTransparentOrdered,
|
|
EnvCellTransparentRoute.Clip);
|
|
AlphaSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
|
Renderer.RenderTransparentOrdered,
|
|
EnvCellTransparentRoute.Alpha);
|
|
var passes = new RetailPViewPassExecutor(
|
|
new NullWorldPassSurface(),
|
|
NullRenderFrameGlState.Instance,
|
|
ClipFrame.NoClip(),
|
|
terrain: null,
|
|
Renderer,
|
|
(WbDrawDispatcher)RuntimeHelpers.GetUninitializedObject(typeof(WbDrawDispatcher)),
|
|
sky: null,
|
|
particles: null,
|
|
particleRenderer: null,
|
|
portalDepthMask: null,
|
|
Queue,
|
|
(WorldRenderDiagnostics)RuntimeHelpers.GetUninitializedObject(typeof(WorldRenderDiagnostics)),
|
|
(TerrainDrawDiagnosticsController)RuntimeHelpers.GetUninitializedObject(
|
|
typeof(TerrainDrawDiagnosticsController)));
|
|
Leaf = new WalkProductionLeafRenderer(
|
|
passes,
|
|
new RetailPViewFrameInput(),
|
|
new ClipFrameAssembly(),
|
|
static () => { },
|
|
static () => { },
|
|
static () => 0);
|
|
|
|
_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 WalkProductionLeafRenderer Leaf { get; }
|
|
|
|
public void RunWholeLeafFrame()
|
|
{
|
|
Queue.BeginFrame();
|
|
Leaf.DrawCellShell(CellId);
|
|
Queue.EndFrame();
|
|
}
|
|
|
|
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 NullWorldPassSurface : IWorldPassSurface
|
|
{
|
|
public void PrepareClipFrame() { }
|
|
|
|
public void EnableClipDistances() { }
|
|
|
|
public void DisableClipDistances() { }
|
|
|
|
public void ClearInteriorDepth() => throw new InvalidOperationException();
|
|
}
|
|
|
|
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<string> log) : IRetailAlphaDrawSource
|
|
{
|
|
private int[] _prepared = [];
|
|
|
|
public void PrepareAlphaDraws(ReadOnlySpan<int> 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()
|
|
{
|
|
}
|
|
}
|
|
|
|
}
|