游戏引擎学习第四天
视频参考:https://www.bilibili.com/video/BV1aDmqYnEnc/
BitBlt 是 Windows GDI(图形设备接口)中的一个函数,用于在设备上下文(device context, DC)之间复制位图数据。BitBlt 的主要用途是将一个图像区域从一个地方复制到另一个地方,常用于图像绘制、屏幕刷新、动画等。
BitBlt 函数的基本签名如下:
BOOL BitBlt(
HDC hdcDest, // 目标设备上下文句柄
int nXDest, // 目标区域左上角的X坐标
int nYDest, // 目标区域左上角的Y坐标
int nWidth, // 复制区域的宽度
int nHeight, // 复制区域的高度
HDC hdcSrc, // 源设备上下文句柄
int nXSrc, // 源区域左上角的X坐标
int nYSrc, // 源区域左上角的Y坐标
DWORD dwRop // 光栅操作代码
);
按照下面修改对 DIBSection 的内存分配进行了优化,通过直接使用 VirtualAlloc 和 VirtualFree 管理内存,这种方法比之前使用 CreateDIBSection 更加灵活,且具备一定的性能和内存管理优势。以下是两个版本的主要区别和优化原因:
-
直接控制内存分配和释放
后面的代码版本用 VirtualAlloc 和 VirtualFree 直接管理内存,而不是依赖 CreateDIBSection 自动创建的位图内存。这种方式可以带来以下好处:更灵活的内存管理:可以更精确地控制内存分配和释放的方式(MEM_COMMIT 和 MEM_RELEASE),避免了不必要的内存开销。
减少资源依赖:省去了调用 DeleteObject 和 CreateDIBSection 的复杂性,减少对 GDI(图形设备接口)的依赖,简化代码逻辑。 -
避免频繁的设备上下文操作
在前一个版本中,每次都需要检查和重新生成设备上下文(BitmapDeviceContext)。而后面版本取消了设备上下文的依赖,通过 VirtualAlloc 直接分配内存,从而避免了设备上下文的频繁操作,这样可以减少系统开销并提高性能。 -
改善代码健壮性和性能
后面的代码版本简化了 DIBSection 的创建过程,同时提高了代码的健壮性。例如:
在释放内存前检查 BitmapMemory 是否为 NULL,确保不会重复释放无效的内存地址。
设置了 BytesPerPixel 和 BitmapMemorySize 来准确计算内存大小,避免了内存浪费。 -
增加跨平台适应性
虽然 CreateDIBSection 在 Windows 平台上性能优越,但直接使用 VirtualAlloc 可以更轻松地适配其他平台的内存管理逻辑。因此,改用 VirtualAlloc 更有利于将代码改进为跨平台兼容的代码。 -
精确管理内存的页属性
通过 VirtualAlloc 和 VirtualFree,可以更好地设置页面属性,例如 PAGE_READWRITE,从而实现更细粒度的内存访问控制。在某些特殊场景下(如多线程渲染或共享内存),可以显著提高内存访问的效率和安全性。
删掉
global_variable HBITMAP BitmapHandle;
global_variable HDC BitmapDeviceContext;
删掉
// TODO: 释放之前的 DIBSection
if (BitmapHandle) { // 如果位图句柄有效,释放之前创建的 DIBSection
DeleteObject(BitmapHandle); // 删除现有的位图对象
}
// 如果没有有效的设备上下文(BitmapDeviceContext),则创建一个
if (!BitmapDeviceContext) {
// TODO: 在某些特殊情况下是否需要重新创建这些对象
BitmapDeviceContext =
CreateCompatibleDC(0); // 创建一个与屏幕兼容的设备上下文
}
删掉
BitmapHandle =
CreateDIBSection(BitmapDeviceContext, // 设备上下文,NULL 表示不绑定设备
&BitmapInfo, // 位图信息,包括位图的大小、颜色深度等
DIB_RGB_COLORS, // 使用 RGB 颜色类型
&BitmapMemory, // 位图的内存指针,返回图像数据的指针
NULL, // 内存映射文件句柄,NULL 表示不使用
0); // 偏移量,通常设置为 0
// 释放设备上下文(BitmapDeviceContext)
ReleaseDC(0, BitmapDeviceContext); // 释放设备上下文(不再需要继续使用)
在创建 DIBSection(设备独立位图)时,需要指定内存来存储位图的数据,这个数据包括像素的 RGB 值,可能还要考虑字节对齐和填充。为确保位图可以正确显示,我们需要理解每行像素的对齐方式。
1. 位图内存计算方法
在位图中,每行的宽度(以字节为单位)需要是 4 字节的倍数,这是因为 Windows 位图格式要求每行都对齐到 4 字节。若行宽不足 4 字节,系统会自动填充,使得每行的内存占用达到 4 字节的倍数。这个填充过程称为“位图行填充”或“字节对齐”。
每行的实际字节数计算公式为:
行字节数
=
(
(
位图宽度
×
位深度
+
31
)
/
32
)
×
4
\text{行字节数} = ((\text{位图宽度} \times \text{位深度} + 31) / 32) \times 4
行字节数=((位图宽度×位深度+31)/32)×4
以 32 位位图为例:
- 每像素 4 字节(32 位 = RGBA,每通道 8 位)。
- 所以,每行的字节数应该是
width * 4
,若不满 4 字节倍数,则按上述公式补齐。
2. 分配内存大小
总的内存大小就是每行实际字节数乘以位图的高度:
总内存大小 = 行字节数 × 位图高度 \text{总内存大小} = \text{行字节数} \times \text{位图高度} 总内存大小=行字节数×位图高度
示例代码
假设我们有一个 width
和 height
定义的 32 位位图。以下代码展示如何创建 DIBSection 并分配内存:
int width = 640; // 位图宽度
int height = 480; // 位图高度
BITMAPINFO BitmapInfo;
ZeroMemory(&BitmapInfo, sizeof(BitmapInfo));
// 设置位图信息头
BitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
BitmapInfo.bmiHeader.biWidth = width;
BitmapInfo.bmiHeader.biHeight = -height; // 负值表示自上而下的位图
BitmapInfo.bmiHeader.biPlanes = 1;
BitmapInfo.bmiHeader.biBitCount = 32; // 32 位(每像素 4 字节)
BitmapInfo.bmiHeader.biCompression = BI_RGB;
// 计算每行所需字节数并分配内存
int rowBytes = ((width * 32 + 31) / 32) * 4; // 计算每行对齐字节数
int totalBytes = rowBytes * height; // 计算总内存大小
void* BitmapMemory;
HBITMAP hBitmap = CreateDIBSection(hdc, &BitmapInfo, DIB_RGB_COLORS, &BitmapMemory, NULL, 0);
if (hBitmap && BitmapMemory) {
// BitmapMemory 现在可以用于存储位图的像素数据
memset(BitmapMemory, 0, totalBytes); // 初始化内存
} else {
// 错误处理
}
注意事项
- 内存对齐:确保行对齐后再进行像素数据写入,否则图像可能会出现错位。
- 高度符号:
biHeight
为负时,位图自上而下排列;正值则自下而上排列。 - 颜色格式:32 位位图中,每像素占 4 字节,顺序为 BGRA。
VirtualAlloc
函数参数详解
参数:
-
lpAddress
:指定内存块的起始地址。- 通常设为
NULL
,由系统自动选择一个合适的地址。 - 如果需要特定的地址(例如共享内存),可以指定一个具体地址(但需要确保该地址未被占用)。
- 通常设为
-
dwSize
:要分配的内存大小,单位是字节。- 内存大小会自动按页面大小(通常是 4KB)向上取整,系统为每一块分配的内存区域至少是一个页面。
-
flAllocationType
:内存分配类型,可以是以下几种:MEM_RESERVE
:保留一块虚拟内存地址空间,但不实际分配物理内存。这通常用于分配较大内存,但暂不使用。MEM_COMMIT
:分配物理内存并映射到虚拟地址。已提交的内存可以被进程实际访问和操作。MEM_RESET
:将已提交的内存重置,并告知系统当前内存内容可以丢弃。MEM_RELEASE
:释放已分配的内存(需配合VirtualFree
使用),可以将之前保留的内存归还给系统。
-
flProtect
:内存保护属性,用于指定分配内存的访问权限:PAGE_READONLY
:内存只读。PAGE_READWRITE
:内存可读写。PAGE_EXECUTE_READWRITE
:内存可执行、可读写。PAGE_NOACCESS
:内存不可访问,用于调试或保护内存不被意外修改。
返回值:
- 返回分配的内存起始地址(
LPVOID
),如果失败则返回NULL
。可以通过GetLastError
获取失败的原因。
其他信息:
- 内存对齐:
VirtualAlloc
返回的内存块通常是页面对齐的(4KB 或 64KB 边界),非常适合高效地管理大块数据。 - 内存保护:
PAGE_READONLY
、PAGE_READWRITE
等保护属性可以用来提高内存安全性,限制不必要的读写和执行权限。 - 性能影响:
VirtualAlloc
属于低级内存管理函数,其效率较低,频繁调用会影响程序性能,通常用于分配较大内存。 - 与
VirtualFree
配合使用:用VirtualAlloc
分配的内存必须使用VirtualFree
来释放。
HeapAlloc
函数参数详解
HeapAlloc
是 Windows API 中用于动态分配内存的函数之一。它在堆中分配内存块,适合需要分配小块内存的场景,相比 VirtualAlloc
更灵活,管理更细粒度。它可以与 HeapFree
配合使用来手动管理进程内存,从而控制内存分配和释放的效率。
biHeight
的作用和含义
BITMAPINFO
结构中的 biHeight
字段用于指定位图的高度,并且具有重要的方向性含义。它的取值直接影响图像在内存中的存储顺序和显示方向。
- 定义高度:
biHeight
表示位图的高度,单位为像素。它指定了图像的行数。 - 控制图像存储方向:
- 如果
biHeight
是 正数,则图像数据会自 底部向上 存储,称为“自底向上位图”(bottom-up bitmap)。这种情况是 Windows 默认的存储方式。 - 如果
biHeight
是 负数,则图像数据会自 顶部向下 存储,称为“自顶向下位图”(top-down bitmap)。这种模式可以让图像按从上至下的方式存储在内存中,更符合大多数图像处理逻辑的顺序。
- 如果
将 char *Row = (char *)BitmapMemory
替换为 uint8 *Row = (uint8 *)BitmapMemory
的好处在于更加清晰地表达了数据的含义和意图,以及可能带来编译器优化和跨平台一致性方面的优势。以下是主要的好处:
1. 更清晰的数据语义
uint8
表示一个无符号的 8 位整数,即范围是 0 到 255。对于位图或图像处理,通常每个像素通道(例如 R、G、B、A 通道)的值在 0 到 255 之间,因此使用uint8
更能表达数据是图像像素的含义。char
类型在 C++ 标准中是一个 8 位有符号或无符号类型,取决于平台或编译器设置。这可能引发一些与负值相关的问题,例如在处理像素数据时,不希望看到负数值,但char
在一些平台上默认是有符号类型。
2. 避免符号位带来的错误或歧义
- 使用
uint8
明确指定为无符号类型,可以避免因符号位导致的计算或比较错误。尤其在处理颜色、图像或二进制数据时,符号位没有实际意义,使用无符号类型可以避免误解。 - 如果将
char
作为 8 位像素数据类型,则在一些平台上char
被解释为有符号类型,这会导致数据值范围从 -128 到 127,而不是图像通常期望的 0 到 255 的范围。使用uint8
解决了此类平台差异。
3. 跨平台一致性和编译器优化
- 使用
uint8
明确指定类型大小,确保无论在哪个平台上,uint8
都是 8 位且无符号的。这在跨平台开发中非常有帮助,避免了char
可能是有符号或无符号的二义性问题。 - 在一些编译器上,
uint8
可能更符合编译器的优化路径,尤其是一些用于图像处理的 SIMD(单指令多数据)指令集通常对无符号数据类型优化更好。
4. 可读性提升
- 使用
uint8
明确表示每个像素通道为 8 位的无符号整数,方便后续代码阅读和维护,减少了潜在的类型转换和符号解释错误,使代码逻辑更加清晰、易读。
总结
将 char *Row
替换为 uint8 *Row
提高了数据表达的精确性,减少了符号位可能带来的歧义,并增加了跨平台的一致性和代码的可读性,尤其适用于图像处理、颜色计算等直接涉及 8 位数据的场景。
然而这个char 可能是16 位的
typedef uint8_t uint8;
和 typedef unsigned int uint8;
在本质上有很大的区别,具体来说,二者主要在类型大小、范围、平台依赖性等方面存在差异。下面是详细的分析:
特性 | typedef uint8_t uint8; | typedef unsigned int uint8; |
---|---|---|
类型大小 | 8 位(固定) | 4 字节(通常,依平台而异) |
取值范围 | 0 到 255 | 0 到 4294967295(通常) |
平台一致性 | 跨平台一致(始终 8 位) | 依平台可能不同(可能是 32 位或 64 位) |
标准化 | C99 标准定义 | C 标准中不对大小有明确规定 |
使用场景 | 字节数据(如图像、网络协议、文件) | 整数运算,可能不适用于精确字节处理 |
uint8 *Row = (uint8 *)BitmapMemory;
1. 指针增量的大小不同
-
uint8 *Row = (uint8 *)BitmapMemory;
Row
是一个指向uint8
类型的指针,uint8
是一个 1 字节(8 位)的类型。因此,指针加法时,每次递增的地址值是 1 字节。也就是说:Row++; // 增加 1 字节,指向下一个 uint8
-
uint16 *Row = (uint16 *)BitmapMemory;
Row
是一个指向uint16
类型的指针,uint16
是一个 2 字节(16 位)的类型。因此,指针加法时,每次递增的地址值是 2 字节。也就是说:Row++; // 增加 2 字节,指向下一个 uint16
选择使用哪个类型的指针取决于数据的存储方式和你需要操作的数据类型大小。如果你正在处理图像数据并且每个像素是 8 位的(如 RGBA 图像的单通道),则使用 uint8 *Row
更合适。如果你的数据是 16 位或 2 字节宽的数据(例如某些音频数据或高分辨率图像),则使用 uint16 *Row
可以更高效地访问数据。
在图像处理和计算机图形学中,pitch 和 stride 常被用来描述图像中行与行之间的字节偏移量,但两者通常有细微的区别,具体如下:
1. Pitch
- 定义:Pitch 通常是图像缓冲区中一行像素实际占用的字节数,包括任何用于对齐的填充字节。
- 用途:通常用于计算在图像行之间的内存偏移量,以确保图像在内存中按行存储时对齐。例如,可能图像的实际宽度不满足某些内存对齐要求,Pitch 就是调整后实际的每行字节数。
- 示例:如果一行像素需要 300 字节,但出于对齐原因需要 320 字节,则 Pitch 是 320 字节。
2. Stride
- 定义:Stride 也指行与行之间的字节偏移量,通常等于图像实际宽度(以像素表示)乘以每个像素的字节数。但在某些环境中,Stride 也可能包括了对齐字节,因此它与 Pitch 含义相同。
- 用途:Stride 是计算行的字节偏移量的关键参数,尤其在不同图像处理库中,Stride 和 Pitch 的使用取决于实现方式,有时可互换。
- 示例:如果图像宽度是 100 像素,每个像素 4 字节,则 Stride 是 400 字节。如果此值包含对齐(比如 512 字节),则和 Pitch 相同。
主要区别
- Pitch 更强调图像缓冲区的实际存储宽度(含对齐字节数)。
- Stride 在某些情况下只代表“图像宽度 * 每像素字节数”的值,也可包含对齐字节。
在具体实现时,许多库将 Pitch 和 Stride 视作同义,但要注意文档细节,因为它们的定义在不同上下文中会有所不同。
通过你的注释可以更清晰地理解 Pitch 和 Stride 的实际效果。以下是详细解释:
内存布局说明
WIDTH ->
0 Width * BytesPerPixel
BitmapMemory -> 0 BB GG RR XX BB GG RR XX BB GG RR XX ...
BitmapMemory + Pitch 1 BB GG RR XX BB GG RR XX BB GG RR XX ...
- WIDTH:表示图像的宽度(以像素为单位)。
- BytesPerPixel:每个像素占用的字节数。此处假设每个像素 4 字节 (BB GG RR XX)。
- BitmapMemory:指向图像在内存中的起始地址。
- Pitch:代表从一行像素数据的开始位置到下一行数据开始位置之间的偏移字节数。
Pitch 和 Stride 区别
在上面布局的情况下,Pitch 是一行在内存中所占的总字节数,Stride 表示图像中一行实际像素宽度乘以每像素的字节数。它们的区别主要是:
-
Pitch 通常大于或等于 Stride,原因是可能包含额外的填充字节,使得图像每行数据按特定对齐方式存储(如 4 字节或 8 字节对齐)。
-
Stride 是图像宽度 (
Width
) 和每像素字节数 (BytesPerPixel
) 的乘积,即Width * BytesPerPixel
。- 例如,如果图像宽度为 100 像素,每像素 4 字节,那么
Stride = 100 * 4 = 400
字节。
- 例如,如果图像宽度为 100 像素,每像素 4 字节,那么
解释示例中的 Pitch
如果 Pitch
和 Stride
相同,说明没有额外的对齐填充字节。
但在某些情况下,为了满足内存对齐要求, Pitch
可能比 Stride
大,比如 Pitch
设置为 512 字节,而实际一行只需要 400 字节(Stride
),则每行多出 112 字节的填充数据,用于保证内存对齐。
内存布局
在这种格式下,每个像素占用 4 个字节,通常按行存储。例如,假设图像为 2x2 像素,内存中的数据可能会如下所示:
像素编号 | 红色 ® | 绿色 (G) | 蓝色 (B) | 填充字节 (XX) |
---|---|---|---|---|
Pixel 1 | 255 | 0 | 0 | 0 |
Pixel 2 | 0 | 255 | 0 | 0 |
Pixel 3 | 0 | 0 | 255 | 0 |
Pixel 4 | 255 | 255 | 0 | 0 |
假设图像是 2x2 像素,每个像素占 4 字节的内存空间:
- Pixel 1:
[255, 0, 0, 0]
- Pixel 2:
[0, 255, 0, 0]
- Pixel 3:
[0, 0, 255, 0]
- Pixel 4:
[255, 255, 0, 0]
内存布局(按行存储):
Row 1: [255, 0, 0, 0, 0, 255, 0, 0] // 第一行,两个像素
Row 2: [0, 0, 255, 0, 255, 255, 0, 0] // 第二行,两个像素
小端模式(Little Endian)是指数据的低字节存在低地址处,数据的高字节存在高地址处。在小端模式下,如果我们将一个 4 字节的整数 0x000000FF 存储在内存中,那么它会被存储为:
内存地址: 0x00 0x01 0x02 0x03
存储数据: 0xFF 0x00 0x00 0x00
如何避免阻塞主窗口
GetMessage
会导致调用它的线程进入阻塞状态,等待消息队列中的新消息。因此,在常见的 Windows GUI 应用程序中,如果主窗口和消息循环都在主线程中运行,那么 GetMessage
的阻塞确实会阻塞主窗口。换句话说,在 GetMessage
等待消息时,主线程会暂停执行其他操作,直到收到新消息,此时才会继续处理。
如果你希望在阻塞的情况下主窗口保持响应,通常可以使用以下方法:
-
多线程:
- 将耗时的操作放到后台线程中运行,这样主线程只处理消息循环而不会卡住 UI。
- 比如,使用
CreateThread
创建一个新线程,或使用标准库中的多线程工具(如std::thread
)。
-
PeekMessage:
- 使用
PeekMessage
而不是GetMessage
,因为PeekMessage
可以非阻塞地检查消息队列是否有消息,允许主线程在没有新消息时继续处理其他事情。 - 不过
PeekMessage
需要搭配一定的循环逻辑来避免占用过多的 CPU 资源。
- 使用
-
消息钩子或定时器:
- 通过
SetTimer
设置定时器来定期触发消息循环,从而让主线程定期处理 UI 更新,而不会因为GetMessage
的阻塞而停顿。 - 还可以用
SetWindowsHookEx
注册消息钩子,以捕获和处理特定类型的消息。
- 通过
使用这些方法,可以让 GetMessage
等待消息的同时,保持主窗口的响应性。
PeekMessageA
函数用于检查应用程序的消息队列中是否有特定消息,并根据需要将该消息从队列中移除。与 GetMessage
不同,PeekMessage
是非阻塞的——即便消息队列中没有符合条件的消息,它也会立即返回。下面是该函数的详细参数说明:
BOOL WINAPI PeekMessageA(
_Out_ LPMSG lpMsg, // 指向接收消息结构的指针
_In_opt_ HWND hWnd, // 指定要检查消息的窗口句柄(可以为 NULL)
_In_ UINT wMsgFilterMin, // 要检查的消息类型的最小值
_In_ UINT wMsgFilterMax, // 要检查的消息类型的最大值
_In_ UINT wRemoveMsg // 指定是否从队列中移除消息
);
参数解释
-
lpMsg
指向一个MSG
结构的指针。PeekMessage
将在lpMsg
中填入符合条件的消息内容。MSG
结构包括消息的类型、窗口句柄、消息时间、消息来源坐标等。 -
hWnd
指定要检查消息的窗口句柄。如果hWnd
为NULL
,则检查当前线程中所有窗口的消息;如果设置为特定的窗口句柄,则只检查该窗口的消息。 -
wMsgFilterMin
和wMsgFilterMax
用于设定消息类型的范围。wMsgFilterMin
是要过滤的最小消息值,而wMsgFilterMax
是最大消息值。此范围可以用于指定只处理某类消息。例如,设置wMsgFilterMin
为WM_KEYFIRST
,wMsgFilterMax
为WM_KEYLAST
可以过滤所有键盘消息。如果希望不过滤消息,通常会将这两个值设置为0
和0
。 -
wRemoveMsg
指定是否从消息队列中移除消息,可以取以下值:PM_NOREMOVE
:不从队列中移除消息,只查看消息内容。PM_REMOVE
:将消息从消息队列中移除,类似于GetMessage
的行为。PM_NOYIELD
:只对 16 位应用程序有效,现代 32 位和 64 位应用程序中不会用到。
返回值
- 返回值为非零 表示消息队列中存在符合条件的消息。
- 返回值为零 表示没有符合条件的消息。
使用场景
PeekMessageA
通常在需要频繁检查消息队列但不希望阻塞的场景下使用,例如游戏主循环或其他实时更新的应用程序中,搭配非阻塞逻辑可以实现更高的响应性:
MSG msg;
while (true) {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// 其他循环逻辑,例如渲染或更新
}
在此代码中,PeekMessage
检查并移除消息,如果没有消息,程序将继续执行其他操作,不会像 GetMessage
那样因阻塞而停止。
// game.cpp : Defines the entry point for the application.
//
#include <cstdint>
#include <stdint.h>
#include <windows.h>
#include <winuser.h>
#define internal static // 用于定义内翻译单元内部函数
#define local_persist static // 局部静态变量
#define global_variable static // 全局变量
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;
// TODO: 全局变量
global_variable bool Running;
global_variable BITMAPINFO BitmapInfo;
global_variable void *BitmapMemory;
// 后备缓冲区的宽度和高度
global_variable int BitmapWidth;
global_variable int BitmapHeight;
global_variable int BytesPerPixel = 4;
internal void RenderWeirdGradient(int xOffset, int yOffset) {
int width = BitmapWidth;
int height = BitmapHeight;
int Pitch = width * BytesPerPixel;
uint8 *Row = (uint8 *)BitmapMemory;
for (int Y = 0; Y < BitmapHeight; ++Y) {
uint32 *Pixel = (uint32 *)Row;
for (int X = 0; X < BitmapWidth; ++X) {
uint8 Blue = (X + xOffset);
uint8 Green = (Y + yOffset);
*Pixel++ = ((Green << 8) | Blue);
}
Row += Pitch;
}
}
// 这个函数用于重新调整 DIB(设备独立位图)大小
internal void Win32ResizeDIBSection(int width, int height) {
// device independent bitmap(设备独立位图)
// TODO: 进一步优化代码的健壮性
// 可能的改进:先不释放,先尝试其他方法,再如果失败再释放。
if (BitmapMemory) {
VirtualFree(
BitmapMemory, // 指定要释放的内存块起始地址
0, // 要释放的大小(字节),对部分释放有效,整体释放则设为 0
MEM_RELEASE); // MEM_RELEASE:释放整个内存块,将内存和地址空间都归还给操作系统
}
// 赋值后备缓冲的宽度和高度
BitmapWidth = width;
BitmapHeight = height;
// 设置位图信息头(BITMAPINFOHEADER)
BitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); // 位图头大小
BitmapInfo.bmiHeader.biWidth = BitmapWidth; // 设置位图的宽度
BitmapInfo.bmiHeader.biHeight = -BitmapHeight; // 设置位图的高度
BitmapInfo.bmiHeader.biPlanes = 1; // 设置颜色平面数,通常为 1
BitmapInfo.bmiHeader.biBitCount = 32; // 每像素的位数,这里为 32 位(即 RGBA)
BitmapInfo.bmiHeader.biCompression = BI_RGB; // 无压缩,直接使用 RGB 颜色模式
// 创建 DIBSection(设备独立位图)并返回句柄
// TODO:我们可以自己分配?
int BitmapMemorySize = (BitmapWidth * BitmapHeight) * BytesPerPixel;
BitmapMemory = VirtualAlloc(
0, // lpAddress:指定内存块的起始地址。
// 通常设为 NULL,由系统自动选择一个合适的地址。
BitmapMemorySize, // 要分配的内存大小,单位是字节。
MEM_COMMIT, // 分配物理内存并映射到虚拟地址。已提交的内存可以被进程实际访问和操作。
PAGE_READWRITE // 内存可读写
);
// TODO:可能会把它清除成黑色
}
// 这个函数用于将 DIBSection 绘制到窗口设备上下文
internal void Win32UpdateWindow(HDC DeviceContext, RECT *windowRect, int X,
int Y, int Width, int Height) {
int WindowWidth = windowRect->right - windowRect->left;
int WindowHeight = windowRect->bottom - windowRect->top;
// 使用 StretchDIBits 将 DIBSection 绘制到设备上下文中
StretchDIBits(
DeviceContext, // 目标设备上下文(窗口或屏幕的设备上下文)
/*
X, Y, Width, Height, // 目标区域的 x, y 坐标及宽高
X, Y, Width, Height,
*/
0, 0, BitmapWidth, BitmapHeight, //
0, 0, WindowWidth, WindowHeight,
// 源区域的 x, y 坐标及宽高(此处源区域与目标区域相同)
BitmapMemory, // 位图内存指针,指向 DIBSection 数据
&BitmapInfo, // 位图信息,包含位图的大小、颜色等信息
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: { // 窗口大小发生变化时的消息
RECT clientRect;
GetClientRect(hwnd, &clientRect);
// 计算绘制区域的宽度和高度
int Height = clientRect.bottom - clientRect.top;
int Width = clientRect.right - clientRect.left;
Win32ResizeDIBSection(Width, Height);
OutputDebugStringA("WM_SIZE\n"); // 输出调试信息,表示窗口大小已改变
} break;
case WM_DESTROY: { // 窗口销毁时的消息
// TODO: 处理错误,用重建窗口
Running = false;
} break;
case WM_CLOSE: { // 窗口关闭时的消息
// TODO: 像用户发送消息进行处理
Running = 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;
RECT clientRect;
GetClientRect(hwnd, &clientRect);
Win32UpdateWindow(DeviceContext, &clientRect, X, Y, Width, Height);
#if 0
local_persist DWORD Operation = WHITENESS;
// 使用 WHITENESS 操作符填充矩形区域为白色
PatBlt(DeviceContext, X, Y, Width, Height, Operation);
// 设置窗体的颜色在刷新时白色和黑色之间来回变换
if (Operation == WHITENESS) {
Operation = BLACKNESS;
} else {
Operation = WHITENESS;
}
#endif
// 调用 EndPaint 结束绘制,并释放设备上下文
EndPaint(hwnd, &Paint);
} break;
default: { // 对于不处理的消息,调用默认的窗口过程
Result = DefWindowProc(hwnd, Message, wParam,
LParam); // 调用默认窗口过程处理消息
} break;
}
return Result; // 返回处理结果
}
int CALLBACK WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, //
PSTR cmdline, int cmdshow) {
WNDCLASS WindowClass = {};
// 使用大括号初始化,所有成员都被初始化为零(0)或 nullptr
// 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;
Running = true;
while (Running) { // 启动一个无限循环,等待和处理消息
MSG Message; // 声明一个 MSG 结构体,用于接收消息
while (PeekMessage(
&Message,
// 指向一个 `MSG` 结构的指针。`PeekMessage`
// 将在 `lpMsg` 中填入符合条件的消息内容。
0,
// `hWnd` 为`NULL`,则检查当前线程中所有窗口的消息;
// 如果设置为特定的窗口句柄,则只检查该窗口的消息。
0, //
0, // 用于设定消息类型的范围
PM_REMOVE // 将消息从消息队列中移除,类似于 `GetMessage` 的行为。
)) {
if (Message.message == WM_QUIT) {
Running = false;
}
TranslateMessage(&Message); // 翻译消息,如果是键盘消息需要翻译
DispatchMessage(&Message); // 分派消息,调用窗口过程处理消息
}
RenderWeirdGradient(xOffset, yOffset);
// 这个地方需要渲染一下不然是黑屏
{
HDC DeviceContext = GetDC(Window);
RECT WindowRect;
GetClientRect(Window, &WindowRect);
int WindowWidth = WindowRect.right - WindowRect.left;
int WindowHeigh = WindowRect.bottom - WindowRect.top;
Win32UpdateWindow(DeviceContext, &WindowRect, 0, 0, WindowWidth,
WindowHeigh);
ReleaseDC(Window, DeviceContext);
}
++xOffset;
}
} else { // 如果窗口创建失败
// 这里可以处理窗口创建失败的逻辑
// 比如输出错误信息,或退出程序等
// TODO:
}
} else { // 如果窗口类注册失败
// 这里可以处理注册失败的逻辑
// 比如输出错误信息,或退出程序等
// TODO:
}
return 0;
}