`
yanlijun250
  • 浏览: 748575 次
文章分类
社区版块
存档分类
最新评论

DirectX11的Shader Reflect的几个问题(2012-2-22更新)

 
阅读更多

很久没有做图形系统的东西了,之前因为家中出了些事情,把精力主要放到了一些杂务和俗务上。最近回过头来看看,离京前的日记里,清楚地标明着,DX11 Shader Reflect的问题还没有解决。

DX9时代带过来的习惯,因为DX9时代的Shader编撰是离不开D3DX库的,因此,感觉上总是习惯把Shader Reflect和ID3DXEffect9理解为同一个层次的东西,而且当时学习DX的时候,也是Effect的文档要远多于Shader Reflect的文档。

因此带来的一个毛病就是,写Shader系统时一直是比较依赖Effect的,组织结构上基于Effect来组织,即便是迁移OpenGL的时候,也会考虑直接使用CG里跟Effect相近的一系列概念。Effect好用、简单、粗暴、有效,围绕其进行组织也没有什么大错,只不过,它确实还不够“本质”,准备写篇“本质论”什么的,如果拿这个堂而皇之地写进去,估计要笑掉围观者的大牙。

因此,当DX11一下子将Effect“扫进垃圾堆”的时候,我多少有些措手不及。编译一个Samples里带的CEffect,总感觉不如自己全新去写一个或者研究一下,到底是怎么个情况,既然您带了CEffect的代码,读读总是无害的。


1.Basic & Misc

1? Shader Parameter以Constant Buffer来组织,不放到某个具体CB的,默认会认为是放到了全局的一个CB中。

2 一次更新一个CBuffer [?],这样比起之前DX9时一刷整个Shader所有的参数要好。如果不用CB,就是DX9那个样子。

3 CB最好按频率来进行组织:PerView、PerDrawCall、PerObject……

4 想到既然CB和Texture一个地位,也就是说Texture和Sampler虽然放到了Global下,但仍然也只是一刷只一个,不会一刷刷一堆的哈,与此同理,UAV哪些东西也是一样的咯?


2.关于Parameter

2.1.基础概念Basic

D3DReflect一个Shader后,交还给用户的是一个ID3D11ShaderReflection接口。这个接口有几个应用是对Parameter分析有关系的:

GetConstantBufferByIndex /GetConstantBufferByName

GetResourceBindingDesc / GetResourceBindingDescByName

Constant Buffer的总数量和Resource Binding的总数量在ID3D11ShaderReflection->GetDesc里面。

那么,这两者什么关系呢?


粗略来说,Resource Binding里包括很多,CBuffer、TBuffer、Texture、Sampler、UAVResource,都在里面,这里面的CBuffer一般就是我们所说的参数部分,这些东西统称为Resource Binding,也就是Resource Binding不仅仅包括CBuffer。

但是Resource Binding Description只有这些资源的一些基本信息:

Name:Resource名称,对于CBuffer来说就是你写的那个"cbuffer xxxx { ...... } "里的那个xxxx,CBuffer的本名。

Type:此Resource是个啥玩意儿?对应D3D_SHADER_INPUT_TYPE。0(D3D_SIT_CBUFFER)就是CBuffer。

BindPoint / BindCount:绑定到了哪个索引上,总共绑了多少个。对于CBuffer而言,就是XSSetConstantBuffer()里那些个index该怎么填,查这里就对了。

uFlags:Resource Binding的这个uFlags对应D3D_SHADER_INPUT_FLAGS。

其它几个都跟纹理有关,跟CBuffer半毛钱关系没有,暂时无视之。

呃,好了,问题来了:这里面一个参数的信息都没有呀?!

没错,因为这里才是对Shader而言真正有用的东西:Shader绑定的资源。

参数是你程序员看了方便的东西,跟Shader本身就半毛钱关系没有,我这绑定资源的接口,凭啥要给你参数信息呢?

不过参数确实是太有用了,没了参数,那不抓瞎了,啥都跟DX文档上那样写,一个cbuffer什么样,我就C++写一个struct也什么样,然后提交CBuffer,我累不累……

所以人微软还是很厚道滴给了另一组信息,就是GetConstantBufferByIndex /GetConstantBufferByName这一组接口。

这组接口里信息就多了,直接一下子返回了一个新的接口:ID3D11ShaderReflectionConstantBuffer,这里面就有一大堆参数信息了,Description里的Variables就是CBuffer里面所有参数的总数量,另一个有用的信息就是Size:指出了这个CBuffer的大小。

使用这个接口,首先通过GetDesc获取Variables的总数量,然后就可以继续foreach,调用GetVariableByIndex,或者直接通过GetVariableByName来获取某个参数的具体信息了。

参数信息存储于GetVariableByxxxx返回的ID3D11ShaderReflectionVariable中。包括Variable本身的信息:

Name:名字,不解释。

StartOffset:在CB里的起始字节。

Size:占了CB多少个字节。

uFlags:对应D3D_SHADER_VARIABLE_FLAGS,比较有用的是检查是不是D3D_SVF_USED,没有Used的就不需要刷了,参数表也可以无视之。

还有就是Type信息,就是这个Variable是int4?float4x4?数组?Struct?这个就不解释了,填值的时候,肯定得对照着这个来填,要不一个int4填成一个float4那就麻烦了。


提醒一下,注意这个uFlags,虽然都是INT uFlags,但在几个不同的地方其对应的实际的enum是不同的。

ID3D11ShaderReflection::GetResourceBindingDesc——D3D_SHADER_INPUT_FLAGS

ID3D11ShaderReflectionConstantBuffer::GetDesc——D3D_SHADER_CBUFFER_FLAGS

ID3D11ShaderReflectionVariable::GetDesc——D3D_SHADER_VARIABLE_FLAGS



2.2关于组织模式:

基本上要解决的几个问题是:

这个Shader总共引用了多少个CBuffer?这个可以通过Resource Binding的分析得到。

每个CBuffer里的参数是怎么组织的?这个可以通过分析ID3D11ShaderReflectionConstantBuffer得到。


原理介绍到这里,现在介绍几种之前看到过的组织模式:

一种是先分析CBuffers,得到所有的Parameter与CB的对应关系,然后建立出CB放到哪里,然后再分析ResourceBinding,再将这些CB按照RB里的Binding Point来Cache到对应的索引上,最后提交时,调用XSSetConstantBuffer刷入所有CB。KlayGE使用的就根这个模式很像,特别是,印象里,Klay并没有把CB按照RB里的Binding Point重排位置,而是按照CBuffers的获取时传入的Index(就是foreach CBuffers那会儿)直接建立了CB表,这两者是否真正一致?待验证。

另一种是直接上来就分析Resource Binding,如果当前分析到的是CBuffer,那么就根据Resource的名字,通过GetConstantBufferByName来获取Constant Buffer,再继续分析Parameters。CEffect和U3都是这个思路。与前者没有根本的不同,只是习惯怎么写方便就怎么写的区别吧。


最后U3的专门要说明一下,首先,每个Shader参数本身存储参数信息的空间肯定是必须有的,我们称之为Shadow Buffer,这个肯定得有,要不你参数计入进来了,没个参照的,难道要直接刷到CBuffer里去?万一没Dirty你这不白白占了个Cpu-Gpu传输吗,无论哪种方法这里没有不同的。组织上有不同的地方,主要是在于CBuffer。

看到其他的实现更多的都是每个Shader留存自己独有的CB列表,估计每个人写,一开始都会直接想到这里的,这么写直观、方便,也没有什么不适的。

但U3的做法不同,粗略看了一下,它的做法是总共就建立了8个(?)CB,所有Shader共享这8个CB。新的Shader到来后,分析时会瞅瞅自己某个索引所需的CB大小是否会超出此索引现有的CB大小,如果会就重刷这个索引的CB。刷参数的时候也很有意思,会根据参数Dirty与否,尽可能刷少的字节数,也就是整个CB可能256个字节,但发现前32个字节动了,就只刷前32个。理论上,刷少量字节确实会取得好的效果,到底能省多少,这块儿最好试验一下。最大的问题在于,不同Shader的CBuffer是铁定不一样的,您这里省了,新Shader跟旧的Shader不一样的地方,就该刷还得刷,这样做到底有什么好处?难道是为了减少显存的占用?(如果Shader数量较多的话,CB确实也是个不小的开销)目前没有想太明白,当然,也有可能是看的太粗略了,还没有掌握精髓,继续看吧。


——2012-2-22更新——

我现在主要采用的是第二种模式,伪代码可能如下:

ShaderReflectionDesc srd = ID3D11ShaderReflection->GetDesc();

uint _CurrentParsingCBIndex= 0;

std::vector<Parameter> CachedParameters;

std::vector<ConstantBuffer> CachedCBuffers;

foreach (ResourceBinding rbin srd.ResourceBindings)

{

if (rb.Type == CONSTANT_BUFFER)

{

// 分析此CBuffer

ID3D11ShaderReflectionConstantBuffer* srcb = ID3D11ShaderReflection->GetConstantBufferByName(rb.Name);

foreach (Variable v in srcb.Variables)

{

// 没用到的Parameter无视

if(v.Flags & FLAG_USED == 0)

continue;

// 增加一个Parameter

Parameter param {

Name = v.Name; Offset = v.Offset; ArraySize = v.ArraySize; ByteSize = v.Size; CBufferIndex =_CurrentParsingCBIndex;

};

CachedParameters.pushback(param);

}

// 增加一个CBuffer到Cache中

CachedCBuffers.pushback( ConstantBuffer{ Name == rb.Name; BindingPoint = rb.BindingPoint; } );

_CurrentParsingCBIndex ++;

assert(CachedCBuffers.size() == _CurrentParsingCBIndex);

}

……

}





2.2.1-----2012-2月22日更新----关于U3

又看了看U3 CBuffer这块儿的组织,不是怪异,而是牛逼。

U3因为它整个渲染引擎是具有一整套完整的体系的,而且借助于其优秀的Vertex Factory / Material Template设计,所以可以做到一开始就把所有的Constant全部合理安排的地步,因此它把自己所可能用到的所有CB按照频率和其它因素设置成了8个Buffer。

例如,其Vertex Factory相关的Buffer,在整个Static Mesh的各个SubMesh渲染完之前,是不会Update的,因为没有Update的必要,里面记录的World Matrix信息在这中间是不会改动的。

这样合理的划分即便是相比于使用Effect Pool,也要合理很多,而很多人使用Effect连Effect Pool都不用,性能上的浪费可想而知!因为Effect的实现,是每个Effect自己为自己下属的所有Shader保存一个Cbuffer Cache的,每切换一次Effect,哪怕Shader本身没有变化,CBuffer都会强制重新提交。而且更重要的是,Shader间的CBuffer不共享,明明两个Shader的某个CBuffer的信息完全一样,但只是因为Shader不一样,这个Shader的World Matrix变化还需要在另一个Shader里面设置一次,使得CBuffer重新提交的数量级迅速增加。而U3的组织流程就不存在这个问题。

当然了,整个游戏每次只需要渲染不到100个批次,你用Effect和自己管理Shader代价当然差不了多少了。

但是10000个呢?材质爆炸后呢?单不同Shader的这些Cache Buffer的浪费就是一个恐怖的数量。须知Unreal3就算在材质良好设计大量使用Material Instance的情况下,Shader Pool仍能保持在K这个数量级,还是没开DX11的,K个Shader的Cached Buffer,如果每个Shader独立存储的话,再加上需要临时记录在内存里的Shadow Buffer,是什么概念呢?可想而知。

但是,为什么U3可以这么做呢?是因为它不是一个通用化图形引擎,而是一个体系化的游戏引擎。

通用化图形引擎你不知道别人会怎么使用你的引擎,有些人做《Magicka》那样的小品级游戏(没有任何别的意思,我很喜欢这种小品级游戏),你迫使他花精力去管理各种Buffer组织什么的,人家才不跟你玩那个呢,XNA比你这套方便多了。

但还有些人,整个场景里1000个物体,1000种不同的材质,每个材质都一个独立Shader,这种游戏你如果只支持Effect式的方案就麻烦了,浪费+切换,在这方面可能就会落后一些。

所以,根据实际情况来选择吧。


2.2.2——2012-2-22——关于KlayGE的CBuffer(?)

“印象里,Klay并没有把CB按照RB里的Binding Point重排位置,而是按照CBuffers的获取时传入的Index(就是foreach CBuffers那会儿)直接建立了CB表,这两者是否真正一致?”

手头暂时没有Klay,之前看过,也不敢确认自己到底是不是记得准确。Klay是个优秀的引擎,事实上去年自己用过一段时间,很好用,写的也很不错。这里本文并无意于去讨论Klay,本文还是主要想讨论CB的使用和组织问题,如果这里小生记错了,还望龚大海涵。

这个问题的答案是——不一致如果在使用CBuffer时,强制通过register(b13)把CBuffer设到靠后的位置,就会很快发现这种不一致。

所以,组织CBuffer时,千万记得按照Resource Binding里的Binding Point信息重排CBuffer的位置,否则在后面Set Constant Buffer时,传入的Index就有可能是错的

例如我建立了1个CBuffer,在register b13,但是按照Desc.ConstantBuffers获取出来的数量是1个,所以如果按照ConstantBuffers来阻止的话,这个CB的索引是0,而不是13,提交时如果也按照这个索引提交,就会把本身应提交到13的CB提交给0。



2.3关于XSSetConstantBuffer

与Effect不同,Shader Reflect还需要解决的一个问题在于:Effect的参数,设置一次,自动就会在应用Pass时决定哪个Shader Update哪些参数。咱们自己Shader Reflect就得自己处理这个工作了。

这个取决于具体的组织模式了,如果您的Shader系统也是按照类似Effect的模式来组织的,那么这里倒不会有太多麻烦,按之前说的,把参数表不要存到Shader里,而是存到Effect里,提交的时候,Shader的CBuffer都去Effect里获取Dirty参数信息,然后决定怎么刷自己,怎么提交,问题不大。

但是如果您的Shader系统跟Effect的组织模式完全不一样呢?

那就麻烦了,首先就要确认什么时候刷参数的问题。Set Shader的时候刷?不合适,因为Set Shader后您再改这个Shader的参数,就得等下次Set同一个Shader时才能起作用了。所以一般对使用者不太造成影响的做法可能就是把提交的时机定在Draw Primitive时,判断当前的Shader是不是有参数更新、或者是不是新的Shader,有就刷,没有就过。
确认什么时候刷参数后,就是最头疼的问题了,对于Effect,可以共享参数,一个WorldMatrix,只需要对一个Effect(甚至一个EffectPool)设置一次,所有这个Effect下的Shader、只要这个参数启用了,就自动刷自己的CB并提交。但对于我们自己Reflect的Shader,怎么处理共享参数呢?

别告诉我您希望对每个Shader:VS、GS、DS、HS、PS,各调用一遍SetParamValue("WorldMatrix", m_matWorld);

这样基本上会导致下面的结果:

1,对写Shader的人要求增加了,他必须自己清楚自己的Shader里到底用到了哪些参数,并在需要的时候更新这些参数。写了没用到的参数,就白白浪费了效率。

2,即便是都有用的参数,也不好优化,VS里的WorldMatrix和PS里的WorldMatrix,必须都得手动调用设置一遍,每次都得查询->刷新,Effect只需要查询一次即可。

目前想到的没更好的办法,只能是按这个说的来,自己写Shader,自己维护参数。


2.3.1——2012-2-22——再论U3的组织

上面说到的问题,感觉U3的那个思路也是很不错的,但是就像之前所说的,那是一种体系化游戏引擎的思路,对于做通用化图形引擎来说并没有太多的参考价值。因为实际的使用者很可能并不能像U3那样去把整个应用程序的CBuffer安排出来,而且这样做对于使用者本身的要求就太高了,会把很多创意高手、图形新手给拒之门外。

另外就是也可以考虑一下Effect Pool的那个思路,把一些可能会共享的参数通过一个Pool,来使得若干个从此Pool产生的Shader可以共享。但是Effect Pool本身事实上是通过对Effect脚本语法分析得出来的,如果要做这个,而不想在D3D的基础上引出太多概念,要怎么做确实还是一个新的问题。

引擎要做成什么样,只有做引擎的人最了解,没有一成不变的方案,只有最适合自己的方案。No Silver Bullets,任何时候都别忘记这一点,既然没有银弹,与其花时间去追寻“一揽子解决方案”,不妨更多时候考虑考虑“他想要什么”。


2.4 关于Samplers、Textures的组织

这玩意基本上比前面的CBuffer要简单,因为它不存在着一个CBuffer里好几个Sampler,Texture每个要去算Offset的问题。
所以只需要围绕着各自Resource Binding的BindingPoint、BindingCount建表即可,也是需要注意Binding Point的问题,Texture和Sampler也是可能会通过register给强制设到后面的。
此外,Sampler、Textures还需要注意的一点在于他们是可以“Array化”的。
是的:
Texture2D GMRTs[4];
可以这样。
有什么不同吗?
两点:
第一,他们的Array信息记录在BindingCount里,上面的情况,BindingCount就不再是正常情况下的1了,而是4。这个是废话,因为前面已经强调过这个问题了。
最需注意的在于第二点,你通过Name获得到的这个变量的变量名不是GMRTs,而是GMRTs[4]!所以如果按照经验,直接把这个Name放到变量表里,后面您可就找不到这个变量咯。
解决方案不麻烦:放到变量表前,先看看有木有“[x]”,有的话干掉,然后再放到变量表即可。
还需注意的是,Texture和Sampler跟CB一样,也是可以在不同的Shader间通用的喔~~所以,优化CBuffer的思路也可以跟这些放到一起来做的~



others...

想到再写,继续维护,


至少会想把TBuffer、Texture、Sampler、Array什么的都给写写,DX这套挺好玩,感觉做起来虽然麻烦点,但却深入到很多底层的层面,探索的乐趣才是真正的乐趣。




分享到:
评论

相关推荐

    安卓桌面应用EyeRoom.zip

    android 源码学习. 资料部分来源于合法的互联网渠道收集和整理,供大家学习参考与交流。本人不对所涉及的版权问题或内容负法律责任。如有侵权,请通知本人删除。感谢CSDN官方提供大家交流的平台

    仿随手记的炫酷饼图.zip

    android 源码学习. 资料部分来源于合法的互联网渠道收集和整理,供大家学习参考与交流。本人不对所涉及的版权问题或内容负法律责任。如有侵权,请通知本人删除。感谢CSDN官方提供大家交流的平台

    webview重载使用&自定义网址.zip

    android 源码学习. 资料部分来源于合法的互联网渠道收集和整理,供大家学习参考与交流。本人不对所涉及的版权问题或内容负法律责任。如有侵权,请通知本人删除。感谢CSDN官方提供大家交流的平台

    C语言学习工程和C语言项目.zip

    C语言诞生于美国的贝尔实验室,由丹尼斯·里奇(Dennis MacAlistair Ritchie)以肯尼斯·蓝·汤普森(Kenneth Lane Thompson)设计的B语言为基础发展而来,在它的主体设计完成后,汤普森和里奇用它完全重写了UNIX,且随着UNIX的发展,c语言也得到了不断的完善。为了利于C语言的全面推广,许多专家学者和硬件厂商联合组成了C语言标准委员会,并在之后的1989年,诞生了第一个完备的C标准,简称“C89”,也就是“ANSI C”,截至2020年,最新的C语言标准为2018年6月发布的“C18”。 [5] C语言之所以命名为C,是因为C语言源自Ken Thompson发明的B语言,而B语言则源自BCPL语言。 1967年,剑桥大学的Martin Richards对CPL语言进行了简化,于是产生了BCPL(Basic Combined Programming Language)语言。

    带暂停功能倒计时TimeCountDown盒子适用.zip

    android 源码学习. 资料部分来源于合法的互联网渠道收集和整理,供大家学习参考与交流。本人不对所涉及的版权问题或内容负法律责任。如有侵权,请通知本人删除。感谢CSDN官方提供大家交流的平台

    Google翻译.txt

    Google翻译.txt

    汽车车灯检测机械臂设计.doc

    汽车车灯检测机械臂设计.doc

    网络购物中心项目源码.rar

    网络购物中心项目源码.rar是一个压缩文件包,包含了一个基于Web技术的电子商务平台的全部源代码和相关资源。这个源码包旨在提供一个功能全面、界面友好的在线购物体验,它集成了商品浏览、搜索、购买、支付以及用户管理等核心电商功能。该项目采用了当下流行的开发框架和编程语言,比如使用HTML5, CSS3, JavaScript, PHP和MySQL数据库等技术,确保了网站的响应速度和跨浏览器兼容性。对于即将毕业的学生或者正在寻找实践项目的课程设计者来说,这个源码包是一个宝贵的资源。它不仅提供了一个实际应用的平台以供学习和研究,还允许用户根据需求进行定制和扩展,如添加新的功能模块或优化现有的代码结构。此外,项目文档详细记录了系统架构、功能实现和部署流程,为初学者提供了清晰的指引。通过分析和修改这份源码,学生可以深化对Web开发的理解,提高编程能力,并且有机会将理论知识转化为实际操作技能。此源码包适合作为计算机科学与技术、软件工程、信息技术等相关专业的毕业设计或课程设计项目,能够帮助学生在完成学业的同时,积累实战经验,增强就业竞争力。无论是作为学习的起点,还是作为未来职业生涯的一个跳板,网络购物

    C语言仓库,存储的是C语言代码.zip

    C语言诞生于美国的贝尔实验室,由丹尼斯·里奇(Dennis MacAlistair Ritchie)以肯尼斯·蓝·汤普森(Kenneth Lane Thompson)设计的B语言为基础发展而来,在它的主体设计完成后,汤普森和里奇用它完全重写了UNIX,且随着UNIX的发展,c语言也得到了不断的完善。为了利于C语言的全面推广,许多专家学者和硬件厂商联合组成了C语言标准委员会,并在之后的1989年,诞生了第一个完备的C标准,简称“C89”,也就是“ANSI C”,截至2020年,最新的C语言标准为2018年6月发布的“C18”。 [5] C语言之所以命名为C,是因为C语言源自Ken Thompson发明的B语言,而B语言则源自BCPL语言。 1967年,剑桥大学的Martin Richards对CPL语言进行了简化,于是产生了BCPL(Basic Combined Programming Language)语言。

    Sora AI Videos的案例站点

    这是Sora AI Videos的案例站点,使用此项目可以帮助你快速构建Sora AI的演示项目。

    2015园林业务齐发展,区域拓展加速(20页).zip

    2015园林业务齐发展,区域拓展加速(20页).zip

    机械臂的物体识别与抓取技术研究.pdf

    机械臂的物体识别与抓取技术研究.pdf

    使用不同的超导间隙模型拟合从穿透深度获得的超流体密度数据matlab代码.zip

    1.版本:matlab2014/2019a/2021a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。

    一个年终抽奖系统,可以根据你需要的去设置

    年终抽奖系统的模型,需要需要特殊定值,可以留言

    埃博拉优化搜索算法matlab代码.zip

    1.版本:matlab2014/2019a/2021a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。

    ECommerceCrawlers-master.zip

    实战多种网站、电商数据爬虫。包含:淘宝商品、微信公众号、大众点评、闲鱼、阿里任务、百度贴吧、豆瓣电影、包图网、全景网、豆瓣音乐、某省药监局、搜狐新闻、机器学习文本采集、fofa资产采集、汽车之家️️️

    viewflow视图切换效果.zip

    android 源码学习. 资料部分来源于合法的互联网渠道收集和整理,供大家学习参考与交流。本人不对所涉及的版权问题或内容负法律责任。如有侵权,请通知本人删除。感谢CSDN官方提供大家交流的平台

    可二次开发广州酒店信息管理平台.rar

    广州酒店信息管理平台是一个针对酒店行业设计的综合性信息管理系统,旨在通过现代化的信息技术手段提高酒店运营效率、改善客户体验,并为管理者提供决策支持。该平台以用户友好的界面和强大的数据处理能力为特点,涵盖了客房预订、入住管理、餐饮服务、库存管理、财务报表等多个模块,实现了酒店业务流程的自动化和智能化。作为毕业设计或课程设计项目,这个平台提供了丰富的功能和灵活的二次开发环境。学生可以根据自己的专业知识和兴趣,对现有系统进行深入分析,提出创新改进方案,或者添加新的功能模块。例如,可以通过集成人工智能算法来优化房间分配策略,利用大数据分析技术预测市场趋势,或者开发移动端应用以便客户能够随时随地访问服务。源码文件包中包含了完整的系统架构设计文档、数据库结构、前后端代码以及详细的API文档,使得学生能够快速理解系统运作机制并开始二次开发。此外,平台采用了模块化的设计思想,便于学生按需修改或扩展功能,同时也有助于培养学生的软件工程实践能力和解决实际问题的能力。总之,广州酒店信息管理平台不仅为学生提供了一个实战演练的机会,而且通过实际操作加深了对酒店管理业务和软件开发流程的理解,是一份极具挑战性和实用

    百度翻译.txt

    百度翻译.txt

    python源码python基础

    python源码python基础提取方式是百度网盘分享地址

Global site tag (gtag.js) - Google Analytics