导语
游戏运行在手机上出现功能问题时可以分析日志来做大致的定位,但是遇到渲染相关疑难问题时却无法即时取得当前一帧的数据来分析。我提供了一个插件,集成到APP之后可以做到取得当前状态GPU一帧,无需连接PC重启App复现截帧
作用
插件集成到游戏APP后,可以简化定位渲染问题的流程, 节省大量时间。
插件设计
基于unity native plugin, Android和iOS有不同的实现方式
iOS平台实现
移动平台的gpu截帧工具
在移动平台上进行 GPU 截帧(Frame Capture)是一种用于调试和优化图形性能的技术。通过截取和分析帧,你可以了解渲染管线的详细信息,找出性能瓶颈,并进行相应的优化。以下是一些常见的工具和方法,用于在移动平台上进行 GPU 截帧。
1. 使用 Android GPU Inspector (AGI)
Android GPU Inspector 是 Google 提供的一款强大的工具,用于分析和调试 Android 设备上的 GPU 性能。
安装和使用步骤:
-
下载和安装 AGI:
- 访问 Android GPU Inspector 官方网站 下载并安装工具。
-
连接设备:
- 使用 USB 线将 Android 设备连接到电脑,并确保设备处于开发者模式和 USB 调试模式。
-
启动 AGI:
- 打开 AGI,选择连接的设备。
-
捕获帧:
- 在 AGI 中选择要分析的应用,然后点击“Capture”按钮开始捕获帧。
- 运行应用并执行你想要分析的操作,然后停止捕获。
-
分析帧:
- AGI 会显示捕获的帧的详细信息,包括渲染调用、纹理、着色器等。你可以逐步检查每个渲染阶段,找出性能瓶颈。
2. 使用 Xcode GPU Frame Capture
对于 iOS 设备,Xcode 提供了内置的 GPU Frame Capture 工具,用于分析和调试图形性能。
安装和使用步骤:
-
安装 Xcode:
- 从 Mac App Store 下载并安装 Xcode。
-
连接设备:
- 使用 USB 线将 iOS 设备连接到 Mac,并确保设备处于开发者模式。
-
启动 Xcode:
- 打开 Xcode,选择你的项目并运行应用。
-
捕获帧:
- 在 Xcode 的调试工具栏中,点击“Debug”菜单,然后选择“Capture GPU Frame”。
- 运行应用并执行你想要分析的操作,然后停止捕获。
-
分析帧:
- Xcode 会显示捕获的帧的详细信息,包括渲染调用、纹理、着色器等。你可以逐步检查每个渲染阶段,找出性能瓶颈。
3. 使用 Unity Profiler 和 Frame Debugger
如果你在 Unity 中开发游戏,Unity 提供了内置的 Profiler 和 Frame Debugger 工具,用于分析和调试图形性能。
使用 Unity Profiler:
-
打开 Profiler:
- 在 Unity 编辑器中,选择“Window” > “Analysis” > “Profiler”。
-
连接设备:
- 确保你的移动设备连接到电脑,并在 Unity 中选择“Active Profiler”下拉菜单,选择你的设备。
-
捕获数据:
- 运行应用并执行你想要分析的操作,Profiler 会实时显示性能数据。
-
分析数据:
- 在 Profiler 窗口中,你可以查看 CPU、GPU、内存等各方面的性能数据,找出性能瓶颈。
使用 Unity Frame Debugger:
-
打开 Frame Debugger:
- 在 Unity 编辑器中,选择“Window” > “Analysis” > “Frame Debugger”。
-
启用 Frame Debugger:
- 在 Frame Debugger 窗口中,点击“Enable”按钮。
-
捕获帧:
- 运行应用并执行你想要分析的操作,Frame Debugger 会捕获当前帧的渲染调用。
-
分析帧:
- Frame Debugger 会显示捕获的帧的详细信息,包括每个渲染调用、纹理、着色器等。你可以逐步检查每个渲染阶段,找出性能瓶颈。
4. 使用 RenderDoc
RenderDoc 是一个开源的图形调试工具,支持多种平台,包括 Windows、Linux 和 Android。
安装和使用步骤:
-
下载和安装 RenderDoc:
- 访问 RenderDoc 官方网站 下载并安装工具。
-
连接设备:
- 使用 USB 线将 Android 设备连接到电脑,并确保设备处于开发者模式和 USB 调试模式。
-
配置 RenderDoc:
- 打开 RenderDoc,点击“Tools”菜单,选择“Manage Android Packages”。
- 在弹出的窗口中,点击“Add Device”,选择你的 Android 设备。
- 在“Package Name”字段中输入你要调试的应用的包名,然后点击“OK”。
-
捕获帧:
- 在 RenderDoc 主界面中,选择你的设备和应用,然后点击“Launch”按钮启动应用。
- 运行应用并执行你想要分析的操作,然后点击“Capture Frame(s)”按钮捕获帧。
-
分析帧:
- RenderDoc 会显示捕获的帧的详细信息,包括渲染调用、纹理、着色器等。你可以逐步检查每个渲染阶段,找出性能瓶颈。
5. 使用 Mali Graphics Debugger
如果你在使用 ARM Mali GPU 的设备上进行开发,ARM 提供了 Mali Graphics Debugger 工具,用于分析和调试图形性能。
安装和使用步骤:
-
下载和安装 Mali Graphics Debugger:
- 访问 ARM Developer 网站 下载并安装工具。
-
连接设备:
- 使用 USB 线将 Android 设备连接到电脑,并确保设备处于开发者模式和 USB 调试模式。
-
配置 Mali Graphics Debugger:
- 打开 Mali Graphics Debugger,选择“File” > “New Connection”。
- 在弹出的窗口中,选择你的设备并输入应用的包名。
-
捕获帧:
- 在 Mali Graphics Debugger 主界面中,点击“Connect”按钮连接设备。
- 运行应用并执行你想要分析的操作,然后点击“Capture Frame”按钮捕获帧。
-
分析帧:
- Mali Graphics Debugger 会显示捕获的帧的详细信息,包括渲染调用、纹理、着色器等。你可以逐步检查每个渲染阶段,找出性能瓶颈。
6. 使用 Snapdragon Profiler
如果你在使用 Qualcomm Snapdragon GPU 的设备上进行开发,Qualcomm 提供了 Snapdragon Profiler 工具,用于分析和调试图形性能。
安装和使用步骤:
-
下载和安装 Snapdragon Profiler:
- 访问 Qualcomm Developer Network 网站 下载并安装工具。
-
连接设备:
- 使用 USB 线将 Android 设备连接到电脑,并确保设备处于开发者模式和 USB 调试模式。
-
配置 Snapdragon Profiler:
- 打开 Snapdragon Profiler,选择“File” > “New Session”。
- 在弹出的窗口中,选择你的设备并输入应用的包名。
-
捕获帧:
- 在 Snapdragon Profiler 主界面中,点击“Start”按钮启动应用。
- 运行应用并执行你想要分析的操作,然后点击“Capture Frame”按钮捕获帧。
-
分析帧:
- Snapdragon Profiler 会显示捕获的帧的详细信息,包括渲染调用、纹理、着色器等。你可以逐步检查每个渲染阶段,找出性能瓶颈。
总结
在移动平台上进行 GPU 截帧是调试和优化图形性能的重要手段。不同的工具和平台提供了不同的功能和使用方法。以下是一些常见的工具和它们的适用场景:
- Android GPU Inspector (AGI):适用于 Android 设备,提供详细的 GPU 性能分析。
- Xcode GPU Frame Capture:适用于 iOS 设备,内置于 Xcode 中,方便 iOS 开发者使用。
- Unity Profiler 和 Frame Debugger:适用于使用 Unity 开发的游戏,提供实时性能数据和帧分析。
sdk插件实现思路
在移动平台上实现 GPU 截帧功能,通常需要使用原生 SDK 插件来与底层图形 API 进行交互。以下是一个实现思路,涵盖了 Android 和 iOS 平台的基本步骤。
Android 平台
1. 使用 OpenGL ES 截帧
在 Android 平台上,你可以使用 OpenGL ES API 来实现 GPU 截帧。以下是一个基本的实现思路:
-
创建一个 OpenGL ES 环境:
- 使用
GLSurfaceView
或GLTextureView
创建一个 OpenGL ES 渲染环境。
- 使用
-
实现一个自定义的 Renderer:
- 实现
GLSurfaceView.Renderer
接口,并在onDrawFrame
方法中进行渲染操作。
- 实现
-
捕获帧缓冲区:
- 在
onDrawFrame
方法中,使用glReadPixels
函数从帧缓冲区读取像素数据。
- 在
-
保存帧数据:
- 将读取的像素数据保存为图像文件或传递给上层应用进行处理。
示例代码:
public class MyGLRenderer implements GLSurfaceView.Renderer {
private int width;
private int height;
private ByteBuffer pixelBuffer;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 初始化 OpenGL ES 环境
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
this.width = width;
this.height = height;
pixelBuffer = ByteBuffer.allocateDirect(width * height * 4);
}
@Override
public void onDrawFrame(GL10 gl) {
// 渲染操作
// ...
// 捕获帧缓冲区
gl.glReadPixels(0, 0, width, height, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, pixelBuffer);
// 处理像素数据
saveFrame(pixelBuffer);
}
private void saveFrame(ByteBuffer buffer) {
// 将像素数据保存为图像文件
// ...
}
}
1. 使用 Metal 截帧
在 iOS 平台上,你可以使用 Metal API 来实现 GPU 截帧。以下是一个基本的实现思路:
-
创建一个 Metal 渲染环境:
- 使用
MTKView
创建一个 Metal 渲染环境。
- 使用
-
实现一个自定义的 Renderer:
- 实现
MTKViewDelegate
接口,并在draw(in:)
方法中进行渲染操作。
- 实现
-
捕获帧缓冲区:
- 在
draw(in:)
方法中,使用MTLTexture
对象从帧缓冲区读取像素数据。
- 在
-
保存帧数据:
- 将读取的像素数据保存为图像文件或传递给上层应用进行处理。
示例代码:
import MetalKit
class MyMetalRenderer: NSObject, MTKViewDelegate {
var device: MTLDevice!
var commandQueue: MTLCommandQueue!
init(mtkView: MTKView) {
self.device = mtkView.device
self.commandQueue = device.makeCommandQueue()
super.init()
}
func draw(in view: MTKView) {
guard let drawable = view.currentDrawable,
let descriptor = view.currentRenderPassDescriptor else {
return
}
let commandBuffer = commandQueue.makeCommandBuffer()
let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: descriptor)
// 渲染操作
// ...
renderEncoder?.endEncoding()
commandBuffer?.present(drawable)
commandBuffer?.commit()
// 捕获帧缓冲区
captureFrame(texture: drawable.texture)
}
private func captureFrame(texture: MTLTexture) {
let width = texture.width
let height = texture.height
let pixelCount = width * height
var pixelData = [UInt8](repeating: 0, count: pixelCount * 4)
let region = MTLRegionMake2D(0, 0, width, height)
texture.getBytes(&pixelData, bytesPerRow: width * 4, from: region, mipmapLevel: 0)
// 处理像素数据
saveFrame(pixelData: pixelData, width: width, height: height)
}
private func saveFrame(pixelData: [UInt8], width: Int, height: Int) {
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
let bitsPerComponent = 8
let bytesPerRow = width * 4
guard let context = CGContext(data: UnsafeMutableRawPointer(mutating: pixelData),
width: width,
height: height,
bitsPerComponent: bitsPerComponent,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: bitmapInfo.rawValue) else {
print("Failed to create CGContext")
return
}
guard let cgImage = context.makeImage() else {
print("Failed to create CGImage")
return
}
let uiImage = UIImage(cgImage: cgImage)
if let pngData = uiImage.pngData() {
let filePath = NSTemporaryDirectory().appending("captured_frame.png")
let fileURL = URL(fileURLWithPath: filePath)
do {
try pngData.write(to: fileURL)
print("Frame saved to \(filePath)")
} catch {
print("Failed to save frame: \(error)")
}
}
}
总结
在移动平台上实现 GPU 截帧功能,通常需要使用原生 SDK 插件来与底层图形 API 进行交互。以下是实现的基本步骤:
-
创建渲染环境:
- 在 Android 平台上使用
GLSurfaceView
或GLTextureView
创建 OpenGL ES 渲染环境。 - 在 iOS 平台上使用
MTKView
创建 Metal 渲染环境。
- 在 Android 平台上使用
-
实现自定义 Renderer:
- 在 Android 平台上实现
GLSurfaceView.Renderer
接口。 - 在 iOS 平台上实现
MTKViewDelegate
接口。
- 在 Android 平台上实现
-
捕获帧缓冲区:
- 在渲染操作完成后,使用
glReadPixels
或MTLTexture.getBytes
函数从帧缓冲区读取像素数据。
- 在渲染操作完成后,使用
-
保存帧数据:
- 将读取的像素数据保存为图像文件或传递给上层应用进行处理。
通过上述步骤,你可以在移动平台上实现 GPU 截帧功能,并将捕获的帧数据用于性能分析、调试或其他用途。
Android RenderDoc 是如何实现截帧的
RenderDoc 是一个强大的开源图形调试工具,它通过拦截和记录图形 API 调用来实现截帧功能。具体到 Android 平台,RenderDoc 通过以下步骤实现截帧:
1. 拦截图形 API 调用
RenderDoc 通过注入代码到目标应用程序的进程中,拦截所有的图形 API 调用(如 OpenGL ES 或 Vulkan)。这通常是通过动态链接库(DLL 或 SO 文件)注入来实现的。注入的代码会替换原始的图形 API 函数,使得所有的图形调用都经过 RenderDoc 的处理。
2. 记录图形命令
在拦截到图形 API 调用后,RenderDoc 会记录这些调用及其参数。这包括所有的绘制命令、状态设置、资源绑定等。通过记录这些信息,RenderDoc 可以重现整个渲染过程。
3. 捕获帧缓冲区
当用户触发帧捕获时,RenderDoc 会在适当的时机捕获当前帧缓冲区的内容。这通常是在交换缓冲区(swap buffers)或提交命令缓冲区(submit command buffer)时进行的。RenderDoc 会保存帧缓冲区的图像数据,以便后续分析。
4. 保存捕获数据
RenderDoc 会将所有记录的图形命令和捕获的帧缓冲区数据保存到一个捕获文件中。这个文件包含了重现整个渲染过程所需的所有信息。
5. 分析和重现
用户可以在 RenderDoc 的图形界面中打开捕获文件,查看和分析捕获的帧。RenderDoc 提供了详细的图形调用序列、资源视图、着色器调试等功能,帮助用户理解和优化渲染过程。
实现细节
以下是一些实现细节,帮助你更好地理解 RenderDoc 在 Android 平台上是如何工作的:
1. 动态链接库注入
RenderDoc 使用动态链接库注入技术,将自己的代码注入到目标应用程序的进程中。在 Android 平台上,这通常是通过 LD_PRELOAD
环境变量来实现的。RenderDoc 会在应用程序启动时加载自己的 SO 文件,并替换原始的图形 API 函数。
2. 拦截和记录图形 API 调用
RenderDoc 的注入代码会拦截所有的图形 API 调用,并将这些调用及其参数记录下来。以下是一个简单的示例,展示了如何拦截 OpenGL ES 函数:
// 原始的 glDrawArrays 函数指针
typedef void (*PFNGLDRAWARRAYS)(GLenum mode, GLint first, GLsizei count);
PFNGLDRAWARRAYS real_glDrawArrays = NULL;
// 拦截的 glDrawArrays 函数
void my_glDrawArrays(GLenum mode, GLint first, GLsizei count) {
// 记录调用
RenderDoc_RecordDrawArrays(mode, first, count);
// 调用原始的 glDrawArrays 函数
real_glDrawArrays(mode, first, count);
}
// 初始化拦截
void InitializeInterception() {
// 获取原始的 glDrawArrays 函数指针
real_glDrawArrays = (PFNGLDRAWARRAYS)dlsym(RTLD_NEXT, "glDrawArrays");
// 替换 glDrawArrays 函数
// 这通常是通过修改函数指针表(如 GOT 表)来实现的
ReplaceFunctionPointer("glDrawArrays", my_glDrawArrays);
}
3. 捕获帧缓冲区
RenderDoc 会在适当的时机捕获当前帧缓冲区的内容。这通常是在交换缓冲区(如 eglSwapBuffers
)或提交命令缓冲区(如 vkQueueSubmit
)时进行的。以下是一个简单的示例,展示了如何捕获 OpenGL ES 帧缓冲区:
// 拦截的 eglSwapBuffers 函数
EGLBoolean my_eglSwapBuffers(EGLDisplay dpy, EGLSurface surface) {
// 捕获帧缓冲区
RenderDoc_CaptureFrameBuffer(dpy, surface);
// 调用原始的 eglSwapBuffers 函数
return real_eglSwapBuffers(dpy, surface);
}
总结
RenderDoc 通过注入代码继续上面的内容,RenderDoc 通过注入代码到目标应用程序的进程中,拦截和记录图形 API 调用,并在适当的时机捕获帧缓冲区的内容。以下是更详细的步骤和实现细节:
4. 保存捕获数据
RenderDoc 会将所有记录的图形命令和捕获的帧缓冲区数据保存到一个捕获文件中。这个文件通常是一个压缩的二进制文件,包含了重现整个渲染过程所需的所有信息。捕获文件包括:
- 图形命令序列:所有的绘制命令、状态设置、资源绑定等。
- 资源数据:纹理、缓冲区、着色器等资源的内容。
- 帧缓冲区数据:捕获的帧缓冲区图像数据。
5. 分析和重现
用户可以在 RenderDoc 的图形界面中打开捕获文件,查看和分析捕获的帧。RenderDoc 提供了详细的图形调用序列、资源视图、着色器调试等功能,帮助用户理解和优化渲染过程。
分析界面
RenderDoc 的分析界面提供了以下功能:
- 调用序列视图:显示所有的图形 API 调用及其参数。
- 资源视图:显示所有的资源(如纹理、缓冲区、着色器等)及其内容。
- 帧缓冲区视图:显示捕获的帧缓冲区图像。
- 着色器调试:允许用户逐步调试着色器代码,查看变量值和执行路径。
实现细节
以下是一些更详细的实现细节,帮助你更好地理解 RenderDoc 在 Android 平台上是如何工作的:
1. 动态链接库注入
RenderDoc 使用动态链接库注入技术,将自己的代码注入到目标应用程序的进程中。在 Android 平台上,这通常是通过 LD_PRELOAD
环境变量来实现的。RenderDoc 会在应用程序启动时加载自己的 SO 文件,并替换原始的图形 API 函数。
2. 拦截和记录图形 API 调用
RenderDoc 的注入代码会拦截所有的图形 API 调用,并将这些调用及其参数记录下来。以下是一个简单的示例,展示了如何拦截 OpenGL ES 函数:
// 原始的 glDrawArrays 函数指针
typedef void (*PFNGLDRAWARRAYS)(GLenum mode, GLint first, GLsizei count);
PFNGLDRAWARRAYS real_glDrawArrays = NULL;
// 拦截的 glDrawArrays 函数
void my_glDrawArrays(GLenum mode, GLint first, GLsizei count) {
// 记录调用
RenderDoc_RecordDrawArrays(mode, first, count);
// 调用原始的 glDrawArrays 函数
real_glDrawArrays(mode, first, count);
}
// 初始化拦截
void InitializeInterception() {
// 获取原始的 glDrawArrays 函数指针
real_glDrawArrays = (PFNGLDRAWARRAYS)dlsym(RTLD_NEXT, "glDrawArrays");
// 替换 glDrawArrays 函数
// 这通常是通过修改函数指针表(如 GOT 表)来实现的
ReplaceFunctionPointer("glDrawArrays", my_glDrawArrays);
}
3. 捕获帧缓冲区
RenderDoc 会在适当的时机捕获当前帧缓冲区的内容。这通常是在交换缓冲区(如 eglSwapBuffers
)或提交命令缓冲区(如 vkQueueSubmit
)时进行的。以下是一个简单的示例,展示了如何捕获 OpenGL ES 帧缓冲区:
// 拦截的 eglSwapBuffers 函数
EGLBoolean my_eglSwapBuffers(EGLDisplay dpy, EGLSurface surface) {
// 捕获帧缓冲区
RenderDoc_CaptureFrameBuffer(dpy, surface);
// 调用原始的 eglSwapBuffers 函数
return real_eglSwapBuffers(dpy, surface);
}
总结
RenderDoc 通过注入代码到目标应用程序的进程中,拦截和记录图形 API 调用,并在适当的时机捕获帧缓冲区的