论文阅读 | Automatic Mesh and Shader Level of Detail

本篇文章给出了在自适应划分的距离组下同时优化网格和 Shader 的 LOD 的优化算法。

文章中首先提出了被称为“交替优化”的优化算法,其中首先对 Shader 利用遗传算法进行变异,得到若干变体,再利用网格简化算法来以 image loss 进行网格简化,使得在给定距离上每个变体对应的运算代价小于给定开销,且误差上满足要求。之后,这些变体会进行排序,前 N% 的变体进入下一轮交替优化,反复多轮后得到结果。

针对交替优化耗时较长的问题,文章中还提出了“分别优化”的算法。该算法会首先分别对网格和 Shader 独立的进行简化,得到一系列质量单调下降的 Shader 和网格变体列,然后再针对每个距离组选择合适的网格和 Shader 对。为了让 LOD 组间的变化尽可能平滑,文章还设置了最平滑的 LOD 切换路线的查找,以及 LOD 组数量的优化操作。

相关工作

  • 网格简化和 LOD 生成
  • Shader 简化和 LOD 生成
  • 基于外观的联合优化

方法总览

Formulation

对于 Shader 和网格简化问题,定义三元组 $ (M_i, S_i, d_i) $,其中

  • $ M_i $ 为原网格 $ M $ 的第 $ i $ 个简化变体
  • $ S_i $ 为原 Shader $ S $ 的第 $ i $ 个简化变体
  • $ d_i $ 为距相机的距离

定义 $ \epsilon_a(i) $ 为简化 $ (M_i, S_i, d_i) $ 变体的绝对图像误差,其定义为

$$ \epsilon_a (i) = \int_H \| f(M_i, S_i, d_i) - \bar{f}(M, S, d_i) \| dH $$

这里的作为误差模型的积分域 $ H = V \times U \times X \times Y $ ,其中

  • $ V $ 为离散的若干个 view direction
  • $ U $ 若干 Shader uniform 参数,如光照方向
  • $ X \times Y $ 为图像空间的两个维度

这里的范数是 pixelwise RGB $ L^2 $ 范数。

另外,定义 $ \epsilon_t(i) $ 为两个简化组之间的视觉差异:

$$ \epsilon_t (i) = \int_H \| f(M_i, S_i, d_{i+1}) - f(M_{i+1}, S_{i+1}, d_{i+1}) \| dH $$

这样,LOD 优化问题就可以看作下面的数学问题:

$$ \mathop{\arg \min}_{M_i, S_i, d_i} t = Cost ( f(M_i, S_i, d_i) ) \\ \mathrm{s.t.}\quad \epsilon_a(i) < e_a (d_i) \cdot s_{d_i} $$

其中 Cost 为在该网格上应用此 Shader 进行着色的时间开销,$ e_a (d_i) $ 为在 $ d_i $ 距离的 absolute per-pixel error bound, $ s_{d_i} $ 为距离 $ d_i $ 时网格 $ M_i $ 的投影大小,

其中 $ e_a(d) $ 采用前面工作提出的一个启发函数:

$$ e_a(d) = (\frac{d-d_{near}}{d_{far} - d_{near}})^Q \cdot e_{max} $$

其中

  • $ d_{near} $ 和 $ d_{far} $ 是设置的视景体参数
  • $ e_{max} $ 是 maximum absolute per pixel error bound
    • 也就是关于 $ e_t(i) $ 的积分项关于积分域里面各个部分的最大值
  • $ Q \in [0, 1] $ 反映了对误差的容忍程度

交替优化

Shader 简化

这里的 Shader 简化工作主要参考了前面的文章:

  • [3] Y. He, T. Foley, N. Tatarchuk, and K. Fatahalian, “A system for rapid, automatic shader level-of-detail,” ACM Trans. on Graph. (TOG), vol. 34, no. 6, p. 187, 2015.
  • [8] R. Wang, X. Yang, Y. Yuan, W. Chen, K. Bala, and H. Bao, “Automatic shader simplification using surface signal approximation,” ACM Trans. on Graph. (TOG), vol. 33, no. 6, p. 226, 2014.
  • [18] F. Pellacini, “User-configurable automatic shader simplification,”
    ACM Trans. Graph., vol. 24, no. 3, pp. 445–452, 2005
  • [21] P. Sitthi-Amorn, N. Modly, W. Weimer, and J. Lawrence, “Genetic programming for shader simplification,” in ACM Transactions on Graphics (TOG), vol. 30, no. 6. ACM, 2011, p. 152.
  1. 将 Vertex Shader 和 Fragment Shader 转换为抽象语法树 (AST) 和程序依赖图 (PDG)
  2. 应用不同的化简规则来生成简化 Shader
    • Operation Removal: 将 $ op(a, b) $ 省略为 $ a $ 或 $ b $
    • Code Transformation: 将 per-pixel 的 pixel shader 操作移动到 per-vertex 或 per-tessellated-vertex 的操作来减少计算量
    • Moving to parameter: 将参数用其均值替换($ n \to average(n) $),并且替换到 “parameter stage” 中进行计算(详见 [3]),并将均值作为结果送入 GPU Shader 中

本文并没有对 Shader 本身的优化方面做出额外的创新。这些方法主要来源于 [3] 这篇文章。

Mesh 简化

Mesh 简化工作:

  • [4] M. Garland and P. S. Heckbert, “Surface simplification using
    quadric error metrics,” in Proceedings of the 24th annual conference on
    Computer graphics and interactive techniques. ACM Press/AddisonWesley Publishing Co., 1997, pp. 209–216.
  • [7] P. Lindstrom and G. Turk, “Image-driven simplification,” ACM
    Transactions on Graphics (ToG), vol. 19, no. 3, pp. 204–241, 2000

主要用了 [7] 中的 Image-driven simplification 的方法。这个方法是基于顶点对折叠的,每次折叠选择使 image error 升高最低的一对顶点。

QEM

https://www.cs.cmu.edu/~./garland/Papers/quadrics.pdf
http://mgarland.org/research/quadrics.html
https://blog.csdn.net/lafengxiaoyu/article/details/72812681

QEM 是 SIGGRAPH’97 提出的经典算法,截至现在已经有大约 5000 次引用。

交替优化

给定网格 $ M $ 和 Shader $ S $,

  1. 搞 Shader 优化 (然后生成一堆变体 $ S_i $)
  2. 对于每个在 Pareto frontier 上的 $ S_i $,利用该 Shader 进行相应的 Mesh 简化,使得新的 $ M_j $ 在满足质量要求 (也就是 error <= absolute error bound) 的情况下为最简

    Pareto frontier 上的 $ S_i $ 满足

    • 不存在另一个 Shader,他的性能一样,质量更好
    • 不存在另一个 Shader,他的质量一样,性能更好
  3. 将这些 $ (M_j, S_i) $ 按渲染性能排序,取前 20% 作为种子进入下一轮迭代

分别优化

生成网格变体

因为没有任何关于简化后 Shader 的信息,所以作者此处采用原 Shader 进行着色后 supersampled / filtered 的图片作为 loss 环节进行网格简化。

因为某些边简化之后对视觉表现没有什么影响,所以这里只选取 K (实现中 K = 500) 个有较大 error 变化的简化网格作为候选变体。

生成 Shader 变体

理论上,对于不同的场景配置 (简化网格 & 距离配置),最优的 Shader 变体是不同的。

但是,因为

  1. First, as has been proven in prior work [3], the performance and error of shader variants can be predicted instead of being actually evaluated. In this way, we do not need to actually render every shader variant under all scene configurations.

    在 [3] 中,性能的预测是通过一种简单的启发函数,即 scalar fp ops + 100 * texture ops 来预测的(不同 Shader stage 有不同权重,parameter 数量有额外惩罚)

    error 的评价是通过 error cache 和偶尔的重新 evaluate 来实现的

  2. Second, we noted that for one shader variant with one simplified mesh, the shading errors at distances could be approximated by filtering the rendered image at the closest distance.

    通过在最近距离生成着色结果,再进行 filter 来模拟在远处的结果

  3. Finally, we further observed that although these Pareto frontiers may change with scene configurations, the shader variants on Pareto frontiers are similar at similar distances and with similarly simplified meshes.

    Pareto 面上的 shader 变体基本上是比较稳定的,随着场景配置的变化不是很多

所以,作者最后只选择有代表性的距离有代表性的简化网格来计算最优 Shader 变体,而不是穷举所有场景配置。

作者选择均匀的从 N 组距离组里面选择 4 组,然后每个距离组里面选择 10 个前面的简化网格(即 Pareto 面左右的十个),就得到了 40 个组合。然后用 genetic programming 的优化方法来得到每个 (距离, 网格) 组上的最优简化 Shader。这些优化好的 Shader 变体都放到一个数组里面。

然后,作者近似的认为整个问题是一个凸区域上找可行域边界的问题,所以只需要 1D search,而不需要遍历 2D 区域。

然后,再用 find smooth path 的技术来获得比较连续的 LOD transition。

具体来说,就是每个边的权重是在边界处的图像损失,这样图像损失小的转换会更容易被选中。

最后,合并区别不大的 LOD 组。

论文阅读 | 平衡精确度和预测范围的黑盒 GPU 性能建模

简介

本篇文章提出了一种跨机器,黑盒,基于微测试 (microbenchmark) 的方法来解析的对不同实现变体的 OpenCL kernel 的执行时间进行预测和最优 kernel 选择。

简单来说,本文大的思路是,收集一些 kernel 中出现的特征和对应特征在运行时会出现的频率,利用 microbenchmark 在目标平台上测量这些特征每次出现会花费的运行时间,再用一个(多重)线性模型来拟合最后的运行时间。

由于文章比较长,此处将文章的大概结构列举如下:

  • Section 1: 简介
  • Section 2: 解释性的例子
  • Section 3: 本文贡献概况
  • Section 4: 本文采用的假设和局限性
  • Section 5: 收集 kernel 统计信息
  • Section 6: 建模 kernel 执行时间
  • Section 7: 校准模型参数
  • Section 8: 结果展示
  • Section 9: 作者调研到的、其它相关的性能建模方法

本文的假设和局限性

本文提到的一些 assumptions:

  • (usefulness) 可以帮助用户理解给定机器的性能特性,并且给优化器提供变体性能数据预测参考,同时降低需要在目标系统实际测量的数据数量
  • (accuracy) 根据检索到的相关文献显示,在本文提及的 GPU kernel 性能预测问题上,没有方法可以一致的获得小于个位数的预测误差,所以本文也设定这样的目标
  • (cost-explanatory): 和其它基于排名的方法不同 (Chen et al. (2018)),虽然本文优化的目标是在各种变体中进行选择,但是本文中模型的主要输出为运行时间,且采用比较可解释的线性模型进行建模

本文提到的一些局限:

  • 硬件资源的利用率:
    • 硬件资源的利用率会影响最终的性能。比如,峰值浮点性能受 SIMD lane 使用率影响,片上状态存储器 (VGPR, Scratchpad Memory) 会影响调度槽位的利用率,进而影响延迟隐藏的能力
    • 不过,采用本文的方法,基本的性能损失系数是比较容易解释和估计的。比如,实际的内存带宽利用率,以及峰值 FLOP/s
    • 即使无法达到硬件资源的全部利用,对于硬件资源利用率随参数变化相对稳定的场合,本文的模型仍然可以适用。不过对于变化的情况,让本文提出的模型适用的唯一可行方法,就是将模型的粒度调低到类似 SIMD lane 的水平,这样利用率的变化就不再相关了。ECM 系列模型就是这样考虑这个问题的。

      ?

    • 为了简化的处理这个问题,本文采用 workgroup size 恒定为 256 的参数设定。
  • 程序建模上的简化:
    • 本文的模型中,主要检测的是基于某种特殊类别的操作 (e.g. 浮点操作,特殊类型的访存) 和检测到该特征出现的次数,其中次数被建模为 non-data-dependent 的一个特征。
      • Polyhedrally-given loop domain?
    • 所有分支指令都假设两个分支均会执行,即假设 GPU 采用 masking 的方式进行执行。

      文章认为这和 GPU 的行为是匹配的,不过显然不完全是。较新的 GPU 是同时支持 branching 和 masking 的。masking 存在的意义是对于短分支来说,可以不打断流水线。

  • 内存访问开销评估:
    • 内存访问的开销受到程序访问的局部性,以及对于 banked memory 来说的 bank 竞争问题的影响。
    • 本文将内存访问切分成了两种:
      • 对于各个程序都常见的,比较简单的访存模式,用 Section 6.1.1 的办法按 interlane stride, utilization radio 和 data width 进行分类

        quasi-affine?

      • 对于更复杂的访存模式,在 Section 7.1.1 中提供一种单独抽出来在循环里面按该模式进行访存,并且进行测量的机制
  • 平台无关:
    • 本文提出的系统作用于 OpenCL 上,但是相似的系统在 CUDA 上也可以比较轻松的实现。

收集 kernel 统计信息

计算每个特征的预期出现次数

前面提到,本文假设程序中出现的所有循环,其循环次数和本次运行所使用的数据无关,即 non-data-dependent。

这种情况下,如果要求解循环体中每个语句的运行次数,简单的做法是将所有循环展开,不过这样效率会比较低。事实上,此处可以把问题看作:在 $ d $ 维的整数空间 $ \mathrm{Z}^d $ 中,可行区域是由一些约束条件构成的超平面截出来的一个子区域,某个语句的循环次数就是在该子区域中整数格点的数目。

文章汇总提到,用 barvinokisl 库一起,可以解决前面这个数循环体内语句执行次数的问题,其中 barvinok 是基于 Barvinok 算法的,这是一个比较高效的、计算有理凸多胞形中的格点数目的算法。

当然,还要分析好一条语句内真正进行计算或数据搬运的相应特征和次数。

为什么要抽象成有理凸多胞形? 这是因为真正循环的次数和 Kernel 本身的一些参数,以及 Kernel 的 Launch parameters 也有关系,这里希望带着这些参数做符号计算,让模型更有用一些(比如说,优化这些参数会变得容易)

计数粒度 (count granularity)

计数粒度设计的思路是,计数出来的次数尽可能贴近真实 GPU 硬件中所执行操作的次数。

比如,我们知道,在 OpenCL 的调度模型中,每个 sub-group 会尽可能匹配 GPU 调度的最小单位,并且视硬件能力 sub-group 内部会支持一些 reduce 和 scatter 等原语,并且算数指令一般也是以 sub-group 为粒度进行调度和实现的。这样,算术指令就应该以 sub-group 为粒度计数。

当然,具体 sub-group 的数目是依赖具体的 Kernel launch parameters 的,不过这里对前面参数的依赖是多项式形式的 (比如 work-group count / 32),所以可以作为一个含参的量,让前面的循环次数计算也成为一个含参的值。

粒度有如下三种:

  • per work-item
    • 同步障操作 (barrier synchronization)
  • per sub-group (subgroup size 需要用户提供)
    • 片上操作:算数指令和 local memory 访问
    • uniform 访问:global memory 访问,但是 lid(0) stride 0,即多个线程访问同一块内存区域
  • per work-group (没有给出例子)

这里的讨论很不详细,需要和下面一起看

建模 kernel 执行时间

$$ T_\text{wall}({\bf n}) = \text{feat}^\text{out}({\bf n}) \approx g(\text{feat}^\text{in}_0({\bf n}), ..., \text{feat}^\text{in}_j({\bf n}), p_0, ..., p_k) $$

其中:

  • $ {\bf n} $ 是整个计算过程中为常数的、仅与各种变体相关的整数向量
  • $ \text{feat}^\text{in}_j({\bf n}) $ 是某种单元特征的出现次数(比如单精度 FP32 乘法数)
  • $ p_i $ 是硬件相关的校正参数
  • $ g $ 是用户提供的可微函数

kernel 特征

数据移动特征

对于大多数计算 kernel 来说,数据搬运所占的开销是大头。

内存访问模式:

  • 内存类别:global / local
  • 访问类型:load / store
  • the local and global strides along each thread axis in the array index
    • 也就是说,每次 gid(0), gid(1), lid(0), lid(1) 自增一的时候,对 array 数组访问的偏移要分别增加多少
  • the ratio of the number of element accesses to the number of elements accessed (access-to-footprint ratio, or AFR)
    • AFR = 1: every element in the footprint is accessed one time
    • AFR > 1: some elements are accessed more than once
      • 这样 Cache 就可能会对速度有加成了

文章中提到,解析形式的模型需要建模很多机器细节,比如 workgroup 调度,内存系统架构等,来达到和黑盒模型相似的精度。一个例子是

1
2
3
4
for (int k_out = 0; k_out <= ((-16 + n) / 16); ++k_out)
...
a_fetch[...] = a[n*(16*gid(1) + lid(1)) + 16*k_out + lid(0)];
b_fetch[...] = b[n*(16*k_out + lid(1)) + 16*gid(0) + lid(0)];

这个例子里面的内存访问模式如下:

Array Ratio Local strides Global strides Loop stride
a n/16 {0:1, 1:n} {0:0, 1:n*16} 16
b n/16 {0:1, 1:n} {0:16, 1:0} 16*n

这两个例子的性能差距在 5 倍左右。

With this approach, a universal model for all kernels on all hardware based on kernel-level features like ours could need a prohibitively large number of global memory access features and corresponding measurement kernels. This motivates our decision to allow proxies of “in-situ” memory accesses to be included as features, which in turn motivates our ‘work removal’ code transformation, discussed in Section 7.1.1. This transformation facilitates generation of microbenchmarks exercising memory accesses which match the access patterns found in specific computations by stripping away unrelated portions of the computation in an automated fashion.

Specifying Data Motion Features in the Model: 弄个 aLD, bLD, f_mem_access_tag

也可以手动指定,不用运行时测量:

1
2
3
4
5
6
7
8
model = Model(
"f_cl_wall_time_nvidia_geforce",
"p_f32madd * f_op_float32_madd + "
"p_f32l * f_mem_access_local_float32 + "
"p_f32ga * f_mem_access_global_float32_load_lstrides:{0:1;1:>15}_gstrides:{0:0}_afr:>1 + "
"p_f32gb * f_mem_access_global_float32_load_lstrides:{0:1;1:>15}_gstrides:{0:16}_afr:>1 + "
"p_f32gc * f_mem_access_global_float32_store"
)

显式语法格式如下:"f_mem_access_tag:<mem access tag>_<mem type>_<data type>_<direction>_lstrides:{<local stride constraints>}_gstrides:{<global stride constraints>}_afr:<AFR constraint>"

算术操作特征

特征:

  • 操作类型:加法、乘法、指数
  • 数据类型:float32, float64

本文中的工作不考虑整数算术特征,因为在模型考虑的 kernel 变体中,整数算术只用在了数组下标计算中。

同步特征

特征:

  • 局部同步障 (local barriers)
  • kernel 启动

这里 Local barriers 是 per work-item 的,然后根据实际程序同步的需要,可能需要进行乘以同时进行同步的 work item 数量。

简单来说就是,认为参与同步的 thread 越多越耗时。

Recall that the statistics gathering module counts the number of synchronizations encountered by a single work-item, so depending on how a user intends to model execution, they may need to multiply a synchronization feature like local barriers by, e.g., the number of work-groups, a feature discussed in the next section.

A user might incorporate synchronization features into this model as follows:

1
2
3
4
5
6
model = Model("f_cl_wall_time_nvidia_geforce",
"p_f32madd * f_op_float32_madd + "
...
"p_barrier * f_sync_barrier_local * f_thread_groups + "
"p_launch * f_sync_kernel_launch"
)

其他特征

  • Thread groups feature
    • 给定 workgroup count,进行不同 workgroup count 间启动时间补偿
  • OpenCL wall time feature
    • 给定 platform 和 device 下,执行 60 遍获得平均 walltime,作为输出特征
    • “We measure kernel execution time excluding any host-device transfer of data.”

一个完整的模型:

1
2
3
4
5
6
7
8
9
10
model = Model("f_cl_wall_time_nvidia_geforce",
"p_f32madd * f_op_float32_madd + "
"p_f32l * f_mem_access_local_float32 + "
"p_f32ga * f_mem_access_global_float32_load_lstrides :{0:1;1:>15}_gstrides:{0:0}_afr:>1 + "
"p_f32gb * f_mem_access_global_float32_load_lstrides :{0:1;1:>15}_gstrides:{0:16}_afr:>1 + "
"p_f32gc * f_mem_access_global_float32_store + "
"p_barrier * f_sync_barrier_local * f_thread_groups + "
"p_group * f_thread_groups + "
"p_launch * f_sync_kernel_launch"
)

校准模型参数

Work Removal Transformation: a code transformation that can extract a set of desired operations from a given computation, while maintaining overall loop structure and sufficient data flow to avoid elimination of further parts of the computation by optimizing compilers

Work Removal 变换会把 on-chip 工作从 kernel 中去掉,达成两方面目的:

  1. 测试 on-chip work 和 global memory access 各自占用时间,决定是否要进行 latency hiding
  2. 测试某种特殊访存模型的时间占用

Measurement kernel 设计

  • Global memory access
    • AFR = 1: Fully specified by local strides, global strides, data size
      • That is, patterns that do not produce a write race and not nested inside sequential loops
      • Performs global load from each of a variable number of input arrays using the specified access pattern
      • Each work-item then stores the sum of the input array values it fetched in a single result array
      • Params: data type, global memory array size, work-group dimensions, number of input arrays, thread index strides
    • AFR > 1:
      • Use Work Removal Tranformation to generate dedicated measurement kernel.
  • Arithmetic operations
    • First, have each work-item initialize 32 private variables of the specified data type
    • Then, perform a loop in which each iteration updates each variable using the target arithmetic operation on values from other variables
      • This is to create structural dependency
    • We unroll the loop by a factor of 64 and arrange the variable assignment order to achieve high throughput using the approach found in the Scalable HeterOgeneous Computing (SHOC) OpenCL MaxFlops.cpp benchmark (Danalis et al. 2010).
      • the 32 variable updates are ordered so that no assignment depends on the most recent four statements
        • 32 is used because it permits maximum SIMD lane utilization & prevent from spilling too many registers
      • we sum the 32 variable values and store the result in a global array according to a user-specified memory access pattern
        • (NOTE: The actual cost can be deduced by change the runcount of arithmetic ops)
        • include the global store to avoid being optimized away
  • Local memory access
    • Tags: data type, global memory array size, iteration count, and workgroup dimensions
      • Data type determines the local data stride
    1. each workitem initializes one element of a local array to the data type specified
    2. Then we have it perform a loop, at each iteration moving a different element from one location in the array to another.
      • We avoid write-races and simultaneous reads from a single memory location, and use an lid(0) stride of 1, avoiding bank conflicts.
    3. After the loop completes, each work-item writes one value from the shared array to global memory
  • Other features
    • executes a variable number of local barriers, to measure operation overlapping behaviour (Section 7.4)
    • Empty kernel launch, to measure kernel launching overhead

文章提出,Using a sufficiently high-fidelity model, we expect that users will be able to differentiate between latency-based costs of a single kernel launch and throughput-related costs that would be incurred in pipelined launches.

怎么做?

计算模型参数

采用最小二乘法来进行拟合,得到 feature 向量中给定 feature 的出现次数和总的运行时间的关系。

Operation Overlap 建模

Global memory 和 On-chip 的延迟之间是有可能互相隐藏的。

本文的建模基于简单的想法,即 $ \max (c_{onchip}, c_{gmem}) $,两类操作的时间求 $ \max $ 操作。

不过 $ \max $ 不是很可导,所以采用一个可微的近似函数来做,详情可以看论文。

论文阅读 | Learning from Shader Program Traces

简介

  • Program trace
    • In software engineering, a trace refers to the record of all states that a program visits during its execution, including all instructions and data.
    • 本文提到的 Shader program trace,只包括中间结果 (data),而不包括程序序列 (instruction)。

Since the fragment shader program operates independently per pixel, we can consider the full program trace as a vector of values computed at each pixel – a generalization from simple RGB.

方法

  • 输入是用(嵌入到 Python 的) DSL 写的 fragment procedural shader program,翻译成 Tensorflow 程序
    • 可以同时输出渲染好的图片和生成的 program trace
    • 分支展开、循环 unroll
    • These policies permit us to express the trace of any shader as a fixed-length vector of the computed scalar values, regardless of the pixel location

输入特征化简

  • 编译器优化
    • 忽略常量值、计算图上重复的节点,因为其在不同 pixel 位置的运行结果应该高度统一
  • 不生成内建函数的 trace
  • 检测并筛除迭代改进模式的循环中的中间 trace 结果
    • 比如,raymarching 找 closest intersection 的迭代
  • 均匀的特征下采样
    • The most straightforward strategy is to subsample the vector by some factor n, retaining only every nth trace feature as ordered in a depth first traversal of the compute graph
  • 其它采样方案 (都不太好用)
    • clustering
    • loop subsampling
    • first or last
    • mean and variance

We first apply compiler optimizations, then subsample the features with a subsampling rate that makes the trace length be most similar to a fixed target length.

For all experiments, we target a length of 200, except where specifically noted such as in the simulation example.

After compiling and executing the shader, we have for every pixel: a vector of dimension N: the number of recorded intermediate values in the trace

特征白化

主要是为了解决 shader trace 里面的异常值,防止干扰训练和推理。用的是 Scaling + clamping。

  • Check if the distribution merits clamping
    • If N <= 10, no need to clamp
    • Else, do clamp
      • Discard NaN, Inf, -Inf
      • let $P_0$ = Lowest p’th percentile, $P_1$ = highest p’th percentile, superparam $ \gamma $
      • Clamp to $ [P_0 − \gamma(P_1− P_0), P_1 + \gamma(P_1 − P_0)] $
    • Do rescale
      • for each intermediate feature, rescale the clamped values to the fixed range $ [-1,1] $
      • Record the bias and scale used (in rescaling)

The scale and bias is recorded and used in both training and testing, but the values will be clamped to range
[-2, 2] to allow data extrapolation.

感觉有点乱…

网络

结构

  • 1x1 Conv + Feature Reduction (N = 200 -> K = 48)
  • 1x1 Conv * 3
  • Dilated Convolution (1, 2, 4, 8, 1)
  • 1x1 Conv * 3
  • 1x1 Conv + Feature Reduction (K = 48 -> 3, that is, RGB color output)

损失函数

$ L_b = L_c + \alpha L_p $

下面还有个 Appendix D,里面有实验的 GAN 的 loss

训练策略

结果展示

和一个 Baseline 方法 RGBx 对比,这个 Baseline 用的手挑特征 normal, depth, diffuse, specular color (where applicable) 来作为输入进行学习。

Denoising fragment shaders

目标是用 1spp 图像来学习 1000spp 的 reference image。

Reconstructing simplified shaders

这个任务是,从简化后的 Shader 的运行结果中,重建原来 Shader 的运行结果。

简化 Shader 采用的是 Loop perforation 和 Genetic Programming Simplification。

用两个 Conditional GAN,分别称为 Spatial GAN 和 Temporal GAN,一个用来从 1spp 的图 $ c_x $ 生成 Ground Truth (原来的 Shader 运行结果) $ c_y $,另一个用来从前面三帧的 1spp 输出 + 前面两帧的 Spatial GAN 的生成器的输出来生成下一帧,也就是用序列 $ \tilde {c_x} $ 生成序列 $ \tilde {c_y} $。

GAN related:

Postprocessing filters

学习一些后处理效果的 Shader,如 edge-aware sharpening filter 和 defocus blur 效果。

Learning to approximate simulation

学习一些进行模拟的 Shader 将来的运行结果。

Trace 有效性分析

这里主要做了两件事:

  1. 哪些 Input feature 比较重要?
    • 这里作者采用求 Loss 关于 input trace feature 的一阶导数来评价重要性
  2. 挑一个 Subset 来做训练?
    • 给定 m 个 feature 的训练 budget,如果要评价任意的 subset,即从 N 个里面抽 m 个来做训练的话,开销太大
      • Oracle: 按 1 中所述重要性评分的前 m 个 input trace feature
      • Opponent: 按 1 中所述重要性评分的后 m 个 input trace feature
      • Uniform: 随便挑 m 个
    • 发现 Oracle > Opponent > Uniform
  3. 多个 Shader 一起学习
    • 多个 Shader 一起学习降噪任务,感觉就像训练一个真·denoiser
一个示例 Vulkan 程序的全流程记录

简介

一些有用的链接:

本文主要分析 glfw 库的 tests/triangle-vulkan.c 文件。

流程

Update 2023-02-13: 补上了漏掉的创建逻辑设备的一步 vkCreateDevice

  • demo_init
    • demo_init_connection
      • glfwSerErrorCallback
      • gladLoadVulkanUserPtr: 设定 glad 使用 glfwGetInstanceProcAddress 来装载所有的 Vulkan 函数指针地址
    • demo_init_vk
      • 启用验证层:
        • vkEnumerateInstanceLayerProperties
        • demo_check_layers: 检查需要的验证层集合是否存在
      • glfwGetRequiredInstanceExtensions: 获得需要的平台 Surface 扩展
      • 准备启用的 Instance 扩展列表
        • VK_EXT_debug_report
        • VK_KHR_portability_enumeration
      • vkCreateInstance
      • vkEnumeratePhysicalDevices
      • 检查设备是否支持 VK_KHR_swapchain
        • vkEnumerateDeviceExtensionProperties
      • vkCreateDebugReportCallbackEXT
      • vkGetPhysicalDeviceProperties
      • vkGetPhysicalDeviceQueueFamilyProperties
      • vkGetPhysicalDeviceFeatures
  • demo_create_window
    • glfwWindowHint
    • glfwCreateWindow
    • glfwSetWindowUserPointer
    • glfwSetWindowRefreshCallback
    • glfwSetFramebufferSizeCallback
    • glfwSetKeyCallback
  • demo_init_vk_swapchain
    • glfwCreateWindowSurface
      • 内部调用 vkCreateWin32SurfaceKHR
    • 查找支持 Present 和 Graphics 的 Queue,需要是同一个 Queue
      • vkGetPhysicalDeviceSurfaceSupportKHR
      • queueFlags & VK_QUEUE_GRAPHICS_BIT
    • demo_init_device
      • vkCreateDevice: 创建 logical device
        • VkDeviceCreateInfo
          • .pQueueCreateInfos
            • .queueFamilyIndex
            • .queueCount
            • .pQueuePriorities
          • .ppEnabledLayerNames
          • .ppEnabledExtensionNames: 要启用的设备扩展

            似乎把 Instance 扩展的名字扔进去也行?

    • vkGetDeviceQueue
    • 选择一个最优的 Surface format
      • vkGetPhysicalDeviceSurfaceFormatsKHR
    • vkGetPhysicalDeviceMemoryProperties
  • demo_prepare
    • 创建 Command Pool
      • vkCreateCommandPool
    • 分配一个 Command Buffer
      • vkAllocateCommandBuffers
        • VkCommandBufferAllocateInfo:
          • .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY
          • .commandBufferCount = 1
    • demo_prepare_buffers
      • 检查 Surface Capabilities 和 Present Modes
        • vkGetPhysicalDeviceSurfaceCapabilitiesKHR
        • vkGetPhysicalDeviceSurfacePresentModesKHR
      • 创建交换链
        • 计算 Swapchain Image Extent
        • .preTransform 使用 VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR,如果没有则使用当前 Surface Transform
        • .minImageCount 使用 Surface Capabilities 的 minImageCount
        • .presentMode 选择 VK_PRESENT_MODE_FIFO_KHR
        • vkCreateSwapchainKHR
        • 如果有老的交换链: vkDestroySwapchainKHR
        • vkGetSwapchainImagesKHR 拿到 VkImage 格式的交换链图像
        • 为每个交换链图像调用 vkCreateImageView 创建 Color Attachment View

          Componet Swizzle: TODO check spec

    • demo_prepare_depth
      • vkCreateImage 创建 depth image
        • .arrayLayers 可以指定 texture array 的 dimension
      • vkGetImageMemoryRequirements 获得 image 的内存要求
      • 选择内存大小和内存类型
        • memory_type_from_properties : todo check this
      • vkAllocateMemory 分配 image 所需内存,返回 VkDeviceMemory
      • vkBindImageMemory 将分配的 VkDeviceMemory 绑定到 VkImage
      • demo_set_image_layout
        • 如果 demo->setup_cmd 为空,则
          • 调用 vkAllocateCommandBuffers 从 demo->cmd_pool 中分配 VK_COMMAND_BUFFER_LEVEL_PRIMARY 的 Buffer
          • vkBeginCommandBuffer
        • 准备 Image Memory Barrier
          • VkImageMemoryBarrier
            • .srcAccessMask = 0
              • 不需要给 src stage 的任何读/写操作 made coherent
            • .dstAccessMask:
              • 对于 VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,设置为 VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT
            • .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,也就是垃圾数据
            • .newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
        • 录制 Pipeline Barrier
          • vkCmdPipelineBarrier
            • srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,也就是 wait for nothing
            • dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,也就是任何下面的指令在开始前都需要等待 Barrier 执行完
            • 同时传入前面的 Image Mmeory Barrier
      • vkCreateImageView 创建深度缓冲对应图像的 ImageView
    • demo_prepare_textures
      • vkGetPhysicalDeviceFormatProperties 获得 VK_FORMAT_B8G8R8A8_UNORM 的 VkFormatProperties
      • 对于每张 texture

        texture_object 来管理每个 texture

        • VkSampler sampler
        • VkImage iamge;
        • VkImageLayout imageLayout;
        • VkDeviceMemory mem;
        • VkImageView view;
        • int32_t tex_width, tex_height;
        • 如果 sampler 支持(对此种 format 的)线性分块 (props.linearTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT)
          • demo_prepare_texture_image with required_props = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
            • vkCreateImage
            • vkGetImageMemoryRequirements
            • memory_type_from_properties
              • 对设备支持的每种内存类型,枚举其是否符合前面 required_props 的要求
            • vkAllocateMemory
            • vkBindImageMemory
            • 如果 memory type 有性质 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
              • vkGetImageSubresourceLayout
              • vkMapMemory: 映射到地址空间
              • 填充之
              • vkUnmapMemory
            • 设置 image layout (前面分析过)
              • VK_IMAGE_LAYOUT_PREINITIALIZED -> VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
              • demo_set_image_layout
        • 如果 sampler 不支持对此种 format 的线性分块,但支持 optimal 分块 (props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT)
          • 分别准备 host coherent 和 host visible 的 staging texture 和 GPU device local 的 texture
            • demo_prepare_texture_image * 2
              • 这里 device local 的显然没能力初始化
            • 注意 memory props
          • 改 layout 以便使用 transfer 命令
            • staging texture: VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
            • device local texture: VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
          • vkCmdCopyImage
          • 将 device local texture 的 layout 改回来
            • demo_set_image_layout
          • demo_flush_init_cmd: 同步方式 flush setup cmd
            • vkEndCommandBuffer
            • vkQueueSubmit
              • no wait / signal semaphores
            • vkQueueWaitIdle
            • vkFreeCommandBuffers
            • demo->setup_cmd = VK_NULL_HANDLE
          • demo_destroy_texture_image 销毁 staging texture
        • 创建对应的 sampler 和 Image View
          • vkCreateSampler
          • vkCreateImageView
    • demo_prepare_vertices

      这里直接用了 Host visible & Host coherent 的 memory 作为 vertex buffer
      而不是 Device local 的,然后单开 staging buffer 做拷贝.

      应该是偷懒了.jpg

      • vkCreateBuffer
        • with .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
      • vkGetBufferMemoryRequirements
      • memory_type_from_properties
      • vkAllocateMemory
      • vkMapMemory
      • vkUnmapMemory
      • vkBindBufferMemory
      • 配置一些结构体
        • VkPipelineVertexInputStateCreateInfo
          • VkVertexInputBindingDescription
          • VkVertexInputAttributeDescription
    • demo_prepare_descriptor_layout
      • vkCreateDescriptorSetLayout
        • VkDescriptorSetLayoutCreateInfo
        • .pBindings = &layout_binding
          • layout_binding: 设置每个 binding 的位置都放什么 - 可以为数组
            1
            2
            3
            4
            5
            6
            7
            const VkDescriptorSetLayoutBinding layout_binding = {
            .binding = 0,
            .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            .descriptorCount = DEMO_TEXTURE_COUNT,
            .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
            .pImmutableSamplers = NULL,
            };
            See also: https://vkguide.dev/docs/chapter-4/descriptors/
      • vkCreatePipelineLayout
        • VkPipelineLayoutCreateInfo: demo->pipeline_layout
          • 指定了到 Descriptor Set Layouts 的数量和数组指针
    • demo_prepare_render_pass
      • vkCreateRenderPass
        • VkRenderPassCreateInfo
          • .pAttachments: VkAttachmentDescription
            • [0]: Color Attachment
              • .samples = VK_SAMPLE_COUNT_1_BIT 图像的 sample 数
              • .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR color & depth 内容在 subpass 开始时如何处理
              • .storeOp = VK_ATTACHMENT_STORE_OP_STORE color & depth 内容在 subpass 结束后如何处理
              • .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE stencil 内容在 subpass 开始时如何处理
              • .stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE stencil 内容在 subpass 结束时如何处理
              • .initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL subpass 开始前 image subresource 的 layout
              • .finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL subpass 结束后 image subresource 将会被自动转换到的 layout
            • [1]: Depth Stencil Attachment
              • .format = demo->depth.format
              • .samples = VK_SAMPLE_COUNT_1_BIT
              • .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR
              • .storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE
              • .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE
              • .stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE
              • .initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
              • .finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
          • .pSubpasses: VkSubpassDescription

            A single render pass can consist of multiple subpasses. Subpasses are subsequent rendering operations that depend on the contents of framebuffers in previous passes, for example a sequence of post-processing effects that are applied one after another. If you group these rendering operations into one render pass, then Vulkan is able to reorder the operations and conserve memory bandwidth for possibly better performance. Render passes - Vulkan Tutorial

            • .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS 该 subpass 支持的 pipeline 类型
            • .pInputAttachments = NULL
            • .pColorAttachments = &color_reference
              • VkAttachmentReference {.attachment = 0, .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}
                引用到上面的 [0]
            • .pDepthStencilAttachment = &depth_reference
              • VkAttachmentReference {.attachment = 1, .layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}
                引用到上面的 [1]
          • .pDependencies: VkSubpassDependency 有多个 subpass 时指定 subpass 间的读写依赖关系

            和 vkCmdPipelineBarrier + VkMemoryBarrier 差不多,区别只是同步作用域限于指定的 subpass 间,而非所有在前在后的操作 (Vulkan Spec)

    • demo_prepare_pipeline
      • vkCreatePipelineCache: (optional for pipeline creation)

        主要用来供实现缓存编译好的 Pipeline; 可以使用 allocator 限制其缓存数据的大小; 可以创建时导入之前 (应用程序) 的 Cache 等

      • vkCreateGraphicsPipelines
        • VkGraphicsPipelineCreateInfo
          • .layout = demo->pipeline_layout
          • .pVertexInputState: VkPipelineVertexInputStateCreateInfo
            • 已经在 demo_prepare_vertices 中准备好
          • .pInputAssemblyState: VkPipelineInputAssemblyStateCreateInfo
            • .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
          • .pRasterizationState: VkPipelineRasterizationStateCreateInfo
            • .polygonMode = VK_POLYGON_MODE_FILL
            • .cullMode = VK_CULL_MODE_BACK_BIT
            • .frontFace = VK_FRONT_FACE_CLOCKWISE
              • front-facing triangle orientation to be used for culling
            • .depthClampEnable = VK_FALSE
              • 不启用深度截断
            • .rasterizerDiscardEnable = VK_FALSE
              • 是否在光栅化阶段前立即丢弃片元
            • .depthBiasEnable = VK_FALSE
            • .lineWidth = 1.0f
              • 光栅化线段宽度
          • .pColorBlendState: VkPipelineColorBlendStateCreateInfo
            • .pAttachments: VkPipelineColorBlendAttachmentState,对每个 color attachment 定义 blend state
              • [0]
                • .colorWriteMask = 0xf
                • .blendEnable = VK_FALSE
                  • 不启用 Blending,直接写入
          • .pMultisampleState: VkPipelineMultisampleStateCreateInfo
            • .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT
            • .pSampleMask = NULL
          • .pViewportState: VkPipelineViewportStateCreateInfo
            • .viewportCount = 1
            • .scissorCount = 1
            • 不过这里用的 Dynamic State,也就是 Viewport 和 Scissor 的信息是在录制 Command Buffer 时提供的,创建 Pipeline 时不提供
              • 详情看 .pDynamicState
          • .pDepthStencilState: VkPipelineDepthStencilStateCreateInfo
            • .depthTestEnable = VK_TRUE
            • .depthWriteEnable = VK_TRUE
            • .depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL
            • .depthBoundsTestEnable = VK_FALSE
            • .stencilTestEnable = VK_FALSE 下面都是 Stencil test 的参数
            • .back.failOp = VK_STENCIL_OP_KEEP
            • .back.passOp = VK_STENCIL_OP_KEEP
            • .back.compareOp = VK_COMPARE_OP_ALWAYS
            • .front = ds.back
          • .pStages: VkPipelineShaderStageCreateInfo
            • [0]
              • .stage = VK_SHADER_STAGE_VERTEX_BIT
              • .pName = "main"
              • .module = demo_prepare_vs(demo)
                • Call demo_prepare_shader_module with vert SPIR-V code
                  • vkCreateShaderModule with size_t codeSize & uint32_t *pCode
            • [1]
              • .stage = VK_SHADER_STAGE_FRAGMENT_BIT
              • .pName = "main"
              • .module = demo_prepare_fs(demo)
                • Similar with above
          • .pDynamicState: VkPipelineDynamicStateCreateInfo
            • .pDynamicStates = dynamicStateEnables
              • 启用了 VK_DYNAMIC_STATE_VIEWPORTVK_DYNAMIC_STATE_SCISSOR
          • .renderPass: VkRenderPass
            传入之前创建的 VkRenderPass
      • vkDestroyPipelineCache
      • vkDestroyShaderModule * 2
        • 删除 vs 和 fs 的两个刚才创建的 Shader Module (demo_prepare_vs / demo_prepare_fs)
    • demo_prepare_descriptor_pool
      • vkCreateDescriptorPool
        • VkDescriptorPoolCreateInfo
          • .pPoolSizes = &type_count
            • VkDescriptorPoolSize
              • .type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
              • .descriptorCount = DEMO_TEXTURE_COUNT
    • demo_prepare_descriptor_set
      • vkAllocateDescriptorSets:按 Descriptor Set Layouts 从 Descriptor Pool 中分配 Descriptor Sets
        • .pSetLayouts = &demo->desc_layout
        • .descriptorPool = demo->desc_pool
      • vkUpdateDescriptorSets
        支持 Write 和 Copy 两种形式的 Descriptor Set 更新请求
        • VkWriteSescriptorSet
          • .dstSet = demo->desc_set 刚分配的 Descriptor Set
          • .descriptorCount = DEMO_TEXTURE_COUNT
          • .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
          • .pImageInfo = tex_descs
            • VkDescriptorImageInfo: 具体的 Descriptor 内容
              • .sampler = demo->textures[i].sampler
              • .imageView = demo->textures[i].view
              • .imageLayout = VK_IMAGE_LAYOUT_GENERAL

                感觉这里应该是选对应的才对,不知道这样可以不可以

    • demo_prepare_framebuffers
      • 创建 demo->swapchainImageCount 个 VkFramebuffer
        • vkCreateFramebuffer
          • VkFramebufferCreateInfo
            • .renderPass = demo->renderpass
            • .pAttachments: VkImageView[]
              • [0]: Color Attachment, demo->buffers[i].view
                • That is, the swapchain image view
              • [1]: Depth Attachment
                • demo->depth.view
            • .width, .height
            • .layers = 1

              正如 VkImage 创建时也可以选择多 layer 一样,这里也可以;不过 Shader 默认写入第一层,除了 Geometry Shader

              多 layer 的 Image / Framebuffer 在 Shader 里面是用的 texture array 的语法来访问的

  • demo_run
    • glfwWindowShouldClose: 检测窗口的 closing 标志
    • glfwPollEvent
    • demo_draw
      • vkCreateSemaphore: imageAcquiredSemaphore
      • vkCreateSemaphore: drawCompleteSemaphore
      • vkAcquireNextImageKHR

        这里有一个问题,这里返回并不意味着 Present 完成 (推荐做法是 Present 设置 Semaphore,然后等 Semaphore)

        那么,什么情况下这里会 block?
        也可以参考 Let’s get swapchain’s image count straight - StackOverflow

        • timeout = UINT64_MAX
        • semaphore = imageAcquiredSemaphore
        • pImageIndex = &demo->current_buffer: index of the next image to use
          • 完成后会 signal 该 semaphore
        • 返回值
          • VK_ERROR_OUT_OF_DATE_KHR
            • demo_resize: 处理 resize 情况:Destroy everything
              • vkDestroyFramebuffer
              • vkDestroyDescriptorPool
              • vkFreeCommandBuffers
              • vkDestroyCommandPool
              • vkDestroyPipeline
              • vkDestroyRenderPass
              • vkDestroyPipelineLayout
              • vkDestroyDescriptorSetLayout
              • vkDestroyBuffer (vertex buffer)
              • vkFreeMemory (vertex buffer memory)
              • vkDestroyImageView
              • vkDestroyImage
              • vkDestroySampler
              • call demo_prepare
            • demo_draw: 重复调用一下自己
          • VK_SUBOPTIMAL_KHR: 不是最优,但是也能 present,所以不管
      • demo_flush_init_cmd: 同步方式 flush setup cmd
        • vkEndCommandBuffer
        • vkQueueSubmit
          • no wait / signal semaphores
        • vkQueueWaitIdle
        • vkFreeCommandBuffers
        • demo->setup_cmd = VK_NULL_HANDLE
      • demo_draw_build_cmd
        • vkBeginCommandBuffer: demo->draw_cmd
        • vkCmdPipelineBarrier
          • Execution barrier 部分
            • srcStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,也就是 wait for everything
            • dstStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT (Specifies no stage of execution)

              VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT is equivalent to VK_PIPELINE_STAGE_ALL_COMMANDS_BIT with VkAccessFlags set to 0 when specified in the first synchronization scope, but specifies no stage of execution when specified in the second scope.

          • Memory barrier 部分: 对 color attachment 做 layout transition
            • VK_IMAGE_LAYOUT_UNDEFINED -> VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
            • .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT
        • vkCmdBeginRenderPass with VK_SUBPASS_CONTENTS_INLINE

          VK_SUBPASS_CONTENTS_INLINE specifies that the contents of the subpass will be recorded inline in the primary command buffer, and secondary command buffers must not be executed within the subpass.

          • VkRenderPassBeginInfo
            • .renderPass
            • .framebuffer - 选择当前的 framebuffer,我们有 swapchainImageCount
            • .renderArea
              • .offset.{x, y}
              • .extent.{width, height}
            • .pClearValues = clear_values (VkClearValue)

              这里是和 RenderPassCreateInfo 指定的 attachments 相对应的

              pClearValues is a pointer to an array of clearValueCount VkClearValue structures containing clear values for each attachment, if the attachment uses a loadOp value of VK_ATTACHMENT_LOAD_OP_CLEAR or if the attachment has a depth/stencil format and uses a stencilLoadOp value of VK_ATTACHMENT_LOAD_OP_CLEAR. The array is indexed by attachment number. Only elements corresponding to cleared attachments are used. Other elements of pClearValues are ignored.

              • [0] = {.color.float32 = {0.2f, 0.2f, 0.2f, 0.2f}}
              • [1] = {.depthStencil = {demo->depthStencil, 0}}
                • demo->depthStencil 用来加一个“无形的墙”
        • vkCmdBindPipeline
          • pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS
        • vkCmdBindDescriptorSets
          • layout = demo->pipeline_layout
            Recall: Pipeline layout <= Descriptor Set Layouts
          • Descriptor Sets
        • vkCmdSetViewport
          • VkViewport
            • .height, .width, .minDepth, .maxDepth
        • vkCmdSetScissor
          • VkRect2D
            • .extent.{width, height}
            • .offset.{x, y}
        • vkCmdBindVertexBuffers

          https://github.com/SaschaWillems/Vulkan/blob/master/examples/instancing/instancing.cpp 可能会印象更深刻

          • firstBinding 参数用于 (CPU 端) 指定绑定到哪里
        • vkCmdDraw
          • vertexCount = 3
          • instanceCount = 1
          • firstVertex = 0
          • firstInstance = 0
        • vkCmdEndRenderPass
        • vkCmdPipelineBarrier
          • Execution barrier:
            • srcStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,也就是 wait for everything
            • dstStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT (Specifies no stage of execution)
          • Memory barrier:

            正如 transfer,present 也需要 layout 改变

            • .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT
            • .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT
            • .oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
            • .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
        • vkEndCommandBuffer: demo->draw_cmd
      • vkQueueSubmit
        • .pCommandBuffers = &demo->draw_cmd
        • .pWaitSemaphores = &imageAcquiredSemaphore
        • .pWaitDstStageMask = &pipe_stage_flags
          • pWaitDstStageMask is a pointer to an array of pipeline stages at which each corresponding semaphore wait will occur.
          • 这里设置成了 VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT
          • 所以,相当于啥也没等
        • .pSignalSemaphores = &drawCompleteSemaphore
      • vkQueuePresentKHR
        • VkPresentInfoKHR
          • .pWaitSemaphores = &drawCompleteSemaphore
          • .pSwapchains = &demo->swapchain
            • 可以多个,用来支持多个 swapchain 用一个 queue present 操作进行 present
          • .pImageIndices = &demo->current_buffer
        • 返回值
          • VK_ERROR_OUT_OF_DATE_KHR
            • demo_resize
          • VK_SUBOPTIMAL_KHR
            • 啥事不干
      • vkQueueWaitIdle
      • vkDestroySemaphore: imageAcquiredSemaphore
      • vkDestroySemaphore: drawCompleteSemaphore
    • demo->depthStencil 周期改变
    • vkDeviceWaitIdle
    • 如果到了指定的帧数,则 glfwSetWindowShouldClose
  • demo_cleanup
    • 删除一万个东西 (literally)
    • glfwDestroyWindow
    • glfwTerminate
论文阅读 | 数据驱动的 PRT

本文省略了一大堆细节,详情参见论文。

TODO: 整理清楚各个维数,因为原论文也不甚详细;

更新后的版本会放到 这里,如果有。

Recap: Precomputed Radiance Transfer

本节主要参考GAMES 202 - 高质量实时渲染课程的 Lecture 6 和 Lecture 7

考虑渲染方程

$$ L({\bf o}) = \int_{\mathcal{H}^2} L({\bf i}) \rho({\bf i}, {\bf o}) V({\bf i}) \max(0, {\bf n} \cdot {\bf i}) d {\bf i} $$

其中

  • $ {\bf i}, {\bf o} $ 为入射和出射方向
  • $ L({\bf i}), L({\bf o}) $ 为入射和出射 radiance
    • 此处省略了作为参数的 shading point 位置 $ \bf x $,下同
  • $ \rho $ 为 BRDF 函数
  • $ V $ 为 Visibility 项

将 $ L({\bf i}) $ 项用级数的有限项进行近似,即

$$ L({\bf i}) \approx \sum_{i=1}^{n} l_i B_i({\bf i}) $$

其中 $ B_i: S^2 \to \mathbb{R} $ 为基函数

带入得到

$$ \begin{aligned} L({\bf o}) &= \int_{\mathcal{H}^2} L({\bf i}) \rho({\bf i}, {\bf o}) V({\bf i}) \max(0, {\bf n} \cdot {\bf i}) d {\bf i} \\ &\approx \sum_i l_i \int_{\mathcal{H}^2} B_i({\bf i}) \rho({\bf i}, {\bf o}) V({\bf i}) \max(0, {\bf n} \cdot {\bf i}) d {\bf i} \\ &= \sum_i l_i T_i({\bf o}) \end{aligned} $$

这里把上面的积分 (“Light transport term”) 记作 $ T_i $.

这里继续进行展开

$$ T_i({\bf o}) \approx \sum_{j=1}^{m} t_{ij} B_j({\bf o}) $$

所以我们得到

$$ \begin{aligned} L({\bf o}) &\approx \sum_i l_i T_i({\bf o}) \\ &\approx \sum_i l_i \left( \sum_j t_{ij} B_j({\bf o}) \right) \\ &\approx \sum_j \left( \sum_i l_i t_{ij} \right) B_j({\bf o}) \\ \end{aligned} $$

也就是说

$$ L({\bf o}) \approx \begin{bmatrix} l_1 & ... & l_n \end{bmatrix} \begin{bmatrix} t_{11} & ... & t_{1m} \\ \vdots & & \vdots \\ t_{n1} & ... & t_{nm} \end{bmatrix} \begin{bmatrix} B_1({\bf o}) \\ \vdots \\ B_m({\bf o}) \end{bmatrix} $$

那么,PRT 的框架就大致如下

  1. 预计算

    • 对每个可能的 shading point $ {\bf x} $
      • 计算该物体的环境光在基函数下对应的系数 $ l_i $
      • 计算该物体光传输展开系数 $ t_{ij} $

    当然,对于 Image based lighting,一般认为 $ L({\bf i}, {\bf x}) \approx L({\bf i}) $,那某些东西就不需要 per-shading point 存储

  2. 运行时

    • 根据视角 $ {\bf o} $ 和位置 $ {\bf x} $ 来读取对应的向量并计算

对于 Diffuse 物体,$ \rho({\bf i}, {\bf o}) $ 是常数,所以不需要继续展开 $ T_i $ 项

Remarks from paper: PRT methods bake the transport matrix using implicit light sources defined by the illumination basis.
Those light sources shade the asset with positive and negative radiance values. Hence, a dedicated light transport algorithm is used for them.

本文思路

本文的框架只考虑漫反射,虽然结果上对于不是特别 Glossy 的材质应该都可以应用。

框架上的思路就是

  • 间接光 $ L_i({\bf x}; t) $ 和直接光 $ L_d({\bf i}, {\bf x}; t) $ 之间存在线性关系
  • 框架:
    • 将 $ {\bf x} $ 和 $ i \times t $ 所在空间分别做一离散化,得到 $ I = MD $
      • 相当于挑了一组基,每个基内部由同一个光照条件下各个位置的 $ L_d $ 组成
    • 对于给定的光照条件 $ x $ (各个位置 $ L_d $的值构成的列向量) ,如何求解 $ L_i $ ?
      • 首先把 $ x $ 分解到该 $ D $ 基下,得到系数向量 $ c = (D^T D)^{-1} D^T x $
      • 每个 $ D $ 基我们都存储有对应的输出,所以结果 $ y = Mx = I(D^T D)^{-1} D^T x $
  • 近似:
    • 对 $ I $ 进行 SVD 分解并保留前 $ k $ 项,得到近似矩阵 $ I = U \Sigma V^T \approx U_n \Sigma_n V_n^T $
    • $ y \approx U_n (\Sigma_n V_n^T) (D^T D)^{-1} D^T x $
      • let $ M_n = (\Sigma_n V_n^T) (D^T D)^{-1} D^T $
    • 存储 $ U_n $ 和 $ M_n $
  • 运行时:
    • 用 G-Buffer 得到 $ \mathcal{X}_D $ 空间上的各 $ L_d({\bf i}, {\bf x}; t) $ 的值
    • 计算 $ y = U_n M_n x $ 的值

估计光传输矩阵

给定环境光条件 $ t \in \mathcal T $,那么在物体表面 $ {\bf x} $ 处,漫反射光传输方程的形式如下

$$ L_i({\bf x}; t) = \frac{1}{2 \pi}\int_{\mathcal{H}^2} L_d({\bf i}, {\bf x}; t) V({\bf i}, {\bf x}) \max(0, {\bf n} \cdot {\bf i}) d {\bf i} $$

其中,$ L_i({\bf x}; t) $ 被称为间接光, $ L_d({\bf i}, {\bf x}; t) $ 被称为直接光

$ L_d({\bf i}, {\bf x}; t) $ 不考虑环境和物体 inter-reflection; 推导中可以先忽略,虽然实际上对于有 inter-transmission 的情况应该也是可以应用的

现在将 $ {\bf x} $ 和 $ i \times t $ 所在空间分别做一离散化,得到 $ \mathcal{X}_D $ 和 $ \mathcal{T}_D $ 两有限维空间,那么在这两个空间上, $ L_d $ 和 $ L_i $ 都可以表示为矩阵形式,这里规定每一列的元素在同一个环境光条件 $ {\bf i}, t $ 上。

比如说,都在环境光为某点光源照射的情况; $ L_d({\bf i}, {\bf x}; t) $ 的 $ {\bf i} $ 一般意义上是依赖 $ t $ 的

记得到的两个矩阵为 $ D $ 和 $ I $,则

$$ I_k = f(D_k) \quad \forall k \in [0, |\mathcal{T}_D|] $$

从前面可以看到,这里的 $f$ 是线性算子 (是嘛?),所以

$$ I = MD $$

又假设我们离散 $ \mathcal T $ 空间离散的很好,那么对任意的环境光条件,直接光向量 $ x $ 都可以表示成 $ D $ 的线性组合,满足

$$ x = Dc $$

左右乘 $ M $ 得到

$$ Mx = MDc = Ic $$

也就是说 $x$ 产生的间接光照可以用 $I$ 中列向量的线性组合来表示

因为 $ x = Dc $,假设 $ D^T D $ 可逆,那么用左逆得到

$$ c = (D^T D)^{-1} D^T x $$

那么

$$ y = Mx = Ic = I (D^T D)^{-1} D^T x $$

这样就给出了任意直接光经过光传输的结果

间接光基函数

我们认为,间接光所对应的空间的秩比较低,所以用 SVD 分解然后保留前 $ n $ 项

$$ I = U \Sigma V^T \approx U_n \Sigma_n V_n^T = U_n C_n $$

其中记 $ C_n = \Sigma_n V_n^T $

带回去,得到任意直接光组合经过光传输方程的近似结果

$$ \begin{aligned} y &\approx U_n C_n (D^T D)^{-1} D^T x \\ &\approx U_n M_n x \end{aligned} $$

其中 $ M_n = C_n (D^T D)^{-1} D^T $

直接光编码

如果有需要的话,可以考虑 SH 基函数,详见文章

对比经典 PRT

First, because classical PRT restricts the frequency content of the incoming lighting, we can see that the directional light leaks behind the object. Our method does not restrict the frequency content of incoming light but rather the space of possible indirect illumination. Hence, we can better reproduce such lighting scenario.

Furthermore, classical PRT is performed on the vertices of the asset. This can cause interpolation artifacts when the asset is poorly tessellated, and it also links performance to the vertex count. Since we rely on a meshless approach, we are free of issues.

局限

Sparse Illumination Measurement. As shown in Section 3.3, the sampling of the measurement points is linked to the achievable lighting dimensionality. Thus, it needs to be sufficiently dense to reproduce the space of observable lighting configurations. It follows that a lighting scenario mixing many light types might require a denser sampling.

No Directionality. We reconstruct a diffuse appearance when reconstructing indirect illumination. However, since our method does not depend on the encoding of the measured indirect illumination, it can be extended to reconstruct glossy appearances e.g. directional distributions using directional sampling or any basis such as Spherical Harmonics. However, our method is likely to be restricted to low frequency gloss here and will not work to render specular reflections.

Large Assets. Our solution is not designed to handle assets such as levels in a game. Because we handle light transport globally and reduce it with a handful of basis functions, we cannot reconstruct the interconnected interiors or large environments in which the combinatorics of possible illumination is large. For such case, our method would require to be extended to handle modular transfer between disjoint transport solutions (Similar to Loos et al. [2011]).

论文阅读 | 连续多重重要性采样

简介

本篇文章扩展了 Veach 在 1995 年提出的、用于 Monte Carlo 多重重要性采样 (Multiple Importance Sampling),将其推广到了具有无限连续采样策略的情况。

多重重要性采样 (MIS)

本方法比较详细的讨论可以参考 Optimally Combining Sampling Techniques
for Monte Carlo Rendering
这篇 SIGGRAPH’95 的论文,是 Veach 和 Guibas 很高引用的文章之一。

也可以参考 Importance Sampling | PBR Book 3rd,不过里面没有证明。

对于积分

$$ I = \int_\Omega f(x) dx $$

我们希望用 Monte Carlo 采样的方法进行积分值的估计。

多重重要性采样 (Multiple Importance Sampling, MIS) 的大致思路如下:有 $ m $ 个采样策略,每个采样策略都可以对样本空间 $ \Omega $ 进行采样,并且每种策略都有概率密度函数 $ p_i(x) $。

对于 Multi-sample MIS,要分别使用每种采样策略独立采样 $ n_i $ 次,获得总计 $ \sum_{i=1}^{m} n_i $ 个采样,然后使用如下的式子进行积分的估计:

$$ \langle I \rangle_{mis} = \sum_{i=1}^m \frac{1}{n_i} \sum_{j=1}^{n_i} \frac{w_i(x_{i,j}) f(x_{i, j})}{p_i(x_{i,j})} $$

其中 $ x_{i, j} $ 表示第 $ i $ 个采样策略第 $ j $ 次采样获得的值,$ w_i(x) $ 为 $ m $ 个与 MIS 相关的权重函数。

首先可以证明,这 $ m $ 个权重函数只要满足 $ \sum_{i=1}^m w_i(x) = 1 $,那么 $ \langle I \rangle_{mis} $ 就是无偏的:

$$ \begin{aligned} \operatorname{E}[\langle I \rangle_{mis}] &= \operatorname{E}\left[ \sum_{i=1}^m \frac{1}{n_i} \sum_{j=1}^{n_i} \frac{w_i(x_{i,j}) f(x_{i, j})}{p_i(x_{i,j})} \right] \\ &= \sum_{i=1}^m \frac{1}{n_i} \operatorname{E}\left[ \sum_{j=1}^{n_i} \frac{w_i(x_{i,j}) f(x_{i, j})}{p_i(x_{i,j})} \right] \\ &= \sum_{i=1}^m \operatorname{E}\left[ \frac{w_i(x_{i,1}) f(x_{i, 1})}{p_i(x_{i,1})} \right] \quad (\because \text{i.i.d})\\ &= \sum_{i=1}^m \int_\Omega \frac{w_i(x) f(x)}{p_i(x)} p_i(x) dx \\ &= \sum_{i=1}^m \int_\Omega w_i(x) f(x) dx \\ &= \int_\Omega \sum_{i=1}^m w_i(x) f(x) dx \\ &= \int_\Omega f(x) dx \\ &= I \end{aligned} $$

那么,哪样的权重会让估计量的方差比较小呢?Veach 和 Guibas 在其论文中,给出了被称为 Balance Heuristic 的估计量:

$$ \hat w_i (x) = \frac{c_i p_i(x)}{\sum_{j=1}^m c_j p_j(x)} \quad \text{where}\ c_i = n_i / \sum_{j=1}^{m} n_j $$

并且他们证明了,使用 $ { \hat w_i(x) }{i=1}^m $ 作为权重函数构造的估计量 $ \langle \hat I{mis} \rangle $ 和任意的权重函数构造的估计量 $ \langle I_{mis} \rangle $ 的方差满足下面的关系:

$$ \operatorname{V}[\langle \hat I \rangle_{mis}] \le \operatorname{V}[\langle I \rangle_{mis}] + \left( \frac{1}{\min_i n_i} - \frac{1}{\sum_i n_i} \right) I^2 $$

这其实在说,Balance Heuristic 从渐进意义上来说是方差比较低的估计。

有的时候,我们只希望采样一次。这种情况下,我们可以首先以 $P(t=i)$ 的概率去采样我们将要使用的采样方法 $t$,然后再使用 MIS 积分估计量:

$$ \langle I \rangle_{mis} = \frac{w_t(x_{t,1}) f(x_{t,1})}{p_t(x_{t,1}) P(t=i)} $$

其中 $ p_t(x_{t,1}) $ 表示采样方法为 $ t $ 情况下抽样到 $ x_{t,1} $ 的条件概率。

Veach 的论文中证明,Balance Heuristic 在任何 One-sample MIS 的情形下都是最优的权重组合。

连续多重重要性采样 (Continuous MIS)

West 等人将上面的工作进行了进一步的推广:如果现在有连续的无限多种采样策略,那么也可以将 MIS 中的估计量进行推广,得到连续多重重要性采样 (Continuous Multiple Importance Sampling, CMIS)。

定义采样方法空间 $ \mathcal{T} $,在其上的每个元素 $ t \in \mathcal{T} $ 都是一种采样策略。

那么自然可以想到,将 $ w_i(x) $ 推广为一个 $ \mathcal{T} \times \mathcal{X} \to \mathrm{R} $ 的函数 $ w(t, x) $,归一化条件 $ \sum_i w_i(x) = 1 $ 推广为 $ \int_\mathcal{T} w(t, x) dt = 1 $。

类似的,可以定义 One-sample CMIS 积分估计量

$$ \langle I \rangle_{CMIS} = \frac{w(t, x)f(x)}{p(t, x)} = \frac{w(t, x)f(x)}{p(t) p(x|t)} $$

其中 $ p(t) $ 是选择策略 $ t $ 的概率密度, $ p(x|t) $ 是在策略 $ t $ 下采样得到 $ x $ 的条件概率。

同时,只要满足如下两个条件,上面的估计量就是无偏的:

  1. $ \int_\mathcal{T} w(t, x) dt = 1 $ 对任何 $ x \in \operatorname{supp} f(x) $ 成立
  2. 当 $ p(t, x) = 0 $ 时,$ w(t, x) = 0 $

    为什么?

类比 MIS,CMIS 也可以定义 Balance Heuristic 如下:

$$ \bar w(t, x) = \frac{p(t)p(x|t)}{\int_\mathcal{T} p(t') p(x|t') dt'} = \frac{p(t, x)}{\int_\mathcal{T} p(t', x)dt} = \frac{p(t, x)}{p(x)} $$

那么其实可以看到,用 Balance Heristic 的 $ w(t, x) $ 带入到 $ \langle I \rangle_{CMIS} $ 之后,其实就会化简成为 $ f(x) / p(x) $,只不过这里的 $ p(x) $ 是 $ p(t, x) $ 的边缘分布。

随机多重重要性采样 (Stochastic MIS)

前面的方法会面临一个问题,有的时候 $ p(x) = \int_\mathcal{T} p(t’, x)dt $ 是没有闭式解的,这样去算 $ \bar w(t, x) $ 的时候会遇到问题。所以,West 等人又提出了随机多重重要性采样 (Stochastic MIS, SMIS)。

SMIS 首先假设在 $ \mathcal{T} \times \mathcal{X} $ 中独立的采样 $ (t_1, x_1), …, (t_n, x_n) $ 共 $n$ 组点。

TODO: implement me

应用

Path Reuse

Spectral Rendering

Volume Single Scattering

一个示例 D3D11 程序的全流程记录

本篇是笔者进行 RenderDoc drivers 层分析时一并记录下来的,是一个渲染 Cube 的简单 D3D11 程序与 API 交互的全流程的记录。

DirectX11-With-Windows-SDK - Rendering A Cube 为例进行说明。

DXGI (DirectX Graphics Infrastructure) 负责抽象和交换链、DAL 相关的公共部分。关于 DXGI 的资料可以参考 MSDN

DXGI 封装了多种对象:

  • 显示适配器 (IDXGIAdapter): 一般对应一块显卡,也可以对应 Reference Rasterizer,或者支持虚拟化的显卡的一个 VF 等
  • 显示输出 (IDXGIOutput): 显示适配器的输出,一般对应一个显示器
  • 交换链 (IDXGISwapChain): 用来暂存要显示到输出窗口/全屏幕的 1 到多个 Surface 的对象
    • g_pSwapChain->GetBuffer 可以拿到表示 Back Buffer 的 ID3D11Texture
    • g_pd3dDevice->CreateRenderTargetView 来创建一个封装该 Texture 的 ID3D11RenderTargetView
    • g_pd3dDeviceContext->OMSetRenderTargets 来设置管线的 RenderTarget
    • g_pd3dDeviceContext->RSSetViewports 来设置管线的 Viewport

GameApp::Init()

  • D3DApp::Init()
    • InitMainWindow()
      • RegisterClass(WNDCLASS *): 注册
      • AdjustWindowRect()
      • CreateWindow()
      • ShowWindow()
      • UpdateWindow()
    • InitDirect3D()
      • D3D11CreateDevice(): 采用 11.1 的 Feature Level,不行则降级

        该函数会返回 Immediate Context (ID3D11DeviceContext), 设备 (ID3D11Device) 和特性等级

      • ID3D11Device::CheckMultisampleQualityLevels: 查询给定 DXGI_FORMAT 是否支持给定倍数的 MSAA

      • 将前面的 ID3D11Device Cast 到 IDXGIDevice

        An IDXGIDevice interface implements a derived class for DXGI objects that produce image data.

      • IDXGIDevice::GetAdapter 拿到 IDXGIAdapter

        The IDXGIAdapter interface represents a display subsystem (including one or more GPUs, DACs and video memory).

      • IDXGIAdapter::GetParent 拿到 IDXGIFactory1

        这里的 GetParentIDXGIAdapter 作为 IDXGIObject 的方法,可以获得构造它的工厂类。

        The IDXGIFactory1 interface implements methods for generating DXGI objects.

      • 尝试将 IDXGIFactory1 Cast 到 IDXGIFactory2 (DXGI 1.2 新增)

        • 如果支持 DXGI 1.2,则用 CreateSwapChainForHwnd 来创建交换链
        • 否则,用 CreateSwapChain 来创建交换链

          这两个函数都可以创建窗口 / 全屏幕交换链;DXGI 1.2 增加了新的、到其它输出目标的交换链创建功能,所以这里进行了重构。

          也要注意,不同 DirectX 可以支持的交换链的交换行为类型是不同的。大体上,交换链的交换行为可以分为

          • DISCARD vs SEQUENTIAL: 可以参考 StackExchange,区别就是一个驱动可以放心扔掉,另一个必须保留回读可能
          • FILP vs BLIT (Bit Block Transfer): 决定是用交换指针还是数据拷贝的方法来从交换链被 Present 的 Surface 中拿取数据
      • IDXGIFactory1::MakeWindowAssociation 来取消让 DXGI 接收 Alt-Enter 的键盘消息并且切换窗口和全屏模式

      • D3D11SetDebugObjectName()

        • ID3D11DeviceChild::SetPrivateData(WKPDID_D3DDebugObjectName, ...) 来设置资源的内部数据,这里是调试名称
      • DXGISetDebugObjectName()

        • IDXGIObject::SetPrivateData(WKPDID_D3DDebugObjectName, ...) 来设置资源的内部数据,这里是调试名称
      • OnResize()

        • IDXGISwapChain::ResizeBuffers()
        • IDXGISwapChain::GetBuffer() 拿到 ID3D11Texture2D 形式的 Back Buffer
        • IDXGISwapChain::CreateRenderTargetView() 创建绑定到上面 Back Buffer 的 Texture 的渲染目标视图
        • D3D11SetDebugObjectName 来设置 Back Buffer 的调试名称
        • ID3D11Device::CreateTexture2D() 来创建深度模板缓冲 (Depth Stencil Buffer),类型 ID3D11Texture2D,包含大小,MipLevel,采样描述等
        • ID3D11Device::CreateDepthStencilView() 来创建前面缓冲对应的深度模板视图
        • ID3D11DeviceContext::OMSetRenderTargets() 来将渲染目标视图和深度木板视图绑定到管线
        • ID3D11DeviceConetxt::RSSetViewports() 绑定 Viewport 信息到光栅器状态
        • D3D11SetDebugObjectName() 设置调试前面各种视图对象的对象名
  • InitEffect()
    • CreateShaderFromFile(): 传入 CSO (Compiled Shader Object) 和 Shader 文件,输出 ID3DBlob *
      • 如果有缓存,则用 D3DReadFileToBlob 装入,并返回
      • D3DCompileFromFile(): 编译并生成 ID3DBlob 对象
      • 如果指定了缓存路径,则 D3DWriteBlobToFile() 进行输出

        分别创建了 vs_5_0ps_5_0 Shader Model 的 Shader Blob

    • ID3D11Device::CreateVertexShader(),根据 Shader Bytecode 创建 ID3D11VertexShader 对象

      注意这个函数支持传入 Class Linkage,这是一种在 Shader 间共享类型和变量的机制,在 Shader Model 5 被引入。更详细的用法可以参考 Dynamic Linking Class | MSDN

      TODO: 研究一下

    • ID3D11Device::CreateInputLayout() 传入输入元素描述符和 Shader,传出 ID3D11InputLayout 对象
    • ID3D11Device::CreatePixelShader() ,根据 Shader Bytecode 创建 ID3D11PixelShader 对象
  • InitResource()
    • ID3D11Device::CreateBuffer() 创建顶点缓冲区 (ID3D11Buffer) ,并传入初始化数据
    • ID3D11Device::CreateBuffer() 创建索引缓冲区 (ID3D11Buffer) ,并传入初始化数据
    • ID3D11DeviceContext::IASetIndexBuffer() 设置 Immediate Context 绑定索引缓冲区
    • ID3D11Device::CreateBuffer() 创建常量缓冲区 (ID3D11Buffer) ,不是用初始化数据
      • 此处设置 D3D11_BUFFER_DESCCPUAccessFlagsD3D11_CPU_ACCESS_WRITE,让 CPU 可以改变其值
    • ID3D11DeviceContext::IASetVertexBuffers() 设置顶点缓冲区,stride 和 offset
    • ID3D11DeviceContext::IASetPrimitiveTopology() 设置图元类型
    • ID3D11DeviceContext::IASetInputLayout() 设置输入布局
    • ID3D11DeviceContext::VSSetShader() 绑定顶点着色器到管线
    • ID3D11DeviceContext::VSSetConstantBuffers() 设置常量缓冲区

      这里当然是拿着 ID3D11Buffer 去设置

    • ID3D11DeviceContext::PSSetShader() 设置像素着色器
    • D3D11SetDebugObjectName() 将 Input Layout, Shader 和 Buffer 设置好调试用名字

GameApp::Run()

关于 Windows 消息机制的相关介绍可以参考 About messages and message queues | MSDN

运行首先依赖 Windows 窗口程序本身的主事件循环(PeekMessage() => TranslateMessage() => DispatchMessage())。

主窗口的消息处理函数中,主要会处理:

  • WM_SIZE: 如果在 WM_ENTERSIZEMOVEWM_EXITSIZEMOVE 中间,则忽略,否则调用 OnResize() 重新配置交换链并绑定到管线
  • WM_ACTIVATE: 窗口不活跃时暂停渲染
  • WM_DESTROY: 窗口退出消息

如果没有待处理的窗口消息,则会进入:

  • CalculateFrameStats(): 根据定时器计算时长并更新窗口标题
  • UpdateScene(): 更新场景 (主要是更新常量缓冲)
    • 计算更新后的常量缓冲区值
    • ID3D11DeviceContext::Map 传入 Constant Buffer 对象

      MSDN: Gets a pointer to the data contained in a subresource, and denies the GPU access to that subresource.

      这里要指定映射类型 (CPU 可读,CPU 可写,CPU 可写且原内容可放弃);不过,这里还有一种类型,叫做 D3D11_MAP_WRITE_NO_OVERWRITE,这块 MSDN 的文档有比较详细的解释。

      也可以看看这篇知乎专栏作为参考。

    • memcpy(mappedData.pData, &cpuCBuffer, sizeof(cpuConstBuffer)) 将数据拷贝到 D3D11_MAPPED_SUBRESOURCE::pData 成员处
    • ID3D11DeviceContext::Unmap 解除内存映射
  • DrawScene(): 绘制场景
    • ID3D11DeviceContext::ClearRenderTargetView(): 用给定颜色清空渲染目标视图
    • ID3D11DeviceContext::ClearDepthStencilView(): 用给定深度和模板值清空深度模板视图
    • ID3D11DeviceContext::DrawIndexed(): 绘制给定的立方体
    • IDXGISwapChain::Present(SyncInterval=0, flags=0): 告知交换链已经完成绘制,可以呈现,并且要求立即呈现

      IDXGISwapChain::Present: Presents a rendered image to the user.

论文阅读 | ICARUS: NeRF 硬件加速器

简介

本篇文章介绍了 NeRF 硬件加速的实现。

NeRF 回顾

Neural Radiance Field,简称 NeRF,最开始在 ECCV 2020 上被提出,提出了以神经网络编码辐射场的一种技术,并且将其运用到了基于图片的场景重建等多个领域中,是近年来受关注度相当高的一篇工作。

NeRF 的网络部分输入为 5D: 位置 $ (x,y,z) $ 和朝向 $ (\theta, \phi) $,输出为该位置的 RGB 颜色和密度。

NeRF 在给定相机位置下最终渲染的输出用类似体渲染 (Volumetric Rendering) 的办法来实现。

NeRF 体渲染

对给定的相机光线 $ {\bf r}(t) = {\bf o} + t{\bf d} $ 来说,最终输出的颜色 $ {\bf C}(r) $ 以下式表示:

$$ C({\bf r}) = \int_{t_n}^{t_f} T(t) \sigma({\bf r}(t)) {\bf c}({\bf r}(t), {\bf d}) dt $$

其中:

  • $ T(t) = \exp (-\int_{t_n}^{t} \sigma({\bf r}(s)) ds ) $ 为光线从 $ t_n $ 能打到 $ t $ 的概率
    • 比如说,如果射线穿过的部分密度都比较大,那 $ T(t) $ 就会比较小
  • $ \sigma({\bf r}(t)) $ 是该 $ t $ 对应的点 $ {\bf r}(t) $ 的密度
  • $ {\bf c}({\bf r}(t), {\bf d}) $ 是网络给定方向和位置后输出的 RGB 颜色值
  • $ t_n $ 和 $ t_f $ 分别为射线进入和射出 NeRF 有效区域的包围盒时所对应的最近和最远参数值

不过这个积分显然不能很容易的解析求解,NeRF 的做法是采用数值积分的那一套。

首先,利用分层抽样 (stratified sampling) 的技术,将 $ [t_n, t_f] $ 分成 $ N $ 个均匀的小区间,然后在每个小区间均匀采样出一个 $ t_i $ 出来。

然后,用下面的量 $ \hat C({\bf r}) $ 来估计上面的 $ C({\bf r}) $:

$$ \hat C({\bf r}) = \sum_{i=1}^{N} T_i (1-\exp(-\sigma_i \delta_i)) {\bf c}_i $$

其中:

  • $ T_i = \exp(- \sum_{j=1}^{i-1} \sigma_j \delta_j) $
  • $ \delta_i = t_{i+1} - t_i $ 为两临近采样点的距离

为什么会变成这个形式?可以参考 arXiv 上的 Volume Rendering Digest (for NeRF)

原文中提到,从所有的 $ ({\bf c}_i, \delta_i) $ 对考虑的话,$ \hat C(r) $ 的计算显然是可微的,并且可以看成从最开始一直用 $ \alpha_i = 1 - \exp(\sigma_i \delta_i) $ 的透明度往上面做 alpha blending。

NeRF 网络

网络部分用位置编码 (Positional Encoding) + Coarse MLP + Fine MLP。

位置编码

位置编码用来改善网络对高频细节的学习效果。

位置编码层可以如下描述:

$$ \gamma(p) = (\sin(2^0 \pi p), \cos(2^0 \pi p), ..., \sin(2^{L-1} \pi p), \cos(2^{L-1} \pi p)) $$

Coarse & Fine MLP

NeRF 同时使用两个 MLP 来表示场景,一个粗粒度 MLP 和一个细粒度 MLP。

渲染的时候,首先用分层抽样的办法,在粗粒度网络中用前面提到的体渲染方法进行渲染,并且得到输出 $ \hat C_c(r) $:

$$ \hat C_c(r) = \sum_{i=1}^{N_c} w_i c_i, \quad w_i = T_i (1-\exp(\sigma_i \delta_i)) $$

然后,计算归一化权重 $ \hat w_i = w_i / \sum_{i=1}^{N_c} w_i $,并且用计算好的归一化权重作为概率分布函数 (cumulative distribution function),再在这条直线上采样 $ N_f $ 个位置,将这 $ N_c + N_f $ 个位置送入细粒度 MLP 进行推理,再用前面的办法渲染得到最终的颜色值。

损失函数

采用简单的把 Coarse MLP 和 Fine MLP 与真实值之间的 $ L^2 $ 损失直接加起来的办法。

ICARUS

NeRF 计算过程回顾

  1. 对像素所发出射线上的采样,得到点 $ ({\bf p}_1, …, {\bf p}_N) $
  2. 查询 MLP 网络:$ ({\bf p}_i, {\bf d}_i) \to ({\bf c_i}, \sigma_i) $
  3. 进行多次 alpha-blending

架构设计

架构设计时主要有以下目标:

  1. “端到端” - 芯片输入位置和方向,输出像素颜色,减少片上片外数据交换的额外开销(计算时间、功耗)
  2. 使用定点数 - 有效降低浮点数运算开销
  3. 架构设计要一定灵活性,尽量兼容比较多的 NeRF 衍生网络

如何使用定点数?

目前的实现是将在 GPU 上训练好的 NeRF 的权重进行量化 (quantization),再导出。不过,目前也有一些工作在 quantization-aware training 方面,可能对这个网络的训练过程有所帮助。

位置编码单元 (PEU)

设计位置编码单元 (Positional Encoding Unit, PEU) 的目的是在 PEU 前和 PEU 后的向量维数增加了很多倍(对原 NeRF 来说位置是 20 倍,方向是 8 倍),如果在 ICARUS 内部进行计算的话,可以减少很大一部分外部存储传输,降低传输总用时。

PEU 部件主要在做这件事:

$$ \phi(x; A) = [\cos A^T x, \sin A^T x] $$

其中 $ A $ 一般为一个行数比列数多的矩阵,用来升维。

PEU 单元对应的设计如下 (Fig. 4(b)):

可以看到,就是矩阵乘法单元和 CORDIC 单元的组合。

一些关于矩阵乘和 CORDIC 单元的大概印象:
矩阵乘:有很多工作,比如搜索 Systolic array 等等

具体设计上来说,ICARUS 支持对 dim=3 和 dim=6 的两种输入进行位置编码,并且扩展到 dim=128。PEU 内部设计有两个 3x128 的内存块和 6 组 MAC (Multiply-ACcumulate) 单元,当计算 dim=6 的输入时会全部启用,当计算 dim=3 的输入时只启用一半。

MLP Engine

MLP 引擎主要进行 $ f(Wx+b) $ 类型的计算。

MLP 引擎包含有:

  • 一个 Multi-output Network block (MONB),负责计算中间的隐藏层
  • 一个 Single-output network block (SONB),负责计算最后的输出层
    • 不继续用 MONB 的原因是,全连接的 MONB 比只输出一个数字的 SONB 面积要大得多
  • 两个 activation memory block

对于 MLP 计算来说,实现是这样的:

首先,将 MLP 的权重拆成 64 x 64 的小块,方便硬件上的复用,并且同样的权重可以被多组输入向量复用,从而降低内存带宽开销,代价方面只需要暂存该 batch 内的中间结果就可以(这里选择 batch_size=128)。

每个 64 x 64 的矩阵-向量乘法再进行分片,变成按矩阵列分割的 64 个列向量 - 向量的内积乘法(即 $ [\alpha_1 … \alpha_{64}] [x_1 … x_{64}]^T $,每个 $ \alpha_i x_i $ 的部分和用一个 RMCM 模块实现:

大概来说,是因为乘法可以变成移位加法:

$$ 3x = 1x << 1 + 1x $$

所以权重 load 进来的作用就是预先选择好路径上的移位和加法器,然后数据从这些器件中流过去就行。

另一个优化是高一半的移位和加法路径直接用上一次的值来替换,然后网络训练的时候也作此改动。这样可以节省 1/3 的面积,同时输出基本没有视觉质量损失。

SONB 的架构基本上和 MONB 差不多,只是 RMCM 块用不到了,用普通的向量乘法块就可以了。

Volume Rendering Unit

VRU 模块主要要负责下面的计算:

这里,他处理成下面的形式:

然后用上面的网络计算。

原型验证

验证平台使用的是 Synopsys HAPS-80 S104,验证时使用的工艺是 40nm CMOS 工艺。

论文阅读 | 基于物理的特征线渲染

简介

本篇文章介绍了基于物理的特征线渲染方法。

特征线渲染是一种非真实感渲染技术,常常被用在剪影、产品效果图等一些需要特殊的艺术效果的场合。

本篇文章提出的,基于路径方法的特征线渲染方法,是基于如下的两方面观察:

  1. 从路径的角度出发,现有的特征线渲染方法将特征线处理成了光源
  2. 特征线相交测试可以对任意的边开展,而不仅仅是在屏幕空间中

基于上面的观察,本文提出的方法

  1. 对一整个路径中每条路径段分别进行和特征线的相交测试
  2. 将交到的特征线视为吸收所有入射光,然后辐射用户自定义颜色的光源

TL;DR: 用 Path Tracing 做描边,把要描的边处理成光源,让描边也有景深、色散和反射等效果。

算法描述

算法的基本架构很简单。

从传统的 Path Tracer 出发:

  1. 对每个 path segment 依次进行和特征线的相交判断,并且
  2. 如果相交,则将特征线视为理想光源,并不再追下面的光源

相交判断

从该 path segment 出发,以固定扩张率和当前路径总长度做一锥体,寻找锥体中的特征线。

实现上,本文采用从锥体较窄的一端发射查询射线,并且判断交点是否为特征线上的点的方法来进行判断,可能和 cone tracing 比较相似。

采样权重修改

前面提到,“如果相交,则将特征线视为理想光源,并不再追下面的光源”。本文中会将这种情况整条采样路径每个点的 pdf 值处理成和打到刚好有光源位于这里的情况完全一致。

不过,这样会让整个估计变成有偏估计,因为还存在有特征线(i.e. 有光源)但是没有采样到的情况,这种情况使用正常 pdf 会让最后相机处接收的 Irradiance 期望偏小,也就是特征线会比无偏的情况更不明显。

比较幸运的是,通过加密相交判断中发射的查询射线的数量,可以渐进的趋于无偏的情况。

特征线判据

特征线的判断通过锥体中采样到的点和本 path segment 的起点和终点联合进行判断,主要有 MeshID, Albedo, Normal 和 Depth 四个方面的判据。

其中 $ t_{\text{depth}} $ 文中提到有一个较为启发的设置方法。

各项的效果如图所示:

效果

可以参考本文 Teaser:

可以看到,本文渲染的特征线有色散、景深模糊、反射等基于物理的效果。

未来的工作

文章最后主要提及了如下的 Future Work:

  • 其它路径采样方法 (i.e. BDPT)
  • 特征线锥形区域估计改进
  • 特征线区域缓存
  • 特征线模型改进
    • 反射 / 半透特征线模型等
  • 将 lens blur 和色散效果集成到 Stylized Focus
    • Stylized Focus 主要通过多个光栅化 pass 的叠加来实现风格化的景深和聚焦效果
让 Hexo 支持内联 LaTeX 数学公式的 Markdown

经过一周多陆陆续续的折腾,到现在本博客基本实现了博客 Markdown 渲染和 Typora, VSCode 等的默认预览中数学公式的表现一致。

Note: 大于号(>)小于号(<)目前没有做额外的转义(它们本身是 HTML 标签的结束和开始),但是这个用 \lt\gt 就行了,所以有点懒得改。

如果要改的话,就对 math 的处理加上这两个东西的 escape 就好,MathJaX 本身会识别出 &lt;&gt; 的。

简单来说,我分别魔改了 markedhexo-renderer-marked 两个包,实现了内联 LaTeX 的正确 Tokenize 和 Renderer (i.e. 什么也不做,原样输出),再由浏览器里运行的 MathJaX 3 来进行 LaTeX 渲染。

魔改后的版本支持块公式 (block math) $$ ... $$ 和内联公式 (inline math) $ ... $,并且内联公式内部的 _ 不会和 Markdown 对 _ 的使用冲突。

下面是渲染 Maxwell 方程组的示例:

$$ \begin{aligned} \nabla \cdot \mathbf{E} &= \frac {\rho} {\varepsilon_0} \\ \nabla \cdot \mathbf{B} &= 0 \\ \nabla \times \mathbf{E} &= -\frac{\partial \mathbf{B}} {\partial t} \\ \nabla \times \mathbf{B} &= \mu_0\left(\mathbf{J} + \varepsilon_0 \frac{\partial \mathbf{E}} {\partial t} \right) \end{aligned} $$

上面的 $ \mathbf{E} $ 是电场强度,$ \mathbf{B} $ 是磁感应强度,$ \mu_0$ 是真空磁导率,$ \epsilon_0 $ 是真空介电系数。

markedhexo-renderer-marked 使用的 Markdown 渲染器,它负责把 Markdown 渲染成 HTML。

为什么要新造一个轮子?

Google 一圈,现在有的 Hexo 内联 LaTeX 的方案都不是很让人满意:

  1. hexo-renderer-kramed 使用的 kramed 从 2016 年开始已经没有再更新了
  2. marked 表示不会加入对 $ ... $$$ ... $$ 的支持 (markedjs/marked, Issue #722)
  3. hexo-renderer-pandoc 需要用户自己安装 pandocpandoc 本身很庞大,并且是 Haskell 编写,本文作者表示改不动;另外,直接 out-of-box 的装上之后,块公式 pandoc 总是会多生成 \[ ... \] 的 pair,决定弃坑
  4. 网络上还存在一些 patch 方案,比如这里,直接把 marked 的 inline rule 改掉,让其不再将 _ 作为合法的强调标志(比如 _asdf_ 之类就不会渲染成 asdf 了)
  5. 其实也可以 摆大烂,把所有 LaTeX 和 Markdown 冲突的关键字都用反斜杠转义掉

可以看到都不是太优雅。

改动简介

其实一开始想给 hexo-renderer-marked 写插件,但是它只支持 extend TokenizerRenderer,把那些乱七八糟规则再写一遍又很难维护,所以最后放弃了这个想法。

主要是对 marked 进行改动,让其支持 $$ .. $$$ .. $ 的 Tokenize,并且能无转义的输出。

marked 采用正则表达式不断匹配的方式进行词法分析,对于部分块对象会继续进行行内的词法分析。词法分析后的 Toekn 流会送到 Renderer 进行输出。

详细可以看 libreliu/marked 上面的提交。

由于人比较懒,没有在 npm 上加自己的包,而是直接

1
npm install github:libreliu/hexo-renderer-marked-math#master --save

这个的缺点是每次 npm update hexo-renderer-marked-math 都要重新拉,并且版本管理上不是很友好。不过只是自己用的话其实无所谓。

(作为菜鸡)踩过的坑

  1. NodeJS 的 require 在找不到 index.js 时,会去 package.json 中查找 main 字段,并且加载对应的模块。

    可以注意到,markedmain./lib/marked.cjs,这个文件需要运行 npm run build 生成。

  2. 调试 Promise 链可以采用 Bluebird 的Promise.longStackTraces()

相关工作

这里在 2015 年对 VSCode 的 Markdown 渲染的 patch 对本更改有参考意义。

math-marked这个项目在我基本写完之后才看到,不过作者没有基于原来的 commit 继续改,后续升版本会比较费劲。