game_debug.cpp: 将ProfileGraph的尺寸初始化为相对较大的值
今天的讨论主要围绕性能分析器(Profiler)以及如何改进它的可用性展开。当前性能分析器已经能够正常工作,但我们希望通过一些改进,使其更易于使用,特别是在调试和分析过程中。
首先,我们希望能够暂停性能分析器的更新,以便可以查看特定的分析数据,而不会被每一帧的更新所打扰。这是因为当前性能分析器会每帧更新,导致在某些情况下,信息的呈现可能会变得不连贯。因此,我们需要一个暂停功能,让我们能够在某一时刻停下来,查看特定的数据,而不被不断变化的内容影响。
其次,我们希望能够引入更多的视图功能。目前,性能分析器缺少一个重要的视图------它无法显示某一帧在特定例程中花费的总时间。我们已经能够展示线程利用率视图,但没有做任何时间聚合操作,也就是没有统计各个例程的执行时间并显示出来。因此,增加这种聚合视图将使我们能更全面地了解程序的性能表现。
为了改善当前的状态,首先需要对初始化界面做一些改进。目前,界面的宽度默认被初始化为零,这显然是不合理的。我们希望修改这一点,将界面的宽度设置为合理的初始值,并使其在大多数情况下能够充满整个屏幕。
在实现这一改动时,具体的做法是通过检查某些条件(比如图形的维度是否为零)来确定是否是第一次初始化图形。如果是第一次初始化,我们就调整图形的尺寸,使其更大,确保能够完整地展示性能数据。
此外,还提到了一些小的代码改进,比如将重复的代码提取到一个单独的函数中,这样可以避免每次都手动输入相同的代码,从而提高代码的可维护性和简洁性。
总结来说,今天的主要任务是通过增加暂停功能、引入聚合视图以及调整界面初始化的尺寸,使得性能分析器变得更加易用和高效。这些改进将帮助更好地分析和理解程序的性能瓶颈,从而优化游戏的运行效果。


运行游戏,查看更为实用的默认尺寸并为今天的工作做准备
目前,已经对性能分析器进行了一些改进,目的是使其更易于使用。当打开性能分析器时,现在默认的窗口大小已经更加合理,能够更好地展示数据。这是相较于之前默认窗口过小的改进,至少现在打开时,界面看起来更加合适,使用起来更方便。
然而,仍然有一些功能尚未完善。首先,分析器界面目前缺少一些基本的按钮和控件。希望能够加入一些按钮,用来切换视图或调整显示样式等,这样可以提升分析器的交互性和灵活性。此外,目前分析器虽然支持深入分析某些数据(即"钻取"功能),但仍然缺乏一个返回按钮,无法返回上一层视图,这也会影响使用体验。
另外,提到的一项改进建议是能够查看每一帧的更多信息,比如每一帧的堆栈数据。这种"堆栈每帧"功能能够帮助更好地理解每帧的执行情况,并且通过展示"时间条"这种形式的图标,可以让用户直观地看到不同操作在时间上的分布。这样的视图能够更清晰地呈现出程序执行的每一部分,尤其是对于调试性能瓶颈非常有帮助。
总的来说,当前的改进已经让性能分析器的基本功能更加实用,但为了更好地满足需求,还需要加入更多的交互功能和视图模式,比如增加切换视图的按钮、添加返回功能以及展示更详细的每帧数据等。
game_debug.cpp: 引入DrawFrameBars,绘制多个帧的堆叠条形图
我们正在尝试实现一种新的分析视图形式,称为"帧条形图(Frame Bars)",这个视图的目标是展示多帧数据,而不是仅仅关注单一帧的性能数据。
当前的实现通过水平条形图来表示每一帧的执行数据,但新的想法是通过垂直条形图来展示多个帧的执行数据。具体来说,我们将为每个事件绘制多个帧的数据,而不仅仅是选择最新的一帧数据。为了实现这一点,我们考虑为每个事件提供多个帧的历史数据,并通过垂直的条形图逐帧展示每个事件在时间上的变化。
为了完成这个目标,我们设计了一个新的draw_frame_bars
函数。这个函数与现有的绘制函数类似,但有几个关键区别:
-
多帧支持:我们不再局限于显示最新的一帧数据,而是支持选择并显示多个帧的数据。这意味着,我们需要遍历多个帧的事件数据并将其绘制出来。
-
每个事件的多个数据:在绘制时,我们将遍历所有的存储事件,并将这些事件的数据以条形图的形式展示出来。每个条形图的高度代表帧的某一部分数据,宽度则表示每一帧的时间跨度。
-
条形图绘制方式的变化:我们从水平条形图改为垂直条形图,这样可以更好地展示多个帧的数据。每个条形图的宽度和高度将根据帧数和时间跨度来确定。
-
线程数据处理:对于每个事件,我们只考虑与其相关的线程数据,并确保每个线程的数据都位于同一条"显示轨道"上。如果某些事件涉及多个线程,我们只会选择显示一个线程的数据显示。这个过程中,我们将跳过无关的线程数据。
-
绘制方式的调整 :我们使用
root
节点来获取事件的基本信息,并计算每个事件的显示跨度。然后,我们根据事件的时间跨度来确定条形图的显示位置和大小。通过这种方式,每一帧的多个数据都能被正确地绘制出来,确保每一帧的执行信息都有清晰的可视化展示。
目前的目标是通过这种方式,能够让开发者在分析器中看到多个帧的事件数据,并且为每一帧的执行情况提供更详细的视图。这项改进还没有完全实现,但它展示了如何通过新的方式来展示和分析每一帧的数据。





game_debug.cpp: 调用DrawFrameBars而不是DrawProfileIn
我们在实现"帧条形图"的过程中遇到了一些问题并进行了一些调整和反思。
首先,在一开始的实现中,我们将根节点设为了帧的根节点,但这个根节点实际包含了多个线程的数据。这导致在绘制过程中,不同线程的数据可能会互相重叠绘制在同一个区域上。也就是说,虽然这些数据可以被绘制出来,但由于它们重叠在一起,实际效果并不好,显得混乱、不清晰。
初步运行时,绘制的元素数量似乎异常庞大,画面被填满了大量图形,这显然不是预期的行为。根据现有逻辑,在遍历事件数据时,是通过从每帧的根事件开始,进入它的第一个子事件,然后通过"兄弟指针"遍历每一个同级的子事件,这样一来,在多帧数据的循环中,实际上是绘制了过多重复或不必要的图形。
出现这个问题的可能原因包括:
-
没有限制绘制帧的数量:当前代码没有明确指定绘制多少帧,导致可能遍历了所有可用的帧,最终绘制出数量庞大的条形图。
-
线程混合:因为根节点包含多个线程的数据,所以不同线程的数据没有被分离,导致条形图重叠。
-
缺少帧索引控制:我们还没有添加帧索引相关的逻辑,例如指定绘制从哪一帧开始、绘制多少帧等,因此目前的绘制范围完全依赖事件链的数量,而不是明确的帧控制。
接下来需要进行的调整包括:
- 添加明确的帧索引控制逻辑,例如:起始帧索引、最大绘制帧数等,从而限制绘制的数据范围,避免绘制过多内容。
- 筛选出特定线程的数据,仅绘制与目标线程相关的事件,避免线程混合导致的重叠问题。
- 检查事件遍历逻辑,确保在多帧数据绘制时不会重复绘制或漏掉任何事件,同时避免无效事件导致图形杂乱。
总之,这一部分的工作核心在于从当前的多线程混合与无控制绘制,逐步过渡到清晰、有组织、可控的逐帧多线程绘图模式。通过对绘图范围、线程隔离、数据遍历路径的规范化,我们将能够构建一个更具实用性和清晰度的性能分析视图。




看上去宽高搞反了

game_debug.cpp: 限制FrameIndex
我们正在构建一个逐帧分析的可视化系统,为了确保条形图的正确性和结构清晰,需要做几个关键调整:
-
确保根事件存在:绘图之前需要验证每一帧是否存在有效的根事件。因为根事件是构建分析图表的基础,它代表了每一帧的入口节点。如果没有它,后续的数据结构遍历和绘制将无法进行。
-
记录帧索引信息:在绘图过程中,我们需要保留每一帧的帧索引。这样做的目的是在调试时能够明确知道当前绘制的是哪一帧的数据,同时也便于在多帧数据中进行定位、回退、或跳转等操作。
-
限制绘图数量:为避免一次性绘制大量帧而导致界面混乱或性能下降,当前设置为只绘制一帧,作为调试阶段的简化处理方式。之后可以根据需要扩展为多帧绘制。
-
统一事件位置:事件的索引、账户信息以及路由访问路径都要统一放置在相同的绘图位置区域中,这样有助于分析每一帧内具体事件在时序上的相对位置与调用结构。
整体目标是构建一个结构清晰、绘制精简的帧分析视图,使得在每帧中发生了哪些函数调用、花了多少时间等信息一目了然,并能够快速进行调试和问题定位。当前阶段以最小绘制单位(一帧)进行测试,为后续扩展到多帧堆叠、线程分层等打下基础。



game_debug.cpp: 将FrameCount增加到10
我们正在尝试将绘制的帧数量从 1 增加到 10,以便测试多帧数据显示的可行性,但出现了一些问题,绘制结果不符合预期。
原本希望看到的是同一结构的重复条形图(每帧一个),但现在的情况却是图形变得"有深度",显示成了嵌套的调用关系图,这不是想要的效果。
经过分析,出现问题的原因包括以下几个方面:
问题一:传递的事件类型不正确
目前传入 draw_frame_bars
的事件并不合适,传入的是某个根节点,而不是一个具有可遍历帧序列的事件数据。
实际上,我们需要的是某种"最早帧"的起始事件,也就是说,要找到当前所能访问的最旧的帧数据作为起点,这样才能通过其 next
指针遍历多个连续帧的数据。
问题二:数据结构设计缺陷
事件的组织方式导致我们无法轻松访问多帧数据。理想情况是:
- 每一帧本身就是一个事件(或者帧事件),
- 每一帧的事件都通过
next
指针链接成链表, - 这样就能很自然地遍历多个帧。
但当前的实现方式中,每一帧的根节点是"合成"的,并未直接链接起来,导致不能通过 next
指针方便地进行多帧遍历,从而使得绘制多帧条形图的逻辑变得繁琐甚至不可行。
当前解决尝试
当前尝试通过手动找到"最旧的一帧"并提取其根节点信息,再以此为基础绘制接下来的多帧数据。但是,由于根节点之间缺乏链式结构,因此这种方式本质上还是依赖了一些"hack"方法,代码显得不够健壮、清晰。
下一步建议与优化方向
- 优化数据结构设计,使每一帧具备事件身份且能够通过链表方式连接;
- 在分析系统中引入一个统一的帧容器结构,便于统一访问每帧数据;
- 更新绘制逻辑,确保传入的对象能够直接进行帧间遍历;
- 进一步检查当前事件系统中"synthetic nodes"的设计,看是否需要重新构造以支持多帧视图。
总体来说,我们已经非常接近多帧条形图的目标,只差对事件结构的重构与适配,接下来的工作将围绕这方面展开。

点击进去

为了方便测试多加一些函数进去






game_debug.cpp: 将RootNode设置为OldestEvent
我们当前正在进一步优化多帧绘制逻辑,目的是让时间轴上的每一帧都能被准确地以垂直条形图的形式展示,清晰显示每个函数或任务在每一帧所消耗的时间,形成一种随时间推移的可视化分析视图。
当前处理思路
- 在代码中获取了当前正在查看的可视元素(viewing element);
- 然后不再进行复杂的搜索,而是直接将"最早的事件"设为绘图的根节点;
- 使用这个最早事件作为起点,从而遍历多个连续帧来绘制数据。
当前结果
- 现在,当选择了一个具体的事件(如 debug collation)之后,可以看到它在后续每一帧中所占用的时间;
- 每帧的条形图绘制成功地排列在水平方向,每条垂直柱状代表一帧中该事件的持续时间;
- 如果继续绘制直到空间不足(剩余空间为 0),图形将向左滚动,展现后续帧。
优点
- 能够直观比较某一操作在不同帧之间的性能波动;
- 实现了从单帧分析向时间序列分析的转变,数据的可视化维度更强;
- 避免了原先因事件未正确串联而无法跨帧遍历的问题。
存在问题
- 当前"正确"只是相对的:虽然绘图看起来有效,但还存在潜在逻辑漏洞;
- 条形图位置和比例仍可能存在不精确或边界问题;
- 滚动逻辑尚未完全实现,仅在空间耗尽时表现为"自动移动";
- 某些帧中事件信息可能丢失或未对齐,需要进一步调试。
下一步优化方向
- 确保事件遍历逻辑严谨,杜绝非法帧或无效事件参与绘图;
- 优化坐标系逻辑,确保条形图尺寸与帧时间一一对应;
- 增加帧时间轴标尺或帧编号提示,提升可读性;
- 进一步完善帧间滚动和缩放机制,实现可交互的历史性能回放视图。
通过这一步改进,我们已成功迈出了将性能分析工具从静态走向动态、从单点走向时间线的关键一步。接下来将继续在结构优化与交互细节方面打磨,使多帧分析功能更加实用与直观。
game_debug.cpp: 将FrameCount增加到128
我们现在进一步拓展条形图的帧数展示范围,目标是一次性查看更多帧的数据变化趋势,以便更好地观察性能波动和行为模式。
主要调整内容
- 将条形图数量设置为 128,也就是一次性展示最近的 128 帧;
- 修改绘图参数,使其能处理更多帧数据的渲染;
- 每一帧绘制为一条垂直条形图,用于展示某个特定事件在该帧中消耗的时间;
- 所有条形图按时间顺序排列,从左至右,构成时间轴上的柱状分布。
预期效果
- 在画面上同时呈现 128 个连续帧的性能数据;
- 可以直观地看到某个事件随时间的耗时变化,例如某个阶段耗时突然增加;
- 提供历史性能趋势回顾,有助于发现突发性能瓶颈或优化点;
- 条形图对齐整齐,形成清晰的数据对比画面;
- 随着后续帧的加入,画面会逐渐填满,最终达到最大显示帧数,形成完整的视觉分析窗口。
当前实现状态
- 调整后能够渲染出指定数量的条形图;
- 每个条形图代表一帧数据,颜色和高度分别对应事件的种类与耗时;
- 时间轴完整拉伸,覆盖了设定的 128 帧宽度;
- 若绘图区空间足够,将完整显示;若不足,后续可加入滑动或压缩机制。
后续优化方向
- 增加帧编号或时间刻度轴,提升数据可读性;
- 实现缩放功能,使用户能在不同粒度上观察数据(例如:16、32、64、128帧切换);
- 增加交互功能,如鼠标悬停显示每帧的详细耗时数据;
- 自动居中或跟随最新帧更新视图,使数据浏览更加顺畅;
- 添加筛选功能,让用户选择特定线程或事件查看帧数据。
通过此次扩展,我们已经将视图从"当前帧"拓展到了"多帧历史",为分析者提供了更强大的工具,帮助快速定位性能趋势、稳定性问题或潜在瓶颈。未来结合交互与可缩放机制,这一视图将成为关键的分析入口之一。

运行游戏,查看更精细的时间数据
我们现在进一步思考如何更有效地支持多帧条形图的渲染,从而实现更细粒度的性能可视化。当前的实现虽然已经初步可以展示多个帧的数据,但存在一些结构性的问题,尤其是在事件链表与存储方式方面。我们希望改进整体架构,以提升渲染效率、数据访问便捷性和结构清晰度。
当前问题
- 目前的事件是通过链表串联的,逻辑上比较复杂;
- 多帧事件的遍历依赖动态链式结构,在绘制条形图时不够高效;
- 每个帧的数据事件不能快速定位,难以在帧维度上进行精准的数据提取;
- 多次出现的事件在同一帧中没有明确组织,导致无法高效索引和绘制。
优化设想
1. 分配固定大小的后备存储块
- 为每个调试元素预留一个固定大小的事件数组(例如 128 帧或 256 帧);
- 避免动态链表构建和销毁,改为直接覆盖式环形缓存;
- 固定空间有助于维护内存一致性与访问效率。
2. 引入帧索引表(Frame Index Table)
- 为每个调试元素维护一个帧索引表,对应最近若干帧(例如 128 帧);
- 每一项指向该调试元素在该帧发生的首个事件;
- 通过这个表,可以快速根据帧编号定位到该帧的事件列表;
- 保留事件之间的链式结构,用于同帧多个事件的串联。
3. 快速帧数据访问与绘制
- 绘制条形图时,根据帧索引表快速查找到对应帧的事件;
- 对事件链进行迭代,收集当前帧所有发生的事件;
- 渲染时使用固定宽度和纵向布局,实现连续帧条形图可视化;
- 这样可以快速横向滑动查看历史帧趋势,同时避免复杂的嵌套结构遍历。
4. 支持更强的帧间对比能力
- 由于结构扁平化,每帧绘制逻辑简洁明了;
- 能够轻松对比多个帧中相同事件的耗时变化;
- 可后续添加交互(如悬停信息、点击详情等)进行深入分析。
结构总结
- 每个调试元素对应一个事件环形缓存 + 一个帧索引表;
- 每个帧索引表项指向该帧第一个事件,后续事件通过
next
指针链表访问; - 内存结构清晰:固定容量、常数访问时间、易于调试与扩展;
- 渲染逻辑简化,仅遍历帧索引表与对应事件链。
通过这种方式,我们不仅提升了性能数据的展示效果,也大大简化了底层数据结构,提高了渲染效率和维护性。今后可以在此基础上进一步支持线程切换、过滤筛选、时间缩放等高级调试功能。
game_debug.h: 移除debug_frame_region和MAX_REGIONS_PER_FRAME
我们在思考如何更合理地传递和管理存储事件(storied event)时,开始重新梳理条形图绘制逻辑。在绘制帧条形图的过程中,我们目前遍历的是每一帧对应的事件集,而接下来希望实现更系统的方式来组织和遍历这些帧数据。
当前结构分析
- 当前的调试元素(debug element)中维护了事件的头尾指针(head 和 tail),这些事件链表包含了该元素从过去到现在所有发生过的事件;
- 由于这些事件是永久累积的,在可视化时会导致数据庞大、不具时效性、不易管理;
- 此外,原本框架中还保留了一些
frame
的结构,但部分内容混乱或冗余,如MAX_REGIONS_PER_FRAME
等字段已经不再有实际用途,需要清理。
计划优化方向
1. 限定帧的数量进行管理
- 将调试元素中维护的事件集合限制在固定帧范围内(如最近的 128 帧),采用环形结构;
- 每个帧在存储中拥有独立的数据区域,便于迭代与访问;
- 移除历史无限累积的事件链,改为定量维护。
2. 使用调试元素中的帧滑动访问
- 在绘制帧条形图时,不再从所有历史事件中筛选;
- 直接按顺序访问调试元素所持有的帧集合,遍历每帧中存储的事件集合进行绘制;
- 可通过一个循环变量模拟"滑动窗口",在固定长度缓存中向前或向后滑动以获取数据。
3. 清理无效数据结构
- 移除不再使用的旧
frames
结构,例如废弃的MAX_REGIONS_PER_FRAME
字段; - 精简框架内数据定义,确保每一项结构都与实际渲染逻辑紧密对应。
渲染行为预期
- 渲染时,从当前帧开始向后(或向前)依次绘制已记录的若干帧;
- 每帧对应一组条形图,显示该帧内调试元素事件的耗时情况;
- 当空间不足时自动横向滚动,形成流畅的帧时间线视觉反馈;
- 后续可引入交互方式,如拖动、缩放、帧跳转等,增强分析能力。
通过这一调整,我们能够实现更高效、清晰且具可控性的帧数据可视化,不再依赖杂乱的事件链表,也便于未来支持更大规模的调试分析。同时框架数据结构将更简洁、更聚焦于实际可视化需求。

game_debug.h: 引入debug_element_frame,以便我们能够跟踪和计时事件
我们正在重构帧与调试元素之间的事件访问机制,目标是实现更直接、快速的数据定位与分析,避免冗余的树结构遍历,提高帧数据可视化与调试效率。
目标与问题概述
我们面临的主要问题是:当前帧(frame)结构本身无法直接提供我们希望访问的事件信息,必须通过遍历根节点构建的分析树才能定位特定调试元素的事件数据,这既低效又复杂。我们真正需要的,是:
- 拿到一个帧索引(frame index)
- 拿到一个调试元素(debug element)
- 直接定位该元素在该帧上的所有事件
这才是理想的访问模式。
数据结构改进方案
1. 引入 DebugElementFrame 结构
设计一种新的结构 DebugElementFrame
,作为调试元素在单帧上的事件集合,具备以下特性:
- 存储该元素在该帧上发生的所有事件;
- 累计耗时统计(例如总 clock 数),便于分析每帧的时间消耗;
- 可以用于后续的排序或筛选,例如找出耗时最多的元素。
每个调试元素将维护一个长度固定的数组(如 128 或 256)的 DebugElementFrame
实例:
c
DebugElementFrame frames[MAX_FRAME_BACKLOG];
2. 使用固定长度循环数组管理帧数据
为了避免复杂的链表和动态内存操作,采用环形缓冲区结构:
- 总帧数固定(如 128);
- 有一个全局的总帧索引
totalFrameIndex
; - 每个调试元素内部用帧序号
frameOrdinal = totalFrameIndex % MAX_FRAME_BACKLOG
来定位在数组中的具体帧; - 不再维护
next
、free
等指针链表结构,直接访问数组即可。
这种方式能在空间可控的前提下保持极高的访问效率,并省去所有内存释放与管理的开销。
访问逻辑与绘制优化
在绘制帧图(Frame Bars)时:
- 使用当前调试元素;
- 根据帧索引直接访问
DebugElementFrame
; - 获取其事件集合与耗时;
- 渲染条形图,无需再从根节点遍历整个事件树;
- 实现帧之间滑动查看(如向前或向后 128 帧的历史数据)。
额外优势
- 便于调试与数据对比:在帧之间轻松对比同一元素的耗时波动;
- 可以轻松添加排序功能:例如显示当前帧耗时排名前十的调试元素;
- 数据结构更紧凑,维护与扩展更加方便;
- 移除了复杂的 free list 和 event 链表逻辑,清晰简洁。
后续注意点
- 总帧索引
totalFrameIndex
是不断递增的,而frameOrdinal
是它在缓冲区中的映射(会循环); - 所有调试相关的帧数据必须基于这一循环数组维护;
- 若帧数超过缓存长度,则最旧帧的数据将被覆盖(这是设计预期);
- 初始渲染时要处理好未填满环形缓冲区的情况。
通过这一方案,我们可建立一个高效、清晰且便于维护的帧事件访问系统,完全摆脱树状遍历带来的性能与逻辑复杂度,使调试系统更接近理想状态。
game_debug.h: 引入FrameOrdinal和MostRecentFrameOrdinal,并使game_debug.cpp中的函数使用它们
我们正在对调试系统中的帧数据结构和访问方式进行进一步清理和重构,以提高效率、简化逻辑,并为后续的可视化交互(如时间轴回溯)提供良好基础。
核心改动与设计意图
-
用 frame ordinal 替代 frame count 逻辑
我们不再使用传统的帧计数方式,而是引入了一个"帧序号"(frame ordinal),它表示的是在环形缓冲区中的下一个可用帧的位置。这个值从 0 开始,随着每一帧的推进递增,并在达到最大帧缓冲数量(如 128 或 256)时循环。
- 当前帧插入位置为
nextFreeFrame
- 最近绘制的帧为
nextFreeFrame - 1
(循环处理) - 如果要回看之前的帧,向后偏移相应的 ordinal 即可
- 当前帧插入位置为
-
将绘图逻辑基于 frame ordinal 重写
在进行调试元素绘制时:
- 引入
frameOrdinal
参数,明确要绘制哪一帧; - 在绘图函数中不再从整个事件链查找,而是根据指定 ordinal,直接访问存储于调试元素内的事件帧数组;
- 简化了
debug_draw_elements
等函数的输入,改为通过debugState
获取当前帧序号; - 添加了
MostRecentFrameOrdinal
统一用于查找"上一个完成的帧",避免重复逻辑和混淆。
- 引入
-
绘图交互支持帧回溯与时间轴滑动
- 随着结构清晰化,可在 UI 上添加"滑动条"或"时间轴"控件,支持用户浏览过去帧的数据;
- 通过
frameOrdinal
参数,轻松实现任意帧的数据展示; - 未来可以添加帧耗时排序、事件高亮等功能。
-
事件释放机制精简化
- 过去事件的释放逻辑需要遍历链表、管理 free list;
- 现在结构为定长数组 + ordinal,释放帧时只需清空当前帧对应的数组槽位;
free list
、next
等逻辑完全移除,减少代码复杂度;- 每次只释放当前
nextFreeFrame
指向的那一帧下的所有调试元素事件。
整体结构优势
- 定长数组 + 循环索引:访问高效、逻辑清晰、不需要动态内存管理;
- 可预测的内存布局:方便调试、维护及未来优化;
- 支持任意帧访问:调试工具可以快速访问某帧中某调试元素的所有事件;
- 完全移除动态链式管理逻辑:减少错误源,提升性能与可维护性;
- 基础设施为后续 UI 扩展打下良好基础:滑动条、帧统计、历史比较等功能变得易于实现。
总结
通过引入 frameOrdinal
、定长帧数组、移除 free list,我们重构了调试系统中帧事件存储与访问机制,极大地简化了代码结构,提高了访问效率,并为后续的可视化和分析功能打下了坚实的结构基础。后续只需围绕这一结构继续扩展即可快速获得更强的调试与性能分析能力。

game_debug.cpp: 修改FreeFrame和FreeOldestFrame,并重构NewFrame为InitFrame
我们进一步完善了调试系统中的帧管理机制,目标是构建一个高效、简洁、易于维护的事件收集与回溯结构。以下是关键设计与实现细节的总结:
核心目标
- 构建一个基于环形缓冲区的帧结构,每帧作为事件容器;
- 通过精简旧帧的释放逻辑,彻底移除自由链表(free list)和相关复杂性;
- 明确三类帧的概念:当前收集帧(Collation Frame) 、最旧帧(Oldest Frame) 、最新完成帧(Most Recent Frame);
- 在内存不足时,自动释放旧帧以腾出空间;
- 保证永远不释放当前正在写入的帧(Collation Frame)。
帧释放机制重构
清除旧帧事件逻辑
释放某一帧的逻辑被简化为:
- 遍历该帧中所有调试元素;
- 将与该帧序号相关的事件全部清除;
- 不再需要维护任何链式结构或事件自由列表;
- 调试元素中原有事件头指针等结构全部移除;
- 所有清理动作均基于帧序号进行索引操作。
释放帧指针前移
- 通过
nextFreeFrame
指针控制当前可覆盖的帧槽位; - 每次释放只需前移该指针;
- 同时更新
oldestFrameOrdinal
,表示当前缓冲区中最旧有效帧; - 若释放操作移除了最新帧(即
oldestFrameOrdinal == mostRecentFrameOrdinal
),则需同步推进mostRecentFrameOrdinal
; - 所有序号变动均采用环形模运算包装,保持在固定长度的帧数组内循环。
新帧生成与替换逻辑
- 不再"创建新帧",而是复用并覆盖旧帧槽位;
- 原函数
NewFrame()
重命名为EmitFrame()
更准确; - 每次
EmitFrame
仅更新当前收集帧所在的帧槽位内容; - 替换前,需释放当前
nextFreeFrame
所指帧,避免内存泄露; - 然后将当前收集帧写入该槽位,并更新帧指针。
存储逻辑中的内存保护
- 写入事件前,必须检查当前帧是否有可用空间;
- 若内存不足,在写入前可调用帧释放函数
FreeOldestFrame()
; - 如果
collationFrameOrdinal == oldestFrameOrdinal
,则表示无法释放,已达到最小帧容量下限,此时为系统级错误; - 否则可安全释放并继续写入;
- 该逻辑为未来自动内存管理提供了基础,甚至可以设置自适应回收策略。
帧序号管理结构(初始值)
名称 | 含义 | 初始值 |
---|---|---|
collationFrameOrdinal |
当前收集帧的序号 | 1 |
oldestFrameOrdinal |
最早有效帧序号 | 0 |
mostRecentFrameOrdinal |
最近完成写入的帧 | 0 |
所有三个变量通过模运算(wrap)保持在 frameCount
范围内循环。
系统优势与后续扩展
- 极简事件清理:不再需要链表结构,按帧清理即可。
- 帧复用机制:没有动态分配和释放,避免碎片问题。
- 内存保护机制清晰:写前检查与安全释放结合,容错能力提升。
- UI 支持增强:可以轻松支持滑动条、时间轴、事件时间回放。
- 逻辑层清晰分明:帧状态的三个角色(收集、最新、最旧)定义明确,操作分离,状态易管理。
通过这些改动,我们完成了从链表式事件管理向环形帧缓冲系统的过渡。新系统性能更高,逻辑更清晰,也为未来调试工具扩展与自动回收机制打下了坚实的基础。


game_debug.cpp: 引入IncrementFrameOrdinal和GetCollationFrame
我们进一步调整了帧系统的序号管理和访问方式,目的是优化帧的引用逻辑,使其更加清晰、高效,同时为后续可能的调整留下灵活空间。以下是本轮工作的详细总结:
帧序号(Frame Ordinal)递增机制
- 每当产生一帧时,我们都会递增帧序号(frame ordinal),保持其单调递增;
- 无论帧是否被覆盖、是否存入数组,我们都确保有一个逻辑上严格递增的帧标识;
- 该帧标识不直接等同于帧缓冲数组中的索引,而是通过模运算映射到对应槽位;
- 实际用于访问的数组下标由
frameOrdinal % kDebugFrameCount
得到; - 这种方式将"帧在历史时间轴上的位置"与"帧在缓冲区中的位置"解耦。
最终帧状态变量及其含义
我们维护了几个关键变量用于标识不同帧的状态:
变量名 | 含义 | 说明 |
---|---|---|
CollationFrameOrdinal |
当前收集中的帧 | 正在写入的新事件所在的帧 |
mostRecentFrameOrdinal |
最近完成的帧 | 最后一个写入完成,可供读取和绘制的帧 |
oldestFrameOrdinal |
缓冲区中最旧的有效帧 | 当前仍被保留的最早帧,释放时从这里开始 |
frameOrdinal |
全局帧编号计数器 | 每次产生帧时递增,仅用于记录时间轴位置 |
访问帧数据的两种策略(设计考虑)
我们目前讨论了两种获取当前帧指针的方法:
-
保持当前设计:
- 使用
frameOrdinal % kDebugFrameCount
每次动态求出缓冲区索引; - 逻辑清晰,简单易维护;
- 开销略微增加但可忽略。
- 使用
-
添加映射指针缓存:
- 每次递增帧序号时,同时维护一个指向对应帧数据的指针变量;
- 快速访问当前帧的指针,无需重复计算模;
- 稍微增加状态复杂度,但可提升访问效率。
目前我们选择保留现有方式(动态模运算),但保留后续优化可能性,后续如频繁访问帧数据,则可再考虑引入帧指针缓存。
一致性与完整性保障
为避免帧状态混乱,添加了以下保护措施:
- 每次递增帧序号时,始终同步更新
CollationFrameOrdinal
; mostRecentFrameOrdinal
总是小于CollationFrameOrdinal
;- 不允许释放
CollationFrameOrdinal
所指帧,一旦检测到即视为严重错误(代表缓冲区容量太小); - 所有帧操作必须使用标准化入口,以防状态不一致。
后续考虑
- 可为调试用途增加接口:将帧序号直接映射为可视化滑动条的下标;
- 若内存极度紧张,可增加"自动帧回收"逻辑,根据占用大小动态释放旧帧;
- 在多线程写入时可将
CollationFrameOrdinal
隔离成每线程变量,但目前先按单线程处理。
通过本轮优化,我们确保帧序号管理逻辑完整、自洽,并为进一步扩展帧索引模式或帧数据结构留足空间。整个系统保持了高可读性和可维护性,后续可灵活调整而不会引入混乱。

game_debug.cpp: 解决编译错误
我们对调试帧系统进行了深入的优化和重构,主要围绕帧的序号管理、帧轮转、帧初始化及清理流程进行了调整,以下是详细的总结:
帧轮转与初始化逻辑优化
- 利用了当前正在收集事件的帧序号(collation frame ordinal),可以直接定位当前正在操作的帧;
- 通过
debugElementFrame = elementFrames + currentCollationOrdinal
获取对应帧位置,无需频繁地访问全局状态结构; - 每次帧推进后,执行
initFrame()
对新帧进行初始化,确保其状态干净,准备接收新的事件数据; - 优化了旧逻辑中对 debugState 的频繁引用,统一在局部缓存变量中处理,提升可读性和效率。
帧序号及状态标记逻辑更新
- 明确使用三大标记值来跟踪调试帧状态:
标记名 | 含义 | 说明 |
---|---|---|
oldestFrameOrdinal |
当前仍然有效的最早帧序号 | 如果新帧即将覆盖此帧,需先释放它 |
mostRecentFrameOrdinal |
刚刚完成写入的帧 | 最新可用于展示或分析的帧 |
collationFrameOrdinal |
当前正在被写入的帧 | 每次事件写入都指向此帧 |
-
每次新帧产生时:
collationFrameOrdinal
向前推进;- 将上一帧的序号写入
mostRecentFrameOrdinal
; - 若新序号覆盖到
oldestFrameOrdinal
,则需先释放最旧帧并推进该序号。
-
所有帧的访问均基于
frameOrdinal % frameCount
映射到实际缓冲区位置,形成循环使用的帧缓冲池。
清理逻辑与冗余剔除
- 清除了项目初始化时对所有帧"清零"的代码段,因实际过程中每帧都会在使用前初始化,无需统一清空;
- 删除了不再必要的条件语句,比如旧逻辑中的 if 判断初始化是否成功或是否需要重置;
- 整合了多个地方冗余的帧访问方式,统一使用
debugState.frames + ordinal
进行引用,逻辑更清晰; - 某些调试打印逻辑被移除或精简,改为直接从
mostRecentFrameOrdinal
读取目标帧指针,提升效率。
统一帧推进和释放流程
在帧推进时执行以下步骤顺序:
-
记录当前帧为"最近帧":
mostRecentFrameOrdinal = collationFrameOrdinal
-
推进当前帧到下一个槽位:
collationFrameOrdinal++
-
检查是否与最旧帧冲突:
- 若
collationFrameOrdinal == oldestFrameOrdinal
,表示缓冲池已满,必须释放旧帧; - 执行释放逻辑并推进
oldestFrameOrdinal
。
- 若
-
初始化新帧:
- 执行
initFrame()
对新帧进行清理和初始化,准备写入新数据。
- 执行
这样保证缓冲区始终以循环方式管理帧内存,不会内存泄漏或访问脏数据。
总帧数统计及简化
- 将
frameIndex
简化为一个totalFrameCount
,单纯作为产生的帧数量统计; - 删除了原本多余的帧索引变量,防止混淆;
- 调试接口只需根据
mostRecentFrameOrdinal
和环形缓冲结构,快速定位当前可查看帧。
小结与后续思路
-
整体帧结构管理已从"静态索引"过渡为"序号驱动+循环缓冲"方式;
-
移除了大量不再适用的初始化和访问逻辑,代码结构大幅简化;
-
当前流程对内存和性能都更加友好;
-
后续可根据内存压力,扩展自动帧释放策略或支持更大的环形缓冲区。
这个重构极大地提升了帧系统的健壮性和可维护性,同时也为后续功能(如快照回溯、事件回放)打下了清晰稳定的基础。




FrameIndex = 511 GUI 是空
写代码打断点试试


继续看DebugState->Frames[1] 怎么初始化的

RootProfileNode 好像有问题

运行游戏,查看它是否(几乎)正常运行
我们完成了帧管理系统的大部分重构和逻辑梳理后,虽然心里清楚肯定还有一些细节被遗漏,预期第一次运行时大概率不会完全成功,但决定先尝试运行一遍看看效果。
初次运行结果
- 出乎意料的是,系统基本上成功运行了,初步看来逻辑并没有立即崩溃;
- 初始输出没有明显错误,也没有立刻崩掉,说明大部分关键路径已经被正确梳理;
- 对某些调试数据进行打印和验证,确实"看起来像是起作用了"。
验证边界情况:帧大小上限
- 为了进一步验证逻辑的健壮性,尝试故意触碰帧缓冲区的上限,即让帧序号在环形缓冲区中"打满",验证是否能正确回收和覆盖最旧帧;
- 如预期一样,在这一操作下系统出现了崩溃或错误,说明虽然基本路径已完成,但在处理帧上限或内存回收边界时还存在漏洞;
- 此次崩溃是预料之中的,因此也说明测试逻辑正常,并验证了我们还需处理更细致的帧覆盖逻辑或内存释放流程。
当前状态总结
- 当前阶段主要目标是将基本帧推进、初始化、访问和回收机制打通;
- 主路径逻辑基本可以运行;
- 对于边界条件,如缓冲区满、帧回收、长时间运行的行为等,仍需进一步完善处理机制;
- 系统整体已经从"结构重构"阶段进入"边界调试与修复"阶段。
我们下一步的重点将是识别并修复在帧缓冲区满时可能出现的崩溃问题,以及确保所有帧管理行为都严格遵守我们的循环缓冲设计原则。
调试器:在FreeFrame处断点,检查序列号
我们在帧管理机制中遇到了新的问题,并对帧释放逻辑进行了深入排查和调试。以下是详细的总结:
当前状态下帧序号的错误行为
- 最新帧(most recent frame)被错误地设置为252;
- 合并帧(collation frame)却从1到7;
- 最旧帧(oldest frame)同样为252;
- 这种状态明显不合理,帧序号重叠而逻辑不对,导致崩溃或错误行为。
初步调试流程和验证
-
设置断点观察帧释放逻辑:
- 程序跑到帧255后,再尝试合并帧0,释放最旧帧;
- 进入
free_frame
时,检查帧0上是否挂有事件; - 发现帧0是空的,说明这是预期行为(因为合并是从帧1开始的);
- 再进一步验证
free_frame
中的释放路径是否触发,结果确实没有触发释放动作; - 初步得出结论:空帧不需要释放,逻辑暂时正常。
-
接着观察帧推进过程:
- 每次帧推进时,会执行释放最旧帧、合并帧推进;
- 当前逻辑看起来是合理的;
- 释放时
get_free_event
被调用,成功释放出分配的内存块; - 验证释放动作确实发生了。
发现新的潜在内存泄漏问题
- 虽然上述释放流程正常运作,但仍有内存未被释放的问题;
- 观察现象:帧不断推进,内存使用未见明显下降,推断是某些资源未被正确释放;
- 猜测问题可能发生在某些帧分配的对象未被纳入释放流程中。
怀疑释放路径遗漏了某些数据
- 目前的释放逻辑只处理了 event 分配数据;
- 质疑某些数据结构为何采用了指针形式,并没有内嵌在帧结构中;
- 如果这些数据(如某些临时存储结构)是通过
malloc/new
分配的,那释放逻辑中必须包含相应的free/delete
调用; - 当前
free_frame
中似乎缺乏对这些结构的释放,导致内存泄漏。
下一步修复方向
-
排查所有在帧生命周期中动态分配的对象:
- 包括事件、临时缓存、调试信息等;
- 明确其是否在
free_frame
中被正确处理。
-
统一内存管理结构:
- 若某些数据是指针形式,应确认是否需要转为嵌套结构;
- 或者在帧释放时显式调用其析构逻辑。
-
增加帧释放后的内存验证逻辑:
- 如:统计每帧分配/释放总量,进行对比;
- 防止静默内存泄漏持续存在。
总结
目前帧系统的主循环、推进机制基本构建完成,但在内存回收路径上仍存在遗漏。某些动态分配的数据未被释放,尤其可能是未纳入帧事件列表中的结构。接下来要集中精力修复内存泄漏路径,确保每帧推进时确实回收上一帧的所有资源。
game_debug.cpp: 有条件地在FreeFrame中执行FREELIST_DEALLOCATE
我们在执行 free frame 操作时,会查看 framework 编号,然后依次处理元素帧、标签帧等,设置 frame 等于当前状态,再将其加入 frames 中,同时处理其序号。
如果 frame 的 profile 编号存在,我们就会尝试分配内存(RAM),但在某些情况下内存增长被阻止了。当前内存使用情况仍未处理完毕,因此想确认这些内存到底来自哪里。但这个来源并没有明确指向,因为时间已经超出很久了,处理已经延迟十分钟左右。
我们现在的问题是,如果当前机制并没有释放所有内容,那么到底有哪些部分没有被释放?这个是我们要明确的关键点。
我们检查了 debug 树,发现这些是正常的,不在回收流程中;deep views 同样不是回收流程的一部分。还有一些 debug threads,理论上它们应该是单独被正确处理的,这些也属于 debug arena 而不是 periphery arena。
我们可以确认每个 frame 只使用一个 per frame arena。因此,我们真正需要关注的是 periphery arena 中到底产生了什么内容。
进一步分析后发现,在整个流程中,只有一个地方会从 arena 中取出内容,那就是 store event。如果这是唯一一个提取操作的地方,那就意味着我们在存储事件时,有些事件在后续的 free event 遍历过程中根本没有被回收。这就是造成内存未释放的根本原因。
因此,当前的关键问题在于事件的存储和释放流程之间存在断层,部分事件被保留在内存中但未能被成功释放。
game_debug.cpp: 仔细检查FreeFrame并对*ElementFrame进行ZeroStruct处理
我们在这一部分明显处理错误了。
在遍历哈希表并尝试提取准备释放的 frame 时,用来恢复所有事件的方法已经不再准确,不确定具体原因,但可以肯定当前方式无法有效找回所有事件。
free event 操作中,我们会先找出 frame 中最早的事件,然后将该事件之后的事件设为新的最早事件,并释放掉当前正在处理的事件。按照这个逻辑,所有该 frame 中的事件都应该被依次释放出去。这个过程会对哈希表中所有的条目进行处理。
问题在于,哈希表中到底还包含了什么内容?这一点值得进一步调查。
此外,frame 处理完之后,理论上应该执行一次清理,比如将其置零或者提取清除(zero extract wipe),目前这一部分似乎未进行。但即便如此,照理来说这不应该影响事件释放的主要流程。
综合来看,事件释放操作的逻辑在设计上看似完整,实际上存在未被释放或未被正确遍历的部分,特别是在哈希表的遍历和事件链表更新中可能出现遗漏。这可能导致部分事件一直残留在内存中,未被释放。需要重点检查 frame 中事件的链表结构更新是否有遗漏,以及哈希表在处理后是否确实被清理干净。

运行游戏并再次确认我们正在泄漏内存
当前的问题可能需要等到明天再继续深入调查,目前还没能立即找到明确的解决办法,但初步现象已经比较明显:内存确实发生了泄漏。
原本事件在使用完毕后是能够被正常释放的,后来某次修改中破坏了这一机制,导致现在事件不能再在处理完后被释放,而是直接被泄漏掉了。
内存使用趋势本应是下降的,但现在却一直持续增长,说明释放机制失效了。最终,在内存达到一定程度后,系统就会出现阻塞或者卡住的情况。
表现为,当尝试分配下一个事件所需的内存时,系统会反复尝试释放旧事件以腾出空间。然而,它"认为"已经释放了所有可以释放的事件,因此会陷入死循环,在循环缓冲区中反复查找,却永远找不到可以释放的内容。
也就是说,当前的状态是:系统错误地认为已经完成了所有的释放操作,但实际上事件仍旧残留在内存中未被释放,导致下一次事件存储时无法分配新内存,从而陷入卡死状态。
这个逻辑问题非常棘手,因为它混淆了系统的释放状态与真实的内存使用情况,造成释放路径被打断,而内存不断积压、无法回收。后续需要重点排查释放逻辑是否完整执行,以及事件状态是否被正确标记和更新。
问答环节
是否需要将+FrameOrdinal包装到FreeFrame函数中?
在处理 + FrameOrdinal
时,是否需要进行封装(wrap)的问题需要明确。
当前存在两个相关位置,一个在这里,另一个也在相应位置。提到"是否需要 wrap",但从逻辑上看,这里并不需要进行取模(mod)运算或手动封装操作。
原因在于,所有传入的 ordinal 都已经是预先处理过的,范围已经被正确限制在允许的区间内,因此不会超出预期范围。换句话说,在调用 free
函数时传入的 ordinal 已经是封装后的值,不存在越界风险。
也就是说,free 函数接收到的 ordinal 是一个已经 wrap 过的值,所以在内部处理时不需要再次进行 wrap 或 mod 操作。整个系统设计中,ordinal 的生命周期中已经保证了它始终处于有效范围内,这避免了重复封装的必要性。关键在于保持 ordinal 的有效性在它被传递前就已经保证。
game_debug.cpp: 断言FrameOrdinal小于DEBUG_FRAME_COUNT
我们可以在这里加入一个断言(assert)来确保安全性,确认传入的 ordinal 没有超出 frame 的有效范围。实际上我们可能确实应该这么做,比如在尝试释放事件时加一句提示:如果有人试图释放一个不在 frame 范围内的内容,那么应该立即触发异常。虽然这种情况理论上不应该发生,因为没人会传入一个超出范围的值,但加上断言可以提高代码的健壮性,防止潜在的问题悄无声息地扩散。
此外,还有一个地方让人感觉有些奇怪,可能是哪里遗漏了什么非常基础但细节上的东西。虽然逻辑上看似合理,但总感觉忘记了某个关键点。
尤其是在 store event
的处理上,这部分逻辑显得有些混乱。通过相关的 correlation framework tunnel,我们是知道在一个 frame 上存储了多少个事件的,这意味着我们应当能明确掌握每个 frame 中事件的数量和状态。
问题可能就隐藏在这种"我们以为知道"的假设中,某处微小的不一致可能造成了事件未被释放,或者事件数量与实际情况不符,导致后续释放操作失效,进而引发内存泄漏或资源积压。
接下来应该重点回顾 store event
的实际行为是否真的与我们预期完全一致,特别是事件数量更新与 frame 状态同步的部分,可能隐藏着逻辑遗漏或者边界处理错误。
game_debug.cpp: 追踪FreedEventCount
还有一个有趣的点是,可以通过断言机制进行校验,从而捕捉问题。
具体来说,可以引入一个 freed_event_count
变量,每次有事件被释放到 free list 时,就对这个计数进行累加。这样在处理完一个 frame 之后,就可以对比该 frame 的 stored_event_count
和 freed_event_count
是否相等。
如果两者相等,就能确认这个 frame 中的事件全部被成功释放;否则说明存在泄漏或释放遗漏的情况。这个对比可以通过 assert 实现,用来在调试阶段直接捕捉问题。
不过,为了让这个校验成立,还需要确保所有释放路径都参与计数,所有存储路径也正确统计事件数量。这意味着除了释放操作要累计 freed_event_count
,在存储事件时也要准确维护 stored_event_count
,确保两者始终同步。
这个方法虽然简单,但能够有效验证释放逻辑是否和存储逻辑严格对应,是定位内存泄漏或 frame 内事件残留问题的有效手段。关键是要保证每次操作的完整性,并对每个 frame 的事件生命周期进行精准跟踪。
运行游戏,崩溃并发现我们释放了比存储的事件更多的事件
情况开始变得混乱。
一开始看上去是还没有进入 physics frames,所以事件数量还没对上。但即便如此,数量依旧对不上,这就更让人无法理解了。
统计显示存储了 56 个事件,却只释放了 50 个,这种不对称意味着有事件在某处被遗漏了释放。
而且这段逻辑中只有一个地方可以生成这样的事件,因此理论上事件数量应该是完全对得上的。如果只有一个入口,那就不可能出现多余的事件。问题在于事实却不符。
开始怀疑这些事件是如何被存储的。查看了 store events
的调用路径,发现确实是在 frame 上进行事件存储操作的。
进一步追查具体存储的对象------element,到底是什么类型或结构?这个 element 被用来存储事件,但它的来源可能影响整个流程。
疑点集中在以下几个方面:
- frame 上的事件数量和释放数量不一致;
- 事件的唯一生成路径理论上应保证数量可控,但实际上却存在偏差;
- element 的具体身份可能隐藏了某些间接存储路径,导致事件数量意外增加;
- profile node 的分配位置也值得关注,可能间接产生了事件或造成未释放的残留。
下一步需要仔细检查 element 是如何构成的,它是否间接导致了事件的创建或者存储行为;同时也要排查是否有其他隐藏路径绕过了释放流程。这个偏差可能来源于某些未显式标记或未明确注册的事件实例。
game_debug.cpp: 移除一个FREELIST_DEALLOCATE
事件实际上是被包装在一个 element 中的,这样做的目的是为了简化处理流程。
但关键在于,这个事件并不一定需要被真正存储下来。也就是说,虽然看起来事件被"包装"了,但并没有明确指明它必须作为一个正式的存储操作被注册到 frame 中。
因此,某些事件虽然经过包装处理,但并没有进入实际的存储流程,这种情况下就不应该计入 stored_event_count,也不需要参与释放计数。
换句话说,这部分逻辑之前的理解存在偏差,误以为所有包装过的事件都会被存储,实际上只有明确被标记存储的事件才会计入统计。由于这一点没有区分清楚,导致事件数量统计出现异常,看起来像是"少释放"了,实际上可能只是多算了没有真正存储的事件。
这种设计虽然灵活,但也容易混淆,需要明确哪些包装操作只是中间处理,哪些才是真正的存储行为,避免将不必要的内容纳入释放逻辑,导致事件计数不一致。接下来要重点理清包装操作和存储操作之间的边界,确保事件生命周期的判断准确。
重新构建并运行,发现错误已解决
重新构建并运行之后,现在可以通过断言检测事件数量是否不一致。如果断言没有触发,那是最理想的情况,意味着存储和释放数量完全匹配。
接着又发生了一次不一致的情况。追踪发现问题是由于事件被重复释放了,也就是说事件被第二次放入 free list 中,这显然是错误操作,会破坏整个 free list 的结构。
虽然这解释了当前为什么出错,但令人不解的是,在引入重复释放之前,系统就已经表现出异常了。这就非常奇怪,理论上那时候还没有做出破坏性的修改,不应该出问题。
最终确认,其实最初并没有 bug,错误是在试图修复"假设中的 bug"时人为引入的。也就是说,原本的系统逻辑是正常的,是在修改过程中误伤了已有的正确流程。
总结起来,这次的问题是对系统行为的误判导致了多余的干预,从而引发了真正的错误。教训在于,在未能充分确认问题根源之前贸然修复,可能会带来新的、更严重的问题。需要更加谨慎地验证现象与根因之间的关系,避免"修 bug 反而造 bug"的情况发生。
我有点落后于进度,但你通常是否会跳过一些简化的内容(例如像第39集那样绘制位图的内容),直接进入渲染器?另外还有结构化资源之类的内容?
目前在进度上稍有落后,不过在某些方面有时也会有意选择绕过某些过程,比如像奥地利式编码那种方式。
很多简化的流程,例如渲染位图之类的内容,和最终目标关系不大时,会选择跳过,比如之前处理 drub bitmap 或 observatory night 那样的场景,直接进入最终渲染逻辑处理。同样地,对于像结构化资源(structured assets)这类系统性内容,如果已经清楚最终目标,通常也不会再绕远路。
换句话说,如果已经知道最终不会使用位图渲染,就没必要花时间去实现一个纯粹的软件渲染器。如果目标最终是 GPU 渲染,那就会直接从 GPU 着手,不会再在软件渲染上投入太多精力。
当然这并不是说从来没有做过软件渲染,早期确实写过,那时 PC 上还没有 GPU,早期写的几个渲染器就是纯粹的软件渲染器。正因如此,才对 GPU 渲染原理理解得更透彻。通过写软件渲染器,能真正理解图形管线是如何工作的;否则,直接跳到 GPU API,反而会忽略底层的图形处理逻辑。
不过如果已经掌握了软件渲染器的实现原理,就不再需要每次都从头写一个软件渲染器,除非有特别充分的理由,否则通常不会再重复做这件事。
至于调试系统(debug system),这部分确实是边做边探索。并没有一开始就确定好结构,而是通过不断尝试、逐步摸索出适合当前需求的架构方式。
设计一个复杂系统本身就是实验性的过程。如果是全新的系统,显然不可能一开始就完全知道如何构建,必须通过尝试逐步完善。尤其是在这次的调试系统中,尝试实现一些以往没做过的新功能,在探索过程中也发现了一些有趣的点。
这也是一种学习方式------展示如何通过试验构建复杂系统。这种设计方式在构建全新功能时依然适用,即便经验再丰富,也无法跳过探索过程。
至于 profile 的重命名,可以留到明天再做,那部分已经规划好,也将会是一个良好的补充。整体进展是积极的,很多尝试都具有探索和教育意义。
对于使用静态数组而不是列表有何想法?
之所以选择使用静态数组而不是链表,是因为考虑到大多数需要实现的功能更偏向于"随机访问"某个特定帧的操作。每次都遍历链表去定位某一帧在效率上并不理想。
很多目标操作都与帧相关,例如:
- 跳转并绘制某一特定帧;
- 绘制当前帧以及前一帧;
- 同时绘制前五帧;
- 或者绘制所有实体的第 4 帧。
这些操作如果使用链表结构,就需要反复遍历查找,而用静态数组则能通过索引直接访问,大大简化逻辑和提高效率。
虽然不能完全排除链表也可以实现这些功能,但可以预见,当这种帧间跳转和并行访问需求频繁出现时,链表结构会变得笨重且难以维护。使用静态数组则能让这类操作更直观、更高效,也更容易组织和扩展。
正是基于这种对系统使用方式的预判,才最终决定采用静态数组结构来管理帧数据。这个选择是为了更好地支持后续的操作便利性和性能表现。
"继承和封装是人类最好的发明"。讨论一下
有观点称"继承"和"封装"是人类最伟大的发明之一,这种说法简直令人震惊,几乎可以说是彻底偏离了理性。简直就像是在"失控"一样。
这样的评价实在太过夸张,把面向对象编程的一些特性神化到这种程度,完全不符合实际经验。现实中,这些机制虽然有其用途,但也经常成为系统设计混乱、难以维护的根源。
盲目推崇继承和封装,往往会导致架构臃肿、代码层级混乱、依赖难以理清,反而让维护成本成倍增加。很多情况下,继承滥用会让代码更难以理解,封装过度也可能掩盖真正的问题。要真正写出高质量、可维护的系统,关键在于合理设计、明确结构,而不是依赖某种抽象语法机制本身。
所以,听到这种话时只能感叹------实在是太离谱了。
你喜欢使用模运算还是掩码进行循环索引?如果操作数是2的幂减1,编译器是否会将模运算替换为按位与?
在循环索引数组时,对于使用取模运算(modulo)还是按位与(masking)的问题,通常的观点是:
大多数情况下直接使用取模运算是没问题的,特别是在调试器、日志系统等执行频率较低的场景中。此时数组大小不必一定是2的幂,这样更灵活,开发更方便。
但在对性能有更高要求的场景中,例如对速度非常敏感的系统,就可能更倾向于使用按位与操作,因为取模在底层是整数除法,性能上相对较慢,而按位与则可以更快地完成相同的操作,前提是数组大小是2的幂。
编译器在优化方面通常也足够智能,当取模操作的除数是2的幂时,大多数现代编译器会自动将其替换为更高效的按位与操作。不过,虽然理论上编译器会做这样的优化,但并没有专门验证过是否在所有情况下都如此。
因此,在特别关注性能的场合,倾向于直接手动使用按位与操作来确保效率。而在常规场合下,直接使用取模运算通常是可以接受的,不需要对这类细节过于"看管"或担心编译器是否优化得当。
我们还会做马拉松直播来应对周一的问答疯狂吗?
我们现在是否还在进行那种"马拉松式"的连续工作,具体情况还不确定。我们也不确定是否会继续以那种疯狂的节奏推进。
不过,我们也保留那样做的可能性。如果有需要,或者在某些情况下确实合适,我们可能还是会继续采用那种高强度的工作方式。虽然现在不一定会那么做,但也不能完全排除那种可能性。
你几周前运行的性能分析代码怎么样了?你之前实现的多个通道功能似乎消失了
为了实现理想的版本(例如发布 Arisia 版本),之前做过的一些功能在过程中出现了问题,其中有些模块甚至"崩塌"了,比如多通道支持。不过多通道功能目前仍然保留着。
其实仍然可以展示那部分功能,尽管一开始说打算明天再进行重命名和整理,但现在仍然可以临时启用它。
以这个最早的测试用例为例,就是当初在处理多通道渲染时使用的第一个测试对象。从这个例子可以看到,多通道功能从早期就存在并一直保留至今,只是有些部分可能还没有最终整合或发布,尚处于开发迭代过程中。整体来看,虽然有些模块经历过调整甚至暂时失效,但功能思路并没有被完全放弃,而是在逐步演化和完善中。

game_debug.cpp: 重新启用性能分析通道
现在需要将这段代码进行移植。由于现在已经有了数组结构,所以原本需要长期保留每个 frame 的逻辑现在变得简单得多。
首先,可以通过 debug_state
获取最近一次的帧编号,赋值给 most_recent_frame_ordinal
。接着,在获取 viewing_element
(假设它存在)的过程中,可以直接访问其根节点,并通过多种方式进行操作。
例如可以先从 viewing_element.frames
中,取出最旧的事件 oldest_event
,也可以从 viewing_element.frames
中,通过 most_recent_frame_ordinal
索引获取数据。
同样地,也可以直接从 debug_state.frames
中,通过 most_recent_frame_ordinal
获取某个帧信息,用于进一步的调试或显示。不过,在使用过程中发现 debug_frame
并未重载索引操作符,因此代码在当前状态下会有报错,必须先处理这个问题,才能继续使用类似数组下标的方式来访问具体帧数据。
运行游戏,查看多个通道,启用软件渲染器并崩溃
当前的性能分析模块代码中可以看到多通道视图已经实现,画面上有三条独立的通道线,但目前其他线程中没有活动数据显示。
如果希望在这些线程中看到实际内容,可以通过启用软件渲染(soft surrender)功能来实现。
不过目前的显示状态并不理想,出现了可视化混乱或显示异常的情况。可能的原因是最近进行了某些更改,尤其是和多帧视图功能(multiple frame view)有关,而这些改动可能引入了一些问题,导致当前的显示呈现出杂乱无序的状态。
另外提到的 drawrectangle
看起来像是渲染过程中出现的某种问题或对象状态异常,也可能是此次显示出错的直接原因之一。整体上看,这是调试过程中常见的情况,接下来需要针对显示混乱的来源逐步排查。
game_debug.cpp: 暂时禁用递归并运行游戏
问题的根本原因可能是因为在性能分析(drug profile)时,绘制了大量的事件。因为事件数量太多,导致显示变得过于复杂和混乱。解决办法是暂时关闭递归,这样就不会绘制所有的事件,减少渲染的负担。
在此基础上,我们现在已经能够看到多通道视图(multi-lane view)的实际运行效果。通过启用这个视图,多个通道现在都在正常运行中。
通常来说,在使用 GPU 渲染时,其他核心或 CPU 的工作量相对较少,因为大部分工作由 GPU 完成。但在软件渲染时,可以看到每个线程被多次调用,处理大量的渲染任务,这使得每个线程的工作量大幅增加。
简而言之,问题的关键在于渲染时事件的数量,导致了不必要的复杂性,关闭递归和减少绘制的事件数量是暂时的解决方法,能够帮助我们更清晰地看到渲染效果。

如果你真的没有从游戏中学到东西怎么办?我已经看了1-28集四遍,觉得自己应该懂了。我尝试做简化的win32层,想要达到独立层,但发现做不到。文档不清晰,我也忘记了哪些东西是做什么的。你会对那些觉得自己没学到东西的人说些什么?
如果发现自己在学习过程中遇到困难,可能有两种原因。首先,可能是需要更加努力地去尝试,克服困难。其次,可能是因为学习的材料对初学者来说有些过于复杂或过早。对于这样的课程,它并不是为从未编程过的人设计的,而是为那些已经掌握编程基础,但对游戏编程或游戏引擎编程不熟悉的人准备的。因此,如果你还不够熟悉编程,可能会觉得跟不上进度,因为这个课程并没有足够的入门材料来帮助完全没有编程基础的人。
在这种情况下,可以先进行自我评估,看看自己是否掌握了足够的基础知识。如果觉得自己的基础还不够扎实,可以先学习一些基础的编程材料,如初级的C语言教程或Windows编程材料,直到能够更熟练地掌握这些基本概念。然后再回过头来学习中的内容,这样会更容易理解。
总之,学习过程中遇到困难可能是因为基础知识不够或者材料对自己来说过于复杂。通过自我评估,找到问题所在,再通过补充基础知识,逐步提高编程能力,最终会顺利掌握游戏编程和游戏引擎编程的内容。
game_debug.cpp: 从DrawProfileIn切换到DrawFrameBars并查看性能分析
现在已经完全准备好,明天可以进入帧条部分了。这样就可以绘制出某个特定元素的所有帧,感觉对这一进展非常满意。