Turing渲染着色器网格技术分析
Turing渲染着色器网格技术分析
图灵体系结构通过使用 网格着色器 引入了一种新的可编程几何着色管道。新的着色器将计算编程模型引入到图形管道中,因为协同使用线程在芯片上直接生成紧凑网格( meshlets ),供光栅化器使用。处理高几何复杂度的应用程序和游戏得益于两阶段方法的灵活性,该方法允许有效的剔除、详细程度的技术以及程序生成。
本文介绍了新的管道,并给出了 GLSL 中用于 OpenGL 或 Vulkan 渲染的一些具体示例。新功能可以通过 OpenGL 和 Vulkan 中的扩展以及使用 DirectX 12 旗舰版 来访问。
以下大部分内容摘自此 录制的演示文稿。
1网格着色管线
2网格和网格着色
3预计算网格
3.1数据结构
3.2呈现资源和数据流
3.3使用任务着色器进行簇消隐
4Conclusion
5References
目标
现实世界是一个视觉丰富、几何复杂的地方。尤其是室外场景可以由数十万种元素(岩石、树木、小植物等)组成。 CAD 模型在复杂形状的表面以及由许多小零件组成的机械上都存在类似的挑战。在视觉效果中,大型结构,例如宇宙飞船,通常都用“ greebles ”来详细说明。图 1 显示了几个例子,其中,具有顶点、细分和几何体着色器的图形管道、实例化和间接多重绘制虽然非常有效,但当全分辨率几何体达到数亿个三角形和数十万个对象时,仍然会受到限制。
图 1. 对增加真实感的需求推动了几何复杂性的大量增加。
上面未显示的其他用例包括在科学计算中发现的几何图形(粒子、字形、代理对象、点云)或程序形状( ele CTR ic 工程布局、 vfx 粒子、带状和轨迹、路径渲染)。
本文研究了 网格着色器 来加速重三角形网格的渲染。原始网格被分割成更小的 meshlets ,如图 2 所示。理想情况下,每个网格都可以优化其中的顶点重用。使用新的硬件阶段和这种分割方案,可以并行地绘制更多的几何图形,同时获取较少的总体数据。
Figure 2. : |
|
![]() |
例如, CAD 数据可以达到几千万到数亿个三角形。即使在 遮挡筛选 之后,也可以存在大量的三角形。在这种情况下,管道中的某些固定函数步骤可能会造成浪费的工作和内存负载:
- 通过硬件的原始分配器每次扫描 indexbuffer 来批量创建顶点,即使拓扑结构没有改变
- 获取不可见数据的顶点和属性(背面、视锥体或亚像素剔除)
网格着色器 为开发人员提供了避免此类瓶颈的新可能性。(例如,在第 4 个缓存中,与第 4 个缓存相对应)的方法(例如,在第 4 个缓存中,直接读取第 4 个三角形)。
网格着色器阶梯为光栅化器生成三角形,但在内部使用协作线程模型,而不是使用单线程程序模型,类似于计算着色器。管道中网格着色器前面是任务着色器。任务着色器的操作类似于细分的控制阶段,因为它能够动态生成工作。但是,与网格着色器一样,它使用协作线程模型,而不是将面片作为输入,将细分决策作为输出,而是用户定义其输入和输出。
这简化了片上几何体的创建,与之前严格且有限的细分和几何体着色器相比,后者只需将线程用于特定任务,如图 3 所示。
图 3. 网格着色器表示处理几何复杂性的下一步
网格着色管线
一个新的两阶段管道替代方案补充了经典的属性获取顶点、细分、几何体着色器 管道。这条新管道由 任务着色器 和 网格着色器: 组成
- 任务着色器 :可编程单元,在工作组中工作,允许每个单元发射(或不发射)网格着色器工作组
- 网格着色器 :一种可编程单元,在工作组中运行,允许每个工作组生成原语
mesh shader stage 在内部使用上述协作线程模型为光栅化器生成三角形。任务着色器的操作类似于细分的外壳着色器阶段,因为它能够动态生成工作。但是,与网格着色器一样,任务着色器也使用协作线程模式。它的输入和输出是用户定义的,而不必将面片作为输入,将细分决策作为输出。
与 像素/片段着色器 的接口不受影响。传统的管道仍然可用,并且可以根据用例提供非常好的结果。图 4 突出显示了管道样式的差异。
图 4. 传统与任务/网格几何管道的区别
新的网格着色器管道为开发人员提供了许多好处:
- 更高的可扩展性 通过着色器单元来减少原语处理中的固定函数影响。现代 GPUs 的通用用途有助于更多种类的应用程序添加更多内核,并提高着色器的通用内存和算术性能。
- Bandwidth-reduction,因为顶点的重复数据消除(顶点重用)可以预先完成,并且可以在多个帧上重复使用。当前的 API 模型意味着每次硬件都必须扫描索引缓冲区。较大的网格意味着更高的顶点重用率,也降低了带宽要求。此外,开发人员可以提出自己的压缩或过程生成方案。
通过任务着色器 进行可选的扩展/筛选允许跳过完全获取更多数据。 - Flexibility 定义网格拓扑和创建图形工作。以前的 细分着色器 仅限于固定的镶嵌模式,而 几何着色器 则存在线程效率低下、编程模型不友好的问题,每个线程都会创建三角形条带。
网格着色遵循 计算着色器 的编程模型,使开发人员可以自由地将线程用于不同的用途,并在线程之间共享数据。当光栅化被禁用时,这两个阶段还可以用于执行具有一个级别扩展的通用计算工作。
图 5. 网格着色器的行为类似于使用协作线程模型计算着色器。
这两个 网格和任务着色器 都遵循 计算着色器 的编程模型,使用协作线程组来计算结果,并有 除工作组索引外没有其他输入 。它们在图形管道上执行;因此硬件直接管理在级之间传递并保存在芯片上的内存。
将展示一个例子,说明如何使用它来进行基本体剔除,因为线程稍后可以访问工作组中的所有顶点。图 6 说明了任务着色器处理早期剔除的能力。
图 6. 尽管可选,任务着色器启用早期剔除以提高吞吐量。
通过 任务着色器 进行的可选扩展允许对一组原语进行早期筛选或预先做出 LOD 决策。该机制在 GPU 上缩放,因此取代了小网格的实例化或多重绘制间接。此配置类似于 细分控制着色器 设置细分补丁(~ task
workgroup )的数量,然后影响创建的 细分评估 调用(~ mesh
workgroups )的数量。
单个 任务工作组 可以发射的 工作网格组 数量是有限制的。第一代硬件支持最多 64K 个子级,可以生成 每个任务。在同一个绘制调用中,所有任务的网格子对象的总数没有限制。同样,如果不使用 任务着色器 ,则对 draw 调用生成的网格工作组的数量没有限制。图 7 说明了这是如何工作的。
图 7. 网格着色器工作组流
任务 T 的子任务保证在任务 T-1 的子任务之后启动。但是, task 和 mesh 工作组是完全流水线的,因此不需要等待以前的子任务或任务的完成。
任务着色器 应用于动态工作生成或过滤。静态设置得益于单独使用 网格着色器 。
网格及其内部图元的栅格化输出顺序保持不变。禁用光栅化后,任务着色器和网格着色器都可以用于实现基本计算树。
网格和网格着色
每个网格单元表示数量可变的顶点和基本体。对于这些原语的连接性没有限制。但是,必须保持在着色器代码中指定的最大值以下。
建议最多使用 64 个顶点和 126 个基本体。 126 中的“ 6 ”不是打字错误。第一代硬件以 128 字节的粒度分配原始索引,并且需要为基元计数预留 4 个字节。因此 3 * 126 + 4 最大化了 3 * 128 = 384 字节块的大小。超过 126 个三角形将分配接下来的 128 个字节。 84 和 40 是其他适用于三角形的最大值。
在每个 GLSL mesh-shader
代码中,每个工作组在图形管道中为每个工作组分配固定数量的网格内存。
最大尺寸和原始输出定义如下:
每个网格单元的分配大小取决于编译时大小信息以及着色器引用的输出属性。分配越小,可以在硬件上并行执行的工作组越多。与 compute 一样,工作组共享可以访问的片上内存的公共部分。因此,建议尽可能高效地使用所有输出或共享内存。对于当前着色器,这已经是正确的。但是,内存占用可能会更高,因为允许比当前编程中更多的顶点和基元。
// Set the number of threads per
workgroup (always one-dimensional).
// The
limitations may be different than in actual compute shaders.
layout(local_size_x=32) in;
// the primitive
type (points,lines or triangles)
layout(triangles) out;
// maximum
allocation size for each meshlet
layout(max_vertices=64, max_primitives=126) out;
// the actual
amount of primitives the workgroup outputs ( <= max_primitives)
out uint gl_PrimitiveCountNV;
// an index
buffer, using list type indices (strips are not supported here)
out
uint gl_PrimitiveIndicesNV[]; // [max_primitives * 3 for triangles]
图灵支持另一个新的 GLSL 扩展, NV_fragment_shader_barycentric ,它使片段着色器能够获取构成一个基本体的三个顶点的原始数据,并手动对其进行插值。这种原始访问意味着可以输出“ uint ”顶点属性,但是使用各种 pack / unpack 函数将浮点存储为 fp16 、 unorm8 或 snorm8 。这可以大大减少法线、纹理坐标和基本颜色值的逐顶点占用空间,并有利于标准和网格着色管道。
顶点和基本体的其他属性定义如下:
out gl_MeshPerVertexNV {
vec4 gl_Position;
float gl_PointSize;
float
gl_ClipDistance[];
float
gl_CullDistance[];
}
gl_MeshVerticesNV[]; //
[max_vertices]
// define your
own vertex output blocks as usual
out Interpolant {
vec2 uv;
}
OUT[]; //
[max_vertices]
// special
purpose per-primitive outputs
perprimitiveNV out gl_MeshPerPrimitiveNV {
int gl_PrimitiveID;
int gl_Layer;
int gl_ViewportIndex;
int
gl_ViewportMask[]; // [1]
}
gl_MeshPrimitivesNV[]; //
[max_primitives]
一个目标是拥有最小数量的网格,从而最大限度地提高网格中顶点的重用率,从而减少分配的浪费。在生成 meshlet 数据之前,在 indexbuffer 上应用顶点缓存优化器是有益的。例如, Tom Forsyth 的线性速度优化器 可用于此。优化顶点位置和索引缓冲区也是有益的,因为使用 网格着色器 时,原始三角形的顺序将保持不变。 CAD 模型通常是用条带“自然”生成的,因此可以具有良好的数据局部性。更改索引缓冲区可能会对此类数据的 meshlet 的簇剔除属性产生负面影响(请参见任务级消隐)。
预计算网格
例如,呈现静态内容,其中 索引缓冲区 在许多帧中没有变化。因此,在将顶点/索引上载到设备内存期间,生成网格数据的成本可以隐藏起来。当 vertex 数据也是静态的(没有逐顶点动画;顶点位置没有变化)时,还可以获得额外的好处,允许预先计算对快速剔除整个网格单元有用的数据。
数据结构
在以后的示例中,将提供一个 meshlet 构建器,它包含一个基本实现,该实现扫描所提供的索引,并在每次遇到大小限制(顶点或基元计数)时创建一个新的 meshlet 。
对于输入三角形网格,它将生成以下数据:
struct MeshletDesc {
uint32_t
vertexCount; // number of vertices used
uint32_t primCount; // number of
primitives (triangles) used
uint32_t vertexBegin; // offset into vertexIndices
uint32_t primBegin; // offset into
primitiveIndices
}
std::vector<meshletdesc> meshletInfos;
std::vector<uint8_t>
primitiveIndices;
// use uint16_t
when shorts are sufficient
std::vector<uint32_t> vertexIndices;
为什么有两个索引缓冲区?
以下原始三角形索引缓冲区序列
// let's look at the first two triangles
of a batch of many more triangleIndices = { 4,5,6, 8,4,6, ...}
分为两个新的索引缓冲区。
当迭代三角形索引时,建立了一组唯一的顶点索引。此过程也称为 顶点重复数据消除 。
vertexIndices = { 4,5,6, 8, ...}
// For the second triangle only vertex 8
must be added
// and the other vertices are re-used.
原始索引相对于 vertexIndices 项进行调整。
// original data
triangleIndices = { 4,5,6,
8,4,6, ...}
// new data
primitiveIndices = { 0,1,2, 3,0,2, ...}
// the primitive indices are local per
meshlet
一旦达到适当的大小限制(要么是唯一顶点太多,要么是基本体太多),就会启动一个新的网格单元。随后的网格将创建自己的唯一顶点集。
呈现资源和数据流
在渲染期间,使用原始顶点缓冲区。但是,使用了三个新的缓冲区,而不是原来的三角形索引缓冲区,如下图 8 所示:
- 顶点索引缓冲区 如上所述。每个网格单元引用一组唯一的顶点。这些顶点的索引按顺序存储在所有网格单元的缓冲区中。
- 原始索引缓冲区 如上所述。每个网格单元表示不同数量的基本体。每个三角形需要三个原始索引,这些索引存储在一个缓冲区中。 Note :可以在每个 meshlet 之后添加额外的索引以获得四字节对齐。
- Meshlet Desc 缓冲区。 存储每个网格单元的工作负载和缓冲区偏移信息,以及集群剔除信息。
这三个缓冲区实际上比原始索引缓冲区小,因为网格着色允许更高的顶点重用。注意到,通常会将索引缓冲区大小减少到原始索引缓冲区大小的 75% 左右。
图 8. 网状缓冲结构
- 网格顶点: vertexBegin 存储开始获取顶点索引的起始位置。 vertexCount 存储所涉及的连续顶点的数量。顶点在网格单元中是唯一的;没有重复的索引值。
- 网格元素: primBegin 存储原始索引的起始位置,将从那里开始获取索引。 primCount 存储 meshlet 中涉及的基本体数量。注意,索引的数量取决于基本体类型(这里: 3 表示三角形)。注意,索引引用的是相对于 vertexBegin 的顶点,这意味着索引“ 0 ”将引用位于 vertexBegin 的顶点索引。
下面的伪代码描述了每个 网格着色器 工作组在原则上执行的操作。它是串行的,仅用于说明目的。
// This code is just a serial pseudo
code,
// and doesn't
reflect actual GLSL code that would
// leverage the
workgroup's local thread invocations.
for (int v = 0; v <
meshlet.vertexCount; v++){
int vertexIndex =
texelFetch(vertexIndexBuffer, meshlet.vertexBegin + v).x;
vec4 vertex = texelFetch(vertexBuffer, vertexIndex);
gl_MeshVerticesNV[v].gl_Position = transform * vertex;
}
for (int p = 0; p <
meshlet.primCount; p++){
uvec3 triangle = getTriIndices(primitiveIndexBuffer, meshlet.primBegin +
p);
gl_PrimitiveIndicesNV[p * 3 + 0] = triangle.x;
gl_PrimitiveIndicesNV[p * 3 + 1] = triangle.y;
gl_PrimitiveIndicesNV[p * 3 + 2] = triangle.z;
}
// one thread
writes the output primitives
gl_PrimitiveCountNV
= meshlet.primCount;
当以并行方式编写时,网格着色器可能看起来如下所示:
void main() {
...
// As the
workgoupSize may be less than the max_vertices/max_primitives
// we still
require an outer loop. Given their static nature
// they should
be unrolled by the compiler in the end.
// Resolved at
compile time
const uint vertexLoops
=
(MAX_VERTEX_COUNT + GROUP_SIZE - 1) / GROUP_SIZE;
for (uint loop = 0;
loop < vertexLoops; loop++){
// distribute
execution across threads
uint v = gl_LocalInvocationID.x + loop * GROUP_SIZE;
// Avoid
branching to get pipelined memory loads.
// Downside is
we may redundantly compute the last
// vertex
several times
v = min(v, meshlet.vertexCount-1);
{
int vertexIndex =
texelFetch( vertexIndexBuffer,
int(meshlet.vertexBegin
+ v)).x;
vec4 vertex = texelFetch(vertexBuffer, vertexIndex);
gl_MeshVerticesNV[v].gl_Position = transform * vertex;
}
}
// Let's pack 8
indices into RG32 bit texture
uint primreadBegin = meshlet.primBegin / 8;
uint primreadIndex = meshlet.primCount * 3 - 1;
uint primreadMax = primreadIndex
/ 8;
// resolved at
compile time and typically just 1
const uint primreadLoops
=
(MAX_PRIMITIVE_COUNT * 3 + GROUP_SIZE * 8 - 1)
/ (GROUP_SIZE * 8);
for (uint loop = 0;
loop < primreadLoops; loop++){
uint p = gl_LocalInvocationID.x + loop * GROUP_SIZE;
p = min(p, primreadMax);
uvec2
topology = texelFetch(primitiveIndexBuffer,
int(primreadBegin +
p)).rg;
// use a
built-in function, we took special care before when
// sizing the
meshlets to ensure we don't exceed the
//
gl_PrimitiveIndicesNV array here
writePackedPrimitiveIndices4x8NV(p * 8 + 0, topology.x);
writePackedPrimitiveIndices4x8NV(p * 8 + 4, topology.y);
}
if
(gl_LocalInvocationID.x == 0) {
gl_PrimitiveCountNV = meshlet.primCount;
}
这是一个直接的例子。由于所有数据获取都是由开发人员完成的,自定义编码、通过子组内部函数或共享内存进行解压缩,或者暂时使用顶点输出,都可以节省额外的带宽。
使用任务着色器进行簇消隐
尝试将更多的信息压缩到一个 meshlet 描述符中以执行早期剔除。已经尝试使用 128 位描述符对前面提到的值进行编码,以及 G.Wihlidal 提出的用于背面聚类剔除的相对 bbox 和一个圆锥体。在生成网格时,需要在良好的簇剔除特性和改进的顶点重用之间取得平衡。一方可能对另一方产生负面影响。
下面的任务着色器最多可剔除 32 个网格。
layout(local_size_x=32) in;
taskNV out Task {
uint baseID;
uint8_t subIDs[GROUP_SIZE];
} OUT;
void main() {
// we padded the
buffer to ensure we don't access it out of bounds
uvec4 desc = meshletDescs[gl_GlobalInvocationID.x];
// implement
some early culling function
bool render =
gl_GlobalInvocationID.x < meshletCount && !earlyCull(desc);
uvec4 vote =
subgroupBallot(render);
uint tasks =
subgroupBallotBitCount(vote);
if
(gl_LocalInvocationID.x == 0) {
// write the
number of surviving meshlets, i.e.
// mesh
workgroups to spawn
gl_TaskCountNV = tasks;
// where the
meshletIDs started from for this task workgroup
OUT.baseID = gl_WorkGroupID.x * GROUP_SIZE;
}
{
// write which
children survived into a compact array
uint idxOffset = subgroupBallotExclusiveBitCount(vote);
if (render) {
OUT.subIDs[idxOffset] =
uint8_t(gl_LocalInvocationID.x);
}
}
}
相应的网格着色器现在使用来自任务着色器的信息来标识要生成的网格。
taskNV in Task {
uint baseID;
uint8_t subIDs[GROUP_SIZE];
} IN;
void main() {
// We can no
longer use gl_WorkGroupID.x directly
// as it now
encodes which child this workgroup is.
uint meshletID = IN.baseID + IN.subIDs[gl_WorkGroupID.x];
uvec4 desc = meshletDescs[meshletID];
...
}
在渲染大三角形模型的上下文中剔除任务着色器中的网格。其他场景可能涉及到根据细节决策的级别选择不同的 meshlet 数据,或者完全生成几何体(粒子、色带等)。下面的图 9 来自一个使用任务着色器进行详细级别计算的演示。
图 9. NVIDIA 小行星演示使用网格着色
Conclusion
一些主要的感受:
- 通过扫描索引缓冲区一次,可以将三角形网格转换为网格。顶点缓存优化器有助于经典渲染,也有助于提高网格填充效率。更复杂的聚类允许改进任务着色器阶段的早期排斥(更紧密的边界框、一致的三角形法线等)。
- 在硬件需要为片上 网格着色器 调用分配顶点/基元内存之前, 任务着色器 允许提前跳过一组原语。如果需要,还可以生成多个子调用。
- 顶点在工作组的线程中并行处理,就像原始的 顶点渲染 一样。
- 顶点着色器 可以与 网格着色器 兼容,并带有一些预处理器插入。
- 由于更高的顶点重用,需要提取的数据更少(经典顶点着色器的操作限制为 max _
vertices = 32 , max _
primitives = 32 )。平均三角形网格价意味着使用两倍数量的三角形作为顶点是有益的。 - 所有数据加载都是通过着色器指令来处理的,而不是经典的固定函数原语 fetch ,因此使用更多的 流式多处理器 可以更好地伸缩。它还允许更容易地使用自定义顶点编码来进一步减少带宽。
- 对于顶点属性的大量使用,同样并行操作的基本消隐阶段可能是有益的。可以剔除掉顶点数据。然而,最好的收获是在任务级别进行有效的筛选。
Turing渲染着色器网格技术分析的更多相关文章
- osg ifc数据渲染着色器
//顶点着色器 static const char* vertShader = { "varying vec4 color;\n" "void main(void)\n& ...
- 使用着色器在WebGL3D场景中呈现行星表面地形
实验目的:按照一定规律生成类地行星地表地形区块,并用合理的方式将地形块显示出来 涉及知识:Babylon.js引擎应用.着色器编程.正态分布.数据处理.canvas像素操作 github地址:http ...
- stage3D基础二-----顶点和片段着色器(转)
来源:http://www.adobe.com/cn/devnet/flashplayer/articles/vertex-fragment-shaders.html 本教程将介绍着色器.着色器是 S ...
- 分形的奥秘!分形着色器!shader 编程入门实战 ! Cocos Creator!
极致的数学之美! 什么是分形? "一个粗糙或零碎的几何形状,可以分成数个部分,且每一部分都(至少近似地)是整体缩小后的形状" 简单来说,分形(fractal)就像这个doge表情包 ...
- (原)Unreal渲染模块 管线 - 着色器(1)
@author: 白袍小道 转载悄悄说明下 随缘查看,施主开心就好 说明: 本篇继续Unreal搬山部分的渲染模块的Shader部分, 主要牵扯模块RenderCore, ShaderCore, RH ...
- GDC2017【神秘海域 4】中所使用的顶点着色器技术
原文链接 http://game.watch.impress.co.jp/docs/news/1047802.html 会場:San Francisco Moscone Convention Ce ...
- Unity 渲染教程(二):着色器基础
转载:https://www.jianshu.com/p/7db167704056 这是关于渲染基础的系列教程的第二部分.这个渲染基础的系列教程的第一部分是有关矩阵的内容.在这篇文章中我们将编写我们的 ...
- D3D三层Texture纹理经像素着色器实现渲染YUV420P
简单记录一下这两天用Texture实现渲染YUV420P的一些要点. 在视频播放的过程中,有的时候解码出来的数据是YUV420P的.表面(surface)通过设置参数是可以渲染YUV420P的,但Te ...
- BGFX 渲染引擎中着色器代码的调试方法
在实时渲染的图形开发中,着色器代码(Shader)越来越复杂,于是单纯的靠经验和不断试错的开发和调试方法早已不能满足实际需求.使用调试工具进行调试,成为开发中重要的方法.Bgfx 是一款跨平台.抽象封 ...
随机推荐
- Mysql 8.0安装
1. 下载安装包至/usr/local目录下 下载地址:https://cdn.mysql.com/Downloads/MySQL-8.0/mysql-8.0.16-el7-x86_64.tar.gz ...
- 【并发编程】Java中的锁有哪些?
0.死锁 两个或者两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象,若无外力作用,他们都将无法让程序进行下去: 死锁条件: 不可剥夺条件: T1持有的资源无法被T2剥夺 请 ...
- 反病毒攻防研究第004篇:利用WinRAR与AutoRun.inf实现自启动
一.前言 由之前的一系列研究可以发现,为了使得"病毒"能够实现自启动,我也是煞费苦心,采取了各种方式,往往需要编写冗长的代码并且还需要掌握系统底层或注册表的很多知识才可以.而这次我 ...
- hdu4768 非常规的二分
题意: n个社团给同学发传单,同学一共有1--2^31这么多,每个社团有三个数A ,B ,C ,只有 满足 A ,A + C ,A + C + C ...A + KC <= B 的学 ...
- 远程分支git换地址了,本地重新关联
由于本人把github远程仓库的名字修改了所以做了以下步骤修改 步骤:两步 (1)先把之前关联的git清除掉 git remote rm origin (2)再关联新的地址 git remote ad ...
- PHP中ftp的连接与操作
1.操作类 <?phpclass FtpService{ protected $connect = 0; public function __construct() { $this->co ...
- python常识系列07-->python利用xlwt写入excel文件
前言 读书之法,在循序而渐进,熟读而精思.--朱熹 抽空又来写一篇,毕竟知识在于分享! 一.xlwt模块是什么 python第三方工具包,用于往excel中写入数据:(ps:只能创建新表格,不能修改表 ...
- window下批量删除指定后缀文件
例子: 批量删除当前路径下后缀为 .jpg和 .json del /a /f /s /q "*.jpg" "*.json" *为通配符/a /f 是强制删除所有 ...
- CAS指令
原文链接:https://www.jianshu.com/p/00edb3d74a33 CAS是CPU的一条指令,其具有原子性,原子性是由CPU硬件层面保证的. CAS原语有三个操作数--内存 ...
- 基于RestAssured实现接口自动化
RestAssured是一款强大的接口自动化框架, 旨在使用方便的DSL,简化的接口自动化. 下面是基于RestAssured扩展的一个简单框架示例, 先看看用例的风格: package testca ...