SharpDX Direct3D 填坑:DataStream 和 Cubemap

填上两个坑:DataStream 使用不当的内存泄漏和在 SharpDX 中使用 cubemap

SharpDX 中,DataStream 提供了一个托管内存向非托管内存写入数据的快捷方式。它的一般创建方式是这样的:

static DataStream Create<T>(T[] data, bool readable, bool writable);

在 SlimDX 中,对应的是一个构造函数的重载版本。在将 CubeMap 示例改写运行起来之后,我就在场景里停留了两分钟,摄像机到处走。然后我发现渲染出现了卡顿的现象,从开始的零星卡顿到后来的帧率直降。我的第一个猜想就是垃圾太多,频繁触发垃圾回收,而任务管理器告诉我内存占用一直在疯涨。毫无疑问,出现了内存泄漏,从而加大了 GC 的压力。Visual Studio 2015 还有一个好工具,性能收集器,它告诉我内存占用是几乎线性增长的,而且有多个阶段都触发了 GC,后期 GC 频率很高。

内存泄漏对于托管代码来说几乎是不可能的,而且我在各个需要访问非托管资源的组件中都实现了 IDisposable 模式。问题出在哪里?

在乍看之下无法看出的情况下,我又祭出了老招数,分块调试。幸好,组件化的设计让我很容易独立调试各个组件。经过几次缩小范围后我发现,内存泄漏发生在 ShapesScene.DrawInternal() 方法中。这一段代码并不长,所以我仔细检查了一下,包括是否有可能的引用类型浪费(例如 BufferDescription,可是经查看,它是一个值类型)。这一段代码分别绘制了几个物体,再缩小范围后发现,在绘制第一个物体(地面网格)的时候就发生了泄漏。核心是这样的:

basicFx.SetWorld(world);
basicFx.SetWorldInvTranspose(worldInvTranspose);
basicFx.SetWorldViewProj(wvp);
basicFx.SetTexTransform(Matrix.Scaling(6, 8, 1));
basicFx.SetMaterial(_gridMat);
basicFx.SetDiffuseMap(_floorTexSRV);
pass.Apply(context);
context.DrawIndexed(_gridIndexCount, _gridIndexOffset, _gridVertexOffset);

数据中只有一个引用对象 _floorTexSRV,但是它自从初始化后内容就不变了。于是我只好翻开去看其他几个 Set* 方法。设置矩阵的那几个,由于矩阵本身是值类型而且不包含引用,因此也不会存在泄漏问题。然而,SetMaterial 是这样的:

public void SetMaterial(Material m) {
_mat.SetRawValue(DataStream.Create(NoireUtilities.StructureToBytes(m), false, false), Material.Stride);
}

由于我从 DataStream 暴露出的成员中推断它是主动分配了非托管内存的,所以立刻就觉察到了:它的 Dispose() 调用在哪?

还真想对了,这个函数可是每帧都调用几次的呢,虽然单次数据量小,但是帧率还是很高的,这样估算一下增速(3 MB/s 左右)是可以解释的。在加上了 Dispose() 之后就好了。我看了一下原始代码,也是没有释放的,运行时同样会出内存泄漏的问题。


SharpDX 的 ShaderResourceView 是不支持读取类型为 cubemap 的 DDS 纹理文件的。我的纹理读取是采用了 Stack Overflow 上的一个回答的方案。这个方案能正常读取普通纹理,如 PNG、JPEG 和类型为 Texture2D 的 DDS,但是在尝试读取 cubemap 贴图的时候失败了,报告说不支持头格式。(DXGI 会尝试使用 FourCC 来识别纹理格式。)我索性就用 texconv 将 DDS 转换成 PNG,直接加载,成了这个样子:

错误的天空盒

我觉得很奇怪,明明 shader 应该是没错的,而且我在代码里换了各种网格,为什么出来的都是立方体呢?而且,为什么只有前后有图,另外四个面全是 wrap 的结果呢?查阅资料后我知道了,原来 DDS 是支持保存 cubemap 而不仅是普通二维纹理的。

在原工程中,SlimDX 加载资源很简单,就一句:

CubeMapSRV = ShaderResourceView.FromFile(device, filename);

但是,SharpDX 的 ShaderResourceView 就根本没有这个静态方法,或者其他从文件加载的手段。原来,SlimDX 中的这个方法是通过 C++/CLI 直接调用了 D3DX11CreateShaderResourceViewFromFile() 这个底层辅助函数。SharpDX 中是有,在 sharpdx/Toolkit,说是需要更新。简单说,就是在 SharpDX 中加载 cubemap,需要自己写。当然,在 DDS 页面中,微软说了可以去查看 DirectXTex 或者 DirectXTK 工程代码。我去看了,在 DirectXTex 里,但是各类之间有着很强的依赖关系,短时间内不好拆,也不好翻译到 C# 上。

但是网上的资源启发了我,不就是6个面么(DxTex 也是可以看到的)?偶然间我找到了这个回答,提到能将6幅普通纹理合成一个 cubemap 纹理。而且,这段代码很容易移植到 SharpDX 为基础的架构中。

接下来的就是尝试了。我通过 NVidia 的 Photoshop 插件 将 cubemap DDS 切成了6个普通纹理(DxTex 做不了),然后载入调试。一切看上去正常,但是在最后创建 ShaderResourceView 的时候总是报告参数错误。这个问题困扰了我一个多小时,最后又是偶然,发现了另一个帖子。根据后者,前面的一个创建参数(在 OptionFlags 字段,对应 C++ 中的 MiscFlags 字段)是错的,此时就应该指定创建 texture cube。修改之后终于运行起来了。效果:

正确的天空盒

如果不想上 GitHub 去看的话,这里直接给出创建 texture cube 的代码:

public static ShaderResourceView CubeMapFrom6Textures(Device device, Texture2D[] texture2Ds) {
Debug.Assert(texture2Ds.Length == 6);
var texElemDesc = texture2Ds[0].Description;
var texArrayDesc = new Texture2DDescription() {
Width = texElemDesc.Width,
Height = texElemDesc.Height,
MipLevels = texElemDesc.MipLevels,
ArraySize = 6,
Format = texElemDesc.Format,
SampleDescription = new SampleDescription(1, 0),
Usage = ResourceUsage.Default,
BindFlags = BindFlags.ShaderResource,
CpuAccessFlags = CpuAccessFlags.None,
OptionFlags = ResourceOptionFlags.TextureCube
};
var texArray = new Texture2D(device, texArrayDesc);
var context = device.ImmediateContext;
var sourceRegion = new ResourceRegion();
for (var i = 0; i < 6; ++i) {
for (var mipLevel = 0; mipLevel < texArrayDesc.MipLevels; ++mipLevel) {
sourceRegion.Left = 0;
sourceRegion.Right = texArrayDesc.Width >> mipLevel;
sourceRegion.Top = 0;
sourceRegion.Bottom = texArrayDesc.Height >> mipLevel;
sourceRegion.Front = 0;
sourceRegion.Back = 1;
if (sourceRegion.Bottom <= 0 || sourceRegion.Right <= 0) {
break;
}
var n = Resource.CalculateSubResourceIndex(mipLevel, i, texArrayDesc.MipLevels);
context.CopySubresourceRegion(texture2Ds[i], mipLevel, sourceRegion, texArray, n);
}
}
var viewDesc = new ShaderResourceViewDescription() {
Format = texArrayDesc.Format,
Dimension = ShaderResourceViewDimension.TextureCube,
TextureCube = new ShaderResourceViewDescription.TextureCubeResource() {
MostDetailedMip = 0,
MipLevels = texArrayDesc.MipLevels
}
};
return new ShaderResourceView(device, texArray, viewDesc);
}
分享到 评论