移动网站建设哪家便宜,即时通讯网站开发源码,特色化示范性软件学院,手机网站预览设定今天的工作计划
今天我们本来是打算继续开发性能分析器#xff08;Profiler#xff09;#xff0c;但在此之前#xff0c;我们认为有一些问题应该先清理一下。虽然这类事情不是我们最关心的核心内容#xff0c;但我们觉得现在是时候处理一下了#xff0c;特别是为了…设定今天的工作计划
今天我们本来是打算继续开发性能分析器Profiler但在此之前我们认为有一些问题应该先清理一下。虽然这类事情不是我们最关心的核心内容但我们觉得现在是时候处理一下了特别是为了让别人能更顺利地运行我们目前的版本。
目前存在的一些问题会让其他人尝试运行这个游戏变得困难尤其是我们为了让 OBS 在处理 OpenGL 程序时能够正常流畅地捕捉画面而做的特殊处理。这段代码本身其实是个很不合理的“黑科技”——可以说是个坏主意。
在构建脚本 build.bat 中我们设置了一个环境变量 GAME_STREAMING1。这个变量控制了在 game_opengl.cpp 文件中的特定代码路径。在那段路径中我们把 wgl 双缓冲设置为了 false即使我们明明在使用双缓冲。这种设置理论上是非法的我们甚至不知道它为何能工作——但它确实让 OBS 可以更好地捕捉画面不需要我们使用一些更复杂的技巧。
对我们来说这种做法确实让直播更轻松但问题是如果有其他人尝试构建游戏而没有意识到必须关闭这个变量或不知道有这回事那么他们的构建结果将会是完全出错的——可能是渲染混乱、崩溃或者别的奇怪错误。
因此现在需要清理这段临时代码或者至少让构建系统在默认状态下是“对其他开发者友好的”不需要他们了解直播中为了兼容 OBS 所做的特别处理。
编辑 build.bat引入并检查 GAME_STREAMING 环境变量
我们希望实现一个功能在我们的构建版本中默认开启游戏串流game streaming而在其他人的构建版本中默认关闭。为此我们考虑通过设置环境变量的方式来控制这个行为。也就是说只有在设置了特定环境变量的情况下游戏串流功能才会被启用否则它将保持默认关闭。
具体设想如下 使用环境变量控制功能开关 我们计划使用一个自定义的环境变量例如 GAME_STREAMING来判断是否启用游戏串流。只有当该变量存在并且非空时我们才会将游戏串流相关的编译选项加入到编译参数中。 批处理脚本实现逻辑 在编译脚本中如 .bat 文件我们添加逻辑判断该环境变量是否被定义 如果没有定义该变量则跳转到一个标签如 :no_streaming跳过添加编译标志的部分如果变量已定义即我们设置了则将相应的编译标志如启用游戏串流追加到通用编译参数中。 编译验证与测试 我们首先验证未设置变量时的构建结果确保不会启用游戏串流然后测试设置了环境变量的情况确保游戏串流功能被正确开启还通过手动在脚本中插入测试信息如设置不同值或输出信息来验证判断分支是否正确执行。 设置系统环境变量 我们还尝试在系统中设置该环境变量以确保每次打开构建环境时都会自动启用该功能。虽然一开始没找到正确的设置路径但我们知道环境变量设置应该在系统设置的某个位置可以配置后续可以完成这部分。 最终效果 实现后 我们的本地构建默认开启游戏串流功能其他人的构建将不会受影响仍然保持默认关闭该设置对他人完全透明他们无需做任何更改或了解这一机制编译流程仍保持简洁、自动化。
这个方案简单、可控、易于维护同时不影响他人的开发流程。
在 CMakeLists.txt 中检查一个环境变量是否被设置可以使用 CMake 的 ENV{} 语法来访问环境变量并结合 IF 语句判断。以下是具体方法和中文解释。 检查环境变量是否被设置是否非空
if(DEFINED ENV{GAME_STREAMING})message(STATUS 环境变量 GAME_STREAMING 已设置启用游戏串流功能。)add_definitions(-DENABLE_GAME_STREAMING1)
else()message(STATUS 未设置 GAME_STREAMING默认关闭游戏串流功能。)
endif()中文说明
ENV{变量名} 用于访问系统环境变量DEFINED ENV{变量名} 用于判断该环境变量是否存在message(STATUS ...) 会在 CMake 配置阶段输出提示add_definitions() 会向编译器添加宏定义例如 -DENABLE_GAME_STREAMING1你也可以使用 set() 把变量保存下来稍后再判断或使用。 如果想进一步判断变量值例如只在值为 1 时启用功能
if(DEFINED ENV{GAME_STREAMING} AND $ENV{GAME_STREAMING} STREQUAL 1)message(STATUS GAME_STREAMING1启用游戏串流。)add_definitions(-DENABLE_GAME_STREAMING1)
else()message(STATUS GAME_STREAMING 未设置为 1跳过游戏串流。)
endif()测试环境变量在命令行里设置环境变量再运行 CMake
Windows CMD:
set GAME_STREAMING1
cmake ..Unix / Linux / macOS:
export GAME_STREAMING1
cmake ..这样就可以在 CMake 中根据环境变量是否存在或其值来决定是否启用某些功能。非常适合做本地构建开关控制。 这是主播为了防止别人OSB直播看不到设置的 设置环境变量试试 重启vscode 话说现在好像不用去.clangd 配置这个宏都高亮了 这个宏cmake中定义的
看来是clangd读取了compile_commands.json里面的宏 奇怪我如果关闭WGL_DOUBLE_BUFFER_ARB 就看不到了 删除一堆已经不再使用的旧代码
以下是对内容的中文详细总结不涉及个人、开发者或作者仅以“我们”视角进行客观陈述 我们在构建流程中检查了几个模块的使用情况发现某些部分目前并未实际启用或已经不再使用因此决定进行精简和清理以简化构建逻辑并减少维护负担。
首先确认了预处理器相关的功能目前并没有实际使用因此选择将该部分逻辑禁用以防止它在构建过程中被不必要地调用避免对其他人造成困扰。与此类似“game_generated” 文件部分也未被使用因此一并处理并移除。此外“game_metadata” 的引用也被删除因为文件已经不再存在再保留相关代码会导致构建报错。
随后检查了其他可能冗余的调试代码例如 debug_dump_struct 相关逻辑。确认已经没有调用因此决定清除这些无效代码以减少项目复杂度。这样做的目的是让其他开发人员不需要再处理这部分内容降低理解和调试负担。
在 OpenGL 的部分代码中存在一些类型转换操作。了解到这些转换对 GCC 编译器存在兼容性问题因此考虑移除相关转换逻辑。部分代码中指针转换看似多余因此也一并清理。
另外提到项目中有大量加载的文件因此非常希望实现一个工具可以扫描并搜索当前项目中所有加载的文件路径。尽管开发这个工具本身非常简单目前尚未实现仅因时间紧张和事务繁忙所致。
继续清理中发现仍存在一些旧的头文件如 metadata.h 等这些文件也已经不再被需要因此决定将它们完全移除。
最后为了继续简化渲染逻辑对 bitmap 相关部分进行排查判断其位于哪个模块。回忆之后确定该部分代码可能存在于 render_group 模块内因此接下来将转向该模块继续清理和优化工作。 总的来说以上操作旨在清除无用代码、简化构建流程、提升项目整洁度并降低他人理解和编译时所面临的复杂性。 修改 game_platform.h将 PointerToU32 改名为 U32FromPointer 并相应更改调用位置避免 GCC 和 Clang 的编译警告 我们在处理纹理句柄texture handle的过程中发现由于其定义为 void* 指针类型在某些编译器如 GCC中进行类型转换时会触发不必要的强制类型转换警告。尽管显式进行了类型转换cast编译器依然认为这是不被推荐的操作表现得过于严格。
为了避免这些多余的警告同时也提升代码的整洁性和可移植性我们决定引入辅助宏或内联函数来封装这类转换操作。具体来说
定义了一个 uint32_from_pointer或类似命名的方法将 void* 类型指针安全地转换为 uint32_t也定义了反向转换 pointer_from_uint32用于从 uint32_t 转换回 void*为了更通用和可扩展可能进一步抽象成模板形式或带类型参数的宏函数例如通过参数指定目标类型并完成转换在转换过程中先将指针值提升到合适的整数宽度如 uintptr_t 或 uint64_t再进行类型转换所有这些目的是为了让编译器接受这些转换而不报错特别是在 Clang 和 GCC 中保持一致性。
除了转换函数本身我们还审查了当前平台层中的一些纹理分配或架构接口发现其中某些部分早已从特定平台模块中独立出来因此不再应当放置于 Windows 相关代码段内。于是计划将相关函数移动到更合理的通用位置以反映架构变化并简化结构。
此外我们也准备在项目中各处将原先手动类型转换的地方替换为刚刚定义的转换函数。这种做法不仅统一风格还避免开发者在不熟悉编译器行为时遇到困惑或错误信息。
整体目标是减少编译器发出的不必要警告使代码更稳定、更易维护并改善他人的开发体验。
根据clangd 的警告去掉警告 运行游戏确认一切正常运行
经过一番调整和清理之后我们的代码现在运行正常达到了预期效果。这些调整包括解决了几个简单且被遗留的问题尤其是那些能够迅速解决的小问题这使得项目变得更加整洁减少了不必要的复杂性。
虽然这些问题已经被清理掉但我们计划继续进一步检查相关的功能确认是否还有其他类似的问题存在。不过目前我们已经解决了最紧迫且最明显的问题所以我们决定先处理这些简单的任务以便尽早清除掉积压的工作。
接着我们意识到已经花费了一些时间检查了代码的执行情况并进行了优化。现在剩下大约 40 分钟的时间。为了高效利用这段时间我们计划继续进行性能分析profiling以进一步提高项目的性能表现。然而在此之前我们还在考虑是否有其他重要的任务可以一并解决。
在接下来的工作中我们会关注变量初始化失败的相关问题这是之前被提到的一项需要修复的情况。同时也有团队成员讨论了如何在 OpenGL 环境下保持 fader渐变效果的正常工作这也是一个需要进一步解决的问题。
总体来说尽管当前已经取得了一些进展但仍有其他问题需要继续优化和修复以确保项目在不同平台和环境下的稳定性和性能。
修改 win32_game.cpp移除淡入淡出效果fader
关于渐变效果fading in and out我们并不想继续支持这一功能。最初添加这个功能是因为某个团队成员想知道如何实现它但我们认为这其实是一个非常糟糕的主意。原因在于这个功能涉及到对 3D 图形卡的初始化这本身就很容易出问题而渐变效果只是给这个问题增加了更多的复杂性可能会导致更多的错误。
因此理想的做法是彻底移除渐变效果的代码。虽然我们已经展示了如何添加这个功能但如果某些开发者想使用它可以参考我们的代码并自行实现我们不会继续支持这一功能。我们认为将这个功能保留在项目中是没有意义的特别是在实际发布时这个功能可能会导致一些图形驱动程序初始化失败或出现其他问题。
所以我们决定去掉这一功能并删除与其相关的所有代码。具体来说我们会删除与渐变效果相关的 theater 代码并且去掉其中的窗口显示控制和其他不必要的部分。我们不再需要这些代码来显示窗口或处理窗口可见性的问题因此这些代码也将被移除。
我们会在游戏启动前确保窗口显示逻辑正常并在初始化完成后直接显示窗口。这样整个程序在启动时就可以顺利运行而不需要处理渐变效果带来的额外麻烦。
如果有人真的想要在自己的项目中使用渐变效果可以参考原来的实现但这并不在我们需要支持的范围内。我们决定不再将这个功能包括在我们的发布版本中因为它可能会影响到不同机器上的图形驱动导致各种潜在的问题。因此渐变效果会被完全移除任何人如果愿意可以自行处理和实现。
去掉结构体挨着修改错误就行 考虑是否要移除多线程的 OpenGL 上下文
我们目前还在思考另一件事情就是是否要撤销 OpenGL 多线程上下文的实现。从目前了解的情况来看多上下文的处理实际上并没有带来太大实际好处除非使用的是高端显卡比如专业的 NVIDIA Quadro 系列。这类显卡提供了真正的“复制引擎”copy engines使得多上下文的图像资源复制能够带来性能提升。
但在大多数消费级显卡上并没有这种优化机制存在。因此在这些普通显卡上使用多个 OpenGL 上下文实际上只是增加了系统的复杂性却没有带来任何性能上的好处。
由此可以得出一个结论我们实际上并不需要额外的上下文。更有效的方式可能是在主线程中直接准备好纹理资源。我们可以在程序初始化或资源加载阶段预设好所有纹理比如使用像 Pixel Buffer ObjectPBO这样的机制来提前准备好纹理数据然后仅在需要时调用一次 glTexImage 进行上传而不再通过多线程上下文去提交这些纹理。
采用这种方式流程将变得更加简单数据准备会更清晰纹理上传也不会被其他线程打断。同时也能避免潜在的多线程 OpenGL 同步和状态管理的问题。
目前还不确定是否要立即对现有的多上下文处理进行改动毕竟虽然这样做可能不是最有效率的但也勉强可以正常运作。可以暂时保留当前实现日后再做优化调整。
总之现在已经成功移除渐变效果fader相关功能这是一个令人满意的阶段性成果。当前正处于项目清理的阶段虽然这些工作比较琐碎无趣但对整体架构的精简和未来维护都是有益的。接下来是否进一步优化纹理上传机制视时间安排和优先级决定。
Pixel Buffer ObjectPBO 是 OpenGL 中用于异步像素数据传输的缓冲对象主要用于提高纹理上传和像素读取的性能。它是 OpenGL 的一个扩展允许在 CPU 和 GPU 之间更高效地传递图像数据。 简单理解
在不使用 PBO 的传统方式中纹理上传或读取像素数据如 glTexImage2D、glReadPixels是同步阻塞操作CPU 会等待 GPU 完成操作导致性能瓶颈。
使用 PBO 后这些操作可以变成异步的
CPU 可以把像素数据传给 PBO继续干别的事GPU 在后台处理这些数据不阻塞 CPU这就实现了CPU 和 GPU 并行工作提升了效率。 用法总结
1. 创建 PBO
GLuint pbo;
glGenBuffers(1, pbo);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
glBufferData(GL_PIXEL_UNPACK_BUFFER, size, NULL, GL_STREAM_DRAW);2. 向 PBO 写入数据
void* ptr glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
// 写入数据到 ptr 指向的内存
glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);3. 上传纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
// 这里的 0 表示从当前绑定的 PBO 中取数据4. 解绑 PBO
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);典型用途
高频纹理更新如视频流、屏幕录制、游戏串流读取像素数据时避免阻塞比如截图多缓冲技术双 PBO 交替读写 注意事项
PBO 不自动提高性能只有在正确使用异步特性时才有优势实现和效果依赖显卡和驱动对于小图像数据开销反而可能更大常与 GL_STREAM_DRAW、GL_STREAM_READ 配合使用。 如果你正在做一个需要频繁传输纹理数据的程序比如串流游戏图像PBO 是一个可以显著减少卡顿和提高帧率的重要工具。
再次运行游戏并指出热重载时分析器存在的一个 bug
我们之前注意到一个可能可以复现的 bug虽然没有人专门在 GitHub 上提到它但我们观察到在某些情况下当执行重构并触发重新加载时字符串数据似乎会丢失导致某些功能异常。比如在某些情况下profiling 数据性能分析数据会消失而这些应该是正常保留的。
我们怀疑这个问题可能出在字符串的管理或重载逻辑上。我们尝试通过修改代码、强制重建并重新加载程序的方式来观察这个 bug 是否能复现。最初的问题表现为在重新加载之后profiling 数据没有出现或者某些字符串数据在界面上丢失。
我们尝试做一些比较明显的代码更改比如直接去掉用于渲染场景中元素比如墙体、武器、怪物等的全部代码来制造比较大的差异强制触发重新加载机制。但是在实际操作中程序并没有像我们预期的那样出错反而依然运行正常profile 数据虽然没有显示但也没有直接报错。这说明可能 bug 本身未能触发或者之前的问题被不经意间修复了。
在尝试制造代码出错行为时我们进入了反汇编调试模式试图理解跳转逻辑和条件断点设置的问题。我们分析了跳转指令、断点条件以及寄存器的变化最后决定绕过特定的跳转条件通过人工覆盖跳转目标的方式来避免断言失败。我们考虑使用一些简单的填充指令例如 NOP或直接修改指令流使其“跳过”有问题的逻辑路径这样就可以继续执行程序而不会中断在不想要的地方。
总结来看
我们曾遇到字符串数据或 profile 数据在重新加载后消失的问题。试图重现这个 bug但目前似乎无法复现。怀疑问题可能与重载逻辑、字符串表、断言机制或跳转指令有关。通过手动修改反汇编逻辑试图继续程序执行跳过错误断点。目前仍在探索问题的根源不过已经有一些方向例如字符串生命周期管理或条件分支处理。
后续可能还会进一步深入调试找到数据丢失的真正触发点并分析其与热重载系统或运行时内存管理之间的关系。 调试器中手动写入 XOR 指令到内存
我们正在处理一个断言触发的问题这个断言的逻辑是用于检测在渲染实体过程中是否遗漏了某些必须处理的情况。我们修改了一部分代码逻辑导致原来的断言开始报错因为它检测到我们没有处理所有应有的实体种类。由于我们现在只是临时注释或移除了一部分逻辑断言因此失败是意料之中的。
为了解决这个问题我们没有直接修改断言的条件或者绕过调用路径而是采用了“外科手术式”的二进制补丁方式。我们进入了反汇编视图定位到了断言触发前的汇编代码段并找到了具体的地址区域。
我们分析了跳转指令和寄存器操作的具体内存布局识别出用于断言判断的 test 指令和 xor 指令所在位置。然后我们决定将这些指令覆盖成一些无害的操作以便程序可以继续执行而不中断。
我们采取的策略是用一系列 test eax, eax对应汇编为 85 C0指令填充原位置这些指令不会对程序状态造成副作用。接着我们插入一个 xor eax, eax 指令来维持寄存器状态的一致性确保后续指令仍能正常运行。
因为 x86-64 是变长指令集所以我们需要精确控制覆盖区域的字节长度确保既不截断现有指令也不留下无效的机器码从而避免运行时崩溃或未定义行为。我们确认整个补丁操作在语义上是“无害”的仅仅是跳过了不必要的断言检测。
最终结果是断言被成功绕过程序逻辑仍能正常继续执行且不会触发错误或中断。
总结
我们遇到了一个由于实体未完全处理而触发的断言。为了避免断言中断程序我们通过手动修改内存指令的方式绕过断言。使用了一系列安全的 test eax, eax 和 xor eax, eax 来填充指令空间。这样修改后程序能稳定运行便于继续调试和观察后续行为。
这是一种典型的底层调试技术适用于需要快速验证变更影响但不想修改大量高层代码逻辑的场景。我们做到了最小化干扰且保证执行环境稳定。
这个修改内存应该打开vs 用vs打开二进制文件的路径 哎不对pdb没删掉吗 之前把依赖项删掉了
重新编译之后可以了 触发之前的段错误
换成提示的vssetings看上去还行 这段机器码替换是你在调试或运行时手动修改内存的一种方式目的是绕过崩溃或断言等非法路径。我们逐字节来解释发生了什么前后含义完全不同下面是详细对比与解释。 原始机器码崩溃前
c7 04 25 00 00 00 00 00 00 00 00逐字节解释
这是一个 mov 指令操作的是绝对地址
c7 04 25 00 00 00 00 - mov dword ptr [0x00000000], 0x00000000它的结构如下
c7 04 25表示使用绝对地址模式mov [imm32], imm3200 00 00 00目标内存地址 0x0000000000 00 00 00写入的值 0x00000000
问题
试图写内存地址 0x00000000空指针地址在现代操作系统下会触发段错误segmentation fault因为这是无效地址操作系统保护了该页不被访问。 修改后的机器码
48 85 c0
48 85 c0
48 85 c0
33 c0拆解解释
按顺序来 48 85 c0 → test rax, rax 测试 rax 是否为 0不会改变寄存器值只会影响标志位。 重复了三次是为了覆盖原先等长的字节 33 c0 → xor eax, eax 将 eax 清零等价于 mov eax, 0但更高效常用。 为什么这样修改
原本是个非法写操作空指针写会崩溃。改成无害操作比如 test逻辑判断但无副作用 xor清零但合法确保总共是 11 字节覆盖原指令长度避免执行跳转错位。 总结用途
原始指令作用问题mov [0x0], 0x0向 NULL 地址写入触发段错误崩溃
替换后指令作用效果test rax, rax x3 xor eax, eax无害清零/判断不会崩溃继续执行 适用场景
绕过无效内存访问临时屏蔽断言或危险行为继续运行程序做后续调试 如果你还想了解怎么在调试器里实际操作这段替换例如在 GDB 或 WinDbg 中我可以进一步示例操作。是否需要
点击continue 继续运行 运行游戏发现字符串显示出问题
这个我没出现
我们发现程序中的字符串内容出现了破损损坏、乱码或丢失经过验证这种问题确实存在并且可以稳定复现。目前已确认字符串数据在某些条件下被破坏这意味着之前关于“字符串应该被正确保存”的假设并不成立保存过程存在问题具体原因尚待进一步调查。
为排查问题我们执行了一些操作
通过一次字符串读取的场景确认字符串确实出错将某些代码逻辑注释掉避免每次重现问题时都需要复杂操作例如手动修改汇编指令或补丁内存成功建立了一个稳定的复现步骤只要执行某段逻辑或设置某个标志为 0就可以触发字符串损坏这让我们可以方便地进入调试流程进一步分析字符串损坏发生的位置和原因。
我们推测字符串损坏的根本原因可能很简单比如字符串没有正确拷贝或指针悬空但由于这是一个热更新/代码重载相关的流程涉及到较多的底层内存操作不能掉以轻心。这类系统非常敏感尤其是与调试功能结合使用时如果字符串系统不可靠会严重影响调试信息的可读性进而影响开发效率。
当前计划是基于这个可复现用例继续深入排查到底是哪个阶段破坏了字符串确保热重载和调试系统可以正常共存。我们需要特别关注字符串的生命周期管理、静态存储与动态更新之间是否存在冲突或者是否有未处理好的数据拷贝边界问题。
修改 game_debug.cpp修复字符串处理问题
我们发现当前调试菜单中显示的字符串特别是头部字符串出现损坏的问题。这些字符串不是存储在单个调试元素中而是保存在调试树结构中的“变量组”节点上通过这些节点中的 name 和 name_length 字段进行引用。经过分析和代码排查我们意识到这些字符串在热更新或重载后被错误地引用或丢失其根本原因是指针仍然指向旧的可能已被释放或无效的内存区域。
为了解决这个问题我们进行了如下处理和优化 一、定位问题字符串的来源
字符串来源于变量组的层级结构而非单个调试项每个变量组在创建时都会传入一段字符串作为名称我们发现这部分字符串可能未被正确拷贝而是直接引用原始内存。 二、改进变量组字符串存储方式
我们决定不再直接使用外部指针来引用名称字符串而是在创建变量组时主动拷贝字符串内容确保其生命周期独立、稳定
删除了原本通过 name 和 name_length 直接引用外部内存的方式改为使用内存池进行字符串复制实现了一个新的函数 PushStringNoTerminate用于将字符串按指定长度拷贝到内存池中并在结尾自动添加 null 终止符这样可以防止重载代码或热更新后原内存失效造成字符串内容混乱。 三、对克隆逻辑进行优化
对于已经存在的变量组在克隆子节点时不再复制字符串内容而是直接让子节点指向原始字符串这样可以避免重复拷贝相同的字符串内容节省内存同时保持一致性。 四、清理和精简判断逻辑
精简了字符串比较函数 StringsAreEqual 的使用通过简化判断过程提高代码可读性减少冗余逻辑。 最终目标和状态
通过这一轮修改我们确保了变量组中的名称字符串
始终存储在有效的内存中不依赖外部不可控的生命周期不会在代码重载或调试时发生内容损坏能够稳定显示在调试菜单中保证调试信息的可用性。
整体来说这是一次围绕调试系统稳定性的改进重点解决了内存生命周期与指针引用不当导致的字符串损坏问题增强了程序在开发过程中的可维护性和可靠性。 恢复之前代码 运行出现奇怪的Bug 再次运行并测试热重载字符串稳定但发生崩溃
我们继续调试整个系统重点是验证字符串相关问题是否已经彻底解决并且顺便检查另一个尚未明确的问题——性能分析Profile数据为何消失。 一、字符串问题修复后的验证
我们在修复字符串复制逻辑后进行了以下验证操作
重新回到之前的问题点例如重新加载渲染组检查热重载或代码修改后是否仍然出现字符串被破坏的问题结果显示当前字符串已经可以稳定保留不再在多轮运行或修改后出现乱码或丢失渲染系统中的调试信息显示正常表明我们对内存池中字符串拷贝与指针引用的修复有效。 二、跨模块验证稳定性
进一步进入游戏逻辑部分而不是仅仅停留在调试模块中原先我们观察到的字符串破坏其实是出现在实际游戏运行中而非调试菜单本身当前进入游戏验证时发现系统运行出现异常似乎崩溃了崩溃发生的位置较为奇怪和之前的渲染或字符串逻辑并不直接相关。 三、当前状态总结
字符串复制和管理的问题目前看起来已经被成功解决字符串不再在热重载过程中丢失调试数据的可读性也已恢复然而系统在实际运行中仍存在潜在崩溃点可能是另一个独立的问题暂时还未定位性能分析Profile功能仍无法正常使用需要作为下一个重点进行排查当前程序可以稳定编译、运行并显示调试信息但仍有其他系统性问题等待解决。 后续计划
跟进崩溃点分析确定具体的出错模块查明性能分析功能失效的原因确保调试工具完整可用全面验证字符串处理在所有运行路径中的一致性确保不会在边缘情况中回退为旧逻辑或触发内存越界。
调试器中检查线程发现 OpeningEvent 不一定是有效的
我们在调试过程中发现一个新的问题这次和字符串无关而是出现在碰撞帧collision frames处理流程中但问题仍可能与内存一致性或重载过程相关。 一、碰撞帧逻辑异常触发
碰撞帧本身和字符串并没有直接关系但是在重新加载reload代码或资源后系统在处理碰撞帧时却触发了错误初步看上去像是某个状态不一致导致的异常。 二、线程状态分析
相关线程是有效的说明调度或任务上下文没有问题第一个打开的代码块也是有效的没有指针悬挂或非法访问但“opening event”开启事件似乎存在问题。 三、开启事件可能的问题
虽然这个事件本身仍在缓冲区buffer中但这个缓冲区是一个静态缓冲区static buffer静态缓冲区通常意味着生命周期贯穿整个程序运行然而可能在热重载过程中缓冲区数据没有正确更新或者某个状态被污染了因此事件数据虽然“形式上”还在但可能已经无效导致了崩溃或错误。 四、当前结论
本次异常出现在碰撞帧处理过程中涉及的数据结构看似存在但内部状态可能已经不可用可能与静态缓冲区的生命周期或热重载后的状态恢复有关虽然字符串模块已修复但系统其他部分在重载后仍可能遗留无效指针或状态不一致的问题需要进一步确认静态资源在重载流程中的有效性与更新逻辑避免引用旧数据或未同步数据。 后续排查方向
检查热重载过程中静态缓冲区是否被正确刷新或重新初始化确认事件系统中所有指针和状态是否同步验证碰撞帧中是否还有其他类似的数据使用了“看似有效实则无效”的引用建议对所有跨重载生命周期的对象添加更严格的校验机制。 问题探讨为何 GlobalDebugTable 是静态缓冲区
这个问题的核心是关于为什么在某个地方使用了静态缓冲区static buffer。经过分析似乎这个设计存在一些不合理之处可能是遗留问题。 一、静态缓冲区的使用疑问 静态缓冲区的定义 这里的静态缓冲区是作为全局调试表global debug table来使用的数据被写入这个表中。 为什么使用静态 使用静态缓冲区的原因不明确可能是因为历史原因或某种特定需求但这种做法可能带来一些问题。 静态的潜在问题 潜在重定位 静态缓冲区在程序重载时可能会被重新定位这会导致原本应该持续有效的数据失效或出错。位置不合理 静态缓冲区的使用地点不当。如果它确实是全局的那么应该放在平台层platform layer而不是游戏层game layer。这样做不仅可以减少游戏层的复杂性还能使平台层更好地管理底层资源。 二、设计缺陷分析
静态缓冲区放置错误 把静态缓冲区放在游戏层可能带来不必要的麻烦尤其是当需要重载或进行内存操作时游戏层对它的管理可能不够细致。平台层的更合理选择 理论上平台层应该负责管理这些底层资源并且将其传递给游戏层使用。这样平台层可以确保资源的生命周期被正确控制避免了游戏层和底层逻辑之间的不必要耦合。 三、反思与总结
设计问题 当前的设计让调试表作为静态缓冲区存在于游戏层这种做法显得有些不合理也容易在重载或状态更新时导致问题。游戏层并不应该直接管理这些底层的资源。更好的设计方式 应该将静态缓冲区的管理职责移到平台层确保平台层对这些资源有更清晰的控制权和生命周期管理。平台层将这些资源传递给游戏层这样可以减少不必要的复杂性也能避免许多潜在的错误。历史遗留问题 看起来这个设计可能是过去的遗留问题当前应该进行重新审视和修正以确保代码更加健壮、灵活。 后续行动
重构建议 对现有的资源管理进行重构特别是将静态缓冲区的管理放到平台层确保资源的生命周期被正确控制避免直接将其放置在游戏层中。进一步调试 在修正资源管理问题的同时确保所有的调试数据都能被正确保存和读取避免再次出现类似的字符串损坏问题。
在 win32_game.cpp 中让 GlobalDebugTable 成为主控版本并重写其整理逻辑
问题与分析
当前的设计存在一个核心问题即如何管理和传递调试表debug table。目前调试表的传递方式存在一些混乱和不合理的地方特别是在游戏和平台之间的交互上。分析后发现现有的实现方式可能并没有经过深思熟虑导致一些不必要的复杂性和潜在问题。
1. 当前的实现方式问题
调试表的传递问题目前调试表的传递方式是游戏从外部获取调试表这样的做法不清晰也容易导致问题。我们并不清楚为什么要这么设计尤其是为什么要从游戏获取调试表。不必要的初始化例如事件数组的索引event array index被设置为零可能是为了防止在没有游戏数据时末尾的数据被覆盖但这种做法显得不够合理。静态缓冲区的管理当前的静态调试表缓冲区放置在游戏层容易导致重载时出现问题。正确的做法应该是将静态调试表放到平台层平台层再将其传递给游戏层。
2. 计划的改进方案
将调试表放置在平台层理想的做法是将调试表放到平台层而不是游戏层。平台层负责管理调试表游戏层只需要使用它。这种方式能够更好地分离游戏逻辑和平台层资源的管理。传递方式调整应该在初始化阶段将调试表传递给游戏层。具体做法是在游戏开始时通过memory debug table来设置全局调试表避免游戏层管理调试表的生命周期。也就是说调试表应该在程序开始时初始化并直接传递给游戏使用。删除不必要的返回值在新设计中调试表不再需要通过返回值从游戏层传递出去而是直接在初始化阶段通过平台层传递给游戏。
3. 改进步骤 调整调试表的管理方式确保调试表作为全局变量应该由平台层负责管理。游戏层只需要在初始化时接收并使用调试表。 初始化调试表在游戏的更新和渲染阶段初始化时直接通过global debug table来设置调试表而不再依赖返回值传递调试信息。 简化代码去除多余的返回和不必要的操作简化代码逻辑减少复杂性。例如将一些冗余的操作如事件索引的设置去除避免不必要的状态更改。 代码结构调整通过将全局调试表的管理从游戏层移至平台层并确保初始化时直接传递来提升代码的清晰度和可维护性。
4. 结果预期
通过以上改进调试表的管理会更加清晰和规范。平台层和游戏层之间的交互将变得更加明确减少了因设计不当导致的潜在错误。同时通过简化代码能提高代码的可读性和可维护性也能降低后续修改和扩展的复杂性。
最终的目标是确保调试表能够稳定地存储和传递而不在游戏层和平台层之间产生不必要的混淆和冲突。 运行游戏并做些微调
问题与目标
当前目标是确保调试代码可以稳定运行并在调试过程中避免崩溃。希望通过反复操作确保在多线程和跨帧边界的情况下调试代码能够正确报告问题。
1. 调试代码的重新加载
调试代码正在重新加载并且不再仅仅依赖于单一的线程和帧。这意味着调试系统现在可以处理更复杂的场景尤其是在多个线程并且跨帧边界的情况下进行捕获和报告。
2. 增加多线程支持
通过对调试系统的改进能够处理多个线程的调试信息这些线程可能会跨越帧的边界确保即使在这种复杂的多线程和跨帧的环境下调试信息也能被正确地捕捉和报告。
3. 程序的稳定性
尽管增加了复杂的调试机制当前的目标是在调试过程中尽量避免程序崩溃。通过对调试代码的进一步操作确保它能在多种环境下稳定运行不会因复杂的操作导致崩溃。
4. 调试代码的灵活性与执行
这次的更新使得调试代码更具灵活性能够适应更多的复杂场景和多线程的需求。通过在程序执行过程中加入更多的调试工具能够更好地监控和捕捉问题。
5. 个人对引擎编程的偏好
在过程中明确表示了对于引擎编程的兴趣和对游戏编程的兴趣相对较低。这也表明在调试和开发过程中优先关注的方向是引擎本身的稳定性和调试功能而非游戏逻辑本身的开发。
总结
整个过程中调试代码得到了改进增强了多线程支持确保即使在复杂的操作和跨帧情况下调试信息也能正确捕捉和报告。程序的稳定性在逐步提升而开发重点则更多地放在引擎层面的优化与调试功能的增强。
调查分析器Profiler存在的问题
问题描述
当前的关键问题是为何配置文件profile会消失。在调试系统中配置文件本应保持存在但在实际运行时配置文件却意外消失。我们怀疑这可能与调试系统的某些操作有关但不清楚具体的原因。
1. 分析调试代码
调试代码中我们会进行调试块的操作并查看是否有正确的根节点配置。调试的过程中系统会执行一系列开始和结束的调试块操作并进行数据记录。正常情况下这些操作是配对的且每一帧的开始都应该有对应的“根”配置节点。
2. 代码重载的问题
一个可能的原因是代码重载过程中没有正确创建根配置节点。在重载代码时系统本应根据当前的调试块重新创建根节点。如果在此过程中出了问题可能会导致没有设置根配置节点从而造成调试信息丢失甚至配置文件消失。
3. 推测的原因
假设代码重载过程中没有正确处理调试块导致在某些情况下调试系统没有正常关闭之前的块从而未能正确创建根配置节点。这会导致后续的调试操作中未能正确识别和设置根节点从而出现配置文件消失的现象。
4. 当前的调试流程
在正常情况下调试系统会在执行时创建调试块每个块都有开始和结束的操作确保调试信息的完整性。当执行完一个操作时调试系统应关闭当前块并开始新的块。这些操作和调试信息应当匹配不应该丢失。
5. 事件数组的问题
另一个需要注意的问题是事件数组的索引为什么会被重置为0。根据调试信息我们发现事件数组的索引被错误地设置为零这可能是问题的根源。如果事件数组的索引始终为零系统可能只能使用一个事件数组这就会导致在进行调试时错过某些数据影响调试信息的收集。
6. 调试表的操作
在调试表中有两个事件数组交替使用即pingpong。然而当我们将事件索引设置为零时实际上表明我们只使用了其中一个数组。这会导致调试信息丢失因为系统只会记录一个数组的内容而另一个数组的内容可能被忽略。
7. 问题的核心
根本问题是调试系统中的事件数组和调试块的管理不当特别是在代码重载和更新过程中导致根配置节点没有正确设置或事件数组索引被重置。最终调试信息无法完整记录导致配置文件丢失。
解决方案方向
修复代码重载中的根配置节点创建问题确保每次代码重载后都会重新创建并正确设置根配置节点。避免将事件数组的索引重置为零确保每次调试操作都使用正确的数组避免错过调试信息。优化调试块的管理确保每个调试块的开始和结束都能正确匹配避免在调试过程中丢失数据。
通过这些调整可以解决配置文件消失的问题并保证调试系统的稳定性和可靠性。 在 win32_game.cpp 中添加条件清除若游戏加载失败则清除调试事件数组
我们分析当前调试系统中事件数组索引的行为并尝试确认其中的问题。具体逻辑如下 1. 当前事件数组索引的切换机制
事件数组索引event array index通过一种**交替切换ping-pong**的机制在两个数组之间来回切换。每一帧结束时会通过 原子交换atomic exchange 操作来交换当前索引的位置这样可以保证新的调试事件写入到一个干净的区域避免与之前帧的数据混淆。 2. 原子交换的作用
原子交换操作不仅负责切换当前使用的事件数组索引同时也会清空被切换到的那一组事件数组中的数据这样可以确保下一次使用时调试系统写入的是一个全新的空数组保证数据干净且一致。 3. 对原先逻辑的怀疑
回顾旧逻辑存在一段代码在每一帧结束frame end时强制将事件数组索引清零。这段逻辑很可能是过去在调试系统中共享调试表debug table时的遗留物。
这种强制清零可能是为了清理未被主动清空的数组数据防止因游戏未加载而导致数据积压。也就是说如果游戏未成功加载就无法进入正常的帧结束流程也就无法正确清除旧数据。因此添加了这段逻辑作为保护机制。 4. 对现状的改进建议
当前系统已不再使用共享调试表因此这段强制清零的代码已经不再必要甚至可能引起调试数据丢失应该只在一种情况清空事件数组索引当游戏加载失败时。这时确实需要清理调试数据以免堆积正常加载游戏的情况下绝不应该强行清零事件数组索引否则会导致调试信息在每帧结束前丢失或错乱。 5. 改进逻辑建议
if (game_load_failed) {// 只有在游戏加载失败的情况下清零debug_event_array_index 0;
}这样做可以确保
游戏未加载成功 → 清除调试事件数组避免积压游戏加载成功 → 继续使用正常的 ping-pong 索引切换不清空保持调试数据完整。 6. 总结
调试系统中原本用于清理事件数组索引的逻辑应当仅限于游戏加载失败的情况。其他情况下这一行为会导致调试数据出错或丢失。通过引入条件判断逻辑可以使系统在保留必要保护机制的同时确保调试信息的完整性与准确性。
运行游戏并继续问题调查
我们认为当前的修改更为正确。之前的做法存在严重问题调试事件总是写入到同一个事件数组中这种做法会导致多个线程在写入调试信息时发生冲突。若其他线程仍在使用同一个数组位置调试信息就会被覆盖或丢失造成调试数据异常。这种行为是错误的可能会导致分析时出现误判或信息缺失。
经过更正后调试事件数组通过 ping-pong 机制交替切换避免线程之间的冲突。这是符合我们预期的行为。
随后进行了验证操作虽然一开始认为这个问题并不是导致性能分析信息丢失的根本原因但修改后发现性能分析信息重新显示出来了说明问题的确是出在调试事件数组被错误清空上。
这说明事件数组被清空后丢失了根分析节点导致后续帧中的性能数据无法正确关联和显示。
进一步分析表明性能分析信息的消失可能还有其他隐患例如线程调试块未正常关闭。如果在代码重载或帧切换时存在悬挂的线程块thread block未被正确结束那么整个分析树的结构就会不完整从而无法正确显示分析信息。这一点也必须特别关注。
总结如下 问题诊断与修复过程 原问题 每一帧结束时错误地强制将事件数组索引重置为零导致调试系统始终写入同一个数组多线程调试信息发生覆盖性能数据丢失导致性能分析界面无法正确显示根节点及相关数据。 修复措施 移除无条件清零操作仅在游戏加载失败的情况下清空事件数组恢复 ping-pong 切换机制实现调试数组安全交替使用。 验证效果 修复后性能分析界面正常显示说明问题确实与调试数组错误清空有关原以为不是这个原因但实际验证表明它就是核心问题之一。 后续需要注意 检查是否存在未正确关闭的线程块特别是在代码重载或帧切换时确保调试信息结构完整避免挂起状态。 这一过程体现了系统调试中隐藏状态的复杂性一点小的逻辑失误可能导致整个调试系统失效。目前通过定位并修正清零逻辑已经显著改善了系统行为。后续仍需持续关注线程块生命周期的正确管理。
调试器中进入 BeginBlock 并检查 DebugState
我们当前正处于问题触发的状态因此我们深入查看了调试信息跟踪相关的代码逻辑试图验证并找出具体原因。
首先检查了线程的调试状态。在调试系统中每个线程都有对应的调试信息其中包括一个“当前打开的代码块”字段。我们查看了当前活跃线程的调试状态发现其打开的块是“调试整理块”debug collation block而这个块的父节点居然指向它自己这是明显不正确的。
这种情况通常只会出现在递归调用中但这里并不是递归函数意味着这个块并没有被正确关闭。这种异常恰好与我们观察到的问题相符 —— 性能分析信息缺失很可能是因为存在未正确结束的调试块。
我们定位到这段问题代码是在某个位置。问题块出现在执行 begin_block() 后本应执行 end_block() 进行关闭但在某种情况下end_block() 并没有被调用。具体来说
这是一个包裹在游戏代码热重载过程中的块在 begin_block() 调用之后游戏代码被卸载或刷新而在新代码重新加载之前end_block() 还没来得及执行由于重载发生调试系统的数据结构也会重置导致这个“悬挂”的块始终保持打开状态不再被关闭。
这就解释了为什么性能分析树无法正确显示根节点或其他信息 —— 根本没有形成完整的块结构。
进一步思考了热重载对调试系统的影响发现问题可能出在块匹配机制本身。系统匹配 begin_block() 和 end_block() 时可能是依赖某些标识符例如一个 GUID id而这个标识符在代码重载过程中可能会变化。
如果在热重载后标识符不一致调试系统就无法正确匹配并关闭原本打开的块从而留下“悬挂块”。这种挂起状态会导致后续帧无法以正确方式嵌套新的分析块。
不过也有疑问即热重载是否真的会导致这些关键字段如 GUID变化一度怀疑 GUID 是问题根源但后来又认为也许 GUID 没有实际变化可能还需进一步验证。 详细总结 问题现象 某些调试块未被正常关闭导致调试树结构损坏性能分析数据无法正确显示调试状态中存在自指自身为父块明显不合法。 具体原因 游戏代码重载过程中begin_block() 已执行但 end_block() 尚未执行重载触发后调试系统重置原有块信息丢失残留块无法关闭形成“挂起状态”进一步导致调试树逻辑异常。 潜在根本原因 块匹配机制依赖某些标识如 GUID id热重载可能导致这些标识不一致匹配失败导致新旧块之间逻辑断裂。 可能的解决方向 在执行重载之前强制关闭所有尚未结束的调试块或者确保重载不会清空调试块的状态结构或对调试块增加更可靠的唯一识别机制避免误匹配。 当前判断比较明确确实是因为调试块在代码重载过程中未能正常结束造成挂起从而引发分析数据异常。这是调试系统与热重载机制交互中的一个典型边界问题后续应在热重载前后处理逻辑中加入强制性调试块闭合保障。
检查事件的 GUID 并进入 EventsMatch 函数
我们正在深入检查调试系统中“事件匹配”的机制目的是确认在代码重载前后是否由于事件匹配逻辑的问题导致调试块无法正确闭合从而造成性能分析数据异常。
首先我们观察了两个调试事件
一个事件来自 game.cpp位置在 2412:34另一个事件来自 debug_interface也是在相同的位置 2412:34两者的 source location 显然是相同的。
从表面看这两个事件应该可以匹配。如果事件匹配逻辑正确这样的定位信息应该能对应成功。
接着我们进一步检查事件的匹配机制到底是如何工作的。
我们的目标是搞清楚在判断两个调试事件是否匹配时系统依据了哪些字段或逻辑条件。也就是说
系统是否只基于 文件名 行号 列号 来判断两个事件是否属于同一个调试块或者还依赖其他标识如 grid id、函数地址、某种运行时 ID如果热重载过程改变了这些关键标识如内存地址、指针等就会导致两个逻辑上相同的事件被识别为不同进而无法匹配 begin_block 和 end_block。
这一点至关重要因为
如果事件匹配依赖了会在重载后改变的字段比如内存地址、函数指针等那么 end_block 在新代码加载后执行时就无法正确关闭此前的 begin_block导致这个“悬挂的调试块”一直保留在线程状态中成为性能分析树的异常根源这将使得每一帧都误认为存在一个未关闭的根块进一步破坏整棵分析树的结构。
因此我们下一步的重点是
明确匹配逻辑到底用到了哪些字段判断这些字段是否在热重载前后会发生改变如果会改变就需要重新设计匹配逻辑或在重载前主动清理这些未结束块。
总结如下 当前调试步骤总结中文 观察 两个事件来源于同一位置 game.cpp 的 2412:34看起来应当可以正确匹配。 推测问题 实际可能没有匹配成功原因可能是匹配逻辑依赖了某些会被热重载重置的字段。 正在调查 调试系统内部是如何判断两个事件是否“属于同一个块”的是基于源代码位置还是其他更易变的运行时信息。 关键问题 如果匹配逻辑中包含了在热重载中会改变的内容如指针或 grid 编号将造成调试块无法闭合产生“悬挂块”从而破坏性能分析的完整性。 接下来我们需要具体定位匹配逻辑的代码并确认其字段来源与行为。如果需要我也可以帮助分析匹配函数内部逻辑只需贴出对应的判断代码。
在 game_debug.cpp 中临时添加断言来检查 EventsMatch 的行为
我们在调试过程中深入检查了性能分析系统中调试块debug block的匹配机制重点是分析事件记录过程是否存在逻辑错误尤其是在处理打开块opening event时。以下是详细的中文总结 问题背景分析
我们希望观察调试块在热重载过程中的行为是否正确尤其是“打开事件opening event”是否被正确记录并能与对应的“结束事件end block”匹配。为此临时把原本的条件判断替换为一个断言assert以确保当发生不符合预期的情况时可以立刻被发现。
一旦程序命中断言我们观察到以下异常现象
打开的事件opening event的 grid 竟然指向了一个结束块end block这是不合理的因为打开事件的 grid 应该对应的是一个“打开中的块”而不是某种已结束的结构同时事件记录中的 record_span 起点被标注为了帧序列开头但这并不准确。 关键调查步骤 检查事件结构 我们打印并比较了当前打开事件opening event和现有事件event的结构发现它们之间的 grid 指针设置似乎不对。 查看调试元素内容 opening event 对应的调试元素是 LoadAssetWorkDirectly这是合理的表示这是在资源加载任务中的某个工作块。 定位设置流程 我们追踪了设置 debug block 的过程发现事件是在调用 AllocateDebugBlock 时被分配和设置的但是进一步查看后发现分配过程中将一个事件的指针赋值给了调试块结构中的某个字段但这个指针是临时变量的地址并没有存储到安全的内存区域中换句话说我们把一个将要失效的指针当作“持久引用”存储进去了。 根本问题定位
我们把一个临时事件结构的地址作为指针保存进了 debug block而没有将其正确拷贝到持久内存或事件数组中。因为
这个事件结构指针是分配时局部变量或临时缓存的地址它在事件生命周期结束后被释放或覆盖导致后续访问这个指针时会得到无效的数据这也解释了为什么 opening event 会指向一个不合理的结束块甚至自身变得无效。 总结结论中文
当前断言命中说明事件匹配机制中存在逻辑问题特别是 opening event 的来源不稳定问题根源是错误地将临时指针作为持久引用存储违反了事件系统的内存管理原则应当在设置调试块时将事件从临时指针拷贝到稳定的事件表或结构中避免使用生命周期不确定的引用这个 bug 很可能就是调试系统中无法正确关闭块、产生悬挂结构、性能分析树异常的直接原因接下来应修改调试块的初始化逻辑确保引用的事件数据稳定可靠。 如需我进一步写出建议的修复代码或继续分析事件分配函数的具体实现逻辑请继续说明我可以立即协助。
注意到事件是临时的因此现在改为使用 StoredEvent 我们在对调试事件处理流程进行深入分析时识别出当前事件管理中存在一项关键的生命周期管理问题。以下是详细的中文总结 分析背景
我们正在处理的事件是短暂的transitory也就是说
事件在被处理完之后就不再有效不能再被依赖当前采用的是增量解析方式incremental parsing因此事件处理后会被清空或覆盖一旦缓冲区被刷新事件对象就会失效。 关键问题识别
由于事件失效不能直接在调试块中保留对事件的引用。然而在现有设计中调试块结构可能保存了对这些临时事件的引用从而导致
数据引用悬空匹配逻辑混乱分析结构可能遭到破坏。 正确做法明确
我们可以放心引用的是复制并保存下来的事件stored event因为它的生命周期足够长至少可以跨多个帧使用。相比之下
当前事件是短暂的复制后的事件是持久的或更长久的所以我们应该只在调试块中引用持久化的事件副本而不是临时数据。 更进一步的优化思路
考虑到即便是**存储事件stored events**也可能在之后被释放因为帧数据数量受限我们提出了更保险的改进方案
直接将需要的调试信息复制到调试块本身内部不再依赖外部事件对象的生命周期这样就能确保调试块内容在任何时候都稳定、独立、可靠。
例如
将事件中的 file, line, frame index, event ID 等重要信息直接复制进调试块避免保留原始事件的指针或引用。 当前实现检查
我们回头确认了当前代码的行为观察如下
正确地调用了 StoreEvent 函数成功地将 event, element, frame index, first open GUID 等复制进入结构中但可能尚未完全脱离对临时数据的依赖。 总结结论中文
当前事件处理方式为增量解析事件本身生命周期极短调试块不能依赖这些短暂事件中的任何指针或引用应当优先使用复制后的“持久事件副本”最理想做法是直接将所需字段拷贝进调试块本体保证结构独立性此改动能根本解决事件失效导致的调试数据损坏问题目前代码虽然部分逻辑正确但仍存在可优化之处值得进一步修正。 调试器中检查 Events 与 StoredEvent
我们现在继续检查这些调试事件虽然其他部分可能仍然存在问题但有一点已经可以确认是明显错误的。 当前观察重点
我们查看了一个调试事件属于打开状态open event在调试显示中是绿色的。然而发现了一个异常值
该事件的 GUID 为 0Good 为 0 这是一个明显不对的现象
在正常情况下每个调试事件应当有唯一的标识符GUID用于匹配开始与结束事件、建立事件层级关系等GUID 为 0 意味着它没有正确初始化或者数据在创建或赋值时被忽略或清空了一个事件如果是“打开”的意味着它是某种逻辑块的起始点更应该拥有合法的标识符出现 GUID 为 0 的现象说明该事件不是一个有效的调试结构的一部分或者已经破坏了调试系统的基本一致性。 进一步的思路
目前这个问题可能来自
事件创建时未正确生成或赋值 GUID事件结构被拷贝或传递过程中发生值丢失事件指针引用了临时或无效的对象例如之前提到的“临时事件生命周期过短”的问题调试系统中用于设置 GUID 的逻辑分支未命中即创建路径异常。 下一步建议
为了定位和解决问题我们应当
回到事件分配和初始化的代码路径检查 GUID 是如何生成和赋值的对调试块中的所有开放事件做一致性检查确保所有“打开块”都有有效 GUID在调试视图中增加断言强制所有开放事件必须拥有非零 GUID若发现使用的是临时事件副本考虑使用持久版本或将 GUID 显式复制进去。 总结要点中文
当前看到的调试事件是一个“打开状态”事件它的 GUID 为 0这是不正确的说明事件结构不完整或创建逻辑出错GUID 是建立事件关联关系的核心字段必须保证存在且正确问题可能来自事件生命周期混乱、赋值遗漏或使用了临时无效数据需要回溯事件生成与赋值流程并加入一致性断言以避免此类错误再次发生。
修改 game_debug.h让 open_debug_block 不再保存整个 debug_event而只保存所需数据
当前我们识别出一个关键性设计错误并对调试系统进行了彻底的结构调整旨在修复调试块匹配逻辑中的一系列隐患与冗余处理。 问题核心
我们原先在调试块debug block中直接存储了“打开事件”的引用open event但这是错误的。原因如下
“打开事件”只是一个临时对象在事件解析结束后即会被释放调试系统中的块结构不应该引用一个生命周期不稳定的对象如果之后需要访问事件中关键数据如 clock 或 thread_id就会发生悬空引用或数据不一致。 正确做法与修改内容
我们已经对系统做了以下调整 不再存储事件本体指针 原来的设计中open debug block 存储了一个指向 open event 的引用现在直接将我们需要的数据从事件中提取出来并存储如 begin_clock。 简化匹配逻辑 原先有一个复杂的 events_match 判断逻辑尝试通过比较事件结构判断配对实际上我们只需要比较线程 ID 是否一致现在将其简化为一个断言assertthread_id 必须匹配。 移除冗余结构 清理了用于事件匹配的一些无用字段和函数对 open_event.clock 等成员引用全部取消替换为 matching_block.begin_clock 等更稳定的字段去掉了一些重复的 events_match 判定逻辑。 在 End Block 阶段立即处理需要的数据 事件的任何重要信息都必须在 end_block 执行时立即提取和存储不允许后续再依赖 open_event因为它已无效。 整体目标与收益
保证调试系统对调试块的引用都是长期有效的消除因事件生命周期失效导致的崩溃或未定义行为提高代码清晰度与可维护性简化事件配对逻辑避免误判、遗漏或跨线程匹配。 总结重点中文
原来的做法错误地在调试块中存储了临时事件指针正确做法是提取所需字段如 clock 和 thread_id直接存储配对逻辑现在只依赖线程 ID已改为断言所有引用事件数据的场合都改为使用 matching_block 中稳定的数据events_match 逻辑和相关引用字段已移除调试系统更加简洁安全。 运行游戏发现分析器数据依然会丢失
目前我们已经进入一个更稳定的状态之前一些不正确的代码逻辑已经被清理掉系统整体运行状态显著改善。以下是当前工作的总结 当前状态分析
系统现在看起来运行正常核心调试数据结构的管理更加清晰尝试切换回“世界模式”world mode后仍然丢失了性能剖析数据profile尽管如此这次已经排除了之前存在的冗余逻辑与无效引用问题系统内部的一些“垃圾逻辑”已被移除那些代码原本就是不正确的现在清理干净之后后续调试将更加高效可靠。 已完成的清理工作价值
修复了调试事件生命周期管理错误避免了使用临时对象指针造成的数据失效与潜在崩溃清除不必要的匹配函数与冗余逻辑判断优化了调试块的数据结构提升可维护性与稳定性。 接下来的计划
当前仍存在剖析数据在切换模式后丢失的问题这个问题将留待下一轮排查预计可以较快解决重点将放在“world mode”切换逻辑与 profile 数据同步机制上当前清理工作为后续排查打下了干净的基础避免了被旧逻辑干扰。 总结重点中文
系统运行状态稳定性明显提升原有调试逻辑中的不当引用已彻底清理剖析数据丢失的问题仍未解决但问题范围已大大缩小下一步将重点排查模式切换与 profile 数据之间的关系当前阶段的清理工作极为必要提升了系统整体正确性与可调试性。
如需继续整理明天的调试排查思路也可以为此提前准备一个定位路径。
QA
是否可以将调试 UI 拆分为独立程序通过管道或套接字与游戏通信这样调试数据不会占用游戏资源游戏崩溃时依然能检查帧数据或保存
这是一个非常合理的想法我们完全可以将调试 GUI 独立出来变成一个与游戏程序通过管道pipes或套接字sockets通信的独立进程。这种架构设计可以带来多方面的优势 架构分离的优点 避免调试开销影响游戏性能 游戏仅负责采集和发送调试数据而不再进行调试可视化渲染减少性能开销尤其是在性能敏感的场景下尤为重要。 提升稳定性 如果游戏崩溃调试界面依然可以运行便于分析崩溃前的最后几帧状态甚至可以将调试帧独立保存到磁盘中进行离线分析。 实现远程调试 架构分离后可以在另一台机器上运行调试 GUI这对于主机平台如 PlayStation、Xbox 等开发尤其重要。例如在主机上运行游戏而在 PC 上查看调试信息是常见需求。 便于商业化与工具链拓展 如果将来希望将调试系统打包成通用工具分离式架构是专业工具的基础有利于模块化、标准化。 当前实现架构的适配性
当前系统设计采用写入共享缓冲区的方式已经具备一定的解耦结构将写入行为替换为通过网络或 IPC进程间通信发送数据不会造成结构上的剧烈变更唯一需要额外注意的部分是“事件相关性处理”correlation 部分需要小心同步和顺序的问题只要将这部分逻辑提取到独立程序中通信协议定义清晰即可实现完整功能迁移。 实现展望
可以考虑
实现一个监听套接字的调试服务端程序接收来自游戏的调试数据游戏只负责发包并尽可能不参与调试数据的解析或显示使用自定义协议或轻量级序列化格式传输数据支持缓存、断线重连、调试帧回放等功能增强调试效率与健壮性。 总结
将调试界面独立为一个与游戏通信的程序确实是一个非常有价值且专业的架构优化方向不但提升稳定性、性能也为后续远程调试、跨平台开发、甚至商业化工具化铺平了道路。目前架构已经有基础如果需要扩展到这一方向实现成本也较低。
你在 GAME_INTERNAL 宏外部引用了 GlobalDebugTable而它是在 UpdateAndRender 中初始化的
在这个阶段担心将全局调试表暴露到外部并没有使用适当的内部保护机制可能会导致很多潜在的问题。全局调试表如果没有适当的控制和保护就有可能被外部意外修改或误用导致调试信息的不一致或者错误的调试结果。 主要问题 外部暴露调试数据 如果没有防护机制调试表可能会被外部代码修改导致程序的调试过程受到干扰。 潜在的不一致性 如果调试表被不适当地修改可能会导致不同模块之间的数据不一致进而影响调试结果的准确性。
解决策略 启用内部保护机制 必须确保调试表只在受控的环境下访问和修改。可以通过引入“内部守护程序”来防止外部直接修改调试数据。 清理冗余数据 等到调试功能关闭或不再使用时才对调试表进行彻底清理避免冗余数据的累积从而影响程序性能或导致调试信息混乱。
通过这种方法可以确保调试数据的安全性和准确性并防止潜在的问题影响调试过程。
你觉得模块化编辑器modular editing目前体验如何
目前处理对象和编辑的速度还是比较慢的。原因是还需要做一些工作来简化清理过程尤其是避免频繁切换不同的模式。只要完成这些工作预计会变得更快甚至可能比之前还要高效。
关于全局调试表的讨论问题出在初始化时它在更新和渲染函数中被使用而这个过程没有适当地被内部保护机制限制。具体来说调试表的初始化和使用没有进行适当的保护导致它在外部可能会被修改从而影响程序的稳定性和数据的完整性。
为什么不用 DebugBreak 替代 *(int*)0 0这样可以在需要时跳过断点同时也能触发调试器
我们讨论了一种名为“cubicle”的工具它使用了一种深度插入deep insertion机制但我们选择打破这种机制改用“step over”方式。这种方式允许我们在表面上操作而无需插入。原因在于我们考虑到了Zibo桥梁和平台的需求。相比之下大多数西方平台需要不同的处理方式因为这些平台通常只在Windows系统上运行。对于Unix系统虽然存在一个等效的工具但其功能和操作方式与Windows版本有所不同。
什么是模块化编辑
这是关于“模态编辑”的讨论。模态编辑是指在不同的编辑模式下键盘输入的行为会有所不同。在一种模式下比如绿色模式键入的内容是普通的文本输入而在另一种模式下如红色模式所有的输入实际上都会被视为命令。在这种模式下不需要按住控制键Control或其他任何键来触发命令所有的输入都是基于当前模式的行为。
你写自己的渲染器时是用左手坐标系还是右手坐标系为什么
在编写自己的渲染代码时通常使用右手坐标系。因为右手坐标系是数学领域中最常用的标准所以没有特别的理由去选择左手坐标系。
你禁用树渲染后注意到调整分析器窗口大小的白点在浅色背景上很难看清就像加阴影前的文字一样
注意到当禁用树的渲染时白点在浅色背景上变得难以看清类似于文本之前难以阅读的情况直到添加了阴影才有所改善。确实如果进入美化阶段应该做一些调整。首先应该让这个点一开始就变得更大。另外还要确保它的大小合适。现在它还没有做出这种调整。我们并没有特别处理这个点的绘制方式因此需要明确一个问题应该同时展示多少个配置文件我觉得我们可能希望显示更多的配置文件。
你觉得边讲解边编码对你理解或编程本身有帮助吗
在编程过程中注释主要是为了记录一些以后可能会忘记的东西。通常注释并不是帮助思考的工具而是为了确保不会遗忘某些细节特别是在处理复杂代码或长时间不再涉及某些部分时。
题外话我注意到 #pragma pack 会改变结构体对齐方式有办法保留原始对齐方式吗
#pragma pack 的作用是改变结构体的对齐方式它的目的是控制结构体成员的内存对齐以节省内存空间。默认情况下结构体成员的对齐方式通常会根据最大类型的对齐要求来决定。使用 #pragma pack 可以调整这个对齐方式从而减少内存的浪费。然而一旦使用了 #pragma pack结构体的内存布局会被压缩这也可能导致性能问题因为不同平台的 CPU 在访问不对齐的内存时可能会变慢。
如果想要保持原有的对齐方式通常不使用 #pragma pack而是依照默认的对齐规则。如果已经应用了 #pragma pack想恢复原来的对齐方式可以通过 #pragma pack(pop) 来恢复到之前的对齐设置。
是否能保存 DebugUI 的布局让撕开的窗口在下次打开时仍保持上次状态
在讨论游戏调试时考虑到是否需要保存调试视图的布局以及当前打开的窗口尽管这样的功能是有用的但在当前的情况下可能并不会特别去实现。相反可能会选择在代码中实现这种功能通过编程的方式来创建额外的视图。这样可以灵活控制调试信息的显示而不需要每次都保存和恢复调试状态。
如果这是一个商业系统尤其是当调试系统作为独立的组件进行发布时保存和恢复调试视图的状态就变得非常重要。在这种情况下肯定会加入这种功能以确保开发人员和用户能够方便地恢复到之前的调试环境。因此是否实现这个功能取决于当前的项目需求和目标。