DreamShaderLang
语言核心

import

import

import

DreamShader » DreamShaderLang » import

一条顶层语句,使某个 .dsh header 的声明在当前文件中可见。

声明于.dsm、.dsf、.dsh;.dss 将其写作 #include "…",并同样接受 import "…";
种类顶层声明
名称一个 .dsh header (自 2.0.0 起;到 1.9.x 为止也可以是 .dsf)
处理者legacy 前端,负责读取该行;绑定器,通过 include 解析器解析它,并把该 header 的名字声明进当前文件 (自 2.0.0 起;到 1.9.x 为止,由编辑器的源加载器在解析之前把 header 的文本粘贴进来)
大小写规则import 为小写;其他大小写读法相同,但给出警告 DSH2253

语法

import "<path>" [;]
import '<path>' [;]          // in a .dsm, .dsf or .dsh

<path> := a path to a .dsh, relative to this file or to its source root,
          or "/"-anchored; the ".dsh" may be left off

路径在导入文件自身的源码根内解析——见 源码根。1.6.0 的根限定形式(Project:…、Plugin.<Name>:…)会被拒绝 (自 2.0.0 起)。

识别

(自 2.0.0 起) import 是语言的一个 token,解析器像读其他顶层声明一样读它。1.x 那套在原始文本中逐行识别的规则已经没有了。

规则
位置文件顶层,块与块之间
注释普通注释:写在 import 里或 // 之后的 /* … */ 会被注释掉 (1.x 的加载器照样执行 /* … */ 里的一个)
路径一个字符串:"…",支持常规转义;在 1.x 文件或 .dsh 中,也可以是同一行里的 '…'
;可选
#if由 预处理器 先解析:没选中的分支里的 import 不会被读取
被拒绝代码
import 后面不是字符串DSH2252
路径指向 .dsf 或 .dsm —— 材质文件或函数文件各自单独编译,从不被包含DSH2252
带根限定的路径(Project:Shared/Common.dsh、Plugin.X:…)DSH2252
解析到的文件不是 .dsh(或 .dss)DSH8295

[!NOTE] 编辑器的依赖扫描器仍然把 import 行当作文本读取,以便知道保存一个 header 时该重新编译哪些源文件:一个去空白后以 import 开头(忽略大小写)、然后是 "…" 或 '…' 路径、可选的 ;、之后只有 // 注释的行。它读的是原始文件,#if 分支也算在内,所以一个只被未选中分支导入的 header 改了,仍然会触发导入方重建。这是有意的:多重建一次花几秒,漏重建一次就留下过期的资产。

Specifier 规范化

路径在解析之前会先规范化:

#步骤
1去掉首尾空白
2把每个 \ 换成 /
3去掉全部前导 ./ 序列
4如果结果完全没有扩展名,追加 .dsh

所以 import "Shared/Common" 和 import "Shared/Common.dsh" 是同一条指令。

[!NOTE] 第 4 步问的是路径有没有扩展名,而不是有没有已知的扩展名,而且它只看最后一段路径。文件名里含有 . 的 —— Shared/Common.v2 —— 算作「已经有扩展名」,所以不会追加 .dsh,这个 specifier 只有在恰好存在同名文件时才能解析到。目录名里的 . —— @scope/pkg.v2/Lib —— 不算,仍然会追加 .dsh。

解析

路径相对于
一个存在的绝对文件路径那个文件
/ 锚定 (自 2.0.0 起):/Shared/Common.dsh,或 /Game/Shared/Common.dsh去掉锚(/ 或 /Game/),先找导入方自己的根——其源目录,再其 packages 目录——然后按根的顺序找其他每个根的这两个目录;第一个存在的文件胜出。永远不相对导入方文件
其他下面三个候选

三个候选按顺序尝试。每个候选都配一个包含根;解析结果落在其根之外的候选会被跳过而不是报错,第一个在磁盘上存在的候选胜出。

#候选包含根
1<directory of the importing file>/<specifier>所有根中,包含该导入文件的最长的源目录或 packages 目录;都不包含时是导入文件自己的目录
2<owning root's source directory>/<specifier>该源目录
3<owning root's packages directory>/<specifier>该 packages 目录
目录默认值项目设置
源目录<Project>/DShaderSource Directory
Packages<Source>/Packages推导得到;不能单独配置

包含比较在所有平台上都忽略大小写。至于候选最终能不能被找到,仍然走普通的文件存在性检查,遵循文件系统自身的大小写行为。

哪里都解析不到的路径是 DSH8292,消息中说明原因。

源码根

候选 2 和 3 属于导入文件的所属根——该文件所在的源码根。相对 specifier 永不离开它:Plugins/MoonToon/DShader 下的 .dsm 不能通过写 Shared/Common.dsh 够到 <Project>/DShader,反过来也一样。

这条规则的存在是为了让添加一个插件不能改变既有 import 的含义。如果项目根是全局兜底,两个都带 Shared/Common.dsh 的插件就会按扫描顺序解析,而禁用一个插件会静默改变另一个根的 import 指向。

不属于任何根的文件——测试夹具、-Source 指向树外的 commandlet——候选 2 和 3 用项目的源目录和 packages 目录,也就是根这个概念存在之前每个文件的行为。

根列表以及插件如何贡献根,见 源文件。

刻意跨根

到 1.9.x 为止,根限定符加 : —— Project:Shared/Common.dsh、Plugin.MoonToon:Shared/Toon.dsh (自 1.6.0 起) —— 让 import 指向另一个根的源目录和 packages 目录。import 拒绝这种形式 (自 2.0.0 起),报 DSH2252。要用另一个根提供的 header,可以通过 / 锚定的路径,它会先找导入方自己的根、再找其他每个根——所以自己根里的同名文件胜出——或者把它复制到本根,或者通过 package。

[!WARNING] 一个伸手到项目根里的插件不再是自包含的:把它交付到另一个项目,这条 import 就悬空了。

包含检查正是阻止相对 specifier 爬出目录树的机制。.. 段会在检查之前被解析掉,因此:

  • 对于直接位于 DShader 下的文件,import "../Secret.dsh" 解析到源目录之上, 因此候选 1 被跳过;候选 2 和 3 会塌缩同样的 .. 并落在各自的根之外,因此被同样的检查跳过;
  • 对于位于 DShader/Packages/@scope/pkg/ 下的文件,.. 可以在 DShader/Packages 内任意穿行, 因为那才是为它选定的包含根;
  • 对于不属于任何根的源目录或 packages 目录的源文件,包含根就是它自己的目录, 因此没有任何 .. specifier 能通过候选 1 解析。

Package 风格路径

@scope/name/… 不是一种独立的路径语法。@ 只是普通的目录名字符, 像 "@typedreammoon/dream-noise/Library/Noise.dsh" 这样的 specifier 能通过候选 3 解析,纯粹是因为磁盘上存在 DShader/Packages/@typedreammoon/dream-noise/Library/Noise.dsh。 没有 scope 注册表,没有版本解析,也没有被特殊处理的根。

[!NOTE] 候选 1 和 2 仍然会先被尝试。放在导入方文件旁边、或者放在 DShader 下 的同名 @typedreammoon/dream-noise/Library/Noise.dsh 文件,会遮蔽 package 里的那一份。

这套约定假定的目录布局见 Package。

header 是怎么被读取的

(自 2.0.0 起) 什么都不内联。每个 header 单独读取,它的声明加进导入方文件的声明里。

行为规则
预处理每个 header 单独对着这次编译的 define 表预处理:header 里的 #define 传不到导入它的文件。header 的 #if 失败是 DSH8291
解析每个 header 用它自己的方言单独解析:.dsh 可以让 1.x 块和 2.0 声明并存,不管导入它的是哪种文件。header 的语法错误报在它自己的行上,随后这条 import 是 DSH8294;读不了是 DSH8293
名字header(以及它导入的 header)的每个声明在导入方文件里都可见。同一个名字在 header 和导入方文件、或两个 header 里都声明了,是 DSH4210
菱形依赖被导入两次的 header 只声明一次
循环直接或经由别的 header 导入自己,是 DSH4211
资产没有:header 不能放资产块(DSH2249),所以 import 只带来名字、永远不带来资产,一个文件也最多只有一个 Shader(DSH2250)
重建这次编译读到的每个 header 的预处理后文本都计入该源文件的构建键;保存一个 header 会把导入它的每个源文件排进重编队列

[!NOTE] 1.9.x 及以前,加载器在解析之前把整个 import 闭包粘贴成一段文本,所以 "一个 Shader"的规则是闭包范围的,.dsh 可以导入 .dsf 并把它的 ShaderFunction 块作为导入方的一部分编译,每个位置都得从拼装后的文本映射回去。

源码行映射

没有什么需要映射的了 (自 2.0.0 起)。诊断带着它所在的文件 —— header 里的错误就是 header —— 以及构造的行和列,打印成 <file>(<line>,<column>): DSHnnnn: <message>。1.9.x 的位置来自拼装文本里的 near index 偏移,在 section 内部并不可靠;那套机制已经没有了。见 诊断。

诊断

码触发条件
DSH2252该行不是对 header 的 import:import 后面没有字符串、是 .dsf / .dsm 路径,或带根限定的路径
DSH2253(警告) import 写成了别的大小写
DSH8292路径解析不到任何文件
DSH8295解析到的文件不是 header
DSH8293header 读不了
DSH8291header 条件编译失败
DSH8294header 解析失败;它自己的错误会先报出来
DSH4211这条 import 闭合了一个循环
DSH4210header 声明的名字又被声明了一次
DSH2249header 里有资产块

示例

<Project>/DShader/
├── Materials/
│   └── M_Water.dsm
├── Shared/
│   └── Common.dsh
└── Packages/
    └── @typedreammoon/
        └── dream-noise/
            └── Library/
                └── Noise.dsh
// DShader/Shared/Common.dsh
Namespace(Name="Common")
{
    Function ApplyTint(in vec3 color, in vec3 tint, out vec3 result) {
        result = color * tint;
    }
}
// DShader/Materials/M_Water.dsm
import "Shared/Common";                                   // -> DShader/Shared/Common.dsh
import '@typedreammoon/dream-noise/Library/Noise.dsh';    // -> DShader/Packages/@typedreammoon/...

Shader(Name="Materials/M_Water")
{
    Outputs = {
        vec3 Color;
        Base.EmissiveColor = Color;
    }

    Graph = {
        vec3 Water = vec3(0.1, 0.3, 0.6);
        Color = Common::ApplyTint(Water, vec3(1.0, 0.9, 0.8));
    }
}

第一条路径没有扩展名,所以会追加 .dsh;M_Water.dsm 旁边不存在候选 1, 所以候选 2 在 DShader 下找到了它。第二条只有候选 3 能找到。 两条指令都随意省略或包含 ;。

每个 header 都单独预处理、单独解析;Common::ApplyTint 以及 Noise.dsh 声明的任何东西随后都是 M_Water.dsm 的名字,Common.dsh 里的诊断报在 DShader/Shared/Common.dsh(<line>,<column>)。

同样两行的 .dss 写法:

#include "Shared/Common.dsh"
#include "@typedreammoon/dream-noise/Library/Noise.dsh"

另见

本页目录