如何在Metal中实现Memoryless RenderTarget
解读
国内Unity项目(尤其iOS XR、数字孪生、高帧率竞技手游)常因高分辨率+MSAA+多Pass后处理导致显存带宽爆炸,帧率骤降。Metal的memoryless机制能把临时RenderTarget(如深度、G-Buffer、MotionVector、MSAA Resolve前缓冲)的片外显存占用降为零,仅活在tile memory里,节省50~200 MB显存带宽,是iOS端优化必考题。面试官想确认两点:
- 是否理解memoryless只适用于tile-based GPU(A7以后Apple全家桶),Desktop-style GPU不可用;
- 能否在Unity/Metal两层给出可落地的代码与踩坑清单,而非背诵概念。
知识点
- tile-based deferred rendering(TBDR) 架构:vertex/fragment先执行,结果写tile memory,tile结束才写主存。
- memoryless attachment 在Metal里=
MTLStorageModeMemoryless(iOS12+),仅可被framebuffer attachment使用,不能采样、不能回读、不能跨帧。 - Unity侧对应RenderTextureMemoryless.MSAA 与 RenderTextureMemoryless.Depth 两个枚举,只在iOS真机且Metal API生效;Editor/模拟器回退为普通显存。
- 使用场景:
- MSAA Color Buffer → 解析后立刻丢弃
- 深度/模板 → 仅用于Early-Z,后续无采样
- MRT中G-Buffer的某些通道 → 只在光照Pass内使用
- 限制与坑:
- 不能开启Random Write(即不能绑定为RWTexture)
- 不能作为Input Attachment给后续Pass采样(除非用iOS14+的memoryless input-attachment扩展)
- 不能与OpenGL ES回退路径混用
- Xcode Frame Capture里显示为“Memoryless”但带宽统计仍会计入,需要看Instrument的Memory Bandwidth才能验证节省
- 验证方法:
- 用Xcode GPU Report → Device Memory栏,观察Attachment是否标记为Memoryless;
- 用Instrument → Metal System Trace → 对比开启前后External Memory Read/Write Bytes/Sec。
答案
分三步给出可直接抄进项目的答案:
- 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);
-
自定义URP RendererFeature(如果团队用URP)
在Create()里注入MemorylessPass,调用ScriptableRenderContext.ExecuteCommandBuffer(),把cmd.GetTemporaryRT(colorID, desc, FilterMode.Point, RenderTextureMemoryless.MSAA)写进去,确保只在AfterRendering之前ReleaseTemporaryRT,否则Xcode会回退为普通显存。 -
原生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已修复)。
拓展思考
-
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替代。 -
与Unity新延迟管线(DRP)的交互
Unity 2022.3新增的Deferred+Tile-Based Lighting在iOS默认把G-Buffer标为memoryless,但自定义G-Buffer格式(如RGB111110Float)会回退,需要手动改Runtime/Shader/DeferredLighting.hlsl里的USE_MEMORYLESS_GBUFFER宏。 -
面试反向提问
当面试官听完标准答案,可主动追问:“咱们项目有没有做MSAA+TAA混合抗锯齿?如果有,memoryless MSAA color是否会在TAA history采样阶段被强制resolve,导致tile memory回写主存?”——把话题引到带宽实测与FrameGraph调度,展示深度优化能力,容易拿到SP offer。