游戏引擎学习第132天

发布于:2025-03-04 ⋅ 阅读:(13) ⋅ 点赞:(0)

记录一下使用 Git 提交模板(Git Commit Template)

因为提交太多每次提交太累之前用插件也很繁琐
查找了好久其实就一个命令就行,

git config --global commit.template <模板文件路径>

举例
在这里插入图片描述

根据这个于是可通过笔记
当前笔记保存时自动生成message填到commit-msg-template.txt
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

一个好的vscode 翻译插件和语音插件

在这里插入图片描述

在这里插入图片描述

回顾

昨天我们做了一些工作,主要是将图形资源的计算移到后台处理。正如昨天所看到的,我们现在在重新计算图形时不再出现卡顿问题,这对我们来说是个好消息。
在很大程度上,这意味着我们的程序现在几乎没有卡顿了,但仍然存在一个隐藏的问题,我们还没有完全解决它。这个问题就是……

隐藏的停顿:资源加载

今天我们要谈到的一个问题是资产加载,尤其是资源流式加载。可以看到,在游戏启动时,我们实际上是手动加载所有的位图(bitmaps)。目前我们还没有真正考虑过该如何处理这个过程。我们曾经在代码中提到过资源流式加载的概念,实际上也有一些线程处理的代码。现在的想法是,既然已经做了大部分准备工作,不如直接实现资源流式加载功能。

资源流式加载其实是非常简单的,几乎没有什么难度。基本上,所需的所有东西我们已经实现了,只需要把它整理出来并开始执行。我认为这是一个合理的决定。如果现在不做其他类型的流式加载也没关系,因为目前我们并不确定是否会有位图和音频之外的其他资源需要流式加载。但就目前来说,我觉得位图和音频文件应该是我们需要加载的主要资源,其他的暂时不需要考虑。

另外,由于我们计划做的是一个2D游戏,所以没有3D资源的需求。我们也计划使用大部分程序化动画,所以流式加载动画文件的需求也不大。也许会有一些额外的内容,比如支持MOD或下载内容的包,它们不会改变光盘上的内容,而是作为附加内容增加进来。如果有这样的需求,那可能涉及到流式加载,但目前来看,主要还是位图和音频文件。

总的来说,流式加载的实现非常简单,不需要过多的复杂处理。当前系统已经有了基本的资源流式加载框架,实际上我们已经为此做了许多准备。接下来就可以直接着手实施这个功能了,整体思路已经很清晰,接下来只需要完成代码的细节。

资源加载的注意事项 当前游戏代码中,资产加载和管理的主要边界层在于游戏代码与资产系统如何约定名称,并确保两者能够一致地引用特定的资源。

在现有代码中,这种绑定关系已经建立。例如,游戏代码使用 grass_bitmapstone_bitmap 这样的名称来引用特定的图像资源,并通过 game_state 访问它们。这些名称在 DebugLoadBitmap 函数中与磁盘上的文件路径进行关联,从而定义了代码中使用的名称与实际数据之间的映射关系。

资产引用的安全性
构建合理的资产流式加载系统,关键在于确保游戏代码引用的名称始终有效,不能因资源被释放或替换而导致崩溃。例如,如果使用指针直接指向内存中的资源,而该资源被流式系统驱逐,那么这个指针就会变得无效,从而引发问题。因此,直接使用内存指针来引用资源不是一个好的方式。

资产请求流程
游戏代码需要提供某种标识符来请求资源,并确保能够正确解析该请求,定位到实际的资源文件。例如,代码需要有一种机制,让游戏逻辑能够明确表达要获取的内容,并且资产系统能够正确地根据该请求加载或提供对应的数据。

目前的资产系统实现仍然比较简单,但已经具备了基本的名称绑定机制。下一步的优化方向是改进资产请求和管理方式,使其支持更灵活的资源加载方式,例如流式加载,以提高性能和资源管理的效率。

在这里插入图片描述

在这里插入图片描述

资源的类型

当前的资产系统已经呈现出三种重要的概念,而这些概念很可能会贯穿整个开发过程,即使代码最初是以最直观的方式编写的。这三种概念分别是:

1. 资产的多选项管理

在游戏中,某些类型的资产可能会有多个变体。例如,“草地”这个概念在程序中可能对应多个不同的位图文件,而美术资源可以提供多个版本的同一种元素。当前的实现方式是使用一个数组来存储这些不同的位图文件,并手动指定不同的草地图片来源。这种方式虽然简单,但它的逻辑概念是重要的,并且将在后续开发中继续沿用。

2. 基础位图的加载与管理

基础的位图资源加载是资产管理的核心部分,每个资源都是一个独立的对象,但可以存在多个不同的选项。当前的实现已经建立了基本的加载逻辑,但未来可能需要增强这一部分,以支持更灵活的资源加载和管理方式,例如流式加载。

3. 结构化的位图集合

在某些情况下,一个游戏对象的显示不仅仅是单个位图,而是多个位图的组合。例如,主角的动画包含多个方向(上、下、左、右)的不同动作,每个动作又由多个独立的部分(如头部、披风、躯干)组成。为了简化渲染逻辑,代码创建了一种结构化的方式,将这些位图组合在一起,使动画系统可以在不关心具体方向的情况下进行渲染。这种结构的设计对于后续扩展角色动画系统至关重要。

改进方向

目前的资产管理系统虽然简单,但基本逻辑已经建立。接下来,需要优化以下几个方面:

  1. 支持流式加载:在保证游戏逻辑正常运行的前提下,使得资产能够按需加载,而不是一次性全部载入内存,以提升性能。
  2. 增加灵活性:例如,目前草地资源的数量是硬编码的,未来可以改进为动态加载,使得引擎能够根据资源包的内容自动适配,而不是依赖手动指定。
  3. 优化硬编码部分:当前很多地方是直接使用C语言硬编码实现的,在进一步优化时,可以考虑移除这些限制,使资产管理更加灵活。

最终目标是在保持代码灵活性的同时,使资产管理更加高效,并确保后台加载能够顺利进行。下一步的重点就是围绕这些优化方向进行改进。

将资源移到一个单独的地方

当前的工作重点是将资产(assets)从代码的其他部分分离出来,使其更独立,并为后续的优化和管理提供更好的基础。

1. 资产与非资产的区分

代码中的某些部分并不涉及磁盘加载,而另一些部分则是直接从磁盘加载的资源。目前的目标是明确这一点,并将所有涉及资产管理的内容集中到一个统一的结构中。

2. 创建资产管理结构

为了实现这一点,引入了 game_assets 结构,并将所有相关的资源(如 grass)整合到其中。这样,所有的资产都可以通过 assets 结构进行访问,而不会散落在代码的不同部分。这种方式不仅让代码更加清晰,也为未来的资产管理优化提供了可能性。

3. 代码重构但功能不变

当前的调整并没有改变代码的功能,只是单纯地将原本分散的资产引用集中到了 game_assets 结构中。例如,原本分布在不同位置的 grass 资源现在被统一存放到 assets.grass,类似的调整也适用于其他资产。

4. 逐步隔离资产管理逻辑

在完成基础分离后,还需要进一步检查其他部分的代码,例如绘制例程(drawing routines)等,使得它们正确引用新的资产管理结构,而不直接依赖散落的变量。这一过程确保了资产管理的逻辑独立,减少了代码耦合,提高了可维护性。

5. 确保代码正常运行

目前的调整仅涉及代码结构的优化,因此运行效果应与之前保持一致。通过这些调整,已经初步建立了一个更清晰的资产管理框架,为未来的优化(如资产流式加载、动态管理等)奠定了基础。

在这里插入图片描述

在这里插入图片描述

将资源类型拆分

当前目标:优化基础资产加载

目前的计划是从最简单的资产类型入手进行优化,包括 背景、阴影、树木、剑和楼梯 这些资源。

1. 资产分类

为了更好地管理和优化资产加载,首先需要对资产进行分类:

  • 数组型资产(Arrayed Assets):指的是同一类资源的多个变体,例如不同种类的草地贴图。
  • 结构化资产(Structured Assets):由多个部分组成的复杂资源,例如角色动画包含头部、披风和躯干等多个元素。
  • 基础资产(Plain Assets):最简单的资源类型,例如单个的背景贴图或楼梯贴图,它们没有复杂的结构,也不需要特殊的元数据,仅仅是一个普通的位图。

当前优先处理的是 基础资产,因为它们的结构最简单,只包含基本的位图数据,以及少量的元数据(如宽度、高度和热点坐标)。相比之下,数组型资产和结构化资产需要更多的逻辑,因此后续再优化它们。

2. 优化策略

  • 抽象资源加载逻辑:将基础资产的加载逻辑进行封装,使得后续添加或修改资源时更加方便。
  • 调整数据存储结构:让基础资产的数据存储更有条理,便于后续扩展到更复杂的资源类型。
  • 减少渲染系统的依赖:确保渲染系统只需要访问统一的资源管理接口,而不需要直接操作散落的资源数据,提高代码的可维护性。

3. 下一步计划

接下来,将对基础资产的加载方式进行调整,使其更加灵活,避免硬编码,并为未来的 流式加载(Streaming)动态管理 提供基础。
在这里插入图片描述

问题:数据必须是可用的,直接引用

当前资产流式加载的问题分析

在思考 资产流式加载(Asset Streaming) 时,我们面临两个主要问题:

1. 资源数据始终占用内存

当前的 游戏资源数据 存储方式存在一个明显的问题——所有资源的数据必须始终保留在内存中。

  • 资源的 位图数据(Bitmap Data) 包含 宽度、高度 等信息,并且所有这些信息 在程序运行时必须持续占用空间
  • 即使某些资源 暂时不需要使用,它们的结构仍然会保留在内存中,导致 不必要的内存占用
  • 目前虽然 数据指针(Pointer) 可能为空,但 位图结构本身仍然占用空间,无法被完全释放。

2. 资源访问方式过于直接

另一个问题是,当前代码中的资源访问方式 过于直接,导致资源管理难以控制:

  • 所有代码直接访问资源数据(例如 game_assets 结构体中的 loaded_bitmap)。
  • 由于 访问是直接的,程序无法跟踪 谁在使用哪些资源,也无法判断 何时可以安全地释放资源
  • 资源数据的位置是固定的,如果某个资源需要被 卸载或替换,则所有直接引用它的地方都会失效,可能导致 访问无效内存(Dangling Pointer) 问题。

解决思路:引入间接引用

为了更好地管理资源,我们需要改变游戏代码访问资源的方式,使其变得更加 灵活和可控

  1. 避免直接访问资源数据,而是通过 某种间接方式(如句柄或索引) 来引用资源。
  2. 让资源管理系统能够 动态分配、加载、释放 资源,而不影响游戏逻辑。
  3. 允许资源在 后台异步加载,并在需要时进行 替换或卸载,提高运行效率。

下一步计划

  • 设计 资源句柄(Asset Handle),让游戏逻辑通过句柄访问资源,而不直接依赖内存地址。
  • 资源可以在后台加载,并在不使用时 自动释放,以优化内存使用。
  • 实现 动态资源管理,确保游戏运行时的资源调度更加高效和灵活。

提取带有ID的静态位图

游戏资产管理的改进

为了优化游戏资产的管理,逐步引入了一种 间接访问 的方式,避免了直接引用指针,从而为将来能够更灵活地进行资产流式加载做准备。具体步骤如下:

1. 引入资产ID

首先,通过引入 游戏资产ID,游戏不再直接通过资源指针访问位图(Bitmap)。而是通过 ID 来引用资产。这样可以避免程序直接操作内存中的指针,减少对具体内存地址的依赖。

  • 每个游戏资源(如背景、阴影、树木等)都有一个唯一的 ID,通过该ID来间接访问资源。
  • 游戏逻辑将不再直接操作位图数据,而是通过 ID 获取资源。

在这里插入图片描述

2. 将资源封装为ID索引

接着,定义一个 游戏资产结构体,其中包含了所有资产的ID。每个ID对应一个资源,可以通过ID获取该资源,而不再直接访问内存中的位图数据。

  • 通过在结构体中维护一个ID数组,可以将所有资产封装为一种 间接的访问方式
  • 这样,任何需要资源的地方,都只能通过ID来请求,而不需要直接访问资源的内存地址。
    在这里插入图片描述

3. 修改资源访问方式

为了实现这个间接访问机制,编写了一个 函数,该函数接受游戏资产结构体和一个ID,然后返回对应的资源位图。

  • 该函数实现了 通过ID间接访问 资源,避免了直接指针访问的方式。
  • 即使资源被卸载或者更新,也能通过这个函数来正确获取资源。
    在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4. 为未来的资产流式加载做准备

当前的修改虽然看似简单,但已经为未来实现 资产流式加载 打下基础。

  • 通过这种间接访问方式,可以更容易地实现资源的 按需加载与卸载
  • 资源将不会被直接占用内存,而是通过ID进行管理,未来可以根据实际需求动态加载资源,提升内存管理的灵活性。

5. 逐步改进的过程

修改过程中,并没有对现有代码结构进行重大改变,只是通过 间接化访问 逐步将资源管理转向更为灵活的方式。这一过程为未来能够更高效地管理游戏资产,避免内存过度占用和资源不当释放提供了必要的支持。

总结

通过引入 ID管理机制间接访问方式,游戏代码可以更灵活地引用和管理资产,避免了直接操作内存的风险。这为将来的 动态资源管理流式加载 打下了基础,使得游戏能够更高效、灵活地使用内存,并为可能的资产更新和卸载提供了支持。

其他资源类型的应急方案

我们目前面临的问题是,如何处理这些数组资产(array assets)和结构化资产(structured assets)。目前在这个部分,我们已经有一些其他的内容在进行,因此暂时不想深入讨论这些资产的处理方式。现在来看,可能还不是适合处理这些内容的最佳时机,因此不打算在这一阶段做出任何重大调整。

目前的目标是让整个流程更加顺畅,因此暂时采取一个临时的方案,而不是最终的处理方式。为了做到这一点,希望能够引用这些位图(bitmaps),确保它们仍然具有一定的灵活性(slop)。不过,在思考这个方案的合理性时,产生了一些犹豫,最终决定暂时不采用这个方法,而是直接搁置这一部分。

因此,在现阶段,先不对这些内容进行深入处理,而是等待其中一个具体实现完成后再进行优化和解释。这样做的原因是,等到具体实现完成后,再来解释会更加清晰。

接下来的重点是 PushBitmap(推送位图调用),接下来要处理的就是这个部分。

扩展PushBitmap以接受ID

目前的目标是让 PushBitmap(推送位图调用)能够接受其中一个位图资源。因此,需要做的事情基本上与当前的位图处理逻辑类似,即位图需要接收 RenderGroup(渲染组)以及一些小的位图模型参数。不同之处在于,要修改其参数,使其接收一个 game_assets_id(游戏资产 ID)。

不过,严格来说,game_assets_id 可能不是最合适的选择,应该使用一个专门的 bitmap id(位图 ID),因为这两个概念并不完全相同。尽管如此,目前还没有做出这样的区分,所以暂时可以使用 game_assets_id,等到真正需要区分时再进行调整,以免过早做出决定。

下一步,提取现有的获取位图代码,并将其调用放置在适当的位置。这里假设 render_group 设有一个专门的存储区域,用于存放渲染所需的资产,因此可以通过 asset pointer(资产指针)来获取与特定 ID 对应的位图。这样就可以顺利地进行后续的渲染。

为了确保这个方案可行,还需要检查 render_group 是否具备获取游戏资产的能力。换句话说,render_group 需要知道它应该从哪里获取这些资产。在渲染某个对象时,render_group 必须能够查询并获取相关的资源。因此,在分配 render_group 时,需要显式传递游戏资产存储的引用,使其能够访问这些资源。

目前的处理方式是,在分配 render_group 时,强制传递 game_assets(游戏资产),这样每个 render_group 都能准确知道其资产来源。这意味着,在初始化 render_group 时,就必须提供这个参数,并将其存储在内部,以便后续调用。

在实现过程中,还考虑到 game_assets 是否应该存放在 game state(游戏状态)中,但经过权衡,认为 game_assets 其实更适合存放在 transient pool(临时存储池)中。因为这些资产是动态变化的,并不会一直保持不变,因此放在临时存储池中更合理。因此,修改了相关逻辑,使其在分配 render_group 时,传递 transient state assets(临时状态资产)。

接着,在 PushBitmap 相关的代码中,调整了原有的位图获取方式,使其能够直接使用 bitmap id 进行查询。这样,在任何可以使用 bitmap id 的地方,都改为直接使用该 ID 进行渲染,而不是原来的方式。

整个修改过程是逐步进行的,每一步都确保不会影响现有功能,并且游戏在每次调整后都能正常运行、正确加载资源。

更重要的是,这一改动使得 renderer(渲染器)现在成为了负责管理资产 ID 并查询对应位图的模块。这一步非常关键,因为它是实现 后台流式加载(background streaming) 的基础。

许多人认为 后台流式加载 是一项复杂的任务,但实际上,它的核心逻辑是非常直观的。关键在于,渲染器需要能够根据 asset id 动态获取资源,而不是直接存储完整的资产数据。这种方式不仅优化了资源管理,也为后续的动态加载奠定了基础。

目前的进展是,renderer 现在可以接收 asset id 并从 asset storage(资产存储)中查询对应的位图,从而完成渲染的关键步骤。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

移除失败的情况

现在可以做的一件事是,如果某个位图(bitmap)不可用,就直接不绘制它。这样可以消除一个关键的错误情况,即尝试渲染某个对象时,指针指向了无效数据,导致更严重的问题。同时,还可以避免另一种情况:当尝试渲染一个刚刚超出屏幕范围的对象时,系统可能尚未完成流式加载(streaming),导致帧锁死(frame lockup),因为它正在等待该资源的加载完成。

通过这种方式,可以完全规避上述问题。新的逻辑是:尝试渲染时,如果资源尚未加载,则直接跳过渲染。这实际上是一种更合理的决策,因为理想情况下,流式加载系统应该始终保持在前,确保资源在需要之前就已经加载完成。如果流式加载跟不上,宁可不渲染该资源,也不要让帧率受到影响。

如果流式加载落后了,这应该被视为一个 bug,因为理想情况下,帧率不应因资源加载延迟而下降。当然,也可以采用另一种方法,即当位图尚未加载时,就让渲染器等待
(stall the frame),但通常不采用这种方式。相反,默认确保流式加载足够快,以满足渲染需求。如果偶尔出现资源未加载的情况,最坏的情况只是短暂的 缺失位图(flicker where a bitmap wasn’t loaded)。此外,还可以使用 备用位图(fallback bitmaps)等方式来进一步减少视觉问题。

具体实现上,现在可以让位图查询返回 null(或 0),如果某个资源尚未加载,渲染器就不会去绘制它。这不会引发任何严重的问题,只是不会显示该资源。

基于此优化,现在可以调整 game assets(游戏资产)的管理方式,使其不再假定所有位图都已加载,而是改为仅存储一个指针,该指针一开始可能为空(null)。当系统初始化时,位图数据尚未加载,但指针已经预留好位置。当资源真正可用时,指针就会指向正确的位图数据。

在当前流程中,这意味着在进行资源查询时,可能会返回一个 null 指针,而渲染系统可以正确处理这一情况。如果指针为空,渲染器就不会尝试渲染该资源。

现阶段,这一调整不会影响现有系统的正常运行,但仍然需要确保在调试过程中有一套 基本的资源加载机制,以便在必要时提供一个 默认资源调试位图,用于观察和验证流式加载系统的行为。因此,需要保留某些基本的资源加载逻辑,以便在调试时能够快速确定哪些资源已经加载,哪些资源仍在等待加载。
在这里插入图片描述

在这里插入图片描述

为位图分配空间

现在,我们要将资产加载功能拆分成一个单独的例程。这个例程的名字可以是“加载资产”(LoadAssets)或者其他喜欢的名字。它的作用是处理所有游戏资产的加载工作。

首先,我们将创建一个名为 LoadAssets 的函数,这个函数将接收游戏资产结构体,并执行加载操作。这里,我们用调试模式下的 DebugLoadBMP 函数来返回一个位图结构体。当前我们还没有为这些位图分配存储空间,因此这是一个问题。

接下来,我们将添加一个临时例程来为位图分配额外的存储空间。这个例程可以命名为 DEBUGAllocateLoadBMP,它会负责为位图分配空间并进行相应的加载。为了做到这一点,首先,我们通过 transient_state 来为位图分配空间。

在调试模式下,我们将调用 DebugLoadBMP,并传入需要的参数。然后,调用 allocate 为位图分配存储空间,并通过 transient arena 来确保有足够的内存。之后,返回加载的位图结果。

然而,这里有一个问题,我们需要确保 LoadAssets 函数能够正确传递参数并返回结果。为此,我们将确保 LoadAssets 函数可以接收并使用传入的参数,并在成功加载后返回位图。

在修改过程中,我们发现需要调整几个细节,比如确保每个函数都正确调用并返回所需的数据。我们还需要确保内存分配的顺利进行,例如正确使用 transient arena 来分配内存,避免内存泄漏或错误。

当这一切设置完成后,我们再次运行并测试函数,确认位图是否成功加载并正确显示。在经过调整后,我们看到位图能够按预期加载并显示出来。这意味着我们已经成功地通过新的加载流程加载了资产。

最终,我们的资产加载系统能够在需要时为游戏提供位图,而不再需要提前加载所有内容。通过这种方式,我们有效地管理了内存和加载过程,并确保了游戏的流畅性。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

一次加载一个资源

我们逐步进行代码的改进,确保每次只进行一个小的修改,以便更好地控制整体逻辑的变化。

首先,我们将现有的代码包装在 switch 语句中,这样就不会一次性调用所有的代码,而是仅调用其中的一个。接下来,我们修改 loadAssets 以接受一个 id,即 game_assets_id,并仅加载对应的资源。这意味着原本直接调用的代码将被移除,只有在适当的时候才会显式调用该函数。

为了优化资源加载的流程,我们考虑了两种方案:

  1. 让渲染器直接请求加载资源
  2. 将资源加载请求放入一个队列,稍后再进行加载

最终,我们选择第一种方案,即让渲染器直接请求加载资源。虽然这种方法在代码结构上稍显不够优雅,但这样可以确保某些后台渲染过程在进行合成时,能够强制等待所需资源加载完成。这一点对于某些必须依赖特定资源才能继续工作的渲染任务来说是必要的。当然,理论上可以从外部强制执行这一加载流程,但目前仍需进一步实验和探索最优方案。

接下来,我们对资源加载过程进行了调整,重点优化了 LoadAssets 调用过程。该调用需要 game_assets 结构体来执行相关操作,但 thread_context 这一参数的存在使得代码略显繁琐。因此,我们对其进行了简化,移除了 thread_context,并用 memoryArena 直接管理 game_assets 的资源分配。这样,game_assets_id 仅需 arenareadEntireFile 这两个必要参数,从而避免了不必要的 thread_context 传递。

在具体实现中,我们对 DEBUGAllocateLoadBMP 进行了重构,移除了 transient_state 的直接依赖,使其直接接受 memoryArena。然后,我们在 transient_state 初始化过程中,为 game_assets 分配了一个 subArena,用于存储所有加载的资源。当前为了保持保守,我们暂未将其设置为特别大的内存块,但未来会调整为适应所有资源存储的需求。

最后,我们对 readEntireFile 进行了初始化,使 game_assets 结构体能够正确使用 debugReadEntireFile 进行资源读取。这些调整确保了资源加载的过程更加合理,并为后续进一步优化提供了良好的基础。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

按需加载资源

现在,我们的资源加载流程已经简化到只需要调用 loadAsset 函数即可。

即使当前 loadAsset 仍然是同步的,我们仍然可以在 renderGroup 内部进行调用。例如,当检测到 bitmap 资源尚未加载时,我们可以直接请求加载该资源,这样在下一次访问时,资源就已经准备好了。这种方法能够提高渲染流程的稳定性,确保所需的资源可以按需动态加载,而不会因为资源缺失导致渲染失败。

此外,为了使资源加载流程更加灵活,我们需要将 loadAsset 设计成可以从外部调用的函数。这样,任何需要资源的模块都能够直接请求加载,而不需要依赖特定的调用上下文。这种调整有助于提高资源管理的解耦性,使整个系统的资源加载逻辑更加通用和高效。

理论上,现在执行代码时,资源会在需要时被动态加载,并且能够在后续调用时被正确使用。这种机制可以有效减少不必要的资源加载,提高运行时的效率。

在当前的实现基础上,如果进一步优化,我们甚至可以让 loadAsset 在更广泛的场景下使用。不过,目前的代码结构尚未完全准备好进行这一调整,因此需要进一步完善后再进行扩展。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

背景加载资源

现在,我们可以让资源加载成为一个后台异步任务

我们可以借鉴之前用于地形块(grand chunks)加载的后台任务管理方式,实现资源的异步加载。具体来说,我们可以创建一个LoadAssetWork 任务,它将包含所有执行加载所需的信息,如:

  • 资源的 IDid),用于标识要加载的资源
  • 资源管理结构体 assets,用于存储加载后的资源
  • 线程上下文(目前是个占位参数,后续会去掉)
  • 文件读取函数(可以直接从 assets 结构体中获取)
  • 文件名,即实际要加载的资源文件

这样,LoadAssetWork 结构体就具备了执行加载所需的全部信息。接下来,我们可以提取 LoadAssetWork 并在需要时填充相应的数据。由于主要需要 assetsid,所以整体逻辑相对清晰。

接下来,我们可以参考地形块加载FillGroundChunkWork)的方式,将 LoadAssetWork 任务加入到后台加载队列。这需要:

  1. 获取一个任务结构体,用于存储 LoadAssetWork 的执行信息
  2. 将该任务提交到优先级任务队列,类似 AssetLoadQueue 之类的队列
  3. 在任务执行时,调用 LoadAssetWork 来执行资源加载

这个流程要求我们使用 beginTaskWithMemory,确保任务在独立的内存上下文中执行。如果记得没错,每个任务都会有自己的 memoryArena,所以可以直接使用它,而不需要额外的分配逻辑。

对于 Work->FileName,它需要存储 assets.fileName,确保在执行任务时能够正确找到对应的文件。整个流程基本上已经接近完成,只差最后一点整理工作,使其能够顺利运行。虽然目前实现仍略显繁琐,但这是让资源加载更加高效、避免同步阻塞的重要一步。

在这里插入图片描述

继续修改背景加载资源

现在,我们需要完善任务(task)结构体,确保它包含所有必要的信息,并且正确执行资源加载。

任务结构补充

LoadAssetWork 任务中,我们已经添加了 id,但除了 id 之外,还有一些对齐(alignment)相关的信息需要加入。暂时不处理它们,但要保留相应的字段,以免后续查找旧版本代码时造成不便。
此外,我们还需要调整 work 结构体,使其包含:

  • 资源 ID
  • 文件名
  • 任务相关的其他参数(如对齐信息)

任务并行执行

目前 DebugAllocate 操作会在单独的线程上执行,但这样做存在一个问题——多线程访问 transientArena(临时内存分配区域)
我们不希望任务直接操作 transientArena,因为这样会导致多线程竞态问题,容易破坏资源管理的一致性。

优化任务的内存管理

为了解决多线程访问 transientArena 的问题,我们进行如下调整:

  1. 加载 bitmap 结果的逻辑要放到主线程,这样只有主线程会修改 AssetArena,避免多线程同步问题。
  2. LoadAssetWork 任务执行时,直接写入 bitmap,但不修改 AssetArena,等任务完成后,主线程再处理 AssetArena
  3. 去除不必要的临时变量,让 debugAllocateBitmap 直接对 bitmap 进行写入。

具体优化步骤

  1. bitmap 加载结果的处理发生在主线程

    • 任务执行时不会直接操作 AssetArena,而是等待主线程统一更新
    • 这样做可以避免资源管理的多线程冲突。
  2. 去除 transientArena 相关操作

    • LoadAssetWork 任务直接写入 bitmap,然后主线程再将 bitmap 添加到 AssetArena
    • 这样 AssetArena 只会在单线程下被修改,保证线程安全。
  3. 优化 debugAllocateBitmap 调用

    • 直接传入 fileNamealignment 参数,减少不必要的额外变量。
    • alignmentXalignmentY 目前尚未处理,但可以作为后续任务来优化。

最终结果

这样,我们实现了:

  • 异步资源加载LoadAssetWork 在后台线程执行)
  • 主线程内存管理AssetArena 仅由主线程修改)
  • 减少不必要的参数传递(优化 DebugAllocateBitmap

在完成这一系列优化后,接下来只需清理代码,使其更简洁易读,就可以结束本次开发任务。
在这里插入图片描述

清理工作

现在,我们在处理 LoadedBitmap 时,需要任务系统(task system)支持,这意味着必须提供 transient_state(临时状态)

调整 LoadAsset 以支持 transient_state

LoadAssetWork 任务中,我们发现:

  1. 执行任务需要 transient_state,但 LoadAsset 之前并未直接关联 transient_state
  2. 虽然避免 LoadAsset 依赖 transient_state 会更理想,但当前实现中仍然需要它,因此不得不添加引用。

为了解决这个问题,我们:

  • LoadAsset 结构体中添加 transient_state 指针,让其可以访问 transient_state
  • LoadAssetWork 任务中,传递 transient_state 作为参数,保证任务执行时能访问所需资源。

优化 LoadAssetWork 的任务管理

调整 LoadAssetWork 以更好地处理任务:

  1. 修改 LoadAssetWork 结构,使其接收 arena(内存分配区)
    • 这样,任务可以直接在 arena 上分配内存,而不需要访问全局 transient_state
  2. 优化 AssetLoadedQueue 处理
    • 之前的 AssetLoadedQueue 可能不再必要,可以直接使用库提供的 queue 进行任务调度。

代码调整步骤

  1. LoadAsset 结构中添加 transient_state 指针

    • 这样 LoadAsset 便可以在需要时访问 transient_state
    • 但这带来了新的问题,例如代码的耦合度增加,需要后续优化。
  2. 优化 LoadAssetWork 任务的内存分配

    • 任务本身应传递 arena,避免直接操作 transient_state
    • LoadAssetWork 任务在 arena 中完成所有分配,减少对外部全局状态的依赖
  3. 使用更高效的 queue 机制

    • 之前使用 AssetLoadedQueue 来存储已加载资源,现在可以直接利用库中的 queue,减少冗余代码。

最终效果

  • LoadAsset 现在可以访问 transient_state确保任务正确执行
  • LoadAssetWork 任务更独立,通过传递 arena 来分配资源,而不是依赖 transient_state
  • 任务系统更精简,减少了 AssetLoadedQueue 的额外管理负担。

虽然目前代码还能运行,但仍有优化空间,例如:

  • 降低 LoadAssettransient_state 的耦合,可以考虑改进 task system,让其自动管理 transient_state
  • 进一步清理任务队列,确保所有任务执行时都不会影响主线程的稳定性。
    在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

现在我们进行后台加载α

资产流式加载已实现,当前进展总结

现在,我们已经成功实现了后台加载(background loading),所有的美术资源(art assets)都可以流式加载(streaming),大大提高了资源管理的效率。

当前存在的问题

  1. 人物位置错误

    • 目前角色位置显示错误,这是因为**对齐(alignment)**参数尚未传递。
    • 这个问题并不复杂,只需要额外编写一些代码,将正确的对齐信息传递过去。
  2. 资源复用(resource reuse)的问题

    • 需要进一步优化资源管理,确保资源的重复利用不会导致问题。
    • 这部分涉及一些数据结构的调整,需要额外的时间进行优化。
  3. 部分逻辑仍需完善

    • 目前虽然大部分工作已经完成,但还有一些中等复杂度(medium complexity)的任务需要解决,例如:
      • 资源管理中的引用计数或回收策略
      • 进一步优化任务队列,以减少性能开销。

下一步计划

  • 修正角色对齐(alignment),保证所有游戏对象都正确摆放
  • 优化资源管理,确保合理复用已加载的资源,减少不必要的开销。
  • 进一步优化后台加载,确保流式加载的资源能够无缝衔接,提高游戏运行时的稳定性。

目前的进展已经非常接近最终目标,只需要一些细节调整,就能完成完整的流式资源加载系统

如果资源可以从外部修改(例如磁盘上的文件覆盖,甚至内存中的文件修改),并且仍然在游戏中正常工作,那有什么用处?在加载资源之后是否现在支持这种功能?因为我没有看到任何回调来检查文件是否被修改,因为你对光滑速度的重构对我来说太快了。

关于资产热加载(Hot Loading)的讨论

目前的资产管理系统尚未支持热加载(Hot Loading),即在游戏运行过程中检测外部文件变更并自动更新资源

如何实现热加载

  • 定期检查文件修改时间(timestamp)

    • 可以遍历已加载资源,检查文件的修改时间是否发生变化。
    • 如果时间戳不同,就重新加载对应资源,从而自动应用外部修改
  • 文件覆盖(overwrite)与内存修改(in-memory update)

    • 文件层面:如果磁盘上的资源文件被覆盖,系统可以检测到变化,并在下一帧重新加载。
    • 内存层面:如果资源已被加载,理论上可以支持直接替换内存中的数据,但要注意线程安全和同步问题。

当前不计划实现热加载的原因

  1. 项目开发方式的限制

    • 目前的开发模式是每天仅进行一小时的直播编程,开发进度较慢。
    • 由于时间有限,热加载的优先级较低,暂时不会投入资源去实现。
  2. 游戏资源的创作流程

    • 美术资源是由美术师独立制作,而且他们无法运行游戏,因为游戏本身还在开发中。
    • 由于美术师并不直接参与游戏调试,热加载对他们的帮助有限,因此目前没有实际需求
  3. 未来的可能性

    • 如果未来项目开发节奏加快,可以考虑实现热加载功能,以便美术师和程序员能够更快迭代游戏资源。
    • 目前暂不计划,但可能会在更远的开发阶段加入这一特性。

总结

  • 热加载可以通过定期检查文件修改时间实现,但当前并未支持。
  • 由于开发时间受限,且美术师并不直接运行游戏,该特性优先级较低,不会马上实现。
  • 未来如果有需求,可能会加入热加载机制,以提升开发效率。

在load_asset_work中,文件名的内存在哪里存储?

关于 load_asset_work 中文件名的存储

load_asset_work 任务中,并没有单独为文件名分配内存,文件名仅仅是字符串常量,它们直接存储在**可执行文件(executable)**中,并随程序一起加载和使用。因此,并没有额外的内存分配来存储文件名。

未来不会使用文件名存储

  • 当前的文件名仅是临时方案,在后续开发中,当资源打包(asset pack)完成后,就不再需要文件名
  • 文件名的作用会消失,资源将直接从打包数据中加载,而不依赖外部文件路径。

如果需要存储文件名

虽然当前不需要额外存储文件名,但如果想要存储,也可以很简单地实现

  • 任务有一个 Task Arena(任务内存池),它是一个临时的 scratch space(临时缓冲区)。
  • 可以在任务内存池中存储文件名,通过 push 操作将其复制到 Task Arena 中,这样每个任务都会有自己的独立字符串副本。

示例代码:

work->FileName = PushString(Arena, OriginalFileName);

这样,文件名会被复制到任务的临时存储区域,而不是直接引用全局字符串常量

为什么目前不做这个改动?

  • 文件名不会长期存在,最终它们会被资源包机制替代,所以优化它的存储并没有太大意义。
  • 潜在的热加载问题:如果未来加入代码热重载(hot reload),全局字符串可能会被移动,导致后台任务引用的字符串变得无效。如果文件名仍然存在,那时才可能考虑将其存储到任务专用的内存池中。
  • 当前开发阶段不需要:既然文件名只是短期解决方案,就没必要对它们进行额外存储或优化。

总结

  • 文件名目前是字符串常量,存储在可执行文件中,没有额外分配内存。
  • 未来资源打包后,文件名将不再使用,因此不需要考虑长期存储。
  • 如果需要存储文件名,可以将其复制到任务的临时内存池(Task Arena)中,但当前阶段没有必要。
  • 如果未来实现代码热重载,可能会考虑优化文件名的存储方式。

如果强制将渲染推送出渲染队列顺序,给他更多控制权,是什么样的权衡?

关于调整渲染推送顺序的权衡

当前的问题涉及渲染队列(rendering queue)的顺序控制,具体而言,是是否允许更强的控制力来强行改变渲染推送的顺序。但由于问题本身并不清晰,因此这里从多个角度分析可能的权衡点


1. 渲染队列的作用

渲染队列的主要目的是按照一定顺序组织渲染命令,通常遵循以下原则:

  • 按深度排序(Z-Order):避免前面的对象遮挡后面的对象,确保正确的视觉层次
  • 按材质/纹理排序:减少状态切换(state changes),提高渲染效率。
  • 按绘制优先级排序:某些元素(如 UI)可能需要特定顺序渲染。

一般来说,渲染队列是提前整理好的,而不是在渲染时动态调整,以提升性能并减少 CPU 负担


2. 强行改变渲染顺序的影响

如果允许某些逻辑强行插入或改变渲染顺序,会带来优劣势

✅ 优势
  1. 更灵活的控制

    • 允许特定的渲染逻辑插入,比如:
      • 后处理效果(Post-processing)
      • 特殊叠加效果(如光晕、粒子、遮罩)
      • UI 元素的动态调整
  2. 更好的调试能力

    • 在调试模式下,可以调整某些对象的渲染顺序,单独查看特定层级的渲染,便于分析渲染问题。
  3. 动态适应需求

    • 可以根据游戏逻辑或优化需求,动态调整某些元素的渲染顺序,比如:
      • 某些场景需要优先绘制远景,以提高填充率(fill rate)。
      • 需要在某些情况下临时改变 UI 层级

❌ 劣势
  1. 可能破坏渲染优化

    • 现代渲染通常会批量处理(batch rendering),如果随意调整顺序,可能会:
      • 增加GPU 状态切换(state changes),导致性能下降。
      • 影响 GPU pipeline 的效率,可能导致卡顿GPU 低效工作
  2. 可能引入 Z-Order 相关问题

    • 由于深度缓冲(Z-Buffer)通常会按照一定顺序渲染,如果随意插入对象,可能会导致:
      • 不正确的遮挡关系(前面的对象被后面的对象覆盖)。
      • 透明物体的错误排序(透明对象通常需要按照从远到近的顺序渲染)。
  3. 增加复杂度

    • 需要额外的逻辑来管理这些“强行插入”的渲染任务
      • 如何定义优先级?
      • 哪些对象可以绕过正常渲染顺序?
      • 如何保证最终渲染结果正确?
    • 可能导致代码维护难度增加

3. 可能的折中方案

如果确实需要强行调整渲染顺序,可以考虑以下几种折中方案:

  1. 使用多个渲染通道

    • 例如将普通物体、透明物体、特殊效果、UI 分成不同的渲染通道,然后在每个通道内仍然保持合理的渲染顺序:
      RenderQueue opaqueObjects;
      RenderQueue transparentObjects;
      RenderQueue specialEffects;
      RenderQueue UIElements;
      
    • 这样可以在不同通道内自由调整,但不会破坏整个渲染流程。
  2. 引入“特殊渲染任务”

    • 允许某些任务手动指定优先级,但需要有合理的规则
      struct RenderTask {
          int Priority;   // 0: normal, 1: high-priority, 2: UI
          void (*RenderFunction)();
      };
      
    • 高优先级任务可以插入队列,但不会随意破坏其他优化机制。
  3. GPU 侧处理

    • 例如使用 Compute Shader 或者 Deferred Rendering,推迟某些特殊任务到 GPU 侧统一处理,而不是在 CPU 端随意插入命令。

4. 结论

  • 改变渲染顺序的主要好处更灵活的渲染控制,适用于特殊效果和 UI 需求。
  • 主要风险在于可能破坏渲染优化,影响 GPU 效率,增加代码复杂度。
  • 合理的折中方案使用多个渲染通道特殊任务队列,确保灵活性的同时不影响整体性能。

最终是否要允许“强行插入渲染任务”,取决于实际需求,如果没有特殊需求,通常遵循渲染队列的顺序是更好的选择

你说我们在一个小时内完成了资源流式传输,但我们是否需要多次通过它?如果需要,为什么?

关于资源流式加载的进一步优化

当前的资源流式加载(asset streaming)虽然已经完成了基础部分,即实现了延迟加载(deferred loading),但整体来说,这只是第一步,后续仍有许多需要完善和优化的地方。


1. 现阶段的资源流式加载状态

  • 资源可以按需加载,不需要在游戏启动时一次性加载所有资源。
  • 减少了初始内存占用,提高了游戏启动速度和运行效率。

然而,目前的实现仍然存在以下限制

  1. 没有资源回收机制

    • 目前所有加载的资源都会一直保留在内存中,不会被自动释放
    • 如果所有资源的总大小超过可用内存,游戏可能会崩溃或者性能下降。
  2. 缺少内存预算控制

    • 目前的实现假设系统有足够的内存来存储所有资源。
    • 但在实际应用中,可能需要限制资源占用的最大内存,比如:
      • 限制游戏最多使用 256MB1GB2GB 资源内存。
      • 当内存超出预算时,自动**驱逐(evict)**不常用资源。
  3. 尚未支持复杂对象的流式加载

    • 当前只实现了**基本资源(如纹理、音频等)**的流式加载。
    • 但更复杂的对象(如动画、网格、关卡数据)仍需要进一步扩展

2. 为什么需要多个加载阶段(Multiple Passes)

在资源管理的优化过程中,一个单独的资源加载步骤是不够的,需要多个阶段来实现完整的流式加载系统,原因如下:

(1)资源回收机制(Eviction)
  • 目前资源一旦加载,就不会自动释放,这会导致内存持续增长
  • 需要一个资源驱逐策略,当资源占用的内存超过预算时:
    1. 优先回收不常用资源(如最近未使用的贴图)。
    2. 保留关键资源(如正在显示的 UI 纹理)。
    3. 预加载即将需要的资源(根据玩家位置、摄像机方向等)。

示例

if (CurrentMemoryUsage > MaxMemoryBudget) {
    EvictLeastUsedAssets();
}

(2)智能内存管理
  • 目前所有资源都保留在内存中,但可以优化为动态调整
    1. 低内存设备(如移动端)可以使用低分辨率贴图
    2. 高性能设备(如PC)可以动态加载更高质量的资源
    3. 使用分层存储(如 SSD 缓存 + RAM),提升加载效率。

示例

if (DeviceType == LOW_MEMORY_DEVICE) {
    LoadLowResolutionTextures();
} else {
    LoadHighResolutionTextures();
}

(3)支持更复杂的资源对象
  • 当前系统仅支持基本资源加载(如纹理、音频)。
  • 下一步需要支持更复杂的对象,如:
    1. 动画数据(Streaming Skeletal Animations)
    2. 物理数据(如关卡碰撞信息)
    3. 网格数据(高模 & 低模切换)
    4. 场景切换(不同关卡的无缝加载)

3. 后续的优化方向

为了让资源流式加载系统更加完善,需要增加以下功能:

功能 当前状态 优化方向
资源延迟加载 ✅ 已完成 -
资源驱逐(Eviction) ❌ 未实现 需要自动回收不常用资源
内存预算控制 ❌ 未实现 需要设定最大内存占用
动态分辨率切换 ❌ 未实现 根据设备性能动态加载不同质量资源
复杂资源支持 ❌ 未实现 需要支持动画、网格、物理数据
预加载优化 ❌ 未实现 需要提前加载即将使用的资源

4. 结论

虽然基础的资源流式加载已经实现,但系统仍然不完整,需要多个阶段的优化来实现:

  1. 添加资源回收机制,确保不常用资源可以自动释放。
  2. 设定内存预算,保证游戏在不同硬件环境下都能流畅运行。
  3. 支持更复杂的资源类型,提升整体资源管理能力。

只有完成这些优化,资源流式加载系统才能真正高效、稳定地运行。

你检查资源是否已经加载的地方,如果一个资源加载时间非常长,是否会再次检查它是否已加载,并尝试重新加载该资源?我不确定这个问题是否清楚。

关于资源加载检查机制的总结

目前的资源加载系统没有实现智能的加载状态检查,导致同一资源可能被重复加载。以下是详细的分析和改进计划:


1. 现有问题:重复加载

  • 目前,当调用 load asset(加载资源)时,并没有标记该资源是否已经被加载。
  • 如果一个资源的加载时间较长,当前系统可能会尝试重复加载相同资源,即便该资源已经在加载队列中,这样会导致重复任务被触发。

2. 可能的问题

  • 如果多个任务同时尝试加载相同资源,虽然当前系统中资源加载的任务数量较少,还不会立即造成严重问题,但在资源数量增加时,这种重复加载会造成不必要的性能浪费,并且可能导致内存浪费
  • 举例来说,如果尝试加载32个树木资源,系统可能会由于没有判断资源状态,导致同一资源被重复加载多次,进而影响性能。

3. 解决方案:引入资源状态标记

为了避免重复加载,可以引入资源的状态标记机制。具体做法如下:

(1)引入资源句柄(Asset Handle)
  • 通过为每个资源引入一个资源句柄asset handle),该句柄可以存储该资源的加载状态。
  • 资源句柄会标记该资源的状态,例如是否正在加载、是否已经加载完成等。
(2)队列标记(Queued Status)
  • 当一个资源被加载时,首先会检查它的当前状态。
  • 如果该资源正在加载,系统会将其标记为已加入队列(queued),并避免重复触发加载任务。
  • 这样,在资源正在加载时,其他任务会跳过这个资源,避免多次加载。
(3)改进后的加载流程
  • 当调用加载任务时,会首先检查该资源的状态,如果已经在队列中,则不会重新启动加载任务。
  • 如果资源加载完成,系统会将其标记为已加载,并允许后续的任务继续访问该资源。

示例代码:

// 定义资源状态枚举
enum asset_state {
    AssetState_Unloaded, // 资源未加载
    AssetState_Queued,   // 资源已加入队列
    AssetState_Loaded,   // 资源已加载
};

// 资源句柄结构
struct asset_handle {
    asset_state State;   // 当前资源的状态
    loaded_bitmap *Bitmap;  // 资源加载后的图像数据
};

// 加载资源函数
void loadAsset(asset_handle& handle) {
    // 如果资源已经加载,跳过加载
    if (handle.State == AssetState_Loaded) {
        return;
    }

    // 如果资源正在加载中,加入队列,防止重复加载
    if (handle.State == AssetState_Queued) {
        return;
    }

    // 将资源标记为已加入队列,避免多次加载
    handle.State = AssetState_Queued;

    // 执行资源加载任务(假设loadResourceFromDisk()是加载资源的具体实现)
    loadResourceFromDisk(handle);

    // 资源加载完成后,更新状态为已加载
    handle.State = AssetState_Loaded;
}

// 资源加载函数的实现(假设是从磁盘加载)
void loadResourceFromDisk(asset_handle& handle) {
    // 实际加载资源的代码
    // 在这里加载资源并初始化Bitmap
    handle.Bitmap = loadBitmapFromDisk();
}

// 示例:从磁盘加载图像数据
loaded_bitmap* loadBitmapFromDisk() {
    // 这里实现从磁盘加载图片的代码
    // 返回加载后的Bitmap指针
    return new loaded_bitmap();  // 假设这是加载后的图片
}

4. 后续改进计划

  • 目前这种状态检查机制尚未实现,在明天的工作计划中,可能会引入asset state这样的概念,将资源的加载状态和任务队列管理更加清晰地组织起来,避免重复加载带来的问题。
  • 资源状态标记可以帮助确保资源只会被加载一次,避免由于多次加载同一资源导致的内存和性能问题。

5. 小结

目前的资源加载系统没有考虑到资源状态的管理,可能会导致重复加载问题,尤其是资源加载时间较长时。为了改进这一点,可以通过引入资源句柄和队列标记,确保每个资源只有一个加载任务,避免重复加载,提高系统的效率和稳定性。
在这里插入图片描述

你认为地形也是一种资源,可以进行流式传输吗?

在讨论资源流式加载时,地形和其他资源的处理方式有所不同。地形是动态生成的,因此并不像传统的静态资源那样加载。在这种情况下,地形并不以通常的方式进行流式加载,而是通过实时生成来处理。这意味着地形不需要通过加载队列来管理,它不依赖于外部的文件系统或磁盘读取,而是根据需要动态创建和调整。这种动态生成的方式与传统的资源加载方法有所区别,主要是在于其生成方式和使用时的内存管理上。

为什么要使用“if (Task) { work }”而不是“if (!Task) return; work”?

有一个问题是关于代码结构的,具体来说是为什么选择某种写法而不是另一种写法。之所以不使用某些特定的代码结构,是因为始终希望函数能够有一个清晰的退出点。这样一来,如果将来需要在函数结束后添加新代码,就可以确保这些代码会按预期执行,不会受到中途提前返回的影响。为了避免其他开发者在中途通过 return 跳出函数而影响执行流程,始终坚持使用具有明确退出点的结构,这样可以确保函数在完成时能够顺利地执行所有必要的操作。

当渲染队列是多线程时,如果给渲染队列更多控制权,强制将渲染推送出队列顺序进行渲染,可以更精细地控制渲染,这样的权衡是什么?你在直播中考虑过这个问题。

问题是关于在多线程渲染系统中,是否应该给渲染队列更多的控制权,让它在需要时强行推送渲染任务,或者如何更好地平衡渲染和其他任务的分配。这个问题的核心是,是否可以通过让渲染队列在需要时夺取其他线程的任务来提高渲染性能。

我的看法是,目前并不需要这么做,因为在系统中渲染的性能已经表现得相当不错。虽然确实有多个线程在运行,比如渲染线程和执行其他后台工作的线程(例如处理地面瓦片或加载资源的线程),但我认为目前并不需要通过让渲染线程去“偷取”其他任务来提升性能。

实际上,GPU 的性能还没有充分利用,渲染性能已经是瓶颈的反面了,因此,更有效的做法是集中精力在优化任务调度和如何更高效地利用硬件资源。如果未来在性能要求上有更高的需求,可能会考虑更多的优化方案,但目前来看,强行让渲染队列控制其他线程并不一定能带来显著的提升,反而可能是一种时间浪费。

总的来说,现在的渲染性能已经足够好,所以不需要通过复杂的任务盗取机制来优化渲染。

你目前在代码中预先设计了多少典型的DirectX/OpenGL管线?

目前,代码中并没有预先设计任何关于 DirectX 或 OpenGL 的典型管线。主要原因是,随着技术的发展,未来几年内,很多开发者都将转向使用 Vulkan、DirectX 12 或 Metal,而不再使用 DirectX 或 OpenGL。因此,传统的 DirectX 或 OpenGL 管线已经逐渐被淘汰,处于“过时”的状态,未来的重点会转向这些新的 API。

你如何看待微软的_vectorcall调用约定,它能提高速度吗?

关于 Microsoft 提到的 _vectorcall 作为一种调用约定来提高速度的问题,首先,_vectorcall 主要是针对函数调用时,尽可能使用向量寄存器(例如 SIMD 寄存器)来传递参数。这样做的目的是提高性能,尤其是当函数需要传递大量数据时。

然而,通常情况下,函数调用约定已经规定了大部分的参数传递方式(通常通过寄存器传递参数,直到某个数量上限)。_vectorcall 的核心区别在于,它通过更高效地使用寄存器,减少了栈的操作。此举的主要考虑是寄存器的使用和释放之间的权衡,通常涉及的是栈的推送和弹出操作的开销。

从个人角度来看,这类优化并不特别吸引人。我的想法是,如果频繁地进行函数调用,导致调用约定成为一个性能瓶颈,那可能是代码结构存在问题。这类问题通常可以通过重新设计代码结构来避免。因此,虽然 _vectorcall 可能在某些情况下有效,但我认为它通常是为了解决一些本不该出现在代码中的问题,可能更好通过代码重构来解决。

当然,如果经过分析发现性能瓶颈确实是由于栈操作(推送和弹出)导致的,_vectorcall 可能会带来提升。所以,从这个角度看,_vectorcall 也有它的价值。但总体来说,这个概念其实已经存在了一段时间,且在多数情况下,并不是必须的优化。

https://learn.microsoft.com/en-us/cpp/cpp/vectorcall?view=msvc-170

__vectorcall 调用约定概述

__vectorcall 调用约定指定在可能的情况下将函数的参数通过寄存器传递。__vectorcall 使用比 __fastcall 或默认的 x64 调用约定更多的寄存器来传递参数。该调用约定仅在支持 Streaming SIMD Extensions 2 (SSE2) 及以上版本的 x86 和 x64 处理器的原生代码中得到支持。使用 __vectorcall 可以加速那些传递多个浮点数或 SIMD 向量类型的参数,并且执行依赖于这些参数加载到寄存器中的操作的函数。以下列出了 __vectorcall 在 x86 和 x64 实现中的一些共同特征,具体的差异将在后面详细说明。


调用约定的一般要求和功能

命名约定
  • 函数名后缀有两个“@”符号,后面跟着参数列表的字节数(以十进制表示)。
大小写转换约定
  • 不进行大小写转换。
编译选项
  • 使用 /Gv 编译器选项时,会将模块中的每个函数都编译为 __vectorcall,除非该函数是成员函数、声明时使用了冲突的调用约定、使用了变长参数列表,或其名字是 main。
三种寄存器传递的参数类型
  1. 整数类型:它必须适应处理器的原生寄存器大小(例如,x86 为 4 字节,x64 为 8 字节),并且可以转换为寄存器长度的整数类型后再转换回来,而不改变其比特表示。比如,x86 中可以转换为 int 类型的 char 或 short。
  2. 向量类型:包括浮点类型(如 float 或 double)或 SIMD 向量类型(如 __m128 或 __m256)。
  3. 同质向量聚合类型(HVA):由最多四个具有相同向量类型的数据成员组成,HVA 类型的对齐要求与其成员的向量类型相同。
成员函数
  • 对于类的非静态成员函数,如果函数是分离定义的,那么不需要在定义时再次指定调用约定。

x64 平台上的 __vectorcall

在 x64 上,__vectorcall 扩展了标准的 x64 调用约定,以便利用更多的寄存器。整型参数和向量类型参数会根据在参数列表中的位置映射到寄存器。HVA 参数会分配给未使用的向量寄存器。

  • 整数类型参数:如果是前四个参数中的整数类型参数,分别传递给 RCX, RDX, R8, 或 R9 寄存器。隐藏的 this 指针被视为第一个整数类型参数,并通过寄存器传递。
  • 向量类型参数:前六个参数中的向量类型参数传递给 XMM0 到 XMM5 寄存器。
  • HVA 类型参数:数据成员按顺序分配给未使用的 XMM0 到 XMM5 或 YMM0 到 YMM5 寄存器。
  • 返回值:整数类型的返回值通过 RAX 返回,向量类型的返回值通过 XMM0 或 YMM0 返回。

x86 平台上的 __vectorcall

在 x86 上,__vectorcall 调用约定遵循 __fastcall 调用约定,用 SSE 向量寄存器来传递向量类型和 HVA 参数。

  • 整数类型参数:前两个整数类型参数依次放入 ECX 和 EDX 寄存器,隐藏的 this 指针通过 ECX 传递。
  • 向量类型参数:前六个向量类型参数通过 XMM 寄存器传递(XMM0 到 XMM5)。如果没有足够的寄存器,参数会通过栈传递。
  • HVA 参数:数据成员按顺序分配到 XMM0 到 XMM5 或 YMM0 到 YMM5 寄存器,如果寄存器不足,会通过栈传递。
  • 返回值:整数类型的返回值通过 EAX 返回,向量类型的返回值通过 XMM0 或 YMM0 返回。

__vectorcall 示例

x64 示例
__m128 __vectorcall example1(__m128 a, __m128 b, __m256 c, __m128 d, __m256 e) {
   return d;
}

该例子演示了如何通过 __vectorcall 在 x64 中传递多个向量参数。

x86 示例
__m128 __vectorcall example1(__m128 a, __m128 b, __m256 c, __m128 d, __m256 e) {
   return d;
}

此例子展示了如何在 x86 上使用 __vectorcall 传递整数和向量参数。


总结

__vectorcall 是一种优化调用约定,专为高效地传递大量浮动点和向量数据而设计,尤其是在使用 SIMD 指令时,可以提高函数调用的效率。它通过使用更多的寄存器来减少内存访问次数,从而提高性能,但需要注意,它只在支持 SSE2 及以上版本的处理器上可用。

操作系统是否有可能丢失你请求的内存?

关于操作系统是否可能丢失你请求的内存,答案可以说是有可能的,但这种情况通常是由于操作系统本身存在 bug。具体来说,如果操作系统的内存管理出现了问题,比如在分配内存时未正确更新页表(page tables),那么当进程被调度回来时,操作系统可能会错误地使用错误的内存地址布局,导致问题发生。

但是如果这种情况发生,通常会导致程序崩溃。因为操作系统无法正确映射内存地址,进程会访问到无效的内存区域,这将触发错误,进而使得程序崩溃。这类问题通常是操作系统的严重 bug,一旦发生,很难修复并且会导致系统的不稳定。因此,如果出现这种问题,基本上就无法正常运行,操作系统的内存管理部分必须修复。

你能详细解释一下OpenGL和DirectX的“衰退”吗?

OpenGL和DirectX的“衰退”主要是因为它们所采用的模型相对过时,无法有效地映射到硬件上。这些传统的图形API主要基于设置单个状态的方式进行操作,这种方式并没有充分考虑现代显卡的需求。显卡更需要的是一种更结构化的、与硬件实际工作方式更加一致的接口。

OpenGL和DirectX的旧版本在驱动程序中需要做大量的转换工作,以将API调用转化为显卡能够理解的操作。这种转换的过程引入了很多不必要的开销,影响了性能和效率。

为了应对这种问题,Vulkan和DirectX 12等新一代图形API应运而生。这些API的设计灵感部分来自于早期的Mantle和Metal图形API,它们的目标是更加直接地与硬件交互,减少不必要的抽象层,从而实现更高效的性能。通过这些API,开发者能够更细粒度地控制图形渲染的各个方面,而不需要依赖传统的图形状态设置模型。

总结来说,Vulkan和DirectX 12的出现是为了优化硬件与软件之间的接口,它们消除了大量过去由驱动程序承担的工作,使得显卡可以更加高效地执行渲染任务,提升了图形性能和减少了驱动程序的复杂度。

有什么好的方法可以扩展资源流式传输,以支持增加的细节级别?(我想到的是,先加载一个较小版本的资源,然后再加载更大的版本)

在支持逐渐增加细节级别的资源加载时,一种常见的做法是首先加载资源的较小版本,然后再加载更大尺寸的资源。具体来说,如果要实现这一点,需要对游戏资产的加载方式进行一些调整,使其能够根据需求动态更新资源的细节级别。

首先,可以将每个游戏资产视为一个唯一的ID。如果需要处理不同细节级别的资产,可以在资源的处理系统中加入对“细节级别”的管理。例如,资源的句柄不仅需要知道该资产本身,还需要知道它当前加载的细节级别。当渲染时,游戏会根据屏幕上绘制位图的大小来决定需要什么样的细节级别。此时,游戏资产会检查当前细节级别是否符合需求:

  1. 如果当前细节级别满足渲染需求,直接返回当前资源。
  2. 如果不满足要求,则会排队加载更高细节级别的资源,同时返回低细节级别的资源,直到更高细节的资源加载完成。

这个方法相对简单,基本上就是根据屏幕大小和渲染需求,动态选择并加载合适的资源细节级别。这种方式不仅能有效地管理资源的加载,还能确保游戏的流畅运行,特别是在三维游戏中,细节级别的管理显得尤为重要。虽然在二维游戏中,细节级别的调整不那么常见,但仍然可以在某些情况下使用这种方法。

处理一个大型AAA项目中的庞大资源管线,涉及数十个软件,处理起来有多令人恼火?

处理大型AAA项目中的庞大资源管道,尤其是涉及到多种软件工具时,通常是非常令人沮丧的。这是因为许多第三方软件不仅难以操作,而且还经常带有各种各样的问题和不便。首先,这些工具往往设计得非常笨重,且很难与其他系统无缝集成。此外,它们还通常需要复杂的许可管理系统来运行,导致开发机器无法正常使用。如果许可证服务器没有安装,甚至可能无法运行该软件。尤其是在Linux平台上,很多软件并没有提供相应的支持版本,这使得问题更加严重。

这类问题让人十分烦恼,因为这些工具并不是自己开发的,而是购买的现成产品。虽然它们可能在某些领域有效,但通常会带来很多限制,且不容易绕过这些限制。随着软件版本的更新,常常需要重新适配导出插件或编写新的代码,而这本身就让人头疼。

例如,使用像Photoshop这样的软件时,虽然它的文件可以直接读取,减少了对导出插件的依赖,但许多三维软件就远不是如此,它们常常需要为不同版本的程序编写适配代码,并且每次版本更新都可能导致需要重做工作。

因此,开发者在处理大型AAA项目时,必须经历大量的麻烦和复杂问题,尽管这些问题对于最终玩家来说可能并不明显,但它们确实影响着开发过程。总的来说,这种开发工作并不轻松,充满了挑战和不愉快的经历。

今天就到这里

总结一下,今天的工作进展顺利,尽管仍然有一些事情需要继续处理,但基本上已经完成了大部分任务,尤其是在资产流式加载方面。这项任务看起来比渲染更简单,虽然会有一些调试工作,但整体来说,资产流式加载是一个相对直接的过程,完成它通常只需要几个小时。尽管有些人认为它是一个复杂的任务,实际上它并不像看起来那么困难,除非遇到一些不常见的问题,比如指针处理不当。

接下来的几天会继续做一些工作,让这个系统更加完善。虽然今天的演示看起来简便,但实际上还需要投入更多时间来解决一些复杂的问题。接下来几小时的工作会让它变得更加真实和高效。