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)")
}
}
}
注意事项
- 内存管理:纹理是重量级资源,及时释放不用的纹理
- 线程安全:MTLTexture 创建和访问需要在正确的线程上进行
- 格式选择:根据实际需求选择最合适的像素格式
- 存储模式:根据使用模式选择正确的存储模式
- 设备兼容性:某些格式和功能可能不被所有设备支持
- 性能监控:定期监控纹理内存使用情况
通过合理的纹理复用策略,可以显著减少内存分配开销,提高渲染性能。关键是要根据应用的具体需求设计合适的缓存和池化机制。
Giscus 未启用:请在
src/site.config.ts中配置 repoId 与 categoryId。