深入了解 metal 4 的最新改进。我们将介绍全新光线追踪功能,助你将复杂度极高且视觉效果丰富的工作负载成功移植到 apple 芯片上。了解 metalfx 如何通过渲染画质提升、帧插值和场景去噪来扩展工作负载。
为了充分从这个讲座中获益,我们建议你先观看“探索 metal 4”和“探索 metal 4 游戏”。
此文章由AI生成,可能存在错误,如有问题,请联系djs66256@163.com
深入探索 Metal 4 游戏
Metal 4 的推出为 Apple 平台上的游戏开发者带来了全新的可能性,特别是在光线追踪、渲染性能和视觉效果提升方面。本次演讲由 Matias Koskela 主讲,深入探讨了如何利用 MetalFX 和 Metal 4 的新特性来优化游戏性能,并将复杂的工作负载成功移植到 Apple 芯片上。以下是对演讲内容的全面解析。
简介
随着游戏画质的不断提升,实时渲染的复杂性和计算成本也在显著增加。例如《赛博朋克2077》这样的现代游戏,通过实时路径追踪技术实现了令人惊叹的画面效果,但这也对硬件性能提出了更高的要求。Metal 提供了一系列工具和 API,帮助开发者在 iPhone 到 Mac 的所有 Apple 平台上实现高质量的图形渲染。MetalFX Upscaling 和帧插值等技术可以有效应对高分辨率和高帧率带来的挑战,而新的光线追踪功能则进一步提升了渲染的真实感。
为了更好地理解这些新技术,建议先观看“探索 Metal 4”和“探索 Metal 4 游戏”,以获得关于 Metal 4 基础知识及其游戏应用的背景信息。
放大渲染
放大渲染是一种广泛使用的技术,它可以帮助开发者在不牺牲质量的前提下提高渲染性能。MetalFX 提供了一个基于机器学习的放大器,自 2022 年起便成为 Apple 平台的一部分,并逐年改进。以下是实现高质量放大的关键技术要点:
曝光参数设置
正确设置曝光输入参数对于放大器的输出质量至关重要。如果传递的曝光值与色调映射器不匹配,可能会导致闪烁或鬼影现象。为此,MetalFX 提供了一个名为“曝光调试器”的工具,可以通过设置环境变量 MTLFX_EXPOSURE_TOOL_ENABLED 来启用。该工具会在帧上叠加一个灰色棋盘格图案,并根据曝光值调整其亮度。如果曝光值设置正确,棋盘格应保持恒定的中灰色。
// 在主机代码中启用曝光调试器
setenv("MTLFX_EXPOSURE_TOOL_ENABLED", "1", 1);
动态分辨率支持
许多游戏采用动态分辨率渲染以适应不同场景的复杂度。MetalFX 时间放大器现在支持动态大小的输入,允许每帧传递不同分辨率的纹理。在高缩放比率下,建议将最大缩放比例限制在 2 倍以内,以确保缩放质量。
反应掩码
当渲染透明效果或粒子时(如烟花),这些元素通常不会被写入运动和深度纹理中,从而可能导致伪影或鬼影问题。MetalFX 引入了反应掩码功能,用于标记这些效果覆盖的区域。
// 在着色器中创建反应掩码设置
out.reactivity = m_material_id == eRain ? (m_material_id == eSpark ? 1.0f : 0.0f) : 0.8f;
// 在主机上编码放大器之前设置反应掩码
temporalUpscaler.reactiveMask = reactiveMaskTexture;
通过合理使用反应掩码,可以显著改善透明效果的渲染质量。
插值帧
帧插值是提高刷新率的一种有效方法,特别适用于需要流畅体验的游戏。MetalFX 帧插值器能够通过已渲染的像素生成插值帧,从而实现更高的帧率。
集成帧插值器
要使用 MetalFX 帧插值器,首先需要设置插值器对象并绑定必要的纹理输入,包括颜色、深度和运动矢量纹理。
// 创建并配置插值器描述符
MTLFXFrameInterpolatorDescriptor* desc = [MTLFXFrameInterpolatorDescriptor new];
desc.scaler = temporalScaler;
// 创建插值器并设置参数
id<MTLFXFrameInterpolator> interpolator = [desc newFrameInterpolatorWithDevice:device];
interpolator.motionVectorScaleX = mvecScaleX;
interpolator.motionVectorScaleY = mvecScaleY;
interpolator.depthReversed = YES;
// 设置输入纹理
interpolator.colorTexture = colorTexture;
interpolator.prevColorTexture = prevColorTexture;
interpolator.depthTexture = depthTexture;
interpolator.motionTexture = motionTexture;
interpolator.outputTexture = outputTexture;
UI 渲染策略
在插值帧中渲染 UI 是一个关键步骤。常见的三种方法包括合成 UI、离屏 UI 和每帧 UI。每种方法都有其优缺点,开发者可以根据需求选择最适合的方案。
- 合成 UI:插值器会尝试移除原始帧中的 UI 并将其放置在插值帧中的正确位置。
- 离屏 UI:将 UI 渲染到独立的纹理中,然后由插值器进行合成。
- 每帧 UI:完全由开发者负责更新插值帧的 UI,可提供最流畅的体验。
帧节奏协调
插值帧的呈现顺序和间隔对游戏的整体流畅性至关重要。通过使用 Metal HUD 工具,开发者可以轻松识别帧节奏是否偏离目标刷新率。
#include <thread>
#include <mutex>
#include <sys/event.h>
#include <mach/mach_time.h>
class PresentThread
{
int m_timerQueue;
std::thread m_encodingThread, m_pacingThread;
std::mutex m_mutex;
std::condition_variable m_scheduleCV, m_threadCV, m_pacingCV;
float m_minDuration;
uint32_t m_width, m_height;
MTLPixelFormat m_pixelFormat;
const static uint32_t kNumBuffers = 3;
uint32_t m_bufferIndex, m_inputIndex;
bool m_renderingUI, m_presentsPending;
CAMetalLayer *m_metalLayer;
id<MTLCommandQueue> m_presentQueue;
id<MTLEvent> m_event;
id<MTLSharedEvent> m_paceEvent, m_paceEvent2;
uint64_t m_eventValue;
uint32_t m_paceCount;
int32_t m_numQueued, m_framesInFlight;
id<MTLTexture> m_backBuffers[kNumBuffers];
id<MTLTexture> m_interpolationOutputs[kNumBuffers];
id<MTLTexture> m_interpolationInputs[2];
id<MTLRenderPipelineState> m_copyPipeline;
std::function<void(id<MTLRenderCommandEncoder>)> m_uiCallback = nullptr;
void PresentThreadFunction();
void PacingThreadFunction();
void CopyTexture(id<MTLCommandBuffer> commandBuffer, id<MTLTexture> dest, id<MTLTexture> src, NSString *label);
public:
PresentThread(float minDuration, CAMetalLayer *metalLayer);
~PresentThread()
{
std::unique_lock<std::mutex> lock(m_mutex);
m_numQueued = -1;
m_threadCV.notify_one();
m_encodingThread.join();
}
void StartFrame(id<MTLCommandBuffer> commandBuffer)
{
[commandBuffer encodeWaitForEvent:m_event value:m_eventValue++];
}
void StartUI(id<MTLCommandBuffer> commandBuffer)
{
assert(m_uiCallback == nullptr);
if(!m_renderingUI)
{
CopyTexture(commandBuffer, m_interpolationInputs[m_inputIndex], m_backBuffers[m_bufferIndex], @"Copy HUDLESS");
m_renderingUI = true;
}
}
void Present(id<MTLFXFrameInterpolator> frameInterpolator, id<MTLCommandQueue> queue);
id<MTLTexture> GetBackBuffer()
{
return m_backBuffers[m_bufferIndex];
}
void Resize(uint32_t width, uint32_t height, MTLPixelFormat pixelFormat);
void DrainPendingPresents()
{
std::unique_lock<std::mutex> lock(m_mutex);
while(m_presentsPending)
m_scheduleCV.wait(lock);
}
bool UICallbackEnabled() const
{
return m_uiCallback != nullptr;
}
void SetUICallback(std::function<void(id<MTLRenderCommandEncoder>)> callback)
{
m_uiCallback = callback;
}
};
PresentThread::PresentThread(float minDuration, CAMetalLayer *metalLayer)
: m_encodingThread(&PresentThread::PresentThreadFunction, this)
, m_pacingThread(&PresentThread::PacingThreadFunction, this)
, m_minDuration(minDuration)
, m_numQueued(0)
, m_metalLayer(metalLayer)
, m_inputIndex(0u)
, m_bufferIndex(0u)
, m_renderingUI(false)
, m_presentsPending(false)
, m_framesInFlight(0)
, m_paceCount(0)
, m_eventValue(0)
{
id<MTLDevice> device = metalLayer.device;
m_presentQueue = [device newCommandQueue];
m_presentQueue.label = @"presentQ";
m_timerQueue = kqueue();
metalLayer.maximumDrawableCount = 3;
Resize(metalLayer.drawableSize.width, metalLayer.drawableSize.height, metalLayer.pixelFormat);
m_event = [device newEvent];
m_paceEvent = [device newSharedEvent];
m_paceEvent2 = [device newSharedEvent];
}
void PresentThread::Present(id<MTLFXFrameInterpolator> frameInterpolator, id<MTLCommandQueue> queue)
{
id<MTLCommandBuffer> commandBuffer = [queue commandBuffer];
if(m_renderingUI)
{
frameInterpolator.colorTexture = m_interpolationInputs[m_inputIndex];
frameInterpolator.prevColorTexture = m_interpolationInputs[m_inputIndex^1];
frameInterpolator.uiTexture = m_backBuffers[m_bufferIndex];
}
else
{
frameInterpolator.colorTexture = m_backBuffers[m_bufferIndex];
frameInterpolator.prevColorTexture = m_backBuffers[(m_bufferIndex + kNumBuffers - 1) % kNumBuffers];
frameInterpolator.uiTexture = nullptr;
}
frameInterpolator.outputTexture = m_interpolationOutputs[m_bufferIndex];
[frameInterpolator encodeToCommandBuffer:commandBuffer];
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull) {
std::unique_lock<std::mutex> lock(m_mutex);
m_framesInFlight--;
m_scheduleCV.notify_one();
m_paceCount++;
m_pacingCV.notify_one();
}];
[commandBuffer encodeSignalEvent:m_event value:m_eventValue++];
[commandBuffer commit];
std::unique_lock<std::mutex> lock(m_mutex);
m_framesInFlight++;
m_numQueued++;
m_presentsPending = true;
m_threadCV.notify_one();
while((m_framesInFlight >= 2) || (m_numQueued >= 2))
m_scheduleCV.wait(lock);
m_bufferIndex = (m_bufferIndex + 1) % kNumBuffers;
m_inputIndex = m_inputIndex^1u;
m_renderingUI = false;
}
void PresentThread::CopyTexture(id<MTLCommandBuffer> commandBuffer, id<MTLTexture> dest, id<MTLTexture> src, NSString *label)
{
MTLRenderPassDescriptor *desc = [MTLRenderPassDescriptor new];
desc.colorAttachments[0].texture = dest;
desc.colorAttachments[0].loadAction = MTLLoadActionDontCare;
desc.colorAttachments[0].storeAction = MTLStoreActionStore;
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:desc];
[renderEncoder setFragmentTexture:src atIndex:0];
[renderEncoder setRenderPipelineState:m_copyPipeline];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
if(m_uiCallback)
m_uiCallback(renderEncoder);
renderEncoder.label = label;
[renderEncoder endEncoding];
}
void PresentThread::PacingThreadFunction()
{
NSThread *thread = [NSThread currentThread];
[thread setName:@"PacingThread"];
[thread setQualityOfService:NSQualityOfServiceUserInteractive];
[thread setThreadPriority:1.f];
mach_timebase_info_data_t info;
mach_timebase_info(&info);
// maximum delta (0.1ms) in machtime units
const uint64_t maxDeltaInNanoSecs = 100000000;
const uint64_t maxDelta = maxDeltaInNanoSecs * info.denom / info.numer;
uint64_t time = mach_absolute_time();
uint64_t paceEventValue = 0;
for(;;)
{
std::unique_lock<std::mutex> lock(m_mutex);
while(m_paceCount == 0)
m_pacingCV.wait(lock);
m_paceCount--;
lock.unlock();
// we get signal...
const uint64_t prevTime = time;
time = mach_absolute_time();
m_paceEvent.signaledValue = ++paceEventValue;
const uint64_t delta = std::min(time - prevTime, maxDelta);
const uint64_t timeStamp = time + ((delta*31)>>6);
struct kevent64_s timerEvent, eventOut;
struct timespec timeout;
timeout.tv_nsec = maxDeltaInNanoSecs;
timeout.tv_sec = 0;
EV_SET64(&timerEvent,
0,
EVFILT_TIMER,
EV_ADD | EV_ONESHOT | EV_ENABLE,
NOTE_CRITICAL | NOTE_LEEWAY | NOTE_MACHTIME | NOTE_ABSOLUTE,
timeStamp,
0,
0,
0);
kevent64(m_timerQueue, &timerEvent, 1, &eventOut, 1, 0, &timeout);
// main screen turn on...
m_paceEvent2.signaledValue = ++paceEventValue;
}
}
void PresentThread::PresentThreadFunction()
{
NSThread *thread = [NSThread currentThread];
[thread setName:@"PresentThread"];
[thread setQualityOfService:NSQualityOfServiceUserInteractive];
[thread setThreadPriority:1.f];
uint64_t eventValue = 0;
uint32_t bufferIndex = 0;
uint64_t paceEventValue = 0;
for(;;)
{
std::unique_lock<std::mutex> lock(m_mutex);
if(m_numQueued == 0)
{
m_presentsPending = false;
m_scheduleCV.notify_one();
}
while(m_numQueued == 0)
m_threadCV.wait(lock);
if(m_numQueued < 0)
break;
lock.unlock();
@autoreleasepool
{
id<CAMetalDrawable> drawable = [m_metalLayer nextDrawable];
lock.lock();
m_numQueued--;
m_scheduleCV.notify_one();
lock.unlock();
id<MTLCommandBuffer> commandBuffer = [m_presentQueue commandBuffer];
[commandBuffer encodeWaitForEvent:m_event value:++eventValue];
CopyTexture(commandBuffer, drawable.texture, m_interpolationOutputs[bufferIndex], @"Copy Interpolated");
[commandBuffer encodeSignalEvent:m_event value:++eventValue];
[commandBuffer encodeWaitForEvent:m_paceEvent value:++paceEventValue];
if(m_minDuration > 0.f)
[commandBuffer presentDrawable:drawable afterMinimumDuration:m_minDuration];
else
[commandBuffer presentDrawable:drawable];
[commandBuffer commit];
}
@autoreleasepool
{
id<MTLCommandBuffer> commandBuffer = [m_presentQueue commandBuffer];
id<CAMetalDrawable> drawable = [m_metalLayer nextDrawable];
CopyTexture(commandBuffer, drawable.texture, m_backBuffers[bufferIndex], @"Copy Rendered");
[commandBuffer encodeWaitForEvent:m_paceEvent2 value:++paceEventValue];
if(m_minDuration > 0.f)
[commandBuffer presentDrawable:drawable afterMinimumDuration:m_minDuration];
else
[commandBuffer presentDrawable:drawable];
[commandBuffer commit];
}
bufferIndex = (bufferIndex + 1) % kNumBuffers;
}
}
void PresentThread::Resize(uint32_t width, uint32_t height, MTLPixelFormat pixelFormat)
{
if((m_width != width) || (m_height != height) || (m_pixelFormat != pixelFormat))
{
id<MTLDevice> device = m_metalLayer.device;
if(m_pixelFormat != pixelFormat)
{
id<MTLLibrary> lib = [device newDefaultLibrary];
MTLRenderPipelineDescriptor *pipelineDesc = [MTLRenderPipelineDescriptor new];
pipelineDesc.vertexFunction = [lib newFunctionWithName:@"FSQ_VS_V4T2"];
pipelineDesc.fragmentFunction = [lib newFunctionWithName:@"FSQ_simpleCopy"];
pipelineDesc.colorAttachments[0].pixelFormat = pixelFormat;
m_copyPipeline = [device newRenderPipelineStateWithDescriptor:pipelineDesc error:nil];
m_pixelFormat = pixelFormat;
}
DrainPendingPresents();
m_width = width;
m_height = height;
MTLTextureDescriptor *texDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:pixelFormat width:width height:height mipmapped:NO];
texDesc.storageMode = MTLStorageModePrivate;
for(uint32_t i = 0; i < kNumBuffers; i++)
{
texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageShaderWrite|MTLTextureUsageRenderTarget;
m_backBuffers[i] = [device newTextureWithDescriptor:texDesc];
texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageRenderTarget;
m_interpolationOutputs[i] = [device newTextureWithDescriptor:texDesc];
}
texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageRenderTarget;
m_interpolationInputs[0] = [device newTextureWithDescriptor:texDesc];
m_interpolationInputs[1] = [device newTextureWithDescriptor:texDesc];
}
};
正确的帧节奏表现为平坦的线或规律的重复模式,具体取决于目标刷新率。
使用 Metal 4 追踪光线
光线追踪技术在高端渲染场景中越来越普及,Metal 4 提供了多项新功能来简化光线追踪的实现和优化。
交集函数缓冲区
交集函数缓冲区是一个包含指向场景交集函数句柄的参数缓冲区,允许开发者轻松管理多个交集函数。通过在实例和几何级别设置交集函数表偏移,可以灵活地定义每个几何体的交集行为。
// 在主机端几何描述符上设置交集函数表偏移
NSMutableArray<MTLAccelerationStructureGeometryDescriptor *> *geomDescs ...;
for (auto g = 0; g < geomList.size(); ++g)
{
MTLAccelerationStructureGeometryDescriptor *descriptor = ...;
descriptor.intersectionFunctionTableOffset = g;
...
[geomDescs addObject:descriptor];
}
加速结构构建优化
Metal 4 提供了更多控制加速结构构建的选项,包括快速交集和最小化内存使用。这些选项可以通过标志按需设置,以满足不同场景的需求。
// 光线追踪交集函数缓冲区
intersection_function_buffer_arguments ifb_arguments;
ifb_arguments.intersection_function_buffer = raytracingResources.ifbBuffer;
ifb_arguments.intersection_function_buffer_size = raytracingResources.ifbBufferSize;
ifb_arguments.intersection_function_stride = raytracingResources.ifbBufferStride;
metal::raytracing::ray r = { origin, direction };
auto result = trace.intersect(r, ads, ifb_arguments);
在放大时去噪
实时光线追踪的应用范围不断扩大,从混合光线追踪到复杂的路径追踪都得到了广泛应用。然而,光线追踪的性能和质量权衡始终是一个挑战。MetalFX 去噪放大器结合了去噪和放大功能,能够显著简化渲染管线的实现。
去噪放大器集成
去噪放大器的集成非常简单,只需在典型的时间放大器基础上添加额外的输入纹理,包括法线、漫反射反照率、粗糙度和镜面反照率。
// 设置去噪放大器的输入纹理
denoisingUpscaler.normalTexture = normalTexture;
denoisingUpscaler.albedoTexture = albedoTexture;
denoisingUpscaler.roughnessTexture = roughnessTexture;
denoisingUpscaler.specularTexture = specularTexture;
提高质量的技巧
去噪放大器的效果可以通过一些可选输入进一步提升,例如镜面命中距离、去噪强度掩码和透明度叠加。同时,需要注意避免输入过于嘈杂的问题,建议采用先进的采样技术,如重要性采样和下一次事件估计。
下一步
Metal 4 和 MetalFX 的新功能为开发者提供了强大的工具,用以优化游戏性能和提升视觉效果。无论是通过帧插值实现更高的刷新率,还是通过去噪放大器减少光线预算,这些技术都能显著改善玩家的体验。如果你尚未开始使用 MetalFX 放大器,这是一个值得尝试的机会。
相关视频
探索 Metal 4
探索 Metal 4 游戏
用于打造沉浸式 App 的 Metal 渲染的新功能
Metal 光线追踪指南
利用 MetalFX Upscaling 提升性能
