游戏引擎学习第五天
这节貌似没讲什么
视频参考:https://www.bilibili.com/video/BV1Gmm2Y5EwE/
uint8 *A = somewhere in memory;
uint8 *B = somewhere in memory;
//BEFORE WE GOT TO HERE
int Y = *B; // == whatever was actually there before the 5
*A = 5;
int X = *B; // == 5
//Obviously! Y and X
//DUH? The compiler should just load it once
在你的代码示例中:
以下是对每部分的解释:
-
uint8 *A
和uint8 *B
:这两个是指向内存位置的指针,指向某个内存区域。具体的内存地址在代码中没有明确给出,因此我们不清楚它们指向的具体内存。 -
int Y = *B;
:这行代码将指针B
指向的内存位置的值解引用(即读取*B
)并将其赋值给Y
。 -
*A = 5;
:这行代码将指针A
指向的内存位置的值设置为5
。 -
int X = *B;
:最后,代码再次解引用指针B
,将其值赋给X
。
问题:为什么编译器不应该重新加载 *B
,而是直接使用 Y
的值?
- “编译器应该只加载一次”: 这段注释表示,从理论上讲,一旦
*B
的值被加载到Y
中,之后不应该再次加载,因为在代码中并没有直接修改B
指向的内存。因此,编译器本可以通过将*B
的值存储到临时变量中,从而避免在X
的赋值中再次解引用B
。
为什么这种优化可能没有发生?
-
内存访问优化: 编译器在优化内存访问时,通常假设内存可能被程序的其他部分修改,尤其是如果这些内存可能是共享的。比如,
A
和B
如果指向同一内存区域,或者内存的值可能被其他线程或硬件修改,编译器就不能假设*B
在两次访问之间没有发生变化。 -
指针别名(Pointer Aliasing): 如果
A
和B
是指向同一块内存区域的指针,编译器不能假设*B
的值在两次解引用之间不变,因为对*A
的赋值可能会影响到*B
(例如,A
和B
指向同一内存)。因此,编译器不能简单地优化掉第二次对*B
的解引用。 -
优化标志: 现代编译器有不同的优化等级,某些优化(如这类优化)只有在设置了较高的优化标志时才会执行(例如,GCC 中的
-O2
或-O3
)。如果没有进行优化,或者优化级别较低(如-O0
),编译器可能不会执行这种优化。 -
内存的易变性(Volatility): 如果
A
和B
指向的内存被标记为volatile
,编译器就会强制每次都解引用它们,而不进行任何假设或优化。volatile
常用于与内存映射的 I/O、硬件寄存器或并发环境中的共享内存交互。
总结
简单来说,编译器 可以 通过将 *B
的值存储到一个临时变量中,避免第二次解引用 *B
(即优化掉这次内存加载),前提是 编译器能够保证 *B
的值在两次解引用之间没有改变。然而,由于指针别名、并发操作或其他潜在的副作用,编译器可能不能进行这种优化。编译器是否进行优化取决于它对内存模型的理解以及它是否能保证两次解引用之间 *B
的值不变。
在这段代码中,MSG Message;
的声明位置取决于你对 MSG
变量的作用范围的需求。一般来说,建议将其声明放在 while
循环内部,而不是外部,原因如下:
-
局部性和性能:
如果将MSG
声明放在循环外部,MSG
变量将在每次循环时保持其值,可能导致不必要的残留数据。如果将其放在循环内部,每次循环都会创建一个新的MSG
变量,避免了不必要的状态残留。 -
清晰的作用范围:
将MSG Message;
放在循环内部可以限制其作用范围,使得它只在当前的循环中有效。这使得代码更加清晰,并且减少了作用域问题。例如,如果你在多个地方使用Message
,会更容易理解它的生命周期和作用域。 -
避免潜在的逻辑错误:
如果MSG Message;
放在循环外部,它可能会在消息队列没有更新时继续保留上一个循环的消息,这可能会导致错误的行为。将其放在内部能确保每次迭代都能正确接收和处理新的消息。
因此,将 MSG Message;
放在 while
循环内部是更好的选择。你的代码可以这样优化:
if (Window) { // 检查窗口句柄是否有效,若有效则进入消息循环
int xOffset = 0;
int yOffset = 0;
Running = true;
while (Running) { // 启动一个无限循环,等待和处理消息
MSG Message; // 将 MSG 声明移至 while 循环内部
while (PeekMessage(
&Message,
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 WindowHeight = WindowRect.bottom - WindowRect.top;
Win32UpdateWindow(DeviceContext, &WindowRect, 0, 0, WindowWidth, WindowHeight);
ReleaseDC(Window, DeviceContext);
}
++xOffset;
}
}
这样可以确保每次循环都从消息队列中正确获取新的消息,并且 MSG
变量的作用范围是局部的。
根据您提供的栈中变量地址和内容,可以绘制出堆栈结构图,展示各个局部变量如何在栈帧中存储。以下是一个简化的堆栈结构图:
栈结构图
+----------------------------------------------------+ <- 栈顶
| 返回地址 |
+----------------------------------------------------+
| 参数 cmdshow (int *) | <- 参数 cmdshow 存储在此,指向栈上的一个 int 值
| 0x000000721a8ffbc8 |
+----------------------------------------------------+
| 参数 cmdline (char **) | <- 参数 cmdline 存储在此,指向栈上的一个字符串
| 0x000000721a8ffbc0 |
+----------------------------------------------------+
| 参数 hInstPrev (HINSTANCE__ **) | <- 参数 hInstPrev 存储在此,指向一个空指针
| 0x000000721a8ffbb8 |
+----------------------------------------------------+
| 参数 hInst (HINSTANCE__ **) | <- 参数 hInst 存储在此,指向一个有效的 HINSTANCE
| 0x000000721a8ffbb0 |
+----------------------------------------------------+
| WindowClass (WNDCLASS) | <- WindowClass 存储在此
| 0x000000721a8ff2f0 |
| style = 0 |
| lpfnWndProc = 0x0000000000000000 |
| cbClsExtra = 0 |
| ... |
+----------------------------------------------------+
| BigOldBlockOfMemory (1024KB) | <- 大的内存块,占用 1024KB
| 0x000000721a8042d0 |
| [1024KB 内存空间] | <- 1MB 数据
+----------------------------------------------------+ <- 栈底
解释:
- 栈顶(返回地址):函数
WinMain
返回时,程序会跳到此地址。 - 参数
cmdshow
,cmdline
,hInstPrev
,hInst
:这些参数的值存储在栈帧中,按顺序排列:cmdshow
存储了一个int *
类型的地址,指向栈上的值10
。cmdline
存储了一个char **
类型的地址,指向栈上的字符串(为空字符串)。hInstPrev
存储了一个HINSTANCE__ **
类型的地址,指向NULL
。hInst
存储了一个HINSTANCE__ **
类型的地址,指向有效的程序映像基址game.exe
。
WindowClass
:是一个WNDCLASS
结构体,包含多种成员(如style
,lpfnWndProc
等)。每个成员被初始化为零或nullptr
。BigOldBlockOfMemory
:这是一个 1MB 大小的uint8
数组,栈上分配。它存储在BigOldBlockOfMemory
地址处,内部包含 1024KB 的数据。
注意:
- 栈分配顺序:栈的分配是从高地址向低地址方向进行的,所以栈顶的变量(返回地址)先存储,然后是函数参数,最后是局部变量(如
WindowClass
和BigOldBlockOfMemory
)。 - 指针的存储:对于指针类型的变量(如
cmdshow
,cmdline
,hInstPrev
,hInst
),它们存储的是指向实际数据的地址。这些数据的内容在栈帧中的其他位置。
// game.cpp : Defines the entry point for the application.
//
#include <cstdint>
#include <stdint.h>
#include <windows.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;
struct win32_offscreen_buffer {
BITMAPINFO Info;
void *Memory;
// 后备缓冲区的宽度和高度
int Width;
int Height;
int Pitch;
int BytesPerPixel;
};
// TODO: 全局变量
global_variable bool Running;
global_variable win32_offscreen_buffer GlobalBackbuffer;
// 添加这个去掉重复的冗余代码
struct win32_window_dimension {
int Width;
int Height;
};
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: 处理错误,用重建窗口
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;
win32_window_dimension Dimension = Win32GetWindowDimension(hwnd);
Win32DisplayBufferInWindow(DeviceContext, Dimension.Width, Dimension.Height,
GlobalBackbuffer, 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) {
uint8 BigOldBlockOfMemory[1004 * 1024];
WNDCLASS WindowClass = {};
// 使用大括号初始化,所有成员都被初始化为零(0)或 nullptr
Win32ResizeDIBSection(&GlobalBackbuffer, 1280, 720);
// 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(GlobalBackbuffer, xOffset, yOffset);
// 这个地方需要渲染一下不然是黑屏
{
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);
}
++xOffset;
}
} else { // 如果窗口创建失败
// 这里可以处理窗口创建失败的逻辑
// 比如输出错误信息,或退出程序等
// TODO:
}
} else { // 如果窗口类注册失败
// 这里可以处理注册失败的逻辑
// 比如输出错误信息,或退出程序等
// TODO:
}
return 0;
}