using ChocolArm64.Memory; using OpenTK.Graphics.OpenGL; using Ryujinx.Common; using Ryujinx.Graphics.Gal; using Ryujinx.Graphics.Memory; using System; using System.Collections.Generic; namespace Ryujinx.Graphics.Texture { public static class ImageUtils { [Flags] private enum TargetBuffer { Color = 1 << 0, Depth = 1 << 1, Stencil = 1 << 2, DepthStencil = Depth | Stencil } private struct ImageDescriptor { public int BytesPerPixel { get; private set; } public int BlockWidth { get; private set; } public int BlockHeight { get; private set; } public int BlockDepth { get; private set; } public TargetBuffer Target { get; private set; } public ImageDescriptor(int BytesPerPixel, int BlockWidth, int BlockHeight, int BlockDepth, TargetBuffer Target) { this.BytesPerPixel = BytesPerPixel; this.BlockWidth = BlockWidth; this.BlockHeight = BlockHeight; this.BlockDepth = BlockDepth; this.Target = Target; } } private const GalImageFormat Snorm = GalImageFormat.Snorm; private const GalImageFormat Unorm = GalImageFormat.Unorm; private const GalImageFormat Sint = GalImageFormat.Sint; private const GalImageFormat Uint = GalImageFormat.Uint; private const GalImageFormat Float = GalImageFormat.Float; private const GalImageFormat Srgb = GalImageFormat.Srgb; private static readonly Dictionary s_TextureTable = new Dictionary() { { GalTextureFormat.RGBA32, GalImageFormat.RGBA32 | Sint | Uint | Float }, { GalTextureFormat.RGBA16, GalImageFormat.RGBA16 | Snorm | Unorm | Sint | Uint | Float }, { GalTextureFormat.RG32, GalImageFormat.RG32 | Sint | Uint | Float }, { GalTextureFormat.RGBA8, GalImageFormat.RGBA8 | Snorm | Unorm | Sint | Uint | Srgb }, { GalTextureFormat.RGB10A2, GalImageFormat.RGB10A2 | Snorm | Unorm | Sint | Uint }, { GalTextureFormat.RG8, GalImageFormat.RG8 | Snorm | Unorm | Sint | Uint }, { GalTextureFormat.R16, GalImageFormat.R16 | Snorm | Unorm | Sint | Uint | Float }, { GalTextureFormat.R8, GalImageFormat.R8 | Snorm | Unorm | Sint | Uint }, { GalTextureFormat.RG16, GalImageFormat.RG16 | Snorm | Unorm | Sint | Float }, { GalTextureFormat.R32, GalImageFormat.R32 | Sint | Uint | Float }, { GalTextureFormat.RGBA4, GalImageFormat.RGBA4 | Unorm }, { GalTextureFormat.RGB5A1, GalImageFormat.RGB5A1 | Unorm }, { GalTextureFormat.RGB565, GalImageFormat.RGB565 | Unorm }, { GalTextureFormat.R11G11B10F, GalImageFormat.R11G11B10 | Float }, { GalTextureFormat.D24S8, GalImageFormat.D24S8 | Unorm | Uint }, { GalTextureFormat.D32F, GalImageFormat.D32 | Float }, { GalTextureFormat.D32FX24S8, GalImageFormat.D32S8 | Float }, { GalTextureFormat.D16, GalImageFormat.D16 | Unorm }, //Compressed formats { GalTextureFormat.BptcSfloat, GalImageFormat.BptcSfloat | Float }, { GalTextureFormat.BptcUfloat, GalImageFormat.BptcUfloat | Float }, { GalTextureFormat.BptcUnorm, GalImageFormat.BptcUnorm | Unorm | Srgb }, { GalTextureFormat.BC1, GalImageFormat.BC1 | Unorm | Srgb }, { GalTextureFormat.BC2, GalImageFormat.BC2 | Unorm | Srgb }, { GalTextureFormat.BC3, GalImageFormat.BC3 | Unorm | Srgb }, { GalTextureFormat.BC4, GalImageFormat.BC4 | Unorm | Snorm }, { GalTextureFormat.BC5, GalImageFormat.BC5 | Unorm | Snorm }, { GalTextureFormat.Astc2D4x4, GalImageFormat.Astc2D4x4 | Unorm | Srgb }, { GalTextureFormat.Astc2D5x5, GalImageFormat.Astc2D5x5 | Unorm | Srgb }, { GalTextureFormat.Astc2D6x6, GalImageFormat.Astc2D6x6 | Unorm | Srgb }, { GalTextureFormat.Astc2D8x8, GalImageFormat.Astc2D8x8 | Unorm | Srgb }, { GalTextureFormat.Astc2D10x10, GalImageFormat.Astc2D10x10 | Unorm | Srgb }, { GalTextureFormat.Astc2D12x12, GalImageFormat.Astc2D12x12 | Unorm | Srgb }, { GalTextureFormat.Astc2D5x4, GalImageFormat.Astc2D5x4 | Unorm | Srgb }, { GalTextureFormat.Astc2D6x5, GalImageFormat.Astc2D6x5 | Unorm | Srgb }, { GalTextureFormat.Astc2D8x6, GalImageFormat.Astc2D8x6 | Unorm | Srgb }, { GalTextureFormat.Astc2D10x8, GalImageFormat.Astc2D10x8 | Unorm | Srgb }, { GalTextureFormat.Astc2D12x10, GalImageFormat.Astc2D12x10 | Unorm | Srgb }, { GalTextureFormat.Astc2D8x5, GalImageFormat.Astc2D8x5 | Unorm | Srgb }, { GalTextureFormat.Astc2D10x5, GalImageFormat.Astc2D10x5 | Unorm | Srgb }, { GalTextureFormat.Astc2D10x6, GalImageFormat.Astc2D10x6 | Unorm | Srgb } }; private static readonly Dictionary s_ImageTable = new Dictionary() { { GalImageFormat.RGBA32, new ImageDescriptor(16, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.RGBA16, new ImageDescriptor(8, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.RG32, new ImageDescriptor(8, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.RGBX8, new ImageDescriptor(4, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.RGBA8, new ImageDescriptor(4, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.BGRA8, new ImageDescriptor(4, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.RGB10A2, new ImageDescriptor(4, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.R32, new ImageDescriptor(4, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.RGBA4, new ImageDescriptor(2, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.BptcSfloat, new ImageDescriptor(16, 4, 4, 1, TargetBuffer.Color) }, { GalImageFormat.BptcUfloat, new ImageDescriptor(16, 4, 4, 1, TargetBuffer.Color) }, { GalImageFormat.BGR5A1, new ImageDescriptor(2, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.RGB5A1, new ImageDescriptor(2, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.RGB565, new ImageDescriptor(2, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.BGR565, new ImageDescriptor(2, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.BptcUnorm, new ImageDescriptor(16, 4, 4, 1, TargetBuffer.Color) }, { GalImageFormat.RG16, new ImageDescriptor(4, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.RG8, new ImageDescriptor(2, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.R16, new ImageDescriptor(2, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.R8, new ImageDescriptor(1, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.R11G11B10, new ImageDescriptor(4, 1, 1, 1, TargetBuffer.Color) }, { GalImageFormat.BC1, new ImageDescriptor(8, 4, 4, 1, TargetBuffer.Color) }, { GalImageFormat.BC2, new ImageDescriptor(16, 4, 4, 1, TargetBuffer.Color) }, { GalImageFormat.BC3, new ImageDescriptor(16, 4, 4, 1, TargetBuffer.Color) }, { GalImageFormat.BC4, new ImageDescriptor(8, 4, 4, 1, TargetBuffer.Color) }, { GalImageFormat.BC5, new ImageDescriptor(16, 4, 4, 1, TargetBuffer.Color) }, { GalImageFormat.Astc2D4x4, new ImageDescriptor(16, 4, 4, 1, TargetBuffer.Color) }, { GalImageFormat.Astc2D5x5, new ImageDescriptor(16, 5, 5, 1, TargetBuffer.Color) }, { GalImageFormat.Astc2D6x6, new ImageDescriptor(16, 6, 6, 1, TargetBuffer.Color) }, { GalImageFormat.Astc2D8x8, new ImageDescriptor(16, 8, 8, 1, TargetBuffer.Color) }, { GalImageFormat.Astc2D10x10, new ImageDescriptor(16, 10, 10, 1, TargetBuffer.Color) }, { GalImageFormat.Astc2D12x12, new ImageDescriptor(16, 12, 12, 1, TargetBuffer.Color) }, { GalImageFormat.Astc2D5x4, new ImageDescriptor(16, 5, 4, 1, TargetBuffer.Color) }, { GalImageFormat.Astc2D6x5, new ImageDescriptor(16, 6, 5, 1, TargetBuffer.Color) }, { GalImageFormat.Astc2D8x6, new ImageDescriptor(16, 8, 6, 1, TargetBuffer.Color) }, { GalImageFormat.Astc2D10x8, new ImageDescriptor(16, 10, 8, 1, TargetBuffer.Color) }, { GalImageFormat.Astc2D12x10, new ImageDescriptor(16, 12, 10, 1, TargetBuffer.Color) }, { GalImageFormat.Astc2D8x5, new ImageDescriptor(16, 8, 5, 1, TargetBuffer.Color) }, { GalImageFormat.Astc2D10x5, new ImageDescriptor(16, 10, 5, 1, TargetBuffer.Color) }, { GalImageFormat.Astc2D10x6, new ImageDescriptor(16, 10, 6, 1, TargetBuffer.Color) }, { GalImageFormat.D16, new ImageDescriptor(2, 1, 1, 1, TargetBuffer.Depth) }, { GalImageFormat.D24, new ImageDescriptor(4, 1, 1, 1, TargetBuffer.Depth) }, { GalImageFormat.D24S8, new ImageDescriptor(4, 1, 1, 1, TargetBuffer.DepthStencil) }, { GalImageFormat.D32, new ImageDescriptor(4, 1, 1, 1, TargetBuffer.Depth) }, { GalImageFormat.D32S8, new ImageDescriptor(8, 1, 1, 1, TargetBuffer.DepthStencil) } }; public static GalImageFormat ConvertTexture( GalTextureFormat Format, GalTextureType RType, GalTextureType GType, GalTextureType BType, GalTextureType AType, bool ConvSrgb) { if (!s_TextureTable.TryGetValue(Format, out GalImageFormat ImageFormat)) { throw new NotImplementedException($"Format 0x{((int)Format):x} not implemented!"); } if (!HasDepth(ImageFormat) && (RType != GType || RType != BType || RType != AType)) { throw new NotImplementedException($"Per component types are not implemented!"); } GalImageFormat FormatType = ConvSrgb ? Srgb : GetFormatType(RType); GalImageFormat CombinedFormat = (ImageFormat & GalImageFormat.FormatMask) | FormatType; if (!ImageFormat.HasFlag(FormatType)) { throw new NotImplementedException($"Format \"{CombinedFormat}\" not implemented!"); } return CombinedFormat; } public static GalImageFormat ConvertSurface(GalSurfaceFormat Format) { switch (Format) { case GalSurfaceFormat.RGBA32Float: return GalImageFormat.RGBA32 | Float; case GalSurfaceFormat.RGBA32Uint: return GalImageFormat.RGBA32 | Uint; case GalSurfaceFormat.RGBA16Float: return GalImageFormat.RGBA16 | Float; case GalSurfaceFormat.RGBA16Unorm: return GalImageFormat.RGBA16 | Unorm; case GalSurfaceFormat.RG32Float: return GalImageFormat.RG32 | Float; case GalSurfaceFormat.RG32Sint: return GalImageFormat.RG32 | Sint; case GalSurfaceFormat.RG32Uint: return GalImageFormat.RG32 | Uint; case GalSurfaceFormat.BGRA8Unorm: return GalImageFormat.BGRA8 | Unorm; case GalSurfaceFormat.BGRA8Srgb: return GalImageFormat.BGRA8 | Srgb; case GalSurfaceFormat.RGB10A2Unorm: return GalImageFormat.RGB10A2 | Unorm; case GalSurfaceFormat.RGBA8Unorm: return GalImageFormat.RGBA8 | Unorm; case GalSurfaceFormat.RGBA8Srgb: return GalImageFormat.RGBA8 | Srgb; case GalSurfaceFormat.RGBA8Snorm: return GalImageFormat.RGBA8 | Snorm; case GalSurfaceFormat.RG16Snorm: return GalImageFormat.RG16 | Snorm; case GalSurfaceFormat.RG16Unorm: return GalImageFormat.RG16 | Unorm; case GalSurfaceFormat.RG16Sint: return GalImageFormat.RG16 | Sint; case GalSurfaceFormat.RG16Float: return GalImageFormat.RG16 | Float; case GalSurfaceFormat.R11G11B10Float: return GalImageFormat.R11G11B10 | Float; case GalSurfaceFormat.R32Float: return GalImageFormat.R32 | Float; case GalSurfaceFormat.R32Uint: return GalImageFormat.R32 | Uint; case GalSurfaceFormat.RG8Unorm: return GalImageFormat.RG8 | Unorm; case GalSurfaceFormat.RG8Snorm: return GalImageFormat.RG8 | Snorm; case GalSurfaceFormat.R16Float: return GalImageFormat.R16 | Float; case GalSurfaceFormat.R16Unorm: return GalImageFormat.R16 | Unorm; case GalSurfaceFormat.R16Uint: return GalImageFormat.R16 | Uint; case GalSurfaceFormat.R8Unorm: return GalImageFormat.R8 | Unorm; case GalSurfaceFormat.R8Uint: return GalImageFormat.R8 | Uint; case GalSurfaceFormat.B5G6R5Unorm: return GalImageFormat.RGB565 | Unorm; case GalSurfaceFormat.BGR5A1Unorm: return GalImageFormat.BGR5A1 | Unorm; case GalSurfaceFormat.RGBX8Unorm: return GalImageFormat.RGBX8 | Unorm; } throw new NotImplementedException(Format.ToString()); } public static GalImageFormat ConvertZeta(GalZetaFormat Format) { switch (Format) { case GalZetaFormat.D32Float: return GalImageFormat.D32 | Float; case GalZetaFormat.S8D24Unorm: return GalImageFormat.D24S8 | Unorm; case GalZetaFormat.D16Unorm: return GalImageFormat.D16 | Unorm; case GalZetaFormat.D24X8Unorm: return GalImageFormat.D24 | Unorm; case GalZetaFormat.D24S8Unorm: return GalImageFormat.D24S8 | Unorm; case GalZetaFormat.D32S8X24Float: return GalImageFormat.D32S8 | Float; } throw new NotImplementedException(Format.ToString()); } public static byte[] ReadTexture(IMemory Memory, GalImage Image, long Position) { MemoryManager CpuMemory; if (Memory is NvGpuVmm Vmm) { CpuMemory = Vmm.Memory; } else { CpuMemory = (MemoryManager)Memory; } ISwizzle Swizzle = TextureHelper.GetSwizzle(Image); ImageDescriptor Desc = GetImageDescriptor(Image.Format); (int Width, int Height, int Depth) = GetImageSizeInBlocks(Image); int BytesPerPixel = Desc.BytesPerPixel; //Note: Each row of the texture needs to be aligned to 4 bytes. int Pitch = (Width * BytesPerPixel + 3) & ~3; int DataLayerSize = Height * Pitch * Depth; byte[] Data = new byte[DataLayerSize * Image.LayerCount]; int TargetMipLevel = Image.MaxMipmapLevel <= 1 ? 1 : Image.MaxMipmapLevel - 1; int LayerOffset = ImageUtils.GetLayerOffset(Image, TargetMipLevel); for (int Layer = 0; Layer < Image.LayerCount; Layer++) { for (int Z = 0; Z < Depth; Z++) { for (int Y = 0; Y < Height; Y++) { int OutOffs = (DataLayerSize * Layer) + Y * Pitch + (Z * Width * Height * BytesPerPixel); for (int X = 0; X < Width; X++) { long Offset = (uint)Swizzle.GetSwizzleOffset(X, Y, Z); CpuMemory.ReadBytes(Position + (LayerOffset * Layer) + Offset, Data, OutOffs, BytesPerPixel); OutOffs += BytesPerPixel; } } } } return Data; } public static void WriteTexture(NvGpuVmm Vmm, GalImage Image, long Position, byte[] Data) { ISwizzle Swizzle = TextureHelper.GetSwizzle(Image); ImageDescriptor Desc = GetImageDescriptor(Image.Format); (int Width, int Height, int Depth) = ImageUtils.GetImageSizeInBlocks(Image); int BytesPerPixel = Desc.BytesPerPixel; int InOffs = 0; for (int Z = 0; Z < Depth; Z++) for (int Y = 0; Y < Height; Y++) for (int X = 0; X < Width; X++) { long Offset = (uint)Swizzle.GetSwizzleOffset(X, Y, Z); Vmm.Memory.WriteBytes(Position + Offset, Data, InOffs, BytesPerPixel); InOffs += BytesPerPixel; } } // TODO: Support non 2D public static bool CopyTexture( NvGpuVmm Vmm, GalImage SrcImage, GalImage DstImage, long SrcAddress, long DstAddress, int SrcX, int SrcY, int DstX, int DstY, int Width, int Height) { ISwizzle SrcSwizzle = TextureHelper.GetSwizzle(SrcImage); ISwizzle DstSwizzle = TextureHelper.GetSwizzle(DstImage); ImageDescriptor Desc = GetImageDescriptor(SrcImage.Format); if (GetImageDescriptor(DstImage.Format).BytesPerPixel != Desc.BytesPerPixel) { return false; } int BytesPerPixel = Desc.BytesPerPixel; for (int Y = 0; Y < Height; Y++) for (int X = 0; X < Width; X++) { long SrcOffset = (uint)SrcSwizzle.GetSwizzleOffset(SrcX + X, SrcY + Y, 0); long DstOffset = (uint)DstSwizzle.GetSwizzleOffset(DstX + X, DstY + Y, 0); byte[] Texel = Vmm.ReadBytes(SrcAddress + SrcOffset, BytesPerPixel); Vmm.WriteBytes(DstAddress + DstOffset, Texel); } return true; } public static int GetSize(GalImage Image) { ImageDescriptor Desc = GetImageDescriptor(Image.Format); int ComponentCount = GetCoordsCountTextureTarget(Image.TextureTarget); if (IsArray(Image.TextureTarget)) ComponentCount--; int Width = DivRoundUp(Image.Width, Desc.BlockWidth); int Height = DivRoundUp(Image.Height, Desc.BlockHeight); int Depth = DivRoundUp(Image.Depth, Desc.BlockDepth); switch (ComponentCount) { case 1: return Desc.BytesPerPixel * Width * Image.LayerCount; case 2: return Desc.BytesPerPixel * Width * Height * Image.LayerCount; case 3: return Desc.BytesPerPixel * Width * Height * Depth * Image.LayerCount; default: throw new InvalidOperationException($"Invalid component count: {ComponentCount}"); } } public static int GetGpuSize(GalImage Image, bool forcePitch = false) { return TextureHelper.GetSwizzle(Image).GetImageSize(Image.MaxMipmapLevel) * Image.LayerCount; } public static int GetLayerOffset(GalImage Image, int MipLevel) { if (MipLevel <= 0) { MipLevel = 1; } return TextureHelper.GetSwizzle(Image).GetMipOffset(MipLevel); } public static int GetPitch(GalImageFormat Format, int Width) { ImageDescriptor Desc = GetImageDescriptor(Format); int Pitch = Desc.BytesPerPixel * DivRoundUp(Width, Desc.BlockWidth); Pitch = (Pitch + 0x1f) & ~0x1f; return Pitch; } public static int GetBlockWidth(GalImageFormat Format) { return GetImageDescriptor(Format).BlockWidth; } public static int GetBlockHeight(GalImageFormat Format) { return GetImageDescriptor(Format).BlockHeight; } public static int GetBlockDepth(GalImageFormat Format) { return GetImageDescriptor(Format).BlockDepth; } public static int GetAlignedWidth(GalImage Image) { ImageDescriptor Desc = GetImageDescriptor(Image.Format); int AlignMask; if (Image.Layout == GalMemoryLayout.BlockLinear) { AlignMask = Image.TileWidth * (64 / Desc.BytesPerPixel) - 1; } else { AlignMask = (32 / Desc.BytesPerPixel) - 1; } return (Image.Width + AlignMask) & ~AlignMask; } public static (int Width, int Height, int Depth) GetImageSizeInBlocks(GalImage Image) { ImageDescriptor Desc = GetImageDescriptor(Image.Format); return (DivRoundUp(Image.Width, Desc.BlockWidth), DivRoundUp(Image.Height, Desc.BlockHeight), DivRoundUp(Image.Depth, Desc.BlockDepth)); } public static int GetBytesPerPixel(GalImageFormat Format) { return GetImageDescriptor(Format).BytesPerPixel; } private static int DivRoundUp(int LHS, int RHS) { return (LHS + (RHS - 1)) / RHS; } public static bool HasColor(GalImageFormat Format) { return (GetImageDescriptor(Format).Target & TargetBuffer.Color) != 0; } public static bool HasDepth(GalImageFormat Format) { return (GetImageDescriptor(Format).Target & TargetBuffer.Depth) != 0; } public static bool HasStencil(GalImageFormat Format) { return (GetImageDescriptor(Format).Target & TargetBuffer.Stencil) != 0; } public static bool IsCompressed(GalImageFormat Format) { ImageDescriptor Desc = GetImageDescriptor(Format); return (Desc.BlockWidth | Desc.BlockHeight) != 1; } private static ImageDescriptor GetImageDescriptor(GalImageFormat Format) { GalImageFormat PixelFormat = Format & GalImageFormat.FormatMask; if (s_ImageTable.TryGetValue(PixelFormat, out ImageDescriptor Descriptor)) { return Descriptor; } throw new NotImplementedException($"Format \"{PixelFormat}\" not implemented!"); } private static GalImageFormat GetFormatType(GalTextureType Type) { switch (Type) { case GalTextureType.Snorm: return Snorm; case GalTextureType.Unorm: return Unorm; case GalTextureType.Sint: return Sint; case GalTextureType.Uint: return Uint; case GalTextureType.Float: return Float; default: throw new NotImplementedException(((int)Type).ToString()); } } public static TextureTarget GetTextureTarget(GalTextureTarget GalTextureTarget) { switch (GalTextureTarget) { case GalTextureTarget.OneD: return TextureTarget.Texture1D; case GalTextureTarget.TwoD: case GalTextureTarget.TwoDNoMipMap: return TextureTarget.Texture2D; case GalTextureTarget.ThreeD: return TextureTarget.Texture3D; case GalTextureTarget.OneDArray: return TextureTarget.Texture1DArray; case GalTextureTarget.OneDBuffer: return TextureTarget.TextureBuffer; case GalTextureTarget.TwoDArray: return TextureTarget.Texture2DArray; case GalTextureTarget.CubeMap: return TextureTarget.TextureCubeMap; case GalTextureTarget.CubeArray: return TextureTarget.TextureCubeMapArray; default: throw new NotSupportedException($"Texture target {GalTextureTarget} currently not supported!"); } } public static bool IsArray(GalTextureTarget TextureTarget) { switch (TextureTarget) { case GalTextureTarget.OneDArray: case GalTextureTarget.TwoDArray: case GalTextureTarget.CubeArray: return true; default: return false; } } public static int GetCoordsCountTextureTarget(GalTextureTarget TextureTarget) { switch (TextureTarget) { case GalTextureTarget.OneD: return 1; case GalTextureTarget.OneDArray: case GalTextureTarget.OneDBuffer: case GalTextureTarget.TwoD: case GalTextureTarget.TwoDNoMipMap: return 2; case GalTextureTarget.ThreeD: case GalTextureTarget.TwoDArray: case GalTextureTarget.CubeMap: return 3; case GalTextureTarget.CubeArray: return 4; default: throw new NotImplementedException($"TextureTarget.{TextureTarget} not implemented yet."); } } } }