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.
1116 lines
46 KiB
C#
1116 lines
46 KiB
C#
using System.Numerics;
|
||
using System.Runtime.InteropServices;
|
||
using AcDream.App.Rendering.Gpu;
|
||
using AcDream.App.Rendering.Residency;
|
||
using AcDream.App.Rendering.Selection;
|
||
using AcDream.Core.Lighting;
|
||
using AcDream.Core.Meshing;
|
||
using AcDream.Core.Rendering;
|
||
using DatReaderWriter.Enums;
|
||
|
||
namespace AcDream.App.Rendering.Wb;
|
||
|
||
/// <summary>
|
||
/// Campaign V slice V6j: the world entity dispatcher's RHI submission arm.
|
||
///
|
||
/// <para>V4c's content, re-landed as a second arm rather than a replacement —
|
||
/// §5.5.6 selected that shape after NVIDIA rendered the V4c binary 10/10 and
|
||
/// 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. 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
|
||
/// alpha-to-coverage does nothing at one sample.</para>
|
||
/// </summary>
|
||
public sealed unsafe partial class WbDrawDispatcher
|
||
{
|
||
private readonly IGpuDevice? _device;
|
||
private readonly ICurrentGpuFrameSource? _frames;
|
||
private readonly IWorldPassScope? _scope;
|
||
|
||
/// <summary>
|
||
/// The five retail material/blend 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
|
||
/// this dispatcher draws in two passes with different counts — the
|
||
/// multisampled backbuffer world pass, and the single-sampled offscreen
|
||
/// paperdoll/appraisal target, which the contract fixes at one sample. Plan
|
||
/// §5.5.16 defect 3 named exactly this as the reason those viewports could
|
||
/// not exist on the Vulkan arm, and the answer is the same shape §5.5.8 gave
|
||
/// the depth-format problem: materialise both, select at bind time from what
|
||
/// the live pass actually is. Both are built at startup against the persisted
|
||
/// cache, so no frame ever compiles one.</para>
|
||
/// </summary>
|
||
internal sealed record MeshPipelineSet(
|
||
int SampleCount,
|
||
IGpuPipeline Opaque,
|
||
IGpuPipeline OpaqueAlphaToCoverage,
|
||
IGpuPipeline AlphaBlend,
|
||
IGpuPipeline AlphaAdditive,
|
||
IGpuPipeline AlphaInverse);
|
||
|
||
private MeshPipelineSet? _backbufferPipelines;
|
||
private MeshPipelineSet? _offscreenPipelines;
|
||
|
||
private const string OpaqueTimerScope = "wb-entities-opaque";
|
||
private const string TransparentTimerScope = "wb-entities-transparent";
|
||
|
||
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
|
||
/// alpha payload is written once and bound many times, so the allocation's
|
||
/// <c>ref struct</c> lifetime is escaped through these ordinary values.
|
||
/// </summary>
|
||
private readonly record struct RhiSection(
|
||
IGpuBuffer? Buffer,
|
||
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;
|
||
private RhiSection _alphaGlobalLights;
|
||
private RhiSection _alphaLightSets;
|
||
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>: five base pipelines compile <c>mesh_modern</c>;
|
||
/// batch data already carries the device's own
|
||
/// <c>GpuTextureSlot</c> (V4t) rather than a bindless handle.
|
||
/// </summary>
|
||
internal WbDrawDispatcher(
|
||
IGpuDevice device,
|
||
ICurrentGpuFrameSource frames,
|
||
IWorldPassScope scope,
|
||
TextureCache textures,
|
||
WbMeshAdapter meshAdapter,
|
||
EntitySpawnAdapter entitySpawnAdapter,
|
||
EntityClassificationCache classificationCache,
|
||
AcDream.Core.Rendering.TranslucencyFadeManager translucencyFades,
|
||
IRetailSelectionRenderSink? selectionSink = null,
|
||
RetailAlphaQueue? alphaQueue = null,
|
||
long? alphaScratchBudgetBytes = null,
|
||
TerrainAtlas.RetailDetailTextureBinding buildingDetail = default,
|
||
Func<bool>? buildingDetailEnabled = null,
|
||
Func<uint, float>? hierarchicalTranslucency = null)
|
||
{
|
||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
|
||
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
|
||
_textures = textures ?? throw new ArgumentNullException(nameof(textures));
|
||
_meshAdapter = meshAdapter ?? throw new ArgumentNullException(nameof(meshAdapter));
|
||
_entitySpawnAdapter = entitySpawnAdapter
|
||
?? throw new ArgumentNullException(nameof(entitySpawnAdapter));
|
||
_cache = classificationCache
|
||
?? throw new ArgumentNullException(nameof(classificationCache));
|
||
_translucencyFades = translucencyFades
|
||
?? throw new ArgumentNullException(nameof(translucencyFades));
|
||
_selectionSink = selectionSink;
|
||
_selectionLighting = selectionSink as IRetailSelectionLightingSource;
|
||
_alphaQueue = alphaQueue;
|
||
_hierarchicalTranslucency = hierarchicalTranslucency;
|
||
_alphaSource = new AlphaDrawSource(this);
|
||
_buildingDetail = buildingDetail;
|
||
_buildingDetailEnabled = buildingDetailEnabled ?? DisableDetailTextures;
|
||
long scratchBudget = alphaScratchBudgetBytes
|
||
?? AlphaScratchBudgetProfile.Create(
|
||
ResidencyBudgetOptions.Default.AlphaScratchBytes)
|
||
.DispatcherBytes;
|
||
_alphaScratchPolicy = new RetainedScratchCapacityPolicy(scratchBudget);
|
||
|
||
int samples = scope.SampleCount;
|
||
try
|
||
{
|
||
_backbufferPipelines = CreateMeshPipelineSet(device, samples);
|
||
// One sample is what an IGpuRenderTarget is by contract, so a second
|
||
// set only exists when the backbuffer is multisampled.
|
||
_offscreenPipelines = samples == 1
|
||
? _backbufferPipelines
|
||
: CreateMeshPipelineSet(device, 1);
|
||
}
|
||
catch
|
||
{
|
||
DisposeRhiResources();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
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";
|
||
var created = new List<IGpuPipeline>(5);
|
||
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)));
|
||
}
|
||
catch
|
||
{
|
||
for (int i = created.Count - 1; i >= 0; i--)
|
||
created[i].Dispose();
|
||
throw;
|
||
}
|
||
|
||
IGpuPipeline Track(IGpuPipeline pipeline)
|
||
{
|
||
created.Add(pipeline);
|
||
return pipeline;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// The pipeline set whose sample count matches the pass being recorded into.
|
||
/// Taken from the live pass rather than from the scope, because the offscreen
|
||
/// viewport borrows the scope with a pass of its own.
|
||
/// </summary>
|
||
private MeshPipelineSet PipelinesFor(IGpuPassEncoder encoder) =>
|
||
encoder.Pass.SampleCount > 1
|
||
? _backbufferPipelines!
|
||
: _offscreenPipelines!;
|
||
|
||
/// <summary>
|
||
/// The imperative <c>Enable/Disable/BlendFunc/DepthMask</c> brackets became
|
||
/// pipeline variants: opaque, opaque with alpha-to-coverage, and the three
|
||
/// retail blends. Cull mode and front face stay dynamic per MDI run, exactly
|
||
/// where <c>ApplyCullMode</c> sets them, because core Vulkan 1.3 makes those
|
||
/// dynamic and blend and alpha-to-coverage not.
|
||
///
|
||
/// <para>Depth compare is <see cref="AcDream.App.Rendering.WorldDepthContract.WorldCompare"/>
|
||
/// (<c>Less</c>), not the contract's <c>LessOrEqual</c> default — see that
|
||
/// type for the full citation. The short version: the world frame runs
|
||
/// under <c>GL_LESS</c> and this renderer never called <c>glDepthFunc</c>,
|
||
/// so it inherited it. Baking <c>LessOrEqual</c> would change which of two
|
||
/// coplanar retail surfaces wins.</para>
|
||
/// </summary>
|
||
private static IGpuPipeline CreateMeshPipeline(
|
||
IGpuDevice device,
|
||
string name,
|
||
GpuBlendMode blend,
|
||
bool depthWrite,
|
||
bool alphaToCoverage,
|
||
int sampleCount,
|
||
string shaderName = "mesh_modern",
|
||
GpuShaderSet? shaders = null,
|
||
GpuCompareOp depthCompare = AcDream.App.Rendering.WorldDepthContract.WorldCompare,
|
||
bool usesRenderPackShaderAbi = false) =>
|
||
device.CreatePipeline(new GpuPipelineDescription
|
||
{
|
||
Name = name,
|
||
Shaders = shaders ?? new GpuShaderSet(shaderName),
|
||
VertexLayout = GpuVertexLayout.WorldMesh,
|
||
Topology = GpuPrimitiveTopology.TriangleList,
|
||
Blend = blend,
|
||
Depth = new GpuDepthState(Test: true, Write: depthWrite, depthCompare),
|
||
Cull = GpuCullMode.Back,
|
||
FrontFace = GpuFrontFace.Clockwise,
|
||
AlphaToCoverage = alphaToCoverage,
|
||
ColorWrite = true,
|
||
UsesRenderPackShaderAbi = usesRenderPackShaderAbi,
|
||
SampleCount = sampleCount,
|
||
});
|
||
|
||
/// <summary>
|
||
/// Records the opaque and transparent multi-draws into the borrowed world
|
||
/// pass. Phases 1–4 above are untouched — the bucketing, the sorts, the
|
||
/// indirect-command array and every retail fidelity decision are the same
|
||
/// CPU code on both arms; only where the bytes land differs.
|
||
/// </summary>
|
||
private void SubmitRhi(
|
||
Matrix4x4 viewProjection,
|
||
int immediateInstances,
|
||
int totalDraws,
|
||
bool diag)
|
||
{
|
||
IWorldPassScope scope = _scope!;
|
||
IGpuPassEncoder encoder = scope.RequireEncoder();
|
||
IGpuFrame frame = RequireRhiFrame();
|
||
GlobalMeshBuffer mesh = _meshAdapter.MeshManager?.GlobalBuffer
|
||
?? throw new InvalidOperationException("The shared mesh arena is not published.");
|
||
|
||
var pushConstants = new GpuPushConstants
|
||
{
|
||
ViewProjection = viewProjection,
|
||
DrawIdOffset = 0,
|
||
LightingMode = 0,
|
||
RenderPass = 0,
|
||
LightDebug = RenderingDiagnostics.LightDebugMode,
|
||
TextureIndexA = 0,
|
||
TextureIndexB = 0,
|
||
ParamA = 0f,
|
||
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,
|
||
frame,
|
||
out DirectionalShadowFrameBinding shadowBinding);
|
||
BindPipelineWithMesh(
|
||
encoder,
|
||
AlphaToCoverage ? pipelines.OpaqueAlphaToCoverage : pipelines.Opaque,
|
||
mesh);
|
||
encoder.SetPushConstants(in pushConstants);
|
||
BindDirectionalShadowReceiver(encoder, in shadowBinding);
|
||
|
||
BindSection(
|
||
encoder,
|
||
GpuBindingModel.StorageInstances,
|
||
instanceTransforms);
|
||
BindRingSection<BatchData>(
|
||
encoder, frame, GpuBindingModel.StorageBatches,
|
||
_batchData.AsSpan(0, totalDraws));
|
||
BindRingSection<uint>(
|
||
encoder, frame, GpuBindingModel.StorageClipSlots,
|
||
_clipSlotData.AsSpan(0, immediateInstances));
|
||
BindGlobalLightsRhi(encoder, frame);
|
||
BindRingSection<int>(
|
||
encoder, frame, GpuBindingModel.StorageInstanceLightSets,
|
||
_lightSetData.AsSpan(0, immediateInstances * LightManager.MaxLightsPerObject));
|
||
BindRingSection<uint>(
|
||
encoder, frame, GpuBindingModel.StorageInstanceIndoor,
|
||
_indoorData.AsSpan(0, immediateInstances));
|
||
BindRingSection<float>(
|
||
encoder, frame, GpuBindingModel.StorageInstanceAlpha,
|
||
_alphaData.AsSpan(0, immediateInstances));
|
||
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 = WriteIndirectCommands(
|
||
frame,
|
||
_indirectCommands.AsSpan(0, totalDraws),
|
||
transformBaseInstance);
|
||
IGpuBuffer commandBuffer = commands.Buffer;
|
||
uint commandBase = commands.OffsetBytes;
|
||
ReadOnlySpan<uint> usedDetailCategories =
|
||
_detailCategoryData.AsSpan(0, immediateInstances);
|
||
bool detailEnabled = RetailDetailTextureContract.ShouldRender(
|
||
_buildingDetailEnabled(),
|
||
_buildingDetail)
|
||
&& _buildingDetail.Tiling != 0f;
|
||
|
||
// ── Phase 7: opaque pass ─────────────────────────────────────────────
|
||
if (_opaqueDrawCount > 0)
|
||
{
|
||
// Blend-off, depth-write-on and A.5 T20's alpha-to-coverage all come
|
||
// from the pipeline rather than an imperative bracket. Issue #52's
|
||
// per-pass batch offset is unchanged: the opaque section of Batches[]
|
||
// starts at index 0, and Vulkan's gl_DrawID resets per
|
||
// vkCmdDrawIndexedIndirect exactly as GL's does.
|
||
pushConstants.RenderPass = 0;
|
||
pushConstants.DrawIdOffset = 0;
|
||
encoder.SetPushConstants(in pushConstants);
|
||
using (BeginRhiTimer(encoder, diag, OpaqueTimerScope))
|
||
{
|
||
DrawDetailAwareRangeRhi(
|
||
encoder, ref pushConstants, commandBuffer, commandBase,
|
||
0, _opaqueDrawCount, usedDetailCategories, detailEnabled);
|
||
}
|
||
}
|
||
|
||
// ── Phase 8: transparent pass ────────────────────────────────────────
|
||
if (_transparentDrawCount > 0)
|
||
{
|
||
// 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.
|
||
pushConstants.RenderPass = 1;
|
||
pushConstants.DrawIdOffset = _opaqueDrawCount;
|
||
encoder.SetPushConstants(in pushConstants);
|
||
using (BeginRhiTimer(encoder, diag, TransparentTimerScope))
|
||
{
|
||
DrawImmediateTransparentRhi(
|
||
encoder,
|
||
mesh,
|
||
pipelines,
|
||
ref pushConstants,
|
||
commandBuffer,
|
||
commandBase,
|
||
usedDetailCategories,
|
||
detailEnabled);
|
||
}
|
||
}
|
||
|
||
SampleRhiTimers(diag);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Writes the prepared deferred-alpha payload into the frame ring once. The
|
||
/// sections survive as ordinary values so every later
|
||
/// <c>DrawPreparedAlphaBatch</c> binds the same bytes without recopying.
|
||
/// </summary>
|
||
private void PrepareRhiAlphaSections(int count)
|
||
{
|
||
_preparedAlphaInstanceCount = count;
|
||
IGpuFrame frame = RequireRhiFrame();
|
||
_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);
|
||
int uploadCount = lightCount > 0 ? lightCount : 1;
|
||
_alphaGlobalLights = WriteRingSection<float>(
|
||
frame,
|
||
_globalLightData.AsSpan(0, uploadCount * GlobalLightPacker.FloatsPerLight));
|
||
_alphaLightSets = WriteRingSection<int>(
|
||
frame,
|
||
_lightSetData.AsSpan(0, count * LightManager.MaxLightsPerObject));
|
||
_alphaIndoor = WriteRingSection<uint>(frame, _indoorData.AsSpan(0, count));
|
||
_alphaOpacity = WriteRingSection<float>(frame, _alphaData.AsSpan(0, count));
|
||
_alphaSelectionLighting = WriteRingSection<Vector2>(
|
||
frame,
|
||
_selectionLightingData.AsSpan(0, count));
|
||
_alphaDetailCategory = WriteRingSection<uint>(
|
||
frame,
|
||
_detailCategoryData.AsSpan(0, count));
|
||
GpuRingAllocation commands = WriteIndirectCommands(
|
||
frame,
|
||
_indirectCommands.AsSpan(0, count),
|
||
transformBaseInstance);
|
||
_alphaCommands = new RhiSection(
|
||
commands.Buffer,
|
||
commands.OffsetBytes,
|
||
checked((uint)(count * DrawCommandStride)));
|
||
}
|
||
|
||
/// <summary>
|
||
/// The push-constant/pipeline/storage-binding setup <see cref="DrawPreparedAlphaBatchRhi"/>'s
|
||
/// drain and <see cref="DrawImmediateAlphaInstanceRhi"/>'s single-
|
||
/// instance immediate draw (S4-c2 fix round 1, M3) share — the only
|
||
/// difference between the two call sites is <paramref name="viewProjection"/>
|
||
/// (the drain reads the deferred scope's own <c>_deferredAlphaViewProjection</c>;
|
||
/// an immediate draw uses whatever the CURRENT frame's viewProjection
|
||
/// is). The immediate site additionally arms detail on its original draw.
|
||
/// </summary>
|
||
private GpuPushConstants BindAlphaDrawState(
|
||
IGpuPassEncoder encoder,
|
||
IGpuFrame frame,
|
||
GlobalMeshBuffer mesh,
|
||
Matrix4x4 viewProjection,
|
||
out MeshPipelineSet pipelines)
|
||
{
|
||
var pushConstants = new GpuPushConstants
|
||
{
|
||
ViewProjection = viewProjection,
|
||
DrawIdOffset = 0,
|
||
LightingMode = 0,
|
||
RenderPass = 1,
|
||
LightDebug = RenderingDiagnostics.LightDebugMode,
|
||
TextureIndexA = 0,
|
||
TextureIndexB = _alphaTransformBaseInstance,
|
||
ParamA = 0f,
|
||
ParamB = 0f,
|
||
};
|
||
|
||
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);
|
||
BindSection(encoder, GpuBindingModel.StorageGlobalLights, _alphaGlobalLights);
|
||
BindSection(encoder, GpuBindingModel.StorageInstanceLightSets, _alphaLightSets);
|
||
BindSection(encoder, GpuBindingModel.StorageInstanceIndoor, _alphaIndoor);
|
||
BindSection(encoder, GpuBindingModel.StorageInstanceAlpha, _alphaOpacity);
|
||
BindSection(
|
||
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);
|
||
return pushConstants;
|
||
}
|
||
|
||
/// <summary>
|
||
/// S4-c2 fix round 1 (M3c): delayed replay is ALWAYS <c>detailEnabled=0</c>
|
||
/// (OH1 contract §4/§5 — <c>FlushAlphaList</c>'s replay calls
|
||
/// <c>RenderMeshSubset(..., 0, entry.clip, ...)</c>) because every
|
||
/// detail-eligible subset now takes Row 1 immediate instead
|
||
/// (<c>SubmitToAlphaQueue</c>'s <c>detailSurfaceActive</c> gate) and
|
||
/// never reaches this drain at all — so this loop batches purely by
|
||
/// blend kind, with no per-command detail-category branch left.
|
||
/// </summary>
|
||
private void DrawPreparedAlphaBatchRhi(
|
||
GlobalMeshBuffer mesh,
|
||
int firstPreparedDraw,
|
||
int drawCount)
|
||
{
|
||
if (_alphaCommands.Buffer is null)
|
||
return;
|
||
|
||
IGpuPassEncoder encoder = _scope!.RequireEncoder();
|
||
IGpuFrame frame = RequireRhiFrame();
|
||
GpuPushConstants pushConstants = BindAlphaDrawState(
|
||
encoder, frame, mesh, _deferredAlphaViewProjection, out MeshPipelineSet pipelines);
|
||
|
||
if (firstPreparedDraw < 0
|
||
|| drawCount < 0
|
||
|| firstPreparedDraw > _preparedAlphaInstanceCount - drawCount)
|
||
{
|
||
throw new ArgumentOutOfRangeException(
|
||
nameof(firstPreparedDraw),
|
||
"The prepared-alpha draw range exceeds its uploaded instance/category payload.");
|
||
}
|
||
|
||
int runStart = firstPreparedDraw;
|
||
int preparedEnd = firstPreparedDraw + drawCount;
|
||
while (runStart < preparedEnd)
|
||
{
|
||
TranslucencyKind blend = _deferredAlphaKinds[runStart];
|
||
int runEnd = runStart + 1;
|
||
while (runEnd < preparedEnd && _deferredAlphaKinds[runEnd] == blend)
|
||
runEnd++;
|
||
|
||
// ApplyRetailBlend's three cases are three pipelines, including the
|
||
// inverse-alpha one GpuBlendMode.InverseAlpha was added for.
|
||
BindPipelineWithMesh(encoder, PipelineForBlend(pipelines, blend), mesh);
|
||
encoder.SetPushConstants(in pushConstants);
|
||
DrawIndirectRangeRhi(
|
||
encoder,
|
||
ref pushConstants,
|
||
_alphaCommands.Buffer!,
|
||
_alphaCommands.OffsetBytes,
|
||
runStart,
|
||
runEnd - runStart);
|
||
runStart = runEnd;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// S4-c2 fix round 1 (M3b): <c>DrawMesh</c> row 1's "render immediately;
|
||
/// enqueue nothing" outcome for ONE translucent building-shell instance
|
||
/// under building detail — arm the retail one-pass material on the
|
||
/// original blend pipeline and submit the single slot
|
||
/// <see cref="WbDrawDispatcher.DrawImmediateAlphaInstance"/> just
|
||
/// prepared. The caller only reaches this method when
|
||
/// <c>detailSurfaceActive</c> was true, so detail always applies here —
|
||
/// the drain's own per-command detail CHECK is gone
|
||
/// (M3c) precisely because detail-eligible content no longer reaches it.
|
||
/// </summary>
|
||
private void DrawImmediateAlphaInstanceRhi(
|
||
GlobalMeshBuffer mesh, TranslucencyKind blend, Matrix4x4 viewProjection)
|
||
{
|
||
if (_alphaCommands.Buffer is null)
|
||
return;
|
||
|
||
IGpuPassEncoder encoder = _scope!.RequireEncoder();
|
||
IGpuFrame frame = RequireRhiFrame();
|
||
GpuPushConstants pushConstants = BindAlphaDrawState(
|
||
encoder, frame, mesh, viewProjection, out MeshPipelineSet pipelines);
|
||
|
||
BindPipelineWithMesh(encoder, PipelineForBlend(pipelines, blend), mesh);
|
||
encoder.SetPushConstants(in pushConstants);
|
||
ArmBuildingDetail(ref pushConstants);
|
||
DrawIndirectRangeRhi(
|
||
encoder,
|
||
ref pushConstants,
|
||
_alphaCommands.Buffer!,
|
||
_alphaCommands.OffsetBytes,
|
||
startCommand: 0,
|
||
commandCount: 1);
|
||
ClearDetailPushConstants(ref pushConstants);
|
||
encoder.SetPushConstants(in pushConstants);
|
||
}
|
||
|
||
private void DrawImmediateTransparentRhi(
|
||
IGpuPassEncoder encoder,
|
||
GlobalMeshBuffer mesh,
|
||
MeshPipelineSet pipelines,
|
||
ref GpuPushConstants pushConstants,
|
||
IGpuBuffer commandBuffer,
|
||
uint commandBase,
|
||
ReadOnlySpan<uint> usedDetailCategories,
|
||
bool detailEnabled)
|
||
{
|
||
int command = _opaqueDrawCount;
|
||
int end = command + _transparentDrawCount;
|
||
while (command < end)
|
||
{
|
||
TranslucencyKind blend = _groupInputScratch[command].Translucency;
|
||
bool hasDetail = detailEnabled
|
||
&& CommandContainsDetailCategory(
|
||
_indirectCommands[command],
|
||
usedDetailCategories);
|
||
int runEnd = command + 1;
|
||
while (runEnd < end
|
||
&& _groupInputScratch[runEnd].Translucency == blend
|
||
&& !hasDetail
|
||
&& (!detailEnabled
|
||
|| !CommandContainsDetailCategory(
|
||
_indirectCommands[runEnd],
|
||
usedDetailCategories)))
|
||
{
|
||
runEnd++;
|
||
}
|
||
|
||
BindPipelineWithMesh(
|
||
encoder,
|
||
PipelineForBlend(pipelines, blend),
|
||
mesh);
|
||
if (hasDetail)
|
||
ArmBuildingDetail(ref pushConstants);
|
||
else
|
||
ClearDetailPushConstants(ref pushConstants);
|
||
DrawIndirectRangeRhi(
|
||
encoder,
|
||
ref pushConstants,
|
||
commandBuffer,
|
||
commandBase,
|
||
command,
|
||
runEnd - command);
|
||
command = runEnd;
|
||
}
|
||
ClearDetailPushConstants(ref pushConstants);
|
||
encoder.SetPushConstants(in pushConstants);
|
||
}
|
||
|
||
private void DrawDetailAwareRangeRhi(
|
||
IGpuPassEncoder encoder,
|
||
ref GpuPushConstants pushConstants,
|
||
IGpuBuffer commandBuffer,
|
||
uint commandBase,
|
||
int firstCommand,
|
||
int commandCount,
|
||
ReadOnlySpan<uint> detailCategories,
|
||
bool detailEnabled)
|
||
{
|
||
int command = firstCommand;
|
||
int end = firstCommand + commandCount;
|
||
while (command < end)
|
||
{
|
||
bool hasDetail = detailEnabled
|
||
&& CommandContainsDetailCategory(
|
||
_indirectCommands[command], detailCategories);
|
||
int runEnd = command + 1;
|
||
while (runEnd < end
|
||
&& !hasDetail
|
||
&& (!detailEnabled
|
||
|| !CommandContainsDetailCategory(
|
||
_indirectCommands[runEnd], detailCategories)))
|
||
{
|
||
runEnd++;
|
||
}
|
||
|
||
if (hasDetail)
|
||
ArmBuildingDetail(ref pushConstants);
|
||
else
|
||
ClearDetailPushConstants(ref pushConstants);
|
||
DrawIndirectRangeRhi(
|
||
encoder,
|
||
ref pushConstants,
|
||
commandBuffer,
|
||
commandBase,
|
||
command,
|
||
runEnd - command);
|
||
command = runEnd;
|
||
}
|
||
ClearDetailPushConstants(ref pushConstants);
|
||
encoder.SetPushConstants(in pushConstants);
|
||
}
|
||
|
||
private void ArmBuildingDetail(ref GpuPushConstants pushConstants)
|
||
{
|
||
pushConstants.TextureIndexA = _buildingDetail.TextureSlot.Index;
|
||
pushConstants.ParamA = _buildingDetail.Tiling;
|
||
}
|
||
|
||
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
|
||
{
|
||
TranslucencyKind.Additive => pipelines.AlphaAdditive,
|
||
TranslucencyKind.InvAlpha => pipelines.AlphaInverse,
|
||
_ => pipelines.AlphaBlend,
|
||
};
|
||
|
||
/// <summary>
|
||
/// Reads cull modes from <paramref name="cullModes"/> when the caller
|
||
/// supplies one, or from the shared <see cref="_drawCullModes"/> scratch
|
||
/// otherwise (every pre-FW3.2a call site). Campaign FW stage FW3.2a:
|
||
/// <see cref="SubmitOrderedStream"/> passes its OWN scratch
|
||
/// (<see cref="_orderedDrawCullModes"/>) so a walk-ordered submission can
|
||
/// interleave with a mid-flight <see cref="RetailAlphaQueue"/> scope —
|
||
/// <c>_drawCullModes</c> is written fresh by
|
||
/// <c>PrepareDeferredAlphaDraws</c> at every alpha flush and read right
|
||
/// back by this method for that draw; an ordered submission sharing the
|
||
/// same array between those two steps could silently draw the alpha
|
||
/// content's faces with the ordered content's cull mode, or vice
|
||
/// versa (the FW2 caveat this stage retires — see
|
||
/// <c>WbDrawDispatcher.OrderedStream.cs</c>).
|
||
/// </summary>
|
||
private void DrawIndirectRangeRhi(
|
||
IGpuPassEncoder encoder,
|
||
ref GpuPushConstants pushConstants,
|
||
IGpuBuffer commandBuffer,
|
||
uint commandBaseOffsetBytes,
|
||
int startCommand,
|
||
int commandCount,
|
||
CullMode[]? cullModes = null)
|
||
{
|
||
CullMode[] modes = cullModes ?? _drawCullModes;
|
||
int end = startCommand + commandCount;
|
||
int command = startCommand;
|
||
while (command < end)
|
||
{
|
||
CullMode cullMode = modes[command];
|
||
ApplyCullModeRhi(encoder, cullMode);
|
||
|
||
int runCount = 1;
|
||
while (command + runCount < end && modes[command + runCount] == cullMode)
|
||
runCount++;
|
||
|
||
// Each multi-draw-indirect call restarts gl_DrawID at 0, so a run
|
||
// that begins partway into the batch array must carry its absolute
|
||
// command index or it reads BatchData[0] again (issue #52).
|
||
pushConstants.DrawIdOffset = command;
|
||
encoder.SetPushConstants(in pushConstants);
|
||
encoder.MultiDrawIndexedIndirect(
|
||
commandBuffer,
|
||
commandBaseOffsetBytes + (uint)(command * DrawCommandStride),
|
||
(uint)runCount,
|
||
(uint)DrawCommandStride);
|
||
|
||
command += runCount;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// WB <c>BaseObjectRenderManager.cs:850-866</c> applies CullMode per MDI
|
||
/// group and WB <c>GameScene.cs:843</c> sets FrontFace(CW) globally. Both are
|
||
/// dynamic state in core Vulkan 1.3, and both must be re-issued after every
|
||
/// <c>BindPipeline</c>, which restores the pipeline's own defaults.
|
||
/// </summary>
|
||
private static void ApplyCullModeRhi(IGpuPassEncoder encoder, CullMode mode)
|
||
{
|
||
encoder.SetFrontFace(GpuFrontFace.Clockwise);
|
||
switch (mode)
|
||
{
|
||
case CullMode.None:
|
||
encoder.SetCullMode(GpuCullMode.None);
|
||
break;
|
||
case CullMode.Clockwise:
|
||
encoder.SetCullMode(GpuCullMode.Front);
|
||
break;
|
||
case CullMode.CounterClockwise:
|
||
case CullMode.Landblock:
|
||
encoder.SetCullMode(GpuCullMode.Back);
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Binds a pipeline and immediately re-establishes the mesh source. Every
|
||
/// pipeline owns its own vertex array, and vertex attribute pointers plus the
|
||
/// index binding are vertex-array state, so a pipeline switch inside a pass
|
||
/// silently drops them while storage bindings survive.
|
||
/// </summary>
|
||
private static void BindPipelineWithMesh(
|
||
IGpuPassEncoder encoder,
|
||
IGpuPipeline pipeline,
|
||
GlobalMeshBuffer mesh)
|
||
{
|
||
encoder.BindPipeline(pipeline);
|
||
encoder.BindVertexBuffer(
|
||
0,
|
||
mesh.VertexStore ?? throw new InvalidOperationException(
|
||
"The shared mesh arena has no vertex store."),
|
||
0);
|
||
encoder.BindIndexBuffer(
|
||
mesh.IndexStore ?? throw new InvalidOperationException(
|
||
"The shared mesh arena has no index store."),
|
||
0,
|
||
GpuIndexType.UInt16);
|
||
}
|
||
|
||
private void BindGlobalLightsRhi(IGpuPassEncoder encoder, IGpuFrame frame)
|
||
{
|
||
int lightCount = GlobalLightPacker.Pack(_pointSnapshot, ref _globalLightData);
|
||
int uploadCount = lightCount > 0 ? lightCount : 1;
|
||
BindRingSection<float>(
|
||
encoder,
|
||
frame,
|
||
GpuBindingModel.StorageGlobalLights,
|
||
_globalLightData.AsSpan(0, uploadCount * GlobalLightPacker.FloatsPerLight));
|
||
}
|
||
|
||
private static void BindRingSection<T>(
|
||
IGpuPassEncoder encoder,
|
||
IGpuFrame frame,
|
||
uint binding,
|
||
ReadOnlySpan<T> data)
|
||
where T : unmanaged =>
|
||
BindSection(encoder, binding, WriteRingSection(frame, data));
|
||
|
||
private static void BindSection(
|
||
IGpuPassEncoder encoder,
|
||
uint binding,
|
||
in RhiSection section)
|
||
{
|
||
if (section.Buffer is null)
|
||
return;
|
||
encoder.BindStorageBuffer(
|
||
binding,
|
||
section.Buffer,
|
||
section.OffsetBytes,
|
||
section.SizeBytes);
|
||
}
|
||
|
||
private static RhiSection WriteRingSection<T>(
|
||
IGpuFrame frame,
|
||
ReadOnlySpan<T> data,
|
||
GpuRingUsage usage = GpuRingUsage.Storage)
|
||
where T : unmanaged
|
||
{
|
||
int elementBytes = sizeof(T);
|
||
int byteCount = Math.Max(data.Length * elementBytes, elementBytes);
|
||
GpuRingAllocation allocation = frame.AllocateRing(byteCount, usage);
|
||
if (!data.IsEmpty)
|
||
data.CopyTo(allocation.AsSpan<T>());
|
||
return new RhiSection(allocation.Buffer, allocation.OffsetBytes, (uint)byteCount);
|
||
}
|
||
|
||
/// <summary>
|
||
/// #443 — the next classic draw is a PRIVATE pass (paperdoll, appraisal,
|
||
/// chargen preview) and must take the plain ring transform path with
|
||
/// <c>firstInstance = 0</c>, never an append into the shared world
|
||
/// transform frame. The default mesh shaders index every parallel
|
||
/// per-instance array (clip slots, light sets, indoor, opacity, selection,
|
||
/// detail category) zero-based — only the packed world submission's
|
||
/// shader convention subtracts a shared-arena prefix — so an
|
||
/// arena-appended classic draw with a non-zero base reads zeroed
|
||
/// per-instance data (opacity 0 ⇒ an invisible doll whenever a world
|
||
/// frame is active; portal space worked only because the arena was
|
||
/// inactive there). The private pass owns its own camera, lighting, and
|
||
/// target; per the self-contained-render-state rule it must not depend on
|
||
/// the world frame's pose address space at all. Consumed and cleared by
|
||
/// the next <see cref="WriteWorldTransformSection"/>.
|
||
/// </summary>
|
||
internal bool NextClassicDrawIsPrivatePass;
|
||
|
||
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)));
|
||
bool privatePass = NextClassicDrawIsPrivatePass;
|
||
NextClassicDrawIsPrivatePass = false;
|
||
if (privatePass || !_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
|
||
// draw that has not been bracketed by BeginFrame has no slot to write to.
|
||
if (!_dynamicFrameStarted)
|
||
throw new InvalidOperationException("BeginFrame must be called before drawing world entities.");
|
||
|
||
return _frames!.CurrentFrame
|
||
?? throw new InvalidOperationException(
|
||
"WbDrawDispatcher requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
|
||
}
|
||
|
||
private static IDisposable BeginRhiTimer(
|
||
IGpuPassEncoder encoder,
|
||
bool diag,
|
||
string scopeName) =>
|
||
diag ? encoder.BeginTimerScope(scopeName) : NullRhiTimerScope.Instance;
|
||
|
||
/// <summary>
|
||
/// The [WB-DIAG] median/p95 window still measures opaque + transparent GPU
|
||
/// time; the sample now comes from the device's timer pool — the most recent
|
||
/// retired result — rather than a hand-rolled 3-deep query ring read at N-3.
|
||
/// A sample can therefore repeat when the GPU has not finished a newer query,
|
||
/// where the old code dropped it. Diagnostic-only.
|
||
/// </summary>
|
||
private void SampleRhiTimers(bool diag)
|
||
{
|
||
if (!diag || _device is null)
|
||
return;
|
||
|
||
double totalMs = 0;
|
||
bool any = false;
|
||
if (_device.Timers.TryResolve(OpaqueTimerScope, out double opaqueMs))
|
||
{
|
||
totalMs += opaqueMs;
|
||
any = true;
|
||
}
|
||
if (_device.Timers.TryResolve(TransparentTimerScope, out double transparentMs))
|
||
{
|
||
totalMs += transparentMs;
|
||
any = true;
|
||
}
|
||
if (!any)
|
||
return;
|
||
|
||
_gpuSamples[_gpuSampleCursor] = (long)(totalMs * 1000.0);
|
||
_gpuSampleCursor = (_gpuSampleCursor + 1) % _gpuSamples.Length;
|
||
}
|
||
|
||
private void DisposeRhiResources()
|
||
{
|
||
MeshPipelineSet? backbuffer = _backbufferPipelines;
|
||
MeshPipelineSet? offscreen = _offscreenPipelines;
|
||
_backbufferPipelines = null;
|
||
_offscreenPipelines = null;
|
||
DisposeMeshPipelineSet(backbuffer);
|
||
// Reference-equal when the backbuffer is single-sampled, in which case
|
||
// 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)
|
||
{
|
||
if (pipelines is null)
|
||
return;
|
||
pipelines.Opaque.Dispose();
|
||
pipelines.OpaqueAlphaToCoverage.Dispose();
|
||
pipelines.AlphaBlend.Dispose();
|
||
pipelines.AlphaAdditive.Dispose();
|
||
pipelines.AlphaInverse.Dispose();
|
||
}
|
||
|
||
private sealed class NullRhiTimerScope : IDisposable
|
||
{
|
||
internal static NullRhiTimerScope Instance { get; } = new();
|
||
|
||
public void Dispose()
|
||
{
|
||
}
|
||
}
|
||
}
|