Article / 2026/05/29

MTLTexture API 完整指南

MTLTexture 是 Metal 框架中表示纹理资源的核心协议,继承自 MTLResource。它可以存储1D、2D、3D图像数据,用于着色器中的采样、渲染目标等多种用途。理解如何创建、使用和复用 MTLTexture 对性能优化...

🚀 MTLTexture API 完整指南

概述

MTLTexture 是 Metal 框架中表示纹理资源的核心协议,继承自 MTLResource。它可以存储1D、2D、3D图像数据,用于着色器中的采样、渲染目标等多种用途。理解如何创建、使用和复用 MTLTexture 对性能优化至关重要。

基本属性

纹理尺寸信息

// 获取纹理基本尺寸
let width = texture.width        // 宽度(像素)
let height = texture.height      // 高度(像素)  
let depth = texture.depth        // 深度(3D纹理)

// Mipmap 相关
let mipmapLevelCount = texture.mipmapLevelCount  // Mipmap 级别数量

// 数组和切片
let arrayLength = texture.arrayLength            // 纹理数组长度
let sampleCount = texture.sampleCount            // 多重采样数量

纹理类型和格式

// 纹理类型
let textureType = texture.textureType
// 可能的类型:
// - .type1D: 一维纹理
// - .type1DArray: 一维纹理数组
// - .type2D: 二维纹理(最常用)
// - .type2DArray: 二维纹理数组
// - .type2DMultisample: 多重采样2D纹理
// - .typeCube: 立方体贴图
// - .typeCubeArray: 立方体贴图数组
// - .type3D: 三维纹理

// 像素格式
let pixelFormat = texture.pixelFormat
// 常用格式:
// - .rgba8Unorm: 8位无符号归一化RGBA
// - .bgra8Unorm: 8位无符号归一化BGRA
// - .rgba16Float: 16位浮点RGBA
// - .rgba32Float: 32位浮点RGBA
// - .depth32Float: 32位深度
// - .r8Unorm: 单通道8位

资源属性(继承自 MTLResource)

// 基础资源属性
let label = texture.label                    // 调试标签
let device = texture.device                  // 所属设备
let cpuCacheMode = texture.cpuCacheMode      // CPU缓存模式
let storageMode = texture.storageMode        // 存储模式
let usage = texture.usage                    // 使用方式
let resourceOptions = texture.resourceOptions // 资源选项

// 内存占用
let allocatedSize = texture.allocatedSize    // 分配的内存大小(iOS 11.0+)

// 堆管理(用于资源堆)
let heap = texture.heap                      // 所属资源堆
let heapOffset = texture.heapOffset          // 在堆中的偏移

创建纹理的方法

1. 使用 MTLDevice 创建

// 创建纹理描述符
let descriptor = MTLTextureDescriptor()
descriptor.textureType = .type2D
descriptor.pixelFormat = .rgba8Unorm
descriptor.width = 512
descriptor.height = 512
descriptor.usage = [.shaderRead, .renderTarget]
descriptor.storageMode = .private

// 创建纹理
guard let texture = device.makeTexture(descriptor: descriptor) else {
    fatalError("Failed to create texture")
}

2. 从现有纹理创建视图

// 创建不同像素格式的视图
let viewTexture = texture.makeTextureView(pixelFormat: .bgra8Unorm)

// 创建纹理切片视图
let sliceTexture = texture.makeTextureView(
    pixelFormat: texture.pixelFormat,
    textureType: .type2D,
    levels: 0..<1,
    slices: 0..<1
)

3. 使用 MTKTextureLoader 创建

let textureLoader = MTKTextureLoader(device: device)
let texture = try textureLoader.newTexture(name: "image.jpg", scaleFactor: 1.0, bundle: nil, options: nil)

纹理数据操作

读取纹理数据

// 同步获取纹理字节数据
func getBytes(_ pixelBytes: UnsafeMutableRawPointer, 
              bytesPerRow: Int, 
              from region: MTLRegion, 
              mipmapLevel: Int)

// 使用示例
let region = MTLRegionMake2D(0, 0, texture.width, texture.height)
let bytesPerRow = texture.width * 4 // RGBA = 4字节
var pixelBuffer = [UInt8](repeating: 0, count: texture.width * texture.height * 4)

pixelBuffer.withUnsafeMutableBytes { bytes in
    texture.getBytes(bytes.baseAddress!, 
                    bytesPerRow: bytesPerRow, 
                    from: region, 
                    mipmapLevel: 0)
}

写入纹理数据

// 同步替换纹理数据
func replace(region: MTLRegion, 
            mipmapLevel: Int, 
            withBytes pixelBytes: UnsafeRawPointer, 
            bytesPerRow: Int)

// 使用示例  
let newPixelData: [UInt8] = generatePixelData() // 你的像素数据
newPixelData.withUnsafeBytes { bytes in
    texture.replace(region: region, 
                   mipmapLevel: 0, 
                   withBytes: bytes.baseAddress!, 
                   bytesPerRow: bytesPerRow)
}

3D纹理和纹理数组操作

// 3D纹理数据操作
func getBytes(_ pixelBytes: UnsafeMutableRawPointer, 
              bytesPerRow: Int, 
              bytesPerImage: Int, 
              from region: MTLRegion, 
              mipmapLevel: Int, 
              slice: Int)

// 替换3D纹理数据
func replace(region: MTLRegion, 
            mipmapLevel: Int, 
            slice: Int, 
            withBytes pixelBytes: UnsafeRawPointer, 
            bytesPerRow: Int, 
            bytesPerImage: Int)

创建纹理视图

基本纹理视图

// 创建相同格式的视图
let textureView = texture.makeTextureView(pixelFormat: texture.pixelFormat)

// 创建不同格式的视图(格式必须兼容)
let srgbView = texture.makeTextureView(pixelFormat: .rgba8Unorm_srgb)

高级纹理视图

// 创建特定级别和切片的视图
let mipmapView = texture.makeTextureView(
    pixelFormat: texture.pixelFormat,
    textureType: .type2D,
    levels: 1..<3,     // 使用mipmap级别1-2
    slices: 0..<1      // 使用第0个切片
)

// 从立方体贴图创建2D视图
let cubeTexture: MTLTexture = // 立方体贴图
let cubeFaceView = cubeTexture.makeTextureView(
    pixelFormat: cubeTexture.pixelFormat,
    textureType: .type2D,
    levels: 0..<1,
    slices: 0..<1  // 第0个面(+X面)
)

纹理复用策略

1. 纹理池 (Texture Pool)

class TexturePool {
    private let device: MTLDevice
    private var availableTextures: [String: [MTLTexture]] = [:]
    private let maxPoolSize = 10
    
    init(device: MTLDevice) {
        self.device = device
    }
    
    func getTexture(width: Int, height: Int, pixelFormat: MTLPixelFormat) -> MTLTexture {
        let key = "\(width)x\(height)_\(pixelFormat)"
        
        // 尝试从池中获取
        if var pool = availableTextures[key], !pool.isEmpty {
            let texture = pool.removeLast()
            availableTextures[key] = pool
            return texture
        }
        
        // 创建新纹理
        let descriptor = MTLTextureDescriptor.texture2DDescriptor(
            pixelFormat: pixelFormat,
            width: width,
            height: height,
            mipmapped: false
        )
        descriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
        descriptor.storageMode = .private
        
        guard let texture = device.makeTexture(descriptor: descriptor) else {
            fatalError("Failed to create texture")
        }
        
        texture.label = "PooledTexture_\(key)"
        return texture
    }
    
    func returnTexture(_ texture: MTLTexture) {
        let key = "\(texture.width)x\(texture.height)_\(texture.pixelFormat)"
        
        if availableTextures[key] == nil {
            availableTextures[key] = []
        }
        
        if availableTextures[key]!.count < maxPoolSize {
            availableTextures[key]!.append(texture)
        }
        // 超过最大池大小的纹理会被自动释放
    }
    
    func clearPool() {
        availableTextures.removeAll()
    }
}

2. 纹理缓存管理器

class TextureCacheManager {
    private let cache = NSCache<NSString, MTLTexture>()
    private let pool = TexturePool(device: device)
    
    init() {
        cache.countLimit = 50 // 最多缓存50个纹理
        cache.totalCostLimit = 100 * 1024 * 1024 // 100MB限制
    }
    
    func getTexture(for key: String, 
                   width: Int, 
                   height: Int, 
                   pixelFormat: MTLPixelFormat) -> MTLTexture {
        
        // 先检查缓存
        if let cachedTexture = cache.object(forKey: key as NSString) {
            return cachedTexture
        }
        
        // 从池中获取或创建新纹理
        let texture = pool.getTexture(width: width, height: height, pixelFormat: pixelFormat)
        
        // 计算内存成本
        let cost = width * height * pixelFormatBytesPerPixel(pixelFormat)
        cache.setObject(texture, forKey: key as NSString, cost: cost)
        
        return texture
    }
    
    func releaseTexture(for key: String) {
        if let texture = cache.object(forKey: key as NSString) {
            cache.removeObject(forKey: key as NSString)
            pool.returnTexture(texture)
        }
    }
    
    private func pixelFormatBytesPerPixel(_ format: MTLPixelFormat) -> Int {
        switch format {
        case .rgba8Unorm, .bgra8Unorm: return 4
        case .rgba16Float: return 8
        case .rgba32Float: return 16
        case .r8Unorm: return 1
        case .rg8Unorm: return 2
        default: return 4
        }
    }
}

3. 临时纹理管理

class TemporaryTextureManager {
    private let device: MTLDevice
    private var temporaryTextures: [MTLTexture] = []
    private let semaphore = DispatchSemaphore(value: 1)
    
    init(device: MTLDevice) {
        self.device = device
    }
    
    func getTemporaryTexture(descriptor: MTLTextureDescriptor) -> MTLTexture {
        semaphore.wait()
        defer { semaphore.signal() }
        
        // 查找可复用的纹理
        for (index, texture) in temporaryTextures.enumerated() {
            if isCompatible(texture: texture, descriptor: descriptor) {
                temporaryTextures.remove(at: index)
                return texture
            }
        }
        
        // 创建新的临时纹理
        guard let texture = device.makeTexture(descriptor: descriptor) else {
            fatalError("Failed to create temporary texture")
        }
        
        texture.label = "TemporaryTexture"
        return texture
    }
    
    func returnTemporaryTexture(_ texture: MTLTexture) {
        semaphore.wait()
        temporaryTextures.append(texture)
        semaphore.signal()
    }
    
    func cleanupOldTextures() {
        semaphore.wait()
        // 保留最近使用的少量纹理,释放其他的
        if temporaryTextures.count > 5 {
            temporaryTextures.removeFirst(temporaryTextures.count - 5)
        }
        semaphore.signal()
    }
    
    private func isCompatible(texture: MTLTexture, descriptor: MTLTextureDescriptor) -> Bool {
        return texture.width >= descriptor.width &&
               texture.height >= descriptor.height &&
               texture.pixelFormat == descriptor.pixelFormat &&
               texture.textureType == descriptor.textureType &&
               texture.usage.contains(descriptor.usage)
    }
}

纹理使用最佳实践

1. 渲染管线中的纹理管理

class RenderPipeline {
    private let device: MTLDevice
    private let texturePool: TexturePool
    private var intermediateTextures: [MTLTexture] = []
    
    init(device: MTLDevice) {
        self.device = device
        self.texturePool = TexturePool(device: device)
    }
    
    func render(inputTexture: MTLTexture, commandBuffer: MTLCommandBuffer) -> MTLTexture {
        let width = inputTexture.width
        let height = inputTexture.height
        
        // 获取中间纹理
        let intermediateTexture = texturePool.getTexture(
            width: width, 
            height: height, 
            pixelFormat: .rgba16Float
        )
        
        // 第一个渲染通道
        performFirstPass(input: inputTexture, output: intermediateTexture, commandBuffer: commandBuffer)
        
        // 获取输出纹理
        let outputTexture = texturePool.getTexture(
            width: width, 
            height: height, 
            pixelFormat: .rgba8Unorm
        )
        
        // 第二个渲染通道
        performSecondPass(input: intermediateTexture, output: outputTexture, commandBuffer: commandBuffer)
        
        // 添加完成回调来回收纹理
        commandBuffer.addCompletedHandler { [weak self] _ in
            self?.texturePool.returnTexture(intermediateTexture)
        }
        
        return outputTexture
    }
    
    private func performFirstPass(input: MTLTexture, output: MTLTexture, commandBuffer: MTLCommandBuffer) {
        // 渲染实现...
    }
    
    private func performSecondPass(input: MTLTexture, output: MTLTexture, commandBuffer: MTLCommandBuffer) {
        // 渲染实现...
    }
}

2. 纹理大小优化

class AdaptiveTextureManager {
    private let device: MTLDevice
    private var currentTextures: [String: MTLTexture] = [:]
    
    init(device: MTLDevice) {
        self.device = device
    }
    
    func getTexture(for identifier: String, requiredSize: CGSize, pixelFormat: MTLPixelFormat) -> MTLTexture {
        let requiredWidth = Int(requiredSize.width)
        let requiredHeight = Int(requiredSize.height)
        
        // 检查现有纹理是否可以复用
        if let existingTexture = currentTextures[identifier] {
            if existingTexture.width >= requiredWidth && 
               existingTexture.height >= requiredHeight &&
               existingTexture.pixelFormat == pixelFormat {
                return existingTexture
            } else {
                // 尺寸不匹配,需要创建新的
                currentTextures.removeValue(forKey: identifier)
            }
        }
        
        // 创建新纹理(稍微大一点以备将来使用)
        let expandedWidth = nextPowerOfTwo(requiredWidth)
        let expandedHeight = nextPowerOfTwo(requiredHeight)
        
        let descriptor = MTLTextureDescriptor.texture2DDescriptor(
            pixelFormat: pixelFormat,
            width: expandedWidth,
            height: expandedHeight,
            mipmapped: false
        )
        descriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
        
        guard let texture = device.makeTexture(descriptor: descriptor) else {
            fatalError("Failed to create adaptive texture")
        }
        
        texture.label = "AdaptiveTexture_\(identifier)"
        currentTextures[identifier] = texture
        
        return texture
    }
    
    private func nextPowerOfTwo(_ value: Int) -> Int {
        guard value > 0 else { return 1 }
        return 1 << (Int.bitWidth - (value - 1).leadingZeroBitCount)
    }
}

3. 内存监控和清理

class TextureMemoryManager {
    private let device: MTLDevice
    private var allocatedTextures: [(texture: MTLTexture, timestamp: Date)] = []
    private let maxTextureAge: TimeInterval = 30.0 // 30秒
    
    init(device: MTLDevice) {
        self.device = device
        
        // 监听内存警告
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleMemoryWarning),
            name: UIApplication.didReceiveMemoryWarningNotification,
            object: nil
        )
        
        // 定期清理
        Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { _ in
            self.cleanupOldTextures()
        }
    }
    
    func trackTexture(_ texture: MTLTexture) {
        allocatedTextures.append((texture: texture, timestamp: Date()))
    }
    
    @objc private func handleMemoryWarning() {
        print("收到内存警告,清理纹理缓存")
        cleanupOldTextures(forceCleanup: true)
    }
    
    private func cleanupOldTextures(forceCleanup: Bool = false) {
        let now = Date()
        let cutoffTime = now.addingTimeInterval(-maxTextureAge)
        
        allocatedTextures.removeAll { textureInfo in
            let shouldRemove = forceCleanup || textureInfo.timestamp < cutoffTime
            if shouldRemove {
                print("清理纹理: \(textureInfo.texture.label ?? "Unknown")")
            }
            return shouldRemove
        }
    }
    
    func getMemoryUsage() -> Int {
        return allocatedTextures.reduce(0) { total, textureInfo in
            total + textureInfo.texture.allocatedSize
        }
    }
}

性能优化建议

1. 纹理格式选择

// 根据用途选择合适的格式
func chooseOptimalFormat(for usage: TextureUsage) -> MTLPixelFormat {
    switch usage {
    case .colorBuffer:
        return .bgra8Unorm  // 最常用的颜色缓冲格式
    case .hdrColorBuffer:
        return .rgba16Float // HDR渲染
    case .normalMap:
        return .rg8Snorm    // 法线贴图只需要XY分量
    case .heightMap:
        return .r16Unorm    // 高度图单通道足够
    case .mask:
        return .r8Unorm     // 遮罩纹理
    }
}

2. Mipmap 管理

func shouldGenerateMipmaps(for texture: MTLTexture, usage: TextureUsage) -> Bool {
    // 渲染目标通常不需要mipmap
    if usage == .renderTarget {
        return false
    }
    
    // 小纹理不需要mipmap
    if texture.width < 64 || texture.height < 64 {
        return false
    }
    
    // UI纹理通常不需要mipmap
    if usage == .ui {
        return false
    }
    
    return true
}

3. 存储模式优化

func chooseStorageMode(for usage: TextureUsage, device: MTLDevice) -> MTLStorageMode {
    #if os(iOS)
    // iOS设备统一内存架构
    switch usage {
    case .staticTexture:
        return .shared  // CPU创建一次,GPU多次读取
    case .renderTarget:
        return .private // GPU专用,性能最佳
    case .dynamicTexture:
        return .shared  // 需要CPU更新
    }
    #else
    // macOS设备分离内存架构
    switch usage {
    case .staticTexture:
        return .managed // 自动同步
    case .renderTarget:
        return .private // GPU专用
    case .dynamicTexture:
        return .managed // 需要同步
    }
    #endif
}

调试和分析

纹理信息打印

extension MTLTexture {
    func printDebugInfo() {
        print("=== 纹理信息 ===")
        print("标签: \(label ?? "无标签")")
        print("尺寸: \(width) x \(height) x \(depth)")
        print("类型: \(textureType)")
        print("格式: \(pixelFormat)")
        print("Mipmap级别: \(mipmapLevelCount)")
        print("数组长度: \(arrayLength)")
        print("采样数: \(sampleCount)")
        print("用途: \(usage)")
        print("存储模式: \(storageMode)")
        print("分配大小: \(allocatedSize) 字节")
        print("==================")
    }
    
    var memoryFootprint: Int {
        let bytesPerPixel = pixelFormat.bytesPerPixel
        var totalBytes = 0
        
        for level in 0..<mipmapLevelCount {
            let levelWidth = max(1, width >> level)
            let levelHeight = max(1, height >> level)
            let levelDepth = max(1, depth >> level)
            
            totalBytes += levelWidth * levelHeight * levelDepth * bytesPerPixel * arrayLength
        }
        
        return totalBytes
    }
}

extension MTLPixelFormat {
    var bytesPerPixel: Int {
        switch self {
        case .r8Unorm, .r8Snorm: return 1
        case .rg8Unorm, .rg8Snorm: return 2
        case .rgba8Unorm, .bgra8Unorm: return 4
        case .rgba16Float: return 8
        case .rgba32Float: return 16
        case .depth32Float: return 4
        default: return 4
        }
    }
}

常见问题和解决方案

1. 纹理格式兼容性

func isFormatCompatible(_ format1: MTLPixelFormat, _ format2: MTLPixelFormat) -> Bool {
    // 检查两种格式是否可以互相转换
    let compatibleGroups: [[MTLPixelFormat]] = [
        [.rgba8Unorm, .rgba8Unorm_srgb],
        [.bgra8Unorm, .bgra8Unorm_srgb],
        [.rgba16Float, .rgba16Unorm],
    ]
    
    return compatibleGroups.contains { group in
        group.contains(format1) && group.contains(format2)
    }
}

2. 内存泄漏检测

class TextureLeakDetector {
    private static var allocatedTextures: Set<ObjectIdentifier> = []
    private static let queue = DispatchQueue(label: "texture.leak.detector")
    
    static func trackTexture(_ texture: MTLTexture) {
        queue.async {
            allocatedTextures.insert(ObjectIdentifier(texture))
        }
    }
    
    static func releaseTexture(_ texture: MTLTexture) {
        queue.async {
            allocatedTextures.remove(ObjectIdentifier(texture))
        }
    }
    
    static func printLeakReport() {
        queue.async {
            print("当前未释放的纹理数量: \(allocatedTextures.count)")
        }
    }
}

注意事项

  1. 内存管理:纹理是重量级资源,及时释放不用的纹理
  2. 线程安全:MTLTexture 创建和访问需要在正确的线程上进行
  3. 格式选择:根据实际需求选择最合适的像素格式
  4. 存储模式:根据使用模式选择正确的存储模式
  5. 设备兼容性:某些格式和功能可能不被所有设备支持
  6. 性能监控:定期监控纹理内存使用情况

通过合理的纹理复用策略,可以显著减少内存分配开销,提高渲染性能。关键是要根据应用的具体需求设计合适的缓存和池化机制。

Giscus 未启用:请在 src/site.config.ts 中配置 repoId 与 categoryId。