Ryujinx-uplift/Ryujinx.Graphics.Vulkan/TextureView.cs
gdkchan f8beeeb7d3
Support safe blit on non-2D textures (#4374)
* Support safe blit on non-2D textures (except multisample)

* Change safe blit with different levels and layers to match CmdBlitImage path

* Remove now unused variables

* Multisample safe blit support
2023-02-07 13:55:59 -03:00

876 lines
29 KiB
C#

using Ryujinx.Common.Memory;
using Ryujinx.Graphics.GAL;
using Silk.NET.Vulkan;
using System;
using System.Collections.Generic;
using VkBuffer = Silk.NET.Vulkan.Buffer;
using VkFormat = Silk.NET.Vulkan.Format;
namespace Ryujinx.Graphics.Vulkan
{
class TextureView : ITexture, IDisposable
{
private readonly VulkanRenderer _gd;
private readonly Device _device;
private readonly Auto<DisposableImageView> _imageView;
private readonly Auto<DisposableImageView> _imageViewIdentity;
private readonly Auto<DisposableImageView> _imageView2dArray;
private Dictionary<GAL.Format, TextureView> _selfManagedViews;
private TextureCreateInfo _info;
public TextureCreateInfo Info => _info;
public TextureStorage Storage { get; }
public int Width => Info.Width;
public int Height => Info.Height;
public int Layers => Info.GetDepthOrLayers();
public int FirstLayer { get; }
public int FirstLevel { get; }
public float ScaleFactor => Storage.ScaleFactor;
public VkFormat VkFormat { get; }
public bool Valid { get; private set; }
public TextureView(
VulkanRenderer gd,
Device device,
TextureCreateInfo info,
TextureStorage storage,
int firstLayer,
int firstLevel)
{
_gd = gd;
_device = device;
_info = info;
Storage = storage;
FirstLayer = firstLayer;
FirstLevel = firstLevel;
storage.IncrementViewsCount();
gd.Textures.Add(this);
var format = _gd.FormatCapabilities.ConvertToVkFormat(info.Format);
var usage = TextureStorage.GetImageUsageFromFormat(info.Format);
var levels = (uint)info.Levels;
var layers = (uint)info.GetLayers();
VkFormat = format;
var type = info.Target.ConvertView();
var swizzleR = info.SwizzleR.Convert();
var swizzleG = info.SwizzleG.Convert();
var swizzleB = info.SwizzleB.Convert();
var swizzleA = info.SwizzleA.Convert();
if (info.Format == GAL.Format.R5G5B5A1Unorm ||
info.Format == GAL.Format.R5G5B5X1Unorm ||
info.Format == GAL.Format.R5G6B5Unorm)
{
var temp = swizzleR;
swizzleR = swizzleB;
swizzleB = temp;
}
else if (VkFormat == VkFormat.R4G4B4A4UnormPack16 || info.Format == GAL.Format.A1B5G5R5Unorm)
{
var tempB = swizzleB;
var tempA = swizzleA;
swizzleB = swizzleG;
swizzleA = swizzleR;
swizzleR = tempA;
swizzleG = tempB;
}
var componentMapping = new ComponentMapping(swizzleR, swizzleG, swizzleB, swizzleA);
var aspectFlags = info.Format.ConvertAspectFlags(info.DepthStencilMode);
var aspectFlagsDepth = info.Format.ConvertAspectFlags(DepthStencilMode.Depth);
var subresourceRange = new ImageSubresourceRange(aspectFlags, (uint)firstLevel, levels, (uint)firstLayer, layers);
var subresourceRangeDepth = new ImageSubresourceRange(aspectFlagsDepth, (uint)firstLevel, levels, (uint)firstLayer, layers);
unsafe Auto<DisposableImageView> CreateImageView(ComponentMapping cm, ImageSubresourceRange sr, ImageViewType viewType, ImageUsageFlags usageFlags)
{
var usage = new ImageViewUsageCreateInfo()
{
SType = StructureType.ImageViewUsageCreateInfo,
Usage = usageFlags
};
var imageCreateInfo = new ImageViewCreateInfo()
{
SType = StructureType.ImageViewCreateInfo,
Image = storage.GetImageForViewCreation(),
ViewType = viewType,
Format = format,
Components = cm,
SubresourceRange = sr,
PNext = &usage
};
gd.Api.CreateImageView(device, imageCreateInfo, null, out var imageView).ThrowOnError();
return new Auto<DisposableImageView>(new DisposableImageView(gd.Api, device, imageView), null, storage.GetImage());
}
_imageView = CreateImageView(componentMapping, subresourceRange, type, ImageUsageFlags.SampledBit);
// Framebuffer attachments and storage images requires a identity component mapping.
var identityComponentMapping = new ComponentMapping(
ComponentSwizzle.R,
ComponentSwizzle.G,
ComponentSwizzle.B,
ComponentSwizzle.A);
_imageViewIdentity = CreateImageView(identityComponentMapping, subresourceRangeDepth, type, usage);
// Framebuffer attachments also require 3D textures to be bound as 2D array.
if (info.Target == Target.Texture3D)
{
if (gd.Capabilities.PortabilitySubset.HasFlag(PortabilitySubsetFlags.No3DImageView))
{
if (levels == 1 && (info.Format.IsRtColorCompatible() || info.Format.IsDepthOrStencil()))
{
subresourceRange = new ImageSubresourceRange(aspectFlags, (uint)firstLevel, levels, (uint)firstLayer, 1);
_imageView2dArray = CreateImageView(identityComponentMapping, subresourceRange, ImageViewType.Type2D, ImageUsageFlags.ColorAttachmentBit);
}
}
else
{
subresourceRange = new ImageSubresourceRange(aspectFlags, (uint)firstLevel, levels, (uint)firstLayer, (uint)info.Depth);
_imageView2dArray = CreateImageView(identityComponentMapping, subresourceRange, ImageViewType.Type2DArray, usage);
}
}
Valid = true;
}
public Auto<DisposableImage> GetImage()
{
return Storage.GetImage();
}
public Auto<DisposableImageView> GetImageView()
{
return _imageView;
}
public Auto<DisposableImageView> GetIdentityImageView()
{
return _imageViewIdentity;
}
public Auto<DisposableImageView> GetImageViewForAttachment()
{
return _imageView2dArray ?? _imageViewIdentity;
}
public void CopyTo(ITexture destination, int firstLayer, int firstLevel)
{
var src = this;
var dst = (TextureView)destination;
if (!Valid || !dst.Valid)
{
return;
}
_gd.PipelineInternal.EndRenderPass();
var cbs = _gd.PipelineInternal.CurrentCommandBuffer;
var srcImage = src.GetImage().Get(cbs).Value;
var dstImage = dst.GetImage().Get(cbs).Value;
if (!dst.Info.Target.IsMultisample() && Info.Target.IsMultisample())
{
int layers = Math.Min(Info.GetLayers(), dst.Info.GetLayers() - firstLayer);
_gd.HelperShader.CopyMSToNonMS(_gd, cbs, src, dst, 0, firstLayer, layers);
}
else if (dst.Info.Target.IsMultisample() && !Info.Target.IsMultisample())
{
int layers = Math.Min(Info.GetLayers(), dst.Info.GetLayers() - firstLayer);
_gd.HelperShader.CopyNonMSToMS(_gd, cbs, src, dst, 0, firstLayer, layers);
}
else
{
TextureCopy.Copy(
_gd.Api,
cbs.CommandBuffer,
srcImage,
dstImage,
src.Info,
dst.Info,
src.FirstLayer,
dst.FirstLayer,
src.FirstLevel,
dst.FirstLevel,
0,
firstLayer,
0,
firstLevel);
}
}
public void CopyTo(ITexture destination, int srcLayer, int dstLayer, int srcLevel, int dstLevel)
{
var src = this;
var dst = (TextureView)destination;
if (!Valid || !dst.Valid)
{
return;
}
_gd.PipelineInternal.EndRenderPass();
var cbs = _gd.PipelineInternal.CurrentCommandBuffer;
var srcImage = src.GetImage().Get(cbs).Value;
var dstImage = dst.GetImage().Get(cbs).Value;
if (!dst.Info.Target.IsMultisample() && Info.Target.IsMultisample())
{
_gd.HelperShader.CopyMSToNonMS(_gd, cbs, src, dst, srcLayer, dstLayer, 1);
}
else if (dst.Info.Target.IsMultisample() && !Info.Target.IsMultisample())
{
_gd.HelperShader.CopyNonMSToMS(_gd, cbs, src, dst, srcLayer, dstLayer, 1);
}
else
{
TextureCopy.Copy(
_gd.Api,
cbs.CommandBuffer,
srcImage,
dstImage,
src.Info,
dst.Info,
src.FirstLayer,
dst.FirstLayer,
src.FirstLevel,
dst.FirstLevel,
srcLayer,
dstLayer,
srcLevel,
dstLevel,
1,
1);
}
}
public void CopyTo(ITexture destination, Extents2D srcRegion, Extents2D dstRegion, bool linearFilter)
{
var dst = (TextureView)destination;
if (_gd.CommandBufferPool.OwnedByCurrentThread)
{
_gd.PipelineInternal.EndRenderPass();
var cbs = _gd.PipelineInternal.CurrentCommandBuffer;
CopyToImpl(cbs, dst, srcRegion, dstRegion, linearFilter);
}
else
{
var cbp = _gd.BackgroundResources.Get().GetPool();
using var cbs = cbp.Rent();
CopyToImpl(cbs, dst, srcRegion, dstRegion, linearFilter);
}
}
private void CopyToImpl(CommandBufferScoped cbs, TextureView dst, Extents2D srcRegion, Extents2D dstRegion, bool linearFilter)
{
var src = this;
var srcFormat = GetCompatibleGalFormat(src.Info.Format);
var dstFormat = GetCompatibleGalFormat(dst.Info.Format);
bool srcUsesStorageFormat = src.VkFormat == src.Storage.VkFormat;
bool dstUsesStorageFormat = dst.VkFormat == dst.Storage.VkFormat;
int layers = Math.Min(dst.Info.GetDepthOrLayers(), src.Info.GetDepthOrLayers());
int levels = Math.Min(dst.Info.Levels, src.Info.Levels);
if (srcUsesStorageFormat && dstUsesStorageFormat)
{
if ((srcRegion.X1 | dstRegion.X1) == 0 &&
(srcRegion.Y1 | dstRegion.Y1) == 0 &&
srcRegion.X2 == src.Width &&
srcRegion.Y2 == src.Height &&
dstRegion.X2 == dst.Width &&
dstRegion.Y2 == dst.Height &&
src.Width == dst.Width &&
src.Height == dst.Height &&
src.VkFormat == dst.VkFormat)
{
if (src.Info.Samples > 1 && src.Info.Samples != dst.Info.Samples && src.Info.Format.IsDepthOrStencil())
{
// CmdResolveImage does not support depth-stencil resolve, so we need to use an alternative path
// for those textures.
TextureCopy.ResolveDepthStencil(_gd, _device, cbs, src, dst);
}
else
{
TextureCopy.Copy(
_gd.Api,
cbs.CommandBuffer,
src.GetImage().Get(cbs).Value,
dst.GetImage().Get(cbs).Value,
src.Info,
dst.Info,
src.FirstLayer,
dst.FirstLayer,
src.FirstLevel,
dst.FirstLevel,
0,
0,
0,
0,
layers,
levels);
}
return;
}
else if (_gd.FormatCapabilities.OptimalFormatSupports(FormatFeatureFlags.BlitSrcBit, srcFormat) &&
_gd.FormatCapabilities.OptimalFormatSupports(FormatFeatureFlags.BlitDstBit, dstFormat))
{
TextureCopy.Blit(
_gd.Api,
cbs.CommandBuffer,
src.GetImage().Get(cbs).Value,
dst.GetImage().Get(cbs).Value,
src.Info,
dst.Info,
srcRegion,
dstRegion,
src.FirstLayer,
dst.FirstLayer,
src.FirstLevel,
dst.FirstLevel,
layers,
levels,
linearFilter);
return;
}
}
bool isDepthOrStencil = dst.Info.Format.IsDepthOrStencil();
if (VulkanConfiguration.UseSlowSafeBlitOnAmd && (_gd.Vendor == Vendor.Amd || _gd.IsMoltenVk))
{
_gd.HelperShader.Blit(
_gd,
src,
dst,
srcRegion,
dstRegion,
layers,
levels,
isDepthOrStencil,
linearFilter);
return;
}
Auto<DisposableImage> srcImage;
Auto<DisposableImage> dstImage;
if (isDepthOrStencil)
{
srcImage = src.Storage.CreateAliasedColorForDepthStorageUnsafe(srcFormat).GetImage();
dstImage = dst.Storage.CreateAliasedColorForDepthStorageUnsafe(dstFormat).GetImage();
}
else
{
srcImage = src.Storage.CreateAliasedStorageUnsafe(srcFormat).GetImage();
dstImage = dst.Storage.CreateAliasedStorageUnsafe(dstFormat).GetImage();
}
TextureCopy.Blit(
_gd.Api,
cbs.CommandBuffer,
srcImage.Get(cbs).Value,
dstImage.Get(cbs).Value,
src.Info,
dst.Info,
srcRegion,
dstRegion,
src.FirstLayer,
dst.FirstLayer,
src.FirstLevel,
dst.FirstLevel,
layers,
levels,
linearFilter,
ImageAspectFlags.ColorBit,
ImageAspectFlags.ColorBit);
}
public static unsafe void InsertImageBarrier(
Vk api,
CommandBuffer commandBuffer,
Image image,
AccessFlags srcAccessMask,
AccessFlags dstAccessMask,
PipelineStageFlags srcStageMask,
PipelineStageFlags dstStageMask,
ImageAspectFlags aspectFlags,
int firstLayer,
int firstLevel,
int layers,
int levels)
{
ImageMemoryBarrier memoryBarrier = new ImageMemoryBarrier()
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = srcAccessMask,
DstAccessMask = dstAccessMask,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = image,
OldLayout = ImageLayout.General,
NewLayout = ImageLayout.General,
SubresourceRange = new ImageSubresourceRange(aspectFlags, (uint)firstLevel, (uint)levels, (uint)firstLayer, (uint)layers)
};
api.CmdPipelineBarrier(
commandBuffer,
srcStageMask,
dstStageMask,
0,
0,
null,
0,
null,
1,
memoryBarrier);
}
public TextureView GetView(GAL.Format format)
{
if (format == Info.Format)
{
return this;
}
if (_selfManagedViews != null && _selfManagedViews.TryGetValue(format, out var view))
{
return view;
}
view = CreateViewImpl(new TextureCreateInfo(
Info.Width,
Info.Height,
Info.Depth,
Info.Levels,
Info.Samples,
Info.BlockWidth,
Info.BlockHeight,
Info.BytesPerPixel,
format,
Info.DepthStencilMode,
Info.Target,
Info.SwizzleR,
Info.SwizzleG,
Info.SwizzleB,
Info.SwizzleA), 0, 0);
(_selfManagedViews ??= new Dictionary<GAL.Format, TextureView>()).Add(format, view);
return view;
}
public ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel)
{
return CreateViewImpl(info, firstLayer, firstLevel);
}
public TextureView CreateViewImpl(TextureCreateInfo info, int firstLayer, int firstLevel)
{
return new TextureView(_gd, _device, info, Storage, FirstLayer + firstLayer, FirstLevel + firstLevel);
}
public byte[] GetData(int x, int y, int width, int height)
{
int size = width * height * Info.BytesPerPixel;
using var bufferHolder = _gd.BufferManager.Create(_gd, size);
using (var cbs = _gd.CommandBufferPool.Rent())
{
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
var image = GetImage().Get(cbs).Value;
CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, size, true, 0, 0, x, y, width, height);
}
bufferHolder.WaitForFences();
byte[] bitmap = new byte[size];
GetDataFromBuffer(bufferHolder.GetDataStorage(0, size), size, Span<byte>.Empty).CopyTo(bitmap);
return bitmap;
}
public ReadOnlySpan<byte> GetData()
{
BackgroundResource resources = _gd.BackgroundResources.Get();
if (_gd.CommandBufferPool.OwnedByCurrentThread)
{
_gd.FlushAllCommands();
return GetData(_gd.CommandBufferPool, resources.GetFlushBuffer());
}
else
{
return GetData(resources.GetPool(), resources.GetFlushBuffer());
}
}
public ReadOnlySpan<byte> GetData(int layer, int level)
{
BackgroundResource resources = _gd.BackgroundResources.Get();
if (_gd.CommandBufferPool.OwnedByCurrentThread)
{
_gd.FlushAllCommands();
return GetData(_gd.CommandBufferPool, resources.GetFlushBuffer(), layer, level);
}
else
{
return GetData(resources.GetPool(), resources.GetFlushBuffer(), layer, level);
}
}
private ReadOnlySpan<byte> GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer)
{
int size = 0;
for (int level = 0; level < Info.Levels; level++)
{
size += Info.GetMipSize(level);
}
size = GetBufferDataLength(size);
Span<byte> result = flushBuffer.GetTextureData(cbp, this, size);
return GetDataFromBuffer(result, size, result);
}
private ReadOnlySpan<byte> GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer, int layer, int level)
{
int size = GetBufferDataLength(Info.GetMipSize(level));
Span<byte> result = flushBuffer.GetTextureData(cbp, this, size, layer, level);
return GetDataFromBuffer(result, size, result);
}
public void SetData(SpanOrArray<byte> data)
{
SetData(data, 0, 0, Info.GetLayers(), Info.Levels, singleSlice: false);
}
public void SetData(SpanOrArray<byte> data, int layer, int level)
{
SetData(data, layer, level, 1, 1, singleSlice: true);
}
public void SetData(SpanOrArray<byte> data, int layer, int level, Rectangle<int> region)
{
SetData(data, layer, level, 1, 1, singleSlice: true, region);
}
private void SetData(ReadOnlySpan<byte> data, int layer, int level, int layers, int levels, bool singleSlice, Rectangle<int>? region = null)
{
int bufferDataLength = GetBufferDataLength(data.Length);
using var bufferHolder = _gd.BufferManager.Create(_gd, bufferDataLength);
Auto<DisposableImage> imageAuto = GetImage();
// Load texture data inline if the texture has been used on the current command buffer.
bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer);
var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer();
if (loadInline)
{
_gd.PipelineInternal.EndRenderPass();
}
CopyDataToBuffer(bufferHolder.GetDataStorage(0, bufferDataLength), data);
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
var image = imageAuto.Get(cbs).Value;
if (region.HasValue)
{
CopyFromOrToBuffer(
cbs.CommandBuffer,
buffer,
image,
bufferDataLength,
false,
layer,
level,
region.Value.X,
region.Value.Y,
region.Value.Width,
region.Value.Height);
}
else
{
CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, bufferDataLength, false, layer, level, layers, levels, singleSlice);
}
}
private int GetBufferDataLength(int length)
{
if (NeedsD24S8Conversion())
{
return length * 2;
}
return length;
}
private GAL.Format GetCompatibleGalFormat(GAL.Format format)
{
if (NeedsD24S8Conversion())
{
return GAL.Format.D32FloatS8Uint;
}
return format;
}
private void CopyDataToBuffer(Span<byte> storage, ReadOnlySpan<byte> input)
{
if (NeedsD24S8Conversion())
{
FormatConverter.ConvertD24S8ToD32FS8(storage, input);
return;
}
input.CopyTo(storage);
}
private ReadOnlySpan<byte> GetDataFromBuffer(ReadOnlySpan<byte> storage, int size, Span<byte> output)
{
if (NeedsD24S8Conversion())
{
if (output.IsEmpty)
{
output = new byte[GetBufferDataLength(size)];
}
FormatConverter.ConvertD32FS8ToD24S8(output, storage);
return output;
}
return storage;
}
private bool NeedsD24S8Conversion()
{
return FormatCapabilities.IsD24S8(Info.Format) && VkFormat == VkFormat.D32SfloatS8Uint;
}
public void CopyFromOrToBuffer(
CommandBuffer commandBuffer,
VkBuffer buffer,
Image image,
int size,
bool to,
int dstLayer,
int dstLevel,
int dstLayers,
int dstLevels,
bool singleSlice)
{
bool is3D = Info.Target == Target.Texture3D;
int width = Math.Max(1, Info.Width >> dstLevel);
int height = Math.Max(1, Info.Height >> dstLevel);
int depth = is3D && !singleSlice ? Math.Max(1, Info.Depth >> dstLevel) : 1;
int layer = is3D ? 0 : dstLayer;
int layers = dstLayers;
int levels = dstLevels;
int offset = 0;
for (int level = 0; level < levels; level++)
{
int mipSize = GetBufferDataLength(Info.GetMipSize(dstLevel + level));
int endOffset = offset + mipSize;
if ((uint)endOffset > (uint)size)
{
break;
}
int rowLength = (Info.GetMipStride(dstLevel + level) / Info.BytesPerPixel) * Info.BlockWidth;
var aspectFlags = Info.Format.ConvertAspectFlags();
if (aspectFlags == (ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit))
{
aspectFlags = ImageAspectFlags.DepthBit;
}
var sl = new ImageSubresourceLayers(
aspectFlags,
(uint)(FirstLevel + dstLevel + level),
(uint)(FirstLayer + layer),
(uint)layers);
var extent = new Extent3D((uint)width, (uint)height, (uint)depth);
int z = is3D ? dstLayer : 0;
var region = new BufferImageCopy(
(ulong)offset,
(uint)AlignUpNpot(rowLength, Info.BlockWidth),
(uint)AlignUpNpot(height, Info.BlockHeight),
sl,
new Offset3D(0, 0, z),
extent);
if (to)
{
_gd.Api.CmdCopyImageToBuffer(commandBuffer, image, ImageLayout.General, buffer, 1, region);
}
else
{
_gd.Api.CmdCopyBufferToImage(commandBuffer, buffer, image, ImageLayout.General, 1, region);
}
offset += mipSize;
width = Math.Max(1, width >> 1);
height = Math.Max(1, height >> 1);
if (Info.Target == Target.Texture3D)
{
depth = Math.Max(1, depth >> 1);
}
}
}
private void CopyFromOrToBuffer(
CommandBuffer commandBuffer,
VkBuffer buffer,
Image image,
int size,
bool to,
int dstLayer,
int dstLevel,
int x,
int y,
int width,
int height)
{
var aspectFlags = Info.Format.ConvertAspectFlags();
if (aspectFlags == (ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit))
{
aspectFlags = ImageAspectFlags.DepthBit;
}
var sl = new ImageSubresourceLayers(aspectFlags, (uint)(FirstLevel + dstLevel), (uint)(FirstLayer + dstLayer), 1);
var extent = new Extent3D((uint)width, (uint)height, 1);
int rowLengthAlignment = Info.BlockWidth;
// We expect all data being written into the texture to have a stride aligned by 4.
if (!to && Info.BytesPerPixel < 4)
{
rowLengthAlignment = 4 / Info.BytesPerPixel;
}
var region = new BufferImageCopy(
0,
(uint)AlignUpNpot(width, rowLengthAlignment),
(uint)AlignUpNpot(height, Info.BlockHeight),
sl,
new Offset3D(x, y, 0),
extent);
if (to)
{
_gd.Api.CmdCopyImageToBuffer(commandBuffer, image, ImageLayout.General, buffer, 1, region);
}
else
{
_gd.Api.CmdCopyBufferToImage(commandBuffer, buffer, image, ImageLayout.General, 1, region);
}
}
private static int AlignUpNpot(int size, int alignment)
{
int remainder = size % alignment;
if (remainder == 0)
{
return size;
}
return size + (alignment - remainder);
}
public void SetStorage(BufferRange buffer)
{
throw new NotImplementedException();
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Valid = false;
if (_gd.Textures.Remove(this))
{
_imageView.Dispose();
_imageViewIdentity.Dispose();
_imageView2dArray?.Dispose();
Storage.DecrementViewsCount();
}
}
}
public void Dispose()
{
if (_selfManagedViews != null)
{
foreach (var view in _selfManagedViews.Values)
{
view.Dispose();
}
_selfManagedViews = null;
}
Dispose(true);
}
public void Release()
{
Dispose();
}
}
}