feat(render): Campaign V slice V4t-2 — the world texture stack crosses to GpuTextureSlot
The rest of V4t. The composite, particle and shared-atlas texture paths now
hand out the device's GpuTextureSlot instead of a raw 64-bit
ARB_bindless_texture handle, and GroupKey, CachedBatch and ObjectRenderBatch
carry that slot. WbDrawDispatcher, EnvCellRenderer and ParticleRenderer retire
their interim GlBindlessHandleTable instances and share the device's one
table, exactly as V4t-1 did for terrain. Nothing about world submission
changes otherwise: these three renderers are still raw GL, still bind binding
9 themselves, and still draw the same geometry in the same order.
**What produces a slot now.** CompositeTextureArrayCache's GL backend interns
each array's handle when it makes it resident and retires the entry when it
makes it non-resident, so the pair is created and destroyed together and the
cache above it never learns a device exists — the fake backend its tests use
mints a stand-in slot. TextureCache.AcquireParticleTexture does the same for
the one-layer particle arrays it owns, including on its rollback path.
ObjectMeshManager registers each shared atlas's wrap/clamp handles at batch
upload; registration is idempotent by handle, so the many batches sharing an
atlas share its entry.
**Slot release is stricter than what it replaces, not looser.** The interim
tables never released anything — the class comment said so — and they grew
without bound. The device's table has a fixed 16,384-slot capacity, so an
unreleased entry is now a leak with an end. Every producer therefore retires
its entry: the composite backend at MakeNonResident, the particle backend at
MakeNonResident, and ObjectMeshManager when a retiring atlas's PHYSICAL
retirement completes — the point at which its handles are already non-resident
and its texture already deleted. That last one needs the handles snapshotted
at eviction, because ManagedGLTextureArray.Dispose zeroes its own copies as
its first act. Teardown deliberately does not release: the device is being torn
down alongside its callers, so there is nothing left to recycle a slot into,
and deferring work through a possibly-disposed retirement queue would turn a
clean shutdown into a throw.
**The default value became load-bearing, and that is the one real hazard here.**
BindlessTextureLocation could say "not resolved" with handle 0, because no
texture has handle 0. A slot index has no spare value — default(GpuTextureSlot)
is real slot 0 — so a positional record would have turned every
budget-rejected or still-uploading composite into a silent read of whichever
texture registered first. That is the magenta-placeholder failure shape one
layer down. The type is now a struct storing the slot one-based, so default IS
Unresolved, with a test pinning both halves: default is unresolved, and a
location naming slot 0 is resolved and distinguishable from it. Elsewhere the
sentinel is already exact — GpuTextureSlot.Unassigned is 0xFFFFFFFF, which is
common.glsl's ACDREAM_TEXTURE_NONE — so the classify path's "no texture yet"
test and the particle billboard's untextured branch are unchanged in meaning.
**GroupKey ordering is preserved because the key never ordered anything.**
Handle→slot is a bijection (the device interns one slot per resident handle),
so the same (entity, batch) pairs bucket together as before. The key reaches
equality, hashing and the scene-digest fingerprints — never a comparator:
opaque and translucent groups sort by cull mode then camera distance, the
delayed-alpha path by viewer distance then submission ordinal, and group
enumeration follows the persistent dictionary's insertion order, which a
changed hash does not disturb. The digests hash the slot index where they
hashed the handle; both sides of the render-shadow comparison compute them the
same way, so the value changing is invisible to it. Read
CompareOpaqueSubmissionOrder, CompareTransparentSubmissionOrder and
AlphaFingerprintComparer before doubting this — sort-order drift is a
pixel-visible regression class this project has hit, and it is why the check
was made before the retype rather than after.
**One visibility change, forced rather than chosen.** BindlessTextureLocation
was public and now holds an internal contract type, so it is internal;
ObjectRenderBatch.TextureSlot is internal on an otherwise public class for the
same reason. Nothing outside this assembly and its InternalsVisibleTo test
assemblies named either.
**SkyRenderer keeps its interim table**, and the report should say why: the
sky's textures are minted by SkyRenderer itself from TextureCache's raw GL
texture names, which this slice does not retype, so it would be the one
consumer registering handles it produced — a different shape from the world
stack. The offline gate also masks the sky band, so the one automated
instrument here cannot see a sky regression. V4f owns that renderer.
**Gates.** GL offline pixel gate vs cb2a70b8, measured twice: 31 and 22
differing pixels of 563,200 (5.50e-05, 3.91e-05). The first is above the
plan's documented 15-23 px band, so a control was measured rather than
assumed: two same-commit captures at this tree differ by 19 px, and — the
decisive number — a capture at V4t-1 and a capture at this commit differ by
9 px, fewer than the same-commit control. Maximum channel delta is 41-52 in
every pair including the controls, i.e. the differing pixels are drawn from
one flickering population, not from moved geometry. tools/run-repeat-connected-gate.ps1
-Runs 3: 3/3 RENDERED on both the desktop witness and the client capture. One
Vulkan composition-host run with VK_LAYER_KHRONOS_validation proven inserted
by the loader: zero errors, zero warnings, converged ownership ledger. App
tests 4,077 / 3 skips and the complete Release suite 9,140 / 5 — both the
4,075 and 9,138 baselines plus the two tests added here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b8bcaa3ef2
commit
565c351f93
17 changed files with 463 additions and 281 deletions
|
|
@ -1,3 +1,5 @@
|
|||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Gpu.Gl;
|
||||
using AcDream.Core.Textures;
|
||||
using AcDream.Core.World;
|
||||
using Silk.NET.OpenGL;
|
||||
|
|
@ -5,10 +7,63 @@ using Silk.NET.OpenGL;
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Location of one decoded entity-material composite in a resident bindless
|
||||
/// texture array. The modern mesh shader consumes this exact pair.
|
||||
/// Location of one decoded entity-material composite: the device texture-table
|
||||
/// slot of the array holding it, plus the layer within that array. The modern
|
||||
/// mesh shader consumes this exact pair.
|
||||
///
|
||||
/// <para>Campaign V slice V4t replaced the raw 64-bit
|
||||
/// <c>ARB_bindless_texture</c> handle with <see cref="GpuTextureSlot"/>, and
|
||||
/// that makes the DEFAULT value load-bearing. The old type could say "not
|
||||
/// resolved" with handle 0, because no texture ever has handle 0. A slot index
|
||||
/// has no such spare value — <c>default(GpuTextureSlot)</c> is real slot 0 — so
|
||||
/// a positional record would have turned every budget-rejected or
|
||||
/// still-uploading return into a silent read of whichever texture registered
|
||||
/// first. The slot is therefore stored one-based, which makes <c>default</c>
|
||||
/// exactly <see cref="Unresolved"/>. Same discipline, same reason, as
|
||||
/// <c>UiTextureTableHandle</c> on the UI path.</para>
|
||||
///
|
||||
/// <para>Internal rather than public because <see cref="GpuTextureSlot"/> is
|
||||
/// part of the internal RHI contract. Nothing outside this assembly and its
|
||||
/// InternalsVisibleTo test assemblies ever named this type.</para>
|
||||
/// </summary>
|
||||
public readonly record struct BindlessTextureLocation(ulong Handle, uint Layer);
|
||||
internal readonly struct BindlessTextureLocation : IEquatable<BindlessTextureLocation>
|
||||
{
|
||||
private readonly uint _slotPlusOne;
|
||||
|
||||
public BindlessTextureLocation(GpuTextureSlot slot, uint layer)
|
||||
{
|
||||
_slotPlusOne = slot.IsAssigned ? slot.Index + 1 : 0;
|
||||
Layer = layer;
|
||||
}
|
||||
|
||||
/// <summary>The "no composite yet" value. Identical to <c>default</c>.</summary>
|
||||
public static BindlessTextureLocation Unresolved => default;
|
||||
|
||||
/// <summary>Layer within the array. Meaningless unless <see cref="IsResolved"/>.</summary>
|
||||
public uint Layer { get; }
|
||||
|
||||
public bool IsResolved => _slotPlusOne != 0;
|
||||
|
||||
public GpuTextureSlot Slot =>
|
||||
_slotPlusOne == 0 ? GpuTextureSlot.Unassigned : new GpuTextureSlot(_slotPlusOne - 1);
|
||||
|
||||
public bool Equals(BindlessTextureLocation other) =>
|
||||
_slotPlusOne == other._slotPlusOne && Layer == other.Layer;
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is BindlessTextureLocation other && Equals(other);
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(_slotPlusOne, Layer);
|
||||
|
||||
public static bool operator ==(BindlessTextureLocation left, BindlessTextureLocation right) =>
|
||||
left.Equals(right);
|
||||
|
||||
public static bool operator !=(BindlessTextureLocation left, BindlessTextureLocation right) =>
|
||||
!left.Equals(right);
|
||||
|
||||
public override string ToString() =>
|
||||
IsResolved ? $"{Slot}/layer{Layer}" : "unresolved";
|
||||
}
|
||||
|
||||
internal enum CompositeTextureKind : byte
|
||||
{
|
||||
|
|
@ -74,6 +129,14 @@ internal sealed class CompositeTextureArrayResource
|
|||
{
|
||||
public required uint Name { get; init; }
|
||||
public required ulong Handle { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4t: this array's entry in the device texture table.
|
||||
/// The backend that made <see cref="Handle"/> resident also interned it, so
|
||||
/// the pair is created and retired together and the cache above never has
|
||||
/// to know a backend exists.
|
||||
/// </summary>
|
||||
public required GpuTextureSlot Slot { get; init; }
|
||||
public required int Width { get; init; }
|
||||
public required int Height { get; init; }
|
||||
public required int Capacity { get; init; }
|
||||
|
|
@ -98,11 +161,13 @@ internal sealed unsafe class GlCompositeTextureArrayBackend : ICompositeTextureA
|
|||
{
|
||||
private readonly GL _gl;
|
||||
private readonly Wb.BindlessSupport _bindless;
|
||||
private readonly GlGpuDevice _device;
|
||||
|
||||
public GlCompositeTextureArrayBackend(GL gl, Wb.BindlessSupport bindless)
|
||||
public GlCompositeTextureArrayBackend(GL gl, Wb.BindlessSupport bindless, GlGpuDevice device)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
_bindless = bindless ?? throw new ArgumentNullException(nameof(bindless));
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
_gl.GetInteger(GetPName.MaxArrayTextureLayers, out int maximumLayers);
|
||||
MaximumArrayLayers = Math.Max(1, maximumLayers);
|
||||
}
|
||||
|
|
@ -149,10 +214,15 @@ internal sealed unsafe class GlCompositeTextureArrayBackend : ICompositeTextureA
|
|||
Wb.GpuMemoryTracker.TrackResourceAllocation(Wb.GpuResourceType.Texture);
|
||||
Wb.GpuMemoryTracker.TrackAllocation(bytes, Wb.GpuResourceType.Texture);
|
||||
tracked = true;
|
||||
// Campaign V slice V4t: intern the resident handle into the device's
|
||||
// one texture table. A table-exhaustion throw here is caught by the
|
||||
// same rollback below, and nothing was added to the table if it did.
|
||||
GpuTextureSlot slot = _device.RegisterWorldTextureHandle(handle);
|
||||
return new CompositeTextureArrayResource
|
||||
{
|
||||
Name = name,
|
||||
Handle = handle,
|
||||
Slot = slot,
|
||||
Width = width,
|
||||
Height = height,
|
||||
Capacity = capacity,
|
||||
|
|
@ -248,6 +318,10 @@ internal sealed unsafe class GlCompositeTextureArrayBackend : ICompositeTextureA
|
|||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
$"releasing composite texture handle {resource.Handle} (precondition)");
|
||||
// Campaign V slice V4t: retire the table entry before the handle it
|
||||
// names stops being resident. Idempotent, so this stays correct when
|
||||
// the retryable release ledger re-runs the operation.
|
||||
_device.ReleaseWorldTextureHandle(resource.Handle);
|
||||
_bindless.MakeNonResident(resource.Handle);
|
||||
Wb.GLHelpers.ThrowOnResourceError(_gl, $"releasing composite texture handle {resource.Handle}");
|
||||
}
|
||||
|
|
@ -341,13 +415,14 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
|||
public CompositeTextureArrayCache(
|
||||
GL gl,
|
||||
Wb.BindlessSupport bindless,
|
||||
GlGpuDevice device,
|
||||
IGpuResourceRetirementQueue retirementQueue,
|
||||
long unownedBudgetBytes = DefaultUnownedBudgetBytes,
|
||||
long physicalBudgetBytes = DefaultPhysicalBudgetBytes,
|
||||
int maximumUploadsPerFrame = DefaultMaximumUploadsPerFrame,
|
||||
long maximumUploadBytesPerFrame = DefaultMaximumUploadBytesPerFrame)
|
||||
: this(
|
||||
new GlCompositeTextureArrayBackend(gl, bindless),
|
||||
new GlCompositeTextureArrayBackend(gl, bindless, device),
|
||||
retirementQueue,
|
||||
unownedBudgetBytes,
|
||||
physicalBudgetBytes,
|
||||
|
|
@ -508,7 +583,7 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
|||
ThrowIfUnavailable();
|
||||
if (!_entries.TryGetValue(key, out Entry? entry))
|
||||
{
|
||||
location = default;
|
||||
location = BindlessTextureLocation.Unresolved;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -516,7 +591,7 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
|||
_unowned.MarkOwned(key);
|
||||
entry.Atlas.LastUseSequence = ++_useSequence;
|
||||
location = new BindlessTextureLocation(
|
||||
entry.Atlas.Resource.Handle,
|
||||
entry.Atlas.Resource.Slot,
|
||||
checked((uint)entry.Layer));
|
||||
return true;
|
||||
}
|
||||
|
|
@ -542,7 +617,7 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
|||
// does not fit, stop all later decodes this frame rather than
|
||||
// repeatedly allocating RGBA buffers that cannot be uploaded.
|
||||
_uploadBudgetBlocked = true;
|
||||
location = default;
|
||||
location = BindlessTextureLocation.Unresolved;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -552,7 +627,7 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
|||
// this frame. Tick advances compatible reclamation before the next
|
||||
// frame retries the same logical composite.
|
||||
_uploadBudgetBlocked = true;
|
||||
location = default;
|
||||
location = BindlessTextureLocation.Unresolved;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -585,7 +660,7 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
|||
_owners.Acquire(ownerLocalId, key);
|
||||
_frameUploadCount++;
|
||||
_frameUploadBytes = checked(_frameUploadBytes + bytes);
|
||||
location = new BindlessTextureLocation(atlas.Resource.Handle, checked((uint)layer));
|
||||
location = new BindlessTextureLocation(atlas.Resource.Slot, checked((uint)layer));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
public readonly Vector3 AxisX;
|
||||
public readonly Vector3 AxisY;
|
||||
public readonly uint ColorArgb;
|
||||
public readonly ulong TextureHandle;
|
||||
public readonly AcDream.App.Rendering.Gpu.GpuTextureSlot TextureSlot;
|
||||
public readonly float DistanceSq;
|
||||
|
||||
public ParticleInstance(
|
||||
|
|
@ -54,14 +54,14 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
Vector3 axisX,
|
||||
Vector3 axisY,
|
||||
uint colorArgb,
|
||||
ulong textureHandle,
|
||||
AcDream.App.Rendering.Gpu.GpuTextureSlot textureSlot,
|
||||
float distanceSq)
|
||||
{
|
||||
Position = position;
|
||||
AxisX = axisX;
|
||||
AxisY = axisY;
|
||||
ColorArgb = colorArgb;
|
||||
TextureHandle = textureHandle;
|
||||
TextureSlot = textureSlot;
|
||||
DistanceSq = distanceSq;
|
||||
}
|
||||
}
|
||||
|
|
@ -124,17 +124,20 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
private readonly int _meshTextureIndexLoc = -1;
|
||||
private readonly int _meshTextureLayerLoc = -1;
|
||||
|
||||
// Campaign V slice V2c (2026-07-27): GL-only emulation of the eventual
|
||||
// Vulkan global texture descriptor array (binding=9,
|
||||
// GpuBindingModel.StorageTextureTable). Owns its own table — see
|
||||
// GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why
|
||||
// particles don't share WbDrawDispatcher's/EnvCellRenderer's/
|
||||
// TerrainModernRenderer's tables. There is no automated pixel-gate
|
||||
// coverage for particles (the offline gate's fixed outdoor view has none
|
||||
// in frame), so this indirection is kept strictly mechanical.
|
||||
private readonly GlBindlessHandleTable _textureTable = new();
|
||||
private uint _textureTableSsbo;
|
||||
private int _textureTableSsboCapacityBytes;
|
||||
// Campaign V slice V4t (2026-07-28): the interim per-renderer
|
||||
// GlBindlessHandleTable is retired. Both particle texture sources now hand
|
||||
// out the device's own GpuTextureSlot — TextureCache.AcquireParticleTexture
|
||||
// for billboards, ObjectRenderBatch.TextureSlot for mesh particles — so all
|
||||
// that is left here is flushing and binding that one table before each
|
||||
// raw-GL draw. There is still no automated pixel-gate coverage for
|
||||
// particles (the offline gate's fixed outdoor view has none in frame), so
|
||||
// this change is kept strictly mechanical, exactly as V2c's was.
|
||||
private AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
|
||||
(_meshAdapter
|
||||
?? throw new InvalidOperationException(
|
||||
"ParticleRenderer was constructed without a mesh adapter: its texture " +
|
||||
"slots come from that adapter's GL device table (Campaign V slice V4t)."))
|
||||
.WorldTextureTable;
|
||||
|
||||
private uint _quadVao;
|
||||
private readonly uint _quadVbo;
|
||||
|
|
@ -275,21 +278,6 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
_meshTextureLayerLoc = _gl.GetUniformLocation(_meshShader.Program, "uParamA");
|
||||
}
|
||||
|
||||
// Campaign V slice V2c: binding=9 texture-table SSBO (GL-only
|
||||
// emulation of the eventual Vulkan descriptor array).
|
||||
_textureTableSsbo = TrackedGlResource.CreateBuffer(
|
||||
_gl,
|
||||
"creating particle texture-table SSBO");
|
||||
RetryableGpuResourceRelease textureTableRelease =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl,
|
||||
_textureTableSsbo,
|
||||
() => _textureTableSsboCapacityBytes,
|
||||
"rolling back particle texture-table SSBO");
|
||||
constructionResources.Add(
|
||||
"particle texture-table SSBO",
|
||||
textureTableRelease.Run);
|
||||
|
||||
float[] quadVerts =
|
||||
{
|
||||
-0.5f, -0.5f, 0f, 0f,
|
||||
|
|
@ -568,7 +556,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
_gl.ProgramUniform1(
|
||||
_meshShader.Program,
|
||||
_meshTextureIndexLoc,
|
||||
_textureTable.GetOrAdd(batch.BindlessTextureHandle));
|
||||
batch.TextureSlot.Index);
|
||||
// Slice V6e: uParamA is a float, so the layer is widened here rather
|
||||
// than in the shader's float(uTextureLayer). Layers are small
|
||||
// integers; the sampled value is bit-identical.
|
||||
|
|
@ -763,7 +751,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
_gl.ProgramUniform1(
|
||||
_meshShader.Program,
|
||||
_meshTextureIndexLoc,
|
||||
_textureTable.GetOrAdd(batch.BindlessTextureHandle));
|
||||
batch.TextureSlot.Index);
|
||||
// Slice V6e: uParamA is a float, so the layer is widened here rather
|
||||
// than in the shader's float(uTextureLayer). Layers are small
|
||||
// integers; the sampled value is bit-identical.
|
||||
|
|
@ -962,7 +950,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
axisX,
|
||||
axisY,
|
||||
p.ColorArgb,
|
||||
gfxInfo.TextureHandle,
|
||||
gfxInfo.TextureSlot,
|
||||
distSq)));
|
||||
_submissionScratch.Add(new ParticleSubmission(
|
||||
ParticleSubmissionKind.Billboard,
|
||||
|
|
@ -1005,7 +993,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
for (int batchIndex = 0; batchIndex < renderData.Batches.Count; batchIndex++)
|
||||
{
|
||||
ObjectRenderBatch batch = renderData.Batches[batchIndex];
|
||||
if (batch.IndexCount <= 0 || batch.BindlessTextureHandle == 0)
|
||||
if (batch.IndexCount <= 0 || !batch.TextureSlot.IsAssigned)
|
||||
continue;
|
||||
|
||||
int drawIndex = _meshDrawListScratch.Count;
|
||||
|
|
@ -1084,9 +1072,12 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
/// </summary>
|
||||
private const uint NoTextureSlot = 0xFFFFFFFFu;
|
||||
|
||||
// Campaign V slice V2c: instance method (not static) because it converts
|
||||
// the particle's raw bindless handle to a _textureTable slot.
|
||||
private void WriteBillboardGpuInstance(
|
||||
// Campaign V slice V4t: static again — the particle already carries the
|
||||
// device's table slot, so there is no per-renderer interning left to do.
|
||||
// GpuTextureSlot.Unassigned and NoTextureSlot are the same 0xFFFFFFFF by
|
||||
// construction (the contract's sentinel IS ACDREAM_TEXTURE_NONE), so the
|
||||
// untextured branch collapses into the assignment rather than disappearing.
|
||||
private static void WriteBillboardGpuInstance(
|
||||
ref BillboardGpuInstance destination,
|
||||
ParticleInstance particle)
|
||||
{
|
||||
|
|
@ -1100,9 +1091,9 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
((particle.ColorArgb >> 8) & 0xFF) / 255f,
|
||||
(particle.ColorArgb & 0xFF) / 255f,
|
||||
((particle.ColorArgb >> 24) & 0xFF) / 255f),
|
||||
TextureIndex = particle.TextureHandle == 0UL
|
||||
? NoTextureSlot
|
||||
: _textureTable.GetOrAdd(particle.TextureHandle),
|
||||
TextureIndex = particle.TextureSlot.IsAssigned
|
||||
? particle.TextureSlot.Index
|
||||
: NoTextureSlot,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1282,45 +1273,22 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V2c: uploads <see cref="_textureTable"/>'s handles to
|
||||
/// <see cref="_textureTableSsbo"/> when a new one was registered since the
|
||||
/// last flush (by <see cref="WriteBillboardGpuInstance"/> or either mesh
|
||||
/// draw site's <c>_textureTable.GetOrAdd</c>), then (re)binds it at
|
||||
/// Campaign V slice V4t: drains the device texture table's dirty runs and
|
||||
/// (re)binds it at
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
|
||||
/// Called immediately before every draw call rather than once per pipeline
|
||||
/// switch: a run of consecutive mesh-particle sub-batches can register a
|
||||
/// new handle partway through, and the table must be current for each one.
|
||||
/// Still called immediately before every draw call rather than once per
|
||||
/// pipeline switch, for the reason V2c gave: a run of consecutive
|
||||
/// mesh-particle sub-batches can pull in a texture whose slot was registered
|
||||
/// this frame, and the table must be current for each one.
|
||||
/// </summary>
|
||||
private void FlushAndBindTextureTable()
|
||||
{
|
||||
if (_textureTable.Dirty)
|
||||
{
|
||||
ReadOnlySpan<ulong> handles = _textureTable.Handles;
|
||||
int byteCount = handles.Length * sizeof(ulong);
|
||||
fixed (ulong* p = handles)
|
||||
{
|
||||
_gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, _textureTableSsbo);
|
||||
if (_textureTableSsboCapacityBytes < byteCount)
|
||||
{
|
||||
int grown = DynamicBufferCapacity.Grow(_textureTableSsboCapacityBytes, byteCount);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
_textureTableSsbo,
|
||||
_textureTableSsboCapacityBytes,
|
||||
grown,
|
||||
GLEnum.DynamicDraw,
|
||||
"growing particle texture-table SSBO");
|
||||
_textureTableSsboCapacityBytes = grown;
|
||||
}
|
||||
_gl.BufferSubData(BufferTargetARB.ShaderStorageBuffer, 0, (nuint)byteCount, p);
|
||||
}
|
||||
_textureTable.MarkFlushed();
|
||||
}
|
||||
AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device = WorldTextureTable;
|
||||
device.FlushTextureTable();
|
||||
_gl.BindBufferBase(
|
||||
BufferTargetARB.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
|
||||
_textureTableSsbo);
|
||||
device.TextureTableGlName);
|
||||
}
|
||||
|
||||
private void ApplyMeshCullMode(CullMode mode)
|
||||
|
|
@ -1431,7 +1399,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
? ParticleGfxInfo.Default
|
||||
: info with
|
||||
{
|
||||
TextureHandle = _textures.AcquireParticleTexture(
|
||||
TextureSlot = _textures.AcquireParticleTexture(
|
||||
emitter.Handle,
|
||||
info.SurfaceId),
|
||||
};
|
||||
|
|
@ -1456,7 +1424,9 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
}
|
||||
return AuthoredParticleGfxInfo(
|
||||
gfx,
|
||||
texture: 0,
|
||||
// Shape only: the caller re-resolves the slot per emitter, so
|
||||
// this record is cached with no texture rather than slot 0.
|
||||
texture: AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned,
|
||||
additive,
|
||||
hasMaterial: surfaceId != 0,
|
||||
surfaceId: surfaceId);
|
||||
|
|
@ -1469,7 +1439,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
|
||||
private ParticleGfxInfo AuthoredParticleGfxInfo(
|
||||
GfxObj gfx,
|
||||
ulong texture,
|
||||
AcDream.App.Rendering.Gpu.GpuTextureSlot texture,
|
||||
bool additive,
|
||||
bool hasMaterial,
|
||||
uint surfaceId)
|
||||
|
|
@ -1663,13 +1633,6 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
6L * sizeof(uint),
|
||||
"quad-ebo",
|
||||
"deleting particle quad EBO");
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
_textureTableSsbo,
|
||||
_textureTableSsboCapacityBytes,
|
||||
"texture-table",
|
||||
"deleting particle texture-table SSBO");
|
||||
|
||||
for (int frame = 0; frame < _dynamicBufferSetsByFrame.Length; frame++)
|
||||
{
|
||||
List<DynamicBufferSet> frameSets = _dynamicBufferSetsByFrame[frame];
|
||||
|
|
@ -1766,8 +1729,6 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
_meshInstanceVbo = 0;
|
||||
_instanceVboCapacityBytes = 0;
|
||||
_meshInstanceVboCapacityBytes = 0;
|
||||
_textureTableSsbo = 0;
|
||||
_textureTableSsboCapacityBytes = 0;
|
||||
_particleGfxInfoByEmitter.Clear();
|
||||
_particleGfxInfoByGfxObj.Clear();
|
||||
_geometryKindByGfxObj.Clear();
|
||||
|
|
@ -1776,7 +1737,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
}
|
||||
|
||||
private readonly record struct ParticleGfxInfo(
|
||||
ulong TextureHandle,
|
||||
AcDream.App.Rendering.Gpu.GpuTextureSlot TextureSlot,
|
||||
Vector2 Size,
|
||||
Vector3 AxisX,
|
||||
Vector3 AxisY,
|
||||
|
|
@ -1788,7 +1749,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
{
|
||||
public static ParticleGfxInfo Default { get; } =
|
||||
Billboard(
|
||||
0ul,
|
||||
AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned,
|
||||
Vector2.One,
|
||||
Vector3.Zero,
|
||||
additive: false,
|
||||
|
|
@ -1796,14 +1757,14 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
surfaceId: 0);
|
||||
|
||||
public static ParticleGfxInfo Billboard(
|
||||
ulong textureHandle,
|
||||
AcDream.App.Rendering.Gpu.GpuTextureSlot textureSlot,
|
||||
Vector2 size,
|
||||
Vector3 centerOffset,
|
||||
bool additive,
|
||||
bool hasMaterial,
|
||||
uint surfaceId) =>
|
||||
new(
|
||||
textureHandle,
|
||||
textureSlot,
|
||||
size,
|
||||
Vector3.UnitX,
|
||||
Vector3.UnitY,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ internal sealed class StandaloneBindlessTextureResource
|
|||
public required uint SurfaceId { get; init; }
|
||||
public required uint Name { get; init; }
|
||||
public required ulong Handle { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4t: this texture's entry in the device texture table.
|
||||
/// Created with <see cref="Handle"/>'s residency and retired with it, so a
|
||||
/// particle batch carries a backend-neutral slot rather than a GL handle.
|
||||
/// </summary>
|
||||
public required Gpu.GpuTextureSlot Slot { get; init; }
|
||||
public required long Bytes { get; init; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ public sealed unsafe class TextureCache
|
|||
composite = new CompositeTextureArrayCache(
|
||||
gl!,
|
||||
bindless,
|
||||
WorldDevice,
|
||||
retirementQueue,
|
||||
budgets.CompositeUnownedBytes,
|
||||
budgets.CompositePhysicalBytes);
|
||||
|
|
@ -199,6 +200,20 @@ public sealed unsafe class TextureCache
|
|||
"(Texture2D upload, particle arrays, composite/bindless caches) are " +
|
||||
"unavailable until Campaign V slice V4t ports them onto the RHI.");
|
||||
|
||||
/// <summary>
|
||||
/// The GL backend's device, for the world texture paths' table
|
||||
/// registrations (Campaign V slice V4t). Those paths already require a GL
|
||||
/// context — see <see cref="Gl"/> — so the same construction that makes
|
||||
/// <see cref="_gl"/> non-null makes this cast sound; a Vulkan-composed
|
||||
/// cache serves only the UI path through <see cref="IGpuDevice"/> and never
|
||||
/// reaches here.
|
||||
/// </summary>
|
||||
private GlGpuDevice WorldDevice => _device as GlGpuDevice
|
||||
?? throw new InvalidOperationException(
|
||||
"This TextureCache's device is not the GL backend's: the world " +
|
||||
"texture paths intern their bindless handles into GlGpuDevice's " +
|
||||
"texture table (Campaign V slice V4t).");
|
||||
|
||||
internal void RegisterResidencySources(ResidencyManager manager)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manager);
|
||||
|
|
@ -435,8 +450,13 @@ public sealed unsafe class TextureCache
|
|||
/// Acquires the exact DAT-decoded one-layer texture array for a live
|
||||
/// particle emitter. Equivalent surfaces are shared; the cache ownership
|
||||
/// ends with <see cref="ReleaseParticleTextureOwner"/>.
|
||||
///
|
||||
/// <para>Campaign V slice V4t: returns the device texture-table
|
||||
/// <see cref="GpuTextureSlot"/> rather than the raw bindless handle. The
|
||||
/// handle is still created, made resident and destroyed here — only the
|
||||
/// table entry belongs to the device.</para>
|
||||
/// </summary>
|
||||
internal ulong AcquireParticleTexture(int emitterHandle, uint surfaceId)
|
||||
internal GpuTextureSlot AcquireParticleTexture(int emitterHandle, uint surfaceId)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(emitterHandle);
|
||||
ArgumentOutOfRangeException.ThrowIfZero(surfaceId);
|
||||
|
|
@ -447,7 +467,7 @@ public sealed unsafe class TextureCache
|
|||
surfaceId,
|
||||
out StandaloneBindlessTextureResource? existing))
|
||||
{
|
||||
return existing.Handle;
|
||||
return existing.Slot;
|
||||
}
|
||||
|
||||
DecodedTexture decoded = DecodeFromDats(
|
||||
|
|
@ -462,15 +482,17 @@ public sealed unsafe class TextureCache
|
|||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
Gl,
|
||||
$"making particle surface 0x{surfaceId:X8} resident");
|
||||
GpuTextureSlot slot = WorldDevice.RegisterWorldTextureHandle(handle);
|
||||
var resource = new StandaloneBindlessTextureResource
|
||||
{
|
||||
SurfaceId = surfaceId,
|
||||
Name = name,
|
||||
Handle = handle,
|
||||
Slot = slot,
|
||||
Bytes = checked((long)decoded.Width * decoded.Height * 4L),
|
||||
};
|
||||
textures.AddAndAcquire(ownerId, resource);
|
||||
return handle;
|
||||
return slot;
|
||||
}
|
||||
catch (Exception residencyFailure)
|
||||
{
|
||||
|
|
@ -486,6 +508,10 @@ public sealed unsafe class TextureCache
|
|||
{
|
||||
Attempt(() =>
|
||||
{
|
||||
// Slice V4t: the table entry may or may not have been made
|
||||
// before the failure. Releasing an unregistered handle is a
|
||||
// no-op, so this covers both without asking which.
|
||||
WorldDevice.ReleaseWorldTextureHandle(handle);
|
||||
_bindless!.MakeNonResident(handle);
|
||||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
Gl,
|
||||
|
|
@ -681,6 +707,9 @@ public sealed unsafe class TextureCache
|
|||
{
|
||||
public void MakeNonResident(StandaloneBindlessTextureResource resource)
|
||||
{
|
||||
// Slice V4t: retire the table entry before its handle stops being
|
||||
// resident. Idempotent, so a retried release stays correct.
|
||||
owner.WorldDevice.ReleaseWorldTextureHandle(resource.Handle);
|
||||
owner._bindless!.MakeNonResident(resource.Handle);
|
||||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
owner.Gl,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
|
|
@ -22,7 +23,7 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// </summary>
|
||||
internal readonly record struct CachedBatch(
|
||||
GroupKey Key,
|
||||
ulong BindlessTextureHandle,
|
||||
GpuTextureSlot TextureSlot,
|
||||
Matrix4x4 RestPose,
|
||||
Vector3 LocalSortCenter = default,
|
||||
WbDrawDispatcher.InstanceGroup? Group = null,
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ internal sealed class EntityClassificationCache
|
|||
/// field has drifted from live state.
|
||||
///
|
||||
/// <para>
|
||||
/// Caller passes per-batch live state (Key, BindlessTextureHandle, RestPose)
|
||||
/// Caller passes per-batch live state (Key, TextureSlot, RestPose)
|
||||
/// reconstructed from the same path the populate ran. The cache iterates
|
||||
/// its stored entries in parallel and asserts equality.
|
||||
/// </para>
|
||||
|
|
@ -181,8 +181,8 @@ internal sealed class EntityClassificationCache
|
|||
cached.Key.Equals(live.Key),
|
||||
$"EntityClassificationCache: GroupKey drift for entity {entityId} batch {i}");
|
||||
System.Diagnostics.Debug.Assert(
|
||||
cached.BindlessTextureHandle == live.BindlessTextureHandle,
|
||||
$"EntityClassificationCache: texture handle drift for entity {entityId} batch {i}");
|
||||
cached.TextureSlot == live.TextureSlot,
|
||||
$"EntityClassificationCache: texture slot drift for entity {entityId} batch {i}");
|
||||
System.Diagnostics.Debug.Assert(
|
||||
MatrixApproxEqual(cached.RestPose, live.RestPose, epsilon: 1e-5f),
|
||||
$"EntityClassificationCache: RestPose drift for entity {entityId} batch {i}");
|
||||
|
|
|
|||
|
|
@ -153,18 +153,19 @@ public sealed unsafe class EnvCellRenderer :
|
|||
private uint _sharedClipRegionSsbo;
|
||||
private uint _fallbackClipRegionSsbo;
|
||||
|
||||
// Campaign V slice V2 (2026-07-27): GL-only emulation of the eventual
|
||||
// Vulkan global texture descriptor array (binding=9,
|
||||
// GpuBindingModel.StorageTextureTable). Owns its own table rather than
|
||||
// sharing WbDrawDispatcher's — EnvCellRenderer never had a TextureCache
|
||||
// dependency and nothing requires index agreement between renderers (each
|
||||
// rebinds its own buffer to binding=9 immediately before its own draw
|
||||
// call). See GlBindlessHandleTable's doc comment and the campaign doc's
|
||||
// §5.2. Lazily created; grown/uploaded only when a genuinely new handle
|
||||
// appears (rare — see FlushAndBindTextureTable).
|
||||
private readonly GlBindlessHandleTable _textureTable = new();
|
||||
private uint _textureTableSsbo;
|
||||
private int _textureTableSsboCapacityBytes;
|
||||
// Campaign V slice V4t (2026-07-28): the interim per-renderer
|
||||
// GlBindlessHandleTable is retired. ObjectRenderBatch already carries the
|
||||
// device's own GpuTextureSlot, so this renderer shares WbDrawDispatcher's
|
||||
// table — the device's — and only has to flush and bind it before its own
|
||||
// raw-GL draws. That also removes the V2 caveat that two renderers could
|
||||
// legitimately number the same texture differently: there is now one
|
||||
// numbering, and it is the one the mesh manager assigned at upload.
|
||||
private AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
|
||||
(_meshManager
|
||||
?? throw new InvalidOperationException(
|
||||
"EnvCellRenderer was constructed without a mesh manager: its texture " +
|
||||
"slots come from that manager's GL device table (Campaign V slice V4t)."))
|
||||
.WorldTextureTable;
|
||||
|
||||
// Reusable scratch arrays — avoid per-frame allocation.
|
||||
// WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>()
|
||||
|
|
@ -283,7 +284,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
cullModes.Add(b.CullMode);
|
||||
if (b.IsTransparent) translucent++;
|
||||
if (b.IsAdditive) additive++;
|
||||
if (b.BindlessTextureHandle == 0) zeroHandle++;
|
||||
if (!b.TextureSlot.IsAssigned) zeroHandle++;
|
||||
}
|
||||
var cullList = string.Join(",", cullModes);
|
||||
lines.Add(
|
||||
|
|
@ -1201,7 +1202,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
var rd = _meshManager.TryGetRenderData(gfxObjId);
|
||||
if (rd != null)
|
||||
foreach (var b in rd.Batches)
|
||||
{ batch++; idx += b.IndexCount; if (b.IsTransparent) tr++; if (b.BindlessTextureHandle == 0) zh++; }
|
||||
{ batch++; idx += b.IndexCount; if (b.IsTransparent) tr++; if (!b.TextureSlot.IsAssigned) zh++; }
|
||||
}
|
||||
sb.Append(" [0x").Append(cellId.ToString("X8"))
|
||||
.Append(":gfx=").Append(gfxN).Append(" tf=").Append(tf)
|
||||
|
|
@ -1625,9 +1626,9 @@ public sealed unsafe class EnvCellRenderer :
|
|||
_modernBatches[cmdIndex] = new ModernBatchData
|
||||
{
|
||||
// Campaign V slice V2: table slot, not the raw handle.
|
||||
// See _textureTable's doc comment for why EnvCellRenderer
|
||||
// owns its own table rather than sharing WbDrawDispatcher's.
|
||||
TextureTableIndex = _textureTable.GetOrAdd(item.batch.BindlessTextureHandle),
|
||||
// Slice V4t: the slot is the device's, assigned by the
|
||||
// mesh manager at upload rather than interned here.
|
||||
TextureTableIndex = item.batch.TextureSlot.Index,
|
||||
TextureIndex = (uint)item.batch.TextureIndex,
|
||||
};
|
||||
|
||||
|
|
@ -1992,46 +1993,22 @@ public sealed unsafe class EnvCellRenderer :
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Uploads <see cref="_textureTable"/>'s handles to <see cref="_textureTableSsbo"/>
|
||||
/// when a new one was registered since the last flush, then (re)binds it at
|
||||
/// Campaign V slice V4t: drains the device texture table's dirty runs and
|
||||
/// (re)binds it at
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
|
||||
/// A genuinely new handle is rare — new dat surfaces/atlases, not every
|
||||
/// frame — so this is not part of the ring-buffered per-frame SSBO set;
|
||||
/// see GlBindlessHandleTable's doc comment.
|
||||
/// A genuinely new slot is rare — new dat surfaces/atlases, not every frame
|
||||
/// — but the bind is unconditional, because GL's storage-buffer binding
|
||||
/// points are global and another raw-GL renderer's binding 9 sits there
|
||||
/// between two of these draws.
|
||||
/// </summary>
|
||||
private void FlushAndBindTextureTable()
|
||||
{
|
||||
if (_textureTableSsbo == 0)
|
||||
_textureTableSsbo = TrackedGlResource.CreateBuffer(_gl, "creating EnvCell texture-table SSBO");
|
||||
|
||||
if (_textureTable.Dirty)
|
||||
{
|
||||
ReadOnlySpan<ulong> handles = _textureTable.Handles;
|
||||
int byteCount = handles.Length * sizeof(ulong);
|
||||
fixed (ulong* p = handles)
|
||||
{
|
||||
if (_textureTableSsboCapacityBytes < byteCount)
|
||||
{
|
||||
int grown = DynamicBufferCapacity.Grow(_textureTableSsboCapacityBytes, byteCount);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
_textureTableSsbo,
|
||||
_textureTableSsboCapacityBytes,
|
||||
grown,
|
||||
GLEnum.DynamicDraw,
|
||||
"growing EnvCell texture-table SSBO");
|
||||
_textureTableSsboCapacityBytes = grown;
|
||||
}
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _textureTableSsbo);
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0, (nuint)byteCount, p);
|
||||
}
|
||||
_textureTable.MarkFlushed();
|
||||
}
|
||||
AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device = WorldTextureTable;
|
||||
device.FlushTextureTable();
|
||||
_gl.BindBufferBase(
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
|
||||
_textureTableSsbo);
|
||||
device.TextureTableGlName);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -2202,12 +2179,6 @@ public sealed unsafe class EnvCellRenderer :
|
|||
AcDream.App.Rendering.ClipFrame.CellClipStrideBytes,
|
||||
"fallback-clip-region",
|
||||
"deleting EnvCell fallback clip SSBO");
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
_textureTableSsbo,
|
||||
_textureTableSsboCapacityBytes,
|
||||
"texture-table",
|
||||
"deleting EnvCell texture-table SSBO");
|
||||
_disposeResources = new RetryableResourceReleaseLedger(releases);
|
||||
}
|
||||
|
||||
|
|
@ -2229,8 +2200,6 @@ public sealed unsafe class EnvCellRenderer :
|
|||
_globalLightsSsbo = 0;
|
||||
_instLightSetSsbo = 0;
|
||||
_fallbackClipRegionSsbo = 0;
|
||||
_textureTableSsbo = 0;
|
||||
_textureTableSsboCapacityBytes = 0;
|
||||
_disposeResources = null;
|
||||
IsDisposed = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.Core.Meshing;
|
||||
using DatReaderWriter.Enums;
|
||||
|
||||
|
|
@ -10,12 +11,23 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// <c>internal</c> at file scope (was a private nested type) so
|
||||
/// <see cref="EntityClassificationCache"/> can store it inside <see cref="CachedBatch"/>
|
||||
/// without depending on dispatcher internals.
|
||||
///
|
||||
/// <para>Campaign V slice V4t replaced the raw 64-bit
|
||||
/// <c>ARB_bindless_texture</c> handle with <see cref="GpuTextureSlot"/>. The
|
||||
/// substitution is a bijection — the device interns one slot per resident
|
||||
/// handle — so exactly the same (entity, batch) pairs bucket together as
|
||||
/// before, which is the property that keeps submission order identical. This
|
||||
/// key's VALUE never orders anything: groups are enumerated in the dictionary's
|
||||
/// insertion order and sorted by cull mode then camera distance
|
||||
/// (<c>CompareOpaqueSubmissionOrder</c> / <c>CompareTransparentSubmissionOrder</c>),
|
||||
/// and the delayed-alpha path sorts by viewer distance then submission ordinal.
|
||||
/// The key reaches only equality, hashing, and the scene-digest fingerprints.</para>
|
||||
/// </summary>
|
||||
internal readonly record struct GroupKey(
|
||||
uint FirstIndex,
|
||||
int BaseVertex,
|
||||
int IndexCount,
|
||||
ulong BindlessTextureHandle,
|
||||
GpuTextureSlot TextureSlot,
|
||||
uint TextureLayer,
|
||||
TranslucencyKind Translucency,
|
||||
CullMode CullMode = CullMode.CounterClockwise);
|
||||
|
|
|
|||
|
|
@ -93,7 +93,20 @@ namespace AcDream.App.Rendering.Wb
|
|||
// Modern rendering path fields
|
||||
public uint FirstIndex { get; set; }
|
||||
public uint BaseVertex { get; set; }
|
||||
public ulong BindlessTextureHandle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4t: the shared atlas's entry in the device texture
|
||||
/// table, replacing the raw 64-bit <c>ARB_bindless_texture</c> handle
|
||||
/// this used to carry. Wrap and clamp addressing are two different
|
||||
/// entries because a bindless handle bakes its sampler — see
|
||||
/// <see cref="HasWrappingUVs"/>, which selects between them.
|
||||
///
|
||||
/// <para><c>internal</c> on an otherwise public type because
|
||||
/// <c>GpuTextureSlot</c> belongs to the internal RHI contract. Every
|
||||
/// reader is a renderer inside this assembly.</para>
|
||||
/// </summary>
|
||||
internal AcDream.App.Rendering.Gpu.GpuTextureSlot TextureSlot { get; set; }
|
||||
= AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -107,6 +120,19 @@ namespace AcDream.App.Rendering.Wb
|
|||
private readonly IPreparedAssetSource _preparedAssets;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4t: the GL backend's device, whose texture table
|
||||
/// every shared atlas's bindless handle is interned into. Also the
|
||||
/// device the two world renderers this class feeds
|
||||
/// (<c>WbDrawDispatcher</c>, <c>EnvCellRenderer</c>) flush and bind
|
||||
/// through — they take it from here rather than from a second
|
||||
/// composition wire, because a batch's slot and the table that resolves
|
||||
/// it must come from the same device by construction.
|
||||
/// </summary>
|
||||
private readonly AcDream.App.Rendering.Gpu.Gl.GlGpuDevice _worldTextureTable;
|
||||
|
||||
internal AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable => _worldTextureTable;
|
||||
|
||||
/// <summary>
|
||||
/// The immutable prepared-payload source is injected by composition.
|
||||
/// Production uses the validated pak; UI Studio explicitly supplies the
|
||||
|
|
@ -164,6 +190,14 @@ namespace AcDream.App.Rendering.Wb
|
|||
// the owners here makes that overlap both retryable and observable.
|
||||
private readonly List<TextureAtlasManager> _retiringAtlases = [];
|
||||
|
||||
// Campaign V slice V4t: the two bindless handles a retiring atlas had
|
||||
// when it left the live set. ManagedGLTextureArray.Dispose zeroes its
|
||||
// own copies as its first act, so the values must be snapshotted at the
|
||||
// moment of eviction to be releasable from the device's texture table
|
||||
// once physical retirement completes.
|
||||
private readonly Dictionary<TextureAtlasManager, (ulong Wrap, ulong Clamp)>
|
||||
_retiringAtlasTextureHandles = [];
|
||||
|
||||
// CPU-side cache for prepared mesh data (to avoid re-reading/decoding from DAT)
|
||||
private readonly CpuMeshUploadCache _cpuMeshCache;
|
||||
|
||||
|
|
@ -421,6 +455,10 @@ namespace AcDream.App.Rendering.Wb
|
|||
_graphicsDevice = graphicsDevice
|
||||
?? throw new ArgumentNullException(nameof(graphicsDevice));
|
||||
ArgumentNullException.ThrowIfNull(gpuDevice);
|
||||
// Slice V4t: this class only ever exists on GL — it takes an
|
||||
// OpenGLGraphicsDevice — so the backend cast states that fact rather
|
||||
// than narrowing anything.
|
||||
_worldTextureTable = (AcDream.App.Rendering.Gpu.Gl.GlGpuDevice)gpuDevice;
|
||||
_preparedAssets = preparedAssets
|
||||
?? throw new ArgumentNullException(nameof(preparedAssets));
|
||||
_logger = logger
|
||||
|
|
@ -544,6 +582,9 @@ namespace AcDream.App.Rendering.Wb
|
|||
}
|
||||
_dirtyAtlases.Remove(victim);
|
||||
_retiringAtlases.Add(victim);
|
||||
_retiringAtlasTextureHandles[victim] = (
|
||||
victim.TextureArray.BindlessWrapHandle,
|
||||
victim.TextureArray.BindlessClampHandle);
|
||||
victim.Dispose();
|
||||
RemoveCompletedAtlasRetirements();
|
||||
return true;
|
||||
|
|
@ -560,11 +601,29 @@ namespace AcDream.App.Rendering.Wb
|
|||
{
|
||||
for (int i = _retiringAtlases.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_retiringAtlases[i].IsPhysicalRetirementComplete)
|
||||
_retiringAtlases.RemoveAt(i);
|
||||
if (!_retiringAtlases[i].IsPhysicalRetirementComplete)
|
||||
continue;
|
||||
// Campaign V slice V4t: the array's handles are non-resident and
|
||||
// its texture deleted by the time physical retirement reports
|
||||
// complete, so this is the point at which its two table entries
|
||||
// stop naming anything. Without it, a session that churns atlases
|
||||
// would accumulate entries against the table's fixed capacity —
|
||||
// the interim per-renderer tables grew without bound instead, so
|
||||
// this is stricter than what it replaces, not looser. The
|
||||
// device defers the index itself behind its retirement queue.
|
||||
ReleaseAtlasTextureSlots(_retiringAtlases[i]);
|
||||
_retiringAtlases.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseAtlasTextureSlots(TextureAtlasManager atlas)
|
||||
{
|
||||
if (!_retiringAtlasTextureHandles.Remove(atlas, out (ulong Wrap, ulong Clamp) handles))
|
||||
return;
|
||||
_worldTextureTable.ReleaseWorldTextureHandle(handles.Wrap);
|
||||
_worldTextureTable.ReleaseWorldTextureHandle(handles.Clamp);
|
||||
}
|
||||
|
||||
private void OnAtlasGpuSafeEmpty(TextureAtlasManager atlas)
|
||||
{
|
||||
if (IsDisposed || !atlas.IsGpuSafeEmpty || _safeEmptyAtlases.Contains(atlas))
|
||||
|
|
@ -2036,9 +2095,18 @@ namespace AcDream.App.Rendering.Wb
|
|||
legacyIndexBuffers.Add((ibo, indexArray.Length * sizeof(ushort)));
|
||||
}
|
||||
|
||||
// Campaign V slice V4t: intern the atlas's resident
|
||||
// handle into the device's one texture table and carry
|
||||
// the slot. Registration is idempotent by handle, so the
|
||||
// many batches sharing an atlas share its entry;
|
||||
// ManagedGLTextureArray still owns the residency and the
|
||||
// GL texture, and the entry is retired when the array's
|
||||
// physical retirement completes.
|
||||
ulong bindlessHandle = batch.HasWrappingUVs
|
||||
? atlasManager.TextureArray.BindlessWrapHandle
|
||||
: atlasManager.TextureArray.BindlessClampHandle;
|
||||
AcDream.App.Rendering.Gpu.GpuTextureSlot textureSlot =
|
||||
_worldTextureTable.RegisterWorldTextureHandle(bindlessHandle);
|
||||
|
||||
renderBatches.Add(new ObjectRenderBatch
|
||||
{
|
||||
|
|
@ -2056,7 +2124,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
CullMode = batch.CullMode,
|
||||
FirstIndex = firstIndex,
|
||||
BaseVertex = (uint)batchBaseVertex,
|
||||
BindlessTextureHandle = bindlessHandle,
|
||||
TextureSlot = textureSlot,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -2737,6 +2805,12 @@ namespace AcDream.App.Rendering.Wb
|
|||
_uploadRollbackQueue.Clear();
|
||||
_globalAtlases.Clear();
|
||||
_retiringAtlases.Clear();
|
||||
// Slice V4t: teardown drops the snapshots without releasing their
|
||||
// table entries. The device is torn down alongside this manager, so
|
||||
// there is nothing left to recycle a slot into — and asking a
|
||||
// possibly-already-disposed device to defer work through its
|
||||
// retirement queue would turn a clean shutdown into a throw.
|
||||
_retiringAtlasTextureHandles.Clear();
|
||||
_dirtyAtlases.Clear();
|
||||
_safeEmptyAtlases.Clear();
|
||||
_currentNonArenaGpuMemory = 0;
|
||||
|
|
|
|||
|
|
@ -607,14 +607,14 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
out bool compositePending);
|
||||
if (compositePending)
|
||||
reusableAcrossFrames = false;
|
||||
if (texture.Handle == 0)
|
||||
if (!texture.Slot.IsAssigned)
|
||||
continue;
|
||||
|
||||
var key = new GroupKey(
|
||||
batch.FirstIndex,
|
||||
(int)batch.BaseVertex,
|
||||
batch.IndexCount,
|
||||
texture.Handle,
|
||||
texture.Slot,
|
||||
texture.Layer,
|
||||
translucency,
|
||||
batch.CullMode);
|
||||
|
|
@ -714,7 +714,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
FirstIndex = key.FirstIndex,
|
||||
BaseVertex = key.BaseVertex,
|
||||
IndexCount = key.IndexCount,
|
||||
BindlessTextureHandle = key.BindlessTextureHandle,
|
||||
TextureSlot = key.TextureSlot,
|
||||
TextureLayer = key.TextureLayer,
|
||||
Translucency = key.Translucency,
|
||||
CullMode = key.CullMode,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Residency;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.Core.Lighting;
|
||||
|
|
@ -493,16 +494,17 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
private uint _instSelectionLightingSsbo;
|
||||
private int _instSelectionLightingSsboCapacityBytes;
|
||||
|
||||
// Campaign V slice V2 (2026-07-27): GL-only emulation of the eventual
|
||||
// Vulkan global texture descriptor array (binding=9,
|
||||
// GpuBindingModel.StorageTextureTable). A genuinely new handle is rare —
|
||||
// new dat surfaces/atlases, not every frame — so this single buffer is
|
||||
// NOT part of the ring-buffered DynamicBufferSet below; see
|
||||
// GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why
|
||||
// this table is owned here rather than by GlGpuDevice. Lazily created.
|
||||
private readonly GlBindlessHandleTable _textureTable = new();
|
||||
private uint _textureTableSsbo;
|
||||
private int _textureTableSsboCapacityBytes;
|
||||
// Campaign V slice V4t (2026-07-28): the interim per-renderer
|
||||
// GlBindlessHandleTable is retired. Batch data now carries the device's own
|
||||
// GpuTextureSlot, produced by the texture stack at upload time, so this
|
||||
// renderer only has to flush and bind that one table at
|
||||
// GpuBindingModel.StorageTextureTable before each of its raw-GL draws — it
|
||||
// does not submit through the encoder, so it never reaches
|
||||
// GlGpuDevice.FlushBeforeDraw. Taken from the mesh adapter rather than
|
||||
// wired separately: a batch's slot and the table that resolves it must come
|
||||
// from the same device by construction.
|
||||
private AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
|
||||
_meshAdapter.WorldTextureTable;
|
||||
|
||||
private sealed class DynamicBufferSet
|
||||
{
|
||||
|
|
@ -2521,15 +2523,15 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// Campaign V slice V2: instance method (not static) because it converts
|
||||
// the group's raw bindless handle to a _textureTable slot.
|
||||
private IndirectGroupInput ToInput(InstanceGroup g) => new(
|
||||
// Campaign V slice V4t: static again — the group already carries the
|
||||
// device's table slot, so there is no per-renderer interning left to do.
|
||||
private static IndirectGroupInput ToInput(InstanceGroup g) => new(
|
||||
IndexCount: g.IndexCount,
|
||||
FirstIndex: g.FirstIndex,
|
||||
BaseVertex: g.BaseVertex,
|
||||
InstanceCount: g.InstanceCount,
|
||||
FirstInstance: g.FirstInstance,
|
||||
TextureIndex: _textureTable.GetOrAdd(g.BindlessTextureHandle),
|
||||
TextureIndex: g.TextureSlot.Index,
|
||||
TextureLayer: g.TextureLayer,
|
||||
Translucency: g.Translucency,
|
||||
CullMode: g.CullMode);
|
||||
|
|
@ -2599,7 +2601,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
g.FirstIndex,
|
||||
g.BaseVertex,
|
||||
g.IndexCount,
|
||||
g.BindlessTextureHandle,
|
||||
g.TextureSlot,
|
||||
g.TextureLayer,
|
||||
g.Translucency,
|
||||
g.CullMode);
|
||||
|
|
@ -2777,7 +2779,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
hash.Add(key.FirstIndex);
|
||||
hash.Add(key.BaseVertex);
|
||||
hash.Add(key.IndexCount);
|
||||
hash.Add(key.BindlessTextureHandle);
|
||||
hash.Add(key.TextureSlot.Index);
|
||||
hash.Add(key.TextureLayer);
|
||||
hash.Add((int)key.Translucency);
|
||||
hash.Add((int)key.CullMode);
|
||||
|
|
@ -2827,7 +2829,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
hash.Add(key.FirstIndex);
|
||||
hash.Add(key.BaseVertex);
|
||||
hash.Add(key.IndexCount);
|
||||
hash.Add(key.BindlessTextureHandle);
|
||||
hash.Add(key.TextureSlot.Index);
|
||||
hash.Add(key.TextureLayer);
|
||||
hash.Add((int)key.Translucency);
|
||||
hash.Add((int)key.CullMode);
|
||||
|
|
@ -2983,7 +2985,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
_batchData[i] = new BatchData
|
||||
{
|
||||
// Campaign V slice V2: table slot, not the raw handle.
|
||||
TextureIndex = _textureTable.GetOrAdd(key.BindlessTextureHandle),
|
||||
TextureIndex = key.TextureSlot.Index,
|
||||
TextureLayer = key.TextureLayer,
|
||||
Flags = 0,
|
||||
};
|
||||
|
|
@ -3032,12 +3034,12 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
_gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 6, _instIndoorSsbo);
|
||||
_gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 7, _instAlphaSsbo);
|
||||
_gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 8, _instSelectionLightingSsbo);
|
||||
// Campaign V slice V2: already flushed/uploaded in UploadDeferredAlphaBuffers
|
||||
// (this is the same non-ring buffer as the main draw path); just rebind.
|
||||
// Campaign V slice V2: already flushed in UploadDeferredAlphaBuffers
|
||||
// (this is the same device table as the main draw path); just rebind.
|
||||
_gl.BindBufferBase(
|
||||
BufferTargetARB.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
|
||||
_textureTableSsbo);
|
||||
WorldTextureTable.TextureTableGlName);
|
||||
BindClipRegionBinding2();
|
||||
_gl.BindVertexArray(global.VAO);
|
||||
_gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, _indirectBuffer);
|
||||
|
|
@ -3173,8 +3175,9 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
UploadSsbo(_instSelectionLightingSsbo, 8, ref _instSelectionLightingSsboCapacityBytes,
|
||||
p, count * sizeof(float) * 2);
|
||||
UploadGlobalLights();
|
||||
// Campaign V slice V2 (binding=9): PrepareDeferredAlphaDraws registers
|
||||
// handles into _textureTable above; flush/rebind before DrawPreparedAlphaBatch.
|
||||
// Campaign V slice V2 (binding=9): flush/rebind the device texture table
|
||||
// before DrawPreparedAlphaBatch, which submits without going through
|
||||
// here again.
|
||||
FlushAndBindTextureTable();
|
||||
|
||||
fixed (DrawElementsIndirectCommand* p = _indirectCommands)
|
||||
|
|
@ -3457,39 +3460,24 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V2: uploads <see cref="_textureTable"/>'s handles to
|
||||
/// <see cref="_textureTableSsbo"/> when a new one was registered since the
|
||||
/// last flush (by <see cref="ToInput"/> or <see cref="PrepareDeferredAlphaDraws"/>),
|
||||
/// then (re)binds it at <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
|
||||
/// A genuinely new handle is rare — new dat surfaces/composite overrides,
|
||||
/// not every frame — so unlike the SSBOs above this is not part of the
|
||||
/// ring-buffered <see cref="DynamicBufferSet"/>; see
|
||||
/// <see cref="GlBindlessHandleTable"/>'s doc comment.
|
||||
/// Campaign V slice V4t: drains the device texture table's dirty runs and
|
||||
/// (re)binds it at
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
|
||||
/// The drain is normally a no-op — a genuinely new slot means a new dat
|
||||
/// surface or composite override, not a new frame — but the bind is
|
||||
/// unconditional, because GL storage-buffer binding points are global and
|
||||
/// another raw-GL renderer's binding 9 sits there between two of these
|
||||
/// draws. Deleted with the raw-GL world path once these draws go through the
|
||||
/// encoder, which binds the same table on every pipeline bind.
|
||||
/// </summary>
|
||||
private unsafe void FlushAndBindTextureTable()
|
||||
private void FlushAndBindTextureTable()
|
||||
{
|
||||
if (_textureTableSsbo == 0)
|
||||
_textureTableSsbo = TrackedGlResource.CreateBuffer(_gl, "creating WB texture-table SSBO");
|
||||
|
||||
if (_textureTable.Dirty)
|
||||
{
|
||||
ReadOnlySpan<ulong> handles = _textureTable.Handles;
|
||||
int byteCount = handles.Length * sizeof(ulong);
|
||||
fixed (ulong* p = handles)
|
||||
{
|
||||
UploadDynamicBuffer(
|
||||
BufferTargetARB.ShaderStorageBuffer,
|
||||
_textureTableSsbo,
|
||||
ref _textureTableSsboCapacityBytes,
|
||||
p,
|
||||
byteCount);
|
||||
}
|
||||
_textureTable.MarkFlushed();
|
||||
}
|
||||
AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device = WorldTextureTable;
|
||||
device.FlushTextureTable();
|
||||
_gl.BindBufferBase(
|
||||
BufferTargetARB.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
|
||||
_textureTableSsbo);
|
||||
device.TextureTableGlName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -3764,7 +3752,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
FirstIndex = key.FirstIndex,
|
||||
BaseVertex = key.BaseVertex,
|
||||
IndexCount = key.IndexCount,
|
||||
BindlessTextureHandle = key.BindlessTextureHandle,
|
||||
TextureSlot = key.TextureSlot,
|
||||
TextureLayer = key.TextureLayer,
|
||||
Translucency = key.Translucency,
|
||||
CullMode = key.CullMode,
|
||||
|
|
@ -3944,13 +3932,17 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
out bool compositePending);
|
||||
if (compositePending)
|
||||
allTexturesReady = false;
|
||||
if (texture.Handle == 0) continue;
|
||||
ulong texHandle = texture.Handle;
|
||||
// Campaign V slice V4t: an unassigned slot is the "no texture yet"
|
||||
// case a zero handle used to signal. It is a real sentinel
|
||||
// (GpuTextureSlot.Unassigned == ACDREAM_TEXTURE_NONE), not the
|
||||
// default value, so nothing here can silently resolve to slot 0.
|
||||
if (!texture.Slot.IsAssigned) continue;
|
||||
GpuTextureSlot texSlot = texture.Slot;
|
||||
uint texLayer = texture.Layer;
|
||||
|
||||
var key = new GroupKey(
|
||||
batch.FirstIndex, (int)batch.BaseVertex,
|
||||
batch.IndexCount, texHandle, texLayer, translucency, batch.CullMode);
|
||||
batch.IndexCount, texSlot, texLayer, translucency, batch.CullMode);
|
||||
|
||||
InstanceGroup grp = GetOrCreateInstanceGroup(key);
|
||||
grp.Matrices.Add(model);
|
||||
|
|
@ -3962,7 +3954,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
grp.SelectionLighting.Add(_currentEntitySelectionLighting);
|
||||
collector?.Add(new CachedBatch(
|
||||
key,
|
||||
texHandle,
|
||||
texSlot,
|
||||
restPose,
|
||||
renderData.SortCenter,
|
||||
grp,
|
||||
|
|
@ -3971,7 +3963,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
return allTexturesReady;
|
||||
}
|
||||
|
||||
private readonly record struct ResolvedTexture(ulong Handle, uint Layer);
|
||||
private readonly record struct ResolvedTexture(GpuTextureSlot Slot, uint Layer);
|
||||
|
||||
private ResolvedTexture ResolveTexture(
|
||||
in RenderInstanceCandidate entity,
|
||||
|
|
@ -4038,8 +4030,8 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
origTexOverride,
|
||||
paletteOverride!,
|
||||
paletteIdentity);
|
||||
compositePending = texture.Handle == 0;
|
||||
return new ResolvedTexture(texture.Handle, texture.Layer);
|
||||
compositePending = !texture.IsResolved;
|
||||
return new ResolvedTexture(texture.Slot, texture.Layer);
|
||||
}
|
||||
|
||||
case WbTextureResolutionKind.OriginalTextureOverride:
|
||||
|
|
@ -4049,13 +4041,13 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
localEntityId,
|
||||
surfaceId,
|
||||
overrideOrigTex);
|
||||
compositePending = texture.Handle == 0;
|
||||
return new ResolvedTexture(texture.Handle, texture.Layer);
|
||||
compositePending = !texture.IsResolved;
|
||||
return new ResolvedTexture(texture.Slot, texture.Layer);
|
||||
}
|
||||
|
||||
case WbTextureResolutionKind.SharedAtlas:
|
||||
return new ResolvedTexture(
|
||||
batch.BindlessTextureHandle,
|
||||
batch.TextureSlot,
|
||||
checked((uint)batch.TextureIndex));
|
||||
|
||||
default:
|
||||
|
|
@ -4156,13 +4148,6 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
"deleting entity fallback clip SSBO",
|
||||
_gl.DeleteBuffer);
|
||||
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
_textureTableSsbo,
|
||||
_textureTableSsboCapacityBytes,
|
||||
"texture-table",
|
||||
"deleting entity texture-table SSBO");
|
||||
|
||||
if (!_gpuQueriesInitialized)
|
||||
return;
|
||||
for (int i = 0; i < GpuQueryRingDepth; i++)
|
||||
|
|
@ -4265,8 +4250,6 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
_instAlphaSsbo = 0;
|
||||
_instSelectionLightingSsbo = 0;
|
||||
_fallbackClipRegionSsbo = 0;
|
||||
_textureTableSsbo = 0;
|
||||
_textureTableSsboCapacityBytes = 0;
|
||||
Array.Clear(_gpuQueryOpaque);
|
||||
Array.Clear(_gpuQueryTransparent);
|
||||
_gpuQueriesInitialized = false;
|
||||
|
|
@ -4440,7 +4423,10 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
public uint FirstIndex;
|
||||
public int BaseVertex;
|
||||
public int IndexCount;
|
||||
public ulong BindlessTextureHandle; // 64-bit (was uint TextureHandle in N.4)
|
||||
// Campaign V slice V4t: the device texture-table slot (was a raw 64-bit
|
||||
// ARB_bindless_texture handle, and a uint TextureHandle in N.4).
|
||||
public AcDream.App.Rendering.Gpu.GpuTextureSlot TextureSlot =
|
||||
AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned;
|
||||
public uint TextureLayer; // Layer in either the pooled composite array or WB shared atlas.
|
||||
public TranslucencyKind Translucency;
|
||||
public CullMode CullMode;
|
||||
|
|
|
|||
|
|
@ -251,6 +251,19 @@ public sealed class WbMeshAdapter
|
|||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4t: the GL device whose texture table every mesh
|
||||
/// batch's <c>GpuTextureSlot</c> indexes. The world renderers this adapter
|
||||
/// feeds flush and bind that table before their raw-GL draws, and taking it
|
||||
/// from here rather than from a second composition wire is what makes "the
|
||||
/// slot and the table came from the same device" true by construction.
|
||||
/// </summary>
|
||||
internal AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
|
||||
(_meshManager
|
||||
?? throw new InvalidOperationException(
|
||||
"An initialized mesh adapter is required for the world texture table."))
|
||||
.WorldTextureTable;
|
||||
|
||||
internal void RegisterResidencySources(ResidencyManager manager)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manager);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,54 @@ namespace AcDream.App.Tests.Rendering;
|
|||
|
||||
public sealed class CompositeTextureArrayCachePolicyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Campaign V slice V4t: the retype from a 64-bit bindless handle to
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.GpuTextureSlot"/> removed the spare
|
||||
/// value that used to mean "not resolved" — no texture has handle 0, but
|
||||
/// slot 0 is a perfectly good slot. This pins the replacement invariant:
|
||||
/// the DEFAULT location is unresolved, and it is distinguishable from a
|
||||
/// location that genuinely names slot 0. Getting this wrong would not throw;
|
||||
/// it would draw the first-registered texture on every unresolved
|
||||
/// composite, which is the magenta-placeholder failure shape one layer down.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DefaultLocationIsUnresolvedAndDistinctFromSlotZero()
|
||||
{
|
||||
Assert.False(default(BindlessTextureLocation).IsResolved);
|
||||
Assert.Equal(BindlessTextureLocation.Unresolved, default(BindlessTextureLocation));
|
||||
Assert.False(BindlessTextureLocation.Unresolved.Slot.IsAssigned);
|
||||
|
||||
var slotZero = new BindlessTextureLocation(
|
||||
new AcDream.App.Rendering.Gpu.GpuTextureSlot(0), layer: 3);
|
||||
Assert.True(slotZero.IsResolved);
|
||||
Assert.Equal(0u, slotZero.Slot.Index);
|
||||
Assert.Equal(3u, slotZero.Layer);
|
||||
Assert.NotEqual(BindlessTextureLocation.Unresolved, slotZero);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BudgetRejectionReturnsAnUnresolvedLocation()
|
||||
{
|
||||
// One layer per atlas and room for exactly one atlas, so the second
|
||||
// composite is refused by the physical budget rather than uploaded.
|
||||
var backend = new FakeBackend(maximumLayers: 1);
|
||||
var retirements = new DeferredRetirementQueue();
|
||||
using var cache = CreateCache(
|
||||
backend,
|
||||
retirements,
|
||||
physicalBudgetBytes: 64);
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(1), Texture(4, 4), out _));
|
||||
|
||||
Assert.False(cache.TryAddAndAcquire(
|
||||
2,
|
||||
Key(2),
|
||||
Texture(4, 4),
|
||||
out BindlessTextureLocation rejected));
|
||||
Assert.False(rejected.IsResolved);
|
||||
Assert.False(rejected.Slot.IsAssigned);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResidencySnapshotSeparatesResidentRequestedAndRetiringStorage()
|
||||
{
|
||||
|
|
@ -77,7 +125,7 @@ public sealed class CompositeTextureArrayCachePolicyTests
|
|||
Assert.True(cache.TryAddAndAcquire(1, Key(2), Texture(4, 4), out BindlessTextureLocation second));
|
||||
|
||||
Assert.Equal(first, shared);
|
||||
Assert.Equal(first.Handle, second.Handle);
|
||||
Assert.Equal(first.Slot, second.Slot);
|
||||
Assert.NotEqual(first.Layer, second.Layer);
|
||||
Assert.Single(backend.Created);
|
||||
Assert.Equal(2, backend.Uploads.Count);
|
||||
|
|
@ -124,7 +172,7 @@ public sealed class CompositeTextureArrayCachePolicyTests
|
|||
Assert.Equal(1, retirements.Count);
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(2, Key(1), Texture(4, 4), out BindlessTextureLocation replacement));
|
||||
Assert.NotEqual(old.Handle, replacement.Handle);
|
||||
Assert.NotEqual(old.Slot, replacement.Slot);
|
||||
Assert.Equal(2, backend.Created.Count);
|
||||
|
||||
retirements.DrainAll();
|
||||
|
|
@ -369,7 +417,7 @@ public sealed class CompositeTextureArrayCachePolicyTests
|
|||
cache.BeginFrame();
|
||||
cache.Tick();
|
||||
Assert.True(cache.TryAddAndAcquire(2, Key(2), Texture(4, 4), out BindlessTextureLocation reused));
|
||||
Assert.Equal(first.Handle, reused.Handle);
|
||||
Assert.Equal(first.Slot, reused.Slot);
|
||||
Assert.Single(backend.Created);
|
||||
Assert.Equal(64, cache.AllocatedBytes);
|
||||
}
|
||||
|
|
@ -606,6 +654,10 @@ public sealed class CompositeTextureArrayCachePolicyTests
|
|||
{
|
||||
Name = name,
|
||||
Handle = 1000UL + name,
|
||||
// Slice V4t: the GL backend interns each resident handle into
|
||||
// GlGpuDevice's table; the fake mints a deterministic stand-in
|
||||
// so slot identity is still comparable across entries.
|
||||
Slot = new AcDream.App.Rendering.Gpu.GpuTextureSlot(name),
|
||||
Width = width,
|
||||
Height = height,
|
||||
Capacity = capacity,
|
||||
|
|
|
|||
|
|
@ -300,6 +300,7 @@ public sealed class StandaloneBindlessTextureCacheTests
|
|||
SurfaceId = surfaceId,
|
||||
Name = surfaceId,
|
||||
Handle = surfaceId + 1_000UL,
|
||||
Slot = new AcDream.App.Rendering.Gpu.GpuTextureSlot(surfaceId),
|
||||
Bytes = bytes,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public class InstanceGroupClearTests
|
|||
cache.Populate(
|
||||
entityId: 100,
|
||||
landblockHint: 0xA9B40000u,
|
||||
batches: [new CachedBatch(key, 0xAA, Matrix4x4.Identity)],
|
||||
batches: [new CachedBatch(key, Slot(0xAA), Matrix4x4.Identity)],
|
||||
selectionParts: parts);
|
||||
|
||||
Assert.True(cache.TryGet(100, 0xA9B40000u, out EntityCacheEntry? entry));
|
||||
|
|
@ -91,10 +91,10 @@ public class InstanceGroupClearTests
|
|||
public void DispatcherFingerprint_EqualDistanceAlphaUsesOriginalSubmissionOrder()
|
||||
{
|
||||
WbDrawDispatcher.InstanceGroup first = MakeCompleteGroup(
|
||||
textureHandle: 0xAA,
|
||||
textureSlot: 0xAA,
|
||||
submissionOrder: 0);
|
||||
WbDrawDispatcher.InstanceGroup second = MakeCompleteGroup(
|
||||
textureHandle: 0xBB,
|
||||
textureSlot: 0xBB,
|
||||
submissionOrder: 1);
|
||||
var scratch = new List<WbDrawDispatcher.AlphaFingerprint>();
|
||||
|
||||
|
|
@ -143,7 +143,7 @@ public class InstanceGroupClearTests
|
|||
public void DispatcherFingerprint_ZeroVisibleInstancesIgnoresStaleGroupStorage()
|
||||
{
|
||||
WbDrawDispatcher.InstanceGroup stale = MakeCompleteGroup(
|
||||
textureHandle: 0xAA,
|
||||
textureSlot: 0xAA,
|
||||
submissionOrder: 0);
|
||||
var scratch = new List<WbDrawDispatcher.AlphaFingerprint>();
|
||||
|
||||
|
|
@ -177,12 +177,12 @@ public class InstanceGroupClearTests
|
|||
FirstIndex: 0,
|
||||
BaseVertex: 0,
|
||||
IndexCount: 6,
|
||||
BindlessTextureHandle: 0xAA,
|
||||
TextureSlot: Slot(0xAA),
|
||||
TextureLayer: 0,
|
||||
Translucency: TranslucencyKind.Opaque);
|
||||
var cached = new CachedBatch(
|
||||
key,
|
||||
0xAA,
|
||||
Slot(0xAA),
|
||||
Matrix4x4.Identity,
|
||||
Group: group,
|
||||
GroupRegistration: 17);
|
||||
|
|
@ -221,7 +221,7 @@ public class InstanceGroupClearTests
|
|||
var retiredKeys = new List<GroupKey>();
|
||||
var staleCache = new CachedBatch(
|
||||
retiredKey,
|
||||
retiredKey.BindlessTextureHandle,
|
||||
retiredKey.TextureSlot,
|
||||
Matrix4x4.Identity,
|
||||
Group: retired,
|
||||
GroupRegistration: retired.Registration);
|
||||
|
|
@ -274,11 +274,13 @@ public class InstanceGroupClearTests
|
|||
Assert.Equal(22, paperdoll.Registration);
|
||||
}
|
||||
|
||||
private static GroupKey MakeKey(ulong textureHandle) => new(
|
||||
private static AcDream.App.Rendering.Gpu.GpuTextureSlot Slot(uint index) => new(index);
|
||||
|
||||
private static GroupKey MakeKey(uint textureSlot) => new(
|
||||
FirstIndex: 0,
|
||||
BaseVertex: 0,
|
||||
IndexCount: 6,
|
||||
BindlessTextureHandle: textureHandle,
|
||||
TextureSlot: Slot(textureSlot),
|
||||
TextureLayer: 0,
|
||||
Translucency: TranslucencyKind.Opaque);
|
||||
|
||||
|
|
@ -294,7 +296,7 @@ public class InstanceGroupClearTests
|
|||
}
|
||||
|
||||
private static WbDrawDispatcher.InstanceGroup MakeCompleteGroup(
|
||||
ulong textureHandle,
|
||||
uint textureSlot,
|
||||
int submissionOrder)
|
||||
{
|
||||
var group = new WbDrawDispatcher.InstanceGroup
|
||||
|
|
@ -302,7 +304,7 @@ public class InstanceGroupClearTests
|
|||
FirstIndex = 0,
|
||||
BaseVertex = 0,
|
||||
IndexCount = 6,
|
||||
BindlessTextureHandle = textureHandle,
|
||||
TextureSlot = Slot(textureSlot),
|
||||
TextureLayer = 0,
|
||||
Translucency = TranslucencyKind.AlphaBlend,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ public class EntityClassificationCacheTests
|
|||
var cache = new EntityClassificationCache();
|
||||
var batches = new[]
|
||||
{
|
||||
MakeCachedBatch(ibo: 1, firstIndex: 0, indexCount: 6, texHandle: 0xAA),
|
||||
MakeCachedBatch(ibo: 1, firstIndex: 6, indexCount: 6, texHandle: 0xBB),
|
||||
MakeCachedBatch(ibo: 1, firstIndex: 0, indexCount: 6, texSlot: 0xAA),
|
||||
MakeCachedBatch(ibo: 1, firstIndex: 6, indexCount: 6, texSlot: 0xBB),
|
||||
};
|
||||
|
||||
cache.Populate(entityId: 100, landblockHint: 0xA9B40000u, batches);
|
||||
|
|
@ -46,7 +46,7 @@ public class EntityClassificationCacheTests
|
|||
Assert.True(cache.TryGet(100, 0u, out var entry));
|
||||
Assert.NotNull(entry);
|
||||
Assert.Single(entry!.Batches);
|
||||
Assert.Equal(0xCCu, entry.Batches[0].BindlessTextureHandle);
|
||||
Assert.Equal(0xCCu, entry.Batches[0].TextureSlot.Index);
|
||||
}
|
||||
|
||||
// ── #119 root-cause regression (2026-06-11): colliding entity ids across
|
||||
|
|
@ -152,15 +152,15 @@ public class EntityClassificationCacheTests
|
|||
ibo: (uint)(subPart + 1),
|
||||
firstIndex: (uint)(b * 6),
|
||||
indexCount: 6,
|
||||
texHandle: (ulong)(0x100 + subPart * 2 + b));
|
||||
texSlot: (uint)(0x100 + subPart * 2 + b));
|
||||
}
|
||||
cache.Populate(99, 0u, batches);
|
||||
|
||||
Assert.True(cache.TryGet(99, 0u, out var entry));
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(6, entry!.Batches.Length);
|
||||
Assert.Equal(0x100u, entry.Batches[0].BindlessTextureHandle);
|
||||
Assert.Equal(0x105u, entry.Batches[5].BindlessTextureHandle);
|
||||
Assert.Equal(0x100u, entry.Batches[0].TextureSlot.Index);
|
||||
Assert.Equal(0x105u, entry.Batches[5].TextureSlot.Index);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -248,7 +248,7 @@ public class EntityClassificationCacheTests
|
|||
Assert.True(cache.TryGet(100, 0xA9B40000u, out var entry));
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(batchesV2, entry!.Batches);
|
||||
Assert.Equal(0xCCu, entry.Batches[0].BindlessTextureHandle);
|
||||
Assert.Equal(0xCCu, entry.Batches[0].TextureSlot.Index);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
|
@ -328,15 +328,15 @@ public class EntityClassificationCacheTests
|
|||
#endif
|
||||
|
||||
private static CachedBatch MakeCachedBatch(
|
||||
uint ibo, uint firstIndex, int indexCount, ulong texHandle)
|
||||
uint ibo, uint firstIndex, int indexCount, uint texSlot)
|
||||
{
|
||||
var key = new GroupKey(
|
||||
FirstIndex: firstIndex,
|
||||
BaseVertex: 0,
|
||||
IndexCount: indexCount,
|
||||
BindlessTextureHandle: texHandle,
|
||||
TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot),
|
||||
TextureLayer: 0,
|
||||
Translucency: TranslucencyKind.Opaque);
|
||||
return new CachedBatch(key, texHandle, Matrix4x4.Identity);
|
||||
return new CachedBatch(key, new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot), Matrix4x4.Identity);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
uint ibo,
|
||||
uint firstIndex,
|
||||
int indexCount,
|
||||
ulong texHandle,
|
||||
uint texSlot,
|
||||
Matrix4x4? restPose = null,
|
||||
Vector3? localSortCenter = null)
|
||||
{
|
||||
|
|
@ -424,12 +424,12 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
FirstIndex: firstIndex,
|
||||
BaseVertex: 0,
|
||||
IndexCount: indexCount,
|
||||
BindlessTextureHandle: texHandle,
|
||||
TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot),
|
||||
TextureLayer: 0,
|
||||
Translucency: TranslucencyKind.Opaque);
|
||||
return new CachedBatch(
|
||||
key,
|
||||
texHandle,
|
||||
new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot),
|
||||
restPose ?? Matrix4x4.Identity,
|
||||
localSortCenter ?? Vector3.Zero);
|
||||
}
|
||||
|
|
@ -459,8 +459,8 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
const uint LandblockId = 0xA9B40000u;
|
||||
|
||||
// First MeshRef contributes 2 batches (mimics ClassifyBatches output).
|
||||
scratch.Add(MakeCachedBatch(ibo: 1, firstIndex: 0, indexCount: 6, texHandle: 0xAA));
|
||||
scratch.Add(MakeCachedBatch(ibo: 1, firstIndex: 6, indexCount: 6, texHandle: 0xBB));
|
||||
scratch.Add(MakeCachedBatch(ibo: 1, firstIndex: 0, indexCount: 6, texSlot: 0xAA));
|
||||
scratch.Add(MakeCachedBatch(ibo: 1, firstIndex: 6, indexCount: 6, texSlot: 0xBB));
|
||||
|
||||
uint? populateEntityId = null;
|
||||
uint populateLandblockId = 0u;
|
||||
|
|
@ -479,8 +479,8 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
Assert.True(cache.TryGet(EntityId, LandblockId, out var entry));
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(2, entry!.Batches.Length);
|
||||
Assert.Equal(0xAAul, entry.Batches[0].BindlessTextureHandle);
|
||||
Assert.Equal(0xBBul, entry.Batches[1].BindlessTextureHandle);
|
||||
Assert.Equal(0xAAu, entry.Batches[0].TextureSlot.Index);
|
||||
Assert.Equal(0xBBu, entry.Batches[1].TextureSlot.Index);
|
||||
|
||||
// Frame 2: cache hit. ApplyCacheHit walks the cached batches and
|
||||
// appends RestPose * entityWorld to a per-frame group dict.
|
||||
|
|
@ -535,7 +535,7 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
ibo: 1,
|
||||
firstIndex: 0,
|
||||
indexCount: 6,
|
||||
texHandle: 0xAA,
|
||||
texSlot: 0xAA,
|
||||
localSortCenter: authoredCenter),
|
||||
],
|
||||
};
|
||||
|
|
@ -638,16 +638,16 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
populateEntityId, populateLandblockId, EntityId, cache, scratch);
|
||||
|
||||
// Mimic ClassifyBatches' collector output for THIS MeshRef:
|
||||
// 2 batches with distinct (ibo, firstIndex, texHandle) so the
|
||||
// 2 batches with distinct (ibo, firstIndex, texSlot) so the
|
||||
// ordering can be verified post-hoc.
|
||||
for (int b = 0; b < BatchesPerMeshRef; b++)
|
||||
{
|
||||
ulong texHandle = (ulong)(0x100 + meshRefIdx * BatchesPerMeshRef + b);
|
||||
uint texSlot = (uint)(0x100 + meshRefIdx * BatchesPerMeshRef + b);
|
||||
scratch.Add(MakeCachedBatch(
|
||||
ibo: (uint)(meshRefIdx + 1),
|
||||
firstIndex: (uint)(b * 6),
|
||||
indexCount: 6,
|
||||
texHandle: texHandle));
|
||||
texSlot: texSlot));
|
||||
}
|
||||
|
||||
// After ClassifyBatches, Draw sets the tracker (matching the
|
||||
|
|
@ -676,7 +676,7 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
// Per-batch ordering check: batches arrived in MeshRef order, so
|
||||
// texture handles run 0x100..0x105 in the order they were appended.
|
||||
for (int i = 0; i < ExpectedTotalBatches; i++)
|
||||
Assert.Equal((ulong)(0x100 + i), entry.Batches[i].BindlessTextureHandle);
|
||||
Assert.Equal((uint)(0x100 + i), entry.Batches[i].TextureSlot.Index);
|
||||
|
||||
// After flush, scratch is cleared so the next entity starts fresh.
|
||||
Assert.Empty(scratch);
|
||||
|
|
@ -717,12 +717,12 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
currentEntityIncomplete = true;
|
||||
|
||||
// Tuple 1 (MeshRef[1]): renderData valid -> classify, accumulate.
|
||||
scratch.Add(MakeCachedBatch(ibo: 1, firstIndex: 0, indexCount: 6, texHandle: 0xAAul));
|
||||
scratch.Add(MakeCachedBatch(ibo: 1, firstIndex: 0, indexCount: 6, texSlot: 0xAA));
|
||||
populateEntityId = EntityId;
|
||||
populateLandblockId = LandblockId;
|
||||
|
||||
// Tuple 2 (MeshRef[2]): renderData valid -> classify, accumulate.
|
||||
scratch.Add(MakeCachedBatch(ibo: 2, firstIndex: 0, indexCount: 6, texHandle: 0xBBul));
|
||||
scratch.Add(MakeCachedBatch(ibo: 2, firstIndex: 0, indexCount: 6, texSlot: 0xBB));
|
||||
populateEntityId = EntityId;
|
||||
populateLandblockId = LandblockId;
|
||||
|
||||
|
|
@ -774,7 +774,7 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
for (int i = 0; i < CachedBatchCount; i++)
|
||||
{
|
||||
batches[i] = MakeCachedBatch(
|
||||
ibo: 1u, firstIndex: (uint)i, indexCount: 6, texHandle: (ulong)(0x100 + i));
|
||||
ibo: 1u, firstIndex: (uint)i, indexCount: 6, texSlot: (uint)(0x100 + i));
|
||||
}
|
||||
cache.Populate(entityId: 100, landblockHint: 0xA9B40000u, batches);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue