狂吐槽:把这一集献给一家糟糕的公司
这是一个我们在直播中一起完成游戏开发的节目。没有使用引擎,没有使用库,只有我们自己。在这一期中,我们有一个非常特别的内容。我们决定将这一期献给微软,表达我们对微软的强烈不满,因为过去两天的时间,几乎都被微软的"精彩表现"占据了。
首先,我升级了工作机器,这是自2008年以来第一次升级,我从Windows 7升级到了Windows 8。然而,这个过程是极其糟糕的,尤其是字体渲染部分。平时我用的是Liberation Mono字体,它是我日常编程时使用的字体。结果,在工作机器上,使用这款字体时,所有的下划线都消失了。在使用的字号下,完全看不到下划线,只有调高字号后才会显示出来。这是因为微软的反锯齿字体渲染代码竟然无法正确渲染一个1像素宽的下划线,这简直不可思议。所以现在,我不得不去选择一种新的字体来编程,因为微软竟然连一个下划线都不能渲染好。
第二个问题,我试图将Visual Studio 2012 Professional版本从旧机器迁移到新机器上。我使用相同的安装介质,输入了相同的产品密钥,结果它却显示"无效的产品密钥"。我打电话给微软客服,他们告诉我"这是一个有效的产品密钥",但是他们却无法帮我解决问题。如果我想要解决这个问题,需要支付499美元,才能开一个支持工单,让他们查明为何他们的DRM系统无法正常工作。这是一个非常荒谬的经历,打电话时花了大约30分钟,结果告诉我:你购买了我的产品,但我们的DRM系统竟然无法在你的机器上运行,然后你还得为了解决这个问题支付500美元。
这种情况让我非常愤怒,因为我是合法购买的Windows和Visual Studio产品,微软却因为自己的问题不肯解决,还要向我收取高昂的费用。我甚至开始怀疑微软的开发人员是不是只有一个人,或者一群人,他们负责了字体渲染和DRM系统这两个完全不同的系统,可能他们甚至连基本的测试都没有做过,随便写了点代码就交付使用。
这些糟糕的体验让我对微软产生了极大的厌恶,每次使用微软的软件,都会感到非常糟糕,我甚至一度想要完全放弃Windows,转而回到Linux,尽管我现在并不常使用Linux了。有时,如果没有Photoshop需要使用,我可能真的会放弃Windows,因为相比这些问题,使用稍微差一些的系统,似乎要好得多。
最后,虽然我购买的是正版的Windows和Visual Studio 2012 Professional版,而不是免费的Visual Studio版本,但我依然被微软的糟糕服务和产品给深深打击了。我真的希望微软能够继续走向行业的边缘,彻底失去市场份额。
总的来说,这段时间的经历让我对微软的产品和服务产生了非常深的失望,也让我产生了是否以后要完全脱离Windows平台的想法。今天,我们将继续进行我们的调试代码工作,而这一期也将专门献给微软的糟糕表现。
在最后一个能够渲染字体的 Windows 版本上启动这个
接下来,我们来谈谈在Windows 7上的一些问题。我认为Windows 7可能是最后一个还能在字体渲染方面正常工作的Windows版本。谁知道呢,也许微软以后再也不会发布能够正确渲染字体的版本了,甚至可能会开始逐步从英语字母中删除一些,直到剩下它们能处理的字母为止。
我们上次停留在一个地方,那里我们已经有了一些很酷的调试定时器,并且它们运作得不错。事实上,这些调试定时器现在可以显示出一些非常棒的信息。值得一提的是,它们使用的是我所谓的"Windows安全字母",也就是新下划线。这让人惊讶的是,即便是在Windows 8上,微软也能成功渲染这些符号。虽然Windows 8在这个方面做得有些吃力,但至少它能勉强显示下划线。
我们的调试计时器系统仍然缺少一些重要功能:
尽管目前已经实现了一些功能,但我们注意到在这个早期阶段,还是有一些问题需要改进。虽然功能已经实现,但有些地方可能并不是最理想的,尤其是当真正使用它时,这些问题可能会变得更加明显。
1) 时序信息难以可视化
目前的问题之一是,信息的展示方式并不够直观,尤其是当数据变化很快时。比如,如果我们以每秒60帧的速度运行,数据显示可能会快速闪烁,导致我们很难理解所看到的内容。因此,可能需要添加一些功能,以便更简洁地查看这些值的变化情况。具体来说,我们可以通过显示最小值、最大值、平均值等基本信息来帮助我们更清晰地了解数据的变化。例如,我们可以查看过去60帧中的最小值、最大值和平均值等,这样能更好地帮助我们理解数据,而不仅仅是试图找到某些趋势。
2) 缺乏能够指出是哪段代码导致错过帧或其他性能问题的功能
另一个与此密切相关的问题是,如果出现突然的性能波动或峰值,我们目前无法有效地察觉到这些变化。例如,偶尔会看到某些数值快速跳动,或者画面略微抖动。这些可能是重要的性能波动,可能是某些操作引起的,但由于目前游戏的复杂度较低,主要是一些测试代码和引擎的基本功能,所以发生严重性能问题的可能性较小。然而,随着游戏的开发进展,出现某些导致帧率下降的性能瓶颈的可能性会增加,因此我们希望能够追踪到这些问题,尤其是在我们错过帧率时,能够明确是谁导致了这一问题。
当前的系统只能看到偶尔的帧率波动,但无法追溯到具体的原因,因为这些数据已经过去且消失了。为了能够更好地理解这些问题,应该引入一种图形化的方式,帮助我们清晰地看到帧率的变化情况,以及在哪一帧发生了帧率问题。这样可以更直观地查看帧率的变化,了解哪些帧是正常的,哪些帧存在问题。我们还希望保持数据的可视化呈现,尽量避免使用Windows不支持的字符,避免出现像下划线等问题。如果确实需要使用下划线来表示,我们甚至可以考虑联系微软的技术支持,看看他们是否能解决这个问题,毕竟他们曾要求支付高达500美元的费用来解决其他问题。
我们希望保持一个更长的性能计数器历史记录
在代码中,我们有一个开始的调试输出系统,它通过 overlay cycle counters
打印出当前的循环计数器信息。这个过程是在调试系统中读取数据并重置计数器时进行的,输出结果被送到调试文本缓冲区,这样我们就能同时看到计数器信息。
但是,当前的做法是,在渲染过程中直接提取并重置这些数据。为了更好地实现历史数据的追踪,我们需要将数据提取的部分与渲染过程分离开来。具体来说,渲染过程将只负责显示已存储的信息,而不再参与数据的实时提取和重置。
为了做到这一点,我们打算将数据提取的功能移到一个独立的模块中,专门用于处理调试检查。这样,我们就能开始保持更长时间的数据历史,能够追踪到过去的一些信息。这将使得渲染过程能够变得更加复杂,随着时间的推移,展示更多的调试信息,并能够呈现一些有趣的动态变化。
这一步其实是之前讨论过的目标之一,我们已经准备好开始处理这个时间维度的需求,接下来就可以着手实现这一功能了。
我们有兴趣跟踪平台特定层中发生的事件,因此,调试系统的一部分必须位于该层
现在,我计划提前进行一些预测性操作,因为我对一些事情已经有了预判。过去有很多项目经验,因此我倾向于提前解决问题,而不是等到后来再发现。这是我认为我们可能会遇到的问题,并且我猜测在开发过程中我们可能会希望调试系统能够可视化一些大部分发生在平台独立代码之外的事情。
例如,我们可能希望窗口系统、Linux、Mac OS X、Raspberry Pi 或其他平台能够向调试系统提供关于当前帧的信息。为了实现这一点,我打算提升调试框架的功能,使得平台层本身能够调用这个调试框架,这样就可以确保当平台层知道一帧已经完全完成时,能够正确地将信息存入系统中。
这样,我们就能避免调试信息显示为下一帧的情况,因为当前帧的调试信息是在 update
和 render
过程中重置的,而更新和渲染的结果是在屏幕上显示后才发生的。通过这种方式,可以确保调试信息的同步和正确排序,避免出现显示顺序错误的问题。
平台层会通知调试系统每一帧的结束
在这段内容中,目标是优化调试系统,使得每一帧的调试数据可以在合适的时间进行记录和更新。具体来说,计划在程序的主循环中添加调试重置功能,尤其是在帧计数器检查之后。这一操作可以通过调用一个新的函数指针来完成,这个函数会接入调试系统,完成相应的重置操作。
在重置操作之后,可能会删除掉之前仅仅输出到控制台的帧数信息,因为现在调试系统已经有了更好的支持。接下来,调试信息将被传递给新的调试框架进行处理。所有这些信息,包括每帧的毫秒数等,将被传递并记录下来,而不再仅仅是输出到控制台。
此外,还有对计算帧时间的部分进行反思和改进,之前的计算方法可能存在问题,特别是时间计算的时机。为了准确记录每一帧的时间,需要确保所有的时间计算都在合适的时机进行,而不是在窗口系统更新之后。最终的目标是能够更加准确地跟踪和记录每帧的性能数据,以便后续优化和调试。
平台层还会通知调试系统其他一些事件
目的是通过记录多个关键时刻的数据来优化帧的调试信息。具体来说,目标是在每一帧结束时,记录多个"标记点"(strobes),即在特定的时间点获取一些计时信息,并将这些信息传递到调试系统中。
首先,在每一帧结束时,计划更新一个计时器(last counter),并通过记录几个关键点的时间差来计算每一帧的执行时间。这样就能为每一帧提供更多的调试信息,而不仅仅是每帧的毫秒数。这些信息将被存储在一个结构体中,包括以下内容:
- 执行准备时间(ExecutableReady)
- 输入处理时间(InputProcessed)
- 游戏更新时间(GameUpdated)
- 音频更新时间(AudioUpdated)
- 帧渲染完成时间(FramerateWaitComplete)
为了实现这一点,设计了一个 debug_frame_info
结构体,里面存储了这些标记点信息。每一帧结束时,都会通过这些时间戳记录的值来生成调试信息,并将其传递到调试系统。为了进一步记录这些时间,定义了一个新的调试函数 debug_frame_end
,这个函数将负责记录帧结束时的各种数据。
接下来,更新了平台API,添加了一个新的调试函数 debug_frame_end
,用于从平台层调用调试函数。这样,游戏代码就能够将调试信息传递给平台层,完成调试记录的收集。
为了实现这一点,游戏的代码需要在适当的地方调用这些新的调试函数,并确保平台层能够正确接收到这些信息。还需要在游戏代码中设置必要的函数导出,确保这些调试信息能够传递到平台层,从而实现更精确的调试记录。
在代码中,还需要处理一些边界情况,例如,如果游戏没有提供调试函数,可以避免调用这些未定义的函数,避免程序崩溃。此外,去除了原来多余的FPS输出代码,将这些输出信息整合到新的调试系统中。最终,所有的调试数据都将以一种更结构化、更易于分析的方式进行输出,而不仅仅是通过打印简单的帧时间数据。
整个过程的目的是为每一帧提供更细粒度的调试信息,帮助开发人员更好地理解每一帧的执行过程,优化性能并提高调试的效率。









实现DEBUGGameFrameEnd
在平台层分配调试内存
首先,目标是能够从系统中提取调试内存。调试内存应该是一个单独的区域,不与其他内存数据共享。为了实现这一点,考虑到目前平台系统中已经有了用于临时存储的内存分配方式,计划引入第三种内存类型,专门用于调试存储。这种调试内存需要独立管理,确保它与游戏的其他功能和数据不会冲突。
具体步骤包括:
-
引入调试内存区域:计划在平台层中添加一个新的内存区域专门用于调试数据。当前,内存分为常规数据存储和临时存储,但为了确保调试信息的独立性,应该为调试存储分配一个独立的内存区域,这样就能避免调试数据与其他游戏数据的混淆或冲突。
-
可调节调试内存大小:为了确保游戏在发布版本中不会使用调试内存,计划让调试内存的大小可以动态调整。在游戏的开发过程中,调试内存可以用于存储调试信息,但在发布版本时,调试信息应被禁用,因此调试内存的大小可以设置为零。这样,只有在开发或调试模式下,调试内存才会占用实际的内存资源。
-
调试内存的独立性:为了实现内存的隔离,可以选择将调试内存区域与其他内存区域完全分开,确保调试数据不会影响到游戏的其他功能。调试内存区域将专门用于存储调试信息,并且不会与其他数据混用。
通过这些步骤,可以确保调试信息的管理更加清晰、独立,并且能够在不同版本的游戏中根据需要灵活开启或关闭调试功能。这种方法能有效地控制调试内存的使用,避免在发布版本中不必要的内存浪费,同时保留开发版本中的调试功能。

即兴设计用于跟踪计数器值的一组结构体
现在可以将调试状态隔离到一个单独的文件中,这个文件将专门用于存储调试相关的信息。具体来说,会有一个"调试状态"对象,可能会包含不同的部分,虽然具体内容目前还不确定,但是我们可以设想,调试状态可能会有一些计数器、快照等数据。
-
调试状态设计:调试状态将包含多个计数器或其他调试相关的变量。这些计数器可能会有很多个,我们可以设定一个上限,虽然数量并不重要,因为这部分数据只是用于调试,不会影响游戏的实际功能。调试状态的设计目的是为了存储和追踪这些数据,而这些数据本身的数量和具体内容可以根据需求调整。
-
调试快照:调试状态还将包含快照(debug counter snapshot)。这些快照记录了每一帧的相关调试信息。例如,可能会记录过去一百二十帧的调试数据。每个快照中会存储与当前帧相关的调试信息,而与特定帧无关的数据会存储在调试状态的其他部分。这样,调试信息就能够被清晰地组织和存储。
-
数据分离和存储:在快照中,只会存储与当前帧直接相关的调试信息,而那些不直接关联到当前帧的部分会放在调试状态对象中。这样做的目的是使调试数据更加有组织,并确保不必要的数据不会被重复存储。可能会将不同类型的数据分开存储,以便更好地管理和访问。
-
输出调试记录:现在不再需要单独的函数来处理调试输出,因为所有的调试记录将被统一地聚集在一起。假设所有的记录都可以在一个地方进行整理和输出,这样就能简化代码的结构。
-
打印调试数据:在输出部分,代码将从调试状态或调试快照中提取数据,并进行打印。虽然目前还不确定具体的输出格式,但计划是将打印功能与调试状态的管理代码结合起来,以便输出调试信息时能够更加灵活、清晰。
总结来说,目标是将调试数据和状态独立存储,确保它们在开发过程中能有效地追踪和管理,并且在游戏发布时能够方便地关闭或禁用调试功能。同时,输出的调试记录将被集中管理,避免冗余和不必要的代码复杂度。

修改 OverlayCycleCounters 来使用调试内存
现在的目标是将调试状态从系统中提取出来,并在平台层进行处理。具体步骤如下:
-
获取调试状态:系统会通过调用调试存储来提取调试状态信息。在平台层第一次进入时,会从调试存储中加载调试状态。为了防止无效的调试存储操作,只有当调试存储大小非零时,才会激活调试功能。通过这种方式,可以使用一个"开关"来控制调试状态的开启和关闭。如果调试存储被设置为零,则调试功能将被完全禁用。
-
处理调试状态:在调试存储被激活后,系统会遍历所有的计数器状态。每个计数器会包含一些信息,具体来说,计数器会存储调试过程中的各种数据,比如计数器的命中次数和周期数。为了获得这些数据,系统需要检查调试计数器的状态,并查看其快照信息。
-
使用快照数据:每个调试计数器可能会有多个快照。当前,系统选择使用快照零(snapshot zero)中的数据,并提取相关的命中计数和周期计数等信息。此时,调试状态的快照数据还没有完全整合,暂时只是获取第一个快照的数据进行验证。虽然这种方法还未完善,但足够用于当前的测试。
-
输出调试信息:通过将调试计数器的命中次数(hit count)和周期数(cycle count)从快照零中提取出来,系统可以将其打印输出。打印时,除了命中计数和周期计数外,其他调试信息(例如文件名和函数名等)应该也能正常输出。
-
验证和编译:完成上述功能后,系统需要编译并验证是否能够正确地从调试存储中提取数据并进行输出。运行时会确保所有的功能都能按预期工作。如果出现问题,需要检查变量名(例如快照的首字母是否大小写正确等)。
总结来说,整个过程的关键在于能够有效地从调试存储中加载调试状态,提取相应的计数器数据,并通过快照提供详细的调试信息,最终将这些信息输出进行验证。在这一步,使用快照零的简单方法作为测试,确保基本功能正常工作。

让 DEBUGGameFrameEnd 更新调试记录
在目前的工作中,目标是确保调试存储能够正确初始化和更新,并将计数器信息存储到调试状态中。具体步骤如下:
-
初始化调试状态:首先,系统需要判断是否启用了调试存储。如果调试存储的值为零,代码不会执行任何与调试相关的操作。因此,在更新过程中,系统需要检查是否存在有效的调试存储。如果没有,则跳过相关操作,避免进行不必要的计算。
-
更新调试记录:当系统开始更新时,需要确保调试记录能够被正确捕获和存储。在每次更新时,系统会从当前的计数器中提取信息,并将这些信息存入调试状态中。为了做到这一点,必须首先从计数器中读取当前的状态,并将这些值复制到调试记录中。
-
遍历计数器并存储数据:系统将遍历所有的计数器,并在调试状态中记录它们的当前值。在开始处理之前,系统会将计数器计数初始化为零,然后在更新过程中逐步递增,以便跟踪总共处理了多少个计数器。
-
同步调试状态和计数器数据:调试状态的记录将从源计数器(debug counter)中提取数据,并将这些数据存储到调试状态(debug_counter_state)中。这意味着,系统需要执行原子交换操作来重置计数器的值,并将其数据复制到调试状态中。
-
快照和其他信息:在保存计数器数据时,系统目前将所有信息写入到快照零(snapshot zero)中。未来可能会有多个快照,因此系统需要支持写入不同的快照数据。此外,除了计数器值外,系统还会抓取文件名、函数名、行号等其他相关信息,以便调试时可以跟踪和定位问题。
-
传递调试状态:在处理更新时,调试状态必须作为参数传递,以确保调试记录能够写入到正确的位置。每个更新操作都需要传递调试状态,以确保调试信息被有效存储。
总结来说,系统的关键在于能够正确初始化调试状态,并通过读取和更新计数器数据来维护调试记录。这些数据不仅包括计数器的当前状态,还包括其他相关的调试信息,如文件名、函数名、行号等。随着调试状态和计数器数据的更新,系统逐步完善调试功能,确保在不同的快照和更新周期中都能有效捕获调试信息。

为调试存储分配内存
在当前的工作中,目标是给系统分配适当的内存,以便能够正确地进行调试存储操作。具体步骤如下:
-
内存分配:当前代码中,调试功能还没有看到任何实际的输出,因为系统没有为调试存储分配内存。因此,首先需要确保分配适量的内存。通过修改内存分配相关的代码,确保可以为调试存储提供足够的空间。
-
设置调试存储大小:在内存分配部分,系统会设置调试存储的大小。这是通过增加一个新的内存区域来完成的,通常该区域会位于所有其他内存区域的末尾。对于调试存储的大小,并没有严格的要求,可以设为一个固定的数值,也可以设为无限大,这取决于实际需求。
-
修改内存管理器:在内存管理部分,需要增加一个新的字段,表示调试存储的大小。通过这一修改,系统可以为调试信息分配适当的内存空间。这个大小并不需要太多的考虑,可以根据实际需要调整。
-
设置调试存储:在为调试存储分配内存后,系统应该在初始化时设置调试存储的大小。这意味着可以为调试存储设置一定的内存容量,并确保在后续操作中能够使用这些内存。
总结来说,关键的修改在于内存管理部分,通过增加调试存储的分配,将其与其他内存区域区分开来,并根据需要为调试存储提供足够的空间。通过这些步骤,系统将能够正确处理调试信息并进行后续的调试操作。
调试信息现在通过新系统进行传递
现在,调试存储已经成功分配并开始工作,系统中开始输出预期的值,表明调整已经生效。接下来,调试信息系统开始正常运行,能够逐步处理并显示所需的调试数据。通过这些修改,所有的调试记录能够被捕获并在系统中正确输出,从而验证了整个调试流程的实现。
总结来说,通过为调试存储分配适当的内存并更新相关的管理代码,系统现在能够有效地收集并显示调试信息。这表明系统已经能够顺利处理调试数据,并且所有必要的调试功能已被激活并准备就绪。
将快照记录到滚动缓冲区
现在,我们开始尝试将调试快照的记录进行排序和管理。这个方法可能不太高效,特别是当快照数量增多时,可能会变得有些复杂。但我们决定尝试实现它。
为了记录这些快照,我们在调试状态结构中添加了一个快照索引字段。每当我们更新调试记录时,索引就会自增。更新完成后,调试状态中的快照索引会增加,并检查它是否已经大于或等于定义的最大快照数量。如果超过这个数量,我们将把索引重置为零,形成一个滚动缓冲区。
为了实现这个功能,我们可以设置一个最大调试快照数,比如设置为120或50。这样,每当索引超过最大值时,就会重置,从而允许在运行中不断覆盖旧的快照。
接下来,我们在调试状态中记录和捕获快照。这样,每隔一定的时间(比如每120帧),系统就会更新并输出新的快照数据,通常是每隔大约4秒钟,调试数据就会更新一次,展示新的快照信息。
总的来说,通过引入快照索引和最大快照数量的管理,系统能够动态地存储和更新调试数据,每隔一定时间就更新一次,提供持续的调试信息。



记录计时器的最小值、最大值和平均值
为了查看并分析这些调试记录的值,我们开始思考如何呈现这些数据。虽然时间有限,但我们将尝试设计一种方法来计算和展示统计信息。
首先,我们决定计算计数器的最小值、最大值和平均值。为了实现这一点,我们将创建一个调试统计结构,里面包含了最小值、最大值和平均值的字段。然后,我们遍历所有的快照,并针对每个快照中的计数器值进行统计计算。
具体步骤如下:
- 初始化统计数据结构,设定最小值和最大值为极限值,平均值为零。
- 遍历所有快照,并遍历每个快照中的计数器值。
- 在每次遍历时,我们更新最小值、最大值以及累积总和以计算平均值。
- 通过检查计数器数量(即记录的次数),我们计算最终的平均值。如果没有记录的值,则默认最小值、最大值和平均值都为零。
为了实现这些步骤,我们定义了一个名为 debug_statistic
的结构体,包含最小值、最大值和平均值。然后在代码中使用该结构体来统计和更新每个快照中的值。通过累积统计数据,最后可以得到每个计数器的最小值、最大值和平均值。
在每次更新统计数据时,程序将调用一个函数来处理并更新统计数据。为了避免未记录任何数据时产生错误或不合理的输出,我们加入了检查逻辑:如果没有任何数据被记录,统计信息将保持为零,以避免输出错误的统计结果。
通过这种方式,我们能够动态地计算并展示每个快照中计数器的统计数据。这为调试过程提供了更直观的信息,帮助了解系统在不同时间点的表现。
使用双精度来存储计时器的值
为了进一步完善调试功能,我们决定采用双精度浮点数(double
)来存储统计数据,以确保即使在面对非常大的值(如极高的周期计数)时,也能有足够的头空间处理这些值。由于这是调试代码,性能不太是重点,除非它在实际使用中变得过于繁重。
接下来,创建了一个计数器变量,用来追踪每次更新时的次数。这个计数器并不需要非常复杂,因此选择了一个合理的值来表示计数。当计数器有效时,进行统计计算,包括计算最小值、最大值和平均值。
具体步骤如下:
- 初始化统计数据 :最小值和最大值通过浮动值(
double
)进行初始化,以确保足够的精度。 - 更新统计信息 :
- 当更新时,我们会检查当前的最小值和最大值,并根据需要更新它们。如果当前值小于最小值,则更新最小值;如果当前值大于最大值,则更新最大值。
- 同时,计算并更新平均值。平均值的计算依赖于记录的计数,因此需要确保计算时有有效的计数。
- 处理计数器:通过更新计数器的值来追踪状态数量。若计数器有效,则通过累计统计来更新最小值、最大值和平均值。
- 清除无效数据:如果计数无效(例如,未记录任何值),则会重置所有统计数据。
通过这些步骤,我们可以在调试过程中动态地记录并更新每个计数器的统计信息,这样就可以轻松地查看不同快照中的状态,并对其进行分析。总之,这种方法为调试过程提供了一个灵活的统计工具,能够应对不同情况的调试需求。
打印统计数据
现在,我们准备打印出统计数据。首先,我们检查命中计数 (hit count)是否大于零,如果大于零,则打印出相关信息。为了测试,我们决定先打印平均值,这对于调试和验证代码是足够的。
具体步骤如下:
-
检查命中计数 :首先,检查命中计数的最大值是否存在,意思是检查在之前的运行中是否有任何值被记录过。如果命中计数的最大值大于零,则表示至少有一次命中记录,我们就打印出来。
-
打印统计数据 :如果命中计数有效,就打印出相关的统计数据。这一部分可以暂时简化为打印平均值,以便查看整体趋势,而不必过于关注细节。
通过这种方式,可以逐步实现输出并展示调试信息,帮助我们验证和分析系统的运行状态。
输出看起来不正确,因为我们在 _snprintf_s 中没有使用适当的类型说明符
现在,遇到了一些问题,代码并没有如预期那样工作。问题出在浮点数值的处理上。为了打印出这些数值,我们需要将它们转换成适合打印的类型,例如将浮点数转化为整数。这是一个常见的调试问题,解决了之后就可以继续进行。
做了这些调整后,理论上现在应该能看到平均值的输出。虽然这些数值仍然会有波动,但现在可以看到的是平均值,而不是原始的动态数据。虽然目前这些数据的实用性不强,但这一步是向目标迈进的重要进展。
总体来说,虽然还有些细节问题需要调整,但已经接近正确的实现。

(黑板) 推导出平均周期/命中的正确表达式。
首先,遇到的一个问题是平均值的计算方式不太正确。我们希望的是计算每次命中的平均周期数,而不是将周期数除以命中数得到的平均值。问题在于,如果只是简单地将周期数除以命中数,可能会得到不准确的统计结果,因为这两者并不是同一回事。
在思考如何正确计算时,考虑了两种方法:一种是将每个周期数和命中数的比值分别求出平均值,另一种是先将周期数和命中数的总和相加,然后再求平均。经过进一步的思考,确定了前者的方法更为直接和合理。具体来说,通过计算每个周期数和命中数的比值并累积它们,可以得到更精确的平均周期数。
接着,利用调试统计功能,可以更轻松地添加新的统计项。例如,添加一个新的统计项"命中数除以周期数"的平均值时,只需将相关的计数值与周期数一起累积,而无需复杂的数学运算。
虽然目前已经能够存储所需的调试信息,但还没有办法有效地展示这些数据。因此,接下来的工作重点将是如何将这些存储的数据输出为有用的信息,这将是在未来的工作中要集中解决的问题。
总结来说,现在的工作进展已经能存储所有需要的调试数据,并能通过简单的统计处理计算出有用的信息,但仍需优化如何展示这些数据,以便最终能够得出有价值的调试结果。
假设我们正在调试一个游戏或者系统中的性能问题。我们需要记录每次操作(例如,某个游戏事件或者函数调用)的"命中数"和"周期数"(即执行时间)。目标是通过统计这些数据,得出一些有用的调试信息,比如平均每次操作需要多少周期,或者每次命中操作的平均周期数。
举个例子:
-
数据记录 :
假设我们记录到以下数据:
操作 命中数(Hit Count) 周期数(Cycle Count) 操作1 5 100 操作2 8 150 操作3 3 75 这些数据表示,每次操作的命中数和该操作消耗的周期数。
-
计算"每次命中所需的平均周期数" :
我们现在想要计算每个操作的平均周期数,也就是每次命中所需的周期数。计算方式为:
每次命中的平均周期数 = 周期数 / 命中数
- 对于操作1:100 周期 / 5 命中 = 20 周期/命中
- 对于操作2:150 周期 / 8 命中 = 18.75 周期/命中
- 对于操作3:75 周期 / 3 命中 = 25 周期/命中
-
计算这些操作的整体"平均命中周期数" :
通过上面的计算,我们得到每个操作的平均周期数,接下来我们要计算所有操作的总平均值。我们可以把所有操作的周期数和命中数加起来,再计算平均值。
公式:
总周期数 = 操作1的周期数 + 操作2的周期数 + 操作3的周期数 = 100 + 150 + 75 = 325 总命中数 = 操作1的命中数 + 操作2的命中数 + 操作3的命中数 = 5 + 8 + 3 = 16 平均周期数 = 总周期数 / 总命中数 = 325 / 16 = 20.31 周期/命中
所以,所有操作的整体平均每次命中所需的周期数 是 20.31 周期/命中。
-
调试统计输出 :
通过调试统计功能,可以将这些数据输出,得到例如这样的调试信息:
操作1的每次命中平均周期数:20 周期/命中 操作2的每次命中平均周期数:18.75 周期/命中 操作3的每次命中平均周期数:25 周期/命中 总体平均周期数:20.31 周期/命中

C 编译器编译 C 代码比 C++ 编译器编译 C 类 C++ 代码要快吗?
关于编译器的速度问题,通常情况下,C编译器在编译C代码时会比处理C++代码的编译器更快。这是因为C语言的编译器设计较为简单,专注于处理C语言本身,而C++编译器则需要处理更多的复杂特性,如面向对象编程(OOP)、模板、异常处理等。这些额外的特性增加了编译的复杂度,因此C++编译器的编译速度通常会慢一些。
然而,这并不意味着没有可能让一个C++编译器编译C风格的代码时速度很快。如果编译器经过优化,特别是当它专注于处理类似C的代码时,它也可以在合理的时间内完成编译。举个例子,Turbo C这个较老的编译器,在20世纪80年代和90年代的25MHz机器上编译速度非常快。即使是在今天的现代计算机上运行,Turbo C的编译速度可能会非常快,几乎无法察觉,几乎是瞬间完成编译。因为如今的计算机处理速度远远超过了那时候的机器,所以即使是旧的Turbo C编译器,也能轻松处理过去的任务。
总的来说,C编译器因为专注于C语言,通常比C++编译器快,但如果处理的代码是类似C的代码,经过优化的C++编译器也能达到不错的速度。
在 C 中是否有标准的、便于移植的方法来获取指向内存块/区域的 FILE*?在 Linux 中有 fmemopen,但我在 Windows 中似乎找不到等效方法。网上似乎有人建议使用 MapViewOfFile,但我认为那不是它,我不想要内存映射文件,只是需要指向内存区域的 FILE*。有没有这样的东西,还是我在以错误的方式寻找?
在C语言中,获取文件指针指向内存中的一块区域的标准方法并不常见,因为"文件指针"(file pointer)通常是由C运行时库管理的,它是对文件的一种抽象。在Linux中,虽然有fm_open
这样的函数,但并不是标准的C库接口,且它并不直接解决"文件指针"映射到内存区域的问题。而在Windows中,一般使用MapViewOfFile
来实现内存映射文件,但这并不符合问题中提到的需求,因为它涉及到内存映射文件而不仅仅是将文件的指针映射到内存区域。
事实上,"文件指针"并不是操作系统提供的功能,而是C运行时库中的概念。这就意味着,跨操作系统的实现,像Linux和Windows,它们的运行时库会有所不同。文件指针本身并不与操作系统直接相关,主要是运行时库如何处理文件的读取和写入操作。
对于问题中提到的需求,其实是希望将文件内容直接加载到内存中,而不依赖于文件系统的其他操作。实现这个目标的核心思路是将文件的内容完全加载到内存的缓冲区中,从而避免文件的实际读写操作。这相当于为文件分配一个内存区域,这个区域直接存储文件内容,而且该区域不需要通过操作系统的文件I/O接口进行任何额外的文件操作。
从理论上来说,C运行时库本身可以支持这种操作,因为它可以通过分配内存并读取文件内容来实现这种文件指针映射到内存的方式。实际上,只要C运行时库能够提供一个内存缓冲区来存储文件内容,就能够实现这种功能,尽管这个实现可能并不常见,且不会直接涉及操作系统的内存映射文件操作。
使用组合方式编写游戏引擎会不会像 OOP 一样存在一些固有问题?
在使用"组合"(composition)来编写游戏引擎时,确实存在一些潜在的问题。组合通常指的是通过将多个小的、可重用的组件组合成一个复杂的对象,而不是使用继承或其他结构来构建系统。虽然这种方法在很多情况下可以提供灵活性和可扩展性,但也有一些需要注意的挑战。
首先,组合可能会导致系统中的依赖关系变得复杂。在大型游戏引擎中,组合的使用可能会导致大量的对象和组件之间的相互依赖,这些依赖关系可能会增加维护的难度和代码的复杂度。尤其是当不同的组件需要相互通信或共同工作时,如何管理这些组件之间的状态和行为变得更加复杂。
其次,如果每个组件都是独立的,可能会出现性能瓶颈。例如,如果游戏引擎中的多个组件(如物理引擎、渲染引擎、声音引擎等)频繁地相互通信,可能会导致不必要的开销,影响性能。虽然组合本身并不会直接导致性能问题,但过度的组件交互和数据共享可能会增加不必要的计算负担。
此外,组合的设计如果不够清晰和规范,可能会导致代码的可读性和可维护性差。每个组件都有可能有不同的接口和行为,这可能会让团队成员在理解和修改代码时遇到困难,尤其是在大型的游戏引擎中。
最后,虽然组合通常能够提高灵活性,但如果没有好的设计原则和明确的接口定义,可能会导致系统设计变得松散,难以保证各个组件的协作性和一致性。
总之,使用组合来编写游戏引擎时,需要考虑到这些潜在的问题,如组件之间的复杂依赖、性能瓶颈、代码的可维护性和系统的整体设计。如果能够合理管理这些问题,组合可以成为一个非常强大且灵活的设计工具。
符号将参数转换为字符串,而不能反过来做字符串拆分,获取字符串的内部值并拼接起来?我猜是因为这样做会使得宏无法在编译时确定,因为我们可以传递任意字符串?
在C语言中,宏预处理器(通过#
符号)可以将一个参数转换为字符串,但反过来却无法直接将一个字符串转换为其他类型。这主要是因为预处理器的设计和编译时的处理方式。
首先,预处理器是一个在编译时执行的操作,它的目的是为了文本替换和宏展开。在处理宏时,#
符号将宏参数转化为字符串字面量,但这种操作只能发生在编译时,而不适用于动态字符串。也就是说,如果字符串的内容在编译时无法确定,预处理器就无法对其进行操作。
其次,预处理器在设计时并没有考虑到反向转换,即将字符串转化为其他类型。这主要是因为预处理器的核心目标是做简单的文本替换,而不是进行复杂的数据转换。因此,预处理器并没有实现将字符串内容提取并转化为其他数据类型(比如数字)的功能。
此外,这种设计上的限制可能与早期的C语言设计者的个人偏好有关。C语言的设计者对预处理器进行了严格限制,避免它过度复杂化,甚至对某些复杂的特性表示反感。比如,允许在预处理器中进行动态计算或解析字符串可能会导致编译器和预处理器的过度复杂化,因此被设计者排除在外。
总之,预处理器无法进行字符串的反向转换,主要是因为它的设计目的和实现方式限制了这种功能的实现,而且C语言设计者的哲学是尽量避免让预处理器过于复杂和功能过多。
平均数的平均数需要按总体数量加权。这是一个常见的错误,甚至在科学论文中也会出现,试图将其他科学论文的结果结合起来。
在计算平均值时,平均值的计算往往需要根据不同群体的大小进行加权,这是一个常见的错误,即没有考虑到每个群体的大小,直接将多个平均值进行简单的平均。这种错误在很多科学论文中都有出现,尤其是在结合多个科学论文的结果时。
这里出现了一个问题,就是如果简单地计算平均值,可能会忽略群体大小的不同,这就导致了误差。因此,正确的做法是将每个平均值乘以相应群体的权重,然后再求平均,才能得出更准确的结果。
在计算时,可能会在某些步骤中多加了一个除法操作,但这种做法的目的并不是简单的计算,而是为了展示如何通过一些技巧来计算需要的结果。通过这种方式,可以灵活地计算出任何所需的统计数据。
总结来说,计算加权平均时,必须考虑每个群体的大小,不应直接对多个平均值进行简单平均。
Turbo C 的继任者是 Embarcadero 的 C++ Builder
Turbo C 的继任者是 Embarcadero 的 C++ Builder,但 C++ Builder 并不如 Turbo C 那么优秀。即使在 Turbo C 转变为 C++ Builder 的早期阶段,C++ Builder 的性能和功能也远不如 Turbo C。在过去,Turbo C 的表现已经非常出色,而 C++ Builder 让很多人失望,因此并没有得到广泛的好评。
对于 C++ Builder 的当前版本,也存在很大的怀疑,很多人认为它可能变得更差,甚至不如以前的版本。总结来说,尽管 C++ Builder 是 Turbo C 的继任者,但它在很多方面的表现并不令人满意,可能随着时间的推移情况变得更糟。
元编程问题:假设我想在我的游戏库中有一个通用的动态列表(可调整大小的数组)。空指针和宏是实现这一点的两种方法;另一种方法是为我想要的唯一列表类型生成代码。你所说的"我写 C 程序,它输出 C 程序"是不是就是这种意思?如果是的话,我们可以在编译时做这个生成(就像我们在 jai 中那样)还是只能编写并运行一个可执行文件来进行代码生成?
在讨论如何处理游戏库中的动态列表时,可以使用 void
指针和宏,或者为需要的特定类型编写代码。这样做的目的是为了处理不同类型的数据,而不需要使用通用的动态列表。
当提到"写 C 程序时,是不是类似于写Forth 程序"的时候,实际上指的就是通过编写特定类型的代码来解决问题,而不是完全依赖于通用的代码或库。对于这类问题,可以通过编译时生成的代码来优化,而不是依赖运行时处理。
Forth是1960年代末期,由查理斯·摩尔发展出来在天文台使用的电脑自动控制系统及程序设计语言,允许用户很容易组合系统已有的简单指令,定义成为功能较复杂的高阶指令。由于其结构精简、执行快速、操作方便,广为当代天文学界使用。
例如,像 JI 这样的工具可以支持在编译时生成代码,即使它还处于初期阶段,它也能做一些 C 语言和其他语言能做到的工作。而像 C 语言这种语言,其编译过程通常需要分为多个阶段。这时,可以通过构建、运行、再构建的方式来实现,具体做法是生成一些代码,然后用生成的代码再编译其余的部分,最终得到可执行文件。
这种方法使得在编译阶段可以实现更多的优化和自定义,虽然可能会增加一些复杂度,但从长远来看能够提高效率和灵活性。
关于组合方式:我有一个 Player 结构体,包含其他一些表示玩家组成部分的结构体。比如 Object 结构体,可以添加到任何我想要在游戏中作为物体行为的实体上。然后我为物体创建函数,如 objectMove(Object *obj),这样游戏中任何我想要移动的物体只需在其中添加一个 Object 结构体。
关于组合(Composition)在游戏开发中的使用,讨论的内容主要围绕如何设计游戏中的玩家对象及其组成部分。假设有一个玩家结构体,它由多个其他结构体组成,每个结构体代表玩家的一部分功能,例如对象(object)结构体。这些结构体可以添加到游戏中的任何实体对象中,以便给这些实体附加功能,比如移动功能。
在这个方案中,组合的使用可以让不同的实体(比如玩家、物体等)通过继承某个对象功能来实现多样的行为。为了让这些实体具备移动等操作,每个实体会拥有抽象功能,例如可以包含"移动"的抽象方法,其他实体就可以继承这个方法并运用到自己身上。
然而,组合并不是在所有情况下都特别有用,尤其是当这些组成部分之间需要有复杂的互动时。通常,游戏中最有趣的部分往往是不同部分之间的交互。因此,虽然组合模式简单灵活,但要实现复杂的交互,可能需要更为复杂的设计方法。
在实际开发过程中,尝试一些新的方法来处理对象间的组合与交互,可能会带来更有趣的结果。比如,在这个开发者的经验中,他们会尝试一些新的方式来实现这些交互,并且希望能比传统的组合方法更加高效和创新。
在编写游戏引擎时,程序员应该了解多少编译器的工作原理,学习编译器底层原理的好方法是什么?
在学习游戏开发时,了解编译器的工作原理是非常重要的,尤其是在游戏的性能和可移植性方面。具体来说,如果游戏有性能瓶颈或需要考虑跨平台支持,那么理解编译器如何工作就显得尤为重要。而如果游戏对性能要求不高,了解编译器的工作原理的重要性则相对较低。
想要深入了解编译器如何工作的最简单方法是通过查看编译后的汇编代码。通过调试工具,可以查看代码的反汇编结果,了解编译器如何将源代码转换为机器代码。步骤通常是进入调试模式,右键点击代码并选择"查看反汇编",这样就能看到编译器生成的机器指令。
掌握这些反汇编结果后,可以帮助开发者在性能关键的部分检查编译器的表现。通过比较调试版本和发布版本的差异,可以进一步了解编译器在不同优化阶段的处理方式。通过这些观察,可以逐步积累对编译器行为的理解,从而发现哪些地方表现好,哪些地方表现差。
此外,使用一些工具(例如iowaka等)可以分析代码,生成性能分析报告,查看代码结构和编译器的差异。这种分析有助于了解编译器对代码的处理方式,以及如何调整代码结构来提高性能。通过反复测试和分析,开发者可以不断优化代码,提升游戏性能。
嘿!我想开始学习编程,我有一些基础知识,但真的希望知道从哪里开始,你觉得学习哪些语言比较好?
关于编程入门的问题,当前的重点是如何开始学习编程。尽管已经有一些基础知识,想要进一步了解编程的学习路径,但目前并没有专门的材料或项目来引导初学者进入编程领域。假设学习者已经具备了一些编程基础,并且已经开始涉及较小的游戏编程项目。
目前并没有特别为初学者设计的资源或项目指南,因此对于那些完全没有编程经验的人,可能需要从其他地方寻找相关的学习资源。未来可能会提供更多关于如何从零开始学习编程的资源,但现在主要是针对已经有一定编程基础的人群。
在学习路径上,推荐寻找相关的学习平台或教程,有些学习者或许能分享他们的经验和推荐的学习资源。
我们在深入游戏玩法之前,还需要做多少引擎相关的工作?
在游戏引擎开发过程中,游戏玩法的实现和引擎工作是交织在一起的,尤其是当引擎的部分与游戏逻辑紧密耦合时,很多时候这些工作是无法完全分开的。
目前的工作主要集中在引擎的构建和一些基本的游戏功能实现,例如碰撞检测、AI等,虽然这些属于引擎工作,但它们与游戏的实际玩法密切相关,因此很难单独处理或分离出来。
你能在游戏中跑一跑吗?上次我看是一个月或两个月前,我很想看看它进展如何。
目前,游戏的引擎工作已经完成了大量的进展,但游戏本身的开发并没有太大变化。引擎的主要部分已经搭建起来,包括资产文件的管理和流媒体的处理,音频流也已经实现,但目前暂时关闭了,因为它变得有些烦人。引擎的性能很不错,尽管我们还在使用软件进行实时10 ATP渲染,但整体运行已经非常流畅。
尽管引擎的功能已经相当完善,但仍有一些工作需要完成,比如排序优化、调试代码以及一些最终的调整工作。还需要回去做一些渲染、粒子系统和光照等方面的改进,虽然这些工作尚未完成,但整体来说,已经非常接近完成。
引擎的渲染是完全手动实现的,速度非常快,支持多线程运行,即使没有GPU支持,依然能够良好运行。目前的重点是调试代码和一些细节的完善,整体进展顺利。
你对在游戏引擎中使用脚本语言有什么看法?
对于使用脚本语言与游戏引擎的结合,个人并不太喜欢使用脚本语言。主要的原因是为了让C代码能够即时重新加载,这样就不需要使用脚本语言,并且能够完全访问所有功能。
关于性能计数器,能否根据它们偏离理想或预期范围的程度进行颜色编码?类似地,是否有方法大致估算一个函数的理想周期计数?
创建性能计数器并根据它们偏离理想状态的程度进行颜色编码的想法很有趣,但估算一个函数的理想周期计数其实非常困难。因为函数的执行时间受内存访问的影响很大,而内存访问的时延是非常不稳定的。如果假设所有内存访问都在缓存中,那么估算理想状态会相对容易,这可以通过一些工具来实现。但是,实际的理想值并没有太多意义,因为首先要对是否所有数据都在缓存中形成自己的判断。
我明天有面试,要在白板上作答。如何才能不紧张?
在面试中,如果要求在白板上解题,最重要的是不要感到紧张。首先,了解如果面试官要求你做这类题目,说明他们的面试方式本身就有问题。因为在现实工作中,没有人会在白板上编程,也没有人在别人面前解决问题。这种做法并不代表能有效区分优秀和普通的程序员,只会让你处于不利的处境。
面试官通常想看到的是你解决问题的思考过程,而不仅仅是对答案的准确性。因此,面试官并不会要求你完美地完成题目,而是希望了解你如何处理问题,如何思考和解决问题。所以,即使你没有完全解答问题,也不要太担心。面试的目的是了解你过去的工作经验,而不是测试你面对陌生问题时的反应能力。
正确的面试方式应该是让候选人解释他们过去做过的项目,讲述他们已经处理过的问题,并且用白板展示出来。这种方式能够真正了解候选人是否具备实际的技术能力。面试官应该更多地关注候选人的工作经验,而不是通过一些不相关的白板编程问题来评估能力。
很多顶尖的程序员可能在面对陌生问题时反应较慢,但他们在解决实际问题时非常有能力。因为他们会深入思考,花时间整理出更好的解决方案。因此,如果面试中要求白板编程解题,这种方式对于筛选优秀程序员是无效的。它并不能区分优秀的程序员和普通程序员。
总结来说,面试中如果面试官要求你在白板上解答编程问题,只需知道这不是评估你能力的正确方式,面试官可能并不懂得如何有效地面试程序员。这些了解有助于缓解你的紧张情绪。