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 )
#endif

DLL_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中创建木斧需要木板和木棍,这些可以作为斧头的依赖图。或者是某个事件在触发之前需要满足某些依赖条件(常见于非线性分支)
工具演示
基本界面
演示视频(略)
功能:
  1. 虚拟目录 File Browser
  2. 选中显示该资源的 Dependency Graph
  3. DG可以选中结点,移动结点,多选减选。缩放视图,移动视图。双击结点跳转
  4. 选中DG的结点后,上方显示Detail信息
  5. 可以全展开,多重展开。依赖了无效的资源会以Invalid显示
  6. 搜索Name、Path、GUID,Browser会显示查询结果。误操作会弹窗警告
  7. 界面整体特性:UI自适应,Splitter拖拽控件,快捷键
工具对比
  • Unity:只提供了正向资源依赖查询,依赖项直接显示在Project视图中,没有依赖图
  • UE4:提供引用查看器,显示依赖图,与引擎原生兼容

技术架构

C++ Core

  1. 负责工具底层,解析工程资源
  2. 维护GUID与资源信息的映射表
  3. 维护依赖关系表

C# 原型

  1. 与C++混合编程,展示工具原型
  2. 使用Winform开发并用GDI+绘图
  3. 作为后续添加功能的验证与Debug工具

Python

  1. 使用PyQt开发工具界面,为以后接入Sunshine做准备
  2. 与C++混合编程,在boost.python,ctypes,python extending c三种技术中选择了boost.python
  3. 功能扩展,界面美化,操作简化等等
其他功能
Package Checker 资源检查
  1. 仓库文件损坏: 任何一个xml无法正确读取都可以归类这个(repository,同样遍历的时候)
  2. 重复GUID: 任何一个资源不应该会和另外一个资源有重复的xml(冗余的GUID:遍历的时候就可以知道)
  3. 重复的虚拟路径: 任何一个资源不应该会和另外一个资源有重复的虚拟路径(生成treeview时检查同名)
  4. 资源缺失: xml里面依赖了某个资源, 但是却没有(guid find end)
  5. 依赖缺失: Deps里面依赖了某个资源, 却没有, 不过这个因为Deps和xml的依赖应该一致, 所以可以归类为同一个(guid find end)可以在迭代器中检查
  6. 材质依赖贴图不在Deps: 因为原来的xml依赖Deps这个东西来加速加载, xml里面依赖了, 也应该有Deps(set加入时如果没有exists就是了)
  7. 额外的: 错误的引用关系; 我们固定是LODModel 引用 Model 引用 Mesh 引用 Material 引用 Texture,
    只要引用关系里面的方式和这个不一致就是错误
依赖修改器
修正或修改原先的资源依赖关系(通过GUID或路径)
修改效果
多重展开
  1. 限制层级的展开(如Level展开)
  2. 全展开
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











comments powered by Disqus