// SPDX-FileCopyrightText: 2023 Unity Technologies and the KTX for Unity authors // SPDX-License-Identifier: Apache-2.0 using System.Threading.Tasks; using UnityEngine; using UnityEngine.Experimental.Rendering; using UnityEngine.Profiling; using Unity.Collections; using UnityEngine.Assertions; namespace KtxUnity { /// /// Loads a KTX texture from the StreamingAssets folder, a URL, or a buffer. /// public class KtxTexture : TextureBase { KtxNativeInstance m_Ktx; // ReSharper disable MemberCanBePrivate.Global /// /// Query if the texture is in a transcodable format. /// public bool needsTranscoding => m_Ktx.needsTranscoding; /// /// True if the texture has an alpha channel. /// public bool hasAlpha => m_Ktx.hasAlpha; /// /// True if both pixel width and height are a power of two. /// public bool isPowerOfTwo => m_Ktx.isPowerOfTwo; /// /// True if both pixel width and height are a multiple of four. /// public bool isMultipleOfFour => m_Ktx.isMultipleOfFour; /// /// True if texture is square (width equals height) /// public bool isSquare => m_Ktx.isSquare; /// /// Width of largest mipmap level in pixels /// public uint baseWidth => m_Ktx.baseWidth; /// /// Height of largest mipmap level in pixels /// public uint baseHeight => m_Ktx.baseHeight; /// /// Depth of largest mipmap level in pixels /// public uint baseDepth => m_Ktx.baseDepth; /// /// Number of levels /// public uint numLevels => m_Ktx.numLevels; /// /// True if texture is of type array /// public bool isArray => m_Ktx.isArray; /// /// True if texture is of type cube map /// public bool isCubemap => m_Ktx.isCubemap; /// /// True if texture is compressed /// public bool isCompressed => m_Ktx.isCompressed; /// /// Number of dimensions /// public uint numDimensions => m_Ktx.numDimensions; /// /// Number of layers /// public uint numLayers => m_Ktx.numLayers; /// /// Number of faces (e.g. six for cube maps) /// public uint numFaces => m_Ktx.numFaces; /// /// Texture's orientation /// public TextureOrientation orientation => m_Ktx.orientation; // ReSharper restore MemberCanBePrivate.Global /// public override ErrorCode Open(NativeSlice data) { KtxNativeInstance.CertifySupportedPlatform(); return OpenInternal(data); } /// public override async Task LoadTexture2D( bool linear = false, uint layer = 0, uint faceSlice = 0, uint mipLevel = 0, bool mipChain = true ) { Assert.IsNotNull(m_Ktx, "KtxTexture in invalid state. Open has to be called first."); return await LoadTexture2DInternal( linear, layer, faceSlice, mipLevel, mipChain); } /// public override async Task LoadTexture2D( GraphicsFormat targetFormat, uint layer = 0, uint faceSlice = 0, uint mipLevel = 0, bool mipChain = true ) { Assert.IsNotNull(m_Ktx, "KtxTexture in invalid state. Open has to be called first."); if (!TranscodeFormatHelper.IsFormatSupported(targetFormat)) { return new TextureResult(ErrorCode.FormatUnsupportedBySystem); } return await LoadTexture2DInternal( true, layer, faceSlice, mipLevel, mipChain, targetFormat); } internal async Task LoadFromBytesInternal( NativeSlice data, bool linear = false, uint layer = 0, uint faceSlice = 0, uint mipLevel = 0, bool mipChain = true ) { var result = new TextureResult { errorCode = OpenInternal(data) }; if (result.errorCode != ErrorCode.Success) return result; result = await LoadTexture2DInternal(linear, layer, faceSlice, mipLevel, mipChain); Dispose(); return result; } ErrorCode OpenInternal(NativeSlice data) { m_Ktx = new KtxNativeInstance(); return m_Ktx.Load(data); } async Task LoadTexture2DInternal( bool linear = false, uint layer = 0, uint faceSlice = 0, uint mipLevel = 0, bool mipChain = true, GraphicsFormat? targetFormat = null ) { var result = new TextureResult(); var graphicsFormat = GraphicsFormat.None; if (m_Ktx.valid) { if (m_Ktx.ktxClass == KtxClassId.KtxTexture2) { if (m_Ktx.needsTranscoding) { TranscodeFormatTuple? formats; if (targetFormat.HasValue) { formats = TranscodeFormatHelper.GetTranscodeFormats(targetFormat.Value); } else { // TODO: Maybe do this somewhere more central TranscodeFormatHelper.Init(); formats = GetFormat(m_Ktx, m_Ktx, linear); } if (formats.HasValue) { graphicsFormat = formats.Value.format; #if KTX_VERBOSE Debug.LogFormat( "Transcode to GraphicsFormat {0} ({1})", formats.Value.format, formats.Value.transcodeFormat ); #endif result.errorCode = await TranscodeInternal( m_Ktx, formats.Value.transcodeFormat, layer, faceSlice, mipLevel ); result.orientation = m_Ktx.orientation; } else { result.errorCode = ErrorCode.UnsupportedFormat; } } else { graphicsFormat = m_Ktx.graphicsFormat; if (graphicsFormat == GraphicsFormat.None) { result.errorCode = ErrorCode.UnsupportedFormat; } else if (!TranscodeFormatHelper.IsFormatSupported(graphicsFormat, linear)) { result.errorCode = ErrorCode.FormatUnsupportedBySystem; } } } else { result.errorCode = ErrorCode.UnsupportedVersion; } } else { result.errorCode = ErrorCode.LoadingFailed; } if (result.errorCode != ErrorCode.Success) { return result; } Assert.IsTrue(m_Ktx.valid); Profiler.BeginSample("CreateTexture"); #if KTX_UNITY_GPU_UPLOAD if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.OpenGLCore || SystemInfo.graphicsDeviceType == GraphicsDeviceType.OpenGLES2 || SystemInfo.graphicsDeviceType == GraphicsDeviceType.OpenGLES3 ) { m_Ktx.EnqueueForGpuUpload(); Texture2D texture; bool success; while (!m_Ktx.TryCreateTexture(out texture, out success, m_Format)) { Profiler.EndSample(); await Task.Yield(); } if (success) { return new TextureResult { texture = texture }; } return new TextureResult(ErrorCode.LoadingFailed); } #endif try { var texture = m_Ktx.LoadTextureData( graphicsFormat, layer, mipLevel, faceSlice, mipChain ); result.texture = texture; } catch (UnityException) { result.errorCode = ErrorCode.LoadingFailed; } Profiler.EndSample(); return result; } /// public override void Dispose() { Assert.IsNotNull(m_Ktx, "KtxTexture in invalid state. Open has to be called first."); m_Ktx.Unload(); m_Ktx = null; } internal GraphicsFormat GetGraphicsFormat() { if (m_Ktx.valid && m_Ktx.ktxClass == KtxClassId.KtxTexture2 && !m_Ktx.needsTranscoding) { return m_Ktx.graphicsFormat; } return GraphicsFormat.None; } async Task TranscodeInternal( KtxNativeInstance ktx, TranscodeFormat format, uint layer, uint faceSlice, uint mipLevel ) { if (layer >= (isArray ? numLayers : 1)) { return ErrorCode.InvalidLayer; } if (isCubemap && faceSlice >= numFaces) { return ErrorCode.InvalidFace; } if (numDimensions > 2 && faceSlice >= baseDepth) { return ErrorCode.InvalidSlice; } if (mipLevel >= numLevels) { return ErrorCode.InvalidLevel; } var result = ErrorCode.Success; Profiler.BeginSample("KtxTranscode"); var job = new KtxTranscodeJob(); var jobHandle = ktx.LoadBytesJob(ref job, format); Profiler.EndSample(); while (!jobHandle.IsCompleted) { await Task.Yield(); } jobHandle.Complete(); if (job.result[0] != KtxErrorCode.Success) { result = ErrorCode.TranscodeFailed; } job.result.Dispose(); return result; } } }