feat(render): Vulkan campaign V11 step 2 — delete the OpenGL backend
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1 already removed ImGui/Studio/DevTools. This step deletes the GL rendering backend itself: every Gpu/Gl/** implementation, the Wb ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/ BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache, RenderBootstrap, and RenderFrameGlStateController. GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/ OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone — there is nothing left to select between. The five world-draw dual-arm renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer, ParticleRenderer, SkyRenderer) and the composition roots (WorldRenderComposition, HostInputCameraComposition, LivePresentationComposition, FrameRootComposition) collapse to their RHI-only arm. GL-only diagnostic properties with a live external reader (DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op rather than disappearing, since the reader is out of this commit's scope. A few GL-flavored mechanisms turned out to be backend-neutral once isolated: GlConstructionCleanupLedger is renamed ResourceConstructionCleanupLedger (exception-chain walking has nothing to do with GL), and GlfwNativePlatformProbe moved out of the otherwise GL-only GraphicalCapabilityRecord.cs into GraphicalWindowBackendSelection.cs before the rest of that file was deleted. Test files with no surviving subject are deleted outright (GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests, PortalDepthShaderParityTests, TextureCacheBindlessTests, TextRendererFailureSafetyTests, ClipFrameUploadTests, every Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests); others get their dead GL-only members trimmed while their live assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now reads GpuBindingModel.StorageClipRegions, the same binding index under its new backend-neutral name; GpuResourceRetirementTransactionTests drops its OpenGLGraphicsDevice-subclassing test double and the two GL queue tests it existed for). EnvCellRendererTests' construction helper now builds a real ObjectMeshManager via VulkanMeshPipelineDevice instead of passing null through a null-forgiving operator, since the RHI constructor never tolerated a null mesh manager and the old GL constructor (which did) is gone. Deferred to the next two steps, deliberately not touched here: the Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl (WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale csproj comment (the package itself is still load-bearing — TextureFormat and friends are used well beyond the deleted ManagedGLUniformBuffer), and the CI/gate scripts. Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors. Tests: full-solution `dotnet test` green across every project (App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all others 100%); the 2 App.Tests names that flake under full-suite parallel execution (#250-family, documented pre-existing) pass in isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b70b9832ff
commit
8a7a0837e1
121 changed files with 1243 additions and 19840 deletions
|
|
@ -3,10 +3,8 @@ using AcDream.Core.Textures;
|
|||
using AcDream.Core.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Gpu.Gl;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using Silk.NET.OpenGL;
|
||||
using System.Linq;
|
||||
using PixelFormatId = DatReaderWriter.Enums.PixelFormat;
|
||||
using SurfaceType = DatReaderWriter.Enums.SurfaceType;
|
||||
|
|
@ -14,22 +12,15 @@ using AcDream.App.Rendering.Residency;
|
|||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
public sealed unsafe class TextureCache
|
||||
public sealed class TextureCache
|
||||
: Wb.IEntityTextureLifetime,
|
||||
IDisposable
|
||||
{
|
||||
private readonly GL? _gl;
|
||||
private readonly IGpuDevice _device;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly string _diagnosticsDirectory;
|
||||
// Handle and decoded dimensions are one atomic cache entry. Keeping them
|
||||
// in separate dictionaries allowed GetOrUpload(surfaceId) followed by the
|
||||
// sized overload to upload a second GL texture and orphan the first.
|
||||
private readonly Dictionary<uint, (uint Handle, int Width, int Height)>
|
||||
_surfacesById = new();
|
||||
private readonly Dictionary<(uint SurfaceId, uint OrigTextureId), (int Width, int Height)>
|
||||
_decodedDimensionsByTexture = new();
|
||||
private uint _magentaHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4a: one registered <see cref="IGpuTexture"/> plus its
|
||||
|
|
@ -55,7 +46,6 @@ public sealed unsafe class TextureCache
|
|||
// GPU texture objects/slots until process exit.
|
||||
private readonly List<GpuUiTextureEntry> _adhocGpuTextures = new();
|
||||
|
||||
private readonly Wb.BindlessSupport? _bindless;
|
||||
private readonly CompositeTextureArrayCache? _compositeTextures;
|
||||
private bool _destinationRevealUploadPriority;
|
||||
|
||||
|
|
@ -105,12 +95,10 @@ public sealed unsafe class TextureCache
|
|||
// contract), and this convenience overload has no real caller today (both
|
||||
// production construction sites already target the internal overload
|
||||
// below) — kept internal rather than deleted to preserve its shape.
|
||||
internal TextureCache(GL? gl, IGpuDevice device, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
|
||||
internal TextureCache(IGpuDevice device, IDatReaderWriter dats)
|
||||
: this(
|
||||
gl,
|
||||
device,
|
||||
dats,
|
||||
bindless,
|
||||
ImmediateGpuResourceRetirementQueue.Instance,
|
||||
Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
|
|
@ -119,142 +107,57 @@ public sealed unsafe class TextureCache
|
|||
{
|
||||
}
|
||||
|
||||
/// <param name="gl">
|
||||
/// The GL context, or null on a backend that has none. Campaign V slice V6h:
|
||||
/// the UI path (<see cref="GetOrUploadRenderSurface"/>, <see cref="UploadRgba8"/>)
|
||||
/// is entirely <see cref="IGpuDevice"/>-driven and runs on either backend,
|
||||
/// while the world paths — the legacy <c>Texture2D</c> upload, particle
|
||||
/// arrays, and the composite/bindless caches — still speak raw GL and are
|
||||
/// unreachable without it. A null context therefore reaches exactly the same
|
||||
/// code a null <paramref name="bindless"/> already gated, and every world
|
||||
/// entry point throws with the slice that owns it named. Removed at V4t,
|
||||
/// which ports the world texture stack onto the RHI.
|
||||
/// </param>
|
||||
internal TextureCache(
|
||||
GL? gl,
|
||||
IGpuDevice device,
|
||||
IDatReaderWriter dats,
|
||||
Wb.BindlessSupport? bindless,
|
||||
IGpuResourceRetirementQueue retirementQueue,
|
||||
string diagnosticsDirectory,
|
||||
ResidencyBudgetOptions? budgets = null)
|
||||
{
|
||||
budgets ??= ResidencyBudgetOptions.Default;
|
||||
_gl = gl;
|
||||
if (gl is null && bindless is not null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Bindless composite/particle texture caches require a GL context.",
|
||||
nameof(bindless));
|
||||
}
|
||||
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
_dats = dats;
|
||||
_bindless = bindless;
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(diagnosticsDirectory);
|
||||
_diagnosticsDirectory = diagnosticsDirectory;
|
||||
ArgumentNullException.ThrowIfNull(retirementQueue);
|
||||
if (bindless is not null)
|
||||
{
|
||||
var resources = new ResourceCleanupGroup();
|
||||
CompositeTextureArrayCache? composite = null;
|
||||
StandaloneBindlessTextureCache? particles = null;
|
||||
try
|
||||
{
|
||||
composite = new CompositeTextureArrayCache(
|
||||
gl!,
|
||||
bindless,
|
||||
WorldDevice,
|
||||
retirementQueue,
|
||||
budgets.CompositeUnownedBytes,
|
||||
budgets.CompositePhysicalBytes);
|
||||
resources.Add("composite texture cache", composite.Dispose);
|
||||
particles = new StandaloneBindlessTextureCache(
|
||||
new ParticleTextureBackend(this),
|
||||
retirementQueue,
|
||||
budgets.StandaloneUnownedBytes,
|
||||
budgets.StandaloneUnownedEntries);
|
||||
resources.Add("particle texture cache", particles.Dispose);
|
||||
resources.TransferAll();
|
||||
}
|
||||
catch (Exception constructionFailure)
|
||||
{
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"TextureCache construction failed and its child-cache prefix did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
_compositeTextures = composite;
|
||||
_particleTextures = particles;
|
||||
}
|
||||
else
|
||||
// Campaign V slice V6l: both owner-scoped caches exist on both arms.
|
||||
//
|
||||
// Everything about them that matters — sharing equivalent surfaces
|
||||
// between owners, the bounded unowned LRU, the metered upload budget,
|
||||
// and retirement behind the frame-flight fence — is already
|
||||
// backend-neutral; only how one entry is created and destroyed
|
||||
// differs, which is exactly what the two backend interfaces are for.
|
||||
var resources = new ResourceCleanupGroup();
|
||||
CompositeTextureArrayCache? composite = null;
|
||||
StandaloneBindlessTextureCache? particles = null;
|
||||
try
|
||||
{
|
||||
// Campaign V slice V6l: both owner-scoped caches exist on both arms.
|
||||
//
|
||||
// Everything about them that matters — sharing equivalent surfaces
|
||||
// between owners, the bounded unowned LRU, the metered upload budget,
|
||||
// and retirement behind the frame-flight fence — is already
|
||||
// backend-neutral; only how one entry is created and destroyed
|
||||
// differs, which is exactly what the two backend interfaces are for.
|
||||
// RhiCompositeTextureArrayBackend is V6i-2's, built and exercised at
|
||||
// startup since that slice but with no production consumer until now;
|
||||
// ParticleRhiTextureBackend is this slice's.
|
||||
var resources = new ResourceCleanupGroup();
|
||||
CompositeTextureArrayCache? composite = null;
|
||||
StandaloneBindlessTextureCache? particles = null;
|
||||
try
|
||||
{
|
||||
composite = new CompositeTextureArrayCache(
|
||||
new RhiCompositeTextureArrayBackend(device),
|
||||
retirementQueue,
|
||||
budgets.CompositeUnownedBytes,
|
||||
budgets.CompositePhysicalBytes);
|
||||
resources.Add("composite texture cache", composite.Dispose);
|
||||
particles = new StandaloneBindlessTextureCache(
|
||||
new ParticleRhiTextureBackend(this),
|
||||
retirementQueue,
|
||||
budgets.StandaloneUnownedBytes,
|
||||
budgets.StandaloneUnownedEntries);
|
||||
resources.Add("particle texture cache", particles.Dispose);
|
||||
resources.TransferAll();
|
||||
}
|
||||
catch (Exception constructionFailure)
|
||||
{
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"TextureCache construction failed and its child-cache prefix did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
_compositeTextures = composite;
|
||||
_particleTextures = particles;
|
||||
composite = new CompositeTextureArrayCache(
|
||||
new RhiCompositeTextureArrayBackend(device),
|
||||
retirementQueue,
|
||||
budgets.CompositeUnownedBytes,
|
||||
budgets.CompositePhysicalBytes);
|
||||
resources.Add("composite texture cache", composite.Dispose);
|
||||
particles = new StandaloneBindlessTextureCache(
|
||||
new ParticleRhiTextureBackend(this),
|
||||
retirementQueue,
|
||||
budgets.StandaloneUnownedBytes,
|
||||
budgets.StandaloneUnownedEntries);
|
||||
resources.Add("particle texture cache", particles.Dispose);
|
||||
resources.TransferAll();
|
||||
}
|
||||
catch (Exception constructionFailure)
|
||||
{
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"TextureCache construction failed and its child-cache prefix did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
_compositeTextures = composite;
|
||||
_particleTextures = particles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The GL context the world texture paths need. Campaign V slice V6h: a
|
||||
/// Vulkan-composed cache serves the UI path through <see cref="IGpuDevice"/>
|
||||
/// alone and never reaches here, so a failure names the slice that owns the
|
||||
/// port rather than dereferencing null.
|
||||
/// </summary>
|
||||
private GL Gl => _gl ?? throw new InvalidOperationException(
|
||||
"This TextureCache owns no GL context: the world texture paths " +
|
||||
"(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);
|
||||
|
|
@ -286,44 +189,6 @@ public sealed unsafe class TextureCache
|
|||
BudgetBytes: textures.BudgetBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get or upload the GL texture handle for a Surface id. Returns a
|
||||
/// 1x1 magenta fallback if the Surface or its RenderSurface chain is
|
||||
/// missing or uses an unsupported format.
|
||||
/// </summary>
|
||||
public uint GetOrUpload(uint surfaceId)
|
||||
=> GetOrUploadSurfaceCore(surfaceId, out _, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Like <see cref="GetOrUpload(uint)"/> but also returns the decoded
|
||||
/// pixel dimensions. UI 9-slice geometry needs the source size to
|
||||
/// compute slice UVs. Cached alongside the handle.
|
||||
/// </summary>
|
||||
public uint GetOrUpload(uint surfaceId, out int width, out int height)
|
||||
=> GetOrUploadSurfaceCore(surfaceId, out width, out height);
|
||||
|
||||
private uint GetOrUploadSurfaceCore(uint surfaceId, out int width, out int height)
|
||||
{
|
||||
if (_surfacesById.TryGetValue(surfaceId, out var existing))
|
||||
{
|
||||
width = existing.Width;
|
||||
height = existing.Height;
|
||||
return existing.Handle;
|
||||
}
|
||||
|
||||
DecodedTexture decoded = DecodeFromDats(
|
||||
surfaceId,
|
||||
origTextureOverride: null,
|
||||
paletteOverride: null);
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1")
|
||||
DumpAlphaHistogram(surfaceId, decoded);
|
||||
uint h = UploadRgba8(decoded);
|
||||
_surfacesById.Add(surfaceId, (h, decoded.Width, decoded.Height));
|
||||
width = decoded.Width;
|
||||
height = decoded.Height;
|
||||
return h;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload a UI sprite by its RenderSurface DataId (0x06xxxxxx), decoded
|
||||
/// DIRECTLY (Portal/HighRes → DecodeRenderSurface) rather than through the
|
||||
|
|
@ -491,20 +356,12 @@ public sealed unsafe class TextureCache
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// The identity a UI upload is accounted under. On GL it is the texture's
|
||||
/// own GL name — unchanged, so the VRAM ledger and the
|
||||
/// <c>ACDREAM_DUMP_SURFACES</c> histogram key off exactly what they always
|
||||
/// did. On any other backend there is no such name, so a descending
|
||||
/// synthetic counter supplies one; it starts at <c>uint.MaxValue</c> because
|
||||
/// GL hands out small ascending names and the two spaces share the
|
||||
/// <c>_uploadMetadata</c> dictionary. The value is a dictionary key and a
|
||||
/// dedup token only — Campaign V slice V6d removed the last draw-time
|
||||
/// consumer of a raw GL name, so nothing binds it.
|
||||
/// The identity a UI upload is accounted under. There is no GL name on the
|
||||
/// Vulkan-only backend, so a descending synthetic counter supplies one; the
|
||||
/// value is a dictionary key and a dedup token only — Campaign V slice V6d
|
||||
/// removed the last draw-time consumer of a raw GL name, so nothing binds it.
|
||||
/// </summary>
|
||||
private uint UploadAccountingName(IGpuTexture texture) =>
|
||||
texture is GlGpuTexture glTexture
|
||||
? glTexture.GlName
|
||||
: _nextSyntheticUploadName--;
|
||||
private uint UploadAccountingName(IGpuTexture texture) => _nextSyntheticUploadName--;
|
||||
|
||||
private uint _nextSyntheticUploadName = uint.MaxValue;
|
||||
|
||||
|
|
@ -521,44 +378,6 @@ public sealed unsafe class TextureCache
|
|||
GpuAddressMode.Repeat,
|
||||
MaxAnisotropy: 1f);
|
||||
|
||||
/// <summary>
|
||||
/// Alpha-channel histogram for one decoded texture. Used to diagnose
|
||||
/// "why are clouds not transparent" — if cloud textures come out with
|
||||
/// alpha = 1.0 everywhere we know the decode path strips the alpha
|
||||
/// channel somewhere. Printed once per unique surfaceId under
|
||||
/// <c>ACDREAM_DUMP_SKY=1</c>. Adds ~2ms per texture upload, negligible.
|
||||
/// </summary>
|
||||
private static void DumpAlphaHistogram(uint surfaceId, DecodedTexture decoded)
|
||||
{
|
||||
if (decoded.Rgba8.Length == 0 || decoded.Width == 0 || decoded.Height == 0)
|
||||
{
|
||||
System.Console.WriteLine($"[tex-alpha] surf=0x{surfaceId:X8} empty");
|
||||
return;
|
||||
}
|
||||
int total = decoded.Rgba8.Length / 4;
|
||||
// Bucket alpha in 10 bins.
|
||||
var buckets = new int[10];
|
||||
int aMin = 255, aMax = 0;
|
||||
long aSum = 0;
|
||||
for (int i = 0; i < decoded.Rgba8.Length; i += 4)
|
||||
{
|
||||
int a = decoded.Rgba8[i + 3];
|
||||
if (a < aMin) aMin = a;
|
||||
if (a > aMax) aMax = a;
|
||||
aSum += a;
|
||||
int b = a * 10 / 256;
|
||||
if (b > 9) b = 9;
|
||||
buckets[b]++;
|
||||
}
|
||||
float aMean = aSum / (float)total / 255f;
|
||||
var pct = new string[10];
|
||||
for (int i = 0; i < 10; i++) pct[i] = $"{100.0 * buckets[i] / total:F0}%";
|
||||
System.Console.WriteLine(
|
||||
$"[tex-alpha] surf=0x{surfaceId:X8} {decoded.Width}x{decoded.Height} " +
|
||||
$"a_min={aMin / 255f:F3} a_max={aMax / 255f:F3} a_mean={aMean:F3} " +
|
||||
$"bins[0-9]={string.Join(",", pct)}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Acquires the exact DAT-decoded one-layer texture array for a live
|
||||
/// particle emitter. Equivalent surfaces are shared; the cache ownership
|
||||
|
|
@ -588,81 +407,17 @@ public sealed unsafe class TextureCache
|
|||
origTextureOverride: null,
|
||||
paletteOverride: null);
|
||||
|
||||
// Campaign V slice V6l: the RHI arm has no GL name to intern a bindless
|
||||
// handle from, so the image is created through IGpuDevice and paired
|
||||
// with a real sampler object. The decode above is the same one the GL
|
||||
// arm uses, so the pixels are identical; the shader still samples layer
|
||||
// zero of a one-layer array, which is what a Texture2D registered into
|
||||
// the table is on Vulkan (VulkanTextureFormatMapping.SampledViewTypeOf).
|
||||
if (_gl is null)
|
||||
return AcquireParticleTextureRhi(textures, ownerId, surfaceId, decoded);
|
||||
|
||||
uint name = UploadRgba8AsLayer1Array(decoded);
|
||||
ulong handle = 0;
|
||||
try
|
||||
{
|
||||
handle = _bindless!.GetResidentHandle(name);
|
||||
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 slot;
|
||||
}
|
||||
catch (Exception residencyFailure)
|
||||
{
|
||||
List<Exception>? cleanupFailures = null;
|
||||
void Attempt(Action cleanup)
|
||||
{
|
||||
try { cleanup(); }
|
||||
catch (Exception ex) { (cleanupFailures ??= []).Add(ex); }
|
||||
}
|
||||
|
||||
bool residencyReleased = handle == 0;
|
||||
if (handle != 0)
|
||||
{
|
||||
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,
|
||||
"rolling back particle texture residency");
|
||||
residencyReleased = true;
|
||||
});
|
||||
}
|
||||
if (residencyReleased)
|
||||
Attempt(() => DeleteUploadedTexture(name));
|
||||
if (cleanupFailures is not null)
|
||||
{
|
||||
cleanupFailures.Insert(0, residencyFailure);
|
||||
throw new AggregateException(
|
||||
"Particle texture residency and rollback both failed.",
|
||||
cleanupFailures);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
return AcquireParticleTextureRhi(textures, ownerId, surfaceId, decoded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6l: one particle surface as a device texture-table
|
||||
/// slot, owned by the same emitter-scoped cache the GL arm uses.
|
||||
/// slot, owned by the same emitter-scoped cache.
|
||||
///
|
||||
/// <para>Linear/clamped is the filtering the GL arm's own one-layer array
|
||||
/// upload sets on itself, and a particle sheet's UVs never leave [0,1] —
|
||||
/// the quad's own texcoords are the unit square — so the wrap mode is not a
|
||||
/// visible choice, it is just the safe one.</para>
|
||||
/// <para>Linear/clamped matches the filtering the deleted GL arm's own
|
||||
/// one-layer array upload set on itself, and a particle sheet's UVs never
|
||||
/// leave [0,1] — the quad's own texcoords are the unit square — so the
|
||||
/// wrap mode is not a visible choice, it is just the safe one.</para>
|
||||
/// </summary>
|
||||
private GpuTextureSlot AcquireParticleTextureRhi(
|
||||
StandaloneBindlessTextureCache textures,
|
||||
|
|
@ -714,8 +469,9 @@ public sealed unsafe class TextureCache
|
|||
/// Owner-scoped bindless variant for a server-supplied original-texture
|
||||
/// replacement. Stores compatible composites in a pooled Texture2DArray
|
||||
/// and returns its resident handle plus the assigned layer. Equivalent
|
||||
/// composites are shared until their final live owner leaves. Throws if
|
||||
/// BindlessSupport wasn't provided.
|
||||
/// composites are shared until their final live owner leaves. Returns
|
||||
/// <see langword="default"/> (an empty location) if a composite upload
|
||||
/// can't start or the decoded size can't be prepared this frame.
|
||||
/// </summary>
|
||||
internal BindlessTextureLocation GetOrUploadWithOrigTextureOverrideBindless(
|
||||
uint ownerLocalId,
|
||||
|
|
@ -750,7 +506,8 @@ public sealed unsafe class TextureCache
|
|||
/// top of the texture's default palette before decoding, stores compatible
|
||||
/// composites in a pooled Texture2DArray, and returns its resident handle
|
||||
/// plus the assigned layer. Structural identity is computed once per entity.
|
||||
/// Throws if BindlessSupport wasn't provided to the constructor.
|
||||
/// Returns <see langword="default"/> (an empty location) if a composite
|
||||
/// upload can't start or the decoded size can't be prepared this frame.
|
||||
/// </summary>
|
||||
internal BindlessTextureLocation GetOrUploadWithPaletteOverrideBindless(
|
||||
uint ownerLocalId,
|
||||
|
|
@ -853,14 +610,6 @@ public sealed unsafe class TextureCache
|
|||
EnsureCompositeTexturesAvailable().ReleaseOwner(localEntityId);
|
||||
}
|
||||
|
||||
private void EnsureBindlessAvailable()
|
||||
{
|
||||
if (_bindless is null)
|
||||
throw new InvalidOperationException(
|
||||
"TextureCache constructed without BindlessSupport — cannot generate bindless handles. " +
|
||||
"WbDrawDispatcher requires the bindless-aware ctor overload (pass non-null BindlessSupport).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6l: no longer gated on bindless. The composite cache is
|
||||
/// constructed on both arms — V6i-2's RHI backend is what serves the one
|
||||
|
|
@ -880,29 +629,9 @@ public sealed unsafe class TextureCache
|
|||
_particleTextures ?? throw new InvalidOperationException(
|
||||
"This TextureCache owns no standalone particle texture cache.");
|
||||
|
||||
private sealed class ParticleTextureBackend(TextureCache owner)
|
||||
: IStandaloneBindlessTextureBackend
|
||||
{
|
||||
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,
|
||||
$"releasing particle texture handle {resource.Handle}");
|
||||
}
|
||||
|
||||
public void Delete(StandaloneBindlessTextureResource resource)
|
||||
=> owner.DeleteUploadedTexture(resource.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6l: the same ownership boundary on a backend with no
|
||||
/// bindless handles. The table slot is released first and the image second,
|
||||
/// which is the same order the GL arm uses and for the same reason — a
|
||||
/// submitted-but-unretired frame may still sample the slot, and
|
||||
/// Campaign V slice V6l: the table slot is released first and the image
|
||||
/// second — a submitted-but-unretired frame may still sample the slot, and
|
||||
/// <see cref="IGpuDevice.ReleaseTextureSlot"/> is what defers its reuse.
|
||||
/// </summary>
|
||||
private sealed class ParticleRhiTextureBackend(TextureCache owner)
|
||||
|
|
@ -1043,7 +772,6 @@ public sealed unsafe class TextureCache
|
|||
bucketsByTriple[tripleKey] = bucketsByTriple.GetValueOrDefault(tripleKey) + 1;
|
||||
}
|
||||
|
||||
foreach (var kv in _surfacesById) Emit(kv.Key, kv.Value.Handle);
|
||||
_particleTextures?.VisitEntries(resource => Emit(resource.SurfaceId, resource.Name));
|
||||
_compositeTextures?.VisitEntries((surfaceId, width, height) =>
|
||||
{
|
||||
|
|
@ -1191,101 +919,6 @@ public sealed unsafe class TextureCache
|
|||
return UiTextureTableHandle.FromSlot(entry.Slot);
|
||||
}
|
||||
|
||||
private uint UploadRgba8(DecodedTexture decoded, bool nearest = false)
|
||||
{
|
||||
uint tex = Gl.GenTexture();
|
||||
if (tex == 0)
|
||||
throw new InvalidOperationException("OpenGL did not create a 2D texture.");
|
||||
try
|
||||
{
|
||||
Gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
|
||||
fixed (byte* p = decoded.Rgba8)
|
||||
Gl.TexImage2D(
|
||||
TextureTarget.Texture2D,
|
||||
0,
|
||||
InternalFormat.Rgba8,
|
||||
(uint)decoded.Width,
|
||||
(uint)decoded.Height,
|
||||
0,
|
||||
PixelFormat.Rgba,
|
||||
PixelType.UnsignedByte,
|
||||
p);
|
||||
|
||||
// Point (nearest) sampling for pixel-exact UI text — bilinear softens the dat
|
||||
// font's small glyphs. Other surfaces use bilinear.
|
||||
int filter = nearest ? (int)TextureMinFilter.Nearest : (int)TextureMinFilter.Linear;
|
||||
Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, filter);
|
||||
Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, filter);
|
||||
Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
|
||||
Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
|
||||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
Gl,
|
||||
$"uploading 2D RGBA8 texture {decoded.Width}x{decoded.Height}");
|
||||
|
||||
TrackUploadedTexture(tex, decoded.Width, decoded.Height);
|
||||
return tex;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Gl.DeleteTexture(tex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="UploadRgba8"/> that uploads pixel data as a 1-layer
|
||||
/// Texture2DArray. Required by the WB modern rendering path which samples via
|
||||
/// sampler2DArray in its bindless shader. Pixel data is identical.
|
||||
/// </summary>
|
||||
private uint UploadRgba8AsLayer1Array(DecodedTexture decoded)
|
||||
{
|
||||
uint tex = Gl.GenTexture();
|
||||
if (tex == 0)
|
||||
throw new InvalidOperationException("OpenGL did not create a one-layer texture array.");
|
||||
try
|
||||
{
|
||||
Gl.BindTexture(TextureTarget.Texture2DArray, tex);
|
||||
|
||||
fixed (byte* p = decoded.Rgba8)
|
||||
Gl.TexImage3D(
|
||||
TextureTarget.Texture2DArray,
|
||||
0,
|
||||
InternalFormat.Rgba8,
|
||||
(uint)decoded.Width,
|
||||
(uint)decoded.Height,
|
||||
depth: 1,
|
||||
border: 0,
|
||||
PixelFormat.Rgba,
|
||||
PixelType.UnsignedByte,
|
||||
p);
|
||||
|
||||
Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
|
||||
Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
|
||||
Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
|
||||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
Gl,
|
||||
$"uploading one-layer RGBA8 array {decoded.Width}x{decoded.Height}");
|
||||
|
||||
TrackUploadedTexture(tex, decoded.Width, decoded.Height);
|
||||
return tex;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Gl.DeleteTexture(tex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void TrackUploadedTexture(uint name, int width, int height)
|
||||
{
|
||||
_uploadMetadata[name] = (width, height, "RGBA8_DECODED");
|
||||
|
|
@ -1294,19 +927,10 @@ public sealed unsafe class TextureCache
|
|||
Wb.GpuMemoryTracker.TrackAllocation(bytes, Wb.GpuResourceType.Texture);
|
||||
}
|
||||
|
||||
private void DeleteUploadedTexture(uint name)
|
||||
{
|
||||
Gl.DeleteTexture(name);
|
||||
Wb.GLHelpers.ThrowOnResourceError(Gl, $"deleting uploaded texture {name}");
|
||||
UntrackUploadedTexture(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Memory-tracking bookkeeping only, without a raw GL delete — used for
|
||||
/// the Campaign V slice V4a UI-path <see cref="IGpuTexture"/> entries,
|
||||
/// whose GL name is released by <see cref="IGpuTexture.Dispose"/> through
|
||||
/// the device's own retirement queue rather than by
|
||||
/// <see cref="DeleteUploadedTexture"/>.
|
||||
/// Memory-tracking bookkeeping only — used for every <see cref="IGpuTexture"/>
|
||||
/// entry, whose GPU resource is released by <see cref="IGpuTexture.Dispose"/>
|
||||
/// through the device's own retirement queue.
|
||||
/// </summary>
|
||||
private void UntrackUploadedTexture(uint name)
|
||||
{
|
||||
|
|
@ -1328,17 +952,6 @@ public sealed unsafe class TextureCache
|
|||
|
||||
_paletteIndexedByTexture.Clear();
|
||||
|
||||
// Legacy Texture2D textures.
|
||||
foreach (var entry in _surfacesById.Values)
|
||||
DeleteUploadedTexture(entry.Handle);
|
||||
_surfacesById.Clear();
|
||||
|
||||
if (_magentaHandle != 0)
|
||||
{
|
||||
DeleteUploadedTexture(_magentaHandle);
|
||||
_magentaHandle = 0;
|
||||
}
|
||||
|
||||
// RenderSurface (UI sprite) textures — Campaign V slice V4a: each
|
||||
// entry's IGpuTexture.Dispose() releases the underlying GL name
|
||||
// through the device's own retirement queue, so only the memory-
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue