feat(render): implement Campaign AR and terrain fidelity
This commit is contained in:
parent
99cf26e00c
commit
7a5f96ede5
368 changed files with 50611 additions and 950 deletions
|
|
@ -16,8 +16,8 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// <para>Two structural differences from V4c. It records into the pass
|
||||
/// <c>VulkanWorldScenePhase</c> opened rather than opening
|
||||
/// <c>"envcell-shells"</c> of its own, because the frame's one backbuffer pass
|
||||
/// resolves. And there is no binding-9 texture table: V4t moved the slot onto
|
||||
/// the device, and on Vulkan that table is set 2, which the encoder binds.</para>
|
||||
/// resolves. And the global texture table is set 2 rather than a storage
|
||||
/// buffer: storage binding 9 is now #226's per-instance detail category.</para>
|
||||
/// </summary>
|
||||
public sealed unsafe partial class EnvCellRenderer
|
||||
{
|
||||
|
|
@ -27,10 +27,14 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
private IGpuPipeline? _opaquePipeline;
|
||||
private IGpuPipeline? _alphaPipeline;
|
||||
private IGpuPipeline? _additivePipeline;
|
||||
private IGpuPipeline? _detailPipeline;
|
||||
private IGpuPipeline? _transparentDetailPipeline;
|
||||
private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail;
|
||||
private readonly Func<bool> _buildingDetailEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the
|
||||
/// three pipelines ARE this renderer's program, so there is no second step
|
||||
/// five pipelines ARE this renderer's program, so there is no second step
|
||||
/// and no <c>Shader</c> to hand in.
|
||||
/// </summary>
|
||||
internal EnvCellRenderer(
|
||||
|
|
@ -38,13 +42,17 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
ICurrentGpuFrameSource frames,
|
||||
IWorldPassScope scope,
|
||||
ObjectMeshManager meshManager,
|
||||
WbFrustum frustum)
|
||||
WbFrustum frustum,
|
||||
TerrainAtlas.RetailDetailTextureBinding environmentDetail = default,
|
||||
Func<bool>? buildingDetailEnabled = null)
|
||||
{
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
|
||||
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
|
||||
_meshManager = meshManager ?? throw new ArgumentNullException(nameof(meshManager));
|
||||
_frustum = frustum ?? throw new ArgumentNullException(nameof(frustum));
|
||||
_environmentDetail = environmentDetail;
|
||||
_buildingDetailEnabled = buildingDetailEnabled ?? DisableDetailTextures;
|
||||
|
||||
_opaquePipeline = CreateShellPipeline(
|
||||
device, "envcell-opaque", GpuBlendMode.None, depthWrite: true, scope.SampleCount);
|
||||
|
|
@ -52,9 +60,29 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
device, "envcell-alpha", GpuBlendMode.StraightAlpha, depthWrite: false, scope.SampleCount);
|
||||
_additivePipeline = CreateShellPipeline(
|
||||
device, "envcell-additive", GpuBlendMode.Additive, depthWrite: false, scope.SampleCount);
|
||||
_detailPipeline = CreateShellPipeline(
|
||||
device,
|
||||
"envcell-retail-detail",
|
||||
GpuBlendMode.RetailDetail,
|
||||
depthWrite: true,
|
||||
scope.SampleCount,
|
||||
shaderName: "mesh_detail",
|
||||
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
|
||||
transparent: false));
|
||||
_transparentDetailPipeline = CreateShellPipeline(
|
||||
device,
|
||||
"envcell-retail-detail-alpha",
|
||||
GpuBlendMode.RetailDetail,
|
||||
depthWrite: false,
|
||||
scope.SampleCount,
|
||||
shaderName: "mesh_detail",
|
||||
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
|
||||
transparent: true));
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
private static bool DisableDetailTextures() => false;
|
||||
|
||||
/// <summary>
|
||||
/// One pipeline per blend state the shell pass uses. Everything else is
|
||||
/// shared: <c>mesh_modern</c>, the 32-byte world-mesh vertex, triangle lists,
|
||||
|
|
@ -70,15 +98,17 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
string name,
|
||||
GpuBlendMode blend,
|
||||
bool depthWrite,
|
||||
int sampleCount) =>
|
||||
int sampleCount,
|
||||
string shaderName = "mesh_modern",
|
||||
GpuCompareOp depthCompare = GpuCompareOp.Less) =>
|
||||
device.CreatePipeline(new GpuPipelineDescription
|
||||
{
|
||||
Name = name,
|
||||
Shaders = new GpuShaderSet("mesh_modern"),
|
||||
Shaders = new GpuShaderSet(shaderName),
|
||||
VertexLayout = GpuVertexLayout.WorldMesh,
|
||||
Topology = GpuPrimitiveTopology.TriangleList,
|
||||
Blend = blend,
|
||||
Depth = new GpuDepthState(Test: true, Write: depthWrite, GpuCompareOp.Less),
|
||||
Depth = new GpuDepthState(Test: true, Write: depthWrite, depthCompare),
|
||||
Cull = GpuCullMode.Back,
|
||||
FrontFace = GpuFrontFace.Clockwise,
|
||||
AlphaToCoverage = false,
|
||||
|
|
@ -196,6 +226,7 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
BindRingSection<int>(
|
||||
encoder, frame, GpuBindingModel.StorageInstanceLightSets,
|
||||
_lightSetData.AsSpan(0, uniqueInstanceCount * lightStride));
|
||||
BindEnvironmentDetailCategory(encoder, frame);
|
||||
|
||||
// The frame-global sections, bound after this renderer's own binds
|
||||
// because those binds are what select the descriptor scope.
|
||||
|
|
@ -210,6 +241,9 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
MemoryMarshal.AsBytes(_commands.AsSpan(0, totalDraws)).CopyTo(commands.Data);
|
||||
IGpuBuffer commandBuffer = commands.Buffer;
|
||||
uint commandBase = commands.OffsetBytes;
|
||||
bool detailEnabled = RetailDetailTextureContract.ShouldRender(
|
||||
_buildingDetailEnabled(),
|
||||
_environmentDetail);
|
||||
|
||||
for (int drawRangeIndex = 0; drawRangeIndex < _mdiDrawRanges.Count; drawRangeIndex++)
|
||||
{
|
||||
|
|
@ -222,13 +256,16 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
if (cullMode == CullMode.Landblock) cullMode = CullMode.None;
|
||||
|
||||
bool isAdditive = groupIndex >= 4;
|
||||
IGpuPipeline rangeBasePipeline = isAdditive
|
||||
? _additivePipeline!
|
||||
: _alphaPipeline!;
|
||||
if (renderPass == WbRenderPass.Transparent)
|
||||
{
|
||||
// Blend state is the pipeline's; switching variants mid-pass has
|
||||
// to re-establish the mesh, which is vertex-array state.
|
||||
BindPipelineWithMesh(
|
||||
encoder,
|
||||
isAdditive ? _additivePipeline! : _alphaPipeline!,
|
||||
rangeBasePipeline,
|
||||
mesh);
|
||||
}
|
||||
|
||||
|
|
@ -241,12 +278,85 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
: (int)renderPass;
|
||||
pushConstants.DrawIdOffset = drawRange.FirstCommand;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
|
||||
// Retail DrawMesh's two-pass fallback redraws each transparent
|
||||
// RenderMeshSubset immediately, before the next delayed-alpha
|
||||
// subset. Preserve that base/detail adjacency so another shell or
|
||||
// particle cannot be composited between the two contributions.
|
||||
if (renderPass == WbRenderPass.Transparent && detailEnabled)
|
||||
{
|
||||
int rangeEnd = drawRange.FirstCommand + drawRange.CommandCount;
|
||||
for (int command = drawRange.FirstCommand; command < rangeEnd; command++)
|
||||
{
|
||||
BindPipelineWithMesh(encoder, rangeBasePipeline, mesh);
|
||||
SetCullMode(encoder, cullMode);
|
||||
pushConstants.RenderPass = isAdditive
|
||||
? (int)renderPass | 0x100
|
||||
: (int)renderPass;
|
||||
pushConstants.DrawIdOffset = command;
|
||||
pushConstants.TextureIndexA = 0;
|
||||
pushConstants.ParamA = 0f;
|
||||
pushConstants.ParamB = 0f;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
commandBuffer,
|
||||
commandBase + (uint)(command * sizeof(DrawElementsIndirectCommand)),
|
||||
1,
|
||||
(uint)sizeof(DrawElementsIndirectCommand));
|
||||
|
||||
BindPipelineWithMesh(encoder, _transparentDetailPipeline!, mesh);
|
||||
SetCullMode(encoder, cullMode);
|
||||
pushConstants.DrawIdOffset = command;
|
||||
pushConstants.TextureIndexA = _environmentDetail.TextureSlot.Index;
|
||||
pushConstants.ParamA = _environmentDetail.Tiling;
|
||||
pushConstants.ParamB = 0f;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
commandBuffer,
|
||||
commandBase + (uint)(command * sizeof(DrawElementsIndirectCommand)),
|
||||
1,
|
||||
(uint)sizeof(DrawElementsIndirectCommand));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
commandBuffer,
|
||||
commandBase + (uint)(drawRange.FirstCommand * sizeof(DrawElementsIndirectCommand)),
|
||||
(uint)drawRange.CommandCount,
|
||||
(uint)sizeof(DrawElementsIndirectCommand));
|
||||
}
|
||||
|
||||
// Retail DrawEnvCell category (2). Replay the already-filtered opaque
|
||||
// shell commands, including ClipMap built-mesh subsets, and apply the
|
||||
// 10-50 m positive-view-depth fade. The existing
|
||||
// "Building Detail Textures" option gates both this and buildings,
|
||||
// matching LScape::ChangeRegion.
|
||||
if (renderPass == WbRenderPass.Opaque
|
||||
&& detailEnabled)
|
||||
{
|
||||
BindPipelineWithMesh(encoder, _detailPipeline!, mesh);
|
||||
pushConstants.RenderPass = 0;
|
||||
pushConstants.TextureIndexA = _environmentDetail.TextureSlot.Index;
|
||||
pushConstants.ParamA = _environmentDetail.Tiling;
|
||||
pushConstants.ParamB = 0f;
|
||||
|
||||
for (int drawRangeIndex = 0; drawRangeIndex < _mdiDrawRanges.Count; drawRangeIndex++)
|
||||
{
|
||||
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
|
||||
var cullMode = (CullMode)(drawRange.GroupIndex % 4);
|
||||
if (cullMode == CullMode.Landblock)
|
||||
cullMode = CullMode.None;
|
||||
SetCullMode(encoder, cullMode);
|
||||
pushConstants.DrawIdOffset = drawRange.FirstCommand;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
commandBuffer,
|
||||
commandBase + (uint)(drawRange.FirstCommand * sizeof(DrawElementsIndirectCommand)),
|
||||
(uint)drawRange.CommandCount,
|
||||
(uint)sizeof(DrawElementsIndirectCommand));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BindPipelineWithMesh(
|
||||
|
|
@ -315,6 +425,25 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
(uint)byteCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds one category word for <c>mesh_detail.vert</c>'s statically used
|
||||
/// binding 9. EnvCell draws select their renderer-wide category via
|
||||
/// <c>uParamB=0</c>, so the value is semantically unused, but Vulkan still
|
||||
/// requires the declared descriptor to be valid.
|
||||
/// </summary>
|
||||
internal static void BindEnvironmentDetailCategory(
|
||||
IGpuPassEncoder encoder,
|
||||
IGpuFrame frame)
|
||||
{
|
||||
Span<uint> category = stackalloc uint[1];
|
||||
category[0] = 1u;
|
||||
BindRingSection<uint>(
|
||||
encoder,
|
||||
frame,
|
||||
GpuBindingModel.StorageInstanceDetailCategory,
|
||||
category);
|
||||
}
|
||||
|
||||
private void DisposeRhiResources()
|
||||
{
|
||||
_opaquePipeline?.Dispose();
|
||||
|
|
@ -323,5 +452,9 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
_alphaPipeline = null;
|
||||
_additivePipeline?.Dispose();
|
||||
_additivePipeline = null;
|
||||
_detailPipeline?.Dispose();
|
||||
_detailPipeline = null;
|
||||
_transparentDetailPipeline?.Dispose();
|
||||
_transparentDetailPipeline = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1290,6 +1290,11 @@ public sealed partial class EnvCellRenderer :
|
|||
// mesh manager at upload rather than interned here.
|
||||
TextureTableIndex = item.batch.TextureSlot.Index,
|
||||
TextureIndex = (uint)item.batch.TextureIndex,
|
||||
// #226: this renderer submits built EnvCell meshes.
|
||||
// Retail DrawMesh forwards curr_detail_surface to
|
||||
// RenderMeshSubset for every material subset, including
|
||||
// ClipMap, transparent, additive and inverse alpha.
|
||||
Flags = 1u,
|
||||
};
|
||||
|
||||
_commands[cmdIndex] = new DrawElementsIndirectCommand
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
using AcDream.App.Rendering.Gpu;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
public sealed partial class WbDrawDispatcher
|
||||
{
|
||||
internal sealed class DirectionalShadowReceiverPipelineState : IDisposable
|
||||
{
|
||||
private readonly MeshPipelineSet _backbuffer;
|
||||
private readonly MeshPipelineSet _offscreen;
|
||||
|
||||
internal DirectionalShadowReceiverPipelineState(
|
||||
IDirectionalShadowReceiverSource source,
|
||||
MeshPipelineSet backbuffer,
|
||||
MeshPipelineSet offscreen)
|
||||
{
|
||||
Source = source;
|
||||
_backbuffer = backbuffer;
|
||||
_offscreen = offscreen;
|
||||
}
|
||||
|
||||
internal IDirectionalShadowReceiverSource Source { get; }
|
||||
|
||||
internal MeshPipelineSet ForSampleCount(int sampleCount) =>
|
||||
sampleCount > 1 ? _backbuffer : _offscreen;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeMeshPipelineSet(_backbuffer);
|
||||
if (!ReferenceEquals(_offscreen, _backbuffer))
|
||||
DisposeMeshPipelineSet(_offscreen);
|
||||
}
|
||||
}
|
||||
|
||||
private DirectionalShadowReceiverPipelineState? _directionalShadowReceiver;
|
||||
|
||||
/// <summary>
|
||||
/// Builds both sample-count variants without publishing either. A different
|
||||
/// pack always gets pipelines compiled from that candidate's shader blobs;
|
||||
/// no prior pack pipeline is reused by shader name or nullable caching.
|
||||
/// </summary>
|
||||
internal DirectionalShadowReceiverPipelineState? PrepareDirectionalShadowReceiver(
|
||||
IDirectionalShadowReceiverSource? source,
|
||||
int sampleCount)
|
||||
{
|
||||
if (source is null)
|
||||
return null;
|
||||
IGpuDevice device = _device
|
||||
?? throw new InvalidOperationException("Directional receivers require the modern RHI device.");
|
||||
if (_scope is null || sampleCount != _scope.SampleCount)
|
||||
throw new InvalidOperationException("Receiver and world-pass sample counts must match.");
|
||||
|
||||
MeshPipelineSet? backbuffer = null;
|
||||
MeshPipelineSet? offscreen = null;
|
||||
try
|
||||
{
|
||||
backbuffer = CreateMeshPipelineSet(
|
||||
device,
|
||||
sampleCount,
|
||||
baseShaders: source.PipelineShaders.WorldReceiver,
|
||||
namePrefix: "wb-mesh-atmospheric",
|
||||
usesRenderPackShaderAbi: true);
|
||||
offscreen = sampleCount == 1
|
||||
? backbuffer
|
||||
: CreateMeshPipelineSet(
|
||||
device,
|
||||
1,
|
||||
baseShaders: source.PipelineShaders.WorldReceiver,
|
||||
namePrefix: "wb-mesh-atmospheric",
|
||||
usesRenderPackShaderAbi: true);
|
||||
return new DirectionalShadowReceiverPipelineState(source, backbuffer, offscreen);
|
||||
}
|
||||
catch
|
||||
{
|
||||
DisposeMeshPipelineSet(backbuffer);
|
||||
if (!ReferenceEquals(offscreen, backbuffer))
|
||||
DisposeMeshPipelineSet(offscreen);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Assignment-only publication; returned state retires after the coupled swap.</summary>
|
||||
internal DirectionalShadowReceiverPipelineState? SwapDirectionalShadowReceiver(
|
||||
DirectionalShadowReceiverPipelineState? candidate)
|
||||
{
|
||||
DirectionalShadowReceiverPipelineState? prior = _directionalShadowReceiver;
|
||||
_directionalShadowReceiver = candidate;
|
||||
return prior;
|
||||
}
|
||||
|
||||
private MeshPipelineSet PipelinesFor(
|
||||
IGpuPassEncoder encoder,
|
||||
IGpuFrame frame,
|
||||
out DirectionalShadowFrameBinding shadowBinding)
|
||||
{
|
||||
DirectionalShadowReceiverPipelineState? receiver = _directionalShadowReceiver;
|
||||
IDirectionalShadowReceiverSource? source = receiver?.Source;
|
||||
shadowBinding = DirectionalShadowFrameBinding.Disabled;
|
||||
bool bindingValid = source is not null
|
||||
&& source.TryGetCurrentFrameBinding(frame, out shadowBinding);
|
||||
if (DirectionalShadowReceiverPolicy.ShouldSelectReceiverPipeline(
|
||||
encoder.Pass.Name,
|
||||
source is not null,
|
||||
bindingValid))
|
||||
{
|
||||
return receiver!.ForSampleCount(encoder.Pass.SampleCount);
|
||||
}
|
||||
return PipelinesFor(encoder);
|
||||
}
|
||||
|
||||
private static void BindDirectionalShadowReceiver(
|
||||
IGpuPassEncoder encoder,
|
||||
in DirectionalShadowFrameBinding binding)
|
||||
{
|
||||
if (!binding.Enabled || binding.Buffer is null)
|
||||
return;
|
||||
encoder.BindUniformBuffer(
|
||||
GpuBindingModel.UniformDirectionalShadow,
|
||||
binding.Buffer,
|
||||
binding.OffsetBytes,
|
||||
binding.SizeBytes);
|
||||
}
|
||||
|
||||
private void DisposeDirectionalShadowReceiverPipelines()
|
||||
{
|
||||
DirectionalShadowReceiverPipelineState? state =
|
||||
SwapDirectionalShadowReceiver(null);
|
||||
state?.Dispose();
|
||||
}
|
||||
}
|
||||
1186
src/AcDream.App/Rendering/Wb/WbDrawDispatcher.DirectionalShadows.cs
Normal file
1186
src/AcDream.App/Rendering/Wb/WbDrawDispatcher.DirectionalShadows.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -636,6 +636,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
slot,
|
||||
lights,
|
||||
indoor,
|
||||
entity.IsBuildingShell,
|
||||
opacity,
|
||||
selectionLighting);
|
||||
}
|
||||
|
|
@ -661,6 +662,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
slot,
|
||||
lights,
|
||||
indoor,
|
||||
entity.IsBuildingShell,
|
||||
opacity: 1f,
|
||||
selectionLighting);
|
||||
}
|
||||
|
|
@ -684,19 +686,51 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
uint slot,
|
||||
InstanceLightSet lights,
|
||||
bool indoor,
|
||||
bool buildingDetail,
|
||||
float opacity,
|
||||
Vector2 selectionLighting)
|
||||
{
|
||||
InstanceGroup group =
|
||||
GetOrCreatePackedGroup(classified.Key);
|
||||
AppendPackedInstance(
|
||||
group,
|
||||
model,
|
||||
classified.LocalSortCenter,
|
||||
_nextPackedInstanceSubmissionOrder++,
|
||||
slot,
|
||||
lights,
|
||||
indoor,
|
||||
buildingDetail,
|
||||
opacity,
|
||||
selectionLighting);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one packed-route instance and every per-instance attribute in
|
||||
/// lockstep. Keeping the writer in one testable seam prevents a newly
|
||||
/// introduced storage binding from covering the legacy classifier while
|
||||
/// leaving the production packed classifier with a shorter parallel list.
|
||||
/// </summary>
|
||||
internal static void AppendPackedInstance(
|
||||
InstanceGroup group,
|
||||
Matrix4x4 model,
|
||||
Vector3 localSortCenter,
|
||||
int submissionOrder,
|
||||
uint slot,
|
||||
InstanceLightSet lights,
|
||||
bool indoor,
|
||||
bool buildingDetail,
|
||||
float opacity,
|
||||
Vector2 selectionLighting)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(group);
|
||||
group.Matrices.Add(model);
|
||||
group.LocalSortCenters.Add(
|
||||
classified.LocalSortCenter);
|
||||
group.SubmissionOrders.Add(
|
||||
_nextPackedInstanceSubmissionOrder++);
|
||||
group.LocalSortCenters.Add(localSortCenter);
|
||||
group.SubmissionOrders.Add(submissionOrder);
|
||||
group.Slots.Add(slot);
|
||||
group.LightSets.Add(lights);
|
||||
group.IndoorFlags.Add(indoor ? 1u : 0u);
|
||||
group.DetailCategories.Add(buildingDetail ? 1u : 0u);
|
||||
group.Opacities.Add(opacity);
|
||||
group.SelectionLighting.Add(selectionLighting);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// AMD's GL stack did not. Every GL statement in the sibling file is untouched;
|
||||
/// everything here runs only when there is no GL context.</para>
|
||||
///
|
||||
/// <para>Three differences from V4c, each because the tree moved under it. There
|
||||
/// is no binding-9 texture table — V4t put the slot on the device and Vulkan
|
||||
/// binds set 2. The pass is BORROWED from <see cref="IWorldPassScope"/> rather
|
||||
/// <para>Three differences from V4c, each because the tree moved under it. The
|
||||
/// texture table moved to set 2; #226 now uses storage binding 9 for the detail
|
||||
/// category. The pass is BORROWED from <see cref="IWorldPassScope"/> rather
|
||||
/// than opened, because the frame's one backbuffer pass resolves. And the
|
||||
/// pipelines carry the device's sample count, because Vulkan requires a
|
||||
/// pipeline's <c>rasterizationSamples</c> to match the pass and
|
||||
|
|
@ -33,7 +33,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
private readonly IWorldPassScope? _scope;
|
||||
|
||||
/// <summary>
|
||||
/// The five mesh pipelines at ONE sample count.
|
||||
/// The seven mesh pipelines at ONE sample count.
|
||||
///
|
||||
/// <para>Campaign V slice V6l: there are two of these. Vulkan requires a
|
||||
/// pipeline's <c>rasterizationSamples</c> to equal the pass it draws in, and
|
||||
|
|
@ -46,19 +46,25 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
/// the live pass actually is. Both are built at startup against the persisted
|
||||
/// cache, so no frame ever compiles one.</para>
|
||||
/// </summary>
|
||||
private sealed record MeshPipelineSet(
|
||||
internal sealed record MeshPipelineSet(
|
||||
int SampleCount,
|
||||
IGpuPipeline Opaque,
|
||||
IGpuPipeline OpaqueAlphaToCoverage,
|
||||
IGpuPipeline AlphaBlend,
|
||||
IGpuPipeline AlphaAdditive,
|
||||
IGpuPipeline AlphaInverse);
|
||||
IGpuPipeline AlphaInverse,
|
||||
IGpuPipeline RetailDetail,
|
||||
IGpuPipeline RetailDetailTransparent);
|
||||
|
||||
private MeshPipelineSet? _backbufferPipelines;
|
||||
private MeshPipelineSet? _offscreenPipelines;
|
||||
|
||||
private const string OpaqueTimerScope = "wb-entities-opaque";
|
||||
private const string TransparentTimerScope = "wb-entities-transparent";
|
||||
private const string DetailTimerScope = "wb-buildings-detail";
|
||||
|
||||
private readonly TerrainAtlas.RetailDetailTextureBinding _buildingDetail;
|
||||
private readonly Func<bool> _buildingDetailEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// A ring slice reduced to the three values a later bind needs. The prepared
|
||||
|
|
@ -70,6 +76,11 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
uint OffsetBytes,
|
||||
uint SizeBytes);
|
||||
|
||||
private readonly WorldTransformFrameArena _worldTransformFrames = new();
|
||||
private long _ordinaryTransformDemandFrameSerial = -1;
|
||||
private uint _ordinaryTransformDemandThisFrame;
|
||||
private uint _ordinaryTransformDemandHighWater;
|
||||
|
||||
private RhiSection _alphaInstances;
|
||||
private RhiSection _alphaBatches;
|
||||
private RhiSection _alphaClipSlots;
|
||||
|
|
@ -78,12 +89,78 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
private RhiSection _alphaIndoor;
|
||||
private RhiSection _alphaOpacity;
|
||||
private RhiSection _alphaSelectionLighting;
|
||||
private RhiSection _alphaDetailCategory;
|
||||
private RhiSection _alphaCommands;
|
||||
private int _preparedAlphaInstanceCount;
|
||||
private uint _alphaTransformBaseInstance;
|
||||
|
||||
/// <summary>
|
||||
/// Starts the one authoritative transform address space for an enhanced
|
||||
/// world frame. The compatibility overload writes the shadow prefix into a
|
||||
/// frame ring; the retained overload activates the current flight slot's
|
||||
/// already-published prefix. Ordinary N.5 submissions append later and use
|
||||
/// absolute <c>BaseInstance</c> values into the same bound buffer/range.
|
||||
/// </summary>
|
||||
internal WorldTransformFrameSlice BeginDirectionalShadowTransformFrame(
|
||||
IGpuFrame frame,
|
||||
ReadOnlySpan<Matrix4x4> transforms)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
return _worldTransformFrames.Begin(
|
||||
frame,
|
||||
transforms,
|
||||
ResolveDirectionalShadowTransformBindingSize(transforms.Length));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the one authoritative pose-buffer range before any shadow draw
|
||||
/// records it. The retained owner and ordinary world appenders therefore
|
||||
/// use the same demand-sized address space for the complete frame.
|
||||
/// </summary>
|
||||
internal uint ResolveDirectionalShadowTransformBindingSize(
|
||||
int requiredPrefixInstances,
|
||||
int ordinaryInstanceUpperBound = 0)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(requiredPrefixInstances);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(ordinaryInstanceUpperBound);
|
||||
uint maximum = _device?.Capabilities.MaxStorageBufferRangeBytes
|
||||
?? WorldTransformCapacityPolicy.VulkanGuaranteedMaxStorageBufferRangeBytes;
|
||||
uint currentFrameDemand = checked(
|
||||
(uint)requiredPrefixInstances + (uint)ordinaryInstanceUpperBound);
|
||||
uint requiredCombinedInstances = checked(
|
||||
(uint)requiredPrefixInstances + _ordinaryTransformDemandHighWater);
|
||||
return WorldTransformCapacityPolicy.ResolveBindingSizeBytes(
|
||||
Math.Max(currentFrameDemand, requiredCombinedInstances),
|
||||
maximum);
|
||||
}
|
||||
|
||||
internal WorldTransformFrameSlice BeginDirectionalShadowTransformFrame(
|
||||
IGpuFrame frame,
|
||||
in WorldTransformFrameSlice retainedShadowPrefix)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
return _worldTransformFrames.BeginRetained(
|
||||
frame,
|
||||
in retainedShadowPrefix);
|
||||
}
|
||||
|
||||
internal void CancelDirectionalShadowTransformFrame(IGpuFrame frame)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
_worldTransformFrames.Cancel(frame);
|
||||
}
|
||||
|
||||
internal bool HasDirectionalShadowTransformFrame(long frameSerial) =>
|
||||
_worldTransformFrames.IsActiveFor(frameSerial);
|
||||
|
||||
internal uint DirectionalShadowTransformFrameUsedInstances =>
|
||||
_worldTransformFrames.UsedInstances;
|
||||
|
||||
/// <summary>
|
||||
/// The RHI arm's constructor. No GL context, no <c>Shader</c>, no
|
||||
/// <c>BindlessSupport</c>: the five pipelines compile <c>mesh_modern</c> from
|
||||
/// the committed SPIR-V, and batch data already carries the device's own
|
||||
/// <c>BindlessSupport</c>: five base pipelines compile <c>mesh_modern</c> and
|
||||
/// the detail overlay compiles <c>mesh_detail</c> from committed SPIR-V;
|
||||
/// batch data already carries the device's own
|
||||
/// <c>GpuTextureSlot</c> (V4t) rather than a bindless handle.
|
||||
/// </summary>
|
||||
internal WbDrawDispatcher(
|
||||
|
|
@ -97,7 +174,9 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
AcDream.Core.Rendering.TranslucencyFadeManager translucencyFades,
|
||||
IRetailSelectionRenderSink? selectionSink = null,
|
||||
RetailAlphaQueue? alphaQueue = null,
|
||||
long? alphaScratchBudgetBytes = null)
|
||||
long? alphaScratchBudgetBytes = null,
|
||||
TerrainAtlas.RetailDetailTextureBinding buildingDetail = default,
|
||||
Func<bool>? buildingDetailEnabled = null)
|
||||
{
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
|
||||
|
|
@ -114,6 +193,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
_selectionLighting = selectionSink as IRetailSelectionLightingSource;
|
||||
_alphaQueue = alphaQueue;
|
||||
_alphaSource = new AlphaDrawSource(this);
|
||||
_buildingDetail = buildingDetail;
|
||||
_buildingDetailEnabled = buildingDetailEnabled ?? DisableDetailTextures;
|
||||
long scratchBudget = alphaScratchBudgetBytes
|
||||
?? AlphaScratchBudgetProfile.Create(
|
||||
ResidencyBudgetOptions.Default.AlphaScratchBytes)
|
||||
|
|
@ -137,21 +218,82 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
}
|
||||
}
|
||||
|
||||
private static MeshPipelineSet CreateMeshPipelineSet(IGpuDevice device, int samples)
|
||||
private static bool DisableDetailTextures() => false;
|
||||
|
||||
private static MeshPipelineSet CreateMeshPipelineSet(
|
||||
IGpuDevice device,
|
||||
int samples,
|
||||
string baseShaderName = "mesh_modern",
|
||||
GpuShaderSet? baseShaders = null,
|
||||
string namePrefix = "wb-mesh",
|
||||
bool usesRenderPackShaderAbi = false)
|
||||
{
|
||||
string suffix = samples > 1 ? string.Empty : "-1x";
|
||||
return new MeshPipelineSet(
|
||||
samples,
|
||||
CreateMeshPipeline(
|
||||
device, $"wb-mesh-opaque{suffix}", GpuBlendMode.None, true, false, samples),
|
||||
CreateMeshPipeline(
|
||||
device, $"wb-mesh-opaque-a2c{suffix}", GpuBlendMode.None, true, true, samples),
|
||||
CreateMeshPipeline(
|
||||
device, $"wb-mesh-alpha{suffix}", GpuBlendMode.StraightAlpha, false, false, samples),
|
||||
CreateMeshPipeline(
|
||||
device, $"wb-mesh-additive{suffix}", GpuBlendMode.Additive, false, false, samples),
|
||||
CreateMeshPipeline(
|
||||
device, $"wb-mesh-inverse{suffix}", GpuBlendMode.InverseAlpha, false, false, samples));
|
||||
var created = new List<IGpuPipeline>(7);
|
||||
try
|
||||
{
|
||||
return new MeshPipelineSet(
|
||||
samples,
|
||||
Track(CreateMeshPipeline(
|
||||
device, $"{namePrefix}-opaque{suffix}", GpuBlendMode.None, true, false, samples,
|
||||
shaders: baseShaders,
|
||||
shaderName: baseShaderName,
|
||||
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
|
||||
Track(CreateMeshPipeline(
|
||||
device, $"{namePrefix}-opaque-a2c{suffix}", GpuBlendMode.None, true, true, samples,
|
||||
shaders: baseShaders,
|
||||
shaderName: baseShaderName,
|
||||
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
|
||||
Track(CreateMeshPipeline(
|
||||
device, $"{namePrefix}-alpha{suffix}", GpuBlendMode.StraightAlpha, false, false, samples,
|
||||
shaders: baseShaders,
|
||||
shaderName: baseShaderName,
|
||||
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
|
||||
Track(CreateMeshPipeline(
|
||||
device, $"{namePrefix}-additive{suffix}", GpuBlendMode.Additive, false, false, samples,
|
||||
shaders: baseShaders,
|
||||
shaderName: baseShaderName,
|
||||
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
|
||||
Track(CreateMeshPipeline(
|
||||
device, $"{namePrefix}-inverse{suffix}", GpuBlendMode.InverseAlpha, false, false, samples,
|
||||
shaders: baseShaders,
|
||||
shaderName: baseShaderName,
|
||||
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
|
||||
Track(CreateMeshPipeline(
|
||||
device,
|
||||
$"wb-mesh-retail-detail{suffix}",
|
||||
GpuBlendMode.RetailDetail,
|
||||
true,
|
||||
false,
|
||||
samples,
|
||||
shaderName: "mesh_detail",
|
||||
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
|
||||
transparent: false),
|
||||
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
|
||||
Track(CreateMeshPipeline(
|
||||
device,
|
||||
$"wb-mesh-retail-detail-alpha{suffix}",
|
||||
GpuBlendMode.RetailDetail,
|
||||
false,
|
||||
false,
|
||||
samples,
|
||||
shaderName: "mesh_detail",
|
||||
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
|
||||
transparent: true),
|
||||
usesRenderPackShaderAbi: usesRenderPackShaderAbi)));
|
||||
}
|
||||
catch
|
||||
{
|
||||
for (int i = created.Count - 1; i >= 0; i--)
|
||||
created[i].Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
IGpuPipeline Track(IGpuPipeline pipeline)
|
||||
{
|
||||
created.Add(pipeline);
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -183,19 +325,24 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
GpuBlendMode blend,
|
||||
bool depthWrite,
|
||||
bool alphaToCoverage,
|
||||
int sampleCount) =>
|
||||
int sampleCount,
|
||||
string shaderName = "mesh_modern",
|
||||
GpuShaderSet? shaders = null,
|
||||
GpuCompareOp depthCompare = GpuCompareOp.Less,
|
||||
bool usesRenderPackShaderAbi = false) =>
|
||||
device.CreatePipeline(new GpuPipelineDescription
|
||||
{
|
||||
Name = name,
|
||||
Shaders = new GpuShaderSet("mesh_modern"),
|
||||
Shaders = shaders ?? new GpuShaderSet(shaderName),
|
||||
VertexLayout = GpuVertexLayout.WorldMesh,
|
||||
Topology = GpuPrimitiveTopology.TriangleList,
|
||||
Blend = blend,
|
||||
Depth = new GpuDepthState(Test: true, Write: depthWrite, GpuCompareOp.Less),
|
||||
Depth = new GpuDepthState(Test: true, Write: depthWrite, depthCompare),
|
||||
Cull = GpuCullMode.Back,
|
||||
FrontFace = GpuFrontFace.Clockwise,
|
||||
AlphaToCoverage = alphaToCoverage,
|
||||
ColorWrite = true,
|
||||
UsesRenderPackShaderAbi = usesRenderPackShaderAbi,
|
||||
SampleCount = sampleCount,
|
||||
});
|
||||
|
||||
|
|
@ -230,19 +377,33 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
ParamB = 0f,
|
||||
};
|
||||
|
||||
RhiSection instanceTransforms = WriteWorldTransformSection(
|
||||
frame,
|
||||
_instanceData.AsSpan(0, immediateInstances * 16),
|
||||
out uint transformBaseInstance);
|
||||
// Pack receiver/detail shaders subtract this shared-arena prefix for
|
||||
// every parallel per-instance array while retaining the absolute pose
|
||||
// lookup. The acdream default path always receives zero here.
|
||||
pushConstants.TextureIndexB = transformBaseInstance;
|
||||
|
||||
// Bind the opaque variant first so the ring binds land on a live program;
|
||||
// the transparent bracket rebinds its own variant, and push constants
|
||||
// survive that switch per the encoder contract.
|
||||
MeshPipelineSet pipelines = PipelinesFor(encoder);
|
||||
MeshPipelineSet pipelines = PipelinesFor(
|
||||
encoder,
|
||||
frame,
|
||||
out DirectionalShadowFrameBinding shadowBinding);
|
||||
BindPipelineWithMesh(
|
||||
encoder,
|
||||
AlphaToCoverage ? pipelines.OpaqueAlphaToCoverage : pipelines.Opaque,
|
||||
mesh);
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
BindDirectionalShadowReceiver(encoder, in shadowBinding);
|
||||
|
||||
BindRingSection<float>(
|
||||
encoder, frame, GpuBindingModel.StorageInstances,
|
||||
_instanceData.AsSpan(0, immediateInstances * 16));
|
||||
BindSection(
|
||||
encoder,
|
||||
GpuBindingModel.StorageInstances,
|
||||
instanceTransforms);
|
||||
BindRingSection<BatchData>(
|
||||
encoder, frame, GpuBindingModel.StorageBatches,
|
||||
_batchData.AsSpan(0, totalDraws));
|
||||
|
|
@ -262,19 +423,25 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
BindRingSection<Vector2>(
|
||||
encoder, frame, GpuBindingModel.StorageInstanceSelectionLighting,
|
||||
_selectionLightingData.AsSpan(0, immediateInstances));
|
||||
BindRingSection<uint>(
|
||||
encoder, frame, GpuBindingModel.StorageInstanceDetailCategory,
|
||||
_detailCategoryData.AsSpan(0, immediateInstances));
|
||||
|
||||
AcDream.App.Rendering.WorldFrameSectionBinding.BindClipRegions(
|
||||
encoder, scope.Sections, frame);
|
||||
AcDream.App.Rendering.WorldFrameSectionBinding.BindSceneLighting(
|
||||
encoder, scope.Sections, frame);
|
||||
|
||||
GpuRingAllocation commands = frame.AllocateRing(
|
||||
totalDraws * DrawCommandStride,
|
||||
GpuRingUsage.Indirect);
|
||||
MemoryMarshal.AsBytes(_indirectCommands.AsSpan(0, totalDraws))
|
||||
.CopyTo(commands.Data);
|
||||
GpuRingAllocation commands = WriteIndirectCommands(
|
||||
frame,
|
||||
_indirectCommands.AsSpan(0, totalDraws),
|
||||
transformBaseInstance);
|
||||
IGpuBuffer commandBuffer = commands.Buffer;
|
||||
uint commandBase = commands.OffsetBytes;
|
||||
ReadOnlySpan<DrawElementsIndirectCommand> usedCommands =
|
||||
_indirectCommands.AsSpan(0, totalDraws);
|
||||
ReadOnlySpan<uint> usedDetailCategories =
|
||||
_detailCategoryData.AsSpan(0, immediateInstances);
|
||||
|
||||
// ── Phase 7: opaque pass ─────────────────────────────────────────────
|
||||
if (_opaqueDrawCount > 0)
|
||||
|
|
@ -295,10 +462,64 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
}
|
||||
}
|
||||
|
||||
// Retail DrawBuilding detail category (1). Only consecutive command
|
||||
// runs containing a building are replayed; mesh_detail filters ordinary
|
||||
// instances inside a mixed command. This includes ClipMap built-mesh
|
||||
// subsets: the named retail
|
||||
// DrawMesh path forwards curr_detail_surface to RenderMeshSubset for
|
||||
// every material kind. The setting is read at draw time so the existing
|
||||
// Options checkbox changes the live scene immediately.
|
||||
if (_opaqueDrawCount > 0
|
||||
&& RetailDetailTextureContract.ShouldRender(
|
||||
_buildingDetailEnabled(),
|
||||
_buildingDetail))
|
||||
{
|
||||
int searchStart = 0;
|
||||
if (TryGetNextDetailCommandRun(
|
||||
usedCommands,
|
||||
usedDetailCategories,
|
||||
searchStart,
|
||||
_opaqueDrawCount,
|
||||
out DetailCommandRun run))
|
||||
{
|
||||
BindPipelineWithMesh(encoder, pipelines.RetailDetail, mesh);
|
||||
pushConstants.RenderPass = 0;
|
||||
pushConstants.DrawIdOffset = 0;
|
||||
pushConstants.TextureIndexA = _buildingDetail.TextureSlot.Index;
|
||||
pushConstants.ParamA = _buildingDetail.Tiling;
|
||||
pushConstants.ParamB = 1f;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
using (BeginRhiTimer(encoder, diag, DetailTimerScope))
|
||||
{
|
||||
do
|
||||
{
|
||||
DrawIndirectRangeRhi(
|
||||
encoder,
|
||||
ref pushConstants,
|
||||
commandBuffer,
|
||||
commandBase,
|
||||
run.FirstCommand,
|
||||
run.CommandCount);
|
||||
searchStart = run.FirstCommand + run.CommandCount;
|
||||
}
|
||||
while (TryGetNextDetailCommandRun(
|
||||
usedCommands,
|
||||
usedDetailCategories,
|
||||
searchStart,
|
||||
_opaqueDrawCount,
|
||||
out run));
|
||||
}
|
||||
}
|
||||
|
||||
// Later base passes expect neutral spare push fields.
|
||||
pushConstants.TextureIndexA = 0;
|
||||
pushConstants.ParamA = 0f;
|
||||
pushConstants.ParamB = 0f;
|
||||
}
|
||||
|
||||
// ── Phase 8: transparent pass ────────────────────────────────────────
|
||||
if (_transparentDrawCount > 0)
|
||||
{
|
||||
BindPipelineWithMesh(encoder, pipelines.AlphaBlend, mesh);
|
||||
// Issue #52 again: the transparent section starts at _opaqueDrawCount.
|
||||
// Without the offset each transparent draw reads the OPAQUE section
|
||||
// and the lifestone crystal's texture flickers.
|
||||
|
|
@ -307,9 +528,14 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
encoder.SetPushConstants(in pushConstants);
|
||||
using (BeginRhiTimer(encoder, diag, TransparentTimerScope))
|
||||
{
|
||||
DrawIndirectRangeRhi(
|
||||
encoder, ref pushConstants, commandBuffer, commandBase,
|
||||
_opaqueDrawCount, _transparentDrawCount);
|
||||
DrawImmediateTransparentRhi(
|
||||
encoder,
|
||||
mesh,
|
||||
pipelines,
|
||||
ref pushConstants,
|
||||
commandBuffer,
|
||||
commandBase,
|
||||
usedDetailCategories);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -323,8 +549,13 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
/// </summary>
|
||||
private void PrepareRhiAlphaSections(int count)
|
||||
{
|
||||
_preparedAlphaInstanceCount = count;
|
||||
IGpuFrame frame = RequireRhiFrame();
|
||||
_alphaInstances = WriteRingSection<float>(frame, _instanceData.AsSpan(0, count * 16));
|
||||
_alphaInstances = WriteWorldTransformSection(
|
||||
frame,
|
||||
_instanceData.AsSpan(0, count * 16),
|
||||
out uint transformBaseInstance);
|
||||
_alphaTransformBaseInstance = transformBaseInstance;
|
||||
_alphaBatches = WriteRingSection<BatchData>(frame, _batchData.AsSpan(0, count));
|
||||
_alphaClipSlots = WriteRingSection<uint>(frame, _clipSlotData.AsSpan(0, count));
|
||||
int lightCount = GlobalLightPacker.Pack(_pointSnapshot, ref _globalLightData);
|
||||
|
|
@ -340,10 +571,17 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
_alphaSelectionLighting = WriteRingSection<Vector2>(
|
||||
frame,
|
||||
_selectionLightingData.AsSpan(0, count));
|
||||
_alphaCommands = WriteRingSection<DrawElementsIndirectCommand>(
|
||||
_alphaDetailCategory = WriteRingSection<uint>(
|
||||
frame,
|
||||
_detailCategoryData.AsSpan(0, count));
|
||||
GpuRingAllocation commands = WriteIndirectCommands(
|
||||
frame,
|
||||
_indirectCommands.AsSpan(0, count),
|
||||
GpuRingUsage.Indirect);
|
||||
transformBaseInstance);
|
||||
_alphaCommands = new RhiSection(
|
||||
commands.Buffer,
|
||||
commands.OffsetBytes,
|
||||
checked((uint)(count * DrawCommandStride)));
|
||||
}
|
||||
|
||||
private void DrawPreparedAlphaBatchRhi(
|
||||
|
|
@ -366,14 +604,18 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
RenderPass = 1,
|
||||
LightDebug = RenderingDiagnostics.LightDebugMode,
|
||||
TextureIndexA = 0,
|
||||
TextureIndexB = 0,
|
||||
TextureIndexB = _alphaTransformBaseInstance,
|
||||
ParamA = 0f,
|
||||
ParamB = 0f,
|
||||
};
|
||||
|
||||
MeshPipelineSet pipelines = PipelinesFor(encoder);
|
||||
MeshPipelineSet pipelines = PipelinesFor(
|
||||
encoder,
|
||||
frame,
|
||||
out DirectionalShadowFrameBinding shadowBinding);
|
||||
BindPipelineWithMesh(encoder, pipelines.AlphaBlend, mesh);
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
BindDirectionalShadowReceiver(encoder, in shadowBinding);
|
||||
BindSection(encoder, GpuBindingModel.StorageInstances, _alphaInstances);
|
||||
BindSection(encoder, GpuBindingModel.StorageBatches, _alphaBatches);
|
||||
BindSection(encoder, GpuBindingModel.StorageClipSlots, _alphaClipSlots);
|
||||
|
|
@ -385,18 +627,45 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
encoder,
|
||||
GpuBindingModel.StorageInstanceSelectionLighting,
|
||||
_alphaSelectionLighting);
|
||||
BindSection(
|
||||
encoder,
|
||||
GpuBindingModel.StorageInstanceDetailCategory,
|
||||
_alphaDetailCategory);
|
||||
AcDream.App.Rendering.WorldFrameSectionBinding.BindClipRegions(
|
||||
encoder, scope.Sections, frame);
|
||||
AcDream.App.Rendering.WorldFrameSectionBinding.BindSceneLighting(
|
||||
encoder, scope.Sections, frame);
|
||||
|
||||
bool detailEnabled = RetailDetailTextureContract.ShouldRender(
|
||||
_buildingDetailEnabled(),
|
||||
_buildingDetail);
|
||||
if (firstPreparedDraw < 0
|
||||
|| drawCount < 0
|
||||
|| firstPreparedDraw > _preparedAlphaInstanceCount - drawCount)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(firstPreparedDraw),
|
||||
"The prepared-alpha draw range exceeds its uploaded instance/category payload.");
|
||||
}
|
||||
ReadOnlySpan<uint> usedDetailCategories =
|
||||
_detailCategoryData.AsSpan(0, _preparedAlphaInstanceCount);
|
||||
int runStart = firstPreparedDraw;
|
||||
int preparedEnd = firstPreparedDraw + drawCount;
|
||||
while (runStart < preparedEnd)
|
||||
{
|
||||
TranslucencyKind blend = _deferredAlphaKinds[runStart];
|
||||
bool hasDetail = detailEnabled
|
||||
&& CommandContainsDetailCategory(
|
||||
_indirectCommands[runStart],
|
||||
usedDetailCategories);
|
||||
int runEnd = runStart + 1;
|
||||
while (runEnd < preparedEnd && _deferredAlphaKinds[runEnd] == blend)
|
||||
while (runEnd < preparedEnd
|
||||
&& !hasDetail
|
||||
&& _deferredAlphaKinds[runEnd] == blend
|
||||
&& (!detailEnabled
|
||||
|| !CommandContainsDetailCategory(
|
||||
_indirectCommands[runEnd],
|
||||
usedDetailCategories)))
|
||||
runEnd++;
|
||||
|
||||
// ApplyRetailBlend's three cases are three pipelines, including the
|
||||
|
|
@ -410,10 +679,112 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
_alphaCommands.OffsetBytes,
|
||||
runStart,
|
||||
runEnd - runStart);
|
||||
|
||||
if (hasDetail)
|
||||
{
|
||||
DrawBuildingDetailRangeRhi(
|
||||
encoder,
|
||||
mesh,
|
||||
pipelines.RetailDetailTransparent,
|
||||
ref pushConstants,
|
||||
_alphaCommands.Buffer!,
|
||||
_alphaCommands.OffsetBytes,
|
||||
runStart,
|
||||
1);
|
||||
}
|
||||
runStart = runEnd;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawImmediateTransparentRhi(
|
||||
IGpuPassEncoder encoder,
|
||||
GlobalMeshBuffer mesh,
|
||||
MeshPipelineSet pipelines,
|
||||
ref GpuPushConstants pushConstants,
|
||||
IGpuBuffer commandBuffer,
|
||||
uint commandBase,
|
||||
ReadOnlySpan<uint> usedDetailCategories)
|
||||
{
|
||||
bool detailEnabled = RetailDetailTextureContract.ShouldRender(
|
||||
_buildingDetailEnabled(),
|
||||
_buildingDetail);
|
||||
int command = _opaqueDrawCount;
|
||||
int end = command + _transparentDrawCount;
|
||||
while (command < end)
|
||||
{
|
||||
bool hasDetail = detailEnabled
|
||||
&& CommandContainsDetailCategory(
|
||||
_indirectCommands[command],
|
||||
usedDetailCategories);
|
||||
int runEnd = command + 1;
|
||||
while (runEnd < end
|
||||
&& !hasDetail
|
||||
&& (!detailEnabled
|
||||
|| !CommandContainsDetailCategory(
|
||||
_indirectCommands[runEnd],
|
||||
usedDetailCategories)))
|
||||
{
|
||||
runEnd++;
|
||||
}
|
||||
|
||||
BindPipelineWithMesh(encoder, pipelines.AlphaBlend, mesh);
|
||||
ClearDetailPushConstants(ref pushConstants);
|
||||
DrawIndirectRangeRhi(
|
||||
encoder,
|
||||
ref pushConstants,
|
||||
commandBuffer,
|
||||
commandBase,
|
||||
command,
|
||||
runEnd - command);
|
||||
|
||||
if (hasDetail)
|
||||
{
|
||||
DrawBuildingDetailRangeRhi(
|
||||
encoder,
|
||||
mesh,
|
||||
pipelines.RetailDetailTransparent,
|
||||
ref pushConstants,
|
||||
commandBuffer,
|
||||
commandBase,
|
||||
command,
|
||||
1);
|
||||
}
|
||||
command = runEnd;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawBuildingDetailRangeRhi(
|
||||
IGpuPassEncoder encoder,
|
||||
GlobalMeshBuffer mesh,
|
||||
IGpuPipeline detailPipeline,
|
||||
ref GpuPushConstants pushConstants,
|
||||
IGpuBuffer commandBuffer,
|
||||
uint commandBase,
|
||||
int firstCommand,
|
||||
int commandCount)
|
||||
{
|
||||
BindPipelineWithMesh(encoder, detailPipeline, mesh);
|
||||
pushConstants.TextureIndexA = _buildingDetail.TextureSlot.Index;
|
||||
pushConstants.ParamA = _buildingDetail.Tiling;
|
||||
pushConstants.ParamB = 1f;
|
||||
DrawIndirectRangeRhi(
|
||||
encoder,
|
||||
ref pushConstants,
|
||||
commandBuffer,
|
||||
commandBase,
|
||||
firstCommand,
|
||||
commandCount);
|
||||
ClearDetailPushConstants(ref pushConstants);
|
||||
}
|
||||
|
||||
private static void ClearDetailPushConstants(
|
||||
ref GpuPushConstants pushConstants)
|
||||
{
|
||||
pushConstants.TextureIndexA = 0;
|
||||
pushConstants.ParamA = 0f;
|
||||
pushConstants.ParamB = 0f;
|
||||
}
|
||||
|
||||
private static IGpuPipeline PipelineForBlend(MeshPipelineSet pipelines, TranslucencyKind blend) =>
|
||||
blend switch
|
||||
{
|
||||
|
|
@ -551,6 +922,95 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
return new RhiSection(allocation.Buffer, allocation.OffsetBytes, (uint)byteCount);
|
||||
}
|
||||
|
||||
private RhiSection WriteWorldTransformSection(
|
||||
IGpuFrame frame,
|
||||
ReadOnlySpan<float> matrixFloats,
|
||||
out uint firstInstance)
|
||||
{
|
||||
ResetWorldTransformFrameIfStale(frame.Serial);
|
||||
if ((matrixFloats.Length & 15) != 0)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"World transforms must contain complete 16-float matrices.",
|
||||
nameof(matrixFloats));
|
||||
}
|
||||
ObserveOrdinaryTransformDemand(
|
||||
frame.Serial,
|
||||
checked((uint)(matrixFloats.Length / 16)));
|
||||
if (!_worldTransformFrames.IsActive)
|
||||
{
|
||||
firstInstance = 0;
|
||||
return WriteRingSection(frame, matrixFloats);
|
||||
}
|
||||
|
||||
WorldTransformFrameSlice appended = _worldTransformFrames.Append(
|
||||
frame,
|
||||
MemoryMarshal.Cast<float, Matrix4x4>(matrixFloats));
|
||||
firstInstance = appended.FirstInstance;
|
||||
return new RhiSection(
|
||||
appended.Buffer,
|
||||
appended.BaseOffsetBytes,
|
||||
appended.BindingSizeBytes);
|
||||
}
|
||||
|
||||
private static GpuRingAllocation WriteIndirectCommands(
|
||||
IGpuFrame frame,
|
||||
Span<DrawElementsIndirectCommand> commands,
|
||||
uint baseInstance)
|
||||
{
|
||||
int byteCount = checked(commands.Length * DrawCommandStride);
|
||||
GpuRingAllocation allocation = frame.AllocateRing(
|
||||
byteCount,
|
||||
GpuRingUsage.Indirect);
|
||||
if (baseInstance == 0)
|
||||
{
|
||||
MemoryMarshal.AsBytes(commands).CopyTo(allocation.Data);
|
||||
return allocation;
|
||||
}
|
||||
|
||||
int adjusted = 0;
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < commands.Length; i++)
|
||||
{
|
||||
commands[i].BaseInstance = checked(
|
||||
commands[i].BaseInstance + baseInstance);
|
||||
adjusted++;
|
||||
}
|
||||
MemoryMarshal.AsBytes(commands).CopyTo(allocation.Data);
|
||||
}
|
||||
finally
|
||||
{
|
||||
for (int i = 0; i < adjusted; i++)
|
||||
commands[i].BaseInstance -= baseInstance;
|
||||
}
|
||||
return allocation;
|
||||
}
|
||||
|
||||
private void ResetWorldTransformFrameIfStale(long frameSerial)
|
||||
{
|
||||
_worldTransformFrames.ResetIfStale(frameSerial);
|
||||
}
|
||||
|
||||
private void ObserveOrdinaryTransformDemand(long frameSerial, uint instances)
|
||||
{
|
||||
if (_ordinaryTransformDemandFrameSerial != frameSerial)
|
||||
{
|
||||
_ordinaryTransformDemandFrameSerial = frameSerial;
|
||||
_ordinaryTransformDemandThisFrame = 0;
|
||||
}
|
||||
_ordinaryTransformDemandThisFrame = checked(
|
||||
_ordinaryTransformDemandThisFrame + instances);
|
||||
_ordinaryTransformDemandHighWater = Math.Max(
|
||||
_ordinaryTransformDemandHighWater,
|
||||
_ordinaryTransformDemandThisFrame);
|
||||
}
|
||||
|
||||
private void ResetWorldTransformFrame()
|
||||
{
|
||||
_worldTransformFrames.Reset();
|
||||
}
|
||||
|
||||
private IGpuFrame RequireRhiFrame()
|
||||
{
|
||||
// The same precondition ActivateNextDynamicBufferSet enforces on GL: a
|
||||
|
|
@ -593,6 +1053,11 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
totalMs += transparentMs;
|
||||
any = true;
|
||||
}
|
||||
if (_device.Timers.TryResolve(DetailTimerScope, out double detailMs))
|
||||
{
|
||||
totalMs += detailMs;
|
||||
any = true;
|
||||
}
|
||||
if (!any)
|
||||
return;
|
||||
|
||||
|
|
@ -611,6 +1076,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
// there is one set and disposing it twice would be a double free.
|
||||
if (!ReferenceEquals(offscreen, backbuffer))
|
||||
DisposeMeshPipelineSet(offscreen);
|
||||
DisposeDirectionalShadowReceiverPipelines();
|
||||
}
|
||||
|
||||
private static void DisposeMeshPipelineSet(MeshPipelineSet? pipelines)
|
||||
|
|
@ -622,6 +1088,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
pipelines.AlphaBlend.Dispose();
|
||||
pipelines.AlphaAdditive.Dispose();
|
||||
pipelines.AlphaInverse.Dispose();
|
||||
pipelines.RetailDetail.Dispose();
|
||||
pipelines.RetailDetailTransparent.Dispose();
|
||||
}
|
||||
|
||||
private sealed class NullRhiTimerScope : IDisposable
|
||||
|
|
|
|||
|
|
@ -505,6 +505,11 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
// Mechanically a clone of _clipSlotData.
|
||||
private uint[] _indoorData = new uint[256];
|
||||
|
||||
// #226: per-instance retail detail category (binding=9), parallel to the
|
||||
// transform buffer. 1 = building shell; 0 = ordinary object. Landscape
|
||||
// and generic objects are intentionally never enabled by the retail caller.
|
||||
private uint[] _detailCategoryData = new uint[256];
|
||||
|
||||
// #188: per-instance opacity multiplier (binding=7), one float per
|
||||
// instance, parallel to the instance data. 1.0 = unmodified (the dat's own
|
||||
// material/texture alpha, untouched); < 1.0 multiplies the shader's
|
||||
|
|
@ -539,6 +544,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
// (ParentCellId is an EnvCell). Appended to InstanceGroup.IndoorFlags in
|
||||
// AppendCurrentLightSet; uploaded as binding=6 instanceIndoor[].
|
||||
private bool _currentEntityIndoor;
|
||||
private bool _currentEntityBuildingDetail;
|
||||
private Vector2 _currentEntitySelectionLighting = new(0f, 1f);
|
||||
|
||||
// Phase U.3: the SHARED per-cell clip-region SSBO (binding=2) id, owned by
|
||||
|
|
@ -593,13 +599,13 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
// every existing CPU writer's offsets are unchanged (see
|
||||
// GpuBindingModel.GpuBatchDataStrideBytes). TextureIndex used to be a
|
||||
// 64-bit ulong TextureHandle (an ARB_bindless_texture handle, uvec2 in
|
||||
// GLSL); it is now a slot into the binding=9 handle table
|
||||
// GLSL); it is now a slot into the device texture table
|
||||
// (mesh_modern.vert's BatchData.textureIndex / ACDREAM_TEXTURE_HANDLE),
|
||||
// which is why the struct only needs 4-byte (not 8-byte) packing now.
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
private struct BatchData
|
||||
{
|
||||
public uint TextureIndex; // slot into the binding=9 handle table
|
||||
public uint TextureIndex; // slot into the device texture table
|
||||
public uint Reserved; // pad — keeps TextureLayer/Flags at offsets 8/12
|
||||
public uint TextureLayer;
|
||||
public uint Flags;
|
||||
|
|
@ -611,6 +617,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
uint ClipSlot,
|
||||
InstanceLightSet Lights,
|
||||
uint Indoor,
|
||||
uint DetailCategory,
|
||||
float Opacity,
|
||||
Vector2 SelectionLighting);
|
||||
|
||||
|
|
@ -679,6 +686,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
+ (long)_clipSlotData.Length * sizeof(uint)
|
||||
+ (long)_lightSetData.Length * sizeof(int)
|
||||
+ (long)_indoorData.Length * sizeof(uint)
|
||||
+ (long)_detailCategoryData.Length * sizeof(uint)
|
||||
+ (long)_alphaData.Length * sizeof(float)
|
||||
+ (long)_selectionLightingData.Length * Unsafe.SizeOf<Vector2>()
|
||||
+ (long)_batchData.Length * Unsafe.SizeOf<BatchData>()
|
||||
|
|
@ -795,6 +803,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
public void BeginFrame(int frameSlot)
|
||||
{
|
||||
_ = frameSlot;
|
||||
ResetWorldTransformFrame();
|
||||
if (_groupFrame == long.MaxValue)
|
||||
throw new InvalidOperationException("Instance-group frame identity was exhausted.");
|
||||
|
||||
|
|
@ -1619,6 +1628,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
// is constant across the entity's parts/tuples), by the entity's
|
||||
// bounding sphere — camera-INDEPENDENT (minimize_object_lighting).
|
||||
ComputeEntityLightSet(entity);
|
||||
_currentEntityBuildingDetail = entity.IsBuildingShell;
|
||||
_currentEntitySelectionLighting =
|
||||
_selectionLighting?.TryGetLighting(
|
||||
entity.ServerGuid,
|
||||
|
|
@ -2107,6 +2117,9 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
if (_indoorData.Length < immediateInstances)
|
||||
_indoorData = new uint[immediateInstances + 256];
|
||||
|
||||
if (_detailCategoryData.Length < immediateInstances)
|
||||
_detailCategoryData = new uint[immediateInstances + 256];
|
||||
|
||||
// #188: per-instance opacity buffer, one float per instance, parallel to
|
||||
// _clipSlotData / _instanceData. Grown on demand like the others.
|
||||
if (_alphaData.Length < immediateInstances)
|
||||
|
|
@ -2336,6 +2349,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
_lightSetData,
|
||||
cursor * LightManager.MaxLightsPerObject);
|
||||
_indoorData[cursor] = group.IndoorFlags[i];
|
||||
_detailCategoryData[cursor] = group.DetailCategories[i];
|
||||
_alphaData[cursor] = group.Opacities[i];
|
||||
_selectionLightingData[cursor] = group.SelectionLighting[i];
|
||||
cursor++;
|
||||
|
|
@ -2623,6 +2637,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
hash.Add(lights[lightIndex]);
|
||||
}
|
||||
hash.Add(group.IndoorFlags[index]);
|
||||
hash.Add(group.DetailCategories[index]);
|
||||
hash.Add(group.Opacities[index]);
|
||||
hash.Add(group.SelectionLighting[index]);
|
||||
}
|
||||
|
|
@ -2679,6 +2694,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
group.Slots[i],
|
||||
group.LightSets[i],
|
||||
group.IndoorFlags[i],
|
||||
group.DetailCategories[i],
|
||||
group.Opacities[i],
|
||||
group.SelectionLighting[i]));
|
||||
queue.Submit(
|
||||
|
|
@ -2721,6 +2737,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
WriteMatrix(_instanceData, i * 16, entry.Model);
|
||||
_clipSlotData[i] = entry.ClipSlot;
|
||||
_indoorData[i] = entry.Indoor;
|
||||
_detailCategoryData[i] = entry.DetailCategory;
|
||||
_alphaData[i] = entry.Opacity;
|
||||
_selectionLightingData[i] = entry.SelectionLighting;
|
||||
int lightOffset = i * LightManager.MaxLightsPerObject;
|
||||
|
|
@ -2732,7 +2749,11 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
// Campaign V slice V2: table slot, not the raw handle.
|
||||
TextureIndex = key.TextureSlot.Index,
|
||||
TextureLayer = key.TextureLayer,
|
||||
Flags = 0,
|
||||
// DrawMesh invokes RenderMeshSubset with detail enabled for
|
||||
// every built-mesh material subset while curr_detail_surface
|
||||
// is installed. The per-instance category still rejects
|
||||
// ordinary objects in mesh_detail.
|
||||
Flags = 1,
|
||||
};
|
||||
_indirectCommands[i] = new DrawElementsIndirectCommand
|
||||
{
|
||||
|
|
@ -2781,6 +2802,8 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
_clipSlotData = new uint[count + 256];
|
||||
if (_indoorData.Length < count)
|
||||
_indoorData = new uint[count + 256];
|
||||
if (_detailCategoryData.Length < count)
|
||||
_detailCategoryData = new uint[count + 256];
|
||||
if (_alphaData.Length < count)
|
||||
_alphaData = new float[count + 256];
|
||||
if (_selectionLightingData.Length < count)
|
||||
|
|
@ -2820,6 +2843,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
int bytesPerUnit = checked(
|
||||
16 * sizeof(float)
|
||||
+ sizeof(uint)
|
||||
+ sizeof(uint)
|
||||
+ LightManager.MaxLightsPerObject * sizeof(int)
|
||||
+ sizeof(uint)
|
||||
+ sizeof(float)
|
||||
|
|
@ -2844,6 +2868,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
_lightSetData = new int[
|
||||
checked(targetCapacity * LightManager.MaxLightsPerObject)];
|
||||
_indoorData = new uint[targetCapacity];
|
||||
_detailCategoryData = new uint[targetCapacity];
|
||||
_alphaData = new float[targetCapacity];
|
||||
_selectionLightingData = new Vector2[targetCapacity];
|
||||
_batchData = new BatchData[targetCapacity];
|
||||
|
|
@ -3271,6 +3296,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
{
|
||||
grp.LightSets.Add(_currentEntityLightSet);
|
||||
grp.IndoorFlags.Add(_currentEntityIndoor ? 1u : 0u); // #142, parallel to the light block
|
||||
grp.DetailCategories.Add(_currentEntityBuildingDetail ? 1u : 0u); // #226
|
||||
}
|
||||
|
||||
private bool ClassifyBatches(
|
||||
|
|
@ -3527,8 +3553,8 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
|
||||
/// <summary>
|
||||
/// Public view of the per-group inputs to <see cref="BuildIndirectArrays"/> — used in tests.
|
||||
/// Campaign V slice V2: <c>TextureIndex</c> is a slot into the binding=9
|
||||
/// handle table (was a raw 64-bit bindless <c>TextureHandle</c>).
|
||||
/// Campaign V slice V2: <c>TextureIndex</c> is a slot into the device's
|
||||
/// texture table (was a raw 64-bit bindless <c>TextureHandle</c>).
|
||||
/// </summary>
|
||||
public readonly record struct IndirectGroupInput(
|
||||
int IndexCount,
|
||||
|
|
@ -3602,7 +3628,10 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
TextureIndex = g.TextureIndex,
|
||||
Reserved = 0,
|
||||
TextureLayer = g.TextureLayer,
|
||||
Flags = 0,
|
||||
// #226: this is the built-mesh path. Retail DrawMesh passes
|
||||
// curr_detail_surface through RenderMeshSubset for opaque,
|
||||
// ClipMap, alpha, additive and inverse-alpha subsets alike.
|
||||
Flags = 1u,
|
||||
};
|
||||
|
||||
if (IsOpaque(g.Translucency))
|
||||
|
|
@ -3631,6 +3660,85 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
/// </summary>
|
||||
public static bool IsOpaquePublic(TranslucencyKind t) => IsOpaque(t);
|
||||
|
||||
internal readonly record struct DetailCommandRun(
|
||||
int FirstCommand,
|
||||
int CommandCount);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the next consecutive run containing at least one building instance
|
||||
/// per command. Commands with no building instances are never submitted to
|
||||
/// the detail pipeline; a mixed command remains eligible and relies on the
|
||||
/// shader's per-instance category filter.
|
||||
/// </summary>
|
||||
internal static bool TryGetNextDetailCommandRun(
|
||||
ReadOnlySpan<DrawElementsIndirectCommand> commands,
|
||||
ReadOnlySpan<uint> detailCategories,
|
||||
int searchStart,
|
||||
int exclusiveEnd,
|
||||
out DetailCommandRun run)
|
||||
{
|
||||
if (searchStart < 0
|
||||
|| exclusiveEnd < searchStart
|
||||
|| exclusiveEnd > commands.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(searchStart),
|
||||
"The requested detail-command search range is invalid.");
|
||||
}
|
||||
|
||||
int first = searchStart;
|
||||
while (first < exclusiveEnd
|
||||
&& !CommandContainsDetailCategory(
|
||||
commands[first],
|
||||
detailCategories))
|
||||
{
|
||||
first++;
|
||||
}
|
||||
if (first == exclusiveEnd)
|
||||
{
|
||||
run = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
int end = first + 1;
|
||||
while (end < exclusiveEnd
|
||||
&& CommandContainsDetailCategory(
|
||||
commands[end],
|
||||
detailCategories))
|
||||
{
|
||||
end++;
|
||||
}
|
||||
run = new DetailCommandRun(first, end - first);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether an indirect command contains at least one building
|
||||
/// instance. Retail's detail fallback redraws the exact built-mesh subset
|
||||
/// immediately after its transparent base subset; this predicate lets the
|
||||
/// Vulkan arm preserve that adjacency without replaying ordinary objects.
|
||||
/// </summary>
|
||||
internal static bool CommandContainsDetailCategory(
|
||||
DrawElementsIndirectCommand command,
|
||||
ReadOnlySpan<uint> detailCategories)
|
||||
{
|
||||
ulong first = command.BaseInstance;
|
||||
ulong end = first + command.InstanceCount;
|
||||
if (end > (ulong)detailCategories.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(command),
|
||||
"The indirect instance range exceeds the detail-category buffer.");
|
||||
}
|
||||
|
||||
for (ulong index = first; index < end; index++)
|
||||
{
|
||||
if (detailCategories[(int)index] != 0u)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsOpaque(TranslucencyKind t)
|
||||
=> t == TranslucencyKind.Opaque || t == TranslucencyKind.ClipMap;
|
||||
|
||||
|
|
@ -3724,6 +3832,10 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
// cursor as Matrices, so binding=6 instanceIndoor[] tracks binding=0.
|
||||
public readonly List<uint> IndoorFlags = new();
|
||||
|
||||
// #226: 1 for a building-shell instance, 0 otherwise. Parallel to
|
||||
// Matrices and uploaded at storage binding 9 for the detail replay.
|
||||
public readonly List<uint> DetailCategories = new();
|
||||
|
||||
// #188: per-instance opacity multiplier, parallel to Matrices.
|
||||
// Opacities[i] is 1.0=unmodified, or <1.0 while a TransparentPartHook
|
||||
// fade is in flight for the instance whose matrix is Matrices[i]. At
|
||||
|
|
@ -3753,6 +3865,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
Slots.Clear();
|
||||
LightSets.Clear();
|
||||
IndoorFlags.Clear();
|
||||
DetailCategories.Clear();
|
||||
Opacities.Clear();
|
||||
SelectionLighting.Clear();
|
||||
}
|
||||
|
|
@ -3766,6 +3879,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
Slots.TrimExcess();
|
||||
LightSets.TrimExcess();
|
||||
IndoorFlags.TrimExcess();
|
||||
DetailCategories.TrimExcess();
|
||||
Opacities.TrimExcess();
|
||||
SelectionLighting.TrimExcess();
|
||||
}
|
||||
|
|
|
|||
264
src/AcDream.App/Rendering/Wb/WorldTransformFrameArena.cs
Normal file
264
src/AcDream.App/Rendering/Wb/WorldTransformFrameArena.cs
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Stable non-ref view of the one pack-on world-transform allocation for a
|
||||
/// render frame. Shadow and ordinary world commands address different matrix
|
||||
/// ranges through <c>BaseInstance</c>, but bind this exact buffer, offset, and
|
||||
/// range. The matrices remain the already-published N.5 values; this type owns
|
||||
/// no animation or scene projection.
|
||||
/// </summary>
|
||||
internal readonly record struct WorldTransformFrameSlice(
|
||||
long FrameSerial,
|
||||
IGpuBuffer Buffer,
|
||||
uint BaseOffsetBytes,
|
||||
uint BindingSizeBytes,
|
||||
uint FirstInstance,
|
||||
uint InstanceCount)
|
||||
{
|
||||
internal bool IsValidFor(IGpuFrame frame) =>
|
||||
FrameSerial == frame.Serial
|
||||
&& Buffer is not null
|
||||
&& BindingSizeBytes >= checked(
|
||||
(FirstInstance + InstanceCount) * WorldTransformCapacityPolicy.MatrixBytes)
|
||||
&& BindingSizeBytes % WorldTransformCapacityPolicy.MatrixBytes == 0u
|
||||
&& BaseOffsetBytes % WorldTransformCapacityPolicy.MatrixBytes == 0u
|
||||
&& checked((long)BaseOffsetBytes + BindingSizeBytes) <= Buffer.SizeBytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Demand-sized policy for the one enhanced-frame pose address space. The
|
||||
/// 68,395-matrix connected dense row is the bootstrap observation, not a hard
|
||||
/// ceiling: allocations grow in 64 KiB pages and stop only at the adapter's
|
||||
/// probed <c>maxStorageBufferRange</c>. Vulkan guarantees that limit is at least
|
||||
/// 128 MiB (2,097,152 matrices).
|
||||
/// </summary>
|
||||
internal static class WorldTransformCapacityPolicy
|
||||
{
|
||||
internal const uint MatrixBytes = 64u;
|
||||
internal const uint ConnectedDenseBootstrapInstances = 68_395u;
|
||||
internal const uint AllocationQuantumBytes = 64u * 1024u;
|
||||
internal const uint InitialBindingSizeBytes =
|
||||
((ConnectedDenseBootstrapInstances * MatrixBytes
|
||||
+ AllocationQuantumBytes - 1u) / AllocationQuantumBytes)
|
||||
* AllocationQuantumBytes;
|
||||
internal const uint VulkanGuaranteedMaxStorageBufferRangeBytes =
|
||||
128u * 1024u * 1024u;
|
||||
|
||||
internal static uint ResolveBindingSizeBytes(
|
||||
uint requiredInstances,
|
||||
uint maxStorageBufferRangeBytes)
|
||||
{
|
||||
uint maximum = maxStorageBufferRangeBytes
|
||||
- (maxStorageBufferRangeBytes % MatrixBytes);
|
||||
ulong requiredBytes = (ulong)requiredInstances * MatrixBytes;
|
||||
if (maximum < MatrixBytes || requiredBytes > maximum)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"The enhanced frame needs {requiredInstances:N0} world matrices "
|
||||
+ $"({requiredBytes:N0} bytes), but this adapter exposes only "
|
||||
+ $"{maximum:N0} bytes through one storage-buffer binding. "
|
||||
+ "The pack will fail safe rather than split the authoritative pose buffer.");
|
||||
}
|
||||
|
||||
ulong targetBytes = Math.Max(
|
||||
requiredBytes,
|
||||
(ulong)ConnectedDenseBootstrapInstances * MatrixBytes);
|
||||
ulong growthBytes = checked(
|
||||
((targetBytes + AllocationQuantumBytes - 1u)
|
||||
/ AllocationQuantumBytes)
|
||||
* AllocationQuantumBytes);
|
||||
|
||||
// An implementation is allowed to expose a non-page-aligned maximum.
|
||||
// Use all of it when it is below the preferred growth page, provided
|
||||
// the current demand still fits.
|
||||
return (uint)Math.Min(growthBytes, maximum);
|
||||
}
|
||||
|
||||
internal static void ValidateBindingSizeBytes(
|
||||
uint bindingSizeBytes,
|
||||
uint requiredInstances,
|
||||
uint maxStorageBufferRangeBytes)
|
||||
{
|
||||
ulong requiredBytes = (ulong)requiredInstances * MatrixBytes;
|
||||
if (bindingSizeBytes < requiredBytes
|
||||
|| bindingSizeBytes % MatrixBytes != 0u
|
||||
|| bindingSizeBytes > maxStorageBufferRangeBytes)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(bindingSizeBytes),
|
||||
bindingSizeBytes,
|
||||
$"A shared world-transform binding must be matrix-aligned, contain "
|
||||
+ $"all {requiredInstances:N0} matrices, and not exceed the adapter's "
|
||||
+ $"{maxStorageBufferRangeBytes:N0}-byte storage range.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frame-local address allocator over the pack-on N.5 transform block. The
|
||||
/// backing buffer may be a frame-ring allocation or the active pack's retained
|
||||
/// flight-slot arena. It publishes the prepared shadow prefix once and then
|
||||
/// only appends already-built ordinary world matrices. It deliberately has no
|
||||
/// scene, animation, or transform-derivation dependency.
|
||||
/// </summary>
|
||||
internal sealed class WorldTransformFrameArena
|
||||
{
|
||||
private WorldTransformFrameSlice _allocation;
|
||||
private uint _usedBytes;
|
||||
|
||||
internal bool IsActive => _allocation.Buffer is not null;
|
||||
|
||||
internal uint UsedInstances => _usedBytes / 64u;
|
||||
|
||||
internal WorldTransformFrameSlice Begin(
|
||||
IGpuFrame frame,
|
||||
ReadOnlySpan<Matrix4x4> transforms) => Begin(
|
||||
frame,
|
||||
transforms,
|
||||
WorldTransformCapacityPolicy.ResolveBindingSizeBytes(
|
||||
checked((uint)transforms.Length),
|
||||
WorldTransformCapacityPolicy.VulkanGuaranteedMaxStorageBufferRangeBytes));
|
||||
|
||||
internal WorldTransformFrameSlice Begin(
|
||||
IGpuFrame frame,
|
||||
ReadOnlySpan<Matrix4x4> transforms,
|
||||
uint bindingSizeBytes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
ResetIfStale(frame.Serial);
|
||||
if (IsActive)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The directional-shadow transform frame was already published.");
|
||||
}
|
||||
|
||||
uint byteCount = checked((uint)(transforms.Length * WorldTransformCapacityPolicy.MatrixBytes));
|
||||
if (bindingSizeBytes < byteCount
|
||||
|| bindingSizeBytes % WorldTransformCapacityPolicy.MatrixBytes != 0u)
|
||||
throw new ArgumentOutOfRangeException(nameof(bindingSizeBytes));
|
||||
|
||||
GpuRingAllocation allocation = frame.AllocateRing(
|
||||
checked((int)bindingSizeBytes),
|
||||
GpuRingUsage.Storage);
|
||||
if (!transforms.IsEmpty)
|
||||
MemoryMarshal.AsBytes(transforms).CopyTo(allocation.Data);
|
||||
|
||||
_allocation = new WorldTransformFrameSlice(
|
||||
frame.Serial,
|
||||
allocation.Buffer,
|
||||
allocation.OffsetBytes,
|
||||
bindingSizeBytes,
|
||||
FirstInstance: 0,
|
||||
checked((uint)transforms.Length));
|
||||
_usedBytes = byteCount;
|
||||
return _allocation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates a pack-owned flight-slot arena whose shadow prefix was already
|
||||
/// published. Ordinary N.5 matrices append after that prefix and bind this
|
||||
/// exact buffer/range, preserving the one authoritative pose SSBO without
|
||||
/// copying stable shadow matrices through the frame ring.
|
||||
/// </summary>
|
||||
internal WorldTransformFrameSlice BeginRetained(
|
||||
IGpuFrame frame,
|
||||
in WorldTransformFrameSlice shadowPrefix)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
ResetIfStale(frame.Serial);
|
||||
if (IsActive)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The directional-shadow transform frame was already published.");
|
||||
}
|
||||
if (!shadowPrefix.IsValidFor(frame)
|
||||
|| shadowPrefix.FirstInstance != 0
|
||||
|| shadowPrefix.Buffer.Residency != GpuMemoryResidency.HostWritable
|
||||
|| !shadowPrefix.Buffer.HostWritesAreCoherent
|
||||
|| !shadowPrefix.Buffer.Usage.HasFlag(GpuBufferUsage.Storage))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The retained shadow prefix must be this frame's host-writable "
|
||||
+ "matrix-aligned storage arena at base instance zero.",
|
||||
nameof(shadowPrefix));
|
||||
}
|
||||
|
||||
uint byteCount = checked(
|
||||
shadowPrefix.InstanceCount * WorldTransformCapacityPolicy.MatrixBytes);
|
||||
if (byteCount > shadowPrefix.BindingSizeBytes)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The retained shadow prefix exceeds its storage binding.",
|
||||
nameof(shadowPrefix));
|
||||
}
|
||||
|
||||
_allocation = shadowPrefix;
|
||||
_usedBytes = byteCount;
|
||||
return _allocation;
|
||||
}
|
||||
|
||||
internal WorldTransformFrameSlice Append(
|
||||
IGpuFrame frame,
|
||||
ReadOnlySpan<Matrix4x4> transforms)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
ResetIfStale(frame.Serial);
|
||||
if (!IsActive)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The shared world-transform frame has not been published.");
|
||||
}
|
||||
|
||||
uint byteCount = checked(
|
||||
(uint)(transforms.Length * WorldTransformCapacityPolicy.MatrixBytes));
|
||||
uint start = _usedBytes;
|
||||
uint end = checked(start + byteCount);
|
||||
if (end > _allocation.BindingSizeBytes)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The enhanced frame needs {end / WorldTransformCapacityPolicy.MatrixBytes:N0} world matrices; "
|
||||
+ $"this frame's shared transform binding contains "
|
||||
+ $"{_allocation.BindingSizeBytes / WorldTransformCapacityPolicy.MatrixBytes:N0}. "
|
||||
+ "The pack will fail safe rather than bind a second pose buffer.");
|
||||
}
|
||||
|
||||
if (!transforms.IsEmpty)
|
||||
{
|
||||
_allocation.Buffer.Upload(
|
||||
checked((long)_allocation.BaseOffsetBytes + start),
|
||||
MemoryMarshal.AsBytes(transforms));
|
||||
}
|
||||
_usedBytes = end;
|
||||
return _allocation with
|
||||
{
|
||||
FirstInstance = start / WorldTransformCapacityPolicy.MatrixBytes,
|
||||
InstanceCount = checked((uint)transforms.Length),
|
||||
};
|
||||
}
|
||||
|
||||
internal bool IsActiveFor(long frameSerial) =>
|
||||
IsActive && _allocation.FrameSerial == frameSerial;
|
||||
|
||||
internal void Cancel(IGpuFrame frame)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
if (IsActiveFor(frame.Serial))
|
||||
Reset();
|
||||
}
|
||||
|
||||
internal void ResetIfStale(long frameSerial)
|
||||
{
|
||||
if (IsActive && _allocation.FrameSerial != frameSerial)
|
||||
Reset();
|
||||
}
|
||||
|
||||
internal void Reset()
|
||||
{
|
||||
_allocation = default;
|
||||
_usedBytes = 0;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue