游戏引擎学习第127天

仓库:https://gitee.com/mrxiao_com/2d_game_3

为本周设定阶段

我们目前的渲染器已经实现了令人惊讶的优化,经过过去两周的优化工作后,渲染器在1920x1080分辨率下稳定地运行在60帧每秒。这个结果是意料之外的,因为我们没有预计会达到这样的性能,尤其是在2D游戏中,甚至不再需要GPU就能完成渲染,尽管游戏中仍然涉及到子像素精确的旋转等复杂操作。这种性能的提升令人感到吃惊,因为计算机的处理能力变得异常强大。

尽管如此,我们还需要继续工作,特别是在渲染代码的整理和清理方面。我们目前已经基本完成了渲染代码的核心部分,因此接下来的目标是对当前的代码进行整理并拍摄一个快照,评估一下现有的状态。我们还会进行一些性能预估,特别是关于渲染器的吞吐量,以及在这台特定机器上渲染器的实际能力。尽管我们可能已经接近机器的极限,但还不确定实际的瓶颈在哪里,所以我们需要做出一些预估,以此来确定渲染器的潜力。幸运的是,通过最初的移植工作,我们的代码已经变得非常高效,这似乎也就是优化过程中的意外收获。

演示瓷砖大小的问题

接下来的工作重点是解决一个之前没有处理的问题。这个问题我们一直没有关注,因为它对当前的工作影响不大,但为了最终完成项目,现在必须解决它。

回顾一下之前的进展,渲染器的性能已经非常不错,运行速度相当快。不过,尽管性能良好,但我们并没有完全实现多线程的瓦片渲染器。问题出现在画面上出现了许多黑线。虽然渲染速度较快,而且我们将瓦片缩小了一些,这样可以看到更多的内容,但这显然不是最终的效果,绝对不能用于发布的游戏。我们不希望游戏看起来像是由一堆瓦片拼接而成,尤其是这些黑线的出现,还影响了背景瓦片的显示,使得它们也有了黑线,这样会导致图像的显示效果很差。因此,当前的显示效果不理想,必须解决。

我们计划深入分析为什么会出现这些问题,并解决它们,使得渲染器不再需要依赖这种临时的缩小瓦片的方式,从而避免黑线问题,恢复最终的渲染效果。此外,还需要处理一些其他的小细节问题。

在这里插入图片描述

在这里插入图片描述

确认 _mm_sfence 不必要

我们对一些问题进行了再次确认,查阅了相关资料,并且与一些专家进行了核实,确保信息的准确性。这些专家在处理处理器相关问题上经验丰富,因此是了解处理器实际工作方式的重要参考来源。通过和他们的讨论,我得到了确认,原本怀疑的 _mm_sfence 指令并不是必须的。

之前在讨论多线程代码时,曾提到过 x64 处理器有很强的内存顺序性,通常情况下,写操作会有序执行。然而,我曾对这个结论有所犹豫,因为我知道 _mm_sfence 指令在某些情况下可能是必须的,因此我一度回撤了之前的说法,开始怀疑是否有新的变化或者需要手动插入这些指令。我觉得有必要查阅更多资料,了解到底是否需要使用这些指令。

经过确认,事实证明我的原始理解依然是正确的,_mm_sfence 指令不需要手动插入。这是因为处理器在设计时已经处理了这些问题,所以无需显式的 _mm_sfence 来保证内存的顺序性。这种情况从我最初学习时就已存在,并没有因为新的硬件或技术发展而改变,这对我来说是一个令人欣慰的发现,因为很多时候在工作中我不得不更新一些过时的信息或概念,而这次这个问题并没有变化。

总之,通过这些核实,我们可以继续前进,而不必担心插入额外的 _mm_sfence 指令,确保代码的正确性和高效性。
在这里插入图片描述

黑板: “Write-Combining Memory”

在处理器中,有一个叫做“Write-Combining Memory”的概念,涉及到内存和缓存的工作方式。为了理解这个过程,需要先了解一下处理器的内存管理。处理器有寄存器,所有操作都发生在寄存器中,然后数据通过缓存和主存之间传输。处理器会先把数据加载到缓存中,进行操作后再写回内存。缓存是一个速度较快的存储区域,它存储的是常用的内存位置内容。数据最终会写回内存,而内存本身的访问速度较慢。多个处理器的缓存之间还会通过缓存探测来协调谁拥有某个内存块,以避免数据冲突。

在谈到内存屏障(fence)时,特别是在x64架构上,不需要担心写操作的顺序,因为写操作在默认情况下是有序的。写操作按照代码中的顺序进行,而编译器则会确保不打乱这些写操作的顺序。虽然在代码中使用写屏障可以防止编译器对写操作进行重排,但通常处理器会自动处理写操作的顺序。因此,x64处理器不需要额外的写屏障(如_mm_sfence指令)来确保写操作的顺序。

但是,在某些特殊情况下,可能会遇到“Write-Combining Memory”的问题。Write-Combining Memory通常出现在处理器和其他硬件(如GPU)共享内存的情况中。例如,如果CPU和GPU共享某块内存,CPU会在内存中进行写操作,而GPU通过PCI总线读取或更新内存。这种情况下,处理器可能会尝试合并多个写操作,以减少写入内存的次数,因为直接写入共享内存会比较慢。

这种合并写操作的方式可能导致一些问题:处理器可能会延迟将某些写操作刷新到内存中,直到合并的写操作完成。这样可能会导致某些写操作的顺序发生变化,出现顺序错乱。这时,_mm_sfence指令就派上了用场,它用于防止这种重排,确保在Write-Combining Memory时写操作的正确顺序。但如果不是Write-Combining Memory,处理器已经自动保证了写操作的顺序,这时就不需要使用_mm_sfence

综上所述,对于常规内存,处理器会自动保证写操作的顺序,_mm_sfence指令并不必要。我们只需要使用写屏障来防止编译器重排写操作,而不需要额外的硬件指令来控制内存的写入顺序。

接下来,我们也需要讨论为什么在瓦片渲染的过程中需要缩小瓦片。问题的根源有两个:首先,由于渲染的瓦片大小问题,导致了显示上的一些不正常情况;其次,缩小瓦片实际上是一种临时解决方案,用于避免显示中出现黑线等问题。我们需要通过更智能的方式来解决这些问题,最终恢复正常的瓦片大小。

什么是Write-Combining Memory(写合并内存)。简单来说,这是一种计算机体系结构中的优化技术,主要用于提高内存写入操作的效率,尤其是在处理器与内存之间频繁传输数据时。它通常出现在现代处理器(如Intel、AMD的CPU)中,特别是在涉及到图形处理或高性能计算的场景。

基本概念

写合并内存的核心思想是将多个小的、分散的写操作“合并”成一个更大的、连续的写操作,然后一次性发送到内存。这样可以减少对内存总线的占用,提高数据传输的效率。我们知道,处理器和内存之间的通信有一定的开销,尤其是当数据量小而频繁时,这种开销会变得很明显。写合并内存就是为了解决这个问题。

具体来说,处理器内部会有一些特殊的缓冲区(称为写合并缓冲区,Write-Combining Buffer)。当我们执行写操作时,这些数据不会立刻直接送到内存,而是先暂时存储在这些缓冲区里。等到缓冲区满了,或者某个条件触发(比如要刷新缓存),这些数据才会被一次性写入内存。

工作原理

想象一下,我们在写数据到内存时,不是每次都直接访问内存,而是先把数据攒起来。比如:

  • 我们连续写几个字节到相邻的地址(比如地址100、101、102)。
  • 这些写操作会被收集到写合并缓冲区,形成一个更大的块(比如一个32字节或64字节的缓存行)。
  • 然后,这个块作为一个整体被写入内存,而不是零散地写三次。

这种方式利用了内存访问的“突发传输”(burst transfer)特性,因为现代内存系统(像DDR内存)在处理连续数据时效率更高。

使用场景

写合并内存特别适用于以下情况:

  1. 图形处理:比如GPU或CPU的集成显卡在更新帧缓冲区(framebuffer)时,经常需要写大量连续的小数据块。写合并可以显著减少内存访问次数。
  2. 流式数据:当我们处理连续的流式数据(比如视频编码、科学计算),写操作往往是顺序的,写合并能优化性能。
  3. 非缓存写:有些内存区域被标记为“不可缓存”(uncacheable),直接写会很慢,写合并提供了一种中间方案。

在x86架构中,写合并内存通常和特定的内存类型相关,比如通过MTRR(Memory Type Range Registers)或PAT(Page Attribute Table)设置的“WC”(Write-Combining)内存类型。

优点

  • 减少带宽占用:合并多次小写操作,降低对内存总线的压力。
  • 提高效率:利用突发传输,减少访问延迟。
  • 适合特定负载:对顺序写或局部性强的写操作特别有效。

局限性

  • 不适合随机写:如果写操作是分散的、非连续的,写合并效果就不好,因为缓冲区可能无法有效合并。
  • 一致性问题:数据先存在缓冲区,可能会导致其他设备或线程看不到最新的值,直到缓冲区刷新。
  • 手动管理:有时需要显式刷新(比如用CLFLUSH指令),不然数据可能滞留在缓冲区。

和其他技术对比

  • 对比Write-Through(直写):直写是每次写都直接更新内存,延迟高但一致性强;写合并延迟低但一致性弱。
  • 对比Write-Back(写回):写回用缓存延迟写入,适合读写混用;写合并不缓存,适合只写场景。

总结

写合并内存是一种专门优化写入性能的技术,通过缓冲和合并小块写操作来减少内存访问开销。我们在设计程序或硬件时,如果遇到大量顺序写操作,可以考虑利用这种机制。不过也得注意它的局限性,比如数据一致性和适用场景。总之,它是现代处理器中一个巧妙的平衡性能和效率的工具。

黑板:瓷砖和对齐

首先,需要确保在渲染过程中,始终以四个像素为一个处理单位进行处理。这是基本要求,必须确保每行的像素数始终是四的倍数。如果分配的位图的宽度不是四的倍数,可能会导致最后一行出现不足四个像素的情况,这样就可能造成错误或未对齐的情况。因此,必须保证每行的像素数量是四的倍数,以避免出现只有三像素的“桶”或者某些像素溢出到下一行。

其次,目前的渲染过程中存在未对齐的情况,具体表现为在进行内存读取和写入时,数据是按行读取和写入的,且每行的起始位置和结束位置并不总是对齐的。为了简化处理,选择的是在结束时保证像素位置对齐,但如果存在像素边界跨越的情况,可能会导致不同线程处理的瓦片数据冲突。例如,假设有两个瓦片A和B,其中瓦片A的两个像素和瓦片B的两个像素恰好在一行中相邻,如果两个线程分别处理这两个瓦片,那么就可能出现两个线程同时读取并修改相同的像素区域,从而发生数据冲突。由于在处理器中,数据被加载到寄存器后,处理器的缓存和其他核心无法进行同步,因此如果不对齐,就会导致两个线程同时写回内存时,彼此的修改会相互覆盖,造成数据不一致。

因此,为了避免这种情况,必须确保瓦片的边界是对齐的,避免出现跨越边界的情况。特别是瓦片的宽度必须是四的倍数,这样才能确保每个瓦片的边界不跨越四像素的边界,从而避免多个线程同时访问同一块数据时发生冲突。

此外,虽然当前的代码在某些地方使用了未对齐的读取和写入操作,但通过合理的设计,确保读取和写入操作不会跨越瓦片边界,从而避免了可能的冲突。这样,即便存在未对齐的操作,也不会对渲染结果产生负面影响。

最终,必须通过明确的断言来确保瓦片宽度始终是四的倍数,并且防止任何未对齐的操作跨越瓦片边界。通过这种方式,可以确保在并行处理时,数据不会被不同线程同时修改,从而避免数据冲突和错误。

建议将 TileWidth 和 TileHeight 四舍五入到最接近的 4,并将瓷砖对齐到 4 字节边界

首先,需要确保瓦片的宽度和高度是以四个像素为单位对齐的。这意味着,计算得到的瓦片宽度和高度必须向最近的四的倍数进行四舍五入。如果不进行这种调整,就无法确保内存对齐,从而影响渲染的正确性。

其次,必须确保每个读取和写入操作都对齐到四字节边界。因为在图像处理过程中,每个像素占用四个字节的内存,所以在进行数据操作时,内存必须对齐到四字节边界,才能避免潜在的错误或性能损失。

因此,首先要确保瓦片的宽度和高度是四的倍数,其次要检查并确保内存中任何需要读取或写入的数据都按照四字节边界进行对齐。这是确保图像数据处理正确、高效的关键。

再想一想,然后再改变主意

在处理过程中,虽然有些读取操作是未对齐的,但这实际上并不会造成问题。之所以没有问题,是因为处理的图像数据会根据瓦片进行裁剪,因此无论位图的结束位置在哪里(即使在中间),只要数据在瓦片的边界内,结果依然是有效的。

简而言之,即便读取操作存在未对齐的情况,只要数据被裁剪到合适的瓦片范围,处理过程仍然是正确的。因此,可以忽略这一点,不需要进行额外的处理。

game_render_group.cpp:确保我们总是得到对齐的瓷砖

首先,计划是确保瓦片始终能够对齐。为了实现这一点,首先需要确保输出目标的分配能够满足需要的瓦片大小。如果输出目标正确分配,那么可以依赖上游代码来确保目标内存正确分配并符合要求。

接下来,在处理瓦片时,必须确保将图像分割成一个四乘四的瓦片数组时,每个瓦片的宽度始终是四的倍数。也就是说,在计算瓦片宽度时,需要确保它是四的倍数。如果瓦片宽度不是四的倍数,那么需要通过断言来确认这一点,确保计算结果是符合对齐要求的。

因此,瓦片的宽度必须被正确地计算并且对齐到四的倍数,这样可以确保图像处理过程中不会出现对齐问题。

黑板:计算并填充正确数量的 4 像素单元

具体来说,需要计算瓦片的宽度,并确保瓦片宽度符合四像素的对齐要求。例如,假设需要处理的分辨率为960×967像素,首先计算将宽度分成四像素块的结果。如果直接将967像素除以4,得到的是241.75,这意味着并不能完全对齐到四像素边界。为了确保瓦片宽度是四的倍数,必须将结果向上舍入到242像素。这样做的目的是确保瓦片能够覆盖整个图像的宽度,不会遗漏任何像素。

接下来,需要再调整这个值,以确保最终的瓦片宽度符合四像素的单位。如果242像素还是不完全是四像素单位,那么必须进一步向上舍入,调整到243或244像素,确保处理的每个瓦片的宽度都能以四像素为单位来处理。

在这个过程中,假设每个瓦片的宽度是242像素,再计算每行需要多少个瓦片。由于每个瓦片占据4个像素单位的宽度,因此可以推算出每行需要64个瓦片,最终的有效分辨率是976像素。因此,即便原始分辨率是967像素,实际处理时也会调整为976像素,确保图像能够完整地填充并且每个瓦片都正确对齐。

黑板:目标缓冲区必须总是允许我们覆盖一定的区域

首先,必须确保目标缓冲区有足够的空间来允许我们覆盖它的结尾部分。由于图像是以批次进行处理的,我们需要保证每个瓦片的末尾能够正确对齐,并允许覆盖超出的部分。这是因为在处理图像时,瓦片的宽度和高度需要确保四像素的对齐。虽然可以通过裁剪来限制处理区域,但必须确保最终的瓦片区域能够完全填充并对齐到四像素边界。

具体来说,假设目标分辨率是967像素。为了确保图像能够正确处理,必须确保这个分辨率在四像素的基础上对齐。因此,967像素需要被四舍五入到最近的四像素倍数,即976像素。这是因为在处理图像时,瓦片的宽度是固定的,而最后一个瓦片可能没有填满剩余的所有像素。这时,可以通过裁剪最后的瓦片来避免浪费空间,但要确保即使最后一个瓦片小于实际需要的大小,目标缓冲区也能够覆盖这部分区域。

进一步计算时,假设每个瓦片的宽度是61个像素(即244像素),如果按这个标准处理瓦片,剩余的部分就会是235像素。为了确保图像完全填充,需要将235像素四舍五入到四像素的倍数,即236像素。因此,最后的瓦片区域需要支持覆盖多出的1个像素。

为了避免出现问题,需要确保目标缓冲区能够允许覆盖并处理这些超出部分,同时还要保证瓦片的边界始终对齐到四像素边界。这不仅适用于瓦片本身,也要确保行的起始位置对齐,以避免在内存操作过程中出现未对齐的读取和写入问题。

最后,经过这些处理之后,整个图像应该能够完全填充屏幕,并且每个瓦片都能精确对齐,不会留下边界或空隙。因此,所有的瓦片必须在四像素边界内正确对齐,并且在处理时,目标缓冲区需要支持对最后部分的轻微覆盖。

确保 OutputTarget->Memory 对齐到 16 字节

首先,需要确保输出目标的内存已经对齐到四像素边界。四像素边界意味着每个像素占用4字节空间,因此4个像素占用16字节的内存。如果要验证内存是否对齐,可以检查内存地址的低位(即最低的4位),通过与15进行位与操作(& 15),确保结果为0。如果结果为0,则表示该内存已经对齐。

此外,还需要确保在处理时不会出现对齐错误。因此,必须关闭优化选项,确保每次访问内存时都能验证内存对齐情况。如果平台层没有自动进行内存对齐操作,也需要通过代码确保内存地址正确对齐。

接下来,瓦片宽度需要考虑对齐的要求。由于处理是按水平(横向)而不是垂直(纵向)方向进行的,因此瓦片的高度不会影响对齐,只需要确保瓦片宽度是四像素的倍数。只要瓦片宽度满足这个条件,处理就不会受到影响。在某些情况下,可能需要去掉一些不必要的检查,但瓦片宽度必须始终是4的倍数,这样才能保证在处理时不会出现未对齐的问题。

在这里插入图片描述

在这里插入图片描述

会段错误
在这里插入图片描述

将 TileWidth 向上舍入到最接近的 4

首先,为了确保瓦片宽度始终对齐到四字节的边界,需要将瓦片宽度四舍五入到最接近的4的倍数。为此,可以通过一种常见的技巧来实现:首先将瓦片宽度加上3,然后除以4。这个步骤的目的是让整数部分向上取整,从而确保无论原始瓦片宽度是多少,最终的瓦片宽度总是能对齐到四字节边界。

具体做法是:对瓦片宽度进行加3处理后,再除以4,然后将结果乘以4。这样可以保证即使原始瓦片宽度不是四的倍数,经过四舍五入后,最终的瓦片宽度也会是四的倍数。

例如,如果原本瓦片宽度是241个像素,经过处理后,先加3变成244,除以4得到61,再乘以4得到244。这就保证了瓦片宽度是四的倍数,同时确保内存对齐。

这个方法的好处是,如果以后需要调整瓦片宽度的单位,可以轻松修改相关的代码逻辑,而不必固守原有的特定数字,保持了灵活性。
在这里插入图片描述

看看到底得到了什么数字

首先,我们检查了瓦片宽度,发现其值已经是64,这意味着它本身已经是4的倍数,因此不需要额外的四舍五入。这个值已经是合适的,并且能够被4整除,因此不需要进一步处理。

接下来,我们执行了测试,使用了480的值,并确认结果仍然是适当的,且已经符合了预期的对齐要求。虽然在某些情况下,可能需要处理一些未对齐的值,但目前来看,计算是正确的,不需要做额外的调整。

为了确保瓦片宽度的正确性,我们通过对计算结果进行截断和回乘的方式,保证了最终的结果是经过正确四舍五入处理的。这种方法确保了瓦片宽度在实际计算中始终满足对齐要求。接下来,我们将继续进行其他的必要操作。
在这里插入图片描述

确保 FinalTileWidth 考虑到它会更小的事实

需要确保最后一个瓦片的宽度考虑到它会更小的事实。为了做到这一点,我们需要计算最后一个瓦片的宽度,这个宽度应该是屏幕的总宽度减去前面那些已完成的瓦片的宽度。前面的瓦片宽度可能已经调整过(有时会更大),所以剩余的空间只需要用于最后一个瓦片。

在计算过程中,我们需要明确最后一个瓦片只占据剩余的部分,而不是按照标准的瓦片宽度来处理。因此,在执行这一步时,我们需要确保最后一个瓦片的宽度是准确的,以适应剩余的空间,而不是以固定的瓦片大小来进行填充。

通过这种方式,我们可以保证最终的瓦片布局能够精确地覆盖整个屏幕,且不会出现因为最后一个瓦片过大而导致的屏幕溢出问题。
在这里插入图片描述

删除 FinalTileWidth,并将 ClipRect.MaxX 限制为 OutputTarget->Width

可以简化这个过程。事实上,我们并不需要进行复杂的计算。当我们设置裁剪矩形(clip rect)的最大值时,只需要检查,如果裁剪矩形的最大值大于实际的输出目标,那么就将其设置为输出目标的值。这样就能确保当我们处理到屏幕的边缘时自动进行裁剪,避免超出屏幕范围。

这种方法比其他复杂的方案要简单得多,只需确保裁剪区域不会超过输出目标的边界。这样,我们就可以在处理过程中直接调整裁剪区域,从而避免了处理最后一个瓦片时可能出现的溢出问题。
在这里插入图片描述

删除 ClipRect 调整

不再需要进行这些复杂的调整,因为已经采用了另一种方法来处理这个问题。现在瓦片的宽度已经进行了调整,我们可以继续验证它是否正常工作。接下来可以运行代码来检查当前的实现是否按预期工作。

不过,出现了一个问题,似乎我们正在从一个不允许的地方加载数据。这时,需要调试来确认问题的根源。同时,还不确定是否已经渲染了足够的数据,但可以通过一些额外的手段来进一步调试和确认,比如检查实际的渲染结果或输出目标的相关信息。

在这里插入图片描述

在这里插入图片描述

意识到的问题:我们只是在结束时裁剪,但没有处理开始时的裁剪

现在问题在于,只处理了结尾部分的裁剪,但并没有处理开始部分的裁剪。这可能正是之前提到的需要考虑的部分,虽然每次查看时觉得不需要处理,但现在看起来确实是这个问题。

黑板:裁剪问题

当前的问题在于,当进行位图绘制时,虽然我们会处理右侧的裁剪并对齐到四像素边界,但在写入过程中,左侧的缓冲区可能会出现不对齐的情况,导致我们不小心写入到缓冲区的开始部分,这显然是不合适的。为了解决这个问题,需要确保每次绘制时都从对齐的起始位置开始,避免出现偏移。

具体来说,在进行位图绘制时,虽然我们避免了结束部分的偏移,但是一旦遇到位图刚好结束在四像素边界上,我们就可能出现一个问题——如果图像宽度并不正好是四的倍数,那么我们会写入错误的像素区域。这就意味着需要确保每次都进行合适的对齐,并且对图像的开始和结束部分进行掩码处理。

解决方案是在绘制前,确保整个绘制过程都对齐到最近的四像素边界。如果绘制的起始部分不对齐,那么就需要对起始部分进行掩码处理。同样,结束部分也可能会被裁剪或掩码。

最直接的做法是确保绘制区域的开始和结束都经过掩码处理,保持图像的对齐,而不再试图避免掩码,这样可以确保图像的每一部分都符合预期的对齐规则。

对 MinX 和 MaxX 进行对齐

当前的任务是确保填充矩形(FillRect)的最小值(MinX)和最大值(MaxX)对齐到四像素边界。首先,需要检查最小值 MinX 是否对齐,如果不对齐,需要将其调整。调整的方法是通过将 MinX 向下对齐到四像素边界,这样它就能正确对齐。为了实现这一点,可以通过修改 FillRectMinX 值,使其符合对齐规则。

接下来,需要做的是创建两个剪裁掩码(ClipMasks),一个用于起始位置,另一个用于结束位置。剪裁掩码的作用是遮掩掉不需要的部分,确保只有对齐的部分会被绘制。可以根据 MinX 是否对齐来生成对应的剪裁掩码。

如果 MaxX 也不对齐,那么同样需要将其对齐,只不过这次是向上对齐,而不是向下。通过对 MaxX 做类似的操作,确保其也符合四像素对齐规则。这是通过将 MaxX 向上对齐的方式来完成,具体做法是将 MaxX 增加一定值(如 4),使其对齐到四像素边界。

在完成这些对齐操作之后,如果 FillWidth 没有被再次使用,可以将其移除,因为它已经不再需要。

最后,创建两个剪裁掩码表,用于存储 StartClipMaskEndClipMask。这些掩码的值将基于 MinXMaxX 对齐后的情况来生成。根据 MinXMaxX 的偏移,可以通过位移操作来生成相应的掩码。这些掩码确保在绘制过程中,只绘制对齐的像素区域。

总结来说,关键的步骤包括对 MinXMaxX 进行对齐,确保它们符合四像素边界要求,并且通过生成剪裁掩码来确保在绘制时不会出现不对齐的情况。

黑板:设置 EndClipMasks

在当前的方案中,当 MinXMaxX 对齐到零边界时,我们不需要处理问题,因为它们已经是对齐的。但是,如果它们没有对齐,比如位于某个不对齐的位置,可能会需要进行进一步的操作。例如,如果需要在图像的某个位置写入数据,那么就必须进行位移操作,确保它们符合对齐要求。

具体来说,当 MinXMaxX 没有完全对齐时,需要向右进行位移操作。这个位移操作是通过将数据向右移动 3 位来完成的。这样,我们的目标是确保数据能够对齐到四像素边界,即在处理时确保每个像素的位置都是对齐的。具体的操作方式是,通过进行 3 位的右移,将不对齐的像素移到正确的位置。

为了实现这个,我们使用了剪裁掩码(clipping masks)。这些掩码会根据不同的位移要求来设置。具体来说,我们会设置起始剪裁掩码和结束剪裁掩码,分别处理数据的开始和结束部分。这些掩码将根据位移的需要,进行相应的位移操作。比如,对于某些掩码,可能需要向右移动 3 位,而对于其他掩码,只需要移动 2 位,甚至 1 位。这些操作是为了确保在图像的不同位置,数据能够正确对齐。

简而言之,处理对齐时,如果遇到不对齐的情况,使用位移操作来调整数据的位置,确保它们能够对齐到正确的像素边界。同时,通过使用剪裁掩码来遮掩掉不需要的部分,确保只有对齐后的部分被绘制。

弄清楚何时使用 EndClipMask

在当前的处理过程中,问题出现在何时使用结束剪裁掩码(EndClipMask)。虽然在例程的开始部分已经处理了开始剪裁掩码(StartClipMask),并将其重置为负一,但在某些情况下,我们还需要在最后一次迭代时使用结束剪裁掩码,这就变得稍显复杂。

为了处理这个问题,我们需要引入一些条件判断,来确保在正确的时机应用结束剪裁掩码。具体来说,我们需要检查当前的 X 值以及接下来的 X + 4 值,判断这是否超出了图像的边界。如果下一个 X 值加 4 会超出边界,那么就意味着我们已经达到了图像的最后一部分,这时就应该使用结束剪裁掩码。

因此,在处理时,我们需要在每次迭代时判断,如果是最后一次迭代,则使用结束剪裁掩码来替代开始剪裁掩码。这个操作确保了在绘制过程中,图像的开始和结束部分都能得到正确的剪裁。

通过这种方法,我们能够确保图像在绘制时不会超出预期的范围,同时保持正确的对齐和剪裁操作。

在这里插入图片描述

在这里插入图片描述

查看是否始终对齐

为了避免加载和存储操作出现崩溃,必须确保数据是对齐的。如果数据没有对齐,执行这些操作时可能会导致程序崩溃。因此,需要确认每次加载和存储数据时,都是以正确的对齐方式进行的。通过确保对齐,能够避免这些潜在的崩溃问题,确保操作的稳定性和可靠性。
在这里插入图片描述

在 -O2 (release) 中编译并开启多线程,以确保一切正常

在这里插入图片描述

在确保一切正常之后,设置了适当的参数(如02)并检查效果,结果看起来不错,程序运行顺利,没有出现问题。通过启用线程化的子渲染组,进一步提高了效果。现在,图像渲染没有出现之前的那些错误或不良的瓦片,整体表现很稳定,因此目前的状态是可以接受的。

win32_game.cpp:再次检查平台代码是否分配了对齐的内存

平台代码需要确保正确地分配内存并保证内存对齐,因此需要再次检查是否确实做到了这一点,而不是偶然地实现了对齐。特别是,需要确保所有关于对齐的假设都是有效的。在查看分配内存的地方时,发现使用了VirtualAlloc进行内存分配,并且在此过程中,分配的内存是从页面边界开始的,默认对齐方式会符合要求(通常对齐为4KB)。然而,问题在于分配的内存的宽度(即像素的“pitch”)没有任何保证是对齐到4像素边界。因此,接下来要做的是确保分配的内存至少是按4像素对齐的。

将 Buffer->Pitch 对齐到 16 字节

为了确保内存分配的正确对齐,首先需要将像素宽度(pitch)的计算移到代码的前面。计算方式是宽度乘以每个像素的字节数。接着,内存分配的大小是将计算出的pitch乘以高度。为了保证对齐,需要将分配的内存大小四舍五入到最接近的4字节倍数,或者说16字节倍数。

为此,可以编写一个名为align16的函数来处理这个对齐操作。通过引入这样的对齐机制,确保分配的内存是按照16字节对齐的,从而避免潜在的内存对齐问题。这一调整可能是新的,之前没有专门为对齐引入过类似的代码,因此需要引入这种对齐逻辑以确保稳定性和性能。
在这里插入图片描述

game_platform.h:引入 Align16

为了确保内存的对齐,引入了一个名为align16的函数,它的作用是接受一个值并将其四舍五入到最接近的16的倍数。实现方法是将值加上15,然后通过掩码操作去掉未对齐的部分,这样就能确保该值总是对齐到16字节边界。

这种对齐操作是为了保证渲染器能够处理正确对齐的内存,避免因为内存未对齐导致的潜在问题。虽然之前由于选择的宽度,内存对齐已经可以满足渲染器的要求,但为了确保一致性和稳定性,明确加入了对齐机制。最终,这样做确保了渲染器可以正常工作,并且内存分配的假设不再受到任何影响。
在这里插入图片描述

一切在 1920x1080 分辨率下看起来都很好

渲染方面现在一切正常,分辨率已经达到1920x1080,效果没有问题。虽然我们还没有处理排序等其他部分,但目前的状态已经很稳定,没什么大问题。剩下的时间也不多了,确认了之前的操作,并加入了断言机制,这样当出现问题时会触发,确保代码的健壮性。

此外,尽管还在调试阶段,但启用多线程后,性能提升明显,甚至在1920x1080分辨率下也能顺畅运行,这让当前的实现非常理想。在剩余时间内,可以进行一些进一步的测试,确保没有遗漏的问题,同时也可以尝试进行一些小调整,以验证系统是否稳健。如果一切正常,可以删除不再需要的部分,继续完善剩余的功能。

game.cpp:在开始时设置 PlatformAddEntry 和 PlatformCompleteAllWork

在最后两分钟里,发现了一个问题:之前将全局指针分配放在了内存初始化的过程中,但这样做没有考虑到热重载的情况。热重载时,DLL会被重新加载,这会导致全局变量和静态变量被清空,从而导致崩溃。问题出在,内存指针在重新加载时失效,导致程序崩溃,因为在热重载后这些指针没有被重新赋值。

正确的做法是在一开始就将这些指针正确设置好,这样无论是否发生热重载,都能确保指针有效。这样一来,就能避免崩溃,确保在进行热重载时,程序不会出问题。

目前,渲染效果已经很好,一切都看起来平稳流畅。接下来,可能需要处理调试矩形的精度问题,虽然不一定会影响最终效果,但值得注意。总体来说,现在已经完成了对内存对齐和掩码的处理,状态已经很稳定。对于优化函数中的额外分支,虽然有些担忧,但希望在实际运行时,特别是X64架构上,它的分支处理能力足够强大,不会带来显著的性能问题。因此,当前的进展是非常不错的,下一步可以继续完善和调整。
在这里插入图片描述

位图内存大小计算时平方了 BytesPerPixel,导致分配了不必要的内存

在检查过程中,发现了一个问题:在处理每像素字节时,出现了一个奇怪的情况,似乎在计算时没有正确移除一个字节。这个问题可能会导致字节数计算不准确,从而影响内存对齐或渲染效果。需要重新检查每像素字节的计算方式,确保在调整时移除不必要的字节,避免引发潜在的内存或性能问题。

win32_game.cpp:去除乘以 BytesPerPixel

在调整过程中,发现了一个错误:在提取了pitch时,忘记删除了BytesPerPixel。这个变量已经不再需要,因此它占用了额外的内存。由于这个变量不会再被使用,保留它会造成内存浪费。通过删除不必要的变量,可以优化内存使用,避免浪费。
在这里插入图片描述

能否测试一下奇怪的分辨率是否正常工作?

在测试时,首先检查了是否能正常处理一些不寻常的分辨率,特别是确保在选择一些特殊的分辨率时,程序不会崩溃或者出错。测试中发现渲染器在显示瓦片时出现了微小的偏差,虽然渲染看起来基本正常,但确实存在轻微的剪裁问题。具体表现为有一个非常小的区域没有被填充,可能是由于剪裁计算出现了问题,导致了一些不必要的裁剪。这种问题需要进一步追踪和修复。
在这里插入图片描述

在这里插入图片描述

game_render_group.cpp:确保如果这是最后一个瓷砖,它能够一直绘制到最后

在进行测试时,主要关注了如何确保瓦片的宽度正确地对齐,尤其是在瓦片数量不能均匀分配的情况下,需要对瓦片宽度进行向上取整,以保证其是4的倍数。为了确保瓦片正确填充,可能需要调整最后一个瓦片的尺寸,特别是当它是最后一个瓦片时,确保它能够完全填充到边界。

在测试分辨率时,尝试了几种非标准的分辨率,如1371x913、1277x1279和719x1280等质数分辨率。结果发现,当使用这些不规则分辨率时,出现了一个bug,导致图像渲染的"pitch"(每行字节数)出现问题。初步怀疑问题可能出在Win32的StretchDIBits函数,它没有正确处理pitch,这可能是导致渲染问题的原因。

经过分析,发现StretchDIBits函数中的位图信息结构并没有包含pitch字段,因此它无法正确处理按自定义方式对齐的图像。这导致了渲染错误,但程序本身的渲染逻辑是正常的,只是Win32的图像拷贝操作不能正确处理自定义的内存对齐。

为了解决这个问题,考虑了两种可能的方案:一是通过修改后备缓冲区的大小为标准的1920x1080分辨率,避免涉及pitch问题;二是在渲染过程中模拟分辨率大小,假装使用不同的分辨率进行渲染,但保留原有的后备缓冲区设置。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game.cpp:将游戏的 DrawBuffer 尺寸硬编码,以测试我们对任意分辨率的支持

为了处理图像渲染中的问题,决定通过强制设置一个标准的分辨率,即使Windows的后备缓冲区已经适应了某种尺寸,也保持渲染在一个质数模式下运行。这种方法旨在确保即使在Windows的渲染系统有固定的缓冲区大小时,仍然能够支持不同的、不规则的分辨率,以确保渲染效果不受影响。

此外,为了便于后续的调试和测试,在代码中添加了一个注释,提醒在需要时启用此设置,以测试渲染器在处理不规则缓冲区大小时的表现。这种调整确保了在实际渲染过程中,系统能够灵活应对不同的分辨率和缓冲区要求,避免了由于分辨率不一致导致的问题。
在这里插入图片描述

你会检查缓存别名吗?这么多对齐情况下,缓存别名的发生可能性更大

目前无法快速检查缓存别名问题,因为缺乏获取性能信息的能力。虽然最终有可能解决这个问题,但现在没有相关工具来进行检查。缓存别名(cache aliasing)通常会由于内存对齐不当或不同内存区域的重复映射导致性能下降,但目前缺乏直接的性能数据来验证是否存在这种问题。因此,虽然理论上可能会遇到缓存别名问题,但现在无法做出准确的判断。

缓存关联性别名…

关于缓存别名(cache aliasing)的问题,虽然现在无法快速检查,因为我们缺少获取性能信息的能力,但将来可以实现这一点。我们可以尝试使用Intel的性能收集库,该库能够让我们读取像L2缓存未命中之类的信息。如果记得没错的话,这些数据中应该也包含缓存别名的信息。通过这些数据,我们实际上可以查看缓存别名的情况。

边缘周围的额外像素是为了处理双线性过滤,你会去掉吗?

关于处理双线性过滤时在边缘添加的额外像素,可能不会移除它。我们可能会在某个时间点,比如这周,检查一下关于像素中心和其他相关的处理方式,但是否修复这个问题还不确定。根据我们处理位图的方式,我们可能会选择始终对位图进行填充,以便能够获取额外的像素数据。

编译时将该部分 #if 0 掉,并一窥 4K 艺术的未来

需要更新艺术包以包含完整分辨率的资源。之前为了适应没有缩放功能的情况,将资源进行了下采样,所有资源目前都是经过下采样的。现在,考虑到游戏将运行在1920x1080分辨率,我计划将这些资源调整为适当的尺寸,并将其缩小为2倍的大小。这个更新可能会在几周后进行,等调试输出完成后再开始进行这项工作。

你会保持两行渲染在不同超线程上运行吗?

目前还没有将两条线渲染在不同的超线程上,原因是我们还没有实施这个功能。事实上,可能我们不再需要实现这一点,因为渲染已经非常快速,超出预期,因此将其并行化可能对性能提升影响不大。当前的渲染方式已经足够快速,单线程同时处理两条线。

之所以目前没有实现多线程渲染,是因为在使用作业队列时,确保同一个核心能够同时处理两个相同的瓦片并交错使用线程是比较困难的,这在实现上存在一些挑战。因此,考虑到渲染已经很高效,可能不需要再做这个优化。

也许干脆就放弃每隔一行渲染的方式,直接优化现有渲染方式,这样可能会更简单有效。

所以这款游戏将会是一款 4K 的《塞尔达传说1》风格的游戏?

有些人问游戏是否会是4K的《塞尔达风格》游戏,实际上游戏并不会是4K的。我让美术资源做成了4K分辨率,因为当时不确定需要怎样精确缩放它们。实际游戏的分辨率将会是1920x1080。如果将来几年,大家都拥有4K显示器,且显卡能够稳定输出4K时,可能会发布一版4K版本的游戏,包含4K的美术资源。

不过,目前大多数人并没有4K显示器,更不用说是否有能够运行4K的机器了。所以,目前并不会将4K作为游戏的目标,尽管将来或许可以考虑支持4K,只是目前这并不现实。

评估我们的进展,并展望渲染器优化的未来

今天的工作基本完成了,渲染方面的改进已经做到了一个新的阶段,现在它可以正常使用,因为不再有奇怪的瓦片边界问题,这点非常好。接下来,我们需要在明天继续进行一些优化。首先,需要检查一下代码中的其他方面,特别是渲染队列的处理方式,确保像素中心和纹理中心的计算没有问题。现在已经做了很多工作,所以我们要再检查一遍,确保没有意外的错误。

另外,由于排序问题一直是一个让人困扰的点,可能会考虑把它也放到明天的工作里,看看能否优化这一部分。很多人对排序处理不满意,可能需要进行一些调整。

总的来说,明天的目标是清理渲染代码,确保所有的线程工作正常,并且优化了一个Blit操作(现在它已经变成了一个纹理映射器)。在完成这些后,我们将进一步检查整个渲染器的性能表现,看看目前的内存吞吐量,并与最大吞吐量进行对比,以了解当前的性能水平。

今天就这个地方有疑问不知道为什么会段错误

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/25115.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

leetcode 73. 矩阵置零

题目如下 数据范围 如果一个点m(i,j) 0其中i j都大于0那么按照题目要求对应的m[0][j] m[i][0]都要赋值为0. 所以我们可以令第一行和第一列作为标记是否对应的列和行需要置为0. 又因为我们没法判断第一行和第一列所以需要额外两个变量标记第一列和第二列。 这样就可以满足题…

deepseek-r1-centos-本地服务器配置方法

参考: 纯小白 Centos 部署DeepSeek指南_centos部署deepseek-CSDN博客 https://blog.csdn.net/xingxin550/article/details/145574080 手把手教大家如何在Centos7系统中安装Deepseek,一文搞定_centos部署deepseek-CSDN博客 https://blog.csdn.net/soso67…

机器学习:强化学习的epsilon贪心算法

强化学习(Reinforcement Learning, RL)是一种机器学习方法,旨在通过与环境交互,使智能体(Agent)学习如何采取最优行动,以最大化某种累积奖励。它与监督学习和无监督学习不同,强调试错…

比创达电子科技-EMC干货之防静电技术

EMC干货之防静电技术 什么是静电放电 两个具有不同静电电位的物体,由于直接接触或静电场感应引起两物体间的静电电荷的转移,静电电场的能量达到一定程度后,击穿其间介质而进行放电的现象就是静电放电,简称为ESD(Electro Static Discharge)。 静电产生的原…

JavaWeb-ServletContext应用域接口

文章目录 ServletContext接口简介获取一个ServletContext对象ServletContext接口中的相关方法获取应用域配置参数关于应用域参数的配置要求getContextPath获取项目路径getRealPath获取真实路径log系列方法添加相关日志增删查应用域属性 ServletContext接口简介 ServletContext…

C语言(15)-------------->一维数组

这篇文章介绍的是数组的定义、创建、初始化、使用,在数组中输入内容并输出数组中的内容,并探讨了数组在内存中的存储。里面有些内容建议大家参考下面的一些文章,有助于加深大家对于C语言的理解: C语言(2)-…

AI学习第六天-python的基础使用-趣味图形

在 Python 编程学习过程中,turtle库是一个非常有趣且实用的工具,它可以帮助我们轻松绘制各种图形。结合for循环、random模块以及自定义方法等知识点,能够创作出丰富多彩的图案。下面就来分享一下相关的学习笔记。 一、基础知识点回顾 &…

线程安全问题

线程安全问题是指在多线程环境下,当多个线程同时访问共享资源时,可能出现的错误或不可预测的行为。以下是对其的理解: 1. 根本原因 线程安全问题的根本原因是多个线程对共享资源的并发访问。如果多个线程对共享资源进行读写操作&#xff0c…

ubuntu终端指令集 shell编程基础(一)

磁盘指令 连接与查看:磁盘与 Ubuntu 有两种连接方式;使用ls /dev/sd*查看是否连接成功,通过df系列指令查看磁盘使用信息。若 U 盘已挂载,相关操作可能失败,需用umount取消挂载。磁盘操作:使用sudo fdisk 磁…

第十四届蓝桥杯Scratch11月stema选拔赛真题——小猫照镜子

编程实现: 小猫照镜子。(背景非源素材) 具体要求: 1). 运行程序,角色、背景如图所示; 完整题目可点击下方链接查看,支持在线编程~ 小猫照镜子_scratch_少儿编程题库学习中心-嗨信奥https://www.hixinao.com/tiku/s…

Sublime Text4安装、汉化

-------------2025-02-22可用---------------------- 官方网址下载:https://www.sublimetext.com 打开https://hexed.it 点击打开文件找到软件安装目录下的 ctrlf 查找 8079 0500 0f94 c2右边启用替换替换为:c641 0501 b200 90点击替换按钮 替换完成后 另存为本地…

C++20的指定初始化器(Designated Initializers)

文章目录 指定初始化器的使用条件语法嵌套结构体的初始化数组的指定初始化注意事项优势 C20引入了**指定初始化器(Designated Initializers)**这一特性,允许在初始化结构体、联合体或类的对象时,明确指定成员变量的初始化值&#…

Redis安装及其AnotherRedisDesktopManagera安装使用

一、Redis安装 1. 下载Redis安装包 通过网盘分享的文件:Redis 链接: https://pan.baidu.com/s/1elAT8mk3EIoYQQ3WoVVoNg?pwd7yrz 提取码: 7yrz 2. 解压Redis安装包 下载完成后,将Redis安装包解压到一个指定的目录,例如:C:\Re…

51c嵌入式~电路~合集13

我自己的原文哦~ https://blog.51cto.com/whaosoft/12317946 一、造成PCB焊接缺陷的原因 电路板孔可焊性不好,将会产生虚焊缺陷,影响电路中元件的参数,导致多层板元器件和内层线导通不稳定,引起整个电路功能失效。 所谓可焊性…

Lindorm作为AI搜索基础设施,助力Kimi智能助手升级搜索体验

Kimi智能助手开启“长文本”时代,K系列强化学习模型持续进化中 2023年10月,月之暗面(Moonshot AI)旗下的Kimi智能助手,带着支持输入20万汉字的能力正式发布,提升了全球市场上产品化大模型服务支持的上下文输…

图数据库 | 24、如何进行正确性验证?

图数据库计算和查询结果的正确性,这个重要性当然是不言而喻的! 老夫之前也写文章讲过,今天再手书一篇,旨在向大家系统地介绍一下图数据库查询与计算到底如何进行正确性验证!!! 图数据库中的操…

【二分查找 图论】P8794 [蓝桥杯 2022 国 A] 环境治理|普及

本文涉及的基础知识点 本博文代码打包下载 C二分查找 C图论 [蓝桥杯 2022 国 A] 环境治理 题目描述 LQ 国拥有 n n n 个城市,从 0 0 0 到 n − 1 n - 1 n−1 编号,这 n n n 个城市两两之间都有且仅有一条双向道路连接,这意味着任意两…

vue写一个登录页面

目录 一、安装ui库二、路由跳转三、页面 一、安装ui库 element plus库 Element Plus 是 Element UI 的升级版本,专为 Vue 3.x 设计。它继承了 Element UI 的优秀特性,同时针对 Vue 3 的新特性(如 Composition API、Teleport 等)进…

和鲸科技携手四川气象,以 AI 的力量赋能四川气象一体化平台建设

气象领域与农业、能源、交通、环境科学等国计民生关键领域紧密相连,发挥着不可替代的重要作用。人工智能技术的迅猛发展,为气象领域突破困境带来了新的契机。AI 技术能够深度挖掘气象大数据中蕴含的复杂信息,助力人类更精准地把握自然规律&am…

Ubuntu下QT安装和调试的常见问题(一)__could_not_dertermine_which_make

前言 Ubuntu下QT的安装会有一些奇怪的问题出现,并没有像Windows下Visual Studio的安装那么直接就可以使用那么方便,本文就“make”挂接的问题,给出一些小的感受。 1、问题的提出 很多问题的解答,AI无论是上文心一言,还…