视频参考: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 时钟周期中递增。该计数器通常用于性能测量、时间间隔计算或基准测试等场景。
RDTS
和 RDTSCP
说明:
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, rdx
和mov rbx, rax
是将读取到的 TSC 值存储到rax
和rbx
寄存器,以便后续使用。
使用场景:
-
性能测量:
你可以使用 TSC 来测量时间间隔,精度非常高。通过在两个时间点读取 TSC 值,然后计算它们的差值,可以得到操作的耗时(单位是 CPU 时钟周期)。uint64_t start, end; start = __rdtsc(); // 获取开始时的 TSC 值 // 执行要测量的操作 end = __rdtsc(); // 获取结束时的 TSC 值 uint64_t elapsed = end - start; // 计算两次 TSC 读取的差值,得到执行时间(单位:CPU 时钟周期)
__rdtsc()
是许多编译器提供的内置函数(如 GCC 和 MSVC),用于访问 TSC。
-
代码执行性能分析:
在高性能应用程序中,RDTS
被广泛应用于低级性能分析,来测量某段代码的执行时间。 -
高精度计时器:
当你需要高精度计时时,RDTS
可以用来测量非常小的时间间隔,因为它可以在每个 CPU 时钟周期内递增。
需要注意的事项:
-
非统一性:不同核心上的 TSC 值可能不同,尤其是在旧款处理器上,TSC 并不是完全一致的。现代处理器通常会有一个不随 CPU 频率变化的恒定 TSC,但这并非总是保证的。
-
频率变化:如果 CPU 的频率发生变化(例如,由于节能功能),TSC 值可能不会均匀递增,除非使用具有不变 TSC 的处理器。
-
核心间同步:由于每个核心的 TSC 独立递增,访问不同核心的 TSC 值时,可能会得到不同的结果。为了解决这个问题,通常建议使用
RDTSCP
,因为它会确保读取到的 TSC 值与指令的执行时刻完全同步。
RDTSCP
示例:
RDTSCP
是 RDTS
的改进版本,它确保读取的 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;
解析:
-
联合体 (
union
):LARGE_INTEGER
是一个 联合体类型(union
),意味着它的不同成员共享同一内存空间。每个成员的起始地址相同,所以它们都指向相同的内存位置,但每次只能使用其中一个成员。- 由于
union
成员共享同一块内存,因此它使得不同的数据视图(如按低高 32 位分解,或作为一个完整的 64 位整数)能够方便地进行访问。
-
成员:
-
DUMMYSTRUCTNAME
和u
:这两个结构体的定义完全相同,都包含LowPart
和HighPart
两个字段。LowPart
是一个 32 位的DWORD
(无符号 32 位整数),存储低 32 位数据。HighPart
是一个 32 位的LONG
(带符号 32 位整数),存储高 32 位数据。
-
你可以通过
DUMMYSTRUCTNAME
或u
来访问LowPart
和HighPart
,这两个结构体只是为了方便访问 64 位整数的低 32 位和高 32 位部分。 -
QuadPart
:这是LARGE_INTEGER
的核心字段,表示一个完整的 64 位整数,类型为LONGLONG
。LONGLONG
是一个 64 位的带符号整数类型,用于存储整个 64 位的值。通过QuadPart
,你可以一次性访问和操作这个完整的 64 位整数值。
-
2. 使用 LowPart
和 HighPart
来访问低 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 位部分。
常见时间单位及换算关系:
- 秒 (Second, s)
- 毫秒 (Millisecond, ms)
- 微秒 (Microsecond, µs)
- 纳秒 (Nanosecond, ns)
- 皮秒 (Picosecond, ps)
- 秒 (s) 是基本单位。
- 毫秒 (ms) = 10⁻³ s。
- 微秒 (µs) = 10⁻⁶ s。
- 纳秒 (ns) = 10⁻⁹ s。
- 皮秒 (ps) = 10⁻¹² s。
计算FPS
要理解为什么 int32 FPS = PerfCountFrequency / CounterElapsed;
这样计算 FPS(帧率),我们需要结合 PerfCountFrequency
和 CounterElapsed
的含义来推导。
背景解释
-
PerfCountFrequency
:这是计时器的频率,表示每秒钟计时器的更新次数。它通常由QueryPerformanceFrequency
函数返回。- 举例:如果
PerfCountFrequency
是 1,000,000,那么每秒钟计时器会增加 1,000,000 次(即 1 MHz)。
- 举例:如果
-
CounterElapsed
:这是两个QueryPerformanceCounter
调用之间的增量,表示自上次查询以来计时器更新的次数。- 举例:假设我们经过了 10,000 个计时器周期,那么
CounterElapsed
就是 10,000。
- 举例:假设我们经过了 10,000 个计时器周期,那么
目标:计算每秒的帧数(FPS)
我们想计算每秒钟的帧数,即每秒钟能够渲染多少帧。
推导过程
-
每帧的时间(秒):
假设每次渲染一帧时,从上一次渲染到这一次渲染之间经过了
CounterElapsed
个计时器周期。那么,经过这些周期的时间长度(单位:秒)可以通过以下公式计算:
Frame Time (秒) = CounterElapsed PerfCountFrequency \text{Frame Time (秒)} = \frac{\text{CounterElapsed}}{\text{PerfCountFrequency}} Frame Time (秒)=PerfCountFrequencyCounterElapsed
其中:CounterElapsed
是当前帧经过的计时器周期数。PerfCountFrequency
是计时器的频率,即每秒钟计时器的更新次数。
这就是每帧所需的时间(单位:秒)。
-
计算每秒钟的帧数(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);
假设 MillisecondPerFrame
和 FPS
的值较大,格式化字符串 "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::wstring
和 std::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 的时钟频率以及多核处理器的同步问题。
MULPD
和 MULPS
是 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 指令集 实现的,这些指令集有许多不同的实现,最常见的有:
- MMX (MultiMedia Extensions):最早的 SIMD 扩展,主要用于处理多媒体数据,如视频和音频。
- SSE (Streaming SIMD Extensions):Intel 的 SIMD 扩展,支持更高效的浮点数计算。
- AVX (Advanced Vector Extensions):AVX 是 SSE 的扩展,支持更宽的向量处理,增强了浮点和整数计算的性能。
- 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 的优势
-
提高性能:
SIMD 可以显著提高多媒体、图像处理、科学计算等领域的性能。比如在图像处理时,多个像素的颜色值可以同时处理,而不必一帧一帧地计算。 -
降低指令数:
通过一次性对多个数据元素执行操作,SIMD 可以减少程序中需要执行的指令数,从而降低执行时间。 -
并行计算:
SIMD 是一种“数据并行”模型,即同一操作被同时应用于多个数据项,适合处理大规模的数据集(如大矩阵运算、向量运算等)。
SIMD 应用场景
-
图像处理:
SIMD 可以大大提高图像和视频处理中的效率,特别是在进行色彩转换、滤波、旋转等常见操作时。 -
音视频编解码:
音视频编码和解码通常需要对大量的数据进行相同的计算,SIMD 在此类操作中可以显著加速。 -
科学计算:
在科学计算中,SIMD 可以加速大规模矩阵运算、向量处理和矩阵乘法等。 -
机器学习:
许多机器学习任务(如神经网络的前向传播)需要大量的并行数据处理,SIMD 能加速这些计算过程。
SIMD 的限制
尽管 SIMD 提供了显著的性能提升,但它也有一些局限性:
-
数据依赖:
如果操作的数据存在相互依赖(如循环中一个数据的计算依赖于上一个数据的结果),SIMD 就不能有效地并行化这些计算。 -
编程复杂度:
编写 SIMD 优化代码通常较为复杂,特别是涉及到跨平台和不同硬件架构时。开发者需要特别注意数据对齐和指令选择等细节。 -
硬件支持:
并非所有的硬件都支持高级的 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.14159265359
typedef 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 Hz
int 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.dll
XInputLibrary = 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 接口的指针,并初始化为 NULL
IDirectSound *DirectSound = NULL;
if (DirectSoundCreate && SUCCEEDED(DirectSoundCreate(
0,
// 传入 0 作为设备 GUID,表示使用默认音频设备
&DirectSound,
// 将创建的 DirectSound 对象的指针存储到
// DirectSound 变量中
0
// 传入 0 作为外部未知接口指针,通常为 NULL
))) //
{
// clang-format off
WAVEFORMATEX 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 format
OutputDebugString("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, // 要释放的大小(字节),对部分释放有效,整体释放则设为 0
MEM_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; // 设置颜色平面数,通常为 1
Buffer->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)或 nullptr
Win32ResizeDIBSection(&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 Hz
SoundOutput.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); // 初始化 DirectSound
bool32 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;
}