如何在Metal中实现Memoryless RenderTarget

解读

国内Unity项目(尤其iOS XR、数字孪生、高帧率竞技手游)常因高分辨率+MSAA+多Pass后处理导致显存带宽爆炸,帧率骤降。Metal的memoryless机制能把临时RenderTarget(如深度、G-Buffer、MotionVector、MSAA Resolve前缓冲)的片外显存占用降为零,仅活在tile memory里,节省50~200 MB显存带宽,是iOS端优化必考题。面试官想确认两点:

  1. 是否理解memoryless只适用于tile-based GPU(A7以后Apple全家桶),Desktop-style GPU不可用
  2. 能否在Unity/Metal两层给出可落地的代码与踩坑清单,而非背诵概念。

知识点

  1. tile-based deferred rendering(TBDR) 架构:vertex/fragment先执行,结果写tile memory,tile结束才写主存。
  2. memoryless attachment 在Metal里= MTLStorageModeMemoryless(iOS12+),仅可被framebuffer attachment使用不能采样、不能回读、不能跨帧
  3. Unity侧对应RenderTextureMemoryless.MSAARenderTextureMemoryless.Depth 两个枚举,只在iOS真机且Metal API生效;Editor/模拟器回退为普通显存。
  4. 使用场景
    • MSAA Color Buffer → 解析后立刻丢弃
    • 深度/模板 → 仅用于Early-Z,后续无采样
    • MRT中G-Buffer的某些通道 → 只在光照Pass内使用
  5. 限制与坑
    • 不能开启Random Write(即不能绑定为RWTexture)
    • 不能作为Input Attachment给后续Pass采样(除非用iOS14+的memoryless input-attachment扩展)
    • 不能与OpenGL ES回退路径混用
    • Xcode Frame Capture里显示为“Memoryless”但带宽统计仍会计入,需要看Instrument的Memory Bandwidth才能验证节省
  6. 验证方法
    • Xcode GPU Report → Device Memory栏,观察Attachment是否标记为Memoryless;
    • Instrument → Metal System Trace → 对比开启前后External Memory Read/Write Bytes/Sec

答案

分三步给出可直接抄进项目的答案:

  1. Unity高层脚本(C#)
// 只在iOS Metal真机开启
#if UNITY_IOS && !UNITY_EDITOR
const RenderTextureMemoryless ms = RenderTextureMemoryless.MSAA | RenderTextureMemoryless.Depth;
#else
const RenderTextureMemoryless ms = RenderTextureMemoryless.None;
#endif

var desc = new RenderTextureDescriptor(
    width, height, RenderTextureFormat.Default, 24)   // 24bit深度
{
    msaaSamples = 4,
    memoryless = ms,
    useDynamicScale = true
};
_camera.targetTexture = RenderTexture.GetTemporary(desc);
  1. 自定义URP RendererFeature(如果团队用URP)
    Create()里注入MemorylessPass,调用ScriptableRenderContext.ExecuteCommandBuffer(),把cmd.GetTemporaryRT(colorID, desc, FilterMode.Point, RenderTextureMemoryless.MSAA)写进去,确保只在AfterRendering之前ReleaseTemporaryRT,否则Xcode会回退为普通显存。

  2. 原生Metal验证代码(面试加分项,展示能读Metal)

// 创建memoryless纹理,仅attachment可用
MTLTextureDescriptor* mDesc = [MTLTextureDescriptor
    texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
                                 width:w
                                height:h
                             mipmapped:NO];
mDesc.storageMode = MTLStorageModeMemoryless;   // 关键
mDesc.usage = MTLTextureUsageRenderTarget;      // 不能加Sample
id<MTLTexture> colorMemless = [device newTextureWithDescriptor:mDesc];

// 绑定到renderPass
renderPassDesc.colorAttachments[0].texture = colorMemless;
renderPassDesc.colorAttachments[0].storeAction = MTLStoreActionDontCare; // 必须DontCare

踩坑提醒

  • 如果后续Pass需要读深度做软粒子,depth不能memoryless
  • 若项目同时支持iOS9,需要在运行时判断@available(iOS 12.0, *),否则直接crash;
  • Unity 2021.2之前版本在URP里对memoryless=Depth有bug,深度会被强制store,需手动升级到2021.3.16f1c1(中国版LTS已修复)。

拓展思考

  1. Android端能否复刻?
    Vulkan有VK_ATTACHMENT_STORE_OP_DONT_CARE + LAZILY_ALLOCATED显存,但Mali/GPU架构差异大tile memory尺寸由驱动决定实测节省带宽仅30%左右,且Adreno 530以下驱动bug多,国内项目普遍只敢在iOS开memoryless,Android用RenderScale 0.8 + fp16 RT替代。

  2. 与Unity新延迟管线(DRP)的交互
    Unity 2022.3新增的Deferred+Tile-Based Lighting在iOS默认把G-Buffer标为memoryless,但自定义G-Buffer格式(如RGB111110Float)会回退,需要手动改Runtime/Shader/DeferredLighting.hlsl里的USE_MEMORYLESS_GBUFFER宏。

  3. 面试反向提问
    当面试官听完标准答案,可主动追问:“咱们项目有没有做MSAA+TAA混合抗锯齿?如果有,memoryless MSAA color是否会在TAA history采样阶段被强制resolve,导致tile memory回写主存?”——把话题引到带宽实测与FrameGraph调度,展示深度优化能力,容易拿到SP offer。