using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Texture; using System; using System.Collections.Concurrent; using System.Collections.Generic; namespace Ryujinx.Graphics.Gpu.Image { /// <summary> /// Texture pool. /// </summary> class TexturePool : Pool<Texture, TextureDescriptor>, IPool<TexturePool> { private readonly GpuChannel _channel; private readonly ConcurrentQueue<Texture> _dereferenceQueue = new ConcurrentQueue<Texture>(); private TextureDescriptor _defaultDescriptor; /// <summary> /// Linked list node used on the texture pool cache. /// </summary> public LinkedListNode<TexturePool> CacheNode { get; set; } /// <summary> /// Timestamp used by the texture pool cache, updated on every use of this texture pool. /// </summary> public ulong CacheTimestamp { get; set; } /// <summary> /// Creates a new instance of the texture pool. /// </summary> /// <param name="context">GPU context that the texture pool belongs to</param> /// <param name="channel">GPU channel that the texture pool belongs to</param> /// <param name="address">Address of the texture pool in guest memory</param> /// <param name="maximumId">Maximum texture ID of the texture pool (equal to maximum textures minus one)</param> public TexturePool(GpuContext context, GpuChannel channel, ulong address, int maximumId) : base(context, channel.MemoryManager.Physical, address, maximumId) { _channel = channel; } /// <summary> /// Gets the texture descripor and texture with the given ID with no bounds check or synchronization. /// </summary> /// <param name="id">ID of the texture. This is effectively a zero-based index</param> /// <param name="texture">The texture with the given ID</param> /// <returns>The texture descriptor with the given ID</returns> private ref readonly TextureDescriptor GetInternal(int id, out Texture texture) { texture = Items[id]; ref readonly TextureDescriptor descriptor = ref GetDescriptorRef(id); if (texture == null) { texture = PhysicalMemory.TextureCache.FindShortCache(descriptor); if (texture == null) { TextureInfo info = GetInfo(descriptor, out int layerSize); ProcessDereferenceQueue(); texture = PhysicalMemory.TextureCache.FindOrCreateTexture(_channel.MemoryManager, TextureSearchFlags.ForSampler, info, layerSize); // If this happens, then the texture address is invalid, we can't add it to the cache. if (texture == null) { return ref descriptor; } } texture.IncrementReferenceCount(this, id); Items[id] = texture; DescriptorCache[id] = descriptor; } else { // On the path above (texture not yet in the pool), memory is automatically synchronized on texture creation. texture.SynchronizeMemory(); } return ref descriptor; } /// <summary> /// Gets the texture with the given ID. /// </summary> /// <param name="id">ID of the texture. This is effectively a zero-based index</param> /// <returns>The texture with the given ID</returns> public override Texture Get(int id) { if ((uint)id >= Items.Length) { return null; } if (SequenceNumber != Context.SequenceNumber) { SequenceNumber = Context.SequenceNumber; SynchronizeMemory(); } GetInternal(id, out Texture texture); return texture; } /// <summary> /// Gets the texture descriptor and texture with the given ID. /// </summary> /// <remarks> /// This method assumes that the pool has been manually synchronized before doing binding. /// </remarks> /// <param name="id">ID of the texture. This is effectively a zero-based index</param> /// <param name="texture">The texture with the given ID</param> /// <returns>The texture descriptor with the given ID</returns> public ref readonly TextureDescriptor GetForBinding(int id, out Texture texture) { if ((uint)id >= Items.Length) { texture = null; return ref _defaultDescriptor; } // When getting for binding, assume the pool has already been synchronized. return ref GetInternal(id, out texture); } /// <summary> /// Checks if the pool was modified, and returns the last sequence number where a modification was detected. /// </summary> /// <returns>A number that increments each time a modification is detected</returns> public int CheckModified() { if (SequenceNumber != Context.SequenceNumber) { SequenceNumber = Context.SequenceNumber; SynchronizeMemory(); } return ModifiedSequenceNumber; } /// <summary> /// Forcibly remove a texture from this pool's items. /// If deferred, the dereference will be queued to occur on the render thread. /// </summary> /// <param name="texture">The texture being removed</param> /// <param name="id">The ID of the texture in this pool</param> /// <param name="deferred">If true, queue the dereference to happen on the render thread, otherwise dereference immediately</param> public void ForceRemove(Texture texture, int id, bool deferred) { Items[id] = null; if (deferred) { _dereferenceQueue.Enqueue(texture); } else { texture.DecrementReferenceCount(); } } /// <summary> /// Process the dereference queue, decrementing the reference count for each texture in it. /// This is used to ensure that texture disposal happens on the render thread. /// </summary> private void ProcessDereferenceQueue() { while (_dereferenceQueue.TryDequeue(out Texture toRemove)) { toRemove.DecrementReferenceCount(); } } /// <summary> /// Implementation of the texture pool range invalidation. /// </summary> /// <param name="address">Start address of the range of the texture pool</param> /// <param name="size">Size of the range being invalidated</param> protected override void InvalidateRangeImpl(ulong address, ulong size) { ProcessDereferenceQueue(); ulong endAddress = address + size; for (; address < endAddress; address += DescriptorSize) { int id = (int)((address - Address) / DescriptorSize); Texture texture = Items[id]; if (texture != null) { ref TextureDescriptor cachedDescriptor = ref DescriptorCache[id]; ref readonly TextureDescriptor descriptor = ref GetDescriptorRefAddress(address); // If the descriptors are the same, the texture is the same, // we don't need to remove as it was not modified. Just continue. if (descriptor.Equals(ref cachedDescriptor)) { continue; } if (texture.HasOneReference()) { _channel.MemoryManager.Physical.TextureCache.AddShortCache(texture, ref cachedDescriptor); } texture.DecrementReferenceCount(this, id); Items[id] = null; } } } /// <summary> /// Gets texture information from a texture descriptor. /// </summary> /// <param name="descriptor">The texture descriptor</param> /// <param name="layerSize">Layer size for textures using a sub-range of mipmap levels, otherwise 0</param> /// <returns>The texture information</returns> private TextureInfo GetInfo(in TextureDescriptor descriptor, out int layerSize) { int depthOrLayers = descriptor.UnpackDepth(); int levels = descriptor.UnpackLevels(); TextureMsaaMode msaaMode = descriptor.UnpackTextureMsaaMode(); int samplesInX = msaaMode.SamplesInX(); int samplesInY = msaaMode.SamplesInY(); int stride = descriptor.UnpackStride(); TextureDescriptorType descriptorType = descriptor.UnpackTextureDescriptorType(); bool isLinear = descriptorType == TextureDescriptorType.Linear; Target target = descriptor.UnpackTextureTarget().Convert((samplesInX | samplesInY) != 1); int width = target == Target.TextureBuffer ? descriptor.UnpackBufferTextureWidth() : descriptor.UnpackWidth(); int height = descriptor.UnpackHeight(); if (target == Target.Texture2DMultisample || target == Target.Texture2DMultisampleArray) { // This is divided back before the backend texture is created. width *= samplesInX; height *= samplesInY; } // We use 2D targets for 1D textures as that makes texture cache // management easier. We don't know the target for render target // and copies, so those would normally use 2D targets, which are // not compatible with 1D targets. By doing that we also allow those // to match when looking for compatible textures on the cache. if (target == Target.Texture1D) { target = Target.Texture2D; height = 1; } else if (target == Target.Texture1DArray) { target = Target.Texture2DArray; height = 1; } uint format = descriptor.UnpackFormat(); bool srgb = descriptor.UnpackSrgb(); ulong gpuVa = descriptor.UnpackAddress(); if (!FormatTable.TryGetTextureFormat(format, srgb, out FormatInfo formatInfo)) { if (gpuVa != 0 && (int)format > 0) { Logger.Error?.Print(LogClass.Gpu, $"Invalid texture format 0x{format:X} (sRGB: {srgb})."); } formatInfo = FormatInfo.Default; } int gobBlocksInY = descriptor.UnpackGobBlocksInY(); int gobBlocksInZ = descriptor.UnpackGobBlocksInZ(); int gobBlocksInTileX = descriptor.UnpackGobBlocksInTileX(); layerSize = 0; int minLod = descriptor.UnpackBaseLevel(); int maxLod = descriptor.UnpackMaxLevelInclusive(); // Linear textures don't support mipmaps, so we don't handle this case here. if ((minLod != 0 || maxLod + 1 != levels) && target != Target.TextureBuffer && !isLinear) { int depth = TextureInfo.GetDepth(target, depthOrLayers); int layers = TextureInfo.GetLayers(target, depthOrLayers); SizeInfo sizeInfo = SizeCalculator.GetBlockLinearTextureSize( width, height, depth, levels, layers, formatInfo.BlockWidth, formatInfo.BlockHeight, formatInfo.BytesPerPixel, gobBlocksInY, gobBlocksInZ, gobBlocksInTileX); layerSize = sizeInfo.LayerSize; if (minLod != 0 && minLod < levels) { // If the base level is not zero, we additionally add the mip level offset // to the address, this allows the texture manager to find the base level from the // address if there is a overlapping texture on the cache that can contain the new texture. gpuVa += (ulong)sizeInfo.GetMipOffset(minLod); width = Math.Max(1, width >> minLod); height = Math.Max(1, height >> minLod); if (target == Target.Texture3D) { depthOrLayers = Math.Max(1, depthOrLayers >> minLod); } (gobBlocksInY, gobBlocksInZ) = SizeCalculator.GetMipGobBlockSizes(height, depth, formatInfo.BlockHeight, gobBlocksInY, gobBlocksInZ); } levels = (maxLod - minLod) + 1; } SwizzleComponent swizzleR = descriptor.UnpackSwizzleR().Convert(); SwizzleComponent swizzleG = descriptor.UnpackSwizzleG().Convert(); SwizzleComponent swizzleB = descriptor.UnpackSwizzleB().Convert(); SwizzleComponent swizzleA = descriptor.UnpackSwizzleA().Convert(); DepthStencilMode depthStencilMode = GetDepthStencilMode( formatInfo.Format, swizzleR, swizzleG, swizzleB, swizzleA); if (formatInfo.Format.IsDepthOrStencil()) { swizzleR = SwizzleComponent.Red; swizzleG = SwizzleComponent.Red; swizzleB = SwizzleComponent.Red; if (depthStencilMode == DepthStencilMode.Depth) { swizzleA = SwizzleComponent.One; } else { swizzleA = SwizzleComponent.Red; } } return new TextureInfo( gpuVa, width, height, depthOrLayers, levels, samplesInX, samplesInY, stride, isLinear, gobBlocksInY, gobBlocksInZ, gobBlocksInTileX, target, formatInfo, depthStencilMode, swizzleR, swizzleG, swizzleB, swizzleA); } /// <summary> /// Gets the texture depth-stencil mode, based on the swizzle components of each color channel. /// The depth-stencil mode is determined based on how the driver sets those parameters. /// </summary> /// <param name="format">The format of the texture</param> /// <param name="components">The texture swizzle components</param> /// <returns>The depth-stencil mode</returns> private static DepthStencilMode GetDepthStencilMode(Format format, params SwizzleComponent[] components) { // R = Depth, G = Stencil. // On 24-bits depth formats, this is inverted (Stencil is R etc). // NVN setup: // For depth, A is set to 1.0f, the other components are set to Depth. // For stencil, all components are set to Stencil. SwizzleComponent component = components[0]; for (int index = 1; index < 4 && !IsRG(component); index++) { component = components[index]; } if (!IsRG(component)) { return DepthStencilMode.Depth; } if (format == Format.D24UnormS8Uint) { return component == SwizzleComponent.Red ? DepthStencilMode.Stencil : DepthStencilMode.Depth; } else { return component == SwizzleComponent.Red ? DepthStencilMode.Depth : DepthStencilMode.Stencil; } } /// <summary> /// Checks if the swizzle component is equal to the red or green channels. /// </summary> /// <param name="component">The swizzle component to check</param> /// <returns>True if the swizzle component is equal to the red or green, false otherwise</returns> private static bool IsRG(SwizzleComponent component) { return component == SwizzleComponent.Red || component == SwizzleComponent.Green; } /// <summary> /// Decrements the reference count of the texture. /// This indicates that the texture pool is not using it anymore. /// </summary> /// <param name="item">The texture to be deleted</param> protected override void Delete(Texture item) { item?.DecrementReferenceCount(this); } public override void Dispose() { ProcessDereferenceQueue(); base.Dispose(); } } }