SwapBuffers的等待,虚伪的FPS(转)

本文详细探讨了OpenGL中SwapBuffers函数在实时渲染中的作用,以及如何通过调整垂直同步(Vsync)来优化FPS计算。通过实例分析,揭示了在不同屏幕刷新率下,SwapBuffers函数如何影响渲染效率和最终计算出的帧率,从而提供了一种更准确的FPS计算方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 FPS在实时渲染中扮演着一个重要的角色,也许你会去笑一个不懂FPS是什么的游戏新手,但也许,这只是五十步笑一百步罢了。你能读懂SwapBuffers的深情等待吗?——ZwqXin.com


        frames per second(FPS, 帧率),作为渲染效率的一种衡量,反映的是整个程序在当前的一个渲染状态下平均每秒所能容纳的“渲染循环”执行次数,也表征了平均每个“渲染循环”(帧) 所用的时间。就表面来看,很多人觉得只要在一个每帧运行一次的函数内设置一个计数器,这样计算出一秒内的记数,便可作为该1s瞬间的FPS了;同样也很容 易能求出平均FPS。


       我一直都是这么做的。但是我一直以来有个难解的地方:为什么我的程序的FPS基本不会大于75呢?恩,以前做的DEMO,如果是有计算FPS的,都是这 样:场景复杂的时候它可以降低到1或者直接0掉,但是对于简单的场景它永远只有72左右的FPS;再简单,也是72左右;再再简单,还是72左右……可以 说是“封顶”了。今天我(终于?)想要弄清楚是什么原因了,有以下可能:1.所用的程序框架的某处给它限FPS了;2.初始化过程中沾染了些什么会约束渲 染效率的东西;3.渲染循环体内有一个“每个程序都会用到的函数”是“每个程序的瓶颈”,4.FPS计算的算法有问题;5.其他(啥?)。


       于是我把整个渲染流程核查一次,先把消息处理和循环部分弄成一般WIN32的形式,貌似无关……然后又检查像素格式,直接把最佳像素格式设置为10,(某 些程序DEMO中设计者会轮巡所有像素格式找出最佳匹配的,于是我也容易知道对我最“友好”的像素格式是10号……)貌似也没啥。另外,检查FPS计算的 式子,没发现异常。我又把渲染函数RenderGLScene内的东西一个个注释……才发现,只有当我注释了SwapBuffers时,屏幕显示死掉了, 但FPS上到了3000以上……对比72的FPS我还是比较相信这个——所以,是双缓冲的关系么。在像素设置中改像素描述器的 PFD_DOUBLEBUFFER为PFD_SWAP_COPY、PFD_SWAP_EXCHANGE之类的,这下FPS也是很高,但是屏幕猛闪~ - - 。google吧,步小心发现了一个东西:vsync。诶?甘熟噶?


      在学Irrlicht引擎的时候,建立DEVICE的时候有一个参数选项:vsync,vertical syncronisation,API解释得不清楚,就知道跟屏幕有关……屏幕?屏幕刷新率?我突然想起我做图像处理的WIN32程序时的一个窘 况:release出来的程序在人家面前演示,发现图片/视频出不来,后来明白是屏幕刷新率的问题——屏幕刷新率太高会把WIN32循环对屏幕窗口所做的 东西“消灭”掉。那么,这里呢?好,我去看文章了:vertical syncronisation。


      垂直同步。原来,程序每一帧的函数全部执行完之后,还要等屏幕刷新了才去执行下一帧啊!一查屏幕刷新率——难怪是72左右呀,正因为偶电脑上设置的屏幕刷 新率是72!恩,这里头肯定有个像sleep那样有等待功能的函数做帮凶——噢,亲爱的SwapBuffers,是你呀~


       曾经有人说SwapBuffers比glut库的glutSwapBuffers效率低不少(见这里),但这里不是这个问题。无论是 SwapBuffers还是glutSwapBuffers还是wglSwapBuffers,它们位于渲染体的末端,都有这么个“功能”:当显卡的垂直 同步功能vsyn被置为TRUE时,每一帧渲染完后来到SwapBuffers,都要悲情地等待下一次屏幕刷新的时刻到来,再交换前后缓冲并开始执行下一 帧……上面那篇文章用到了WGL_EXT_swap_control扩展,利用wglSwapIntervalEXT来设置“等待的间隔”。间隔是指一个 屏幕刷新的周期,譬如屏幕刷新率是72Hz,那么一个周期就是(1/72)s, wglSwapIntervalEXT(1)表明要让SwapBuffers类函数开始执行后要等到下一个屏幕刷新时才返回——然后继续下一帧的执行。 wglSwapIntervalEXT(2)就是等下个再下个屏幕刷新了。这么说,wglSwapIntervalEXT(0),哈,就是不用等——关闭 垂直同步Vsync。


      这里安插一个问题,那么如果程序完成一帧本来就可能大于这个屏幕刷新间隔(1/72s)呢?譬如设开始时刻屏幕刚刷新而某帧也同时开始,均设为0,那么程 序的函数集将在第一次刷新之后才完结,进入SwapBuffers。如果没理解错,按照该扩展的spec[EXT_swap_control]所说的话, 之后这个SwapBuffers要一直等待到第二次刷新才返回并交换前后缓冲,开始第二帧——即相当于一帧的“理论的完结”需要两个刷新周期(2/72s = 1/36s),就是说一帧用时稍微比屏幕刷新周期长一点点,也会导致FPS减小一半……而实际上这种不连续性在我以前的“低帧率”程序中没有体现过。究竟 是怎样的呢?留个疑问。


      在我的程序中应用此拓展,在程序初始化阶段就关闭Vsync。哈,很高的FPS。文章vertical syncronisation末尾也提供了该作者写的helper类,很容易理解和应用。好了,这样……等待的结束?还没呢——虚伪的FPS。


      是的,这个FPS很虚伪。不是说它太高了(因为我本身对这种大数值的没概念),而是它本身就不是FPS,它不符合我们所想的FPS概念(本文开始处)。更确切点,以上所说的“帧”的概念与FPS中的帧的概念不一样。


       我们调用函数控制GPU做事,并不是CPU上这种步步执行-返回-下句的形式。我们是按顺序把函数一个一个(作为地址)输送到显卡某个缓存区内,积累一定 量后再让显卡按此顺序执行。所以,代码的执行完成并不代表显卡上相应的硬件功能执行完成。(其实要是显卡看一个执行一个,流水线不就坏掉了么。)也就这个 原因,可以认为两者是不同步的,有延误等等。那么之前讨论中提及的“帧”就是前者的完成时间,而我们最想要的应该是后者的完成时间——包括CPU向GPU 传递函数所用时间和GPU上实际执行函数功能的用时,可以想象两者应该有很大部分的用时是互相重合的,且后者无论如何还是会在函数全部传递完成后仍然需要 额外时间来完成。这部分额外时间取决于之前传入的是哪类函数、GPU具体执行的是什么功能——如果直接关掉垂直同步Vsync得出FPS,这个FPS就没 有包含这些额外的处理时间,仅仅最多能表示两个U之间数据传输的速度。

       所以要让FPS反映真实的每帧——包括数据传输和实际执行、绘图——的用时的话,不能用此法。在glut 教學 - 計算 frame rate 的正確方法 —— 程序设计俱乐部一文中,作者ma_hty(白老鼠(Gary))提供了一种方法,在计算帧率时,用glFinish代替SwapBuffers。它没有 SwapBuffers那种等待的耐心,强制告诉程序和GPU:要结束了,快把剩余的所有函数指令执行完,然后换下一帧。这样的一帧正是“帧”的正确含 义。但是这样将失去双缓冲,屏幕是不会正常显示绘制的东西的,所以不可能用做实时的FPS计算,只用于某时刻用一下,指示当前的真实FPS。

 

       转载自ZwqXin http://www.zwqxin.com/

       原文地址:http://www.zwqxin.com/archives/opengl/swapbuffers-fps-vsync.html

转载于:https://www.cnblogs.com/icmzn/p/5034515.html

#!/usr/bin/env python3 # -*- coding: gbk -*- # 声明GBK编码 import subprocess import re import time import csv from datetime import datetime class DeviceMonitor: def __init__(self, interval=5, output_file="device_performance.csv"): self.interval = interval # 监控间隔() self.output_file = output_file self.header_written = False def check_adb_connection(self): """验证ADB设备连接状态""" try: result = subprocess.run( "adb devices", shell=True, capture_output=True, text=True, encoding='gbk' ) if "device" not in result.stdout: print("设备未连接或未授权") return False return True except Exception as e: print(f"ADB连接检查失败: {str(e)}") return False def get_foreground_package(self): """获取当前前台应用包名""" try: # 方法1: 使用dumpsys window命令 result = subprocess.run( "adb shell dumpsys window windows", shell=True, capture_output=True, text=True, encoding='gbk', errors='ignore' ) output = result.stdout # 尝试多种匹配模式 patterns = [ r'mCurrentFocus=Window\{.*?\s([a-zA-Z0-9._]+)/', r'mFocusedApp=AppWindowToken\{.*?\s([a-zA-Z0-9._]+)/', r'ResumedActivity: ActivityRecord\{.*?\s([a-zA-Z0-9._]+)/' ] for pattern in patterns: match = re.search(pattern, output) if match: return match.group(1) # 方法2: 使用dumpsys activity命令(备用) result = subprocess.run( "adb shell dumpsys activity activities", shell=True, capture_output=True, text=True, encoding='gbk', errors='ignore' ) match = re.search(r'ResumedActivity: ActivityRecord\{.*?\s([a-zA-Z0-9._]+)/', result.stdout) return match.group(1) if match else "unknown" except Exception as e: print(f"获取前台应用失败: {str(e)}") return "unknown" def get_cpu_usage(self): """获取CPU使用率(%)""" try: # 获取总CPU时间和空闲时间 result = subprocess.run( "adb shell top -n 1 -d 0.5 | grep {match.group(1)}", shell=True, capture_output=True, text=True ) cpu_stats = result.stdout.split()[1:] total_time = sum(map(int, cpu_stats)) idle_time = int(cpu_stats[3]) # 计算使用率 usage = 100 * (1 - idle_time / total_time) return round(usage, 1) except: return 0.0 def get_memory_usage(self): """获取内存使用情况(MB)""" try: result = subprocess.run( "adb shell cat /proc/meminfo", shell=True, capture_output=True, text=True ) meminfo = result.stdout.splitlines() total = int(meminfo[0].split()[1]) // 1024 free = int(meminfo[1].split()[1]) // 1024 used = total - free return total, used, free except: return 0, 0, 0 def get_battery_info(self): """获取电池信息和温度()""" try: result = subprocess.run( "adb shell dumpsys battery", shell=True, capture_output=True, text=True ) output = result.stdout level = re.search(r'level: (\d+)', output) status = re.search(r'status: (\d+)', output) temp = re.search(r'temperature: (\d+)', output) return ( int(level.group(1)) if level else 0, int(status.group(1)) if status else 0, int(temp.group(1)) / 10.0 if temp else 0.0 # 换为摄氏度 ) except: return 0, 0, 0.0 def get_cpu_temperature(self): """获取CPU温度() - 需要root权限""" try: result = subprocess.run( "adb shell cat /sys/class/thermal/thermal_zone0/temp", shell=True, capture_output=True, text=True ) return float(result.stdout.strip()) / 1000.0 except: return 0.0 def monitor_performance(self): """主监控循环""" if not self.check_adb_connection(): print("设备连接失败,请检查ADB连接") return print(f"开始设备监控,间隔: {self.interval}秒") print("按Ctrl+C停止监控...") try: while True: timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") package = self.get_foreground_package() cpu_usage = self.get_cpu_usage() mem_total, mem_used, mem_free = self.get_memory_usage() batt_level, batt_status, batt_temp = self.get_battery_info() cpu_temp = self.get_cpu_temperature() # 输出到控制台 print(f"[{timestamp}] 应用: {package[:20]:<20} | " f"CPU: {cpu_usage}% | " f"内存: {mem_used}/{mem_total}MB | " f"电池: {batt_level}% | " f"温度: CPU:{cpu_temp:.1f}℃ 电池:{batt_temp:.1f}℃") # 保存到CSV文件 self.save_to_csv(timestamp, package, cpu_usage, mem_total, mem_used, mem_free, batt_level, batt_status, batt_temp, cpu_temp) time.sleep(self.interval) except KeyboardInterrupt: print("\n监控已停止") def save_to_csv(self, timestamp, package, cpu_usage, mem_total, mem_used, mem_free, batt_level, batt_status, batt_temp, cpu_temp): """保存数据到CSV文件""" data = [ timestamp, package, cpu_usage, mem_total, mem_used, mem_free, batt_level, batt_status, batt_temp, cpu_temp ] with open(self.output_file, 'a', newline='', encoding='utf-8') as f: writer = csv.writer(f) if not self.header_written: writer.writerow([ '时间戳', '前台应用', 'CPU使用率(%)', '内存总量(MB)', '已用内存(MB)', '空闲内存(MB)', '电池电量(%)', '电池状态', '电池温度()', 'CPU温度()' ]) self.header_written = True writer.writerow(data) if __name__ == "__main__": # 创建监控实例并启动(每10秒监控一次) monitor = DeviceMonitor(interval=10) monitor.monitor_performance() 将此脚本加个应用刷新率和帧率
06-21
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值