游戏引擎学习第10天

视频参考:https://www.bilibili.com/video/BV1LyU3YpEam/

介绍intel architecture reference manual

地址:https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
在这里插入图片描述

RDTS(读取时间戳计数器)指令是 x86/x86_64 架构中的一条汇编指令,用于读取处理器的**时间戳计数器(TSC)**的当前值。TSC 是一个高精度的计数器,它会在每个 CPU 时钟周期中递增。该计数器通常用于性能测量、时间间隔计算或基准测试等场景。

RDTSRDTSCP 说明:

  • RDTS(读取时间戳计数器):该指令用于读取当前的时间戳计数器,并将其值存储到寄存器中(在 32 位模式下是 EDX:EAX,在 64 位模式下是 RDX:RAX)。
  • RDTSCP:与 RDTS 类似,但它在读取时间戳时会确保指令执行的顺序性,即它会先确保所有之前的指令都完成,然后再读取 TSC,确保获取的 TSC 值与执行的时刻同步。

汇编示例:

以下是如何使用 RDTS 指令来读取 TSC 值的示例:

; 假设使用 64 位模式
rdtsc                ; 读取时间戳计数器
mov rax, rdx         ; 将高 64 位的 TSC 值移动到 RAX
mov rbx, rax         ; 将低 64 位的 TSC 值移动到 RBX(例如)

在这个例子中:

  • rdtsc 指令将 TSC 的值存储到 EDX:EAX(32 位模式)或 RDX:RAX(64 位模式)寄存器。
  • mov rax, rdxmov rbx, rax 是将读取到的 TSC 值存储到 raxrbx 寄存器,以便后续使用。

使用场景:

  1. 性能测量
    你可以使用 TSC 来测量时间间隔,精度非常高。通过在两个时间点读取 TSC 值,然后计算它们的差值,可以得到操作的耗时(单位是 CPU 时钟周期)。

    uint64_t start, end;
    start = __rdtsc();  // 获取开始时的 TSC 值
    // 执行要测量的操作
    end = __rdtsc();    // 获取结束时的 TSC 值
    uint64_t elapsed = end - start; // 计算两次 TSC 读取的差值,得到执行时间(单位:CPU 时钟周期)
    
    • __rdtsc() 是许多编译器提供的内置函数(如 GCC 和 MSVC),用于访问 TSC。
  2. 代码执行性能分析
    在高性能应用程序中,RDTS 被广泛应用于低级性能分析,来测量某段代码的执行时间。

  3. 高精度计时器
    当你需要高精度计时时,RDTS 可以用来测量非常小的时间间隔,因为它可以在每个 CPU 时钟周期内递增。

需要注意的事项:

  • 非统一性:不同核心上的 TSC 值可能不同,尤其是在旧款处理器上,TSC 并不是完全一致的。现代处理器通常会有一个不随 CPU 频率变化的恒定 TSC,但这并非总是保证的。

  • 频率变化:如果 CPU 的频率发生变化(例如,由于节能功能),TSC 值可能不会均匀递增,除非使用具有不变 TSC 的处理器。

  • 核心间同步:由于每个核心的 TSC 独立递增,访问不同核心的 TSC 值时,可能会得到不同的结果。为了解决这个问题,通常建议使用 RDTSCP,因为它会确保读取到的 TSC 值与指令的执行时刻完全同步。

RDTSCP 示例:

RDTSCPRDTS 的改进版本,它确保读取的 TSC 值对应于程序执行的具体时刻,并且它在执行时会保证指令的顺序性,因此在多核处理器中更加可靠。

rdtscp
; EAX: TSC 的低 32 位, EDX: 高 32 位
; ECX: CPU ID(可选)

rdtscp 中,除了返回 TSC 值外,它还将处理器的 ID 存储在 ECX 中,且保证 TSC 值的读取是同步的。

总结:

RDTS 是一种低级的、高精度的方式,用于访问处理器的时间戳计数器。它广泛用于性能分析、基准测试和高精度计时。需要注意的是,在使用时可能会受到 CPU 频率变化和多核同步等问题的影响,在这种情况下,RDTSCP 更加可靠,因为它保证了指令执行的同步性。

QueryPerformanceCounter

QueryPerformanceCounter 是 Windows 操作系统中的一个高精度计时函数,用于获取系统中的高精度性能计数器的当前值。它可以用于精确测量时间间隔,通常用于性能分析和计时操作。

函数原型:

BOOL QueryPerformanceCounter(LARGE_INTEGER *lpPerformanceCount);

参数:

  • lpPerformanceCount: 指向一个 LARGE_INTEGER 类型的变量的指针,用于接收性能计数器的当前值。LARGE_INTEGER 是一个 64 位的结构,用来存储计数器的值。

返回值:

  • 非零值:表示函数调用成功,*lpPerformanceCount 将包含当前性能计数器的值。
  • 零值:表示函数调用失败,通常是因为不支持高精度计时器。此时,可以使用 GetLastError() 获取错误代码。

使用说明:

QueryPerformanceCounter 返回的是系统的高精度计数器值,通常以时钟周期为单位。它比 timeGetTime 等其他基于毫秒的计时函数具有更高的精度,适用于测量非常短的时间间隔。

典型应用:

QueryPerformanceCounter 常用于高精度的时间测量和性能分析,例如计算一段代码执行的耗时。

示例代码:

以下是一个简单的示例,演示如何使用 QueryPerformanceCounter 来测量代码块的执行时间:

#include <windows.h>
#include <iostream>int main() {LARGE_INTEGER frequency;QueryPerformanceFrequency(&frequency);  // 获取计数器的频率,单位是每秒的计数次数LARGE_INTEGER start, end;QueryPerformanceCounter(&start);  // 获取当前计数器的值,表示开始时间// 需要测量的操作for (volatile int i = 0; i < 1000000; ++i);QueryPerformanceCounter(&end);  // 获取当前计数器的值,表示结束时间// 计算时间差,单位是秒double elapsed = static_cast<double>(end.QuadPart - start.QuadPart) / frequency.QuadPart;std::cout << "Elapsed time: " << elapsed << " seconds." << std::endl;return 0;
}

在这里插入图片描述

LARGE_INTEGER 是 Windows API 中定义的一个联合体类型,用于存储 64 位的整数值。它的设计目的是提供一种跨平台、兼容不同系统架构的方式来处理 64 位的整数数据。下面是该类型的具体结构及其成员的详细解释。

定义结构:

typedef union _LARGE_INTEGER {struct {DWORD LowPart;  // 低 32 位部分LONG HighPart;  // 高 32 位部分} DUMMYSTRUCTNAME;  // 一种匿名结构的定义struct {DWORD LowPart;  // 低 32 位部分LONG HighPart;  // 高 32 位部分} u;              // 另一种结构定义,作用与 DUMMYSTRUCTNAME 相同LONGLONG QuadPart; // 64 位整数的整体值
} LARGE_INTEGER;

解析:

  1. 联合体 (union)

    • LARGE_INTEGER 是一个 联合体类型union),意味着它的不同成员共享同一内存空间。每个成员的起始地址相同,所以它们都指向相同的内存位置,但每次只能使用其中一个成员。
    • 由于 union 成员共享同一块内存,因此它使得不同的数据视图(如按低高 32 位分解,或作为一个完整的 64 位整数)能够方便地进行访问。
  2. 成员:

    • DUMMYSTRUCTNAMEu:这两个结构体的定义完全相同,都包含 LowPartHighPart 两个字段。

      • LowPart 是一个 32 位的 DWORD(无符号 32 位整数),存储低 32 位数据。
      • HighPart 是一个 32 位的 LONG(带符号 32 位整数),存储高 32 位数据。
    • 你可以通过 DUMMYSTRUCTNAMEu 来访问 LowPartHighPart,这两个结构体只是为了方便访问 64 位整数的低 32 位和高 32 位部分。

    • QuadPart:这是 LARGE_INTEGER 的核心字段,表示一个完整的 64 位整数,类型为 LONGLONGLONGLONG 是一个 64 位的带符号整数类型,用于存储整个 64 位的值。通过 QuadPart,你可以一次性访问和操作这个完整的 64 位整数值。

2. 使用 LowPartHighPart 来访问低 32 位和高 32 位值:
LARGE_INTEGER li;
li.QuadPart = 1234567890123456LL;  // 先赋值整个 64 位整数std::cout << "LowPart: " << li.DUMMYSTRUCTNAME.LowPart << std::endl;
std::cout << "HighPart: " << li.DUMMYSTRUCTNAME.HighPart << std::endl;
  • LowPart 存储的是 64 位数值的低 32 位部分。
  • HighPart 存储的是 64 位数值的高 32 位部分。

常见时间单位及换算关系:

  1. 秒 (Second, s)
  2. 毫秒 (Millisecond, ms)
  3. 微秒 (Microsecond, µs)
  4. 纳秒 (Nanosecond, ns)
  5. 皮秒 (Picosecond, ps)
  • 秒 (s) 是基本单位。
  • 毫秒 (ms) = 10⁻³ s。
  • 微秒 (µs) = 10⁻⁶ s。
  • 纳秒 (ns) = 10⁻⁹ s。
  • 皮秒 (ps) = 10⁻¹² s。

在这里插入图片描述

计算FPS

要理解为什么 int32 FPS = PerfCountFrequency / CounterElapsed; 这样计算 FPS(帧率),我们需要结合 PerfCountFrequencyCounterElapsed 的含义来推导。

背景解释

  • PerfCountFrequency:这是计时器的频率,表示每秒钟计时器的更新次数。它通常由 QueryPerformanceFrequency 函数返回。

    • 举例:如果 PerfCountFrequency 是 1,000,000,那么每秒钟计时器会增加 1,000,000 次(即 1 MHz)。
  • CounterElapsed:这是两个 QueryPerformanceCounter 调用之间的增量,表示自上次查询以来计时器更新的次数。

    • 举例:假设我们经过了 10,000 个计时器周期,那么 CounterElapsed 就是 10,000。

目标:计算每秒的帧数(FPS)

我们想计算每秒钟的帧数,即每秒钟能够渲染多少帧。

推导过程

  1. 每帧的时间(秒)

    假设每次渲染一帧时,从上一次渲染到这一次渲染之间经过了 CounterElapsed 个计时器周期。那么,经过这些周期的时间长度(单位:秒)可以通过以下公式计算:
    Frame Time (秒) = CounterElapsed PerfCountFrequency \text{Frame Time (秒)} = \frac{\text{CounterElapsed}}{\text{PerfCountFrequency}} Frame Time ()=PerfCountFrequencyCounterElapsed
    其中:

    • CounterElapsed 是当前帧经过的计时器周期数。
    • PerfCountFrequency 是计时器的频率,即每秒钟计时器的更新次数。

    这就是每帧所需的时间(单位:秒)。

  2. 计算每秒钟的帧数(FPS)

    帧率(FPS)表示每秒钟渲染的帧数。为了计算 FPS,我们可以通过每帧所需的时间来推算每秒钟的帧数:
    FPS = 1 Frame Time (秒) \text{FPS} = \frac{1}{\text{Frame Time (秒)}} FPS=Frame Time ()1
    Frame Time (秒) 代入,得到:
    FPS = 1 CounterElapsed PerfCountFrequency \text{FPS} = \frac{1}{\frac{\text{CounterElapsed}}{\text{PerfCountFrequency}}} FPS=PerfCountFrequencyCounterElapsed1
    进一步简化:
    FPS = PerfCountFrequency CounterElapsed \text{FPS} = \frac{\text{PerfCountFrequency}}{\text{CounterElapsed}} FPS=CounterElapsedPerfCountFrequency

    这就是我们需要的公式,表明 FPS 等于计时器的频率除以当前帧经过的计时器周期数。

具体解释:

  • PerfCountFrequency:每秒钟计时器增加的次数。它描述了计时器的“速度”,即每秒钟有多少个计时器周期。
  • CounterElapsed:两个 QueryPerformanceCounter 调用之间的计时器增量。它表示经过了多少个计时器周期。

由于 FPS 是每秒渲染的帧数,因此通过 PerfCountFrequency / CounterElapsed,我们得到的就是每秒钟显示的帧数。

举例:

假设:

  • PerfCountFrequency = 1,000,000(即每秒 1,000,000 个计时器周期)
  • CounterElapsed = 10,000(即当前帧经过了 10,000 个计时器周期)

那么:
FPS = 1 , 000 , 000 10 , 000 = 100 FPS \text{FPS} = \frac{1,000,000}{10,000} = 100 \, \text{FPS} FPS=10,0001,000,000=100FPS
这意味着,在当前帧上,我们的帧率是 100 帧每秒。

总结:

int32 FPS = PerfCountFrequency / CounterElapsed; 这行代码的作用是通过已知的计时器频率(PerfCountFrequency)和经过的计时器周期数(CounterElapsed),计算出每秒钟渲染的帧数(FPS)。

在这里插入图片描述

视频中有提到wsprintf函数

wsprintf 是 Windows API 中用于格式化字符串的函数,它类似于标准 C 函数 sprintf,但可以处理 Unicode 字符(宽字符)。它将格式化后的字符串写入一个指定的缓冲区中,因此需要注意缓冲区的大小以避免潜在的内存问题,如缓冲区溢出(buffer overflow)。具体来说,使用 wsprintf 时需要注意以下几个方面:

1. 缓冲区溢出(Buffer Overflow)

wsprintf 函数会根据格式化字符串的内容生成输出字符串并将其写入提供的缓冲区。假如格式化字符串所需要的空间超过了缓冲区的大小,就会发生缓冲区溢出,导致覆盖内存中的其他数据,从而可能引发程序崩溃或未定义行为。

例如:
char Buffer[10];
wsprintf(Buffer, "Millisecond/Frame: %dms, %dFPS\n", MillisecondPerFrame, FPS);

假设 MillisecondPerFrameFPS 的值较大,格式化字符串 "Millisecond/Frame: 123ms, 456FPS\n" 可能需要更多的内存空间。如果缓冲区 Buffer 的大小不足以存储这些数据,就会导致缓冲区溢出。

2. 使用 wsprintf 时的建议

  • 始终确保缓冲区足够大:如果不确定格式化后字符串的大小,最好为缓冲区分配足够的空间,或者使用更安全的 swprintf_snwprintf
  • 避免使用固定大小的缓冲区:为避免缓冲区溢出问题,可以动态分配缓冲区,或者使用 swprintf 来指定最大字符数。

3. 替代方案

使用 swprintf_snwprintf 来避免缓冲区溢出的问题。这些函数允许你指定一个最大字符数,避免了不小心写入超出缓冲区大小的情况。

例如:
wchar_t Buffer[256];  // 给缓冲区足够的空间
swprintf(Buffer, sizeof(Buffer)/sizeof(Buffer[0]), L"Millisecond/Frame: %dms, %dFPS\n", MillisecondPerFrame, FPS);

在这个例子中,swprintf 会确保格式化后的字符串不会超出 Buffer 的大小,从而避免缓冲区溢出。

4. 格式化问题

确保传入 wsprintf 的格式化字符串与提供的数据类型匹配。例如,如果你传递的是整数类型,使用 %d%ld;如果是字符串,使用 %s 等等。错误的格式化字符串会导致未定义行为。

例如:
wsprintf(Buffer, L"Value: %d", someValue);  // 正确
wsprintf(Buffer, L"Value: %s", someValue);  // 错误:如果 someValue 是整数类型,会导致问题

5. 类型安全问题

如果传递给 wsprintf 的参数类型不匹配格式化字符串中的类型,可能导致类型不匹配或内存损坏。例如,使用 %s 格式符来打印整数会引发问题。

示例:
int value = 123;
wsprintf(Buffer, L"Value: %s", value);  // 错误,因为 %s 是用来打印字符串的

正确的方式是:

wsprintf(Buffer, L"Value: %d", value);  // 正确,使用 %d 来打印整数

6. 字符集和宽字符问题

wsprintf 是用于宽字符(Unicode)的版本。如果你在多字符集项目中工作,需要确保你的字符类型与目标字符串类型一致。

  • 对于宽字符,使用 wsprintf(这将写入 wchar_t 类型的字符串)。
  • 对于常规字符,可以使用 sprintf(写入 char 类型的字符串)。

7. 不推荐使用 wsprintf

wsprintf 已经过时,并且没有提供缓冲区溢出保护,因此不再推荐使用。在现代代码中,建议使用 swprintf_snwprintf 或 C++ 的 std::wstringstd::wstringstream 等更安全的替代方案。

总结

  • 缓冲区溢出:确保为缓冲区分配足够的内存,并确保格式化后的字符串不会超出缓冲区大小。
  • 类型匹配:确保传入 wsprintf 的参数与格式化字符串中的类型匹配。
  • 优先使用更安全的替代函数:如 swprintf_snwprintf 等,这些函数允许指定缓冲区的最大长度,从而减少溢出风险。

在实际编程中,尽量避免使用 wsprintf,而是使用更现代的、安全的替代方案,确保程序的安全性和稳定性。

rdtsc

__rdtsc() 是 Microsoft Visual C++(MSVC)编译器提供的一个内置函数,用于读取处理器的时间戳计数器(TSC)。它返回自系统启动以来的时钟周期数。这个函数是通过调用 rdtsc 汇编指令来实现的,通常用于测量程序执行的时间或性能分析。

函数原型

DWORD64 __rdtsc(void);
  • 返回类型DWORD64,即一个 64 位整数,表示从计算机开机以来的时钟周期数。返回值的单位是 CPU 时钟周期,而不是实际的时间单位(例如秒或毫秒)。

  • 参数__rdtsc 没有任何参数。

使用方法

__rdtsc 是一个 MSVC 编译器的内置函数,不需要显式地链接外部库。你可以直接调用该函数来获取时间戳计数器的当前值。该函数读取的值表示自计算机启动以来的 CPU 时钟周期数,因此它是一个累积计数器。

示例代码

#include <iostream>
#include <windows.h>// 定义一个宏,简化 __rdtsc 的调用
#define ReadTimeStampCounter() __rdtsc()int main() {// 获取起始时的时钟周期数DWORD64 start = ReadTimeStampCounter();// 模拟一些计算任务for (volatile int i = 0; i < 1000000; ++i);// 获取结束时的时钟周期数DWORD64 end = ReadTimeStampCounter();// 计算消耗的时钟周期数DWORD64 elapsedCycles = end - start;std::cout << "Elapsed CPU cycles: " << elapsedCycles << std::endl;return 0;
}
  • __rdtsc() 是 MSVC 编译器提供的内置函数,用于返回自系统启动以来的 CPU 时钟周期数。
  • 通过计算 __rdtsc() 在两个不同时间点的返回值差异,你可以估算代码执行的时钟周期数。
  • 由于它是基于 CPU 时钟的计数器,使用时需要考虑 CPU 的时钟频率以及多核处理器的同步问题。

在这里插入图片描述

在这里插入图片描述

MULPDMULPS 是 x86 架构中 SSE(Streaming SIMD Extensions) 指令集中的两个指令,用于对浮点数进行乘法操作。它们的区别在于操作数的类型和执行的精度:

1. MULPD (Multiply Packed Double-Precision)

  • 操作数类型:双精度浮点数(double)。
  • 每次操作的数据:它是一个 “packed” 指令,意味着它同时操作多个数据。MULPD 处理 两个 双精度浮点数(64 位每个)。
  • 功能:将两个双精度浮点数相乘,存储结果。
示例:

假设寄存器 XMM1 包含两个双精度浮点数:XMM1[0]XMM1[1],然后执行 MULPD 指令:

MULPD XMM1, XMM2

它将执行以下操作:

  • XMM1[0] = XMM1[0] * XMM2[0]
  • XMM1[1] = XMM1[1] * XMM2[1]

这意味着 MULPD 会在两个双精度浮点数上执行乘法并更新寄存器。

2. MULPS (Multiply Packed Single-Precision)

  • 操作数类型:单精度浮点数(float)。
  • 每次操作的数据:它是一个 “packed” 指令,意味着它同时操作多个数据。MULPS 处理 四个 单精度浮点数(32 位每个)。
  • 功能:将四个单精度浮点数相乘,存储结果。
示例:

假设寄存器 XMM1 包含四个单精度浮点数:XMM1[0]XMM1[1]XMM1[2]XMM1[3],然后执行 MULPS 指令:

MULPS XMM1, XMM2

它将执行以下操作:

  • XMM1[0] = XMM1[0] * XMM2[0]
  • XMM1[1] = XMM1[1] * XMM2[1]
  • XMM1[2] = XMM1[2] * XMM2[2]
  • XMM1[3] = XMM1[3] * XMM2[3]

这意味着 MULPS 会在四个单精度浮点数上执行乘法并更新寄存器。

总结区别:

  • MULPD:处理双精度浮点数(64 位),每次操作两个浮点数。
  • MULPS:处理单精度浮点数(32 位),每次操作四个浮点数。

它们的应用场景通常根据浮点数的精度要求来选择,如果需要更高精度的浮点数运算,就会选择 MULPD(双精度);如果对精度要求较低,且希望进行更高效的并行计算,就可以使用 MULPS(单精度)。

SIMD(Single Instruction, Multiple Data)是一种 并行计算 的技术,它允许单一指令同时对多个数据元素进行相同的操作,从而显著提高处理器在处理大量数据时的效率,尤其在需要重复相同计算的任务中,如图像处理、科学计算、音视频处理等领域。

SIMD 的工作原理:

在传统的 标量处理 中,处理器一次只能对单个数据元素执行操作。而 SIMD 技术通过扩展处理器的指令集,使得它能够一次对多个数据元素进行相同的操作,从而大大提高运算速度和效率。

基本概念:

  • Single Instruction:单条指令,指所有数据都在同一条指令下进行处理。
  • Multiple Data:多数据,指每条指令作用于多个数据元素,通常是在一个向量或矩阵中的数据。

SIMD 操作

SIMD 通常通过扩展指令集来实现。它的主要优点是可以通过处理器的一条指令,同时对多个数据进行相同的运算,这样就大大加快了计算速度。

典型的 SIMD 实现

在现代的处理器中,SIMD 技术是通过 SIMD 指令集 实现的,这些指令集有许多不同的实现,最常见的有:

  1. MMX (MultiMedia Extensions):最早的 SIMD 扩展,主要用于处理多媒体数据,如视频和音频。
  2. SSE (Streaming SIMD Extensions):Intel 的 SIMD 扩展,支持更高效的浮点数计算。
  3. AVX (Advanced Vector Extensions):AVX 是 SSE 的扩展,支持更宽的向量处理,增强了浮点和整数计算的性能。
  4. NEON:ARM 处理器的 SIMD 扩展,类似于 Intel 的 SSE,用于提升 ARM 设备上的多媒体性能。

SIMD 指令

SIMD 指令集提供了多种指令,用于同时处理多个数据元素。以下是一些常见的 SIMD 指令的例子:

  • 加法

    • ADDPS:单精度浮点数加法指令(SSE)
    • ADDPD:双精度浮点数加法指令(SSE)
  • 乘法

    • MULPS:单精度浮点数乘法指令(SSE)
    • MULPD:双精度浮点数乘法指令(SSE)
  • 比较

    • CMPPS:单精度浮点数比较指令(SSE)
    • CMPPD:双精度浮点数比较指令(SSE)
  • 加载与存储

    • MOVAPS:加载/存储对齐的单精度浮点数数据(SSE)
    • MOVAPD:加载/存储对齐的双精度浮点数数据(SSE)
  • 平方根

    • SQRTPS:单精度浮点数平方根(SSE)
    • SQRTPD:双精度浮点数平方根(SSE)

SIMD 的优势

  1. 提高性能
    SIMD 可以显著提高多媒体、图像处理、科学计算等领域的性能。比如在图像处理时,多个像素的颜色值可以同时处理,而不必一帧一帧地计算。

  2. 降低指令数
    通过一次性对多个数据元素执行操作,SIMD 可以减少程序中需要执行的指令数,从而降低执行时间。

  3. 并行计算
    SIMD 是一种“数据并行”模型,即同一操作被同时应用于多个数据项,适合处理大规模的数据集(如大矩阵运算、向量运算等)。

SIMD 应用场景

  1. 图像处理
    SIMD 可以大大提高图像和视频处理中的效率,特别是在进行色彩转换、滤波、旋转等常见操作时。

  2. 音视频编解码
    音视频编码和解码通常需要对大量的数据进行相同的计算,SIMD 在此类操作中可以显著加速。

  3. 科学计算
    在科学计算中,SIMD 可以加速大规模矩阵运算、向量处理和矩阵乘法等。

  4. 机器学习
    许多机器学习任务(如神经网络的前向传播)需要大量的并行数据处理,SIMD 能加速这些计算过程。

SIMD 的限制

尽管 SIMD 提供了显著的性能提升,但它也有一些局限性:

  1. 数据依赖
    如果操作的数据存在相互依赖(如循环中一个数据的计算依赖于上一个数据的结果),SIMD 就不能有效地并行化这些计算。

  2. 编程复杂度
    编写 SIMD 优化代码通常较为复杂,特别是涉及到跨平台和不同硬件架构时。开发者需要特别注意数据对齐和指令选择等细节。

  3. 硬件支持
    并非所有的硬件都支持高级的 SIMD 指令集。虽然现代的 CPU 大都支持 SSE 和 AVX 等指令集,但一些较老的处理器可能没有这些扩展。

总结

SIMD 是一种非常强大的技术,通过并行化数据处理,可以显著提高计算密集型任务的性能,特别是在多媒体、图像处理、科学计算和机器学习等领域。尽管编程上有一定的挑战,但它的优势是显而易见的,尤其在需要大量并行计算的应用场景中。

// game.cpp : Defines the entry point for the application.
//#include <cmath>
#include <cstdint>
#include <dsound.h>
#include <stdint.h>
#include <stdio.h>
#include <windows.h>
#include <xinput.h>#define internal static        // 用于定义内翻译单元内部函数
#define local_persist static   // 局部静态变量
#define global_variable static // 全局变量
#define Pi32 3.14159265359typedef uint8_t uint8;
typedef uint16_t uint16;
typedef uint32_t uint32;
typedef uint64_t uint64;typedef int8_t int8;
typedef int16_t int16;
typedef int32_t int32;
typedef int64_t int64;
typedef int32 bool32;typedef float real32;
typedef double real64;struct win32_offscreen_buffer {BITMAPINFO Info;void *Memory;// 后备缓冲区的宽度和高度int Width;int Height;int Pitch;int BytesPerPixel;
};
// 添加这个去掉重复的冗余代码
struct win32_window_dimension {int Width;int Height;
};struct win32_sound_output {// 音频测试uint32 RunningSampleIndex; // 样本索引int16 ToneVolume;          // 音量int SamplesPerSecond;      // 采样率:每秒采样48000次int ToneHz;                // 波频率:256 Hzint WavePeriod;            // 波周期(样本数)int HalfWavePeriod;        // 波半周期(样本数)int BytesPerSample;        // 一个样本的大小int SecondaryBufferSize;   // 缓冲区大小real32 tSine;              // 保存当前的相位int LatencySampleCount;
};// TODO: 全局变量
// 用于控制程序运行的全局布尔变量,通常用于循环条件
global_variable bool GloblaRunning;
// 用于存储屏幕缓冲区的全局变量
global_variable win32_offscreen_buffer GlobalBackbuffer;
global_variable LPDIRECTSOUNDBUFFER GlobalSecondaryBuffer;/*** @param dwUserIndex // 与设备关联的玩家索引* @param pState // 接收当前状态的结构体*/
#define X_INPUT_GET_STATE(name)                                                \DWORD WINAPI name(DWORD dwUserIndex,                                         \XINPUT_STATE *pState) // 定义一个宏,将指定名称设置为// XInputGetState 函数的类型定义/*** @param dwUserIndex // 与设备关联的玩家索引* @param pVibration  // 要发送到控制器的震动信息*/
#define X_INPUT_SET_STATE(name)                                                \DWORD WINAPI name(                                                           \DWORD dwUserIndex,                                                       \XINPUT_VIBRATION *pVibration) // 定义一个宏,将指定名称设置为// XInputSetState 函数的类型定义typedef X_INPUT_GET_STATE(x_input_get_state); // 定义了 x_input_get_state 类型,为 `XInputGetState`// 函数的类型
typedef X_INPUT_SET_STATE(x_input_set_state); // 定义了 x_input_set_state 类型,为 `XInputSetState`// 函数的类型// 定义一个 XInputGetState 的打桩函数,返回值为
// ERROR_DEVICE_NOT_CONNECTED,表示设备未连接
X_INPUT_GET_STATE(XInputGetStateStub) { //return (ERROR_DEVICE_NOT_CONNECTED);
}// 定义一个 XInputSetState 的打桩函数,返回值为
// ERROR_DEVICE_NOT_CONNECTED,表示设备未连接
X_INPUT_SET_STATE(XInputSetStateStub) { //return (ERROR_DEVICE_NOT_CONNECTED);
}// 设置全局变量 XInputGetState_ 和 XInputSetState_ 的初始值为打桩函数
global_variable x_input_get_state *XInputGetState_ = XInputGetStateStub;
global_variable x_input_set_state *XInputSetState_ = XInputSetStateStub;// 定义宏将 XInputGetState 和 XInputSetState 重新指向 XInputGetState_ 和
// XInputSetState_
#define XInputGetState XInputGetState_
#define XInputSetState XInputSetState_// 加载 XInput DLL 并获取函数地址
internal void Win32LoadXInput(void) { //HMODULE XInputLibrary = LoadLibrary("xinput1_4.dll");if (!XInputLibrary) {// 如果无法加载 xinput1_4.dll,则回退到 xinput1_3.dllXInputLibrary = LoadLibrary("xinput1_3.dll");} else {// TODO:Diagnostic}if (XInputLibrary) { // 检查库是否加载成功XInputGetState = (x_input_get_state *)GetProcAddress(XInputLibrary, "XInputGetState"); // 获取 XInputGetState 函数地址if (!XInputGetState) { // 如果获取失败,使用打桩函数XInputGetState = XInputGetStateStub;}XInputSetState = (x_input_set_state *)GetProcAddress(XInputLibrary, "XInputSetState"); // 获取 XInputSetState 函数地址if (!XInputSetState) { // 如果获取失败,使用打桩函数XInputSetState = XInputSetStateStub;}} else {// TODO:Diagnostic}
}#define DIRECT_SOUND_CREATE(name)                                              \HRESULT WINAPI name(LPCGUID pcGuidDevice, LPDIRECTSOUND *ppDS,               \LPUNKNOWN pUnkOuter);
// 定义一个宏,用于声明 DirectSound 创建函数的原型typedef DIRECT_SOUND_CREATE(direct_sound_create);
// 定义一个类型别名 direct_sound_create,代表
// DirectSound 创建函数internal void Win32InitDSound(HWND window, int32 SamplesPerSecond,int32 BufferSize) {// 注意: 加载 dsound.dll 动态链接库HMODULE DSoundLibrary = LoadLibraryA("dsound.dll");if (DSoundLibrary) {// 注意: 获取 DirectSound 创建函数的地址// 通过 GetProcAddress 函数查找 "DirectSoundCreate" 函数在 dsound.dll// 中的地址,并将其转换为 direct_sound_create 类型的函数指针direct_sound_create *DirectSoundCreate =(direct_sound_create *)GetProcAddress(DSoundLibrary,"DirectSoundCreate");// 定义一个指向 IDirectSound 接口的指针,并初始化为 NULLIDirectSound *DirectSound = NULL;if (DirectSoundCreate && SUCCEEDED(DirectSoundCreate(0,// 传入 0 作为设备 GUID,表示使用默认音频设备&DirectSound,// 将创建的 DirectSound 对象的指针存储到// DirectSound 变量中0// 传入 0 作为外部未知接口指针,通常为 NULL))) //{// clang-format offWAVEFORMATEX WaveFormat = {};WaveFormat.wFormatTag = WAVE_FORMAT_PCM; // 设置格式标签为 WAVE_FORMAT_PCM,表示使用未压缩的 PCM 格式WaveFormat.nChannels = 2;          // 设置声道数为 2,表示立体声(两个声道:左声道和右声道)WaveFormat.nSamplesPerSec = SamplesPerSecond; // 采样率 表示每秒钟的样本数,常见值为 44100 或 48000 等WaveFormat.wBitsPerSample = 16;    // 16位音频 设置每个样本的位深为 16 位WaveFormat.nBlockAlign = (WaveFormat.nChannels * WaveFormat.wBitsPerSample) / 8;// 计算数据块对齐大小,公式为:nBlockAlign = nChannels * (wBitsPerSample / 8)// 这里除以 8 是因为每个样本的大小是按字节来计算的,nChannels 是声道数// wBitsPerSample 是每个样本的位数,除以 8 转换为字节WaveFormat.nAvgBytesPerSec =  WaveFormat.nSamplesPerSec * WaveFormat.nBlockAlign;// 计算每秒的平均字节数,公式为:nAvgBytesPerSec = nSamplesPerSec * nBlockAlign// 这表示每秒音频数据流的字节数,它帮助估算缓冲区大小// clang-format on// 函数用于设置 DirectSound 的协作等级if (SUCCEEDED(DirectSound->SetCooperativeLevel(window, DSSCL_PRIORITY))) {// 注意: 创建一个主缓冲区// 使用 DirectSoundCreate 函数创建一个 DirectSound// 对象,并初始化主缓冲区 具体的实现步骤可以根据实际需求补充DSBUFFERDESC BufferDescription = {};BufferDescription.dwSize = sizeof(BufferDescription); // 结构的大小// dwFlags:设置为// DSBCAPS_PRIMARYBUFFER,指定我们要创建的是主缓冲区,而不是次缓冲区。BufferDescription.dwFlags = DSBCAPS_PRIMARYBUFFER;LPDIRECTSOUNDBUFFER PrimaryBuffer = NULL;if (SUCCEEDED(DirectSound->CreateSoundBuffer(&BufferDescription, // 指向缓冲区描述结构体的指针&PrimaryBuffer,     // 指向创建的缓冲区对象的指针NULL                // 外部未知接口,通常传入 NULL))) {if (SUCCEEDED(PrimaryBuffer->SetFormat(&WaveFormat))) {// NOTE:we have finally set the formatOutputDebugString("SetFormat 成功");} else {// NOTE:OutputDebugString("SetFormat 失败");}} else {}} else {}// 注意: 创建第二个缓冲区// 创建次缓冲区来承载音频数据,并在播放时使用// 对象,并初始化主缓冲区 具体的实现步骤可以根据实际需求补充DSBUFFERDESC BufferDescription = {};BufferDescription.dwSize = sizeof(BufferDescription); // 结构的大小// dwFlags:设置为// DSBCAPS_GETCURRENTPOSITION2 |// DSBCAPS_GLOBALFOCUS两个标志会使次缓冲区在播放时更加精确,同时在应用失去焦点时保持音频输出BufferDescription.dwFlags =DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS;BufferDescription.dwBufferBytes = BufferSize; // 缓冲区大小BufferDescription.lpwfxFormat = &WaveFormat; // 指向音频格式的指针if (SUCCEEDED(DirectSound->CreateSoundBuffer(&BufferDescription,     // 指向缓冲区描述结构体的指针&GlobalSecondaryBuffer, // 指向创建的缓冲区对象的指针NULL                    // 外部未知接口,通常传入 NULL))) {OutputDebugString("SetFormat 成功");} else {OutputDebugString("SetFormat 失败");}// 注意: 开始播放!// 调用相应的 DirectSound API 开始播放音频} else {}} else {}
}internal win32_window_dimension Win32GetWindowDimension(HWND Window) {win32_window_dimension Result;RECT ClientRect;GetClientRect(Window, &ClientRect);// 计算绘制区域的宽度和高度Result.Height = ClientRect.bottom - ClientRect.top;Result.Width = ClientRect.right - ClientRect.left;return Result;
}// 渲染一个奇异的渐变图案
internal void RenderWeirdGradient(win32_offscreen_buffer Buffer, int BlueOffset,int GreenOffset) {// TODO:让我们看看优化器是怎么做的uint8 *Row = (uint8 *)Buffer.Memory;      // 指向位图数据的起始位置for (int Y = 0; Y < Buffer.Height; ++Y) { // 遍历每一行uint32 *Pixel = (uint32 *)Row;          // 指向每一行的起始像素for (int X = 0; X < Buffer.Width; ++X) { // 遍历每一列uint8 Blue = (X + BlueOffset);         // 计算蓝色分量uint8 Green = (Y + GreenOffset);       // 计算绿色分量*Pixel++ = ((Green << 8) | Blue);      // 设置当前像素的颜色}Row += Buffer.Pitch; // 移动到下一行}
}// 这个函数用于重新调整 DIB(设备独立位图)大小
internal void Win32ResizeDIBSection(win32_offscreen_buffer *Buffer, int width,int height) {// device independent bitmap(设备独立位图)// TODO: 进一步优化代码的健壮性// 可能的改进:先不释放,先尝试其他方法,再如果失败再释放。if (Buffer->Memory) {VirtualFree(Buffer->Memory, // 指定要释放的内存块起始地址0, // 要释放的大小(字节),对部分释放有效,整体释放则设为 0MEM_RELEASE); // MEM_RELEASE:释放整个内存块,将内存和地址空间都归还给操作系统}// 赋值后备缓冲的宽度和高度Buffer->Width = width;Buffer->Height = height;Buffer->BytesPerPixel = 4;// 设置位图信息头(BITMAPINFOHEADER)Buffer->Info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); // 位图头大小Buffer->Info.bmiHeader.biWidth = Buffer->Width; // 设置位图的宽度Buffer->Info.bmiHeader.biHeight =-Buffer->Height; // 设置位图的高度(负号表示自上而下的方向)Buffer->Info.bmiHeader.biPlanes = 1; // 设置颜色平面数,通常为 1Buffer->Info.bmiHeader.biBitCount =32; // 每像素的位数,这里为 32 位(即 RGBA)Buffer->Info.bmiHeader.biCompression =BI_RGB; // 无压缩,直接使用 RGB 颜色模式// 创建 DIBSection(设备独立位图)并返回句柄// TODO:我们可以自己分配?int BitmapMemorySize =(Buffer->Width * Buffer->Height) * Buffer->BytesPerPixel;Buffer->Memory = VirtualAlloc(0, // lpAddress:指定内存块的起始地址。// 通常设为 NULL,由系统自动选择一个合适的地址。BitmapMemorySize, // 要分配的内存大小,单位是字节。MEM_COMMIT, // 分配物理内存并映射到虚拟地址。已提交的内存可以被进程实际访问和操作。PAGE_READWRITE // 内存可读写);Buffer->Pitch = width * Buffer->BytesPerPixel; // 每一行的字节数// TODO:可能会把它清除成黑色
}// 这个函数用于将 DIBSection 绘制到窗口设备上下文
internal void Win32DisplayBufferInWindow(HDC DeviceContext, int WindowWidth,int WindowHeight,win32_offscreen_buffer Buffer, int X,int Y, int Width, int Height) {// 使用 StretchDIBits 将 DIBSection 绘制到设备上下文中StretchDIBits(DeviceContext, // 目标设备上下文(窗口或屏幕的设备上下文)/*X, Y, Width, Height, // 目标区域的 x, y 坐标及宽高X, Y, Width, Height,*/0, 0, WindowWidth, WindowHeight,   //0, 0, Buffer.Width, Buffer.Height, //// 源区域的 x, y 坐标及宽高(此处源区域与目标区域相同)Buffer.Memory,  // 位图内存指针,指向 DIBSection 数据&Buffer.Info,   // 位图信息,包含位图的大小、颜色等信息DIB_RGB_COLORS, // 颜色类型,使用 RGB 颜色SRCCOPY); // 使用 SRCCOPY 操作符进行拷贝(即源图像直接拷贝到目标区域)
}LRESULT CALLBACK
Win32MainWindowCallback(HWND hwnd, // 窗口句柄,表示消息来源的窗口UINT Message, // 消息标识符,表示当前接收到的消息类型WPARAM wParam, // 与消息相关的附加信息,取决于消息类型LPARAM LParam) { // 与消息相关的附加信息,取决于消息类型LRESULT Result = 0; // 定义一个变量来存储消息处理的结果switch (Message) { // 根据消息类型进行不同的处理case WM_CREATE: {OutputDebugStringA("WM_CREATE\n");};case WM_SIZE: { // 窗口大小发生变化时的消息} break;case WM_DESTROY: { // 窗口销毁时的消息// TODO: 处理错误,用重建窗口GloblaRunning = false;} break;case WM_SYSKEYDOWN: // 系统按键按下消息,例如 Alt 键组合。case WM_SYSKEYUP:   // 系统按键释放消息。case WM_KEYDOWN:    // 普通按键按下消息。case WM_KEYUP: {    // 普通按键释放消息。uint64 VKCode = wParam; // `wParam` 包含按键的虚拟键码(Virtual-Key Code)bool WasDown = ((LParam & (1 << 30)) != 0);bool IsDown = ((LParam & (1 << 30)) == 0);bool32 AltKeyWasDown = (LParam & (1 << 29)); // 检查Alt键是否被按下// bool AltKeyWasDown = ((LParam & (1 << 29)) != 0); //// 检查Alt键是否被按下if (IsDown != WasDown) {if (VKCode == 'W') { // 检查是否按下了 'W' 键} else if (VKCode == 'A') {} else if (VKCode == 'S') {} else if (VKCode == 'D') {} else if (VKCode == 'Q') {} else if (VKCode == 'E') {} else if (VKCode == VK_UP) {} else if (VKCode == VK_DOWN) {} else if (VKCode == VK_LEFT) {} else if (VKCode == VK_RIGHT) {} else if (VKCode == VK_ESCAPE) {OutputDebugStringA("ESCAPE: ");if (IsDown) {OutputDebugString(" IsDown ");}if (WasDown) {OutputDebugString(" WasDown ");}} else if (VKCode == VK_SPACE) {}}if ((VKCode == VK_F4) && AltKeyWasDown) {GloblaRunning = false;}} break;case WM_CLOSE: { // 窗口关闭时的消息// TODO: 像用户发送消息进行处理GloblaRunning = false;} break;case WM_ACTIVATEAPP: { // 应用程序激活或失去焦点时的消息OutputDebugStringA("WM_ACTIVATEAPP\n"); // 输出调试信息,表示应用程序激活或失去焦点} break;case WM_PAINT: { // 处理 WM_PAINT 消息,通常在窗口需要重新绘制时触发PAINTSTRUCT Paint; // 定义一个 PAINTSTRUCT 结构体,保存绘制的信息// 调用 BeginPaint 开始绘制,并获取设备上下文 (HDC),同时填充 Paint 结构体HDC DeviceContext = BeginPaint(hwnd, &Paint);// 获取当前绘制区域的左上角坐标int X = Paint.rcPaint.left;int Y = Paint.rcPaint.top;// 计算绘制区域的宽度和高度int Height = Paint.rcPaint.bottom - Paint.rcPaint.top;int Width = Paint.rcPaint.right - Paint.rcPaint.left;win32_window_dimension Dimension = Win32GetWindowDimension(hwnd);Win32DisplayBufferInWindow(DeviceContext, Dimension.Width, Dimension.Height,GlobalBackbuffer, X, Y, Width, Height);// 调用 EndPaint 结束绘制,并释放设备上下文EndPaint(hwnd, &Paint);} break;default: { // 对于不处理的消息,调用默认的窗口过程Result = DefWindowProc(hwnd, Message, wParam, LParam);// 调用默认窗口过程处理消息} break;}return Result; // 返回处理结果
}internal void Win32FillSoundBuffer(win32_sound_output *SoundOutput,DWORD ByteToLock, DWORD BytesToWrite) {VOID *Region1; // 第一段区域指针,用于存放锁定后的首部分缓冲区地址DWORD Region1Size; // 第一段区域的大小(字节数)VOID *Region2; // 第二段区域指针,用于存放锁定后的剩余部分缓冲区地址DWORD Region2Size; // 第二段区域的大小(字节数)if (SUCCEEDED(GlobalSecondaryBuffer->Lock(ByteToLock, // 缓冲区偏移量,指定开始锁定的字节位置BytesToWrite, // 锁定的字节数,指定要锁定的区域大小&Region1, // 输出,返回锁定区域的内存指针(第一个区域)&Region1Size, // 输出,返回第一个锁定区域的实际字节数&Region2, // 输出,返回第二个锁定区域的内存指针(可选,双缓冲或环形缓冲时使用)&Region2Size, // 输出,返回第二个锁定区域的实际字节数0 // 标志,控制锁定行为(如从光标位置锁定等)))) {// int16 int16 int16// 左 右 左 右 左 右 左 右 左 右DWORD Region1SampleCount =Region1Size / SoundOutput->BytesPerSample; // 计算第一段区域中的样本数量int16 *SampleOut = (int16 *)Region1; // 将第一段区域指针转换为 16// 位整型指针,准备写入样本数据// 循环写入样本到第一段区域for (DWORD SampleIndex = 0; SampleIndex < Region1SampleCount;++SampleIndex) {real32 SineValue1 = sinf(SoundOutput->tSine);int16 SampleValue = (int16)(SineValue1 * SoundOutput->ToneVolume);*SampleOut++ = SampleValue; // 左声道*SampleOut++ = SampleValue; // 右声道SoundOutput->tSine +=2.0f * (real32)Pi32 * 1.0f / (real32)SoundOutput->WavePeriod;SoundOutput->RunningSampleIndex++;}DWORD Region2SampleCount =Region2Size / SoundOutput->BytesPerSample; // 计算第二段区域中的样本数量SampleOut = (int16 *)Region2; // 将第二段区域指针转换为 16// 位整型指针,准备写入样本数据// 循环写入样本到第二段区域for (DWORD SampleIndex = 0; SampleIndex < Region2SampleCount;++SampleIndex) {// 使用相同逻辑生成方波样本数据real32 SineValue = sinf(SoundOutput->tSine);int16 SampleValue = (int16)(SineValue * SoundOutput->ToneVolume);*SampleOut++ = SampleValue; // 左声道*SampleOut++ = SampleValue; // 右声道SoundOutput->tSine +=2.0f * (real32)Pi32 * 1.0f / (real32)SoundOutput->WavePeriod;SoundOutput->RunningSampleIndex++;}// 解锁音频缓冲区,将数据提交给音频设备GlobalSecondaryBuffer->Unlock(Region1, Region1Size, Region2, Region2Size);}
}int CALLBACK WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, //PSTR cmdline, int cmdshow) {LARGE_INTEGER PerfCountFrequencyResult;QueryPerformanceFrequency(&PerfCountFrequencyResult);int64 PerfCountFrequency = PerfCountFrequencyResult.QuadPart;Win32LoadXInput(); // 加载 XInput 库,用于处理 Xbox 控制器输入WNDCLASS WindowClass = {}; // 初始化窗口类结构,默认值为零// 使用大括号初始化,所有成员都被初始化为零(0)或 nullptrWin32ResizeDIBSection(&GlobalBackbuffer, 1280,720); // 调整 DIB(设备独立位图)大小// WindowClass.style:表示窗口类的样式。通常设置为一些 Windows// 窗口样式标志(例如 CS_HREDRAW, CS_VREDRAW)。WindowClass.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;// CS_HREDRAW 当窗口的宽度发生变化时,窗口会被重绘。// CS_VREDRAW 当窗口的高度发生变化时,窗口会被重绘//  WindowClass.lpfnWndProc:指向窗口过程函数的指针,窗口过程用于处理与窗口相关的消息。WindowClass.lpfnWndProc = Win32MainWindowCallback;// WindowClass.hInstance:指定当前应用程序的实例句柄,Windows// 应用程序必须有一个实例句柄。WindowClass.hInstance = hInst;// WindowClass.lpszClassName:指定窗口类的名称,通常用于创建窗口时注册该类。WindowClass.lpszClassName = "gameWindowClass"; // 类名if (RegisterClass(&WindowClass)) {             // 如果窗口类注册成功HWND Window = CreateWindowEx(0,                         // 创建窗口,使用扩展窗口风格WindowClass.lpszClassName, // 窗口类的名称,指向已注册的窗口类"game",                    // 窗口标题(窗口的名称)WS_OVERLAPPEDWINDOW |WS_VISIBLE, // 窗口样式:重叠窗口(带有菜单、边框等)并且可见CW_USEDEFAULT, // 窗口的初始位置:使用默认位置(X坐标)CW_USEDEFAULT, // 窗口的初始位置:使用默认位置(Y坐标)CW_USEDEFAULT, // 窗口的初始宽度:使用默认宽度CW_USEDEFAULT, // 窗口的初始高度:使用默认高度0,             // 父窗口句柄(此处无父窗口,传0)0,             // 菜单句柄(此处没有菜单,传0)hInst,         // 当前应用程序的实例句柄0 // 额外的创建参数(此处没有传递额外参数));// 如果窗口创建成功,Window 将保存窗口的句柄if (Window) { // 检查窗口句柄是否有效,若有效则进入消息循环// 图像测试int xOffset = 0;int yOffset = 0;win32_sound_output SoundOutput = {}; // 初始化声音输出结构体// 音频测试SoundOutput.RunningSampleIndex = 0;   // 样本索引SoundOutput.ToneVolume = 3000;        // 音量SoundOutput.SamplesPerSecond = 48000; // 采样率:每秒采样48000次SoundOutput.ToneHz = 256;             // 波频率:256 HzSoundOutput.WavePeriod =SoundOutput.SamplesPerSecond / SoundOutput.ToneHz; // 波周期(样本数)SoundOutput.HalfWavePeriod =SoundOutput.WavePeriod / 2;                 // 波半周期(样本数)SoundOutput.BytesPerSample = sizeof(int16) * 2; // 一个样本的大小SoundOutput.SecondaryBufferSize =SoundOutput.SamplesPerSecond *SoundOutput.BytesPerSample; // 缓冲区大小SoundOutput.LatencySampleCount = SoundOutput.SamplesPerSecond / 15;Win32InitDSound(Window, SoundOutput.SamplesPerSecond,SoundOutput.SecondaryBufferSize); // 初始化 DirectSoundbool32 SoundIsPlaying = false;GloblaRunning = true;LARGE_INTEGER LastCounter; // 保留上次计数器的值QueryPerformanceCounter(&LastCounter);int64 LastCycleCount = __rdtsc();while (GloblaRunning) { // 启动一个无限循环,等待和处理消息MSG Message;          // 声明一个 MSG 结构体,用于接收消息while (PeekMessage(&Message,// 指向一个 `MSG` 结构的指针。`PeekMessage`// 将在 `lpMsg` 中填入符合条件的消息内容。0,// `hWnd` 为`NULL`,则检查当前线程中所有窗口的消息;// 如果设置为特定的窗口句柄,则只检查该窗口的消息。0, //0, // 用于设定消息类型的范围PM_REMOVE // 将消息从消息队列中移除,类似于 `GetMessage` 的行为。)) {if (Message.message == WM_QUIT) {GloblaRunning = false;}TranslateMessage(&Message); // 翻译消息,如果是键盘消息需要翻译DispatchMessage(&Message); // 分派消息,调用窗口过程处理消息}// TODO: 我们应该频繁的轮询吗for (DWORD ControllerIndex = 0; ControllerIndex < XUSER_INDEX_ANY;ControllerIndex++) {// 定义一个 XINPUT_STATE 结构体,用来存储控制器的状态XINPUT_STATE ControllerState;// 调用 XInputGetState 获取控制器的状态if (XInputGetState(ControllerIndex, &ControllerState) ==ERROR_SUCCESS) {// 如果获取控制器状态成功,提取 Gamepad 的数据// NOTE:// 获取方向键的按键状态XINPUT_GAMEPAD *Pad = &ControllerState.Gamepad;bool Up = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_UP);bool Down = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_DOWN);bool Left = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_LEFT);bool Right = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_RIGHT);// 获取肩部按钮的按键状态bool LeftShoulder = (Pad->wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER);bool RightShoulder =(Pad->wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER);// 获取功能按钮的按键状态bool Start = (Pad->wButtons & XINPUT_GAMEPAD_START);bool Back = (Pad->wButtons & XINPUT_GAMEPAD_BACK);bool AButton = (Pad->wButtons & XINPUT_GAMEPAD_A);bool BButton = (Pad->wButtons & XINPUT_GAMEPAD_B);bool XButton = (Pad->wButtons & XINPUT_GAMEPAD_X);bool YButton = (Pad->wButtons & XINPUT_GAMEPAD_Y);// 获取摇杆的 X 和 Y 坐标值(-32768 到 32767)int16 StickX = Pad->sThumbLX;int16 StickY = Pad->sThumbLY;// 根据摇杆的 Y 坐标值调整音调和声音xOffset += StickX >> 12;yOffset += StickY >> 12;// 更新音调频率 (ToneHz),通过摇杆的 Y 值来调节// 这里是将 StickY 映射到频率范围内,使得频率与摇杆的上下运动相关。// 512 是基准频率,StickY 值影响音频频率的变化范围。SoundOutput.ToneHz =512 + (int)(256.0f * ((real32)StickY / 30000.0f));// 计算波周期,基于频率,决定波形的周期SoundOutput.WavePeriod =SoundOutput.SamplesPerSecond / SoundOutput.ToneHz;}}DWORD PlayCursor = 0;  // 播放游标,指示当前播放位置DWORD WriteCursor = 0; // 写入游标,指示当前写入位置// 获取音频缓冲区的当前播放和写入位置if (SUCCEEDED(GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, &WriteCursor))) {// 计算需要锁定的字节位置,基于当前样本索引和每样本字节数DWORD ByteToLock =((SoundOutput.RunningSampleIndex * SoundOutput.BytesPerSample) %SoundOutput.SecondaryBufferSize);DWORD TargetCursor = (PlayCursor + (SoundOutput.LatencySampleCount *SoundOutput.BytesPerSample)) %SoundOutput.SecondaryBufferSize;DWORD BytesToWrite = 0; // 需要写入的字节数// 判断 ByteToLock 与 TargetCursor 的位置关系以确定写入量if (ByteToLock == TargetCursor) {// 如果锁定位置正好等于播放位置,写入整个缓冲区BytesToWrite = 0;} else if (ByteToLock > TargetCursor) {// 如果锁定位置在播放位置之后,写入从锁定位置到缓冲区末尾,再加上开头到播放位置的字节数BytesToWrite =(SoundOutput.SecondaryBufferSize - ByteToLock) + TargetCursor;} else {// 如果锁定位置在播放位置之前,写入从锁定位置到播放位置之间的字节数BytesToWrite = TargetCursor - ByteToLock;}Win32FillSoundBuffer(&SoundOutput, ByteToLock, BytesToWrite);}if (!SoundIsPlaying) {GlobalSecondaryBuffer->Play(0, 0, DSBPLAY_LOOPING);SoundIsPlaying = true;}RenderWeirdGradient(GlobalBackbuffer, xOffset, yOffset);// 这个地方需要渲染一下不然是黑屏a{HDC DeviceContext = GetDC(Window);win32_window_dimension Dimension = Win32GetWindowDimension(Window);RECT WindowRect;GetClientRect(Window, &WindowRect);int WindowWidth = WindowRect.right - WindowRect.left;int WindowHeigh = WindowRect.bottom - WindowRect.top;Win32DisplayBufferInWindow(DeviceContext, Dimension.Width,Dimension.Height, GlobalBackbuffer, 0, 0,WindowWidth, WindowHeigh);ReleaseDC(Window, DeviceContext);}int64 EndCycleCount = __rdtsc();LARGE_INTEGER EndCounter;QueryPerformanceCounter(&EndCounter);// TODO: 显示结果int64 CyclesElapsed = EndCycleCount - LastCycleCount;int64 CounterElapsed = EndCounter.QuadPart - LastCounter.QuadPart;real32 MillisecondPerFrame =(real32)((1000.f * (real32)CounterElapsed) /(real32)PerfCountFrequency);real32 FPS = (real32)PerfCountFrequency / (real32)CounterElapsed;real32 MCPF = (real32)CyclesElapsed / (1000.0f * 1000.0f);char Buffer[256];sprintf_s(Buffer, "%fms/f, %ff/s, %fmc/f\n", MillisecondPerFrame, FPS,MCPF);OutputDebugString(Buffer);LastCounter = EndCounter;LastCycleCount = EndCycleCount;}} else { // 如果窗口创建失败// 这里可以处理窗口创建失败的逻辑// 比如输出错误信息,或退出程序等// TODO:}} else { // 如果窗口类注册失败// 这里可以处理注册失败的逻辑// 比如输出错误信息,或退出程序等// TODO:}return 0;
}

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

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

相关文章

计算机网络(11)和流量控制补充

这一篇对数据链路层中的和流量控制进行详细学习 流量控制&#xff08;Flow Control&#xff09;是计算机网络中确保数据流平稳传输的技术&#xff0c;旨在防止数据发送方发送过多数据&#xff0c;导致接收方的缓冲区溢出&#xff0c;进而造成数据丢失或传输失败。流量控制通常…

PVE纵览-安装系统卡“Loading Driver”的快速解决方案

PVE纵览-安装系统卡“Loading Driver”的快速解决方案 文章目录 PVE纵览-安装系统卡“Loading Driver”的快速解决方案摘要通过引导参数解决PVE安装卡在“Loading Driver”问题官方解决方法 关键字&#xff1a; PVE、 显卡、 Loading、 Driver、 nomodeset 摘要 在虚拟机…

Docker在CentOS上的安装与配置

前言 随着云计算和微服务架构的兴起&#xff0c;Docker作为一种轻量级的容器技术&#xff0c;已经成为现代软件开发和运维中的重要工具。本文旨在为初学者提供一份详尽的指南&#xff0c;帮助他们在CentOS系统上安装和配置Docker及相关组件&#xff0c;如Docker Compose和私有…

在 Oracle Linux 8.9 上安装Oracle Database 23ai 23.5

在 Oracle Linux 8.9 上安装Oracle Database 23ai 23.5 1. 安装 Oracle Database 23ai2. 连接 Oracle Database 23c3. 重启启动后&#xff0c;手动启动数据库4. 重启启动后&#xff0c;手动启动 Listener5. 手动启动 Pluggable Database6. 自动启动 Pluggable Database7. 设置开…

实验6记录网络与故障排除

实验6记录网络与故障排除 实验目的及要求&#xff1a; 通过实验&#xff0c;掌握如何利用文档记录网络设备相关信息并完成网络拓扑结构的绘制。能够使用各种技术和工具来找出连通性问题&#xff0c;使用文档来指导故障排除工作&#xff0c;确定具体的网络问题&#xff0c;实施…

Go开发指南- Goroutine

目录&#xff1a; (1)Go开发指南-Hello World (2)Go开发指南-Gin与Web开发 (3)Go开发指南-Goroutine Goroutine 在java中我们要实现并发编程的时候&#xff0c;通常要自己维护一个线程池&#xff0c;并且需要去包装任务、调度任务和维护上下文切换。这个过程需要消耗大量的精…

解决failed to execute PosixPath(‘dot‘) 或者GraphViz‘s executables not found

在网上找了很多方法都没解决&#xff0c;所以写一篇文章帮助和我遇到同样问题的人 解决方法&#xff1a; 因为python解释器会解释转移字符&#xff0c;因此在环境变量中把\bin换成\\bin即可 解决过程&#xff1a; 系统&#xff1a;win10 已安装pip install graphviz&#xff0…

力扣 LeetCode 541. 反转字符串II(Day4:字符串)

解题思路&#xff1a; i可以成段成段的跳&#xff0c;而不是简单的i class Solution {public String reverseStr(String s, int k) {char[] ch s.toCharArray();// 1. 每隔 2k 个字符的前 k 个字符进行反转for (int i 0; i < ch.length; i 2 * k) {// 2. 剩余字符小于 …

官方压测工具memtier-benchmark压测redis

1 概述 memtier_benchmark是一种高吞吐量的性能基准测试工具&#xff0c;主要用于Redis和Memcached。它是 Redis官方开发团队开发的&#xff0c;旨在生成各种流量模式&#xff0c;以便测试和优化以上两个数据库的性能。 memtier_benchmark的一些关键特点如下&#xff1a; 多…

用 Python 从零开始创建神经网络(三):添加层级(Adding Layers)

添加层级&#xff08;Adding Layers&#xff09; 引言1. Training Data2. Dense Layer Class 引言 我们构建的神经网络变得越来越受人尊敬&#xff0c;但目前我们只有一层。当神经网络具有两层或更多隐藏层时&#xff0c;它们变成了“深度”网络。目前我们只有一层&#xff0c…

MFC工控项目实例三十实现一个简单的流程

启动按钮夹紧 密闭&#xff0c;时间0到平衡 进气&#xff0c;时间1到进气关&#xff0c;时间2到平衡关 检测&#xff0c;时间3到平衡 排气&#xff0c;时间4到夹紧开、密闭开、排气关。 相关代码 void CSEAL_PRESSUREDlg::OnTimer_2(UINT nIDEvent_2) {// if (nIDEvent_21 &am…

Java I/O(输入/输出)——针对实习面试

目录 Java I/O&#xff08;输入/输出&#xff09;什么是Java I/O流&#xff1f;字节流和字符流有什么区别&#xff1f;什么是缓冲流&#xff1f;为什么要使用缓冲流&#xff1f;Java I/O中的设计模式有哪些&#xff1f;什么是BIO&#xff1f;什么是NIO&#xff1f;什么是AIO&am…

Exploring Defeasible Reasoning in Large Language Models: A Chain-of-Thought A

文章目录 题目摘要简介准备工作数据集生成方法实验结论 题目 探索大型语言模型中的可废止推理&#xff1a;思路链 论文地址&#xff1a;http://collegepublications.co.uk/downloads/LNGAI00004.pdf#page136 摘要 许多大型语言模型 (LLM) 经过大量高质量数据语料库的训练&…

数据结构--数组

一.线性和非线性 线性&#xff1a;除首尾外只有一个唯一的前驱和后继。eg&#xff1a;数组&#xff0c;链表等。 非线性&#xff1a;不是线性的就是非线性。 二.数组是什么&#xff1f; 数组是一个固定长度的存储相同数据类型的数据结构&#xff0c;数组中的元素被存储在一…

MySQL技巧之跨服务器数据查询:基础篇-更新语句如何写

MySQL技巧之跨服务器数据查询&#xff1a;基础篇-更新语句如何写 上一篇已经描述&#xff1a;借用微软的SQL Server ODBC 即可实现MySQL跨服务器间的数据查询。 而且还介绍了如何获得一个在MS SQL Server 可以连接指定实例的MySQL数据库的连接名: MY_ODBC_MYSQL 以及用同样的…

Unity教程(十八)战斗系统 攻击逻辑

Unity开发2D类银河恶魔城游戏学习笔记 Unity教程&#xff08;零&#xff09;Unity和VS的使用相关内容 Unity教程&#xff08;一&#xff09;开始学习状态机 Unity教程&#xff08;二&#xff09;角色移动的实现 Unity教程&#xff08;三&#xff09;角色跳跃的实现 Unity教程&…

前端学习八股资料CSS(二)

更多详情&#xff1a;爱米的前端小笔记&#xff0c;更多前端内容&#xff0c;等你来看&#xff01;这些都是利用下班时间整理的&#xff0c;整理不易&#xff0c;大家多多&#x1f44d;&#x1f49b;➕&#x1f914;哦&#xff01;你们的支持才是我不断更新的动力&#xff01;找…

云计算研究实训室建设方案

一、引言 随着云计算技术的迅速发展和广泛应用&#xff0c;职业院校面临着培养云计算领域专业人才的迫切需求。本方案旨在构建一个先进的云计算研究实训室&#xff0c;为学生提供一个集理论学习、实践操作、技术研发与创新于一体的综合性学习平台&#xff0c;以促进云计算技术…

通过Python 调整Excel行高、列宽

在Excel中&#xff0c;默认的行高和列宽可能不足以完全显示某些单元格中的内容&#xff0c;特别是当内容较长时。通过调整行高和列宽&#xff0c;可以确保所有数据都能完整显示&#xff0c;避免内容被截断。合理的行高和列宽可以使表格看起来更加整洁和专业&#xff0c;尤其是在…

【代码审计】常见漏洞专项审计-业务逻辑漏洞审计

❤️博客主页&#xff1a; iknow181 &#x1f525;系列专栏&#xff1a; 网络安全、 Python、JavaSE、JavaWeb、CCNP &#x1f389;欢迎大家点赞&#x1f44d;收藏⭐评论✍ 0x01 漏洞介绍 1、 原理 业务逻辑漏洞是一类特殊的安全漏洞&#xff0c;业务逻辑漏洞属于设计漏洞而非实…