You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 17:43:17