Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
1650 lines
60 KiB
C#
1650 lines
60 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using System.Runtime.InteropServices;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.Content;
|
|
using AcDream.Core.Meshing;
|
|
using AcDream.Core.Vfx;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Enums;
|
|
using Silk.NET.OpenGL;
|
|
using RuntimeParticleEmitter = AcDream.Core.Vfx.ParticleEmitter;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// Instanced renderer for retail particle emitters. Scene particles submit to
|
|
/// <see cref="RetailAlphaQueue"/> while a world frame is active so their
|
|
/// compositing order is shared with ordinary translucent GfxObj parts. Sky and
|
|
/// sealed off-screen passes retain their independent immediate path.
|
|
/// </summary>
|
|
public sealed unsafe class ParticleRenderer : IDisposable
|
|
{
|
|
// The texture is per instance through GL_ARB_bindless_texture. Only blend
|
|
// state remains a draw-call boundary, so stable retail distance order no
|
|
// longer degenerates into one draw per alternating particle texture.
|
|
private readonly record struct BatchKey(bool Additive);
|
|
private readonly record struct ParticleDraw(BatchKey Key, ParticleInstance Instance);
|
|
private readonly record struct MeshBatchKey(uint GfxObjId, int BatchIndex);
|
|
private readonly record struct MeshParticleDraw(
|
|
MeshBatchKey Key,
|
|
ObjectRenderBatch Batch,
|
|
MeshParticleInstance Instance);
|
|
private readonly record struct DeferredParticleDraw(
|
|
ParticleSubmissionKind Kind,
|
|
ParticleDraw Billboard,
|
|
MeshParticleDraw Mesh,
|
|
Matrix4x4 ViewProjection);
|
|
|
|
private readonly struct ParticleInstance
|
|
{
|
|
public readonly Vector3 Position;
|
|
public readonly Vector3 AxisX;
|
|
public readonly Vector3 AxisY;
|
|
public readonly uint ColorArgb;
|
|
public readonly ulong TextureHandle;
|
|
public readonly float DistanceSq;
|
|
|
|
public ParticleInstance(
|
|
Vector3 position,
|
|
Vector3 axisX,
|
|
Vector3 axisY,
|
|
uint colorArgb,
|
|
ulong textureHandle,
|
|
float distanceSq)
|
|
{
|
|
Position = position;
|
|
AxisX = axisX;
|
|
AxisY = axisY;
|
|
ColorArgb = colorArgb;
|
|
TextureHandle = textureHandle;
|
|
DistanceSq = distanceSq;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Vertex-instance ABI shared with particle.vert. The resident texture
|
|
/// handle is carried per particle so ordered particles using different
|
|
/// textures remain one instanced draw when their blend mode matches.
|
|
/// </summary>
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
internal struct BillboardGpuInstance
|
|
{
|
|
public Vector4 Center;
|
|
public Vector4 AxisX;
|
|
public Vector4 AxisY;
|
|
public Vector4 Color;
|
|
public uint TextureHandleLow;
|
|
public uint TextureHandleHigh;
|
|
}
|
|
|
|
private readonly struct MeshParticleInstance
|
|
{
|
|
public readonly Matrix4x4 Model;
|
|
public readonly uint ColorArgb;
|
|
public readonly float DistanceSq;
|
|
|
|
public MeshParticleInstance(Matrix4x4 model, uint colorArgb, float distanceSq)
|
|
{
|
|
Model = model;
|
|
ColorArgb = colorArgb;
|
|
DistanceSq = distanceSq;
|
|
}
|
|
}
|
|
|
|
private readonly GL _gl;
|
|
private readonly Shader _shader;
|
|
private readonly Shader? _meshShader;
|
|
private readonly TextureCache? _textures;
|
|
private readonly IDatReaderWriter? _dats;
|
|
private readonly WbMeshAdapter? _meshAdapter;
|
|
private readonly ParticleSystem _particles;
|
|
private readonly RetailAlphaQueue? _alphaQueue;
|
|
private readonly AlphaDrawSource _alphaSource;
|
|
private readonly Dictionary<uint, ParticleGfxInfo> _particleGfxInfoByGfxObj = new();
|
|
private readonly Dictionary<int, ParticleGfxInfo> _particleGfxInfoByEmitter = new();
|
|
private readonly Dictionary<uint, RetailParticleGeometryKind> _geometryKindByGfxObj = new();
|
|
private readonly Dictionary<uint, TranslucencyKind> _meshBlendBySurface = new();
|
|
private readonly ParticleMeshReferenceTracker? _meshReferences;
|
|
private readonly ParticleEmitterRetirementTracker _emitterRetirements;
|
|
private RetryableResourceReleaseLedger? _disposeResources;
|
|
private bool _disposing;
|
|
private bool _disposed;
|
|
private readonly HashSet<uint> _meshLoadRequestedThisFrame = new();
|
|
private readonly int _meshTextureHandleLoc = -1;
|
|
private readonly int _meshTextureLayerLoc = -1;
|
|
|
|
private uint _quadVao;
|
|
private readonly uint _quadVbo;
|
|
private readonly uint _quadEbo;
|
|
private uint _instanceVbo;
|
|
private uint _meshVao;
|
|
private uint _meshInstanceVbo;
|
|
private int _instanceVboCapacityBytes;
|
|
private int _meshInstanceVboCapacityBytes;
|
|
|
|
private sealed class DynamicBufferSet
|
|
{
|
|
public uint BillboardVao;
|
|
public uint BillboardInstanceVbo;
|
|
public int BillboardCapacityBytes;
|
|
public uint MeshVao;
|
|
public uint MeshInstanceVbo;
|
|
public int MeshCapacityBytes;
|
|
}
|
|
|
|
private readonly List<DynamicBufferSet>[] _dynamicBufferSetsByFrame =
|
|
[[], [], []];
|
|
private int _dynamicFrameSlot;
|
|
private int _dynamicBufferSetCursor;
|
|
private bool _dynamicFrameStarted;
|
|
private DynamicBufferSet? _activeDynamicBufferSet;
|
|
|
|
internal (int SetCount, long CapacityBytes) DynamicBufferDiagnostics
|
|
{
|
|
get
|
|
{
|
|
int count = 0;
|
|
long bytes = 0;
|
|
foreach (List<DynamicBufferSet> frameSets in _dynamicBufferSetsByFrame)
|
|
{
|
|
count += frameSets.Count;
|
|
foreach (DynamicBufferSet set in frameSets)
|
|
bytes += set.BillboardCapacityBytes + set.MeshCapacityBytes;
|
|
}
|
|
return (count, bytes);
|
|
}
|
|
}
|
|
|
|
private BillboardGpuInstance[] _instanceScratch = new BillboardGpuInstance[256];
|
|
private float[] _meshInstanceScratch = new float[256 * 20];
|
|
|
|
// MP-Alloc (2026-07-05): Draw() is called up to ~11 times per frame
|
|
// (sky pre/post, scene, per-visible-cell, dynamics, unattached passes),
|
|
// each previously `new`ing a List<ParticleDraw> (BuildDrawList) and a
|
|
// List<ParticleInstance> (the per-batch `run` list) that became garbage
|
|
// as soon as the call returned. All Draw() calls happen sequentially on
|
|
// the render thread (verified: every call site in GameWindow.cs is a
|
|
// plain synchronous invocation from the single-threaded OnRender chain,
|
|
// none dispatched via Task.Run/Parallel) and each call fully drains its
|
|
// lists before returning, so a single pair of reused fields is safe -
|
|
// no call overlaps another's use of these buffers.
|
|
private readonly List<ParticleDraw> _drawListScratch = new(64);
|
|
private readonly List<ParticleInstance> _runScratch = new(64);
|
|
private readonly List<MeshParticleDraw> _meshDrawListScratch = new(64);
|
|
private readonly List<MeshParticleInstance> _meshRunScratch = new(64);
|
|
private readonly List<ParticleSubmission> _submissionScratch = new(128);
|
|
private readonly List<RuntimeParticleEmitter> _scopedEmitterScratch = new(64);
|
|
private readonly List<DeferredParticleDraw> _deferredAlpha = new(128);
|
|
private DeferredParticleDraw[] _preparedAlpha = new DeferredParticleDraw[256];
|
|
private uint[] _preparedInstanceOffsets = new uint[256];
|
|
private int _preparedAlphaCount;
|
|
|
|
private sealed class AlphaDrawSource(ParticleRenderer owner) : IRetailAlphaDrawSource
|
|
{
|
|
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens)
|
|
=> owner.PrepareDeferredAlphaDraws(tokens);
|
|
|
|
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
|
|
=> owner.DrawPreparedAlphaBatch(firstPreparedDraw, drawCount);
|
|
|
|
public void ResetAlphaSubmissions()
|
|
=> owner.ResetDeferredAlpha();
|
|
}
|
|
|
|
internal ParticleRenderer(
|
|
GL gl,
|
|
string shadersDir,
|
|
ParticleSystem particles,
|
|
TextureCache? textures = null,
|
|
IDatReaderWriter? dats = null,
|
|
WbMeshAdapter? meshAdapter = null,
|
|
RetailAlphaQueue? alphaQueue = null)
|
|
{
|
|
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
|
_textures = textures;
|
|
_dats = dats;
|
|
_meshAdapter = meshAdapter;
|
|
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
|
|
_alphaQueue = alphaQueue;
|
|
_alphaSource = new AlphaDrawSource(this);
|
|
if (_meshAdapter is not null)
|
|
{
|
|
_meshReferences = new ParticleMeshReferenceTracker(
|
|
gfxObjId => _meshAdapter.IncrementRefCount(gfxObjId),
|
|
gfxObjId => _meshAdapter.DecrementRefCount(gfxObjId));
|
|
}
|
|
_emitterRetirements = new ParticleEmitterRetirementTracker(
|
|
handle => _meshReferences?.Release(handle),
|
|
handle => _particleGfxInfoByEmitter.Remove(handle),
|
|
handle => _textures?.ReleaseParticleTextureOwner(handle),
|
|
error => Console.Error.WriteLine($"[particles] {error}"));
|
|
_shader = new Shader(_gl,
|
|
System.IO.Path.Combine(shadersDir, "particle.vert"),
|
|
System.IO.Path.Combine(shadersDir, "particle.frag"));
|
|
if (_meshAdapter?.MeshManager?.GlobalBuffer is not null)
|
|
{
|
|
_meshShader = new Shader(_gl,
|
|
System.IO.Path.Combine(shadersDir, "particle_mesh.vert"),
|
|
System.IO.Path.Combine(shadersDir, "particle_mesh.frag"));
|
|
_meshTextureHandleLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureHandle");
|
|
_meshTextureLayerLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureLayer");
|
|
}
|
|
|
|
float[] quadVerts =
|
|
{
|
|
-0.5f, -0.5f, 0f, 0f,
|
|
0.5f, -0.5f, 1f, 0f,
|
|
0.5f, 0.5f, 1f, 1f,
|
|
-0.5f, 0.5f, 0f, 1f,
|
|
};
|
|
uint[] quadIdx = { 0, 1, 2, 0, 2, 3 };
|
|
|
|
uint quadVbo = 0;
|
|
uint quadEbo = 0;
|
|
uint uploadVao = 0;
|
|
bool vboAllocated = false;
|
|
bool eboAllocated = false;
|
|
try
|
|
{
|
|
quadVbo = TrackedGlResource.CreateBuffer(_gl, "creating particle quad VBO");
|
|
fixed (void* p = quadVerts)
|
|
{
|
|
TrackedGlResource.AllocateBufferStorage(
|
|
_gl,
|
|
GLEnum.ArrayBuffer,
|
|
quadVbo,
|
|
0,
|
|
quadVerts.Length * sizeof(float),
|
|
GLEnum.StaticDraw,
|
|
p,
|
|
"uploading particle quad VBO");
|
|
}
|
|
vboAllocated = true;
|
|
|
|
quadEbo = TrackedGlResource.CreateBuffer(_gl, "creating particle quad EBO");
|
|
uploadVao = TrackedGlResource.CreateVertexArray(_gl, "creating particle upload VAO");
|
|
_gl.BindVertexArray(uploadVao);
|
|
fixed (void* p = quadIdx)
|
|
{
|
|
TrackedGlResource.AllocateBufferStorage(
|
|
_gl,
|
|
GLEnum.ElementArrayBuffer,
|
|
quadEbo,
|
|
0,
|
|
quadIdx.Length * sizeof(uint),
|
|
GLEnum.StaticDraw,
|
|
p,
|
|
"uploading particle quad EBO");
|
|
}
|
|
eboAllocated = true;
|
|
_gl.BindVertexArray(0);
|
|
TrackedGlResource.DeleteVertexArray(_gl, uploadVao, "deleting particle upload VAO");
|
|
uploadVao = 0;
|
|
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
|
_quadVbo = quadVbo;
|
|
_quadEbo = quadEbo;
|
|
}
|
|
catch (Exception creationFailure)
|
|
{
|
|
List<Exception>? cleanupFailures = null;
|
|
void Attempt(Action action)
|
|
{
|
|
try { action(); }
|
|
catch (Exception ex) { (cleanupFailures ??= []).Add(ex); }
|
|
}
|
|
if (uploadVao != 0)
|
|
Attempt(() => TrackedGlResource.DeleteVertexArray(_gl, uploadVao, "rolling back particle upload VAO"));
|
|
if (quadEbo != 0)
|
|
Attempt(() => TrackedGlResource.DeleteBuffer(
|
|
_gl,
|
|
quadEbo,
|
|
eboAllocated ? quadIdx.Length * sizeof(uint) : 0,
|
|
"rolling back particle quad EBO"));
|
|
if (quadVbo != 0)
|
|
Attempt(() => TrackedGlResource.DeleteBuffer(
|
|
_gl,
|
|
quadVbo,
|
|
vboAllocated ? quadVerts.Length * sizeof(float) : 0,
|
|
"rolling back particle quad VBO"));
|
|
if (cleanupFailures is not null)
|
|
{
|
|
cleanupFailures.Insert(0, creationFailure);
|
|
throw new AggregateException("Particle static-buffer creation and rollback failed.", cleanupFailures);
|
|
}
|
|
throw;
|
|
}
|
|
|
|
_particles.EmitterDied += OnEmitterDied;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts one render frame. Wb point-of-use recovery is limited to one
|
|
/// request per missing GfxObj even though portal slicing may invoke Draw
|
|
/// many times during the frame.
|
|
/// </summary>
|
|
public void BeginFrame(int frameSlot)
|
|
{
|
|
if ((uint)frameSlot >= (uint)_dynamicBufferSetsByFrame.Length)
|
|
throw new ArgumentOutOfRangeException(nameof(frameSlot));
|
|
|
|
_dynamicFrameSlot = frameSlot;
|
|
_dynamicBufferSetCursor = 0;
|
|
_dynamicFrameStarted = true;
|
|
_activeDynamicBufferSet = null;
|
|
_meshLoadRequestedThisFrame.Clear();
|
|
_emitterRetirements.RetryPending();
|
|
_textures?.TickParticleTextureCache();
|
|
}
|
|
|
|
public void Draw(
|
|
ICamera camera,
|
|
Vector3 cameraWorldPos,
|
|
ParticleRenderPass renderPass = ParticleRenderPass.Scene,
|
|
Func<AcDream.Core.Vfx.ParticleEmitter, bool>? emitterFilter = null)
|
|
{
|
|
if (camera is null)
|
|
return;
|
|
|
|
Matrix4x4.Invert(camera.View, out var invView);
|
|
Vector3 cameraRight = Vector3.Normalize(new Vector3(invView.M11, invView.M12, invView.M13));
|
|
Vector3 cameraUp = Vector3.Normalize(new Vector3(invView.M21, invView.M22, invView.M23));
|
|
BuildDrawLists(
|
|
cameraWorldPos,
|
|
renderPass,
|
|
cameraRight,
|
|
cameraUp,
|
|
emitterFilter,
|
|
scopedEmitters: null);
|
|
FinishDraw(camera, renderPass);
|
|
}
|
|
|
|
public void DrawForOwners(
|
|
ICamera camera,
|
|
Vector3 cameraWorldPos,
|
|
ParticleRenderPass renderPass,
|
|
IReadOnlySet<uint> attachedOwnerIds,
|
|
bool includeUnattached = false,
|
|
IReadOnlySet<uint>? excludedAttachedOwnerIds = null)
|
|
{
|
|
if (camera is null)
|
|
return;
|
|
|
|
_particles.CopyRenderableEmittersForOwners(
|
|
renderPass,
|
|
attachedOwnerIds,
|
|
includeUnattached,
|
|
_scopedEmitterScratch,
|
|
excludedAttachedOwnerIds);
|
|
Matrix4x4.Invert(camera.View, out Matrix4x4 invView);
|
|
Vector3 cameraRight = Vector3.Normalize(new Vector3(invView.M11, invView.M12, invView.M13));
|
|
Vector3 cameraUp = Vector3.Normalize(new Vector3(invView.M21, invView.M22, invView.M23));
|
|
BuildDrawLists(
|
|
cameraWorldPos,
|
|
renderPass,
|
|
cameraRight,
|
|
cameraUp,
|
|
emitterFilter: null,
|
|
_scopedEmitterScratch);
|
|
FinishDraw(camera, renderPass);
|
|
}
|
|
|
|
private void FinishDraw(ICamera camera, ParticleRenderPass renderPass)
|
|
{
|
|
if (_submissionScratch.Count == 0)
|
|
return;
|
|
|
|
if (renderPass == ParticleRenderPass.Scene && _alphaQueue?.IsCollecting == true)
|
|
DeferToRetailAlphaQueue(camera);
|
|
else
|
|
DrawOrdered(camera);
|
|
}
|
|
|
|
private void DeferToRetailAlphaQueue(ICamera camera)
|
|
{
|
|
RetailAlphaQueue queue = _alphaQueue!;
|
|
Matrix4x4 viewProjection = camera.View * camera.Projection;
|
|
for (int i = 0; i < _submissionScratch.Count; i++)
|
|
{
|
|
ParticleSubmission submission = _submissionScratch[i];
|
|
DeferredParticleDraw deferred = submission.Kind == ParticleSubmissionKind.Billboard
|
|
? new DeferredParticleDraw(
|
|
submission.Kind,
|
|
_drawListScratch[submission.DrawIndex],
|
|
default,
|
|
viewProjection)
|
|
: new DeferredParticleDraw(
|
|
submission.Kind,
|
|
default,
|
|
_meshDrawListScratch[submission.DrawIndex],
|
|
viewProjection);
|
|
|
|
int token = _deferredAlpha.Count;
|
|
_deferredAlpha.Add(deferred);
|
|
queue.Submit(
|
|
_alphaSource,
|
|
token,
|
|
MathF.Sqrt(MathF.Max(0f, submission.DistanceSq)));
|
|
}
|
|
}
|
|
|
|
private void DrawOrdered(ICamera camera)
|
|
{
|
|
ParticleSubmissionOrdering.Sort(_submissionScratch);
|
|
GlobalMeshBuffer? global = _meshAdapter?.MeshManager?.GlobalBuffer;
|
|
Matrix4x4 viewProjection = camera.View * camera.Projection;
|
|
|
|
_gl.Enable(EnableCap.DepthTest);
|
|
_gl.Enable(EnableCap.Blend);
|
|
_gl.DepthMask(false);
|
|
|
|
for (int i = 0; i < _submissionScratch.Count;)
|
|
{
|
|
ParticleSubmission submission = _submissionScratch[i];
|
|
if (submission.Kind == ParticleSubmissionKind.Billboard)
|
|
{
|
|
ParticleDraw draw = _drawListScratch[submission.DrawIndex];
|
|
BatchKey key = draw.Key;
|
|
_runScratch.Clear();
|
|
do
|
|
{
|
|
_runScratch.Add(_drawListScratch[submission.DrawIndex].Instance);
|
|
i++;
|
|
if (i >= _submissionScratch.Count)
|
|
break;
|
|
submission = _submissionScratch[i];
|
|
}
|
|
while (submission.Kind == ParticleSubmissionKind.Billboard
|
|
&& _drawListScratch[submission.DrawIndex].Key == key);
|
|
|
|
_gl.BlendFunc(
|
|
BlendingFactor.SrcAlpha,
|
|
key.Additive ? BlendingFactor.One : BlendingFactor.OneMinusSrcAlpha);
|
|
DrawInstances(_runScratch, viewProjection);
|
|
continue;
|
|
}
|
|
|
|
if (_meshShader is null || global is null)
|
|
{
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
MeshParticleDraw meshDraw = _meshDrawListScratch[submission.DrawIndex];
|
|
MeshBatchKey meshKey = meshDraw.Key;
|
|
ObjectRenderBatch batch = meshDraw.Batch;
|
|
_meshRunScratch.Clear();
|
|
do
|
|
{
|
|
_meshRunScratch.Add(_meshDrawListScratch[submission.DrawIndex].Instance);
|
|
i++;
|
|
if (i >= _submissionScratch.Count)
|
|
break;
|
|
submission = _submissionScratch[i];
|
|
}
|
|
while (submission.Kind == ParticleSubmissionKind.Mesh
|
|
&& _meshDrawListScratch[submission.DrawIndex].Key == meshKey);
|
|
|
|
ApplyMeshCullMode(batch.CullMode);
|
|
TranslucencyKind blend = ResolveMeshBlend(batch);
|
|
_gl.BlendFunc(
|
|
blend == TranslucencyKind.InvAlpha
|
|
? BlendingFactor.OneMinusSrcAlpha
|
|
: BlendingFactor.SrcAlpha,
|
|
blend switch
|
|
{
|
|
TranslucencyKind.Additive => BlendingFactor.One,
|
|
TranslucencyKind.InvAlpha => BlendingFactor.SrcAlpha,
|
|
_ => BlendingFactor.OneMinusSrcAlpha,
|
|
});
|
|
|
|
_gl.ProgramUniform2(
|
|
_meshShader.Program,
|
|
_meshTextureHandleLoc,
|
|
(uint)(batch.BindlessTextureHandle & 0xFFFFFFFFu),
|
|
(uint)(batch.BindlessTextureHandle >> 32));
|
|
_gl.ProgramUniform1(_meshShader.Program, _meshTextureLayerLoc, batch.TextureIndex);
|
|
|
|
UploadMeshInstances(_meshRunScratch);
|
|
PrepareMeshPipeline(viewProjection, global);
|
|
_gl.DrawElementsInstancedBaseVertex(
|
|
PrimitiveType.Triangles,
|
|
(uint)batch.IndexCount,
|
|
DrawElementsType.UnsignedShort,
|
|
(void*)(batch.FirstIndex * sizeof(ushort)),
|
|
(uint)_meshRunScratch.Count,
|
|
(int)batch.BaseVertex);
|
|
}
|
|
|
|
_gl.BindVertexArray(0);
|
|
_gl.DepthMask(true);
|
|
_gl.Disable(EnableCap.Blend);
|
|
_gl.Disable(EnableCap.CullFace);
|
|
}
|
|
|
|
private void PrepareDeferredAlphaDraws(ReadOnlySpan<int> tokens)
|
|
{
|
|
if (tokens.Length == 0)
|
|
return;
|
|
|
|
ActivateNextDynamicBufferSet();
|
|
|
|
int count = tokens.Length;
|
|
if (_preparedAlpha.Length < count)
|
|
Array.Resize(ref _preparedAlpha, count + 256);
|
|
if (_preparedInstanceOffsets.Length < count)
|
|
Array.Resize(ref _preparedInstanceOffsets, count + 256);
|
|
if (_instanceScratch.Length < count)
|
|
Array.Resize(ref _instanceScratch, count + 256);
|
|
int neededMeshFloats = count * 20;
|
|
if (_meshInstanceScratch.Length < neededMeshFloats)
|
|
Array.Resize(ref _meshInstanceScratch, neededMeshFloats + 256 * 20);
|
|
|
|
int billboardCount = 0;
|
|
int meshCount = 0;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
DeferredParticleDraw deferred = _deferredAlpha[tokens[i]];
|
|
_preparedAlpha[i] = deferred;
|
|
if (deferred.Kind == ParticleSubmissionKind.Billboard)
|
|
{
|
|
_preparedInstanceOffsets[i] = (uint)billboardCount;
|
|
WriteBillboardGpuInstance(
|
|
ref _instanceScratch[billboardCount++],
|
|
deferred.Billboard.Instance);
|
|
}
|
|
else
|
|
{
|
|
_preparedInstanceOffsets[i] = (uint)meshCount;
|
|
WriteMeshGpuInstance(
|
|
_meshInstanceScratch,
|
|
meshCount++ * 20,
|
|
deferred.Mesh.Instance);
|
|
}
|
|
}
|
|
|
|
if (billboardCount > 0)
|
|
{
|
|
fixed (void* bp = _instanceScratch)
|
|
UploadDynamicArrayBuffer(
|
|
_instanceVbo,
|
|
ref _instanceVboCapacityBytes,
|
|
bp,
|
|
billboardCount * sizeof(BillboardGpuInstance));
|
|
}
|
|
|
|
if (meshCount > 0)
|
|
{
|
|
fixed (void* mp = _meshInstanceScratch)
|
|
UploadDynamicArrayBuffer(
|
|
_meshInstanceVbo,
|
|
ref _meshInstanceVboCapacityBytes,
|
|
mp,
|
|
meshCount * 20 * sizeof(float));
|
|
}
|
|
|
|
PersistActiveDynamicBufferCapacities();
|
|
_preparedAlphaCount = count;
|
|
}
|
|
|
|
private void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
|
|
{
|
|
if (drawCount <= 0)
|
|
return;
|
|
if (firstPreparedDraw < 0
|
|
|| firstPreparedDraw > _preparedAlphaCount - drawCount)
|
|
throw new ArgumentOutOfRangeException(nameof(firstPreparedDraw));
|
|
|
|
GlobalMeshBuffer? global = _meshAdapter?.MeshManager?.GlobalBuffer;
|
|
_gl.Enable(EnableCap.DepthTest);
|
|
_gl.Enable(EnableCap.Blend);
|
|
_gl.DepthMask(false);
|
|
|
|
ParticleSubmissionKind? activeKind = null;
|
|
Matrix4x4 activeViewProjection = default;
|
|
int i = firstPreparedDraw;
|
|
int preparedEnd = firstPreparedDraw + drawCount;
|
|
while (i < preparedEnd)
|
|
{
|
|
DeferredParticleDraw deferred = _preparedAlpha[i];
|
|
if (deferred.Kind == ParticleSubmissionKind.Billboard)
|
|
{
|
|
ParticleDraw draw = deferred.Billboard;
|
|
BatchKey key = draw.Key;
|
|
if (activeKind != ParticleSubmissionKind.Billboard
|
|
|| activeViewProjection != deferred.ViewProjection)
|
|
{
|
|
PrepareBillboardPipeline(deferred.ViewProjection);
|
|
activeKind = ParticleSubmissionKind.Billboard;
|
|
activeViewProjection = deferred.ViewProjection;
|
|
}
|
|
|
|
uint baseInstance = _preparedInstanceOffsets[i];
|
|
int runStart = i;
|
|
do
|
|
{
|
|
i++;
|
|
if (i >= preparedEnd)
|
|
break;
|
|
deferred = _preparedAlpha[i];
|
|
}
|
|
while (deferred.Kind == ParticleSubmissionKind.Billboard
|
|
&& deferred.Billboard.Key == key
|
|
&& deferred.ViewProjection == activeViewProjection);
|
|
|
|
_gl.BlendFunc(
|
|
BlendingFactor.SrcAlpha,
|
|
key.Additive ? BlendingFactor.One : BlendingFactor.OneMinusSrcAlpha);
|
|
_gl.BindVertexArray(_quadVao);
|
|
_gl.DrawElementsInstancedBaseInstance(
|
|
PrimitiveType.Triangles,
|
|
6,
|
|
DrawElementsType.UnsignedInt,
|
|
(void*)0,
|
|
(uint)(i - runStart),
|
|
baseInstance);
|
|
continue;
|
|
}
|
|
|
|
if (_meshShader is null || global is null)
|
|
{
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
MeshParticleDraw meshDraw = deferred.Mesh;
|
|
MeshBatchKey meshKey = meshDraw.Key;
|
|
ObjectRenderBatch batch = meshDraw.Batch;
|
|
if (activeKind != ParticleSubmissionKind.Mesh
|
|
|| activeViewProjection != deferred.ViewProjection)
|
|
{
|
|
PrepareMeshPipeline(deferred.ViewProjection, global);
|
|
activeKind = ParticleSubmissionKind.Mesh;
|
|
activeViewProjection = deferred.ViewProjection;
|
|
}
|
|
|
|
uint meshBaseInstance = _preparedInstanceOffsets[i];
|
|
int meshRunStart = i;
|
|
do
|
|
{
|
|
i++;
|
|
if (i >= preparedEnd)
|
|
break;
|
|
deferred = _preparedAlpha[i];
|
|
}
|
|
while (deferred.Kind == ParticleSubmissionKind.Mesh
|
|
&& deferred.Mesh.Key == meshKey
|
|
&& deferred.ViewProjection == activeViewProjection);
|
|
|
|
ApplyMeshCullMode(batch.CullMode);
|
|
TranslucencyKind blend = ResolveMeshBlend(batch);
|
|
_gl.BlendFunc(
|
|
blend == TranslucencyKind.InvAlpha
|
|
? BlendingFactor.OneMinusSrcAlpha
|
|
: BlendingFactor.SrcAlpha,
|
|
blend switch
|
|
{
|
|
TranslucencyKind.Additive => BlendingFactor.One,
|
|
TranslucencyKind.InvAlpha => BlendingFactor.SrcAlpha,
|
|
_ => BlendingFactor.OneMinusSrcAlpha,
|
|
});
|
|
|
|
_gl.ProgramUniform2(
|
|
_meshShader.Program,
|
|
_meshTextureHandleLoc,
|
|
(uint)(batch.BindlessTextureHandle & 0xFFFFFFFFu),
|
|
(uint)(batch.BindlessTextureHandle >> 32));
|
|
_gl.ProgramUniform1(_meshShader.Program, _meshTextureLayerLoc, batch.TextureIndex);
|
|
|
|
_gl.BindVertexArray(_meshVao);
|
|
_gl.DrawElementsInstancedBaseVertexBaseInstance(
|
|
PrimitiveType.Triangles,
|
|
(uint)batch.IndexCount,
|
|
DrawElementsType.UnsignedShort,
|
|
(void*)(batch.FirstIndex * sizeof(ushort)),
|
|
(uint)(i - meshRunStart),
|
|
(int)batch.BaseVertex,
|
|
meshBaseInstance);
|
|
}
|
|
|
|
_gl.BindVertexArray(0);
|
|
_gl.DepthMask(true);
|
|
_gl.Disable(EnableCap.Blend);
|
|
_gl.Disable(EnableCap.CullFace);
|
|
}
|
|
|
|
private void ResetDeferredAlpha()
|
|
{
|
|
_deferredAlpha.Clear();
|
|
_preparedAlphaCount = 0;
|
|
}
|
|
|
|
private void PrepareBillboardPipeline(Matrix4x4 viewProjection)
|
|
{
|
|
_shader.Use();
|
|
_shader.SetMatrix4("uViewProjection", viewProjection);
|
|
_gl.Disable(EnableCap.CullFace);
|
|
_gl.BindVertexArray(_quadVao);
|
|
}
|
|
|
|
private void PrepareMeshPipeline(Matrix4x4 viewProjection, GlobalMeshBuffer global)
|
|
{
|
|
_meshShader!.Use();
|
|
_meshShader.SetMatrix4("uViewProjection", viewProjection);
|
|
_gl.FrontFace(FrontFaceDirection.CW);
|
|
_gl.BindVertexArray(_meshVao);
|
|
|
|
// GlobalMeshBuffer may grow and replace either backing buffer. Bind
|
|
// today's buffer names on every pipeline switch instead of caching
|
|
// them in this VAO at construction time.
|
|
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, global.VBO);
|
|
int vertexStride = VertexPositionNormalTexture.Size;
|
|
_gl.EnableVertexAttribArray(0);
|
|
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, (uint)vertexStride, (void*)0);
|
|
_gl.EnableVertexAttribArray(1);
|
|
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, (uint)vertexStride, (void*)(3 * sizeof(float)));
|
|
_gl.EnableVertexAttribArray(2);
|
|
_gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, (uint)vertexStride, (void*)(6 * sizeof(float)));
|
|
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, global.IBO);
|
|
|
|
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _meshInstanceVbo);
|
|
const int instanceStride = 20 * sizeof(float);
|
|
for (uint column = 0; column < 4; column++)
|
|
{
|
|
uint location = 3 + column;
|
|
_gl.EnableVertexAttribArray(location);
|
|
_gl.VertexAttribPointer(
|
|
location,
|
|
4,
|
|
VertexAttribPointerType.Float,
|
|
false,
|
|
instanceStride,
|
|
(void*)(column * 4 * sizeof(float)));
|
|
_gl.VertexAttribDivisor(location, 1);
|
|
}
|
|
_gl.EnableVertexAttribArray(7);
|
|
_gl.VertexAttribPointer(7, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(16 * sizeof(float)));
|
|
_gl.VertexAttribDivisor(7, 1);
|
|
}
|
|
|
|
private void BuildDrawLists(
|
|
Vector3 cameraWorldPos,
|
|
ParticleRenderPass renderPass,
|
|
Vector3 cameraRight,
|
|
Vector3 cameraUp,
|
|
Func<AcDream.Core.Vfx.ParticleEmitter, bool>? emitterFilter,
|
|
IReadOnlyList<RuntimeParticleEmitter>? scopedEmitters)
|
|
{
|
|
var draws = _drawListScratch;
|
|
draws.Clear();
|
|
_meshDrawListScratch.Clear();
|
|
_submissionScratch.Clear();
|
|
int sequence = 0;
|
|
if (scopedEmitters is not null)
|
|
{
|
|
for (int i = 0; i < scopedEmitters.Count; i++)
|
|
{
|
|
AppendEmitterDraws(
|
|
scopedEmitters[i],
|
|
cameraWorldPos,
|
|
cameraRight,
|
|
cameraUp,
|
|
ref sequence);
|
|
}
|
|
return;
|
|
}
|
|
|
|
foreach (RuntimeParticleEmitter emitter in _particles.EnumerateRenderableEmitters(renderPass))
|
|
{
|
|
if (emitterFilter is null || emitterFilter(emitter))
|
|
AppendEmitterDraws(emitter, cameraWorldPos, cameraRight, cameraUp, ref sequence);
|
|
}
|
|
}
|
|
|
|
private void AppendEmitterDraws(
|
|
RuntimeParticleEmitter em,
|
|
Vector3 cameraWorldPos,
|
|
Vector3 cameraRight,
|
|
Vector3 cameraUp,
|
|
ref int sequence)
|
|
{
|
|
List<ParticleDraw> draws = _drawListScratch;
|
|
ParticleGfxInfo gfxInfo = default;
|
|
bool gfxInfoResolved = false;
|
|
|
|
for (int idx = 0; idx < em.Particles.Length; idx++)
|
|
{
|
|
ref Particle p = ref em.Particles[idx];
|
|
if (!p.Alive)
|
|
continue;
|
|
// `p.Position` is already in world coordinates: AttachLocal
|
|
// emitters get their AnchorPos refreshed each frame by the
|
|
// owning subsystem (sky-PES driver, animation tick, etc.) which
|
|
// mirrors retail's live-parent-frame read at
|
|
// ParticleEmitter::UpdateParticles 0x0051d2d4 for is_parent_local=1.
|
|
Vector3 pos = p.Position;
|
|
uint gfxObjId = em.Desc.HwGfxObjId != 0 ? em.Desc.HwGfxObjId : em.Desc.GfxObjId;
|
|
if (gfxObjId != 0
|
|
&& ResolveGeometryKind(gfxObjId) == RetailParticleGeometryKind.FullMesh
|
|
&& TryAppendMeshDraws(em, p, gfxObjId, cameraWorldPos, ref sequence))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!gfxInfoResolved)
|
|
{
|
|
gfxInfo = ResolveParticleGfxInfo(em);
|
|
gfxInfoResolved = true;
|
|
}
|
|
bool additive = gfxInfo.HasMaterial
|
|
? gfxInfo.Additive
|
|
: (em.Desc.Flags & EmitterFlags.Additive) != 0;
|
|
var key = new BatchKey(additive);
|
|
Vector3 axisX;
|
|
Vector3 axisY;
|
|
if (gfxInfo.IsBillboard)
|
|
{
|
|
pos += Vector3.UnitZ * (gfxInfo.CenterOffset.Z * p.Size);
|
|
axisX = cameraRight * (gfxInfo.Size.X * p.Size);
|
|
axisY = cameraUp * (gfxInfo.Size.Y * p.Size);
|
|
}
|
|
else
|
|
{
|
|
Quaternion orientation = ParticleOrientation(em, p);
|
|
pos += Vector3.Transform(gfxInfo.CenterOffset * p.Size, orientation);
|
|
axisX = Vector3.Transform(gfxInfo.AxisX, orientation) * (gfxInfo.Size.X * p.Size);
|
|
axisY = Vector3.Transform(gfxInfo.AxisY, orientation) * (gfxInfo.Size.Y * p.Size);
|
|
}
|
|
|
|
float distSq = Vector3.DistanceSquared(pos, cameraWorldPos);
|
|
|
|
int drawIndex = draws.Count;
|
|
draws.Add(new ParticleDraw(
|
|
key,
|
|
new ParticleInstance(
|
|
pos,
|
|
axisX,
|
|
axisY,
|
|
p.ColorArgb,
|
|
gfxInfo.TextureHandle,
|
|
distSq)));
|
|
_submissionScratch.Add(new ParticleSubmission(
|
|
ParticleSubmissionKind.Billboard,
|
|
drawIndex,
|
|
distSq,
|
|
sequence++));
|
|
}
|
|
}
|
|
|
|
private bool TryAppendMeshDraws(
|
|
AcDream.Core.Vfx.ParticleEmitter emitter,
|
|
Particle particle,
|
|
uint gfxObjId,
|
|
Vector3 cameraWorldPosition,
|
|
ref int sequence)
|
|
{
|
|
if (_meshAdapter is null || _meshShader is null)
|
|
return true;
|
|
|
|
_meshReferences!.Register(emitter.Handle, gfxObjId);
|
|
ObjectRenderData? renderData = _meshAdapter.TryGetRenderData(gfxObjId);
|
|
if (renderData is null)
|
|
{
|
|
if (_meshLoadRequestedThisFrame.Add(gfxObjId))
|
|
_meshAdapter.EnsureLoaded(gfxObjId);
|
|
return true;
|
|
}
|
|
|
|
Quaternion orientation = ParticleOrientation(emitter, particle);
|
|
Matrix4x4 model = Matrix4x4.CreateScale(particle.Size)
|
|
* Matrix4x4.CreateFromQuaternion(orientation)
|
|
* Matrix4x4.CreateTranslation(particle.Position);
|
|
float viewerDistance = RetailAlphaOrdering.ComputeViewerDistance(
|
|
renderData.SortCenter,
|
|
model,
|
|
cameraWorldPosition);
|
|
float distanceSq = viewerDistance * viewerDistance;
|
|
var instance = new MeshParticleInstance(model, particle.ColorArgb, distanceSq);
|
|
|
|
for (int batchIndex = 0; batchIndex < renderData.Batches.Count; batchIndex++)
|
|
{
|
|
ObjectRenderBatch batch = renderData.Batches[batchIndex];
|
|
if (batch.IndexCount <= 0 || batch.BindlessTextureHandle == 0)
|
|
continue;
|
|
|
|
int drawIndex = _meshDrawListScratch.Count;
|
|
_meshDrawListScratch.Add(new MeshParticleDraw(
|
|
new MeshBatchKey(gfxObjId, batchIndex),
|
|
batch,
|
|
instance));
|
|
_submissionScratch.Add(new ParticleSubmission(
|
|
ParticleSubmissionKind.Mesh,
|
|
drawIndex,
|
|
distanceSq,
|
|
sequence++));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void DrawInstances(List<ParticleInstance> instances, Matrix4x4 viewProjection)
|
|
{
|
|
if (instances.Count == 0)
|
|
return;
|
|
|
|
ActivateNextDynamicBufferSet();
|
|
|
|
if (_instanceScratch.Length < instances.Count)
|
|
_instanceScratch = new BillboardGpuInstance[instances.Count + 256];
|
|
|
|
for (int i = 0; i < instances.Count; i++)
|
|
WriteBillboardGpuInstance(ref _instanceScratch[i], instances[i]);
|
|
|
|
fixed (void* bp = _instanceScratch)
|
|
UploadDynamicArrayBuffer(
|
|
_instanceVbo,
|
|
ref _instanceVboCapacityBytes,
|
|
bp,
|
|
instances.Count * sizeof(BillboardGpuInstance));
|
|
|
|
PersistActiveDynamicBufferCapacities();
|
|
PrepareBillboardPipeline(viewProjection);
|
|
_gl.DrawElementsInstanced(PrimitiveType.Triangles, 6, DrawElementsType.UnsignedInt, (void*)0, (uint)instances.Count);
|
|
}
|
|
|
|
private void UploadMeshInstances(List<MeshParticleInstance> instances)
|
|
{
|
|
ActivateNextDynamicBufferSet();
|
|
int needed = instances.Count * 20;
|
|
if (_meshInstanceScratch.Length < needed)
|
|
_meshInstanceScratch = new float[needed + 256 * 20];
|
|
|
|
for (int i = 0; i < instances.Count; i++)
|
|
WriteMeshGpuInstance(_meshInstanceScratch, i * 20, instances[i]);
|
|
|
|
fixed (void* bp = _meshInstanceScratch)
|
|
UploadDynamicArrayBuffer(
|
|
_meshInstanceVbo,
|
|
ref _meshInstanceVboCapacityBytes,
|
|
bp,
|
|
instances.Count * 20 * sizeof(float));
|
|
PersistActiveDynamicBufferCapacities();
|
|
}
|
|
|
|
private static void WriteBillboardGpuInstance(
|
|
ref BillboardGpuInstance destination,
|
|
ParticleInstance particle)
|
|
{
|
|
destination = new BillboardGpuInstance
|
|
{
|
|
Center = new Vector4(particle.Position, 0f),
|
|
AxisX = new Vector4(particle.AxisX, 0f),
|
|
AxisY = new Vector4(particle.AxisY, 0f),
|
|
Color = new Vector4(
|
|
((particle.ColorArgb >> 16) & 0xFF) / 255f,
|
|
((particle.ColorArgb >> 8) & 0xFF) / 255f,
|
|
(particle.ColorArgb & 0xFF) / 255f,
|
|
((particle.ColorArgb >> 24) & 0xFF) / 255f),
|
|
TextureHandleLow = (uint)(particle.TextureHandle & 0xFFFFFFFFu),
|
|
TextureHandleHigh = (uint)(particle.TextureHandle >> 32),
|
|
};
|
|
}
|
|
|
|
private static void WriteMeshGpuInstance(
|
|
float[] destination,
|
|
int offset,
|
|
MeshParticleInstance instance)
|
|
{
|
|
Matrix4x4 model = instance.Model;
|
|
destination[offset + 0] = model.M11;
|
|
destination[offset + 1] = model.M12;
|
|
destination[offset + 2] = model.M13;
|
|
destination[offset + 3] = model.M14;
|
|
destination[offset + 4] = model.M21;
|
|
destination[offset + 5] = model.M22;
|
|
destination[offset + 6] = model.M23;
|
|
destination[offset + 7] = model.M24;
|
|
destination[offset + 8] = model.M31;
|
|
destination[offset + 9] = model.M32;
|
|
destination[offset + 10] = model.M33;
|
|
destination[offset + 11] = model.M34;
|
|
destination[offset + 12] = model.M41;
|
|
destination[offset + 13] = model.M42;
|
|
destination[offset + 14] = model.M43;
|
|
destination[offset + 15] = model.M44;
|
|
destination[offset + 16] = ((instance.ColorArgb >> 16) & 0xFF) / 255f;
|
|
destination[offset + 17] = ((instance.ColorArgb >> 8) & 0xFF) / 255f;
|
|
destination[offset + 18] = (instance.ColorArgb & 0xFF) / 255f;
|
|
destination[offset + 19] = ((instance.ColorArgb >> 24) & 0xFF) / 255f;
|
|
}
|
|
|
|
private void ActivateNextDynamicBufferSet()
|
|
{
|
|
if (!_dynamicFrameStarted)
|
|
throw new InvalidOperationException("BeginFrame must be called before drawing particles.");
|
|
|
|
List<DynamicBufferSet> slotSets = _dynamicBufferSetsByFrame[_dynamicFrameSlot];
|
|
if (_dynamicBufferSetCursor == slotSets.Count)
|
|
slotSets.Add(CreateDynamicBufferSet());
|
|
|
|
DynamicBufferSet set = slotSets[_dynamicBufferSetCursor++];
|
|
_activeDynamicBufferSet = set;
|
|
_quadVao = set.BillboardVao;
|
|
_instanceVbo = set.BillboardInstanceVbo;
|
|
_instanceVboCapacityBytes = set.BillboardCapacityBytes;
|
|
_meshVao = set.MeshVao;
|
|
_meshInstanceVbo = set.MeshInstanceVbo;
|
|
_meshInstanceVboCapacityBytes = set.MeshCapacityBytes;
|
|
}
|
|
|
|
private DynamicBufferSet CreateDynamicBufferSet()
|
|
{
|
|
var set = new DynamicBufferSet();
|
|
try
|
|
{
|
|
set.BillboardVao = TrackedGlResource.CreateVertexArray(_gl, "creating particle billboard VAO");
|
|
set.BillboardInstanceVbo = TrackedGlResource.CreateBuffer(_gl, "creating particle billboard instance VBO");
|
|
set.MeshVao = TrackedGlResource.CreateVertexArray(_gl, "creating particle mesh VAO");
|
|
set.MeshInstanceVbo = TrackedGlResource.CreateBuffer(_gl, "creating particle mesh instance VBO");
|
|
|
|
_gl.BindVertexArray(set.BillboardVao);
|
|
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _quadVbo);
|
|
_gl.EnableVertexAttribArray(0);
|
|
_gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, 4 * sizeof(float), (void*)0);
|
|
_gl.EnableVertexAttribArray(1);
|
|
_gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 4 * sizeof(float), (void*)(2 * sizeof(float)));
|
|
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _quadEbo);
|
|
|
|
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.BillboardInstanceVbo);
|
|
uint instanceStride = (uint)sizeof(BillboardGpuInstance);
|
|
_gl.EnableVertexAttribArray(2);
|
|
_gl.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)0);
|
|
_gl.VertexAttribDivisor(2, 1);
|
|
_gl.EnableVertexAttribArray(3);
|
|
_gl.VertexAttribPointer(3, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(4 * sizeof(float)));
|
|
_gl.VertexAttribDivisor(3, 1);
|
|
_gl.EnableVertexAttribArray(4);
|
|
_gl.VertexAttribPointer(4, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(8 * sizeof(float)));
|
|
_gl.VertexAttribDivisor(4, 1);
|
|
_gl.EnableVertexAttribArray(5);
|
|
_gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(12 * sizeof(float)));
|
|
_gl.VertexAttribDivisor(5, 1);
|
|
_gl.EnableVertexAttribArray(6);
|
|
_gl.VertexAttribIPointer(
|
|
6,
|
|
2,
|
|
VertexAttribIType.UnsignedInt,
|
|
instanceStride,
|
|
(void*)(16 * sizeof(float)));
|
|
_gl.VertexAttribDivisor(6, 1);
|
|
|
|
_gl.BindVertexArray(0);
|
|
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
|
GLHelpers.ThrowOnResourceError(_gl, "configuring particle dynamic VAOs");
|
|
return set;
|
|
}
|
|
catch (Exception creationFailure)
|
|
{
|
|
try { DeleteDynamicBufferSet(set); }
|
|
catch (Exception cleanupFailure)
|
|
{
|
|
throw new AggregateException(
|
|
"Particle dynamic-buffer creation and rollback failed.",
|
|
creationFailure,
|
|
cleanupFailure);
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private void DeleteDynamicBufferSet(DynamicBufferSet set)
|
|
{
|
|
List<Exception>? failures = null;
|
|
void Attempt(Action action)
|
|
{
|
|
try { action(); }
|
|
catch (Exception ex) { (failures ??= []).Add(ex); }
|
|
}
|
|
|
|
Attempt(() => TrackedGlResource.DeleteBuffer(
|
|
_gl,
|
|
set.BillboardInstanceVbo,
|
|
set.BillboardCapacityBytes,
|
|
"deleting particle billboard instance VBO"));
|
|
Attempt(() => TrackedGlResource.DeleteBuffer(
|
|
_gl,
|
|
set.MeshInstanceVbo,
|
|
set.MeshCapacityBytes,
|
|
"deleting particle mesh instance VBO"));
|
|
Attempt(() => TrackedGlResource.DeleteVertexArray(
|
|
_gl,
|
|
set.BillboardVao,
|
|
"deleting particle billboard VAO"));
|
|
Attempt(() => TrackedGlResource.DeleteVertexArray(
|
|
_gl,
|
|
set.MeshVao,
|
|
"deleting particle mesh VAO"));
|
|
if (failures is not null)
|
|
throw new AggregateException("One or more particle dynamic resources failed to delete.", failures);
|
|
}
|
|
|
|
private void PersistActiveDynamicBufferCapacities()
|
|
{
|
|
DynamicBufferSet set = _activeDynamicBufferSet
|
|
?? throw new InvalidOperationException("No dynamic particle buffer set is active.");
|
|
set.BillboardCapacityBytes = _instanceVboCapacityBytes;
|
|
set.MeshCapacityBytes = _meshInstanceVboCapacityBytes;
|
|
}
|
|
|
|
private void UploadDynamicArrayBuffer(
|
|
uint buffer,
|
|
ref int capacityBytes,
|
|
void* data,
|
|
int byteCount)
|
|
{
|
|
if (byteCount <= 0)
|
|
throw new ArgumentOutOfRangeException(nameof(byteCount));
|
|
|
|
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, buffer);
|
|
if (capacityBytes < byteCount)
|
|
{
|
|
int grownCapacity = DynamicBufferCapacity.Grow(capacityBytes, byteCount);
|
|
TrackedGlResource.AllocateBufferStorage(
|
|
_gl,
|
|
GLEnum.ArrayBuffer,
|
|
buffer,
|
|
capacityBytes,
|
|
grownCapacity,
|
|
GLEnum.DynamicDraw,
|
|
$"growing particle dynamic buffer {buffer} to {grownCapacity} bytes");
|
|
capacityBytes = grownCapacity;
|
|
}
|
|
|
|
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)byteCount, data);
|
|
}
|
|
|
|
private void ApplyMeshCullMode(CullMode mode)
|
|
{
|
|
_gl.FrontFace(FrontFaceDirection.CW);
|
|
switch (mode)
|
|
{
|
|
case CullMode.None:
|
|
_gl.Disable(EnableCap.CullFace);
|
|
break;
|
|
case CullMode.Clockwise:
|
|
_gl.Enable(EnableCap.CullFace);
|
|
_gl.CullFace(TriangleFace.Front);
|
|
break;
|
|
case CullMode.CounterClockwise:
|
|
case CullMode.Landblock:
|
|
_gl.Enable(EnableCap.CullFace);
|
|
_gl.CullFace(TriangleFace.Back);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private TranslucencyKind ResolveMeshBlend(ObjectRenderBatch batch)
|
|
{
|
|
uint surfaceId = batch.Key.SurfaceId;
|
|
if (surfaceId == 0 || _dats is null)
|
|
return batch.IsAdditive ? TranslucencyKind.Additive : TranslucencyKind.AlphaBlend;
|
|
if (_meshBlendBySurface.TryGetValue(surfaceId, out TranslucencyKind blend))
|
|
return blend;
|
|
|
|
blend = RetailParticleBlendResolver.Resolve(
|
|
surfaceId,
|
|
batch.IsAdditive,
|
|
id => _dats.Get<Surface>(id),
|
|
Console.Error.WriteLine);
|
|
_meshBlendBySurface[surfaceId] = blend;
|
|
return blend;
|
|
}
|
|
|
|
private RetailParticleGeometryKind ResolveGeometryKind(uint gfxObjId)
|
|
{
|
|
if (_geometryKindByGfxObj.TryGetValue(gfxObjId, out RetailParticleGeometryKind kind))
|
|
return kind;
|
|
|
|
uint? firstDegradeMode = null;
|
|
try
|
|
{
|
|
if (_dats?.Get<GfxObj>(gfxObjId) is { } gfx
|
|
&& gfx.Flags.HasFlag(GfxObjFlags.HasDIDDegrade)
|
|
&& gfx.DIDDegrade != 0
|
|
&& _dats.Get<GfxObjDegradeInfo>(gfx.DIDDegrade) is { Degrades.Count: > 0 } degrade)
|
|
{
|
|
firstDegradeMode = degrade.Degrades[0].DegradeMode;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Missing/corrupt content must not invent a billboard. Cache the
|
|
// retail full-mesh choice and let WbMeshAdapter's normal missing-
|
|
// asset diagnostics decide whether geometry can be presented.
|
|
Console.Error.WriteLine(
|
|
$"[particle-geometry] Failed to decode GfxObj 0x{gfxObjId:X8} degrade metadata: {ex.Message}");
|
|
}
|
|
|
|
kind = RetailParticleGeometryClassifier.Classify(firstDegradeMode);
|
|
_geometryKindByGfxObj[gfxObjId] = kind;
|
|
return kind;
|
|
}
|
|
|
|
private void OnEmitterDied(int handle)
|
|
{
|
|
_emitterRetirements.BeginRetirement(handle);
|
|
}
|
|
|
|
private ParticleGfxInfo ResolveParticleGfxInfo(RuntimeParticleEmitter emitter)
|
|
{
|
|
if (_textures is null)
|
|
return ParticleGfxInfo.Default;
|
|
if (_particleGfxInfoByEmitter.TryGetValue(emitter.Handle, out ParticleGfxInfo resolved))
|
|
return resolved;
|
|
|
|
EmitterDesc desc = emitter.Desc;
|
|
|
|
if (desc.TextureSurfaceId != 0)
|
|
{
|
|
resolved = ParticleGfxInfo.Billboard(
|
|
_textures.AcquireParticleTexture(emitter.Handle, desc.TextureSurfaceId),
|
|
Vector2.One,
|
|
Vector3.Zero,
|
|
additive: (desc.Flags & EmitterFlags.Additive) != 0,
|
|
hasMaterial: false,
|
|
surfaceId: desc.TextureSurfaceId);
|
|
_particleGfxInfoByEmitter.Add(emitter.Handle, resolved);
|
|
return resolved;
|
|
}
|
|
|
|
uint gfxObjId = desc.HwGfxObjId != 0 ? desc.HwGfxObjId : desc.GfxObjId;
|
|
if (gfxObjId == 0 || _dats is null)
|
|
return ParticleGfxInfo.Default;
|
|
|
|
if (!_particleGfxInfoByGfxObj.TryGetValue(gfxObjId, out var info))
|
|
{
|
|
info = ReadParticleGfxInfo(gfxObjId);
|
|
_particleGfxInfoByGfxObj[gfxObjId] = info;
|
|
}
|
|
|
|
resolved = info.SurfaceId == 0
|
|
? ParticleGfxInfo.Default
|
|
: info with
|
|
{
|
|
TextureHandle = _textures.AcquireParticleTexture(
|
|
emitter.Handle,
|
|
info.SurfaceId),
|
|
};
|
|
_particleGfxInfoByEmitter.Add(emitter.Handle, resolved);
|
|
return resolved;
|
|
}
|
|
|
|
private ParticleGfxInfo ReadParticleGfxInfo(uint gfxObjId)
|
|
{
|
|
try
|
|
{
|
|
var gfx = _dats?.Get<GfxObj>(gfxObjId);
|
|
if (gfx is null)
|
|
return ParticleGfxInfo.Default;
|
|
|
|
uint surfaceId = gfx.Surfaces.Count > 0 ? gfx.Surfaces[0].DataId : 0u;
|
|
bool additive = false;
|
|
if (surfaceId != 0)
|
|
{
|
|
var surface = _dats?.Get<Surface>(surfaceId);
|
|
additive = surface is not null && surface.Type.HasFlag(SurfaceType.Additive);
|
|
}
|
|
return AuthoredParticleGfxInfo(
|
|
gfx,
|
|
texture: 0,
|
|
additive,
|
|
hasMaterial: surfaceId != 0,
|
|
surfaceId: surfaceId);
|
|
}
|
|
catch
|
|
{
|
|
return ParticleGfxInfo.Default;
|
|
}
|
|
}
|
|
|
|
private ParticleGfxInfo AuthoredParticleGfxInfo(
|
|
GfxObj gfx,
|
|
ulong texture,
|
|
bool additive,
|
|
bool hasMaterial,
|
|
uint surfaceId)
|
|
{
|
|
if (gfx.VertexArray.Vertices.Count == 0)
|
|
return ParticleGfxInfo.Billboard(
|
|
texture,
|
|
Vector2.One,
|
|
Vector3.Zero,
|
|
additive,
|
|
hasMaterial,
|
|
surfaceId);
|
|
|
|
var min = new Vector3(float.PositiveInfinity);
|
|
var max = new Vector3(float.NegativeInfinity);
|
|
foreach (var (_, v) in gfx.VertexArray.Vertices)
|
|
{
|
|
min = Vector3.Min(min, v.Origin);
|
|
max = Vector3.Max(max, v.Origin);
|
|
}
|
|
|
|
var size = max - min;
|
|
var center = (min + max) * 0.5f;
|
|
if (IsPointSprite(gfx))
|
|
{
|
|
float sx = FallbackParticleExtent(size.X) * 0.9f;
|
|
float sy = FallbackParticleExtent(size.Z) * 0.9f;
|
|
return ParticleGfxInfo.Billboard(
|
|
texture,
|
|
new Vector2(sx, sy),
|
|
center,
|
|
additive,
|
|
hasMaterial,
|
|
surfaceId);
|
|
}
|
|
|
|
Vector3 axisX;
|
|
Vector3 axisY;
|
|
Vector2 planeSize;
|
|
if (size.Y > size.X && size.Y > size.Z)
|
|
{
|
|
if (size.X > size.Z)
|
|
{
|
|
axisX = Vector3.UnitX;
|
|
axisY = Vector3.UnitY;
|
|
planeSize = new Vector2(size.X, size.Y);
|
|
}
|
|
else
|
|
{
|
|
axisX = Vector3.UnitY;
|
|
axisY = Vector3.UnitZ;
|
|
planeSize = new Vector2(size.Y, size.Z);
|
|
}
|
|
}
|
|
else if (size.X > size.Y && size.X > size.Z)
|
|
{
|
|
if (size.Z > size.Y)
|
|
{
|
|
axisX = Vector3.UnitX;
|
|
axisY = Vector3.UnitZ;
|
|
planeSize = new Vector2(size.X, size.Z);
|
|
}
|
|
else
|
|
{
|
|
axisX = Vector3.UnitX;
|
|
axisY = Vector3.UnitY;
|
|
planeSize = new Vector2(size.X, size.Y);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (size.X > size.Y)
|
|
{
|
|
axisX = Vector3.UnitX;
|
|
axisY = Vector3.UnitZ;
|
|
planeSize = new Vector2(size.X, size.Z);
|
|
}
|
|
else
|
|
{
|
|
axisX = Vector3.UnitY;
|
|
axisY = Vector3.UnitZ;
|
|
planeSize = new Vector2(size.Y, size.Z);
|
|
}
|
|
}
|
|
|
|
planeSize.X = FallbackParticleExtent(planeSize.X);
|
|
planeSize.Y = FallbackParticleExtent(planeSize.Y);
|
|
return new ParticleGfxInfo(
|
|
texture,
|
|
planeSize,
|
|
axisX,
|
|
axisY,
|
|
center,
|
|
false,
|
|
additive,
|
|
hasMaterial,
|
|
surfaceId);
|
|
}
|
|
|
|
private bool IsPointSprite(GfxObj gfx)
|
|
{
|
|
if (!gfx.Flags.HasFlag(GfxObjFlags.HasDIDDegrade) || gfx.DIDDegrade == 0 || _dats is null)
|
|
return false;
|
|
|
|
try
|
|
{
|
|
var degrade = _dats.Get<GfxObjDegradeInfo>(gfx.DIDDegrade);
|
|
return degrade?.Degrades.Count > 0 && degrade.Degrades[0].DegradeMode == 2;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static float FallbackParticleExtent(float value)
|
|
=> value > 1e-4f ? Math.Clamp(value, 1e-4f, 10_000f) : 1f;
|
|
|
|
private static Quaternion ParticleOrientation(AcDream.Core.Vfx.ParticleEmitter em, Particle p)
|
|
{
|
|
Quaternion orientation = (em.Desc.Flags & EmitterFlags.AttachLocal) != 0
|
|
? em.AnchorRot
|
|
: p.SpawnRotation;
|
|
|
|
if (em.Desc.Type is AcDream.Core.Vfx.ParticleType.ParabolicLVGAGR
|
|
or AcDream.Core.Vfx.ParticleType.ParabolicLVLALR
|
|
or AcDream.Core.Vfx.ParticleType.ParabolicGVGAGR)
|
|
{
|
|
Vector3 angular = p.C * p.Age;
|
|
float radians = angular.Length();
|
|
if (radians > 1e-6f)
|
|
orientation = Quaternion.Normalize(orientation * Quaternion.CreateFromAxisAngle(angular / radians, radians));
|
|
}
|
|
|
|
return orientation;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed || _disposing) return;
|
|
_disposing = true;
|
|
try
|
|
{
|
|
if (_disposeResources is null)
|
|
{
|
|
var releases = new List<(string Name, Action Release)>();
|
|
BuildDisposeReleases(releases);
|
|
_disposeResources = new RetryableResourceReleaseLedger(releases);
|
|
}
|
|
|
|
ResourceReleaseAttempt attempt = _disposeResources.Advance();
|
|
if (!_disposeResources.IsComplete)
|
|
{
|
|
throw attempt.ToException(
|
|
"One or more particle renderer resources could not be released.");
|
|
}
|
|
|
|
CompleteDispose();
|
|
_disposeResources = null;
|
|
_disposed = true;
|
|
|
|
if (attempt.HasFailures)
|
|
{
|
|
throw attempt.ToException(
|
|
"Particle renderer resources released with exceptional committed outcomes.");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_disposing = false;
|
|
}
|
|
}
|
|
|
|
private void BuildDisposeReleases(List<(string Name, Action Release)> releases)
|
|
{
|
|
releases.Add(("emitter-death-subscription", () =>
|
|
_particles.EmitterDied -= OnEmitterDied));
|
|
releases.Add(("emitter-resources", RetireEveryResolvedEmitter));
|
|
if (_meshReferences is not null)
|
|
releases.Add(("mesh-references", _meshReferences.Dispose));
|
|
|
|
AddTrackedBufferRelease(
|
|
releases,
|
|
_quadVbo,
|
|
16L * sizeof(float),
|
|
"quad-vbo",
|
|
"deleting particle quad VBO");
|
|
AddTrackedBufferRelease(
|
|
releases,
|
|
_quadEbo,
|
|
6L * sizeof(uint),
|
|
"quad-ebo",
|
|
"deleting particle quad EBO");
|
|
|
|
for (int frame = 0; frame < _dynamicBufferSetsByFrame.Length; frame++)
|
|
{
|
|
List<DynamicBufferSet> frameSets = _dynamicBufferSetsByFrame[frame];
|
|
for (int index = 0; index < frameSets.Count; index++)
|
|
AddDynamicBufferSetReleases(releases, frameSets[index], frame, index);
|
|
}
|
|
|
|
releases.Add(("billboard-shader", _shader.Dispose));
|
|
if (_meshShader is not null)
|
|
releases.Add(("mesh-shader", _meshShader.Dispose));
|
|
}
|
|
|
|
private void RetireEveryResolvedEmitter()
|
|
{
|
|
int[] handles = [.. _particleGfxInfoByEmitter.Keys];
|
|
for (int i = 0; i < handles.Length; i++)
|
|
_emitterRetirements.BeginRetirement(handles[i]);
|
|
_emitterRetirements.CompleteOrThrow();
|
|
}
|
|
|
|
private void AddDynamicBufferSetReleases(
|
|
List<(string Name, Action Release)> releases,
|
|
DynamicBufferSet set,
|
|
int frame,
|
|
int index)
|
|
{
|
|
AddTrackedBufferRelease(
|
|
releases,
|
|
set.BillboardInstanceVbo,
|
|
set.BillboardCapacityBytes,
|
|
$"dynamic-{frame}-{index}-billboard-vbo",
|
|
"deleting particle billboard instance VBO");
|
|
AddTrackedBufferRelease(
|
|
releases,
|
|
set.MeshInstanceVbo,
|
|
set.MeshCapacityBytes,
|
|
$"dynamic-{frame}-{index}-mesh-vbo",
|
|
"deleting particle mesh instance VBO");
|
|
AddTrackedVertexArrayRelease(
|
|
releases,
|
|
set.BillboardVao,
|
|
$"dynamic-{frame}-{index}-billboard-vao",
|
|
"deleting particle billboard VAO");
|
|
AddTrackedVertexArrayRelease(
|
|
releases,
|
|
set.MeshVao,
|
|
$"dynamic-{frame}-{index}-mesh-vao",
|
|
"deleting particle mesh VAO");
|
|
}
|
|
|
|
private void AddTrackedBufferRelease(
|
|
List<(string Name, Action Release)> releases,
|
|
uint buffer,
|
|
long capacityBytes,
|
|
string name,
|
|
string context)
|
|
{
|
|
if (buffer == 0)
|
|
return;
|
|
RetryableGpuResourceRelease release =
|
|
TrackedGlResource.CreateRetryableBufferDeletion(
|
|
_gl,
|
|
buffer,
|
|
capacityBytes,
|
|
context);
|
|
releases.Add((name, release.Run));
|
|
}
|
|
|
|
private void AddTrackedVertexArrayRelease(
|
|
List<(string Name, Action Release)> releases,
|
|
uint vertexArray,
|
|
string name,
|
|
string context)
|
|
{
|
|
if (vertexArray == 0)
|
|
return;
|
|
RetryableGpuResourceRelease release =
|
|
TrackedGlResource.CreateRetryableVertexArrayDeletion(
|
|
_gl,
|
|
vertexArray,
|
|
context);
|
|
releases.Add((name, release.Run));
|
|
}
|
|
|
|
private void CompleteDispose()
|
|
{
|
|
foreach (List<DynamicBufferSet> frameSets in _dynamicBufferSetsByFrame)
|
|
frameSets.Clear();
|
|
_activeDynamicBufferSet = null;
|
|
_dynamicFrameStarted = false;
|
|
_quadVao = 0;
|
|
_instanceVbo = 0;
|
|
_meshVao = 0;
|
|
_meshInstanceVbo = 0;
|
|
_instanceVboCapacityBytes = 0;
|
|
_meshInstanceVboCapacityBytes = 0;
|
|
_particleGfxInfoByEmitter.Clear();
|
|
_particleGfxInfoByGfxObj.Clear();
|
|
_geometryKindByGfxObj.Clear();
|
|
_meshBlendBySurface.Clear();
|
|
_deferredAlpha.Clear();
|
|
}
|
|
|
|
private readonly record struct ParticleGfxInfo(
|
|
ulong TextureHandle,
|
|
Vector2 Size,
|
|
Vector3 AxisX,
|
|
Vector3 AxisY,
|
|
Vector3 CenterOffset,
|
|
bool IsBillboard,
|
|
bool Additive,
|
|
bool HasMaterial,
|
|
uint SurfaceId)
|
|
{
|
|
public static ParticleGfxInfo Default { get; } =
|
|
Billboard(
|
|
0ul,
|
|
Vector2.One,
|
|
Vector3.Zero,
|
|
additive: false,
|
|
hasMaterial: false,
|
|
surfaceId: 0);
|
|
|
|
public static ParticleGfxInfo Billboard(
|
|
ulong textureHandle,
|
|
Vector2 size,
|
|
Vector3 centerOffset,
|
|
bool additive,
|
|
bool hasMaterial,
|
|
uint surfaceId) =>
|
|
new(
|
|
textureHandle,
|
|
size,
|
|
Vector3.UnitX,
|
|
Vector3.UnitY,
|
|
centerOffset,
|
|
true,
|
|
additive,
|
|
hasMaterial,
|
|
surfaceId);
|
|
}
|
|
}
|