游戏引擎学习第12天

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

这节没讲什么东西,主要是改了一下音频的代码

后面有介绍一些alloc 和malloc,VirtualAlloc 的东西


_alloca 函数(或 alloca)分配的是栈内存,它的特点是:

  1. 生命周期受限于函数调用栈

    • 栈上的内存是函数调用的一部分,分配的内存会在函数返回时自动释放 。因此,_alloca 分配的内存只在分配它的函数的生命周期内有效。
    • 一旦函数返回,栈指针会复位,之前分配的内存就会被标记为可用,新的函数调用可能覆盖这些内容。
  2. 使用场景

    • _alloca 通常用于临时数据存储,例如小型缓冲区,能够快速分配和释放。
    • 不适合用于需要跨函数、长期使用的数据存储,因为这种内存无法脱离栈的生命周期存在。
  3. malloc 的对比

    • malloc/free 使用堆内存(heap memory),生命周期由程序员管理,适合长期存储。
    • _alloca 使用栈内存(stack memory),生命周期由函数作用域控制,适合临时、短期需求。
  4. 风险

    • 栈溢出 :栈内存是有限的,大量或频繁调用 _alloca 可能导致栈溢出(stack overflow)。
    • 悬挂指针 :如果返回指向 _alloca 分配的内存的指针并在外部使用,访问将导致未定义行为。

代码示例

cpp 复制代码
#include <cstdio>
#include <cstdlib>

void test_alloca() {
    char* buffer = (char*)_alloca(128);  // 在栈上分配 128 字节
    snprintf(buffer, 128, "This is temporary storage");
    printf("%s\n", buffer); // 输出正常
    // 函数返回后,buffer 指向的内存无效
}

int main() {
    char* permanent = (char*)malloc(128); // 在堆上分配 128 字节
    snprintf(permanent, 128, "This is permanent storage");
    
    test_alloca();

    printf("%s\n", permanent); // 输出仍正常,堆内存仍有效
    free(permanent); // 手动释放堆内存
    return 0;
}

总结

  • _alloca 分配的内存是临时的,受限于栈的生命周期。
  • 如果需要长期使用或在多个函数间共享数据,应使用堆内存(例如 malloc)。
  • 理解栈和堆的区别有助于避免常见的内存管理问题,如悬挂指针和栈溢出。

game.h

cpp 复制代码
#pragma once
#include <cmath>
#include <cstdint>
#include <malloc.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;

// NOTE: 平台层为游戏提供的服务
// NOTE: 游戏为平台玩家提供的服务
// (这个部分未来可能扩展------例如声音处理可能在单独的线程中)

// 四个主要功能 - 时间管理,控制器/键盘输入,位图缓冲区,声音缓冲区

struct game_offscreen_buffer {
  // TODO(casey):未来,渲染将特别变成一个三层抽象!!!
  void *Memory;
  // 后备缓冲区的宽度和高度
  int Width;
  int Height;
  int Pitch;
  int BytesPerPixel;
};

struct game_sound_output_buffer {
  int SamplesPerSecond; // 采样率:每秒采样48000次
  int SampleCount;
  int16 *Samples;
};

// 游戏更新和渲染的主函数
internal void GameUpdateAndRender(game_offscreen_buffer *Buffer, int BlueOffset,
                                  int GreenOffset);

// 三个主要功能:
// 1. 时间管理(Timing)
// 2. 控制器/键盘输入(Controller/Keyboard Input)
// 3. 位图输出(Bitmap Output)和声音(Sound)
// 使用的缓冲区(Buffer)

game.cpp

cpp 复制代码
#include "game.h"

internal void GameOutputSound(game_sound_output_buffer *SoundBuffer,
                              int ToneHz) {
  local_persist real32 tSine;
  int16 ToneVolume = 3000;
  int16 *SampleOut = SoundBuffer->Samples;
  int WavePeriod = SoundBuffer->SamplesPerSecond / ToneHz;
  // 循环写入样本到第一段区域
  for (int SampleIndex = 0; SampleIndex < SoundBuffer->SampleCount;
       ++SampleIndex) {
    real32 SineValue = sinf(tSine);
    int16 SampleValue = (int16)(SineValue * ToneVolume);
    *SampleOut++ = SampleValue; // 左声道
    *SampleOut++ = SampleValue; // 右声道
    tSine += 2.0f * (real32)Pi32 * 1.0f / (real32)WavePeriod;
  }
}

// 渲染一个奇异的渐变图案
internal void
RenderWeirdGradient(game_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; // 移动到下一行
  }
}

internal void GameUpdateAndRender(game_offscreen_buffer *Buffer, int BlueOffset,
                                  int GreenOffset,
                                  game_sound_output_buffer *SoundBuffer,
                                  int ToneHz) {
  GameOutputSound(SoundBuffer, ToneHz);
  RenderWeirdGradient(Buffer, BlueOffset, GreenOffset);
}

win32_game.cpp

cpp 复制代码
// game.cpp : Defines the entry point for the application.
//

/**
T这不是最终版本的平台层
1. 存档位置
2. 获取自己可执行文件的句柄
3. 资源加载路径
4. 线程(启动线程)
5. 原始输入(支持多个键盘)
6. Sleep/TimeBeginPeriod
7. ClipCursor()(多显示器支持)
8. 全屏支持
9. WM_SETCURSOR(控制光标可见性)
10. QueryCancelAutoplay
11. WM_ACTIVATEAPP(当我们不是活动应用程序时)
12. Blit速度优化(BitBlt)
13. 硬件加速(OpenGL或Direct3D或两者?)
14. GetKeyboardLayout(支持法语键盘、国际化WASD键支持)
只是一个部分清单
*/

#include <cstdint>
#include <dsound.h>
#include <memoryapi.h>
#include <windows.h>
#include <winnt.h>
#include <xinput.h>

#include "game.cpp"
#include "game.h"

// 添加这个去掉重复的冗余代码
struct win32_window_dimension {
  int Width;
  int Height;
};

struct win32_offscreen_buffer {
  BITMAPINFO Info;
  void *Memory;
  // 后备缓冲区的宽度和高度
  int Width;
  int Height;
  int Pitch;
  int BytesPerPixel;
};

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;
}

// 这个函数用于重新调整 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 Win32ClearBuffer(win32_sound_output *SoundOutput) {
  VOID *Region1; // 第一段区域指针,用于存放锁定后的首部分缓冲区地址
  DWORD Region1Size; // 第一段区域的大小(字节数)
  VOID *Region2; // 第二段区域指针,用于存放锁定后的剩余部分缓冲区地址
  DWORD Region2Size; // 第二段区域的大小(字节数)
  if (SUCCEEDED(GlobalSecondaryBuffer->Lock(
          0, // 缓冲区偏移量,指定开始锁定的字节位置
          SoundOutput
              ->SecondaryBufferSize, // 锁定的字节数,指定要锁定的区域大小
          &Region1, // 输出,返回锁定区域的内存指针(第一个区域)
          &Region1Size, // 输出,返回第一个锁定区域的实际字节数
          &Region2, // 输出,返回第二个锁定区域的内存指针(可选,双缓冲或环形缓冲时使用)
          &Region2Size, // 输出,返回第二个锁定区域的实际字节数
          0 // 标志,控制锁定行为(如从光标位置锁定等)
          ))) {
    int8 *DestSample = (int8 *)Region1; // 将第一段区域指针转换为 16
                                        // 位整型指针,准备写入样本数据
    // 循环写入样本到第一段区域
    for (DWORD ByteIndex = 0; ByteIndex < Region1Size; ++ByteIndex) {
      *DestSample++ = 0;
    }
    for (DWORD ByteIndex = 0; ByteIndex < Region2Size; ++ByteIndex) {
      *DestSample++ = 0;
    }
    GlobalSecondaryBuffer->Unlock(Region1, Region1Size, //
                                  Region2, Region2Size);
  }
}
internal void Win32FillSoundBuffer(win32_sound_output *SoundOutput,
                                   DWORD ByteToLock, DWORD BytesToWrite,
                                   game_sound_output_buffer *SourceBuffer) {

  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 *DestSample = (int16 *)Region1; // 将第一段区域指针转换为 16
                                          // 位整型指针,准备写入样本数据
    int16 *SourceSample = SourceBuffer->Samples;
    // 循环写入样本到第一段区域
    for (DWORD SampleIndex = 0; SampleIndex < Region1SampleCount;
         ++SampleIndex) {
      *DestSample++ = *SourceSample++; // 左声道
      *DestSample++ = *SourceSample++; // 右声道
      SoundOutput->RunningSampleIndex++;
    }

    DWORD Region2SampleCount =
        Region2Size / SoundOutput->BytesPerSample; // 计算第二段区域中的样本数量
    DestSample = (int16 *)Region2; // 将第二段区域指针转换为 16
                                   // 位整型指针,准备写入样本数据
    // 循环写入样本到第二段区域
    for (DWORD SampleIndex = 0; SampleIndex < Region2SampleCount;
         ++SampleIndex) {
      // 使用相同逻辑生成方波样本数据
      *DestSample++ = *SourceSample++; // 左声道
      *DestSample++ = *SourceSample++; // 右声道
      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;

      int16 *Samples = (int16 *)VirtualAlloc(0, 48000 * 2 * sizeof(int16),
                                             MEM_RESERVE | MEM_COMMIT,
                                             PAGE_READWRITE); //[48000 * 2];

      Win32InitDSound(Window, SoundOutput.SamplesPerSecond,
                      SoundOutput.SecondaryBufferSize); // 初始化 DirectSound
      Win32ClearBuffer(&SoundOutput);
      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 ByteToLock;
        DWORD PlayCursor = 0;  // 播放游标,指示当前播放位置
        DWORD WriteCursor = 0; // 写入游标,指示当前写入位置
        DWORD TargetCursor = 0;
        bool32 SoundIsValid = false;
        DWORD BytesToWrite = 0; // 需要写入的字节数
        // 获取音频缓冲区的当前播放和写入位置
        if (SUCCEEDED(GlobalSecondaryBuffer->GetCurrentPosition(
                &PlayCursor, &WriteCursor))) {
          ByteToLock =
              ((SoundOutput.RunningSampleIndex * SoundOutput.BytesPerSample) %
               SoundOutput.SecondaryBufferSize);
          TargetCursor = (PlayCursor + (SoundOutput.LatencySampleCount *
                                        SoundOutput.BytesPerSample)) %
                         SoundOutput.SecondaryBufferSize;

          // 判断 ByteToLock 与 TargetCursor 的位置关系以确定写入量
          if (ByteToLock == TargetCursor) {
            // 如果锁定位置正好等于播放位置,写入整个缓冲区
            BytesToWrite = 0;
          } else if (ByteToLock > TargetCursor) {
            // 如果锁定位置在播放位置之后,写入从锁定位置到缓冲区末尾,再加上开头到播放位置的字节数
            BytesToWrite =
                (SoundOutput.SecondaryBufferSize - ByteToLock) + TargetCursor;
          } else {
            // 如果锁定位置在播放位置之前,写入从锁定位置到播放位置之间的字节数
            BytesToWrite = TargetCursor - ByteToLock;
          }
          SoundIsValid = true;
        }

        if (!SoundIsPlaying) {
          GlobalSecondaryBuffer->Play(0, 0, DSBPLAY_LOOPING);
          SoundIsPlaying = true;
        }

        game_sound_output_buffer SoundBuffer = {};
        SoundBuffer.SamplesPerSecond = SoundOutput.SamplesPerSecond;
        SoundBuffer.SampleCount = BytesToWrite / SoundOutput.BytesPerSample;
        SoundBuffer.Samples = Samples;

        game_offscreen_buffer Buffer = {};
        Buffer.Memory = GlobalBackbuffer.Memory;
        Buffer.Width = GlobalBackbuffer.Width;
        Buffer.Height = GlobalBackbuffer.Height;
        Buffer.Pitch = GlobalBackbuffer.Pitch;
        GameUpdateAndRender(&Buffer, xOffset, yOffset, &SoundBuffer,
                            SoundOutput.ToneHz);

        if (SoundIsValid) {
          Win32FillSoundBuffer(&SoundOutput, ByteToLock, BytesToWrite,
                               &SoundBuffer);
          // 计算需要锁定的字节位置,基于当前样本索引和每样本字节数
        }

        // 这个地方需要渲染一下不然是黑屏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);
#if 0
        char Buffer[256];
        sprintf_s(Buffer, "%fms/f, %ff/s, %fmc/f\n", MillisecondPerFrame, FPS,
                  MCPF);
        OutputDebugString(Buffer);
#endif
        LastCounter = EndCounter;
        LastCycleCount = EndCycleCount;
      }
    } else { // 如果窗口创建失败
             // 这里可以处理窗口创建失败的逻辑
             // 比如输出错误信息,或退出程序等
             // TODO:
    }
  } else { // 如果窗口类注册失败
           // 这里可以处理注册失败的逻辑
           // 比如输出错误信息,或退出程序等
           // TODO:
  }

  return 0;
}
相关推荐
bst@微胖子3 分钟前
Flutter之用户输入&网络数据&缓存
android·flutter·缓存
行墨14 分钟前
Kotlin 的 ‌init 代码块‌
android
xiufeia17 分钟前
记录学习的第二十一天
学习
_一条咸鱼_18 分钟前
Android Compose 框架原生集成深度剖析(六十一)
android·前端·设计
木西22 分钟前
React Native项目初始化及相关通用工具集成
android·react native·ios
张屿秋30 分钟前
在Unity中,如果物体上的脚本丢失,可以通过编写一个自定义编辑器脚本来查找并删除这些丢失的组件
unity·编辑器·游戏引擎
QING6183 小时前
Kotlin windowedSequence用法及代码示例
android·kotlin·源码阅读
QING6183 小时前
Kotlin MatchResult.Destructured用法及代码示例
android·kotlin·源码阅读
恋猫de小郭4 小时前
注意,暂时不要升级 MacOS ,Flutter/RN 等构建 ipa 可能会因 「ITMS-90048」This bundle is invalid 被拒绝
android·前端·flutter
梁下轻语的秋缘5 小时前
实验二 VLAN 的配置与应用
网络·学习·计算机网络·智能路由器