Compare commits
2 Commits
3fdc774f3f
...
f1584ec3c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1584ec3c6 | ||
|
|
7da17ccadd |
@@ -20,6 +20,9 @@
|
|||||||
#pragma comment(lib, "dxgi.lib")
|
#pragma comment(lib, "dxgi.lib")
|
||||||
#pragma comment(lib, "d3d12.lib")
|
#pragma comment(lib, "d3d12.lib")
|
||||||
|
|
||||||
|
namespace XEngine::graphics::d3d12::core {
|
||||||
|
constexpr u32 frame_buffer_count{ 3 };
|
||||||
|
}
|
||||||
|
|
||||||
// 定义 DirectX 调试宏 DXCall,用于在调试模式下检查 DirectX API 调用返回值
|
// 定义 DirectX 调试宏 DXCall,用于在调试模式下检查 DirectX API 调用返回值
|
||||||
// 如果调用失败(FAILED),则输出错误信息(文件名、行号、调用语句)并触发断点
|
// 如果调用失败(FAILED),则输出错误信息(文件名、行号、调用语句)并触发断点
|
||||||
@@ -46,9 +49,24 @@ if(FAILED(x)){ \
|
|||||||
#endif // !DXCall
|
#endif // !DXCall
|
||||||
#endif // _DEBUG
|
#endif // _DEBUG
|
||||||
|
|
||||||
// 定义 DirectX 对象命名宏,用于在调试模式下为 Direct3D 12 对象设置名称
|
// 定义 DirectX 对象命名宏,用于在调试模式下为 Direct3D 12 对象设置调试名称
|
||||||
|
// 这些宏仅在 _DEBUG 模式下生效,可帮助开发者在 PIX、RenderDoc 等图形调试工具中
|
||||||
|
// 识别和追踪 D3D12 对象(如缓冲区、纹理、管线状态等)
|
||||||
|
// NAME_D3D12_OBJECT: 为单个对象设置名称
|
||||||
|
// NAME_D3D12_OBJECT_INDEXED: 为数组中的对象设置带索引的名称(如资源数组)
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
#define NAME_D3D12_OBJECT(obj,name) obj->SetName(name); OutputDebugString(L"::D3D12 Object Created: "); OutputDebugString(name); OutputDebugString(L"\n");
|
#define NAME_D3D12_OBJECT(obj,name) obj->SetName(name); OutputDebugString(L"::D3D12 Object Created: "); OutputDebugString(name); OutputDebugString(L"\n");
|
||||||
|
#define NAME_D3D12_OBJECT_INDEXED(obj,n,name) \
|
||||||
|
{ \
|
||||||
|
wchar_t full_name[128]; \
|
||||||
|
if(swprintf_s(full_name, L"%s[%llu]", name, (u64)n) >0 ){ \
|
||||||
|
obj->SetName(full_name); \
|
||||||
|
OutputDebugString(L"::D3D12 Object Created: "); \
|
||||||
|
OutputDebugString(full_name); \
|
||||||
|
OutputDebugString(L"\n"); \
|
||||||
|
}}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
#define NAME_D3D12_OBJECT(obj,name)
|
#define NAME_D3D12_OBJECT(obj,name)
|
||||||
|
#define NAME_D3D12_OBJECT_INDEXED(obj,n,name)
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -4,6 +4,230 @@
|
|||||||
using namespace Microsoft::WRL;
|
using namespace Microsoft::WRL;
|
||||||
namespace XEngine::graphics::d3d12::core {
|
namespace XEngine::graphics::d3d12::core {
|
||||||
namespace {
|
namespace {
|
||||||
|
/**
|
||||||
|
* @brief D3D12命令管理类设计说明
|
||||||
|
* @details 本类采用RAII设计模式封装Direct3D 12的命令队列和命令列表,提供类型安全的GPU命令提交机制
|
||||||
|
*
|
||||||
|
* ## 整体设计思路
|
||||||
|
*
|
||||||
|
* 1. **类型安全封装**: 通过模板或构造函数参数区分Direct/Compute/Copy命令类型,
|
||||||
|
* 避免运行时类型错误,编译期即可确定队列类型
|
||||||
|
*
|
||||||
|
* 2. **资源生命周期管理**:
|
||||||
|
* - 构造函数负责创建命令队列和命令列表
|
||||||
|
* - release()方法统一释放资源,支持异常安全(构造函数失败时调用)
|
||||||
|
* - 析构函数自动调用release(),防止资源泄漏
|
||||||
|
*
|
||||||
|
* 3. **帧同步机制**: 内部command_frame结构体管理每帧的命令分配器,
|
||||||
|
* 支持CPU-GPU帧同步,避免命令分配器重置冲突
|
||||||
|
*
|
||||||
|
* 4. **命名调试支持**: 使用NAME_D3D12_OBJECT宏为D3D对象设置调试名称,
|
||||||
|
* 便于GPU调试工具(PIX/RenderDoc)识别
|
||||||
|
*
|
||||||
|
* ## 核心优势
|
||||||
|
*
|
||||||
|
* - **异常安全**: 构造函数采用goto错误处理模式,任何步骤失败都会自动清理已分配资源
|
||||||
|
* - **多队列支持**: 单一类支持D3D12三种命令队列类型(Direct/Compute/Copy),代码复用率高
|
||||||
|
* - **现代D3D12 API**: 使用ID3D12GraphicsCommandList6接口,支持最新渲染特性(如Mesh Shader)
|
||||||
|
* - **零开销抽象**: 轻量级封装,不引入额外运行时开销,直接操作底层D3D12对象
|
||||||
|
* - **可扩展性**: 预留frame管理接口,可轻松扩展为多缓冲帧循环、命令列表录制状态追踪等功能
|
||||||
|
*/
|
||||||
|
//
|
||||||
|
// ## 多帧渲染架构设计原理
|
||||||
|
//
|
||||||
|
// 现代GPU渲染采用"生产者-消费者"模型:CPU作为命令生产者录制渲染命令,
|
||||||
|
// GPU作为消费者异步执行。为避免CPU等待GPU完成,需要引入帧缓冲机制。
|
||||||
|
//
|
||||||
|
// ### 为什么需要多帧缓冲?
|
||||||
|
//
|
||||||
|
// 1. **CPU-GPU并行性**: 单缓冲模式下,CPU必须等待GPU完成当前帧才能录制下一帧,
|
||||||
|
// 导致CPU空闲等待。多帧缓冲允许CPU提前录制N帧,GPU异步执行,最大化硬件利用率
|
||||||
|
//
|
||||||
|
// 2. **命令分配器冲突解决**: D3D12中ID3D12CommandAllocator在GPU执行期间
|
||||||
|
// 不能被重置。每帧使用独立的分配器,当前帧提交GPU后,CPU可立即重置
|
||||||
|
// (frame_buffer_count-1)帧之前的分配器,实现无等待循环
|
||||||
|
//
|
||||||
|
// 3. **帧时序稳定性**: 缓冲N帧可平滑帧率波动,避免单帧卡顿影响整体流畅度
|
||||||
|
//
|
||||||
|
// ### 帧索引轮转机制
|
||||||
|
//
|
||||||
|
// 使用环形缓冲区(ring buffer)管理帧资源:
|
||||||
|
// - 当前帧索引: current_frame_index % frame_buffer_count
|
||||||
|
// - 每帧提交后递增索引,到达frame_buffer_count时归零
|
||||||
|
// - 确保CPU不会超前GPU超过frame_buffer_count帧,防止资源冲突
|
||||||
|
//
|
||||||
|
// ### 命令列表创建策略
|
||||||
|
//
|
||||||
|
// 创建时使用_cmd_frames[0].cmd_allocator作为初始分配器,原因:
|
||||||
|
// - 命令列表创建时必须绑定一个分配器,即使后续通过Reset()切换
|
||||||
|
// - 选择索引0确保初始化阶段有确定的资源状态
|
||||||
|
// - 实际录制前会调用Reset()绑定当前帧对应的分配器
|
||||||
|
//
|
||||||
|
// ### 立即关闭命令列表的设计考量
|
||||||
|
//
|
||||||
|
// 创建后立即调用Close(),因为:
|
||||||
|
// - 新创建的命令列表处于"录制打开"状态
|
||||||
|
// - 但实际渲染前需要重新Reset()绑定正确的帧分配器
|
||||||
|
// - 先Close()使列表进入可提交状态,避免状态不一致
|
||||||
|
// - 这是一种防御性编程,确保对象始终处于有效状态
|
||||||
|
class d3d12_command
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
d3d12_command() = default;
|
||||||
|
DISABLE_COPY_AND_MOVE(d3d12_command)
|
||||||
|
explicit d3d12_command(ID3D12Device8 *const device, D3D12_COMMAND_LIST_TYPE type)
|
||||||
|
{
|
||||||
|
HRESULT hr{ S_OK };
|
||||||
|
|
||||||
|
// 创建命令队列
|
||||||
|
D3D12_COMMAND_QUEUE_DESC desc{};
|
||||||
|
desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; // 无特殊标志
|
||||||
|
desc.NodeMask = 0; // 单GPU节点
|
||||||
|
desc.Type = type; // 命令队列类型(Direct/Compute/Copy)
|
||||||
|
desc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL; // 普通优先级
|
||||||
|
DXCall(hr = device->CreateCommandQueue(&desc, IID_PPV_ARGS(&_cmd_queue)));
|
||||||
|
if(FAILED(hr)) goto _error;
|
||||||
|
NAME_D3D12_OBJECT(_cmd_queue, type == D3D12_COMMAND_LIST_TYPE_DIRECT ?
|
||||||
|
L"Direct Command Queue" :
|
||||||
|
type == D3D12_COMMAND_LIST_TYPE_COMPUTE ?
|
||||||
|
L"Compute Command Queue" : L" Command Queue");
|
||||||
|
|
||||||
|
// 为所有帧创建命令分配器
|
||||||
|
for(u32 i{ 0 }; i < frame_buffer_count; ++i)
|
||||||
|
{
|
||||||
|
command_frame& frame{_cmd_frames[i]};
|
||||||
|
DXCall(hr = device->CreateCommandAllocator(type, IID_PPV_ARGS(&frame.cmd_allocator)));
|
||||||
|
if(FAILED(hr)) goto _error;
|
||||||
|
NAME_D3D12_OBJECT_INDEXED(frame.cmd_allocator, i, type == D3D12_COMMAND_LIST_TYPE_DIRECT ?
|
||||||
|
L"Direct Command Allocator" :
|
||||||
|
type == D3D12_COMMAND_LIST_TYPE_COMPUTE ?
|
||||||
|
L"Compute Command Allocator" : L" Command Allocator");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建命令列表 - 采用多帧缓冲设计实现CPU-GPU并行渲染,传入第一帧的分配器作为初始分配器,并立即关闭命令列表
|
||||||
|
DXCall(hr = device->CreateCommandList(0,type,_cmd_frames[0].cmd_allocator,nullptr,IID_PPV_ARGS(&_cmd_list)));
|
||||||
|
if(FAILED(hr)) goto _error;
|
||||||
|
DXCall(_cmd_list->Close());
|
||||||
|
NAME_D3D12_OBJECT(_cmd_list, type == D3D12_COMMAND_LIST_TYPE_DIRECT ?
|
||||||
|
L"Direct Command List" :
|
||||||
|
type == D3D12_COMMAND_LIST_TYPE_COMPUTE ?
|
||||||
|
L"Compute Command List" : L" Command List");
|
||||||
|
|
||||||
|
DXCall(hr = device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&_fence)));
|
||||||
|
if(FAILED(hr)) goto _error;
|
||||||
|
NAME_D3D12_OBJECT(_fence, L"D3D12 Fence");
|
||||||
|
|
||||||
|
_fence_event = CreateEventEx(nullptr, nullptr, 0, EVENT_ALL_ACCESS);
|
||||||
|
assert(_fence_event);
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
_error:
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
|
||||||
|
~d3d12_command()
|
||||||
|
{
|
||||||
|
assert(!_cmd_queue && !_cmd_list && !_fence);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待当前帧被标记为完成信号,并重置命令分配器和命令列表
|
||||||
|
void begin_frame()
|
||||||
|
{
|
||||||
|
command_frame& frame{_cmd_frames[_frame_index]};
|
||||||
|
frame.wait(_fence_event, _fence);
|
||||||
|
// 重置命令分配器将释放之前帧分配的命令内存,使其可重新用于录制新帧的命令
|
||||||
|
// 重置命令列表将命令列表重置为可录制状态,准备录制录制命令
|
||||||
|
DXCall(frame.cmd_allocator->Reset());
|
||||||
|
DXCall(_cmd_list->Reset(frame.cmd_allocator, nullptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用新的围栏值来标记这个围栏
|
||||||
|
void end_frame()
|
||||||
|
{
|
||||||
|
//在提交命令列表前,先关闭命令列表,确保命令列表进入可提交状态
|
||||||
|
DXCall(_cmd_list->Close());
|
||||||
|
// 将命令列表转为数组形式提交给命令队列执行
|
||||||
|
// 虽然目前只有单个命令列表且为单线程工作模式,但仍采用数组方式以保持代码的扩展性
|
||||||
|
ID3D12CommandList *const cmd_lists[]{_cmd_list};
|
||||||
|
_cmd_queue->ExecuteCommandLists(_countof(cmd_lists), &cmd_lists[0]);
|
||||||
|
|
||||||
|
u64& fence_value{_fence_value};
|
||||||
|
++fence_value;
|
||||||
|
command_frame& frame{_cmd_frames[_frame_index]};
|
||||||
|
frame.fence_value = fence_value;
|
||||||
|
_cmd_queue->Signal(_fence, fence_value);
|
||||||
|
_frame_index = (_frame_index + 1) % frame_buffer_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 等待所有帧的命令列表执行完成
|
||||||
|
* @details 确保所有帧的命令列表执行完成,避免资源冲突
|
||||||
|
*/
|
||||||
|
void flush()
|
||||||
|
{
|
||||||
|
for(u32 i{ 0 }; i < frame_buffer_count; ++i)
|
||||||
|
{
|
||||||
|
_cmd_frames[i].wait(_fence_event, _fence);
|
||||||
|
}
|
||||||
|
_frame_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void release()
|
||||||
|
{
|
||||||
|
flush();
|
||||||
|
core::release(_fence);
|
||||||
|
_fence_value = 0;
|
||||||
|
|
||||||
|
CloseHandle(_fence_event);
|
||||||
|
_fence_event = nullptr;
|
||||||
|
|
||||||
|
core::release(_cmd_queue);
|
||||||
|
core::release(_cmd_list);
|
||||||
|
|
||||||
|
for(u32 i{ 0 }; i < frame_buffer_count; ++i)
|
||||||
|
{
|
||||||
|
_cmd_frames[i].release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr ID3D12CommandQueue *const command_queue() const {return _cmd_queue;}
|
||||||
|
constexpr ID3D12GraphicsCommandList6 *const command_list() const {return _cmd_list;}
|
||||||
|
constexpr u32 frame_index() const {return _frame_index;}
|
||||||
|
private:
|
||||||
|
|
||||||
|
struct command_frame
|
||||||
|
{
|
||||||
|
ID3D12CommandAllocator* cmd_allocator{ nullptr };
|
||||||
|
u64 fence_value{ 0 };
|
||||||
|
|
||||||
|
void wait(HANDLE fence_event, ID3D12Fence1* fence)
|
||||||
|
{
|
||||||
|
assert(fence && fence_event);
|
||||||
|
// 如果当前的Fence值小于目标值,说明GPU还没有执行完成当前的命令列表
|
||||||
|
if(fence->GetCompletedValue() < fence_value)
|
||||||
|
{
|
||||||
|
// 我们需要等待GPU执行当前的命令列表,设置事件并等待事件触发
|
||||||
|
DXCall(fence->SetEventOnCompletion(fence_value, fence_event));
|
||||||
|
WaitForSingleObject(fence_event, INFINITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void release()
|
||||||
|
{
|
||||||
|
core::release(cmd_allocator);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ID3D12CommandQueue* _cmd_queue{ nullptr };
|
||||||
|
ID3D12GraphicsCommandList6* _cmd_list{ nullptr };
|
||||||
|
ID3D12Fence1* _fence{ nullptr };
|
||||||
|
// 对于围栏值来说他是64位无符号整型,有2^64-1个值,即便每秒1000帧,也需要5.8亿年才能回绕,所以不需要担心一直递增导致溢出的问题
|
||||||
|
u64 _fence_value{ 0 };
|
||||||
|
command_frame _cmd_frames[frame_buffer_count]{};
|
||||||
|
HANDLE _fence_event{ nullptr };
|
||||||
|
u32 _frame_index{ 0 };
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 主 Direct3D 12 设备指针
|
* @brief 主 Direct3D 12 设备指针
|
||||||
@@ -17,6 +241,12 @@ ID3D12Device8* main_device{ nullptr };
|
|||||||
*/
|
*/
|
||||||
IDXGIFactory7* dxgi_factory{ nullptr };
|
IDXGIFactory7* dxgi_factory{ nullptr };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 命令管理类实例
|
||||||
|
* @details 用于管理 Direct3D 12 命令队列和命令列表,提供类型安全的 GPU命令提交机制
|
||||||
|
*/
|
||||||
|
d3d12_command gfx_command;
|
||||||
|
|
||||||
// 最小支持的 Direct3D 特本级别
|
// 最小支持的 Direct3D 特本级别
|
||||||
constexpr D3D_FEATURE_LEVEL minumum_feature_level{ D3D_FEATURE_LEVEL_11_0 };
|
constexpr D3D_FEATURE_LEVEL minumum_feature_level{ D3D_FEATURE_LEVEL_11_0 };
|
||||||
|
|
||||||
@@ -124,6 +354,15 @@ initialize()
|
|||||||
// 为 Direct3D 12 设备设置名称
|
// 为 Direct3D 12 设备设置名称
|
||||||
NAME_D3D12_OBJECT(main_device, L"Main Device");
|
NAME_D3D12_OBJECT(main_device, L"Main Device");
|
||||||
|
|
||||||
|
// 使用 placement new 在已分配的内存上构造对象
|
||||||
|
// new (&gfx_command) 表示在 gfx_command 的地址处调用构造函数
|
||||||
|
// 这种用法允许我们在不分配新内存的情况下,在指定内存位置构造对象
|
||||||
|
// 常用于需要在特定内存地址构造对象,或重新初始化已存在的对象
|
||||||
|
// 这里 gfx_command 是一个类成员变量,我们直接在其内存位置上构造 d3d12_command 对象
|
||||||
|
// 避免了额外的内存分配,同时可以传递构造参数(main_device 和命令队列类型)
|
||||||
|
new (&gfx_command) d3d12_command(main_device, D3D12_COMMAND_LIST_TYPE_DIRECT);
|
||||||
|
if(!gfx_command.command_queue()) return failed_init();
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
{
|
{
|
||||||
ComPtr<ID3D12InfoQueue> info_queue;
|
ComPtr<ID3D12InfoQueue> info_queue;
|
||||||
@@ -137,15 +376,17 @@ initialize()
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
shutdown()
|
shutdown()
|
||||||
{
|
{
|
||||||
|
gfx_command.release();
|
||||||
release(dxgi_factory);
|
release(dxgi_factory);
|
||||||
|
|
||||||
// 关闭调试层,确保最后只有一个活动的主设备
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
|
// 关闭调试层,确保最后只有一个活动的主设备
|
||||||
ComPtr<ID3D12InfoQueue> info_queue;
|
ComPtr<ID3D12InfoQueue> info_queue;
|
||||||
DXCall(main_device->QueryInterface(IID_PPV_ARGS(&info_queue)));
|
DXCall(main_device->QueryInterface(IID_PPV_ARGS(&info_queue)));
|
||||||
|
|
||||||
@@ -166,4 +407,18 @@ shutdown()
|
|||||||
|
|
||||||
release(main_device);
|
release(main_device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
// 等待GPU完成命令列表,并重置命令分配器和命令列表
|
||||||
|
gfx_command.begin_frame();
|
||||||
|
ID3D12GraphicsCommandList6* cmd_list{ gfx_command.command_list() };
|
||||||
|
|
||||||
|
// 记录命令
|
||||||
|
//
|
||||||
|
// 完成命令记录,立即提交命令列表到命令队列执行
|
||||||
|
// 为下一帧标记并增加围栏值
|
||||||
|
gfx_command.end_frame();
|
||||||
|
}
|
||||||
}// namespace XEngine::graphics::d3d12::core
|
}// namespace XEngine::graphics::d3d12::core
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ bool initialize();
|
|||||||
* @details 调用 Direct3D 12 设备的关闭函数,释放所有资源
|
* @details 调用 Direct3D 12 设备的关闭函数,释放所有资源
|
||||||
*/
|
*/
|
||||||
void shutdown();
|
void shutdown();
|
||||||
|
/**
|
||||||
|
* @brief 渲染 Direct3D 12 核心功能
|
||||||
|
* @details 调用 Direct3D 12 设备的渲染函数,渲染当前渲染表面
|
||||||
|
*/
|
||||||
|
void render();
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
constexpr void release(T*& resource)
|
constexpr void release(T*& resource)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ void get_platform_interface(platform_interface& pi)
|
|||||||
{
|
{
|
||||||
pi.initialize = core::initialize;
|
pi.initialize = core::initialize;
|
||||||
pi.shutdown = core::shutdown;
|
pi.shutdown = core::shutdown;
|
||||||
|
pi.render = core::render;
|
||||||
}
|
}
|
||||||
}// namespace XEngine::graphics::d3d12
|
}// namespace XEngine::graphics::d3d12
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ namespace XEngine::graphics{
|
|||||||
struct platform_interface{
|
struct platform_interface{
|
||||||
bool(*initialize)(void);
|
bool(*initialize)(void);
|
||||||
void(*shutdown)(void);
|
void(*shutdown)(void);
|
||||||
|
void(*render)(void);
|
||||||
};
|
};
|
||||||
}// namespace XEngine::graphics
|
}// namespace XEngine::graphics
|
||||||
|
|
||||||
|
|||||||
@@ -39,13 +39,23 @@ set_platform_interface(graphics_platform platform)
|
|||||||
* @param platform 图形渲染平台类型
|
* @param platform 图形渲染平台类型
|
||||||
* @return true 如果初始化成功,否则返回 false
|
* @return true 如果初始化成功,否则返回 false
|
||||||
*/
|
*/
|
||||||
bool initialize(graphics_platform platform)
|
bool
|
||||||
|
initialize(graphics_platform platform)
|
||||||
{
|
{
|
||||||
return set_platform_interface(platform) && gfx.initialize();
|
return set_platform_interface(platform) && gfx.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
void shutdown()
|
void
|
||||||
|
shutdown()
|
||||||
{
|
{
|
||||||
gfx.shutdown();
|
gfx.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
gfx.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}// namespace XEngine::graphics
|
}// namespace XEngine::graphics
|
||||||
|
|||||||
@@ -47,4 +47,10 @@ bool initialize(graphics_platform platform);
|
|||||||
*/
|
*/
|
||||||
void shutdown();
|
void shutdown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 渲染调用接口
|
||||||
|
* @details 调用渲染函数指针,渲染当前渲染表面
|
||||||
|
*/
|
||||||
|
void render();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
|
|
||||||
#if TEST_RENDERER
|
#ifdef TEST_RENDERER
|
||||||
using namespace XEngine;
|
using namespace XEngine;
|
||||||
|
|
||||||
graphics::render_surface _surfaces[4];
|
graphics::render_surface _surfaces[4];
|
||||||
@@ -94,6 +94,7 @@ void
|
|||||||
engine_test::run()
|
engine_test::run()
|
||||||
{
|
{
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
graphics::render();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
@@ -108,4 +109,4 @@ engine_test::shutdown()
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif // TEST_RENDERER
|
||||||
|
|||||||
135
docs/changelogs/2026-03/20260326-d3d12-command-queue.md
Normal file
135
docs/changelogs/2026-03/20260326-d3d12-command-queue.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# 变更记录:命令队列与多帧缓冲
|
||||||
|
|
||||||
|
**提交日期**: 2026-03-26
|
||||||
|
**提交哈希**: `26e18bd`
|
||||||
|
**变更类型**: 功能实现
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变更概述
|
||||||
|
|
||||||
|
本次提交实现了 D3D12 命令队列管理类 `d3d12_command`,支持多帧缓冲渲染架构,实现了 CPU-GPU 并行渲染的基础设施。
|
||||||
|
|
||||||
|
## 修改文件
|
||||||
|
|
||||||
|
### Engine/Graphics/Direct3D12/
|
||||||
|
|
||||||
|
| 文件 | 变更说明 |
|
||||||
|
|------|----------|
|
||||||
|
| `D3D12Core.cpp` | 添加 `d3d12_command` 类,实现命令队列和命令列表管理 |
|
||||||
|
| `D3D12Core.h` | 添加 `render()` 函数声明 |
|
||||||
|
| `D3D12CommonHeader.h` | 添加 `frame_buffer_count` 常量和 `NAME_D3D12_OBJECT_INDEXED` 宏 |
|
||||||
|
|
||||||
|
### Engine/Graphics/
|
||||||
|
|
||||||
|
| 文件 | 变更说明 |
|
||||||
|
|------|----------|
|
||||||
|
| `GraphicsPlatformInterface.h` | 添加 `render` 函数指针 |
|
||||||
|
| `Renderer.h` | 添加 `render()` 函数声明 |
|
||||||
|
| `Renderer.cpp` | 实现 `render()` 函数 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术要点
|
||||||
|
|
||||||
|
### 1. 多帧缓冲架构
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
constexpr u32 frame_buffer_count{ 3 };
|
||||||
|
```
|
||||||
|
|
||||||
|
采用三重缓冲设计,允许 CPU 提前录制命令,GPU 异步执行,最大化硬件利用率。
|
||||||
|
|
||||||
|
### 2. d3d12_command 类
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class d3d12_command
|
||||||
|
{
|
||||||
|
// 创建命令队列(支持 Direct/Compute/Copy 三种类型)
|
||||||
|
// 为每帧创建独立的命令分配器
|
||||||
|
// 创建命令列表
|
||||||
|
|
||||||
|
void begin_frame(); // 等待帧完成,重置分配器和命令列表
|
||||||
|
void end_frame(); // 关闭命令列表,提交执行
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 帧索引轮转机制
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
_frame_index = (_frame_index + 1) % frame_buffer_count;
|
||||||
|
```
|
||||||
|
|
||||||
|
环形缓冲区管理帧资源,确保 CPU 不会超前 GPU 超过 3 帧。
|
||||||
|
|
||||||
|
### 4. command_frame 结构
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct command_frame
|
||||||
|
{
|
||||||
|
ID3D12CommandAllocator* cmd_allocator{ nullptr };
|
||||||
|
void wait(); // 等待 GPU 完成该帧
|
||||||
|
void release(); // 释放资源
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
每帧独立的命令分配器,避免 GPU 执行期间重置冲突。
|
||||||
|
|
||||||
|
### 5. 命名调试宏
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#define NAME_D3D12_OBJECT_INDEXED(obj, n, name) \
|
||||||
|
obj->SetName(full_name); ...
|
||||||
|
```
|
||||||
|
|
||||||
|
支持为多个同类对象设置带索引的调试名称。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 渲染流程
|
||||||
|
|
||||||
|
```
|
||||||
|
render()
|
||||||
|
│
|
||||||
|
├─► begin_frame()
|
||||||
|
│ ├─► 等待当前帧 GPU 完成
|
||||||
|
│ ├─► 重置命令分配器
|
||||||
|
│ └─► 重置命令列表
|
||||||
|
│
|
||||||
|
└─► end_frame()
|
||||||
|
├─► 关闭命令列表
|
||||||
|
├─► 提交命令列表执行
|
||||||
|
└─► 递增帧索引
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 设计原理
|
||||||
|
|
||||||
|
### 为什么需要多帧缓冲?
|
||||||
|
|
||||||
|
1. **CPU-GPU 并行性**: 单缓冲模式下 CPU 必须等待 GPU 完成,多帧缓冲允许 CPU 提前录制 N 帧
|
||||||
|
2. **命令分配器冲突解决**: D3D12 中命令分配器在 GPU 执行期间不能被重置,每帧独立分配器解决此问题
|
||||||
|
3. **帧时序稳定性**: 缓冲 N 帧可平滑帧率波动
|
||||||
|
|
||||||
|
### 命令列表创建后立即关闭
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
DXCall(_cmd_list->Close());
|
||||||
|
```
|
||||||
|
|
||||||
|
新创建的命令列表处于"录制打开"状态,立即关闭使其进入可提交状态,这是一种防御性编程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 后续工作
|
||||||
|
|
||||||
|
- [ ] 实现 Fence 同步机制
|
||||||
|
- [ ] 实现交换链
|
||||||
|
- [ ] 实现描述符堆
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [D3D12学习Wiki](../wiki/D3D12学习Wiki.md)
|
||||||
154
docs/changelogs/2026-03/20260327-d3d12-fence-sync.md
Normal file
154
docs/changelogs/2026-03/20260327-d3d12-fence-sync.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# 变更记录:Fence 同步机制实现
|
||||||
|
|
||||||
|
**提交日期**: 2026-03-27
|
||||||
|
**提交哈希**: `b00a906`
|
||||||
|
**变更类型**: 功能实现
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变更概述
|
||||||
|
|
||||||
|
本次提交实现了 D3D12 Fence(围栏)同步机制,完成 CPU-GPU 帧同步,确保命令列表执行顺序正确,避免资源冲突。
|
||||||
|
|
||||||
|
## 修改文件
|
||||||
|
|
||||||
|
### Engine/Graphics/Direct3D12/
|
||||||
|
|
||||||
|
| 文件 | 变更说明 |
|
||||||
|
|------|----------|
|
||||||
|
| `D3D12Core.cpp` | 添加 Fence 同步机制,实现 `wait()`、`flush()` 方法 |
|
||||||
|
|
||||||
|
### EngineTest/
|
||||||
|
|
||||||
|
| 文件 | 变更说明 |
|
||||||
|
|------|----------|
|
||||||
|
| `TestRenderer.cpp` | 在 `run()` 中调用 `graphics::render()` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术要点
|
||||||
|
|
||||||
|
### 1. Fence 对象
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
ID3D12Fence1* _fence{ nullptr };
|
||||||
|
u64 _fence_value{ 0 };
|
||||||
|
HANDLE _fence_event{ nullptr };
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Fence**: GPU 可设置的计数器,用于同步
|
||||||
|
- **Fence Value**: 64位无符号整数,每帧递增
|
||||||
|
- **Fence Event**: Windows 事件对象,用于 CPU 等待
|
||||||
|
|
||||||
|
### 2. command_frame 结构
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct command_frame
|
||||||
|
{
|
||||||
|
ID3D12CommandAllocator* cmd_allocator{ nullptr };
|
||||||
|
u64 fence_value{ 0 }; // 该帧的围栏值
|
||||||
|
|
||||||
|
void wait(HANDLE fence_event, ID3D12Fence1* fence);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
每帧记录其围栏值,用于判断 GPU 是否完成该帧。
|
||||||
|
|
||||||
|
### 3. 帧同步等待
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void wait(HANDLE fence_event, ID3D12Fence1* fence)
|
||||||
|
{
|
||||||
|
if(fence->GetCompletedValue() < fence_value)
|
||||||
|
{
|
||||||
|
fence->SetEventOnCompletion(fence_value, fence_event);
|
||||||
|
WaitForSingleObject(fence_event, INFINITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 检查 GPU 是否完成到目标围栏值
|
||||||
|
- 未完成则设置事件并等待
|
||||||
|
|
||||||
|
### 4. 帧结束信号
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void end_frame()
|
||||||
|
{
|
||||||
|
// ... 提交命令列表 ...
|
||||||
|
++_fence_value;
|
||||||
|
_cmd_frames[_frame_index].fence_value = _fence_value;
|
||||||
|
_cmd_queue->Signal(_fence, _fence_value);
|
||||||
|
_frame_index = (_frame_index + 1) % frame_buffer_count;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 递增围栏值
|
||||||
|
- 记录当前帧的围栏值
|
||||||
|
- 向 GPU 发送信号
|
||||||
|
|
||||||
|
### 5. flush 方法
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void flush()
|
||||||
|
{
|
||||||
|
for(u32 i{ 0 }; i < frame_buffer_count; ++i)
|
||||||
|
{
|
||||||
|
_cmd_frames[i].wait(_fence_event, _fence);
|
||||||
|
}
|
||||||
|
_frame_index = 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
等待所有帧完成,用于资源释放前确保 GPU 完成。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 同步流程
|
||||||
|
|
||||||
|
```
|
||||||
|
begin_frame()
|
||||||
|
│
|
||||||
|
├─► 检查当前帧的 fence_value
|
||||||
|
│
|
||||||
|
└─► 如果 GPU 未完成,CPU 等待
|
||||||
|
│
|
||||||
|
└─► 重置分配器和命令列表
|
||||||
|
|
||||||
|
end_frame()
|
||||||
|
│
|
||||||
|
├─► 提交命令列表
|
||||||
|
│
|
||||||
|
├─► ++fence_value
|
||||||
|
│
|
||||||
|
├─► 记录当前帧的 fence_value
|
||||||
|
│
|
||||||
|
├─► Signal(fence, fence_value)
|
||||||
|
│
|
||||||
|
└─► 递增帧索引
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 围栏值溢出问题
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 64位无符号整数,即便每秒1000帧,也需要5.8亿年才能回绕
|
||||||
|
u64 _fence_value{ 0 };
|
||||||
|
```
|
||||||
|
|
||||||
|
无需担心溢出问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 后续工作
|
||||||
|
|
||||||
|
- [ ] 交换链实现
|
||||||
|
- [ ] 描述符堆
|
||||||
|
- [ ] 渲染目标视图
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [D3D12学习Wiki](../wiki/D3D12学习Wiki.md)
|
||||||
@@ -16,6 +16,8 @@ changelogs/
|
|||||||
|
|
||||||
| 日期 | 提交 | 变更内容 |
|
| 日期 | 提交 | 变更内容 |
|
||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
|
| 2026-03-27 | [Fence同步机制](./2026-03/20260327-d3d12-fence-sync.md) | D3D12 Fence CPU-GPU 帧同步实现 |
|
||||||
|
| 2026-03-26 | [命令队列与多帧缓冲](./2026-03/20260326-d3d12-command-queue.md) | D3D12 命令队列和多帧渲染架构 |
|
||||||
| 2026-03-26 | [D3D12设备初始化](./2026-03/20260326-d3d12-device-init.md) | D3D12 设备创建与调试层实现 |
|
| 2026-03-26 | [D3D12设备初始化](./2026-03/20260326-d3d12-device-init.md) | D3D12 设备创建与调试层实现 |
|
||||||
| 2026-03-26 | [Graphics模块](./2026-03/20260326-d3d12-foundation.md) | Graphics 模块与 D3D12 后端框架 |
|
| 2026-03-26 | [Graphics模块](./2026-03/20260326-d3d12-foundation.md) | Graphics 模块与 D3D12 后端框架 |
|
||||||
| 2026-03-19 | [DX12初始框架](./2026-03/20260326-dx12-initial.md) | 初始 DX12 基础框架 |
|
| 2026-03-19 | [DX12初始框架](./2026-03/20260326-dx12-initial.md) | 初始 DX12 基础框架 |
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ Microsoft::WRL::ComPtr<ID3D12Device8> device;
|
|||||||
void get_platform_interface(platform_interface& pi) {
|
void get_platform_interface(platform_interface& pi) {
|
||||||
pi.initialize = core::initialize;
|
pi.initialize = core::initialize;
|
||||||
pi.shutdown = core::shutdown;
|
pi.shutdown = core::shutdown;
|
||||||
|
pi.render = core::render;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -172,9 +173,74 @@ void get_platform_interface(platform_interface& pi) {
|
|||||||
- 各后端独立开发和测试
|
- 各后端独立开发和测试
|
||||||
- 上层代码与具体 API 解耦
|
- 上层代码与具体 API 解耦
|
||||||
|
|
||||||
## 6. 渲染表面与窗口
|
## 6. 命令队列与多帧缓冲
|
||||||
|
|
||||||
### 6.1 render_surface 结构
|
### 6.1 d3d12_command 类
|
||||||
|
|
||||||
|
项目实现了命令队列管理类,支持多帧缓冲渲染:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class d3d12_command
|
||||||
|
{
|
||||||
|
void begin_frame(); // 等待帧完成,重置分配器和命令列表
|
||||||
|
void end_frame(); // 关闭命令列表,提交执行
|
||||||
|
private:
|
||||||
|
ID3D12CommandQueue* _cmd_queue;
|
||||||
|
ID3D12GraphicsCommandList6* _cmd_list;
|
||||||
|
command_frame _cmd_frames[frame_buffer_count];
|
||||||
|
u32 _frame_index;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 多帧缓冲原理
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
constexpr u32 frame_buffer_count{ 3 };
|
||||||
|
```
|
||||||
|
|
||||||
|
采用三重缓冲设计:
|
||||||
|
- CPU 提前录制命令
|
||||||
|
- GPU 异步执行
|
||||||
|
- 最大化硬件利用率
|
||||||
|
|
||||||
|
### 6.3 帧索引轮转
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
_frame_index = (_frame_index + 1) % frame_buffer_count;
|
||||||
|
```
|
||||||
|
|
||||||
|
环形缓冲区管理帧资源,确保 CPU 不会超前 GPU 超过 3 帧。
|
||||||
|
|
||||||
|
### 6.4 Fence 同步机制
|
||||||
|
|
||||||
|
项目实现了 Fence(围栏)同步,确保 CPU-GPU 帧同步:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct command_frame
|
||||||
|
{
|
||||||
|
ID3D12CommandAllocator* cmd_allocator{ nullptr };
|
||||||
|
u64 fence_value{ 0 }; // 该帧的围栏值
|
||||||
|
|
||||||
|
void wait(HANDLE fence_event, ID3D12Fence1* fence);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**同步流程**:
|
||||||
|
1. `begin_frame()` - 检查 GPU 是否完成当前帧,未完成则等待
|
||||||
|
2. `end_frame()` - 递增围栏值,向 GPU 发送信号
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 帧结束信号
|
||||||
|
++_fence_value;
|
||||||
|
_cmd_frames[_frame_index].fence_value = _fence_value;
|
||||||
|
_cmd_queue->Signal(_fence, _fence_value);
|
||||||
|
```
|
||||||
|
|
||||||
|
**围栏值溢出**:64位无符号整数,每秒1000帧需要5.8亿年才回绕,无需担心。
|
||||||
|
|
||||||
|
## 7. 渲染表面与窗口
|
||||||
|
|
||||||
|
### 7.1 render_surface 结构
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
struct render_surface {
|
struct render_surface {
|
||||||
@@ -183,7 +249,7 @@ struct render_surface {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.2 多窗口支持
|
### 7.2 多窗口支持
|
||||||
|
|
||||||
TestRenderer 测试展示了多窗口渲染:
|
TestRenderer 测试展示了多窗口渲染:
|
||||||
|
|
||||||
@@ -196,7 +262,7 @@ for (u32 i{0}; i < _countof(_surfaces); ++i) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.3 全屏切换
|
### 7.3 全屏切换
|
||||||
|
|
||||||
通过 `WM_SYSCHAR` 消息处理 Alt+Enter:
|
通过 `WM_SYSCHAR` 消息处理 Alt+Enter:
|
||||||
|
|
||||||
@@ -206,42 +272,42 @@ if (wparam == VK_RETURN && (HIWORD(lparam) & KF_ALTDOWN)) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 7. 后续学习路径
|
## 8. 后续学习路径
|
||||||
|
|
||||||
### 7.1 基础阶段
|
### 8.1 基础阶段
|
||||||
|
|
||||||
- [x] 完成设备创建和适配器枚举
|
- [x] 完成设备创建和适配器枚举
|
||||||
- [ ] 创建命令队列和命令列表
|
- [x] 创建命令队列和命令列表
|
||||||
- [ ] 实现交换链和后台缓冲区
|
- [ ] 实现交换链和后台缓冲区
|
||||||
- [ ] 渲染第一个三角形
|
- [ ] 渲染第一个三角形
|
||||||
|
|
||||||
### 7.2 进阶阶段
|
### 8.2 进阶阶段
|
||||||
|
|
||||||
- [ ] 描述符堆管理
|
- [ ] 描述符堆管理
|
||||||
- [ ] 根签名和管线状态对象
|
- [ ] 根签名和管线状态对象
|
||||||
- [ ] 资源屏障和同步
|
- [ ] 资源屏障和同步
|
||||||
- [ ] 常量缓冲区和着色器资源
|
- [ ] 常量缓冲区和着色器资源
|
||||||
|
|
||||||
### 7.3 高级阶段
|
### 8.3 高级阶段
|
||||||
|
|
||||||
- [ ] 多线程渲染
|
- [ ] 多线程渲染
|
||||||
- [ ] 资源绑定策略
|
- [ ] 资源绑定策略
|
||||||
- [ ] 动态资源管理
|
- [ ] 动态资源管理
|
||||||
- [ ] 性能优化
|
- [ ] 性能优化
|
||||||
|
|
||||||
## 8. 参考资源
|
## 9. 参考资源
|
||||||
|
|
||||||
### 8.1 官方文档
|
### 9.1 官方文档
|
||||||
|
|
||||||
- [Microsoft D3D12 文档](https://docs.microsoft.com/en-us/windows/win32/direct3d12/direct3d-12-graphics)
|
- [Microsoft D3D12 文档](https://docs.microsoft.com/en-us/windows/win32/direct3d12/direct3d-12-graphics)
|
||||||
- [DXGI 文档](https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/dx-graphics-dxgi)
|
- [DXGI 文档](https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/dx-graphics-dxgi)
|
||||||
|
|
||||||
### 8.2 推荐书籍
|
### 9.2 推荐书籍
|
||||||
|
|
||||||
- 《Introduction to 3D Game Programming with DirectX 12》
|
- 《Introduction to 3D Game Programming with DirectX 12》
|
||||||
- 《Real-Time 3D Rendering with DirectX and HLSL》
|
- 《Real-Time 3D Rendering with DirectX and HLSL》
|
||||||
|
|
||||||
### 8.3 项目相关文档
|
### 9.3 项目相关文档
|
||||||
|
|
||||||
- [Graphics渲染架构分析](./Graphics渲染架构分析.md)
|
- [Graphics渲染架构分析](./Graphics渲染架构分析.md)
|
||||||
- [项目约定规范](./项目约定规范.md)
|
- [项目约定规范](./项目约定规范.md)
|
||||||
|
|||||||
Reference in New Issue
Block a user