当前位置: 首页 > news >正文

游戏引擎学习第247天:简化DEBUG_VALUE

欢迎。关于纹理传输的详细情况。

上周我们刚刚完成了纹理下载的相关工作,但实际上并没有完全解决这个问题。问题的核心是,当前关于纹理下载的正确方式仍然存在较大的不确定性。尽管我们在进行纹理下载的工作时已有一定进展,但依旧有不少模糊的地方。

这其实是一个持续进行的过程,可以说是一个“传奇”。我们想要展示的,是如何进行编程的过程,而不仅仅是展示编程完成后的结果。很多编程工作尤其是涉及到图形编程的情况,常常会遇到类似的情况。大家可能会去参考一些推荐的做法,或是去查看一些最新发布的教程和视频,但当实际动手实现时,却往往会发现这些方法并不完全适用。有时可能会发现它们在某些场景下根本不起作用,或者其中的某些部分是错误的,甚至可能完全没有提供实际可行的做法。

现在我们正处于纹理下载的一个“悬而未决”的阶段,实际上这也是大部分编程工作的常态,很多时候会遇到这样的问题,既没有一个标准的答案,也没有现成的解决方案可以直接拿来用。

NVIDIA 还没有回应如何正确地传输重叠的纹理。

尽管我曾与 Nvidia 有过一些沟通,但目前还没有得到关于如何在他们的显卡上正确下载重叠纹理的明确答案。

一些人认为有一定的解决方案,但我并不确定他们的说法是否准确。因为这些人并没有特别权威的证据来证明自己的观点,且没有人能够给出确凿的数据支持他们的说法。

重叠的 CPU 工作和 GPU 工作并不是同一回事。为了让纹理到达 GPU,必须进行两个复制:一个是从磁盘到内存,GPU 可以“看到”的内存,然后从 GPU 可以“看到”的内存到实际驻留在 GPU 上的内存。

有些人认为 CPU 和 GPU 的重叠工作是相同的,然而这些工作实际上并不相同。我们上周讨论过这个问题。

要将纹理传输到 GPU,必须完成两个必要的拷贝。尽管可以有很多不必要的拷贝,但至少有两个是必须的。首先,纹理需要从硬盘拷贝到 GPU 可以看到的内存中;然后,它必须从 GPU 可以看到的内存拷贝到 GPU 的实际内存中。

即使使用直接 GPU 映射(Direct GPU Mapping),这两个拷贝依然是必不可少的。值得一提的是,虽然你可以使用直接 GPU 映射,但这仍然无法避免这两个拷贝过程。

直接 GPU 映射过程描述

甚至存在所谓的“定向队列映射”(Directed Queue Mapping)机制,例如可以让 GPU 直接将数据写入系统内存,然后网络卡可以直接从该内存中读取数据,无需经过中间缓冲区或复制到多个不同的内存映射区域中。这种方式虽然在一定程度上减少了内存移动,但即使使用这样的方式,数据依然需要先从磁盘进入系统内存,然后再从系统内存传输到 GPU,至少需要两次数据复制。

目前为止,还没有听说过 GPU 能直接从磁盘读取数据的机制。因此,在整个过程中,无论使用哪种方法,至少需要两次数据的移动才能将纹理加载进 GPU。

在 OpenGL 中,确实存在一些方法可以绕过第一步复制过程。例如,从磁盘加载到内存时,理论上可以避免某些中间缓冲。但当前我们使用 glTexImage2D 加载纹理时,它会将数据从主机内存复制到 GPU 可见的内存区域,也就是 CPU 可访问且 GPU 可读取的一块内存区域,这样 GPU 才能进一步将其传输到自己的专用显存中。

这部分并不是我们当前讨论的核心。大多数人理解的纹理上传流程其实只是前半段,我们正在关注的并不是从内存复制到 OpenGL 的那一步,而是更深层的后续纹理传输机制,即如何有效控制这两步传输以实现真正的 CPU 和 GPU 异步工作。

我们如何告诉驱动程序开始使用卡片的异步内存传输能力来传输纹理

我们目前面临的问题是:如何通知驱动程序,利用显卡具备的异步内存传输功能来开始传输纹理数据。为此,我们参考了曾经在某显卡厂商官网上发布的最新建议,那是我们能找到的最权威且最新的信息。

根据那份文档的说明,实现这种异步纹理传输的方法,是创建一个次级的 OpenGL 上下文来处理传输操作。目前,尚未发现其他被官方公开认可的方式来告知驱动程序执行异步传输,因此我们暂时只能按照这种方式来尝试。

但实际上我们也处于一种停滞状态。因为没有更明确的官方答复,我们也无法进一步确认这种方法是否就是“正确”的,或者是否已经过时。虽然可以尝试自己做实验来验证,但由于无法获取显卡驱动的源代码,我们根本不知道驱动在内部是如何做出传输决策的。因此,也就无法有针对性地设计实验去引导驱动行为。

换句话说,我们对内部机制一无所知,所以也不知道哪些行为会促使驱动执行我们想要的操作。也许方法很简单,也可能非常复杂——但我们完全无从判断。在没有进一步反馈之前,这件事就只能搁置在这里。

决定等待 NVIDIA 对他们认为最佳纹理传输方式的回应

目前,纹理传输的相关工作基本暂时搁置。我们的计划是,暂时观望,等待显卡厂商是否会回复,并提供更明确的指导意见,说明他们认为传输纹理的最佳实践是什么。

一旦收到他们的反馈,我们就会回过头来,根据新的建议调整现有的传输流程,使其更符合正确、优化的实现方式。因为现阶段我们所参考的方法——即他们之前所发布的方式——现在看来并不像是一个真正理想的方案。

总之,虽然之前的建议提供了一种做法,但结合我们目前对情况的理解,这种方式看起来并不合理或者不再适用。因此,在缺乏更准确信息的前提下,我们选择先暂停修改,静待进一步的说明,然后再做调整,以保证最终方案既正确又高效。

提醒我们在两边都没有设置下载屏障,不确定这是否会造成问题

在纹理传输流程中,有一个步骤我们目前并未执行,而这是他们最初建议中提到的:在传输的两端加上同步屏障(fence)。我们之所以没有加,是因为我们并不关心纹理下载是否有明确的同步完成信号。

但也不排除一种可能:也许如果没有设置同步屏障,驱动程序根本不会意识到需要执行该传输操作,导致操作被忽略或执行不当。现在我们对此无法确定,只能等待进一步的信息。

基于这种不确定性,目前我们决定采用观望态度,不再继续对这个传输流程进行尝试性修改。因为我们认为,如果继续投入时间在这方面摸索,很可能不会带来实质性成果,而且也不是一个高效的时间利用方式。

此外,如果我们真的想深入解决,也可以自己花很多时间做试验、测试各种行为,但由于我们不了解驱动内部的具体实现,也无法预判哪些操作可能有效,这种尝试带来的回报极其有限。因此,现阶段的策略就是暂停处理,等待更明确的技术说明,再决定是否调整做法。

替代方案——我们可以继续在单线程中下载纹理

我们可以继续在单线程上进行纹理下载,这其实并不会带来什么严重后果,也不会对整体造成致命影响。我们原本想做的是确保GPU和CPU工作的重叠能够被正确设置起来。如果最后发现无法实现这种重叠优化,那也没关系。

如果需要的话,我们还有其他方式可以优化,比如通过使用缓冲对象或像素缓冲区来消除第一次复制的开销。这部分优化流程相对比较清晰,未来如果想要进一步提升性能,是可以继续深入处理的。

目前,我们希望能够尽快收到关于最佳传输方法的回复,这样就能根据官方推荐的方式来构建代码结构,从而在未来优化纹理下载时可以更加顺利,打好基础。但在收到回复之前,决定暂时不再继续深挖纹理传输的问题。

至于接下来的计划,当时查看了待办事项文件,虽然一度担心文件没有保存,但最后确认其实是保存下来了。因此目前的待办清单是完整的。

现在有几个不同的方向可以继续推进,例如继续完善渲染方面的工作,修复调试代码,或者处理一部分音频相关的调试内容。但正如之前所提到的,目前还不太确定下一步最有趣、最有价值的事情是什么,需要根据具体情况来判断。可能是整理和完善一些GPU端的代码,也可能是继续推进渲染系统的完善。

决定修复调试代码

我们决定接下来要优先完成调试代码部分,因为目前这块代码写得比较混乱,处理起来也有些令人不舒服。希望能够花点时间彻底清理一遍,让它变得更加整洁,并且确保能稳定运行我们真正需要的调试功能。

具体来说,我们希望建立一个可以可靠使用的性能分析系统(Profiler),以及一些用于控制变量的机制,例如能更方便地在运行时查看和调整变量。除此之外,也希望能够更系统、更连贯地导出或显示各种数据。目前在这方面的功能比较薄弱,缺少足够的工具去直观地检查或绘制程序状态,因此需要增加这类能力。

总体目标是让调试界面更加实用,能够支持更深入的问题排查与分析,为后续开发工作打好基础。之后简单地切回了项目工程,准备开始动手处理,并先进行了一次编译,确保代码是可以正常工作的。

提醒在 Casey 使用的 AMD 机器上,纹理下载问题似乎没有发生

在我们当前使用的这台机器上,由于其是 AMD 架构,之前在讨论中提到的纹理下载相关的问题实际上并没有出现。换句话说,这类问题在这台设备上并不存在,因此在当前环境中是看不到那些错误现象的。

不过必须说明,确实有用户报告过类似的问题,也就是说这些问题是真实存在的,只是在这台机器上不会复现而已。虽然在当前环境中无法直接看到问题的表现,但问题的存在是被多个用户证实的,因此需要认真对待。

此外,还有人预测了一些可能的原因和影响,也就是说大家对于该问题背后的机制其实并不完全清楚,只能依赖经验和推测来判断可能性。我们也因此更加倾向于等待官方的明确回复,而不是在不了解底层机制的前提下盲目尝试。

glFlush() 似乎强制下载在单独的线程上进行

之前遇到纹理下载问题的用户表示,如果在下载纹理后手动调用一次 glFlush,可以迫使纹理下载在一个独立的线程中进行,从而成功规避问题。换句话说,通过强制同步,可以让纹理的异步传输过程变得明确,这样可以暂时解决某些 GPU 上存在的问题。

目前我们也采用了类似的做法,在相应位置添加了 glFlush,虽然我们并不真正依赖它,也不认为这是最终方案,但在等待官方反馈的过程中,这个处理方式对使用某些显卡的用户是有帮助的。因此,暂时保留这段代码是合理的,可以避免他们遇到麻烦,提升程序在更多设备上的兼容性。

我们当前对这段代码并不特别关心,因为最终会根据官方提供的最佳实践做出修改,现在更像是一个临时应对手段,确保程序在不同平台上的基本运行无误。

接下来,我们之前的重点已经回到了调试代码部分。调试系统目前相对混乱,因此打算花点时间来清理,使其能稳定运行我们真正关心的功能,比如:

  • 提供稳定的性能分析器(Profiler),可以可靠地收集运行时数据;
  • 增加对变量的控制能力,方便我们调试和观察特定状态;
  • 改善数据的可视化方式,使信息更易于理解与分析。

当前版本中我们缺乏这类手段,很难直接查看运行时的内部数据或状态变化。我们希望通过完善调试工具,提升对程序行为的洞察能力。

现在打算回到相关代码中检查当前状态,首先进行一次编译,确保基础功能正常,再进入下一步的清理与改进工作。
之前做过了
在这里插入图片描述

我的就是必须flush才行

提醒原始调试代码的目的

我们之前花了不少时间研究调试系统,希望让它具备良好的可记录性和易用性,目标是构建一个既强大又方便的调试工具。不过,最终并没有完全达到“全面完成”的状态。这其中一个原因可能是我们尝试做得太多,加入了各种功能和实验性设计,导致整个系统变得过于复杂。

经过这些尝试后我们意识到,也许需要调整策略,从“它能做什么”回归到“我们真正需要它做什么”。也就是说,为了尽快完成这个调试系统,应该适当收敛功能范围,避免它变成一个庞大的项目。如果决定将调试系统打造成一个高度可复用、功能强大的通用框架,那固然是一个值得投资的方向。但如果目标仅仅是在当前项目中获得实用的调试支持,我们更可能需要聚焦于我们最迫切需要的那一部分功能。

换句话说,与其追求一个尽可能全面的系统,不如先构建一个轻量、高效、符合实际需求的调试工具,让我们可以迅速回到其他开发任务上。这个“收缩”策略更务实,也能避免因调试系统而拖延整个项目进度。未来如果有更多时间和资源,再回头扩展这个系统也不迟。

专注于调试接口

我们现在首先要做的是查看调试接口部分的内容,因为这是我们上次停下来的地方。现在我们进入的是一个“完善和收尾”的阶段,我们打算从几个不同的角度来处理这个系统,逐个明确我们希望实现的功能,并确保它们能够良好地支持我们的需求。

比如我们有一个具体的例子:某些函数如 GlobalPauseGlobalUseSoftwareRendering,目前并没有办法在运行时动态地去修改这些变量。虽然游戏主体部分我们已经支持了代码的热更新,但平台层并不支持这种机制,所以这些变量目前是无法实时更改的。我们需要解决这个问题。

为了解决这个问题,我们希望能让这些变量变得易于编辑。目标是能在调试界面中看到这些变量,并以图形界面的方式对其进行交互操作,比如打勾或输入值。这样我们在调试时就能实时控制程序行为,而不必每次都重启或者重新编译。

此外,我们注意到目前程序启动时在初始化 OpenGL 时耗时非常严重。特别是在创建 OpenGL 上下文的时候,启动速度变得很慢,这对开发调试带来很大不便。有时我们甚至宁愿使用软件渲染来避免这部分性能开销。

因此,当前的重点在于:

  1. 为平台层关键变量提供运行时调试控制界面;
  2. 清理和简化调试接口代码,使其只保留我们真正需要的功能;
  3. 思考是否需要临时回退到软件渲染以加快调试时的开发效率;
  4. 优化调试系统的使用体验,确保我们能够灵活而快速地操作调试信息。

总之,我们希望通过一次全面的、目标明确的梳理,把调试工具从一个“原型”提升到一个更稳定、实用的阶段。

win32_game.cpp 中 GlobalPause 和 GlobalUseSoftwareRendering 很适合包含在接口中

在这里,我们希望能有一个简单的方式来控制一些全局变量,比如 GlobalPauseGlobalUseSoftwareRendering,这些是希望能够在调试过程中方便地修改的变量。现在的目标是找到一种方法,通过合适的界面让这些变量可以被编辑和控制。

最理想的情况是,如果我们处于“元编程”的思维模式下,可能会选择直接在代码中标记这些变量,使它们能够被自动识别并进行修改。这种方式成本几乎为零,可以让这些变量直接暴露给调试工具。但是,这种方式可能会让代码变得过于复杂,所以我们希望保持简单,不引入过多的外部复杂性。

接下来的最佳选择是,在代码中找到一个合适的地方声明这些变量,让它们可以被编辑。在实际操作时,可以将 GlobalPauseGlobalUseSoftwareRendering 放到一个特定的位置,这样就可以通过调试工具将它们设为可编辑。

例如,在程序的主循环部分(如 while running 这一块)中,我们可以声明这些变量,并通过一个调试工具接口将它们暴露出来。我们已经有一种方法可以通过 debug_var 函数将变量暴露出来,在 debug_var 中定义变量类型并将其注册为调试变量。

具体的操作方法是,通过 debug_var 函数将这些变量声明为“可编辑的变量”,这样就能在调试过程中实时修改这些变量的值。不过,目前这种方法是通过全局常量的方式来实现的,而这种做法的缺点是可能导致一些不必要的复杂性。因此,是否继续采用这种方式还需要进一步考虑。

总之,当前的目标是使得 GlobalPauseGlobalUseSoftwareRendering 这类关键变量能够通过调试接口被实时控制,以便在调试过程中更加灵活地管理程序行为,同时确保实现方法足够简单和高效。

win32_game.cpp - DEBUG_EDIT() 用于包含 GlobalPause 和 GlobalUseSoftwareRendering

如前所述,这个问题可能是我们走得有些太远了。现在的目标是简化一些功能,找到一种更直接的方式来处理调试变量,避免过于复杂的实现。在这种情况下,可能我们只需要一个简洁的方式来声明哪些变量是可调试的,而不是追求过于复杂的方案。

理想的做法是,我们可以通过简单的标记来指出某个变量是“可调试的”。这种方式可以让我们快速指定需要调试的变量,同时避免引入过多的复杂性。我们可以像之前那样,使用字符串路径来组织这些变量,以确保界面不会被过多的变量搞得凌乱。通过这种方式,调试的变量可以有分类,避免界面被过多变量占据,影响操作的流畅性。

比如说,在这次的实现中,我们可以将 GlobalPauseGlobalUseSoftwareRendering 这些变量放在合适的路径下,像是 platform/pausedplatform/soft_rendering 这样的路径结构。这样可以保持调试界面的整洁,并且也不需要做太复杂的操作,只需要明确地指示哪些变量是需要编辑的就行了。

总的来说,这种方法是通过在代码中简洁地定义调试变量,给它们指定一个路径,使得它们在调试过程中可以轻松被编辑。这种方式能有效地简化操作,同时保持调试功能的完整性。

GlobalUseSoftwareRendering 貌似之前已经改成DEBUG_IF

在这里插入图片描述

直接添加一个吧

在这里插入图片描述

在这里插入图片描述

对于使用 game_config.h 来指定调试变量的方式表示怀疑

目前的想法是,我们可能需要回到之前的做法,通过配置文件来管理调试变量。然而,使用配置文件的方式让我感觉有些不太合适,因为它导致了一个问题,就是配置文件变得非常庞大,而且每次添加新的变量时,都需要手动去更新配置文件。这种方式虽然可以把所有设置集中在一个文件中,方便抓取和管理,但也让流程变得有些繁琐和复杂。

我并不确定这是一个坏方法,可能这是某些情况下确实有效的做法,但我个人感觉它有些过于复杂。如果我们继续这样做,可能会面临更多的麻烦,比如每次修改或添加代码时,都得再去更新这个配置文件,这给维护带来了很多额外的负担。

另外,尽管这样做可以更容易地管理一整套设置,但是过于依赖配置文件可能会造成不必要的复杂性。我倾向于希望能简化这个过程,减少复杂的配置和管理。或许可以考虑将调试变量直接绑定到调试系统中的某些编辑点,这样每次就只需要关注必要的变量,而不需要每次都去更新配置文件。

因此,我的想法是将调试变量的绑定处理方式简化一点,减少对配置文件的依赖,让开发和维护变得更加直接和高效。这样做虽然不一定比以前的方式更高效,但至少能让过程变得更简单,避免无谓的复杂性。这就是我现在的考虑,目标是减少复杂性,让系统尽可能简单明了。

win32_game.cpp - 用 DEBUG_VALUE() 替换 DEBUG_EDIT() 并检查是否有效

我们正在考虑简化调试变量的管理,可能通过使用debug value这一方式来替代以前的debug variable概念。我们希望能够简化系统,将变量管理方式精简成一种更直接、易于维护的方式。原本我们是通过标记变量来让它们可编辑,或者通过配置文件来管理它们,但这些方法似乎有些复杂且冗余,因此决定尝试一种更简单的方案。

当前的方案是我们之前使用的“数据块”机制,其中包含了debug id和一些关联的标识符。通过给每个数据块分配一个唯一的ID,系统能够稳定地跟踪和识别这些变量。这个ID其实并不复杂,我们可以用任意稳定的指针来作为标识符,这样就能确保每个调试变量都有唯一的标识。通过这种方式,系统能够在不增加过多复杂性的情况下,继续管理和识别这些变量。

这种方法的好处在于,它不需要我们去维护庞大的配置文件,也不需要为每个变量单独设置调试选项。只要为每个变量分配一个唯一的标识符,系统就能自动处理其他部分。这种方式简单直观,并且没有额外的负担。

接下来,我们打算进一步测试这种方法的可行性,看看它是否能更有效地管理调试变量,同时避免过多的复杂配置。如果这种方法行得通,它将大大简化调试过程,提高开发效率。
在这里插入图片描述

修改一下错误
在这里插入图片描述

未处理的结束数据块

当前面临的问题是调试过程中的时间延迟非常长,令人非常不满。经过检查后,发现原本应该通过“开始数据块”和“结束数据块”来捕捉调试信息,但似乎出现了一些问题,导致无法按预期工作。怀疑可能是由于调试代码没有完全完成,造成了一些错误或不一致。

在这个过程中,应该按照正确的流程使用“开始数据块”和“结束数据块”来包裹调试信息,这是最合适的做法。虽然一开始的检查看起来没有什么问题,但还是有可能是由于调试代码的不完善,导致它无法正常工作。因此,接下来需要仔细检查代码,弄清楚是哪里出了问题,特别是看看是否在调试过程中遗漏了某些关键的部分或者配置。
在这里插入图片描述

调试接口在游戏模式下不可见

目前的调试问题是,尽管已经做了调试设置,但在游戏模式下并没有看到调试输出,这让人感到困扰。原本应该能够看到调试信息,但目前没有显示出来,可能是由于某些调试内容没有正确打印出来。

考虑到调试文本是白色的,如果它出现在背景上,确实可能被遮挡住,所以有可能看不清。但问题在于,这些调试信息应该被放置在最上层,而没有出现的原因目前不明确。

在之前的调试过程中,已经把调试相关的调用移到了平台的某个位置,理论上这些调用应该已经正确添加并显示。但目前看不到输出,怀疑是因为某些排序或者图形层级的问题,可能是调试文本没有被正确地放置在前景显示层。

下一步需要深入分析为什么没有看到调试信息,尝试找出具体原因并修复它。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

检查是否通过不渲染世界来绘制调试组件

为了调试问题,首先决定验证调试信息是否真的被渲染出来。方法是通过使游戏世界模式不进行渲染,从而避免它有机会进行任何渲染。这种方法可以简单有效地检查调试信息是否显示。

具体来说,可以修改更新和渲染世界的代码,特别是渲染组部分。如果将其设置为零,渲染过程将被阻止,这样可以确保不会有任何内容被渲染出来,从而不受其他图形的干扰,可以专注于查看调试信息是否被正确绘制。

尝试过这种方式后,发现调试信息实际上仍然被渲染出来。为了进一步确认,需要在渲染之前添加一个清除操作,以确保没有其他图形影响到调试输出的显示。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在绘制之前添加清除操作

可以稍微调整一下,将“if zero”语句向下移动一点。这样做的目的是确保屏幕在渲染之前被清空。这样就可以检查是否仍然能看到调试信息,从而判断问题是否出在排序上。通过这种方式,可以确认调试信息是否因为排序问题未能正确显示。

确认调试接口在游戏中没有显示,因为存在排序问题

如我所料,问题出在这些元素的排序上,它们被排序到低于游戏世界中的内容。因此,需要一种方法确保这些调试信息始终排在最前面,不能被游戏中的其他内容遮挡。问题的关键在于如何有效地做到这一点。

对于排序键(sort key),实际上我们有很大的控制权,因为它是我们自定义的。因此,我可以做的是在执行调试文本输出时,确保这些文本的排序值(z值)被强制设置为更高的优先级。具体来说,在推送位图(push bitmap)时,可以加入一种机制,覆盖默认的z值,从而确保调试文本始终排在前面,避免被游戏世界的其他内容遮挡。

game_render_group.cpp - 向 PushBitmap 添加 SortBias -> Dim.Basis.SortKey

在推送位图(push bitmap)例程中,可以看到它会遍历并推送渲染元素,而这些元素的排序是通过GetUsedBitmapDim调用返回的结果来确定的。这些结果会决定渲染元素的排序方式。但是,实际上可以自由传递其他信息来修改排序行为。

一种方法是可以在排序过程中添加一个偏置值。通过这种方式,可以在排序时对元素进行额外的调整,确保它们按预期的顺序排列。例如,可以在排序键(sort key)中加入一个额外的偏置值,这样做默认情况下不会影响排序,但如果需要的话,可以根据需求调整排序顺序。由于目前排序键是浮点数类型,所以可以在此基础上进行调整,以便在执行推送位图时确保它们按预期的方式排序。

这种方式实现起来非常简单,只需在推送位图时传入一个调整后的排序值,就能轻松控制渲染顺序。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game_debug.cpp - 为 PushBitmap 调用添加 FLT_MAX 排序偏差,以便我们能够获得最靠近我们的调试界面(在 Z 轴上)

为了确保调试信息总是显示在最前面,可以在 push bitmap 调用中加入一个排序偏移量。具体而言,可以在调用的末尾添加一个较大的排序偏差值,使得调试信息始终位于渲染的最前面。

在坐标系统中,我们希望排序值接近我们,即 z 值朝向我们。因此,应该设置一个相对较大的数字作为排序偏差。虽然可以将其设置为最大值,但不一定需要这么做,因为只要确保值足够大,就能保证调试信息显示在前面。使用最大值虽然可以确保偏差的效果,但也需要考虑是否会对浮动点值产生影响。一般来说,如果值过大,系统应该会自动对其进行限制,因此设置一个合理的大值就可以了。

设置了排序偏差后,调试信息回到了屏幕上,但也可以看到它们显示不太清晰。这是因为如果背景色是亮白色,白色的文本会和背景融为一体,导致无法清楚看到调试信息。因此,需要考虑调整文本颜色或背景颜色,使调试信息更为明显。
在这里插入图片描述

在这里插入图片描述

这个函数很烦申明的太多

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game_debug.cpp - 通过复制 PushBitmap 调用、稍微移动位置并改变颜色来为文本添加阴影

为了让调试信息在各种背景下都能清晰可见,可以为调试文本添加简单的阴影效果。具体方法是,在绘制文字时,先绘制一次稍微偏移且为黑色的文本作为阴影,然后再绘制一次正常颜色(如白色)的文本。

具体实现步骤如下:

  • push bitmap 的调用中,进行两次绘制。第一次绘制时,给位图偏移一个小量(例如在 Y 方向下移一点),并设置绘制颜色为黑色。这样生成一个简单的阴影效果。
  • 第二次绘制正常的文字,即位置不偏移,颜色为白色,这样确保文字本身覆盖在阴影之上。

这种方法虽然简单,但足以保证调试文本即使在非常亮或者复杂的背景上依然清晰可见。虽然视觉效果并不是特别精致,但作为调试用途已经足够实用。

另外,因为现在可以自定义排序键(sort key),可以通过设定不同的排序值来确保绘制顺序正确。也就是说,先绘制黑色阴影,再绘制白色文本,确保文本总是在阴影之上显示。具体操作是在两次 push bitmap 调用中分别指定略微不同的排序偏移值,保证阴影先绘制、文本后绘制。

这种处理方式目前已经能很好满足调试显示的需要,以后如果需要更漂亮的调试输出,还可以进一步改进。现在这样已经算是非常不错的阶段性成果。
在这里插入图片描述

为了鼠标能够窗口坐标对应上Win32ResizeDIBSection 必须改为点全屏的大小

在这里插入图片描述

我电脑屏幕 1707 x 960

在这里插入图片描述

在这里插入图片描述

窗口改变坐标就会对不上

在这里插入图片描述

在这里插入图片描述

回到之前的任务 - 检查交互选择是否有效,并确认未处理的结束数据块不是由添加 GlobalPause 和 GlobalUseSoftwareRendering 引入的 bug

我们最初的目标是希望能够编辑这些数值,因此需要回到原本的工作方向上来。目前对于这些“blocks”的具体作用还不是很清楚,同时也不太了解在这个“surface”上的选择功能是否仍然正常运行。经过检查,确认了“surface”的选择功能依然是可以使用的。

进一步测试后发现,之前担心的在数据块(data blocks)中的处理并没有问题。数据显示方面,之前出现的打印信息实际上只是之前在插入数据时犯的一些低级错误导致的,而不是当前逻辑上的错误。至于“begin”和“end”的处理,也确认在新的情况下没有问题,一切运行正常。

当前整体系统的状态良好,之前的功能仍然保持正常,基本没有出现破坏性的问题。因此,下一步的重点是让这些数值变得可以编辑,只要完成这一点,整体功能就能够达到预期,项目就能够顺利推进。
在这里插入图片描述

检查调试打印层级

目前还有一个地方不太清楚,就是对于当前这种readout(读出)的显示情况,具体是怎么导致的。感觉现在的readout似乎应该归属于某个更明确的上层结构之下,而不是像现在这样独立存在。因此怀疑是缩进(indentation)处理得不正确,导致了结构上出现了问题。

为了确认具体原因,在正式开始其他工作之前,决定先快速查看一下相关代码,弄清楚readout显示结构不正确的问题是怎么发生的。目的是在继续开发之前,确保整个结构和逻辑都是清晰且合理的。

win32_game.cpp - 检查“Platform”应该如何在 DEBUG_BEGIN_DATA_BLOCK() 中工作

这里重新回到我们的debug区域,查看debug的起点和终点。当前情况下,涉及到了platform(平台)部分的内容。根据现有的观察,可以推测在这个流程中,platform的处理可能和之前关注的readout结构问题有所关联。

目前推测,在debug的过程中,platform相关的数据或者逻辑可能插入到了不合适的位置,进一步导致了缩进不正确或结构混乱的现象。这也是导致readout没有正确归属到应有层级下的潜在原因之一。

因此,在接下来的步骤中,需要更仔细地检查platform部分的代码逻辑,以及debug区域的整体布局,确保缩进、结构、逻辑层级都符合预期,避免因结构混乱导致其他后续问题。当前主要目标依然是理顺现有结构,为后续的编辑功能改进打下稳定的基础。

game_world_mode.cpp - Simulation_Entity | 确认打印块名称尚未正确实现

目前对于现有的实现方式还不太确定,因此决定再次回头查看一下world move部分,特别是它在执行debug值输出时的具体做法。观察后发现,主要处理的内容似乎是simulation_entity,这是对应的数据块名称。但具体来说,像platform control这一类内容似乎并没有明确的对应或者处理逻辑,也可能是因为当时还没有真正完成打印实际块名称的功能。

推测当时的打印机制本身就比较随意且混乱,并没有经过精细设计,因此输出结果看起来零散且不统一。进一步确认后,基本可以认定目前这部分代码属于未完成或者半成品状态,需要回头进行整理和完善。

在跳回游戏运行环境再次观察后,确认了之前的推测:当前的打印逻辑是简单输出块名称(比如simulation entity),然后遇到结束标记时,因为没有正确处理,所以出现了未处理的情况。说明整体打印系统确实不完善,导致了这些混乱的输出。

因此,接下来的计划是,着手清理这一块的代码。通过完善打印输出逻辑,使之能够统一、清晰地处理所有数据块,而不是依赖于零散的、特例化的if判断。只要把这部分处理完善了,不但可以提升整体系统的可维护性,还可以顺带去掉之前一些临时加上的冗余控制逻辑,从而让代码更简洁、更稳定。

总结来说,当前主要的目标是系统性地清理、统一数据块打印处理逻辑,规范化输出格式,为后续开发打下更坚实的基础。

game_debug_interface.h - 通过移除 DEBUG_IF 和 DEBUG_VARIABLE 来简化复杂性

当前聚焦在scene部分,尤其是debug接口相关的代码上。在查看这部分的实现时,明确了一件事,就是之前的debug if判断和debug variable相关的机制,打算彻底放弃不再使用。那一套设计原本只是一个实验性质的尝试,但随着项目推进,发现它引入了过多不必要的复杂性,反而使整体流程变得混乱。

为了简化系统,决定不再维持那种需要维护.h文件(头文件)并且分开处理的多套系统。现阶段已经非常明确了需求:只需要能做简单直接的数据块(data dump)输出即可,因此没必要继续保留额外的一套debug控制体系。

因此,计划就是直接把这些老的debug接口代码彻底删除,完全清除掉原本用来支持这种复杂debug机制的部分。然后再思考如果没有这些东西,要怎样重新组织代码。目标是将所有需要输出的数据,都统一走新设计的简洁的数据块输出路径,而不再依赖那种冗余且难以维护的老方法。

总结下来,就是专注于降低系统复杂性,移除冗余系统,只保留一套简单清晰的数据输出机制,为后续开发打下更加高效、可控的基础。当前具体行动就是彻底删除旧debug接口代码,并着手替换成统一的简单逻辑。
在这里插入图片描述

将 DEBUG_VARIABLE 和 DEBUG_IF 的调用移到全局变量

我们需要做一些额外的工作,把当前的内容提取出来,整理成一组全局变量。现有的系统虽然已经能够运行,但它并不是最完美或终极的版本,不是所有需求都能完全覆盖。为了控制调试系统的复杂度,决定不继续增加更多复杂的结构,因为原本系统的复杂度已经有些超出了想投入的时间和精力。

为了让功能继续运作,需要把相关的数据整理成可以被访问的全局变量。这些变量需要在某处被统一声明,并且在需要时可以方便地访问。最简单的方法是,将现有使用到的变量收集起来,集中放置到某个统一的位置,比如放到一个像 game.h 这样的头文件中。只需要在那里进行统一的声明即可,这样的处理方式已经能满足当前的需求。

目前已经在某些地方初步有类似的整理趋势了,可以在现有基础上继续沿用这种方式,把需要的变量放进公共区域。这样既能保持系统的简洁,又方便后续的维护和扩展。接下来会具体看看这些变量应该放置在哪里。

game_config.h - 将 #define 更改为 global_variable

可以直接把 game_config 里面的内容拿出来,把其中的变量作为全局变量来处理。其实也可以选择不去特意删除原来的,只需要在新的位置,比如配置部分,把这些变量重新声明成全局的就可以了。可以直接简单粗暴地把它们设置为全局变量,并且公开它们,这样它们可以直接被访问和修改。最初起这样的想法时也没觉得有多大问题,所以就这么处理就行。

对于这些变量的初始赋值,可以直接设为它们原本的默认值,没有必要做太复杂或多余的处理。不需要保持原来那么冗长和详细的命名方式,因为之前的命名之所以那么长,是为了路径嵌套在变量名里,方便定位。但现在这些变量作为全局变量存在后,就不再需要那样详细地嵌套命名了,可以更简洁一些。整体上,既能保持功能完整,又能让代码变得更加清爽简洁。
在这里插入图片描述

game_render_group.cpp - 检查是否没有代码写入 game_config.h

还需要做的一件重要的事情是,移除之前用来向 game_config 写入数据的相关代码。因为现在已经不希望再去覆盖 game_config,所以必须确保这些写操作已经被去掉。检查了一下,确认之前的写入逻辑已经被删除了,这样就不会意外地改动配置内容,从而导致不可预期的问题。

主要是要确保旧的写入逻辑不会继续执行,否则有可能破坏现有的数据,带来很大的麻烦。在确认没有残留写操作之后,接下来可以继续处理后续的整理和调整工作。接下来会进一步查看当前代码状态,确保整体逻辑干净、正确,方便继续推进。
在这里插入图片描述

检查 game_config.h 的包含位置 [在 game_platform.h 中] 并将包含移至 game.h

需要确认一下 game_config 实际上是在哪里被使用的。查看了一下,发现它应该是在 game.h 文件的最顶端被包含进来的,甚至是在定义任何其他类型之前就已经引入了。这种做法显然不太理想,因为希望能够在引入配置之前,先定义好一些必要的类型,方便后续使用。

因此,应该将 game_config 的包含位置往后挪一挪,放到更合适的地方,而不是一开始就引入。同时,考虑到模块结构的清晰性,更倾向于将它放到 game.h 文件内部,而不是继续放在平台相关的部分。平台代码应该独立管理自己的内容,不需要依赖或者插手游戏逻辑这边的配置。

简单来说,就是需要调整包含顺序,避免过早引入配置头文件,保持类型定义和模块结构的清晰、合理,确保各部分职责分明,不互相干扰。接下来会继续细化调整,把整体组织得更加合理。
这个我直接在game_config.h 引入头文件了应该不影响

修正 game_config.h 中全局变量的类型和初始化器,并在其余代码中更改 DEBUG_IF 和 DEBUG_VARIABLE 调用,一一消除编译错误,直到…

首先整理了变量的定义,现在所有需要的变量都是 real32 类型,并且通过等号直接赋初值。这样一来,整体结构已经变得比较清晰,不再需要像以前那样复杂地进行调试相关的宏定义判断。原本基于 DEBUG_IF 这种条件编译的逻辑也可以去掉,直接用简单的 if 判断就可以了,进一步简化了代码。

同时,原来用于单独处理某些宏定义或条件变量的地方,现在也不再需要,因为在统一声明这些全局变量时,相关的初始化过程就会自动发生。不需要再额外写特殊处理逻辑。

在处理过程中,发现有些地方出现了未定义标识符的错误,初步判断是因为头文件引用顺序的问题。比如在 game.h 还没包含相关定义时,game_config 里的内容已经开始使用了,导致编译器找不到对应的类型定义。进一步分析后,确定是之前在 global_constants 这一部分的引用结构有点问题。

决定保留 global 这个命名词汇,因为当前阶段还没有准备好做更大范围的改动,所以在命名时依然希望能清晰标识出这些变量是全局常量。最终选择了在 global_constants 中维护这些变量,并且打算以小规模、逐步推进的方式进行整理,避免一次性改动太大导致额外的问题。

之后对代码中与调试功能相关的一些地方进行了检查,发现并没有真正发生复杂的处理或者有趣的逻辑变化,只是一些简单的重构和清理工作,属于比较基础的代码维护阶段。下一步会继续检查编译器报错的细节,确认是否还有遗漏的问题。
在这里插入图片描述

改了全局变量剩余挨着把DEBUG_IF改为if
在这里插入图片描述

… 到达 Global_Renderer_ShowLightingSamples,它被直接移动到 game_render.cpp

在处理过程中发现 show_lighting_samples 这个变量没有正确生效。检查后发现,在 game_render.cpprender.cpp 这样的文件中,并没有包含配置相关的头文件。初步推测原因是因为这些文件属于平台层,而在平台层建立两层架构系统后,这部分代码并没有自动包含配置头文件,这是可以接受的情况。

既然如此,就决定不强行让这些文件包含统一的配置文件,而是把相关的变量直接分散到各自对应的文件中。比如,show_lighting_samples 只与渲染相关,因此就直接在渲染相关的文件中定义和使用,不再依赖统一的配置头文件。这种处理方式符合模块化的思路,各个文件只负责自己的逻辑,不用依赖不必要的全局内容。

未来可能会彻底移除 game_config.h,把其中的配置变量根据实际用途分别移到合适的源文件或者模块中去。这样可以让项目结构更加清晰,避免无关模块之间互相引用,减少不必要的耦合关系。

当前阶段,为了快速调整和过渡,只需要把需要的变量移动到正确的文件中,保证它们在各自作用域内能够正常访问和使用即可。之后如果有需要,可以再进一步优化和重构。

win32_game.cpp - 移动(并之后删除)Global_Renderer_UseSoftware 从 game_config.h,并创建一个枚举来表示渲染类型,以便更好地管理 Win32DisplayBufferInWindow 的情况。

首先处理了一个全局变量 global_render_use_software,这个变量原本是用于控制渲染方式的,属于平台层的内容,因此决定将其从公共配置中移除,直接放到窗口或渲染相关的文件中,让它归属于正确的模块。这样做可以使各模块的职责更加清晰。

在整理的过程中,发现之前对变量的命名也不够准确和清晰。实际上需要区分两个不同的概念:一个是渲染是否使用软件渲染,另一个是显示时是否使用 OpenGL。这两者本质上是独立的状态组合。为了更合理地管理这些状态,重新设计了一个新的枚举类型 rendering_type,用来清晰地表达渲染和显示的不同模式。

具体定义了三种渲染模式:

  1. 渲染使用 OpenGL,显示也使用 OpenGL(最标准的路径,一切通过 OpenGL 完成)。
  2. 渲染使用软件,显示使用 OpenGL(渲染生成图像后,通过 OpenGL 显示出来)。
  3. 渲染使用软件,显示使用 GDI(完全使用软件生成图像并通过传统的 GDI 接口显示)。

注意到第四种理论组合(渲染用 OpenGL,显示用 GDI)实际上并不被支持,也不会实现,因此没有纳入处理逻辑。

为了保证逻辑正确,加入了断言检查,在运行时确保 global_rendering_type 的值一定在可接受的范围内。如果出现意外的枚举值,则断言失败,便于及时发现逻辑错误。

整理完毕后,不仅去掉了原来命名混乱、易混淆的全局布尔变量,还通过引入明确的枚举类型,让渲染模式的判断逻辑更加简洁、直观。现在代码根据 global_rendering_type 的不同取值来选择不同的渲染和显示路径,整体结构更加清晰,后续扩展和维护也变得更容易。

特别说明,将枚举的排列顺序特意按照使用频率和逻辑优先级排序,比如把最常用、最标准的 OpenGL 渲染显示模式放在第一个位置,以便代码阅读和理解时更加自然顺畅。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

win32_game.cpp - 枚举组件排序的原因是使 0 值成为默认值

为了让系统初始化更简单,把枚举类型的默认值专门设置为 0。这样所有数据在分配时,只需要简单地清零(比如 memset 置零)就能完成默认初始化,无需额外处理。这种做法非常理想,因为能够减少初始化时出错的概率,也能让内存清理后的状态天然就是合法且合理的初始状态。所以在设计时,特意将最常见、最标准的渲染模式(使用 OpenGL 渲染和显示)对应到枚举值 0

完成了渲染类型枚举的调整后,进行了编译测试,确认改动能够顺利编译通过,没有引入新的编译错误。

在完成了新的全局变量 global_rendering_type 的定义和注册后,注意到一个后续的小问题:当系统"宣布"(announce)这些全局变量时,global_rendering_type 这种枚举变量显示出来的是纯数字,而不是人类可读的枚举常量名。换句话说,现在看到的只是数值,比如 012,而不是更清楚的标识比如 RenderType_OpenGL, RenderType_Software_DisplayOpenGL 等等。

虽然这种显示方式能够正常运行,但可读性很差,不利于调试和理解。因此考虑在之后进一步优化这部分,在"宣布"全局变量时,可以改进为以符号名而不是数字的形式显示。这部分计划留到明天再具体处理。

整体来说,这次改动的要点是保证默认初始化简单可靠,同时也提前预想到未来在调试可视化时需要提升人类可读性的问题,为后续细化打下了基础。
在这里插入图片描述

奇怪我的显示怎么是浮点数

DebugType决定的

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

还是不对

在这里插入图片描述

拷贝宏替换的看看调用情况

在这里插入图片描述

在这里插入图片描述

int32吗

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

typedef 没法区分类型

typedef int32_t int32;
typedef int32_t bool32;

定义bool32 应该是为了类型对齐吧

添加一个传类型的宏

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

另外一个问题打印block 的内容

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

明天的计划:为编辑连接调试输出,可能进行清理

首先,目标是减少一些不必要的复杂性,使得代码更简洁,并且更专注于实现调试系统的可用性。通过删除一些不必要的部分,减轻了系统的负担,接下来会集中精力在实现编辑功能上,这样可以使系统变得更加易用和高效。

计划是,明天会继续进行下一步的工作,包括连接编辑功能,并清理当前的代码。这将让系统更接近一个可以正常工作的状态,使其更加稳定和可操作。通过这样的方式,可以确保调试系统更加简洁且具备良好的功能。

总体而言,删除不必要的复杂部分是为了将焦点放在调试系统的功能实现上,这样做能让系统更好地应对未来的需求,而不会在不必要的地方浪费过多精力。

问答环节

在什么时候将所有内容分成多个文件是 的,而什么时候是 的?

关于将代码分割成多个文件,什么时候是好的选择,什么时候又是坏的选择,主要取决于具体的情况,特别是对开发者个人的工作习惯和项目的要求。

首先,文件的目的是为了帮助开发者更好地组织代码。如果一个开发者觉得将所有的代码放在一个文件里更高效,那么就没有问题,完全可以这样做。没有绝对的标准来要求代码必须分成多个文件。开发者可以根据自己的需求将代码按逻辑区域分割成多个文件,方便在编辑器中同时查看和修改。不同的编辑器对代码的展示和导航方式也不同,所以根据使用的工具和开发习惯来分割代码文件是合理的。

但是,存在一些特殊的场景,特别是在大型项目中,需要特别考虑编译效率。在像一些大型引擎(如 Unreal Engine)这种项目中,文件分割非常多,可能导致编译时间长。每次修改文件时,编译器需要重新编译该文件以及它的依赖项,如果这个成本太高,分割文件的策略就需要更加谨慎。因此,在这类环境中,开发者需要非常清楚自己的代码如何分割,避免影响编译效率。

此外,开发者还需要关注项目的构建系统。如果构建系统不支持快速的增量编译,或者每次修改文件都需要重新编译大量内容,那么文件的划分就需要考虑如何减少编译次数。对某些项目来说,分割文件是必要的,因为这直接关系到项目的构建效率。

对于版本控制系统的选择也有影响。如果使用的是无锁的版本控制系统(如 Git),多个开发者可以同时编辑文件,合并变更时才会碰到冲突;而使用锁定的版本控制系统(如 Perforce)时,每次一个人编辑文件时,其他人无法同时编辑,就会限制并发性。在这种情况下,开发者需要更加注意文件的分割策略,因为文件划分的粒度直接影响团队协作效率。

总结来说,文件是否分割以及如何分割,完全取决于开发者的工作习惯、使用的工具、以及项目的具体需求。如果没有特别的构建或版本控制上的约束,分割文件应该是为了提高开发者的工作效率,而不是单纯为了遵守某些规范。

增量编译的实现依赖于对代码变化的追踪、有效的依赖管理以及对中间结果的缓存。下面是如何实现增量编译的基本步骤和思路:

1. 跟踪代码变化

增量编译的核心是能够识别哪些文件或代码部分发生了变化。通常,构建工具会使用以下方法来跟踪变化:

  • 时间戳:通过检查文件的修改时间,来判断文件是否发生了变化。
  • 文件哈希:使用哈希算法(例如 MD5 或 SHA)对文件内容进行校验,以判断文件内容是否发生了变化。
  • 源代码管理系统(如 Git):一些增量编译系统会集成版本控制工具,使用 Git 等系统的变更记录来判断文件或代码段是否发生了变化。

2. 管理依赖关系

增量编译的另一个关键是能够追踪文件之间的依赖关系,确保修改过的文件及其所有依赖的文件都会被重新编译。以下是如何管理依赖关系:

  • 构建文件或依赖图:通过生成一个文件依赖图(dependency graph)来标明哪些文件依赖于哪些其他文件。当文件发生变化时,系统可以通过这个图来计算需要重新编译的文件。
  • 头文件依赖:尤其是在 C 和 C++ 项目中,源文件依赖于头文件。如果头文件发生变化,所有包含该头文件的源文件都需要重新编译。因此,构建系统会自动管理头文件的变化。

3. 缓存中间结果

为了加速增量编译,可以缓存编译过程中的中间结果(例如对象文件 .o),避免重新编译没有变化的部分。实现这一点的方式包括:

  • 对象文件缓存:编译器将生成的中间对象文件(例如 .o 文件)存储在缓存中,如果该文件没有改变,就直接复用这些文件。
  • 增量链接:在编译过程中,链接步骤也可以采用增量方式,即只重新链接修改过的对象文件。

4. 使用构建工具和系统

许多构建工具和编译器都已经实现了增量编译。你可以通过合理配置构建系统来实现增量编译。

常见的构建工具配置增量编译的方法:
  • Make / CMake

    • Makefile:在 make 中,增量编译是基于文件依赖关系的。如果文件 A 依赖文件 B,当文件 B 修改时,make 会自动重新编译文件 A。只需要配置合适的规则和依赖关系即可。
    • CMake:CMake 本身不直接处理增量编译,但它生成的 Makefile 会自动处理依赖关系和增量编译。

    例如,在 Makefile 中定义依赖关系:

    file1.o: file1.cpp file1.hg++ -c file1.cpp -o file1.ofile2.o: file2.cpp file2.hg++ -c file2.cpp -o file2.o
    

    这表示 file1.o 依赖于 file1.cppfile1.h,而 file2.o 依赖于 file2.cppfile2.h。如果 file1.cppfile1.h 改动了,file1.o 会重新编译,而 file2.o 则不会。

  • Ninja
    Ninja 是一个专为增量编译设计的构建系统,它专注于高效的增量构建,通过增量构建可以大大提高编译速度。Ninja 通过生成依赖文件来实现增量编译,支持精确的文件级别依赖追踪。

  • Visual Studio
    在 Visual Studio 中,增量编译通常由其内部的 MSBuild 系统处理。MSBuild 会检测文件的时间戳或哈希值来判断是否需要重新编译文件。如果某个源文件修改了,它会重新编译该源文件,并更新相应的目标文件。

  • Xcode
    Xcode 也内置了增量编译功能。它通过跟踪代码文件的修改时间、依赖关系以及修改的文件来决定哪些需要重新编译。Xcode 使用了强大的编译缓存和依赖追踪系统,确保每次构建时,只编译修改过的部分。

5. 实现增量编译的步骤

假设我们要实现一个简单的增量编译系统,步骤如下:

  1. 文件哈希检查:对每个源文件计算文件的哈希值,记录它们的状态(是否已经编译,或者是否需要重新编译)。
  2. 依赖关系图:生成一个依赖关系图,标明每个文件依赖于哪些文件。
  3. 文件修改检测:每次构建时,通过文件的修改时间或哈希值判断哪些文件已经修改过。
  4. 重编译更新的部分:对于修改过的文件,以及依赖于这些文件的其他文件,执行重新编译。
  5. 生成中间文件并缓存:将编译的中间结果(如 .o 文件)存储起来,以便下次复用。

6. 增量编译的优化

为了进一步提高增量编译的速度,还可以采取以下优化措施:

  • 并行编译:同时编译多个文件,缩短整体的编译时间。
  • 预编译头文件(PCH):将常用的头文件预编译成一个固定的中间结果,以减少每次编译时的开销。
  • 增量链接:只对变更过的目标文件进行链接,而不重新链接整个程序。

总结

增量编译的关键在于高效的文件变化检测、精确的依赖关系管理和缓存机制。在使用构建工具(如 Make、CMake、Ninja、Visual Studio 和 Xcode)时,可以充分利用这些工具提供的增量编译支持,减少编译时间,提高开发效率。

这个直播中的“纹理下载”指的是“glTexImage2D”吗?

目前关于纹理下载的代码结构已经相对正确,虽然没有做过于激进的优化。纹理下载的操作涉及将纹理数据传输到显卡上,这一过程通过调用 glTexImage2D 来实现。为了优化这一过程,资产后台线程可以使用 glTexImage2D 提交纹理到显卡。

然而,在与NVIDIA的合作中发现,直接在后台线程中调用这一操作在某些情况下并不稳定,因此目前的做法是等待NVIDIA提供最佳的实现方式。此前的想法是,NVIDIA可能更倾向于让纹理上传操作在主线程中进行,而不是在后台线程中进行,但需要等待他们的建议和确认。

目前“纹理下载”这个术语并没有特别明确的定义,它只是指将纹理数据从内存传输到显卡内存的过程。在此过程中,唯一的操作是通过 glTexImage2D 完成纹理的传输。虽然在未来,可能会引入其他优化措施,比如直接写入显卡内存,或者进行更高效的纹理传输,但现在这只是一个相对基础的传输过程。

纹理下载有时也被称为纹理上传,这两个术语的使用实际上取决于视角的不同。如果从显卡的角度来看,它是纹理的上传;如果从CPU的角度来看,它是纹理的下载。因此,“纹理传输”是一个更准确的术语,能够描述这一过程的双向性。

你曾经使用过 #pragma section(…) 来将内存分组到不同的段,并读取映射文件,还是一直使用这种元编程方式?

关于进度部分、分组和读取映射文件的问题,通常没有使用这些方法,而是更倾向于采用声明式编程方式。在编程中,并不习惯去做反向读取操作。一般来说,通常采用的是传递信息的方式进行编程,而不是直接读取文件进行操作。

离题:如果你想扩展热代码重载以处理结构体变化,你是不是必须存储每个结构体的元数据以及每个分配的信息,这样你才能遍历数据,调整数据,移动东西并修复指针?

要实现热代码重载以支持结构体的变化,确实需要在每个结构体的分配中存储元数据,包含结构体的布局信息。这样做的目的是在数据更新时,可以遍历和调整数据,确保数据的迁移和结构的更新能够正常进行。然而,这个过程并不像听起来那样复杂。其实,主要是保留与结构体布局相关的信息,这部分工作并不困难。因为C语言编译器的结构相对简单,所以对于这个过程来说,并不会带来巨大的技术难题。

尽管这需要一定的工作量,通常可能需要几天时间,但这并不是一个非常繁琐或者复杂的任务。总的来说,这项工作是可行且并不超出常规的编程工作量。

你是否需要特别指定 inline 让函数内联,还是编译器在找到合适的情况下自动内联?

在C++中,inline关键字用于提示编译器将函数内联。虽然可以通过inline关键字显式标记函数要求内联,但编译器并不一定会遵循这个建议。实际上,编译器会根据自身的判断决定是否内联一个函数。即使函数没有显式标记为inline,编译器也可以选择内联这个函数。因此,inline只是一个提示,编译器的决策权是最大的,决定是否内联的最终判断由编译器来做。

inline本质上是一个建议,并不强制,编译器可以选择忽视它。而且,有些情况下编译器可能根本不支持内联,或者出于性能等原因不选择内联某些函数。对于内联编译,编译器会在编译时做出判断,通常只有在代码中涉及到较为复杂的计算或者性能要求较高的函数时,程序员才会比较关心内联的效果。

对于一些代码量较小且不涉及复杂计算的函数,编译器往往会自己决定是否内联,而程序员一般不需要过于担心是否内联。实际上,在很多情况下,编译器可以根据情况自动优化代码,内联的决定通常不需要开发者干预。

因此,inline关键字的使用并不是非常严格,编译器通常会根据自身的优化策略做出判断。如果编译器认为某个函数不适合内联,或者内联会引起性能问题,它可能会选择不进行内联。如果内联的需求非常强烈,也可以手动将函数实现为宏,或者通过其他方式优化代码。因此,内联是否生效不需要太多担忧,程序员可以更多地关注那些对性能影响较大的核心代码部分,其他部分通常不会有显著的影响。

你会尝试就地修复内存,还是将其复制到新的内存区域?

在处理内存映射时,通常需要将数据从一个内存区域复制到另一个新的内存区域,尤其是当数据结构的大小发生变化时。这是因为当数据结构变得更大时,无法直接在原地修改原有内存空间。原因在于,数据结构变大后,后面的数据也会随之发生变化,如果继续在原来的内存区域中修改,就可能覆盖原本应该保留的数据。因此,在这种情况下,必须创建一个新的内存区域,并将原数据从旧的区域复制到新的区域。

这种做法的关键点在于,无法直接在原有的内存区域进行修改,必须为数据的变更分配新的内存空间,这样可以确保数据结构变更时不会破坏原有数据的完整性。通过这种方式,数据的扩展或变化可以安全地进行,而不会导致内存访问错误或数据丢失。

因此,内存映射通常需要为新数据分配一个新的内存区域,并将数据从旧区域复制到新区域。这种方法相对可靠,也避免了直接修改原内存区域所带来的潜在风险。

编程风格 - 为什么你把函数的 return 放在前一行?只是为了让函数名在第一列吗?

这种编程风格,尤其是函数的返回类型和函数名分开写,源于个人的习惯。在早期编程时,尤其是使用C++的模板时,由于模板的类型参数通常很长,导致函数声明或定义经常需要换行,这样可以避免一行过长。随着模板的使用逐渐减少,但这种写法的习惯依然保留下来。

具体来说,习惯性的做法是在函数返回类型的下一行开始写函数名,这样的布局使得函数的结构更加清晰。尤其是在函数定义较长或者包含复杂模板类型时,分行能让代码更易于阅读和理解,尤其是在早期模板编程中,模板的类型参数很容易导致一行代码过长,不得不换行。

虽然现在不再使用模板了,但这种写法的习惯依然被保留下来。对于个人来说,这种写法让代码看起来更有结构,也符合他们对代码布局的偏好。这也体现了编程中很多细节和习惯的形成,往往和过去的使用场景和编程经验密切相关。

是的,我也不知道发生了什么,当我昨晚问关于内联的问题时,它在所有地方爆炸了。

在编程中,对于函数是否内联(inline),通常并不会过于关注或焦虑。内联函数的使用更多的是为了提醒自己,或者作为一种代码优化的提示。内联的标记可能是出于以下几种原因:

  1. 优化目的:有时候标记为内联,是因为认为这个函数可能只会被调用一次,或者在调用时需要进行优化处理。比如,将多个小函数合并,以便提高效率,减少调用开销。通过将函数内联,可以让编译器把它直接插入到调用位置,从而可能带来优化效果。

  2. 函数调用优化:对于一些很短的函数,直接内联可能会让编译器有机会优化代码。如果某个函数被标记为内联,编译器可以根据上下文决定是否将其插入到调用点,从而减少函数调用的开销。

然而,内联是否真的有效并不是一种简单的假设,尤其是在进行性能优化时。今天的编程环境中,并没有办法仅凭直觉判断是否应该内联某个函数。即使标记为内联,也不能保证编译器一定会这么做。最终的性能影响通常需要通过实际的性能测试来衡量。

因此,大多数情况下,内联标记只是作为一种代码风格和优化的提示,而不会对程序的最终性能产生直接影响。如果性能成为瓶颈,通常需要通过具体的测试来决定是否进行内联,而不是依赖单纯的内联指令。

我在手动进行循环展开时得到了巨大的性能提升,你知道为什么会这样吗?

在手动进行循环展开时,获得显著的性能提升的原因可能与多个因素有关。循环展开是一种优化技术,通过减少循环控制开销和增加指令级并行性来提高程序的执行效率。

要理解为什么循环展开会带来这么大的性能提升,通常需要查看具体的代码和展开前后的汇编语言。这可以帮助分析哪些部分的执行被优化了,从而加速了程序的运行。

在进行循环展开时,编译器需要处理更多的计算任务,并减少了在每次迭代中执行的控制语句(例如分支判断或循环计数更新)的次数。这样可以减少不必要的跳转和延迟,从而提升执行速度。

通过查看展开前后的汇编代码,可能会发现,展开后的代码能更好地利用CPU的流水线和并行执行能力,减少了循环中间的中断或等待时间。此外,某些硬件架构也可能对展开后的代码优化更为友好,因此会进一步加速程序的执行。

因此,手动进行循环展开有时能够通过直接减少循环控制的开销,充分利用CPU的并行处理能力,从而实现显著的性能提升。

相关文章:

  • 游戏引擎学习第248天:清理数据块显示
  • HQChart k线图配置
  • (七)RestAPI 毛子(Http 缓存/乐观锁/Polly/Rate limiting)
  • MIT XV6 - 1.1 Lab: Xv6 and Unix utilities - sleep
  • springboot不连接数据库启动(原先连接了mysql数据库)
  • 【Axure高保真原型】3级多选下拉列表
  • rocketmq一些异常记录
  • 基于云原生架构的后端微服务治理实战指南
  • Python中的协程(Coroutine)
  • django admin 去掉新增 删除
  • 秒杀系统 Kafka 架构进阶优化
  • 用Node.js施展文档比对魔法:轻松实现Word文档差异比较小工具,实现Word差异高亮标注(附完整实战代码)
  • [原创](现代Delphi 12指南):[macOS 64bit App开发]: NSString类型与CFStringRef类型字符串相互转换.
  • Cursor 和Trae 产品使用及MCP应用
  • 【操作系统原理07】输入/输出系统
  • 部署mongodb三幅本集群
  • 02_值相同、类型不同,用 equals() 比较为什么是 false?
  • ipa包安装到apple手机上
  • 单片机-89C51部分:5、点亮LED
  • cocos creator使用jenkins打包流程,打包webmobile
  • 从 “沪惠保” 到 “沪骑保”看普惠保险的 “上海样式”
  • 楼下电瓶车起火老夫妻逃生时被烧伤,消防解析躲火避烟注意事项
  • 人民日报任仲平:为什么中国意味着确定性、未来性、机遇性
  • 秦洪看盘|短线热点降温,A股回落整固
  • 博物馆有一项活动40岁以上不能参加?馆方回应
  • 最大规模的陈逸飞大展启幕:回望他,回望一个时代