perf(rendering): preserve alpha order with bindless particles
Carry each billboard particle's resident texture handle in the instance ABI so stable retail alpha ordering can batch mixed textures without rebinding and redrawing each short texture run. Keep DAT blend transitions as the only billboard draw boundary. The connected 0xC95B stress scene improved from 6 FPS / 171 ms to about 153 FPS / 6.6 ms at the same roughly 21,000 visible entities. Release build succeeds and all 5,916 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
6b0472ee32
commit
2cbf34a668
7 changed files with 150 additions and 57 deletions
|
|
@ -68,9 +68,22 @@ flush boundaries. It preserves DAT blend/cull/lighting state and batches only
|
|||
adjacent compatible submissions so renderer grouping cannot change the
|
||||
compositing order.
|
||||
|
||||
The first connected build exposed a modern-backend performance regression:
|
||||
distance sorting naturally interleaves particle textures, while the old
|
||||
billboard shader required one texture bind/draw for every texture run. After
|
||||
several portals, the dense `0xC95B` scene reached roughly 21,000 visible
|
||||
entities and fell to 6 FPS / 171 ms. This was not a retained queue or streaming
|
||||
leak. Billboard instances now carry their resident bindless texture handle in
|
||||
the vertex-instance ABI; different textures therefore remain in exact sorted
|
||||
order inside one instanced draw, with only DAT blend-mode changes splitting a
|
||||
run. The identical location recovered to about 153 FPS / 6.6 ms in the
|
||||
connected Release client.
|
||||
|
||||
**Files:** `src/AcDream.App/Rendering/RetailAlphaQueue.cs`;
|
||||
`src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs`;
|
||||
`src/AcDream.App/Rendering/ParticleRenderer.cs`;
|
||||
`src/AcDream.App/Rendering/Shaders/particle.vert`;
|
||||
`src/AcDream.App/Rendering/Shaders/particle.frag`;
|
||||
`src/AcDream.App/Rendering/RetailPViewRenderer.cs`.
|
||||
|
||||
**Research:**
|
||||
|
|
|
|||
|
|
@ -138,6 +138,10 @@ acdream-owned runtime seam above the extracted mesh pipeline. During the main
|
|||
world frame, `WbDrawDispatcher` and `ParticleRenderer` submit transparent
|
||||
GfxObj subsets and scene particles into one stable far-to-near stream keyed by
|
||||
the transformed DAT `SortCenter`; only adjacent compatible entries may batch.
|
||||
Billboard particle textures are resident bindless `sampler2DArray` handles in
|
||||
the per-instance vertex ABI, so different textures preserve that sorted order
|
||||
inside one instanced draw; only a DAT blend-mode boundary splits the run. This
|
||||
keeps dense particle fields from becoming one GL draw per alternating texture.
|
||||
`RetailPViewRenderer` drains the landscape scope before the optional depth
|
||||
clear, and `GameWindow` drains the final scope before private viewports/UI.
|
||||
Sky and sealed off-screen render targets remain independent. No DAT reader,
|
||||
|
|
|
|||
|
|
@ -177,7 +177,10 @@ The named retail client remains the behavioral oracle.
|
|||
retaining retail's missing per-`CPartCell` lists. AP-34 records that narrow
|
||||
architectural residual explicitly.
|
||||
4. The queue itself does not reorder by renderer or material. Only adjacent
|
||||
compatible submissions may batch.
|
||||
compatible submissions may batch. The modern billboard vertex-instance
|
||||
record carries a bindless texture handle, so adjacent particles with
|
||||
different textures are still one compatible ordered draw; blend mode is
|
||||
the only billboard state boundary.
|
||||
5. Every flush keeps depth testing enabled and depth writes disabled.
|
||||
6. Each submission preserves its DAT blend mode, cull mode, transform,
|
||||
texture, lighting, opacity, and selection-lighting state.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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;
|
||||
|
|
@ -20,7 +21,10 @@ namespace AcDream.App.Rendering;
|
|||
/// </summary>
|
||||
public sealed unsafe class ParticleRenderer : IDisposable
|
||||
{
|
||||
private readonly record struct BatchKey(uint TextureHandle, bool UseTexture, bool Additive);
|
||||
// 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(
|
||||
|
|
@ -39,18 +43,42 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
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, 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;
|
||||
|
|
@ -89,7 +117,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
private readonly uint _meshVao;
|
||||
private readonly uint _meshInstanceVbo;
|
||||
|
||||
private float[] _instanceScratch = new float[256 * 16];
|
||||
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
|
||||
|
|
@ -193,20 +221,33 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
|
||||
_instanceVbo = _gl.GenBuffer();
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
|
||||
_gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)(256 * 16 * sizeof(float)), (void*)0, BufferUsageARB.DynamicDraw);
|
||||
uint instanceStride = (uint)sizeof(BillboardGpuInstance);
|
||||
_gl.BufferData(
|
||||
BufferTargetARB.ArrayBuffer,
|
||||
(nuint)(256 * instanceStride),
|
||||
(void*)0,
|
||||
BufferUsageARB.DynamicDraw);
|
||||
|
||||
_gl.EnableVertexAttribArray(2);
|
||||
_gl.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, 16 * sizeof(float), (void*)0);
|
||||
_gl.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)0);
|
||||
_gl.VertexAttribDivisor(2, 1);
|
||||
_gl.EnableVertexAttribArray(3);
|
||||
_gl.VertexAttribPointer(3, 4, VertexAttribPointerType.Float, false, 16 * sizeof(float), (void*)(4 * sizeof(float)));
|
||||
_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, 16 * sizeof(float), (void*)(8 * sizeof(float)));
|
||||
_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, 16 * sizeof(float), (void*)(12 * sizeof(float)));
|
||||
_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);
|
||||
|
||||
|
|
@ -289,7 +330,6 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
_gl.Enable(EnableCap.DepthTest);
|
||||
_gl.Enable(EnableCap.Blend);
|
||||
_gl.DepthMask(false);
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
|
||||
ParticleSubmissionKind? activeKind = null;
|
||||
for (int i = 0; i < _submissionScratch.Count;)
|
||||
|
|
@ -320,8 +360,6 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
_gl.BlendFunc(
|
||||
BlendingFactor.SrcAlpha,
|
||||
key.Additive ? BlendingFactor.One : BlendingFactor.OneMinusSrcAlpha);
|
||||
_shader.SetInt("uUseTexture", key.UseTexture ? 1 : 0);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, key.UseTexture ? key.TextureHandle : 0);
|
||||
DrawInstances(_runScratch);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -383,7 +421,6 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
(int)batch.BaseVertex);
|
||||
}
|
||||
|
||||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
_gl.BindVertexArray(0);
|
||||
_gl.DepthMask(true);
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
|
|
@ -399,7 +436,6 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
_gl.Enable(EnableCap.DepthTest);
|
||||
_gl.Enable(EnableCap.Blend);
|
||||
_gl.DepthMask(false);
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
|
||||
ParticleSubmissionKind? activeKind = null;
|
||||
Matrix4x4 activeViewProjection = default;
|
||||
|
|
@ -435,8 +471,6 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
_gl.BlendFunc(
|
||||
BlendingFactor.SrcAlpha,
|
||||
key.Additive ? BlendingFactor.One : BlendingFactor.OneMinusSrcAlpha);
|
||||
_shader.SetInt("uUseTexture", key.UseTexture ? 1 : 0);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, key.UseTexture ? key.TextureHandle : 0);
|
||||
DrawInstances(_runScratch);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -501,7 +535,6 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
(int)batch.BaseVertex);
|
||||
}
|
||||
|
||||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
_gl.BindVertexArray(0);
|
||||
_gl.DepthMask(true);
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
|
|
@ -512,7 +545,6 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
{
|
||||
_shader.Use();
|
||||
_shader.SetMatrix4("uViewProjection", viewProjection);
|
||||
_shader.SetInt("uParticleTexture", 0);
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
_gl.BindVertexArray(_quadVao);
|
||||
}
|
||||
|
|
@ -598,12 +630,10 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
}
|
||||
|
||||
var gfxInfo = ResolveParticleGfxInfo(em.Desc);
|
||||
uint texture = gfxInfo.TextureHandle;
|
||||
bool useTexture = texture != 0;
|
||||
bool additive = gfxInfo.HasMaterial
|
||||
? gfxInfo.Additive
|
||||
: (em.Desc.Flags & EmitterFlags.Additive) != 0;
|
||||
var key = new BatchKey(texture, useTexture, additive);
|
||||
var key = new BatchKey(additive);
|
||||
Vector3 axisX;
|
||||
Vector3 axisY;
|
||||
if (gfxInfo.IsBillboard)
|
||||
|
|
@ -623,7 +653,15 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
float distSq = Vector3.DistanceSquared(pos, cameraWorldPos);
|
||||
|
||||
int drawIndex = draws.Count;
|
||||
draws.Add(new ParticleDraw(key, new ParticleInstance(pos, axisX, axisY, p.ColorArgb, distSq)));
|
||||
draws.Add(new ParticleDraw(
|
||||
key,
|
||||
new ParticleInstance(
|
||||
pos,
|
||||
axisX,
|
||||
axisY,
|
||||
p.ColorArgb,
|
||||
gfxInfo.TextureHandle,
|
||||
distSq)));
|
||||
_submissionScratch.Add(new ParticleSubmission(
|
||||
ParticleSubmissionKind.Billboard,
|
||||
drawIndex,
|
||||
|
|
@ -689,33 +727,25 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
if (instances.Count == 0)
|
||||
return;
|
||||
|
||||
int needed = instances.Count * 16;
|
||||
if (_instanceScratch.Length < needed)
|
||||
_instanceScratch = new float[needed + 256 * 16];
|
||||
if (_instanceScratch.Length < instances.Count)
|
||||
_instanceScratch = new BillboardGpuInstance[instances.Count + 256];
|
||||
|
||||
for (int i = 0; i < instances.Count; i++)
|
||||
{
|
||||
var p = instances[i];
|
||||
int o = i * 16;
|
||||
_instanceScratch[o + 0] = p.Position.X;
|
||||
_instanceScratch[o + 1] = p.Position.Y;
|
||||
_instanceScratch[o + 2] = p.Position.Z;
|
||||
_instanceScratch[o + 3] = 0f;
|
||||
|
||||
_instanceScratch[o + 4] = p.AxisX.X;
|
||||
_instanceScratch[o + 5] = p.AxisX.Y;
|
||||
_instanceScratch[o + 6] = p.AxisX.Z;
|
||||
_instanceScratch[o + 7] = 0f;
|
||||
|
||||
_instanceScratch[o + 8] = p.AxisY.X;
|
||||
_instanceScratch[o + 9] = p.AxisY.Y;
|
||||
_instanceScratch[o + 10] = p.AxisY.Z;
|
||||
_instanceScratch[o + 11] = 0f;
|
||||
|
||||
_instanceScratch[o + 12] = ((p.ColorArgb >> 16) & 0xFF) / 255f;
|
||||
_instanceScratch[o + 13] = ((p.ColorArgb >> 8) & 0xFF) / 255f;
|
||||
_instanceScratch[o + 14] = (p.ColorArgb & 0xFF) / 255f;
|
||||
_instanceScratch[o + 15] = ((p.ColorArgb >> 24) & 0xFF) / 255f;
|
||||
_instanceScratch[i] = new BillboardGpuInstance
|
||||
{
|
||||
Center = new Vector4(p.Position, 0f),
|
||||
AxisX = new Vector4(p.AxisX, 0f),
|
||||
AxisY = new Vector4(p.AxisY, 0f),
|
||||
Color = new Vector4(
|
||||
((p.ColorArgb >> 16) & 0xFF) / 255f,
|
||||
((p.ColorArgb >> 8) & 0xFF) / 255f,
|
||||
(p.ColorArgb & 0xFF) / 255f,
|
||||
((p.ColorArgb >> 24) & 0xFF) / 255f),
|
||||
TextureHandleLow = (uint)(p.TextureHandle & 0xFFFFFFFFu),
|
||||
TextureHandleHigh = (uint)(p.TextureHandle >> 32),
|
||||
};
|
||||
}
|
||||
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
|
||||
|
|
@ -723,7 +753,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
{
|
||||
_gl.BufferData(
|
||||
BufferTargetARB.ArrayBuffer,
|
||||
(nuint)(instances.Count * 16 * sizeof(float)),
|
||||
(nuint)(instances.Count * sizeof(BillboardGpuInstance)),
|
||||
bp,
|
||||
BufferUsageARB.DynamicDraw);
|
||||
}
|
||||
|
|
@ -853,7 +883,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
|
||||
if (desc.TextureSurfaceId != 0)
|
||||
return ParticleGfxInfo.Billboard(
|
||||
_textures.GetOrUpload(desc.TextureSurfaceId),
|
||||
_textures.GetOrUploadBindless(desc.TextureSurfaceId),
|
||||
Vector2.One,
|
||||
Vector3.Zero,
|
||||
additive: (desc.Flags & EmitterFlags.Additive) != 0,
|
||||
|
|
@ -881,7 +911,9 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
return ParticleGfxInfo.Default;
|
||||
|
||||
uint surfaceId = gfx.Surfaces.Count > 0 ? gfx.Surfaces[0].DataId : 0u;
|
||||
uint texture = surfaceId != 0 && _textures is not null ? _textures.GetOrUpload(surfaceId) : 0u;
|
||||
ulong texture = surfaceId != 0 && _textures is not null
|
||||
? _textures.GetOrUploadBindless(surfaceId)
|
||||
: 0u;
|
||||
bool additive = false;
|
||||
if (surfaceId != 0)
|
||||
{
|
||||
|
|
@ -896,7 +928,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private ParticleGfxInfo AuthoredParticleGfxInfo(GfxObj gfx, uint texture, bool additive, bool hasMaterial)
|
||||
private ParticleGfxInfo AuthoredParticleGfxInfo(GfxObj gfx, ulong texture, bool additive, bool hasMaterial)
|
||||
{
|
||||
if (gfx.VertexArray.Vertices.Count == 0)
|
||||
return ParticleGfxInfo.Billboard(texture, Vector2.One, Vector3.Zero, additive, hasMaterial);
|
||||
|
|
@ -1026,7 +1058,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
}
|
||||
|
||||
private readonly record struct ParticleGfxInfo(
|
||||
uint TextureHandle,
|
||||
ulong TextureHandle,
|
||||
Vector2 Size,
|
||||
Vector3 AxisX,
|
||||
Vector3 AxisY,
|
||||
|
|
@ -1036,10 +1068,10 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
bool HasMaterial)
|
||||
{
|
||||
public static ParticleGfxInfo Default { get; } =
|
||||
Billboard(0u, Vector2.One, Vector3.Zero, additive: false, hasMaterial: false);
|
||||
Billboard(0ul, Vector2.One, Vector3.Zero, additive: false, hasMaterial: false);
|
||||
|
||||
public static ParticleGfxInfo Billboard(
|
||||
uint textureHandle,
|
||||
ulong textureHandle,
|
||||
Vector2 size,
|
||||
Vector3 centerOffset,
|
||||
bool additive,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
#version 430 core
|
||||
#extension GL_ARB_bindless_texture : require
|
||||
|
||||
in vec2 vTex;
|
||||
in vec4 vColor;
|
||||
flat in uvec2 vTextureHandle;
|
||||
out vec4 fragColor;
|
||||
|
||||
uniform sampler2D uParticleTexture;
|
||||
uniform bool uUseTexture;
|
||||
|
||||
void main() {
|
||||
vec4 texel;
|
||||
if (uUseTexture) {
|
||||
texel = texture(uParticleTexture, vTex);
|
||||
if (any(notEqual(vTextureHandle, uvec2(0)))) {
|
||||
sampler2DArray particleTexture = sampler2DArray(vTextureHandle);
|
||||
texel = texture(particleTexture, vec3(vTex, 0.0));
|
||||
} else {
|
||||
vec2 d = vTex - vec2(0.5, 0.5);
|
||||
float r = length(d) * 2.0;
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ layout(location = 2) in vec4 aCenter;
|
|||
layout(location = 3) in vec4 aAxisX;
|
||||
layout(location = 4) in vec4 aAxisY;
|
||||
layout(location = 5) in vec4 aColor;
|
||||
layout(location = 6) in uvec2 aTextureHandle;
|
||||
|
||||
uniform mat4 uViewProjection;
|
||||
|
||||
out vec2 vTex;
|
||||
out vec4 vColor;
|
||||
flat out uvec2 vTextureHandle;
|
||||
|
||||
void main() {
|
||||
vec3 world = aCenter.xyz
|
||||
|
|
@ -22,5 +24,6 @@ void main() {
|
|||
|
||||
vTex = aTex;
|
||||
vColor = aColor;
|
||||
vTextureHandle = aTextureHandle;
|
||||
gl_Position = uViewProjection * vec4(world, 1.0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class ParticleBindlessInstanceTests
|
||||
{
|
||||
[Fact]
|
||||
public void BillboardGpuInstance_MatchesVertexAttributeAbi()
|
||||
{
|
||||
Assert.Equal(72, Marshal.SizeOf<ParticleRenderer.BillboardGpuInstance>());
|
||||
Assert.Equal(
|
||||
new IntPtr(64),
|
||||
Marshal.OffsetOf<ParticleRenderer.BillboardGpuInstance>(
|
||||
nameof(ParticleRenderer.BillboardGpuInstance.TextureHandleLow)));
|
||||
Assert.Equal(
|
||||
new IntPtr(68),
|
||||
Marshal.OffsetOf<ParticleRenderer.BillboardGpuInstance>(
|
||||
nameof(ParticleRenderer.BillboardGpuInstance.TextureHandleHigh)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BillboardShaders_ConsumeOneBindlessTextureHandlePerInstance()
|
||||
{
|
||||
string shadersDirectory = Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
"Rendering",
|
||||
"Shaders");
|
||||
string vertex = File.ReadAllText(Path.Combine(shadersDirectory, "particle.vert"));
|
||||
string fragment = File.ReadAllText(Path.Combine(shadersDirectory, "particle.frag"));
|
||||
|
||||
Assert.Contains("layout(location = 6) in uvec2 aTextureHandle;", vertex);
|
||||
Assert.Contains("flat out uvec2 vTextureHandle;", vertex);
|
||||
Assert.Contains("#extension GL_ARB_bindless_texture : require", fragment);
|
||||
Assert.Contains("sampler2DArray(vTextureHandle)", fragment);
|
||||
Assert.DoesNotContain("uniform sampler2D uParticleTexture", fragment);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue