DirectX 12顶点缓冲区输出流无法工作,返回全零数据求助
DirectX 12流输出缓冲区读取异常:计数器为0且数据全零,但PIX显示缓冲区有正确数据
问题描述
实现DirectX 12顶点缓冲区输出流以获取世界空间变换后的顶点位置时,程序无编译/运行报错,但读取到的计数器值为0,流输出数据全为零。已尝试搜索资料、调试工具排查,用dxcpl.exe开启调试信息,仍未解决。
代码基于Direct3D 12基础教程开发,支持VS2022编译。
关键代码片段
顶点着色器代码
struct VertexOutput // 同时作为像素着色器输入 { float3 position : POSITION; // 流输出占用3*4字节 float3 worldPos: WORLDPOS; // 流输出世界位置,占用3*4字节 float4 sv_position : SV_POSITION; // 流输出位置,占用4*4字节,单顶点总大小40字节 }; struct VertexInput { float3 position : POSITION; float3 worldPos: WORLDPOS; float4 sv_position : SV_POSITION; }; VertexOutput main(VertexInput input) { VertexOutput vo; vo.worldPos = float3(1.1f, 1.2f, 1.3f); // 先写死值测试,后续替换为世界变换计算 vo.position = input.position; vo.sv_position = float4(input.position, 1.0f); return vo; }
主函数关键代码
D3D12_STREAM_OUTPUT_BUFFER_VIEW streamOutputBufferView = {}; streamOutputBufferView.BufferFilledSizeLocation = streamOutputBuffer->GetGPUVirtualAddress(); streamOutputBufferView.BufferLocation = streamOutputBufferView.BufferFilledSizeLocation + sizeof(UINT64); streamOutputBufferView.SizeInBytes = bufferSize;/* 缓冲区大小 */; // 绑定输出缓冲区到流输出阶段 commandList->SOSetTargets(0/* 起始槽位 */, 1/* 视图数量 */, &streamOutputBufferView); commandList->DrawInstanced(3, 1, 0, 0); // 绘制3个顶点的三角形 // 将源缓冲区状态转换为D3D12_RESOURCE_STATE_COPY_SOURCE CD3DX12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition( streamOutputBuffer, // 目标资源 D3D12_RESOURCE_STATE_STREAM_OUT, // 原状态 D3D12_RESOURCE_STATE_COPY_SOURCE // 目标状态 ); // 插入资源屏障到命令列表 commandList->ResourceBarrier(1, &barrier); // 定义堆属性 D3D12_HEAP_PROPERTIES heapProperties = {}; heapProperties.Type = D3D12_HEAP_TYPE_UPLOAD; // 使用CPU可访问的UPLOAD堆 heapProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; heapProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; // 定义缓冲区描述 CD3DX12_RESOURCE_DESC bufferDesc = CD3DX12_RESOURCE_DESC::Buffer(totalBufferSize); // 在CPU可访问堆中创建目标缓冲区 ID3D12Resource* destinationBuffer; device->CreateCommittedResource( &heapProperties, // 堆属性 D3D12_HEAP_FLAG_NONE, // 堆标志 &bufferDesc, // 资源描述 D3D12_RESOURCE_STATE_GENERIC_READ, // 初始资源状态 nullptr, // 清除值(缓冲区未使用) IID_PPV_ARGS(&destinationBuffer) // 目标指针 ); // 计算计数器大小(假设为UINT64) const UINT counterSize = sizeof(UINT64); // 计算要复制的总数据大小 const UINT totalDataSize = totalBufferSize; // 执行缓冲区复制 commandList->CopyBufferRegion( destinationBuffer, // 目标缓冲区 0, // 目标偏移 streamOutputBuffer, // 源缓冲区 0, // 源偏移 totalDataSize // 复制大小 ); // 将目标缓冲区映射到CPU可访问内存 UINT8* mappedData = nullptr; CD3DX12_RANGE readRange(0, 0); // 读取整个缓冲区 hr = destinationBuffer->Map(0, &readRange, reinterpret_cast<void**>(&mappedData)); if (FAILED(hr)) { OutputDebugString(L"MAP FAILED"); } // 从映射内存读取数据 // 假设前sizeof(UINT64)字节是计数器,其余是流输出数据 UINT64 counterValue = *reinterpret_cast<UINT64*>(mappedData); BYTE* streamOutputData = mappedData + sizeof(UINT64); // 此处使用counterValue和streamOutputData // 取消映射缓冲区 destinationBuffer->Unmap(0, nullptr); commandList->SOSetTargets(0/* 起始槽位 */, 0/* 视图数量 */, nullptr);
补充信息
PIX调试截图显示着色器输出缓冲区中存在正确的世界变换位置,但程序读取不到。
排查与解决方案
1. 命令列表未提交执行
当前代码在CopyBufferRegion后直接映射读取数据,但GPU命令是异步执行的,此时复制操作尚未完成,读取到的是缓冲区初始零值。
修复方式:
在复制操作后提交命令列表到GPU队列,等待执行完成后再读取数据:
// 关闭命令列表 commandList->Close(); // 提交到命令队列 commandQueue->ExecuteCommandLists(1, reinterpret_cast<ID3D12CommandList**>(&commandList)); // 等待GPU执行完成 commandQueue->Signal(fence, fenceValue); if (fence->GetCompletedValue() < fenceValue) { fence->SetEventOnCompletion(fenceValue, fenceEvent); WaitForSingleObject(fenceEvent, INFINITE); } fenceValue++;
注:需提前初始化ID3D12Fence和事件对象。
2. 流输出声明缺失
顶点着色器定义了输出语义,但创建管道状态对象(PSO)时未明确指定流输出声明,GPU不知道哪些语义需要输出到缓冲区。
修复方式:
在创建PSO时添加流输出描述:
D3D12_STREAM_OUTPUT_DESC streamOutputDesc = {}; // 定义流输出元素 D3D12_SO_DECLARATION_ELEMENT soElements[] = { { 0, "POSITION", 0, 0, 3, D3D12_SO_COMPONENT_FLOAT, 0 }, { 0, "WORLDPOS", 0, 3, 3, D3D12_SO_COMPONENT_FLOAT, 0 }, { 0, "SV_POSITION", 0, 6, 4, D3D12_SO_COMPONENT_FLOAT, 0 } }; streamOutputDesc.NumEntries = _countof(soElements); streamOutputDesc.pSODeclaration = soElements; streamOutputDesc.NumStrides = 1; UINT strides[] = { 40 }; // 单顶点总字节数 streamOutputDesc.pBufferStrides = strides; streamOutputDesc.RasterizedStream = 0; // 在PSO描述中绑定流输出 D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {}; // ... 其他PSO设置(顶点着色器、输入布局等) psoDesc.StreamOutput = streamOutputDesc;
3. 缓冲区状态与大小问题
- 确认
streamOutputBuffer创建时的初始状态为D3D12_RESOURCE_STATE_STREAM_OUT,若状态不符,需提前添加转换屏障再绑定SO目标。 - 检查
bufferSize和totalBufferSize计算是否正确:3个顶点需要至少3*40 + sizeof(UINT64)字节的缓冲区,避免溢出导致计数器异常。
4. 映射读取范围错误
使用CD3DX12_RANGE readRange(0, 0)表示不读取任何数据,虽DirectX允许该操作,但明确设置读取范围为整个缓冲区,能确保数据正确同步到CPU:
CD3DX12_RANGE readRange(0, totalBufferSize);
内容的提问来源于stack exchange,提问作者Twinsen
相关产品推荐
相关产品推荐

