游戏引擎学习第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.mzph.cn/web/59605.shtml

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

相关文章

正则表达式(补充)

定义一个正则表达式 const 变量名 /表达式/ const reg /前端/ 匹配看字符串中有无前端俩字 正则对象上的一些方法 test() 用于查看正则表达式与指定的字符串是否匹配 const reg /前端/ const res reg.test(学前端&#xff0c;找黑马) //匹配到返回true,匹配不到返回fa…

CSS 语法规范

基本语法结构 CSS 的基本语法结构包含 选择器 和 声明块,两者共同组成 规则集。规则集可以为 HTML 元素设置样式,使页面结构和样式实现分离,便于网页的美化和布局调整。 CSS 规则集的结构如下: selector {property: value; }选择器(Selector) 选择器用于指定需要应用…

【在Linux世界中追寻伟大的One Piece】多路转接epoll(续)

目录 1 -> epoll的工作方式 1.1 -> 水平触发(Level Triggered)工作模式 1.2 -> 边缘触发(Edge Triggered)工作模式 2 -> 对比LT与ET 3 -> 理解ET模式和非阻塞文件描述符 4 -> epoll的使用场景 5 -> epoll示例 5.1 -> epoll服务器(LT模式) 5.2…

响应“一机两用”政策 落实政务外网安全

在数字化时代&#xff0c;政务办公外网安全的重要性日益凸显&#xff0c;特别是在“一机两用”的背景下&#xff0c;即同一台终端既要处理政务内网的数据&#xff0c;又要访问互联网&#xff0c;这对网络安全提出了更高的要求。深信达SPN安全上网方案&#xff0c;即反向沙箱技术…

计算机网络基础——针对实习面试

目录 计算机网络基础OSI七层模型TCP/IP四层模型为什么网络要分层&#xff1f;常见网络协议 计算机网络基础 OSI七层模型 开放系统互连参考模型&#xff08;Open Systems Interconnection Reference Model&#xff0c;简称OSI模型&#xff09;是一个概念性模型&#xff0c;用于…

leetcode100:相同的树

给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 示例 1&#xff1a; 输入&#xff1a;p [1,2,3], q [1,2,3] 输出&#xff1a;true示例 2&…

将已有的MySQL8.0单机架构变成主从复制架构

过程: 把数据库做一个完全备份, 恢复到从节点上, 恢复后从备份的那个点开始往后复制,从而保证后续数据的一致性。 步骤: 修改 master 主节点 的配置&#xff08; server-id log-bin &#xff09;master 主节点 完全备份&#xff08; mysqldump &#xff09;master 主节点 创建…

如何在jupyter notebook切换python环境

目录 参考链接 首先确保conda已经正常安装 conda --version 或者conda -V 以下请将“myenv”替换成自己的命名&#xff01;&#xff01;&#xff01; 1-查看虚拟环境目录 conda env list 2-创建虚拟环境命令 conda create -n myenv 或者 conda create --name myenv 3-激活虚拟环…

【嵌入式软件-STM32】OLED显示屏+调试方法

目录 一、调试方式 1&#xff09;串口调试 优势 弊端 2&#xff09;显示屏调试 优势 弊端 3&#xff09;Keil调试模式 4&#xff09;点灯调试法 5&#xff09;注释调试法 6&#xff09;对照法 二、OLED简介 OLED组件 OLED显示屏 0.96寸OLED模块 OLED外观和种类…

求字符 ‘a‘ 和 ‘b‘ 组成的,最大长度为n的字符串中字典序第 k 个字符串

求字符 ‘a’ 和 ‘b’ 组成的&#xff0c;最大长度为n的字符串中字典序第 k 个字符串 先来解释一下这个题目&#xff0c;假设最大长度为3&#xff0c;那么由字符a和b组成的字符串有&#xff1a; a, b, ab, aaa, aba...把这些字符串按照字典序排序: aaaaaaaabababaabbbbabaab…

再见 阿里巴巴EasyExcel替代品EasyExcel-Plus即将诞生

最近阿里发布公告通知&#xff0c;停止对EasyExcel 更新和维护&#xff0c;EasyExcel 是一款知名的 Java Excel 工具库&#xff0c;由阿里巴巴开源&#xff0c;作者是玉霄&#xff0c;在 GitHub 上有 30k stars、7.5k forks。 据了解&#xff0c;EasyExcel作者玉霄)去年已经从…

如何保证MySQL与Redis缓存的数据一致性?

文章目录 一、引言二、场景来源三、高并发解决方案1. 先更新缓存&#xff0c;再更新数据库2. 先更新数据库&#xff0c;再更新缓存3. 先删除缓存&#xff0c;再更新数据库4. 先更新数据库&#xff0c;再删除缓存小结 四、拓展方案1. 分布式锁与分布式事务2. 消息队列3. 监听bin…

暴露IP地址会影响网络隐私安全吗?

​我的IP地址暴露后会影响隐私安全吗&#xff1f; 互联网飞速发展以来&#xff0c;短短数十年&#xff0c;我们的工作生活就不能够离开互联网。那么作为网络连接传递数据的门户——IP地址&#xff0c;大家都有一定的疑惑和好奇。其中关于自身安全的尤为重要&#xff0c;所以IP…

通过 SSH 隧道将本地端口转发到远程主机

由于服务器防火墙,只开放了22端口,想要通过5901访问服务器上的远程桌面,可以通过下面的方式进行隧道转发。 一、示例命令 这条代码的作用是通过 SSH 创建一个 本地端口转发,将你本地的端口(5901)通过加密的 SSH 隧道连接到远程服务器上的端口(5901)。这种方式通常用于在…

CTF攻防世界小白刷题自学笔记14

fileclude&#xff0c;难度&#xff1a;1&#xff0c;方向&#xff1a;Web 题目来源:CTF 题目描述:好多file呀&#xff01; 给一下题目链接&#xff1a;攻防世界Web方向新手模式第17题。 打开一看&#xff0c;这熟悉的味道&#xff0c;跟上一篇文章基本一摸一样的&#xff…

微信小程序开发,仿小红书瀑布流实现

文章目录 1. 涉及到的知识点2. 功能描述3. 通用属性3. 代码实现过程4. 报错问题&#xff0c;解决方法5. 运行效果图 1. 涉及到的知识点 grid-view的使用官方文档指南&#xff1a;https://developers.weixin.qq.com/miniprogram/dev/component/grid-view.html 2. 功能描述 Sk…

ssm102“魅力”繁峙宣传网站的设计与实现+vue(论文+源码)_kaic

摘 要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;“魅力”繁峙宣传网站系统当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xff0c;带动了“魅力”繁峙宣传网站系统的发展&#x…

基于Matlab的碎纸片的自动拼接复原技术

碎纸片的自动拼接复原技术 摘要&#xff1a;破碎文件的拼接在司法物证复原、历史文献修复以及军事情报获取等领域都有着重要的应用。目前发现对碎纸片的拼接大部分由人工完成&#xff0c;准确率较高&#xff0c;但耗费大量人力财力及时间&#xff0c;效率很低。随着计算机技术的…

Kafka-Eagle的配置——kafka可视化界面

通过百度网盘分享的文件&#xff1a;kafka-eagle-bin-2.0.8.tar.gz 链接&#xff1a;https://pan.baidu.com/s/1H3YONkL97uXbLTPMZHrfdg?pwdsltu 提取码&#xff1a;sltu 一、界面展示 二、软件配置 1、关闭kafka集群 kf.sh stop 2、将该软件上传到/opt/modules下 cd /opt…

【C#】CS0246: 未能找到类型或命名空间名“MySql”

前言 在学习C#,一定要学会了使用NuGet,以后包问题都可以通过此方法解决。望大家不加班~ 问题描述 项目 visual studio 2022 .NETFramework,Version=v4.8错误 CS0246: 未能找到类型或命名空间名“MySql”问题 CS0246 错误表示编译器无法在当前项目中找到名为“MySql”的…