Unity BatchRendererGroup 在低端设备上也实现高帧率

news2025/1/23 3:58:14

在这篇文章中,我们描述了一个小型射击游戏样本,它可以动画和渲染几个交互式对象。许多演示只针对高端pc,但这里的目标是在使用GLES 3.0的廉价手机上实现高帧率。这个例子使用了BatchRendererGroup, Burst编译器和c#作业系统。它运行在Unity 2022.3中,不需要实体或实体。图形DOTS包。

让我们开始吧。

介绍样本

让我们直接进入样本是什么。这个示例在2019年预算的三星Galaxy A51(使用Mali G72-MP3 GPU)上以稳定的60 fps运行。图形API设置为gles3.0。

u

您可以通过从GitHub下载该项目来研究代码并在您最喜欢的平台上试用。你只需要Unity 2022.3。

在这篇文章中,我们主要关注BatchRendererGroup和样例类BRG_Container.cs。你也可以在BRG_Background.csBRG_Debris.cs类中学习动画和物理代码。

场景设置

  • 在深入研究如何制作它之前,让我们先探索一下我们所看到的。

  • 背景地板由许多立方体构成。所有的盒子都可以上下移动。

  • 主船在屏幕上水平移动,并向彩色球体发射导弹。(你可以通过点击屏幕更快地发射导弹。)

  • 当一枚导弹飞过地板时,一个磁场会轻微提升并突出地板细胞。它还将地面碎片抛向空中。

  • 当导弹击中球体时,它会爆炸成彩色碎片。

  • 当碎片撞到地板上时,地板上的碰撞细胞会闪烁出白色。撞击细胞的碎片越多,细胞的颜色就越暗。此外,碎片的重量会在地面上造成凹痕。

Rendering

地板细胞和碎片都是由立方体构成的。每个立方体都有不同的位置和颜色。我们想要动画和管理一切使用CPU,使地板和碎片之间的相互作用更容易。(碎片不仅仅是一种美观的视觉效果,所以它不能只使用GPU。)

对于渲染,我们不会为每个道具创建一个GameObject,以避免在低端移动设备上造成不必要的性能冲击。相反,我们使用新引入的BatchRendererGroup API。

为什么不使用经典的Graphics.DrawMeshInstanced?

Graphics.DrawMeshInstanced是一种方便快捷的方法,可以在不同位置渲染许多相似的网格。然而,与BatchRendererGroup API相比,它有以下限制:

  • 它需要提供带有矩阵的托管内存数组,因此您可能会获得垃圾收集。此外,反向矩阵是cpu计算的,即使着色器不需要它(例如,使用URP/unlit)。
  • 如果你想自定义obj2world矩阵以外的任何属性(比如每个实例有一种颜色),你需要提供你自己的自定义着色器,要么从头开始编写,要么使用着色器图形。
  • 矩阵或自定义数据必须在每次绘制时上传到GPU内存。你不能用Graphics.DrawMeshInstanced拥有持久的GPU内存数据。根据上下文的不同,这可能会对性能造成巨大影响。

什么是 BatchRendererGroup?

BatchRendererGroup(或BRG)是一个API,可以有效地从c#生成绘制命令并生成gpu实例化绘制调用。由于它不使用托管内存,您还可以使用Burst编译器生成命令。

优点缺点
能够从突发作业中快速生成drawstance命令你必须自己生成最优批次的绘制命令
使用持久的大型GPU缓冲区来存储每个实例的任何自定义属性您必须自己管理GPU内存和自定义属性偏移分配
支持广泛的平台,包括OpenGLES 3.0及以上版本
兼容标准的SRP着色器(点亮和未点亮)。不需要编写自定义着色器

提示: entities.graphics entities. graphics 包是用来渲染实体(ECS包),并建立在BRG之上。实体。软件包为您完成所有GPU内存管理和最佳绘制命令创建。在这个样本中我们没有使用ECS,所以我们将直接驱动BRG。

BRG着色器数据模型

BRG使用特定的GPU数据布局和专用的着色器变体。shader变体可以从标准常量缓冲区(UnityPerMaterial)或自定义的大型GPU缓冲区(BRG raw缓冲区)中获取数据。这是由你来管理你如何存储你的数据在原始缓冲区,这是一个Shader存储缓冲对象(SSBO,或字节地址缓冲区)。默认的BRG数据布局是数组结构(SoA)类型。

Properties per instance – or not

你可以实例化材料的任何属性,而不必创建自定义着色器。在这个示例中,我们希望实例化每个盒子实例的obj2world矩阵(用于定位立方体)、world2obj矩阵(用于照明)和BaseColor(因为每个地板单元或碎片都有自己的颜色)。

所有其他属性对于所有多维数据集都是相同的(例如,平滑度值),并且您可以使用元数据描述哪些属性将具有每个实例的自定义值。

BRG metadata

BRG元数据是一个可选的32位值,你可以设置每个着色器属性。它告诉着色器代码如何从GPU内存和从哪里加载属性值。第0-30位定义BRG原始缓冲区中属性的偏移量,第31位告诉我们属性值对于所有实例是相同的,还是偏移量是数组的开始,每个实例有一个值。

BRG元数据的确切含义也取决于着色器属性类型。让我们总结一下所有的可能性:

图1:使用BRG元数据,您可以描述每个实例中哪些属性具有自定义值(如obj2world、world2obj、baseColor)。所有其他属性对所有实例具有完全相同的值(并且仍然使用经典UnityPerMaterial cbuffer作为数据源)。

BRG剔除和能见度指数

不同的图形。drawmeshinstance, BRG使用一个持久的GPU内存缓冲区。假设在原始缓冲区中有10个立方体位置和颜色,但是只有立方体0、3和7是可见的。你只想画三个立方体,但你需要着色器正确读取这些立方体的位置和颜色。为了做到这一点,BRG着色器使用了一个小的额外间接。这个可见性缓冲区只是一个在生成绘制命令时填充的“int”数组。

在这个例子中,你需要用{0,3,7}填充一个包含三个整数的数组,然后生成一个包含三个实例的BRG绘制命令。

图2: BRG着色器变体总是使用可见性间接从持久原始缓冲区中获取数据。这个小的可见性间接缓冲可以根据你的需要为每一帧生成。

获取“baseColor”属性的着色器代码如下所示:

if ( metadata_baseColor&(1<<31) )
{
        // get the real index from the visibility buffer indirection
	int visibleId = brg_visibility_array[GPU_instanceId];
	uint base = (metadata_baseColor&0x7ffffffc);
	uint offset = visibleId * sizeof(baseColor);
        // fetch data from a custom array in BRG raw buffer
        baseColor = brg_raw_buffer.Load( base + offset );
}
else
{
        // fetch data from UnityPerMaterial (as usual)
	baseColor = UnityPerMaterial.baseColor;
}

更进一步:因为你可以实例化SRP着色器的任何属性(unlit, simplelit, lit),所有材质属性都有一个“if元数据&(1<<31)”分支。即使您不需要为每个实例定制平滑度值,这也会带来一些性能成本。在这个示例中,我们只想实例化baseColor。你可以创建一个Shader Graph,其中只有颜色将被定义为BRG实例化。因此,生成的代码有大量的数据获取间接只有颜色属性。Shader应该在低端GPU上运行得稍微快一点。

Rendering floor cells

在我们的游戏样本中,地板由32x100个单元格构成,即3200个单元格。每个单元都有位置、高度和颜色,并且单元格在相机保持静止时滚动。当一行滚动出视图时,我们注入一个包含32个单元格的新行。

当整行滚动出视图时,将插入一行新的单元格。新单元使用随机的高度和颜色。您可以查看示例中的BRG_Background.InjectNewSlice()。

在任何时间点都有3,200个单元格,因此实际上没有必要进行剔除(所有单元格始终在相机的视图内)。要定位每个单元格,您需要每个单元格一个obj2world矩阵,照明的逆矩阵和一个颜色。要渲染完整的地板,我们将使用单个BRG绘制命令。

渲染爆炸碎片

所有的碎片都有简单的重力物理,并与地板细胞相互作用。一切都是使用Burst c#作业在CPU上运行的

样本的碎片由小立方体组成,每个立方体都有一个位置、颜色和垂直轴上的旋转。这与地板细胞非常相似。为此,我们创建了BRG_Container.cs。这个类管理一个BRG对象来渲染地板单元或爆炸碎片。所有的物理动画和交互都是用c#代码使用BRG_Debris.cs完成的。

与地板单元不同,整个框架的碎片数量各不相同。在初始化时,指定BRG_Container的最大条目数。在我们的样本中,碎片是16,384(每次爆炸由1,024个碎片立方体组成),我们使用异步作业来动画重力场中的碎片。当碎片撞击到地板细胞时,它会通过挖入地面来相互作用。

BRG matrix format

为了优化GPU内存存储和带宽,BRG使用float3x4而不是float4x4来存储矩阵。请记住,原始缓冲区中的BRG矩阵是48字节,而不是64字节。

BRG矩阵仅为48字节(即三个float4),以提高GPU带宽

原始缓冲区看起来像这样:

图3:使用SoA布局,350kib的SSBO原始缓冲区包含3200个实例的数据。

提示: 碎片原始缓冲区数据看起来与地板数据相似,因为它也使用三个自定义属性(obj2world, world2obj和color)。碎片的最大条目数为16,384,这意味着原始缓冲区为112x16,384字节,即1.75 MiB。并不是所有的碎片在大多数情况下都会被渲染,这取决于给定时间存在的碎片立方体的数量。

Animating floor cells

我们有一个358,400字节的GPU GraphicsBuffer。由于动画是用CPU完成的,我们也在系统内存中分配了一个类似的缓冲区(CPU可以在系统内存中全速处理数据)。让我们将第二个缓冲区称为GPU内存的“影子副本”。c#代码将使地板单元格动画化,使用sin和来自阴影副本的碎片。动画完成后,我们使用GraphicsBuffer将阴影复制缓冲区上传到GPU - SetData API。

比示例更进一步: 优化GPU渲染通常意味着优化数据量。在我们的示例中,我们使用标准和库存SRP着色器。这就是为什么我们为矩阵使用三个float4,为颜色使用一个float4。你可以更进一步,编写一个自定义着色器来减少数据大小,或者你可以使用一个32位的地板单元格高度值。

如果你想继续,使用单元索引来计算它的世界位置,然后在着色器中计算矩阵和逆矩阵。最后,使用32位整数来存储颜色。最后,每个项目上传8字节而不是112字节。这使得GPU数据上传的速度提高了14倍。这意味着重写着色器获取代码。

BRG BatchID

任何BRG绘制命令都需要一个MeshID, MaterialID和BatchID。前两个很容易理解,但BatchID更微妙。可以将BatchID视为“某种批处理”。要渲染地板,需要注册一种批次,定义如下:

  1. “unity_ObjectToWorld”属性是一个从BRG原始缓冲区的偏移量0开始的数组;
  2. " unity_WorldToObject "属性是一个从偏移量153,600开始的数组;
  3. “_BaseColor”属性是一个数组,从偏移量307,200开始;

在创建时注册这种批处理的代码看起来类似于:

int objectToWorldID = Shader.PropertyToID("unity_ObjectToWorld");
        int worldToObjectID = Shader.PropertyToID("unity_WorldToObject");
        int colorID = Shader.PropertyToID("_BaseColor");
        var batchMetadata = new NativeArray<MetadataValue>(3, Allocator.Temp, NativeArrayOptions.UninitializedMemory);

		batchMetadata[0] = CreateMetadataValue(objectToWorldID, 0, true);       // matrices
		batchMetadata[1] = CreateMetadataValue(worldToObjectID, 3200*3*16, true); // inverse matrices
		batchMetadata[2] = CreateMetadataValue(colorID, 3200*3*16*2, true); // colors
		m_batchId = m_BatchRendererGroup.AddBatch(batchMetadata, m_GPUPersistentRawBuffer.bufferHandle, 0, 0);

我们在创建时获得m_batchId,然后可以将其用于每个BRG绘制命令(因此着色器确切地知道如何为这种批处理获取数据)。

提示:BatchRendererGroup。AddBatch不是渲染命令。它用于注册一种批处理,用于将来的呈现命令。

The devil’s in the details: GLES exception

到目前为止,我们可以动画地板细胞,将阴影复制系统内存缓冲区上传到GPU,并使用3200个实例的单个DrawCommand渲染所有细胞。

这将在大多数平台上工作:DirectX, Vulkan, Metal和各种游戏控制台,但不支持GLES。问题是大多数GLES 3.0设备无法在顶点阶段访问SSBO(即GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS值为0),因此,当图形API设置为GLES时,BRG将使用恒定缓冲区或UBO来存储原始数据。

这增加了约束:一个恒定的缓冲区可以是任何大小,但只有一小部分(一个窗口)是可见的,在任何给定的时间,当着色器正在运行。窗口大小取决于硬件和驱动程序,但普遍接受的值是16 KiB。

提示: 在UBO模式下,您应该始终使用BatchRendererGroup.GetConstantBufferMaxWindowSize() API来获得正确的BRG窗口大小。

让我们看看如果我们想在GLES上运行,我们的代码是如何变化的。对于底层单元,数据总量为350kib。我们不能做一个单一的drawstance(3200),因为着色器不能一次看到350kib。因此,我们必须在UBO中拆分数据,以最大限度地提高每次抽取的实例数量,使其适合16 KiB块。一个层单元是112字节(两个矩阵和一种颜色),因此您可以在一个16 KiB块中容纳16,384除以112或146个实例。为了呈现3200个实例,我们需要发出21个drawinststance(146)和最后一个drawinststance(134)。

现在,350KiB的UBO将被分成22个窗口块,每个16KiB,如下所示:

图4:在GLES中,原始缓冲区是UBO(而不是SSBO)。3200个实例的数据被分成22个窗口。每个drawinstance(146)将从16 KiB区域获取数据。请注意,最后一个窗口只包含134个实例,这就是为什么最后一个黄色、绿色和蓝色区域之间有一个小间隙。

提示:在UBO模式下,每个窗口偏移量应该对齐到batchrenderergroup . getconstantbufferoffsetalalignment()。典型的对齐值范围是4到256字节。

在GLES中,由于UBO和16个KiB窗口,您需要注册22个BatchID来存储每个窗口的偏移量。然后初始化代码需要一个循环:

 // Register one BatchID per 16KiB window, using the right offsets
        m_batchIDs = new BatchID[m_windowCount];
        for (int b = 0; b < m_windowCount; b++)
        {
            batchMetadata[0] = CreateMetadataValue(objectToWorldID, 0, true);       // matrices
            batchMetadata[1] = CreateMetadataValue(worldToObjectID, m_maxInstancePerWindow * 3 * 16, true); // inverse matrices
            batchMetadata[2] = CreateMetadataValue(colorID, m_maxInstancePerWindow * 3 * 2 * 16, true); // colors
            int offset = b * m_alignedGPUWindowSize;
            m_batchIDs[b] = m_BatchRendererGroup.AddBatch(batchMetadata, m_GPUPersistentInstanceData.bufferHandle, (uint)offset,(uint)m_alignedGPUWindowSize);

提示:  为了在游戏示例中支持GLES (UBO)和其他图形API (SSBO), BRG_Container.cs在初始化时设置了一些变量。在SSBO模式下,m_windowCount为1,m_alignedGPUWindowSize为总缓冲区大小。在UBO模式下,m_alignedGPUWindowSize为16 KiB, m_windowCount包含16 KiB块的个数。(16kib的值是为了可读性。使用GetConstantBufferMaxWindowSize() API来获得正确的值。

上传数据

一旦CPU更新了系统内存中的所有矩阵和颜色,您就可以将数据上传到GPU。这是通过BRG_Container完成的。UploadGpuData函数。由于SoA数据模型的原因,您不能上传单个内存块。对于碎片,缓冲区为16,384项。在GLES模式下,如果屏幕上有16,384个碎片,这意味着每个16 KiB的113个窗口。

但是如果在一个给定的框架中只有5300个碎片立方体呢?因为每个窗口有146个项目,这意味着应该上传前36个连续的16 KiB窗口,这样您就可以使用单个SetData (36x16 KiB)。在最后一个窗口中,应该只显示44个碎片立方体。要上传44个矩阵,反转矩阵和颜色,并使用三个SetData命令。在最后,应该发出四个SetData命令。

最多四个GfxBuffer。需要使用SetData命令上传N个项目。

BRG主用户回呼

BRG的主要入口点是在创建时提供的剔除回调函数。原型看起来像:

public JobHandle OnPerformCulling(BatchRendererGroup rendererGroup, BatchCullingContext cullingContext, BatchCullingOutput cullingOutput, IntPtr userContext)

你在这个回调中的代码负责两件事:

  1. 将所有绘制命令生成到输出BatchCullingOut结构中;
  2. 在您自己的筛选代码中使用(或不使用)BatchCullingContext只读结构体中提供的信息;

通用场景

注意:如果您希望启动异步作业来执行这些操作,则回调将返回JobHandle。引擎将使用它在需要结果的点同步,因此您的命令生成代码不会阻塞主线程。

BatchCullingContext包含像相机矩阵,相机截锥体计划等信息。基本上,您需要剔除所有数据并生成更少的绘制命令。在示例中,所有对象都适合相机视图(地板细胞和碎片),因此不需要使用剔除代码。

BatchCullingOutputDrawCommands结构包含各种数据,包括数组。为这些数组分配本机内存是用户的责任。引擎负责在数据被消耗后释放内存(你负责分配,Unity负责释放)。内存分配应该是Allocator.TempJob类型。

 private static T* Malloc<T>(uint count) where T : unmanaged
    {
        return (T*)UnsafeUtility.Malloc(
            UnsafeUtility.SizeOf<T>() * count,
            UnsafeUtility.AlignOf<T>(),
            Allocator.TempJob);
    }

您应该分配的第一个数组是可见性int数组。在示例中,我们假设所有内容都是可见的,所以我们只是用增量值填充可见性int数组,例如{0,1,2,3,4,…}。

Draw commands generation

一个BRG绘制命令几乎是一个GPU drawstance调用。分配和填充最重要的数组是BatchDrawCommand。假设当前帧中有4737个碎片立方体。

m_maxInstancePerWindow在GLES模式下为146。你可以计算绘制命令的数量,并使用m_instanceCount除以m_maxInstancePerWindow的上限值来分配缓冲区:

int drawCommandCount = (m_instanceCount + m_maxInstancePerWindow - 1) / m_maxInstancePerWindow;
drawCommands.drawCommands = Malloc<BatchDrawCommand>((uint)drawCommandCount);

为了避免在几个绘制命令中重复类似的参数,BatchCullingOutputDrawCommands有一个BatchDrawRange结构数组。您可以在BatchDrawRange中设置各种参数。filterSettings,如renderingLayerMask,接收阴影标志等。由于所有绘制命令将共享相同的渲染设置,您可以分配一个单一的DrawCommandRange结构,它将从绘制命令0应用并包含所有drawCommandCount命令。

drawCommands.drawRanges[0] = new BatchDrawRange
{
    drawCommandsBegin = 0,
    drawCommandsCount = (uint)drawCommandCount,
    filterSettings = new BatchFilterSettings
    {
        renderingLayerMask = 1,
        layer = 0,
        motionMode = MotionVectorGenerationMode.Camera,
        shadowCastingMode = m_castShadows ? ShadowCastingMode.On : ShadowCastingMode.Off,
        receiveShadows = true,
        staticShadowCaster = false,
        allDepthSorted = false
    }
};

然后,填充绘制命令。每个BatchDrawCommand包含一个meshID, batchID(知道如何使用元数据)和materialID。它还包含可见性int数组缓冲区中的起始偏移量。因为我们在上下文中不需要任何截锥体剔除,所以我们用{0,1,2,3,…}填充可见性数组。然后,所有绘制命令将引用相同的{0,1,2,3,…} indirection,因此每个BatchDrawCommand将使用0作为可见性数组的开始偏移量。下面的代码分配和填充所有需要的绘制命令:

drawCommands.drawCommands = Malloc<BatchDrawCommand>((uint)drawCommandCount);
int left = m_instanceCount;
for (int b = 0; b < drawCommandCount; b++)
{
    int inBatchCount = left > maxInstancePerDrawCommand ? maxInstancePerDrawCommand : left;
    drawCommands.drawCommands[b] = new BatchDrawCommand
    {
        visibleOffset = (uint)0,    // all draw command is using the same {0,1,2,3...} visibility int array
        visibleCount = (uint)inBatchCount,
        batchID = m_batchIDs[b],
        materialID = m_materialID,
        meshID = m_meshID,
        submeshIndex = 0,
        splitVisibilityMask = 0xff,
        flags = BatchDrawCommandFlags.None,
        sortingPosition = 0
    };
    left -= inBatchCount;
}

End.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1127214.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Git基础教程

一、Git简介 1、什么是Git&#xff1f; Git是一个开源的分布式版本控制系统&#xff0c;用于敏捷高效地处理任何或大或小的项目。 Git是Linus Torvalds为了帮助管理Linux内核开发而开发的一个开放源代码的版本控制软件。 Git与常用的版本控制工具CVS、Subversion等不同&#…

你是否能应对每天数十亿次的IP访问?Top100查找技术解析

亲爱的小伙伴们&#xff0c;大家好&#xff01;我是小米&#xff0c;一个热爱技术&#xff0c;热衷分享的IT小伙伴。今天&#xff0c;我要和大家聊一个程序员面试常见的问题&#xff1a;如何在每天海量IP访问的情况下&#xff0c;使用Java找出访问频率最高的Top 100&#xff1f…

【LeetCode刷题-数组】--344.反转字符串

344.反转字符串 使用双指针&#xff1a; class Solution {public void reverseString(char[] s) {int l 0,r s.length-1;while(l<r){char c s[l];s[l] s[r];s[r] c; l;r--;}} }

鸿蒙HarmonyOS应用开发:多码识别

华为HMS Core的扫码接口更适用于简单的个人使用。在商业环境&#xff0c;如货架、医用试管、图书馆书架等&#xff0c;常常遇到复杂的多码扫描需求&#xff0c;这时需要专业的扫码SDK。尽管当前市场上的主流商业SDK尚未支持鸿蒙HarmonyOS&#xff0c;但我们仍可以通过HTTP请求来…

「干货分享」如何使用CLion轻松解决C++开发者的7大痛点

CLion是一款专为开发C及C所设计的跨平台IDE&#xff0c;它是以IntelliJ为基础设计的&#xff0c;包含了许多智能功能来提高开发人员的生产力。这种强大的IDE帮助开发人员在Linux、OS X和Windows上来开发C/C&#xff0c;同时它还使用智能编辑器来提高代码质量、自动代码重构并且…

【UE】一个扫描效果(不使用后期处理体积)

效果 步骤 一、制作扫描效果 1. 在3dsmax中新建一个圆形 设置半径为50mm&#xff0c;勾选“自适应” 转换为可编辑多边形 导出 2. 打开虚幻编辑器&#xff0c;导入制作好的模型 3. 创建一个材质&#xff0c;这里命名为“M_Sphere” 打开“M_Sphere”&#xff0c;添加一个“V…

PCIe总线中Root Complex(RC)

在不同的处理器系统中&#xff0c;RC的实现有较大差异。PCIe总线规范并没有规定RC的实现细则。在有些处理器系统中&#xff0c;RC相当于PCIe主桥&#xff0c;也有的处理器系统也将PCIe主桥称为PCIe总线控制器。而在x86处理器系统中&#xff0c;RC除了包含PCIe总线控制器之外&am…

C语言:实现对单链表的反转 函数封装

需求&#xff1a; 实现对单链表的反转 代码实现&#xff1a; #include <stdio.h>typedef struct node{int data;struct node* next; }NODE;void PrintLink(NODE* phead) {NODE* p phead;while(p ! NULL){printf("%d ",p->data);p p->next;}printf(&…

好商品好内容好运营,图文免佣这些爆单技巧你会吗?

图文带货正在成为抖音电商商家们生意增长的新阵地。不同于平台传统的短视频、直播带货&#xff0c;图文体裁以其用户需求大、消费粘性高、深度互动强、正向反馈好的优势&#xff0c;令商家们看到生意新可能&#xff1b;而几张图背景音乐就能实现图文带货的便捷&#xff0c;也为…

瞬态抑制二极管TVS的核心参数?|深圳比创达电子EMC(上)

TVS二极管具有响应速度快、漏电流小、钳位电压稳以及无寿命衰减的特性&#xff0c;从小到信号线静电防护&#xff0c;大到电力系统抗雷击浪涌&#xff0c;TVS都发挥着至关重要的作用。本章对瞬态抑制二极管TVS核心参数展开分析&#xff0c;供产品选型参考。接下来就跟着深圳比创…

PCIe架构的处理器系统介绍

不同的处理器系统中&#xff0c;PCIe体系结构的实现方式不尽相同。PCIe体系结构以Intel的x86处理器为蓝本实现&#xff0c;已被深深地烙下x86处理器的印记。在PCIe总线规范中&#xff0c;有许多内容是x86处理器独有的&#xff0c;也仅在x86处理器的Chipset中存在。在PCIe总线规…

初识Node.js开发

一、Node.js是什么 1.node.js是什么 官方对Node.js的定义&#xff1a; Node.js是一个基于V8 JavaScript引擎的JavaScript运行时环境。 也就是说Node.js基于V8引擎来执行JavaScript的代码&#xff0c;但是不仅仅只有V8引擎&#xff1a; 前面我们知道V8可以嵌入到任何C 应用…

保证跨境商城源码稳定性的重要性

稳定性保证源码的可靠性 在跨境电商领域&#xff0c;跨境商城源码的稳定性对于商城运营至关重要。一个稳定、可靠的源码能够确保商城的正常运行&#xff0c;提升用户体验&#xff0c;增加交易成功率&#xff0c;并为商城的可持续发展打下基础。 稳定性保证系统顺利运行 跨境商城…

第四个 1024,属于我们!

第四个 1024&#xff0c;只属于我们&#xff0c;愿人世间没有 BUG&#xff01; if (today1024) {printf("拒绝加班&#xff01;")} else {printf("没有Bug&#xff01;")}2023 - 1024 加油&#xff01; 我是故屿 一个在互联网苟且偷生的工具人 若非生…

内衣洗衣机便宜好用的牌子有哪些?四款公认好用洗内裤机推荐

在机器解放了双手的时代中&#xff0c;洗衣机走进了千家万户&#xff0c;虽然在某种程度上缓解了人们手洗衣服的压力&#xff0c;但还是有不少人选择了人工手洗自己的内衣内裤&#xff0c;甚至连袜子都是手工洗的&#xff0c;这让人很是郁闷&#xff0c;倒不是说洗衣机不方便&a…

Jenkins CLI二次开发工具类

使用Jenkins CLI进行二次开发 使用背景 公司自研CI/DI平台&#xff0c;借助JenkinsSonarQube进行代码质量管理。对接版本 Jenkins版本为&#xff1a;Version 2.428 SonarQube版本为&#xff1a;Community EditionVersion 10.2.1 (build 78527)技术选型 Java对接Jenkins有第…

研发效能(DevOps)职业技术认证-第六期开班啦丨IDCF

本证书是由国家工业和信息化部教育与考试中心颁发的职业技术证书&#xff0c;也是国内首个《研发效能&#xff08;DevOps&#xff09;工程师职业技术认证》。该《认证》对研发效能&#xff08;DevOps&#xff09;工程师的职业技术分为初级、中级、高级三个专业等级。 IDCF社区…

nrf52832 PWM配置

PWM使用时sdk_config.h文件中配置如下:#define PWM_ENABLED 1 #define PWM0_ENABLED 1 #define NRFX_PWM_ENABLED 1 #define NRFX_PWM0_ENABLED 0sdk_config.h 文件中添加下列配置 // <e> NRFX_PWM_ENABLED - nrfx_pwm - PWM peripheral driver // #ifndef NRFX_PWM_ENA…

uniapp实现webview页面关闭功能

实现思路&#xff1a; 1.关闭按钮是使用原生button添加的close属性。&#xff08;见page.json页面&#xff09; 2.监听关闭按钮的方法。&#xff08;onNavigationBarButtonTap&#xff09; 3.写实现关闭webview所有页面的逻辑。 废话不多说&#xff0c;直接上代码 1.page.…

Java的泛型高级篇

以前在看源码的时候&#xff0c;老是能够看见大写的字母&#xff1a;T D...... 他就是用来标志泛型的。就拿ArrarList E就是 &#xff1a;element的简写 T就是Type。 另外一种就很高级&#xff0c;父级和自己约束性泛型&#xff1a; 定义一个顶级父类&#xff1a;TimeTest publ…