DependencyGraph 基于依赖图的资源检查与可视化修改工具
Visualization and Modification Tool Based on Resource Dependency Graph
详见ppt,这里做内容转述
C++/C#/Python-混合编程注意项
主要是boost.python,其他的都还好,就这个写起来有点麻烦。不过它也比其他的方式更强大,是C++而不是C
原型阶段用C#调用C++的时候,发现只能调用C,而且需要在C#这边声明好,用buffer机制传参,类似这样:
class AssetDependencyWrapper{[DllImport("AssetDependency", EntryPoint = "CSharpTest")]public static extern void CSharpTest();[DllImport("AssetDependency", EntryPoint = "Start")]public static extern void Start(string inputPath);[DllImport("AssetDependency", EntryPoint = "FindDepsMap", CharSet = CharSet.Unicode)]static extern bool FindDepsMapInner(string guidKey, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)]byte[] guids, int guidCsvSize);public static bool FindDepedMap(string guidKey, out string guids);// c++的callback参数声明(要用IntPtr接受)[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)]delegate void LoopVPCallbackInner(IntPtr parentKey, IntPtr nodeToAdd, IntPtr guid);// C#侧的callback参数(用string接受)public delegate void LoopVPCallback(string parentKey, string nodeToAdd, string guid);// 调用C++,参数是callback[DllImport("AssetDependency", EntryPoint = "GetAllVirualPath", CharSet = CharSet.Unicode)]static extern bool GetAllVirualPathInner(LoopVPCallbackInner callback);// 给C#调用的接口(要传递LoopVPCallback)public static bool GetAllVirualPath(LoopVPCallback callback){byte[] buffer = new byte[4096];Array.Clear(buffer, 0, buffer.Length);// 委托的实现,调用C#函数(用callback是为了降低耦合)LoopVPCallbackInner dl = delegate (IntPtr parentKey, IntPtr nodeToAdd, IntPtr guid){string a = Marshal.PtrToStringUni(parentKey);string b = Marshal.PtrToStringUni(nodeToAdd);string c = Marshal.PtrToStringUni(guid);callback(a, b, c);};return GetAllVirualPathInner(dl); // 调用C++并传递callback}}
这边将callback传给C++,等那边执行完时通过callback通知回来
另外,不同语言之间兼容性会有一些差别。比如类型之间的转换 对应关系有区别
Python 调用 C++ (Boost.Python)
其中的python库可以用来封装c++被python调用, 功能比较强大, 不但可以封装函数还能封装类, 类成员
安装boost.python, apt-get install libboost-python-dev
需要单独写一个c/cpp文件,假设命名为 pymodule.c 好了。这个文件就是我们要写的"接口",要明确告诉boost 需要wrap哪些接口。注意一般的函数是不需要在这里声明参数的。声明参数的只有constructor,而且constructor支持多重定义的
Boost::Python是Python/C API的一个封装(wrapper)。使用Python/C API,你需要在Python和C代码之间传递指针的前后,手动处理指针问题,比如指针不再指向原来的对象时,Boost::Python接管了这个任务,自动处理
部分源码:AssetDependency.h
#pragma once#include <pugixml.hpp>#include <experimental/filesystem>#ifdef DLL_EXPORT#define DLL_API extern "C" __declspec( dllexport )#else#define DLL_API extern "C" __declspec( dllimport )#endifDLL_API void __stdcall Launch(std::wstring inputPath);DLL_API std::wstring __stdcall GetNextVirtualPath();DLL_API std::wstring __stdcall GetDeps(std::wstring guidKey);DLL_API std::wstring __stdcall GetDeped(std::wstring guidKey);DLL_API const wchar_t* __stdcall GetBrokenReposity();DLL_API const wchar_t* __stdcall GetRepeatedGuid();DLL_API const wchar_t* __stdcall GetGuidNotFind();DLL_API const wchar_t* __stdcall GetDepsLoss();DLL_API const wchar_t* __stdcall GetErrorDependency();DLL_API int __stdcall ModifyDepsGuid(std::wstring itemGuid, std::wstring srcGuid, std::wstring destGuid);struct Item{public:std::wstring Type;std::wstring GUID;std::wstring Package;std::wstring Class;std::unordered_set<std::wstring> Deps;std::wstring Name;std::experimental::filesystem::path RepositoryPath;Item(){Type = L"";GUID = L"";Package = L"";Class = L"";Name = L"";}Item(std::wstring type, std::wstring guid, std::wstring package,std::unordered_set<std::wstring> deps, std::wstring name) : Type(type),GUID(guid), Package(package), Deps(deps), Name(name) { }Item(std::wstring type, std::wstring guid, std::wstring package, std::wstring cls, std::unordered_set<std::wstring> deps, std::wstring name, std::experimental::filesystem::path repositoryPath) : Type(type),GUID(guid), Package(package), Class(cls), Deps(deps), Name(name), RepositoryPath(repositoryPath) { }};
定义:

因为要回调C#所以要定义delegate的函数指针这里,DLL_API_FOR_Python是给Python调用的接口,主要逻辑就在里面。而后面的是For C#的、包含部分其他测试接口(xxLaunch、Test),python接口形如:
DLL_API void __stdcall Launch(std::wstring inputPath){fs::path packagePath(inputPath);loadPackgeDir(packagePath);}
AssetDependencyWrapper:
形如给AssetDependency.h的方法加个Wrap后缀包装
void LaunchWrap(std::wstring inputPath){Launch(inputPath);}
需要再封装一层提供给Python调用资源依赖
游戏中的Level,Model,Material,Texture,Mesh等资源之间会互相引用,产生依赖关系。这个关系可能是错综复杂的,不会像层级结构一样规范易用
需求:工具以Messiah引擎为例,检查Messiah工程的资源依赖关系,将其可视化展示出来,并进一步操作
物理目录与虚拟目录:Package文件夹下的Repository和Worlds文件夹,其下是物理目录,是打成了不同的包的。我们要生成使用者习惯的虚拟目录
存在问题:Messiah工程的资源类型非常多,相互之间的引用关系未必是严格遵守正确结构的,repository文件记录依赖关系是不可靠的,level文件的结构可能会变动,这些问题工具会检测和修复
依赖图
依赖图是表示几个对象相互依赖的有向图。如果依赖图不具有任何循环依赖性,则可以从依赖图中导出评估顺序(拓扑排序)
用途(Dependency Graphs In Game 等)

- 场景中对象之间的依赖关系。比如游戏中有些对象需要跟随另一个对象移动,比如玩家的宠物。比如相机在拍摄汽车时需要跟随汽车的移动
- 学校排课等等。比如学习某课程之前需要完成的前提课程,即依赖性课程
- Automated software installers。检测软件使用所需要的依赖包
- FrameGraph。结构化渲染流程
- 多线程。并行结点,进一步加快运行速度 LibEE
- 游戏设计。比如MC中创建木斧需要木板和木棍,这些可以作为斧头的依赖图。或者是某个事件在触发之前需要满足某些依赖条件(常见于非线性分支)
工具演示
基本界面

演示视频(略)
功能:
- 虚拟目录 File Browser
- 选中显示该资源的 Dependency Graph
- DG可以选中结点,移动结点,多选减选。缩放视图,移动视图。双击结点跳转
- 选中DG的结点后,上方显示Detail信息
- 可以全展开,多重展开。依赖了无效的资源会以Invalid显示
- 搜索Name、Path、GUID,Browser会显示查询结果。误操作会弹窗警告
- 界面整体特性:UI自适应,Splitter拖拽控件,快捷键
工具对比

- Unity:只提供了正向资源依赖查询,依赖项直接显示在Project视图中,没有依赖图
- UE4:提供引用查看器,显示依赖图,与引擎原生兼容
技术架构
C++ Core
- 负责工具底层,解析工程资源
- 维护GUID与资源信息的映射表
- 维护依赖关系表
C# 原型

- 与C++混合编程,展示工具原型
- 使用Winform开发并用GDI+绘图
- 作为后续添加功能的验证与Debug工具
Python
- 使用PyQt开发工具界面,为以后接入Sunshine做准备
- 与C++混合编程,在boost.python,ctypes,python extending c三种技术中选择了boost.python
- 功能扩展,界面美化,操作简化等等
其他功能
Package Checker 资源检查



- 仓库文件损坏: 任何一个xml无法正确读取都可以归类这个(repository,同样遍历的时候)
- 重复GUID: 任何一个资源不应该会和另外一个资源有重复的xml(冗余的GUID:遍历的时候就可以知道)
- 重复的虚拟路径: 任何一个资源不应该会和另外一个资源有重复的虚拟路径(生成treeview时检查同名)
- 资源缺失: xml里面依赖了某个资源, 但是却没有(guid find end)
- 依赖缺失: Deps里面依赖了某个资源, 却没有, 不过这个因为Deps和xml的依赖应该一致, 所以可以归类为同一个(guid find end)可以在迭代器中检查
- 材质依赖贴图不在Deps: 因为原来的xml依赖Deps这个东西来加速加载, xml里面依赖了, 也应该有Deps(set加入时如果没有exists就是了)
- 额外的: 错误的引用关系; 我们固定是LODModel 引用 Model 引用 Mesh 引用 Material 引用 Texture,只要引用关系里面的方式和这个不一致就是错误
依赖修改器

修正或修改原先的资源依赖关系(通过GUID或路径)修改效果

多重展开

- 限制层级的展开(如Level展开)
- 全展开
Level展开

全展开:超大依赖图

自定义皮肤

二进制缓冲加载
多线程I/O验证

效率对比
工程测试结果(保密)
未来展望
Sunshine集成
易用性
File Browser添加图标
功能性
优化
推销等等
引用
致谢
最后,附MultithreadedValidation的解析:
目的是为了验证MsLoad和DgLoad的差异,首先两者都定义Launch和FindDependency,C# call C++的Wrap
验证的目标是Pipeline,里面做一个Producer-Consumer的Model,其中Dg以顺序加载的形式Load,Messiah以cb的形式Load
最后结果是DgLoad稍优于MsLoad