【Unity】万人同屏高级篇, BRG Jobs实战应用, 海量物体同屏

news2024/11/24 6:30:43

博文开发测试环境:

  • Unity:Unity 2022.3.10f1,URP 14.0.8,Burst 1.8.8,Jobs 0.70.0-preview.7,热更HybridCLR 4.0.6
  • PC:Win11,CPU i7-13700KF,GPU 3070 8G,RAM 32G;
  • 移动端:Android,骁龙8 gen2,RAM 12G;

上篇博文通过最基本的自定义BRG(Batch Renderer Group) + RVO避障实现了10万人同屏动态避障:【Unity】十万人同屏寻路? 基于Dots技术的多线程RVO2避障_TopGames的博客-CSDN博客

 这里的BRG功能并不完善,不支持多种Mesh和Material渲染,没有拆分Batch,没有Culling处理,不会剔除相机视口外的物体。并且没有根据渲染数量拆分多个Draw Command。

BRG代码写起来并不容易,此博文基于Unity中国DOTS技术主管开源的一个BRG示例工程修改,这里仅描述主要的实现原理:

参考文章:Unity Open Day 北京站-技术专场:深入理解 Entities Gr - 技术专栏 - Unity官方开发者社区

BRG示例工程:
https://github.com/vinsli/batch-renderericon-default.png?t=N7T8https://github.com/vinsli/batch-renderer

 实现目标:

为了方便使用BRG功能,需要封装一个BatchRenderComponent脚本。不使用ECS,仅使用传统创建GameObject的方式与BRG无缝衔接,最大程度上不改变传统工作流的同时使用BRG合批渲染大幅提升性能;

其中GameObject是只有Transform组件的空物体,不使用Unity渲染组件,通过封装后的BatchRenderComponent进行合批渲染。

最终效果:

PC端5W人, AOT模式(不使用HybridCLR),开启阴影:

Android端5K人,AOT模式(不使用HybridCLR),开启阴影:

 Android端5K人, HybridCLR热更模式,开启阴影:

 由于HybridCLR不支持Burst加速,Jobs代码以解释方式执行,所以相比AOT性能大打折扣。

 一,支持多Mesh/多Material

 使用BRG必须开启SRP Batcher,  SRP支持相同Material合批。因此支持多Material就需要根据不同Material拆分Batch,针对不同Material使用多个Batch渲染。

每个物体需要向GPU上传以下数据:

  • 两个3x4矩阵,决定物体渲染的位置/旋转/缩放;
  • _BaseColor,物体混合颜色;
  • _ClipId, GPU动画id, 用于切换动画;
int objectToWorldID = Shader.PropertyToID("unity_ObjectToWorld");
int worldToObjectID = Shader.PropertyToID("unity_WorldToObject");
int colorID = Shader.PropertyToID("_BaseColor");
int gpuAnimClipId = Shader.PropertyToID("_ClipId");

如果Shader还需要动态修改其它参数需要自行扩展,根据参数所占内存还需要重新组织内存分配;

注意,必须在Shader Graph中把参数类型设置为Hybrid Per Installed,否则无法正常将数据传递给shader:

 将每个物体依赖的数据组织成一个struct便于管理RendererNode,由于要在Jobs中使用所以必须为struct类型:

using Unity.Mathematics;
using static Unity.Mathematics.math;
using Unity.Burst;

[BurstCompile]
public struct RendererNode
{
    public RendererNodeId Id { get; private set; }
    public bool Enable
    {
        get
        {
            return active && visible;
        }
    }
    /// <summary>
    /// 是否启用
    /// </summary>
    public bool active;
    /// <summary>
    /// 是否在视口内
    /// </summary>
    public bool visible;
    /// <summary>
    /// 位置
    /// </summary>
    public float3 position;
    /// <summary>
    /// 旋转
    /// </summary>
    public quaternion rotation;
    /// <summary>
    /// 缩放
    /// </summary>
    public float3 localScale;
    /// <summary>
    /// 顶点颜色
    /// </summary>
    public float4 color;

    /// <summary>
    /// 动画id
    /// </summary>
    public float4 animClipId;
    /// <summary>
    /// Mesh的原始AABB(无缩放)
    /// </summary>
    public AABB unscaleAABB;

    /// <summary>
    /// 受缩放影响的AABB
    /// </summary>
    public AABB aabb
    {
        get
        {
            //var result = unscaleAABB;
            //result.Extents *= localScale;
            return unscaleAABB;
        }
    }
    public bool IsEmpty
    {
        get
        {
            return unscaleAABB.Size.Equals(Unity.Mathematics.float3.zero);
        }
    }
    public static readonly RendererNode Empty = new RendererNode();
    public RendererNode(RendererNodeId id, float3 position, quaternion rotation, float3 localScale, AABB meshAABB)
    {
        this.Id = id;
        this.position = position;
        this.rotation = rotation;
        this.localScale = localScale;
        this.unscaleAABB = meshAABB;
        this.color = float4(1);
        this.active = false;
        this.visible = true;
        this.animClipId = 0;
    }
    /// <summary>
    /// 构建矩阵
    /// </summary>
    /// <returns></returns>
    [BurstCompile]
    public float4x4 BuildMatrix()
    {
        return Unity.Mathematics.float4x4.TRS(position, rotation, localScale);
    }
}

初始化渲染数据Buffer列表:

为了维护数据简单,并避免物体数量变化后频繁重新创建列表,所以根据RendererResource的Capacity大小,维护一个固定长度的列表。并且根据不同的RendererResource拆分多个渲染批次:

private void CreateRendererDataCaches()
    {
        m_BatchesVisibleCount.Clear();
        int index = 0;
        foreach (var rendererRes in m_RendererResources)
        {
            var drawKey = rendererRes.Key;
            m_BatchesVisibleCount.Add(drawKey, 0);
            NativeList<int> perBatchNodes;
            if (!m_DrawBatchesNodeIndexes.ContainsKey(drawKey))
            {
                perBatchNodes = new NativeList<int>(2048, Allocator.Persistent);
                m_DrawBatchesNodeIndexes.Add(drawKey, perBatchNodes);

                NativeQueue<BatchDrawCommand> batchDrawCommands = new NativeQueue<BatchDrawCommand>(Allocator.Persistent);
                m_BatchDrawCommandsPerDrawKey.Add(drawKey, batchDrawCommands);
            }
            else
            {
                perBatchNodes = m_DrawBatchesNodeIndexes[drawKey];
            }
            for (int i = 0; i < rendererRes.capacity; i++)
            {

                var color = SpawnUtilities.ComputeColor(i, rendererRes.capacity);
                var aabb = rendererRes.mesh.bounds.ToAABB();
                var node = new RendererNode(new RendererNodeId(drawKey, index), Unity.Mathematics.float3.zero, Unity.Mathematics.quaternion.identity, float3(1), aabb);
                node.color = color;
                perBatchNodes.Add(index);
                m_AllRendererNodes[index++] = node;
            }
        }
    }

组织拆分后每个Batch的数据:

由于不同硬件性能不同,单个Draw Command数量是有上限的,所以还需要根据渲染数量拆分至多个BatchDrawCommand。

private void GenerateBatches()
    {
#if UNITY_ANDROID || UNITY_IOS
        int kBRGBufferMaxWindowSize = 16 * 256 * 256;
#else
        int kBRGBufferMaxWindowSize = 16 * 1024 * 1024;
#endif
        const int kItemSize = (2 * 3 + 2);  //每个物体2个3*4矩阵,1个颜色值,1个动画id,内存大小共8个float4
        m_MaxItemPerBatch = ((kBRGBufferMaxWindowSize / kSizeOfFloat4) - 4) / kItemSize;  // -4 "float4" for 64 first 0 bytes ( BRG contrainst )
                                                                                          // if (_maxItemPerBatch > instanceCount)
                                                                                          //     _maxItemPerBatch = instanceCount;

        foreach (var drawKey in m_DrawBatchesNodeIndexes.GetKeyArray(Allocator.Temp))
        {
            if (!m_BatchesPerDrawKey.ContainsKey(drawKey))
            {
                m_BatchesPerDrawKey.Add(drawKey, new NativeList<int>(128, Allocator.Persistent));
            }

            var instanceCountPerDrawKey = m_DrawBatchesNodeIndexes[drawKey].Length;
            m_WorldToObjectPerDrawKey.Add(drawKey, new NativeArray<float4>(instanceCountPerDrawKey * 3, Allocator.Persistent));
            m_ObjectToWorldPerDrawKey.Add(drawKey, new NativeArray<float4>(instanceCountPerDrawKey * 3, Allocator.Persistent));

            var maxItemPerDrawKeyBatch = m_MaxItemPerBatch > instanceCountPerDrawKey ? instanceCountPerDrawKey : m_MaxItemPerBatch;
            //gather batch count per drawkey
            int batchAlignedSizeInFloat4 = BufferSizeForInstances(kBytesPerInstance, maxItemPerDrawKeyBatch, kSizeOfFloat4, 4 * kSizeOfFloat4) / kSizeOfFloat4;
            var batchCountPerDrawKey = (instanceCountPerDrawKey + maxItemPerDrawKeyBatch - 1) / maxItemPerDrawKeyBatch;

            //create instance data buffer
            var instanceDataCountInFloat4 = batchCountPerDrawKey * batchAlignedSizeInFloat4;
            var instanceData = new GraphicsBuffer(GraphicsBuffer.Target.Raw, GraphicsBuffer.UsageFlags.LockBufferForWrite, instanceDataCountInFloat4, kSizeOfFloat4);
            m_InstanceDataPerDrawKey.Add(drawKey, instanceData);

            //generate srp batches
            int left = instanceCountPerDrawKey;
            for (int i = 0; i < batchCountPerDrawKey; i++)
            {
                int instanceOffset = i * maxItemPerDrawKeyBatch;
                int gpuOffsetInFloat4 = i * batchAlignedSizeInFloat4;

                var batchInstanceCount = left > maxItemPerDrawKeyBatch ? maxItemPerDrawKeyBatch : left;
                var drawBatch = new SrpBatch
                {
                    DrawKey = drawKey,
                    GraphicsBufferOffsetInFloat4 = gpuOffsetInFloat4,
                    InstanceOffset = instanceOffset,
                    InstanceCount = batchInstanceCount
                };

                m_BatchesPerDrawKey[drawKey].Add(m_DrawBatches.Length);
                m_DrawBatches.Add(drawBatch);
                left -= batchInstanceCount;
            }
        }

        int objectToWorldID = Shader.PropertyToID("unity_ObjectToWorld");
        int worldToObjectID = Shader.PropertyToID("unity_WorldToObject");
        int colorID = Shader.PropertyToID("_BaseColor");
        int gpuAnimClipId = Shader.PropertyToID("_ClipId");
        var batchMetadata = new NativeArray<MetadataValue>(4, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
        for (int i = 0; i < m_DrawBatches.Length; i++)
        {
            var drawBatch = m_DrawBatches[i];
            var instanceData = m_InstanceDataPerDrawKey[drawBatch.DrawKey];

            var baseOffset = drawBatch.GraphicsBufferOffsetInFloat4 * kSizeOfFloat4;
            int gpuAddressOffset = baseOffset + 64;
            batchMetadata[0] = CreateMetadataValue(objectToWorldID, gpuAddressOffset, true);       // matrices
            gpuAddressOffset += kSizeOfPackedMatrix * drawBatch.InstanceCount;
            batchMetadata[1] = CreateMetadataValue(worldToObjectID, gpuAddressOffset, true); // inverse matrices
            gpuAddressOffset += kSizeOfPackedMatrix * drawBatch.InstanceCount;
            batchMetadata[2] = CreateMetadataValue(colorID, gpuAddressOffset, true); // colors
            gpuAddressOffset += kSizeOfFloat4 * drawBatch.InstanceCount;
            batchMetadata[3] = CreateMetadataValue(gpuAnimClipId, gpuAddressOffset, true);

            if (BatchRendererGroup.BufferTarget == BatchBufferTarget.ConstantBuffer)
            {
                drawBatch.BatchID = m_BRG.AddBatch(batchMetadata, instanceData.bufferHandle, (uint)BatchRendererGroup.GetConstantBufferOffsetAlignment(), (uint)BatchRendererGroup.GetConstantBufferMaxWindowSize());
            }
            else
            {
                drawBatch.BatchID = m_BRG.AddBatch(batchMetadata, instanceData.bufferHandle);
            }
            m_DrawBatches[i] = drawBatch;
        }
    }

二,Culling剔除视口外物体渲染

使用Jobs判定物体AABB包围盒是否在相机视口内,把视口外RendererNode的visible标记为false

[BurstCompile]
    private unsafe struct CullingJob : IJobParallelFor
    {
        [ReadOnly]
        public NativeArray<Plane> CullingPlanes;
        [DeallocateOnJobCompletion]
        [ReadOnly]
        public NativeArray<SrpBatch> Batches;
        [ReadOnly]
        public NativeArray<RendererNode> Nodes;
        [ReadOnly]
        public NativeList<int> NodesIndexes;
        [ReadOnly]
        [NativeDisableContainerSafetyRestriction]
        public NativeArray<float4> ObjectToWorldMatrices;
        [ReadOnly]
        public int DrawKeyOffset;

        [WriteOnly]
        [NativeDisableUnsafePtrRestriction]
        public int* VisibleInstances;
        [WriteOnly]
        public NativeQueue<BatchDrawCommand>.ParallelWriter DrawCommands;

        public void Execute(int index)
        {
            var batchesPtr = (SrpBatch*)Batches.GetUnsafeReadOnlyPtr();
            var objectToWorldMatricesPtr = (float4*)ObjectToWorldMatrices.GetUnsafeReadOnlyPtr();
            ref var srpBatch = ref batchesPtr[index];
            int visibleCount = 0;
            int batchOffset = DrawKeyOffset + srpBatch.InstanceOffset;
            int idx = 0;
            for (int instanceIdx = 0; instanceIdx < srpBatch.InstanceCount; instanceIdx++)
            {
                idx = srpBatch.InstanceOffset + instanceIdx;

                int nodeIndex = NodesIndexes[idx];
                var node = Nodes[nodeIndex];
                if (!node.active) continue;
                //Assume only have 1 culling split and have 6 culling planes
                var matrixIdx = idx * 3;
                var worldAABB = Transform(ref objectToWorldMatricesPtr[matrixIdx], ref objectToWorldMatricesPtr[matrixIdx + 1], ref objectToWorldMatricesPtr[matrixIdx + 2], node.aabb);
                if (!(node.visible = (Intersect(CullingPlanes, ref worldAABB) != FrustumPlanes.IntersectResult.Out)))
                    continue;

                VisibleInstances[batchOffset + visibleCount] = instanceIdx;
                visibleCount++;
            }

            if (visibleCount > 0)
            {
                var drawKey = srpBatch.DrawKey;
                DrawCommands.Enqueue(new BatchDrawCommand
                {
                    visibleOffset = (uint)batchOffset,
                    visibleCount = (uint)visibleCount,
                    batchID = srpBatch.BatchID,
                    materialID = drawKey.MaterialID,
                    meshID = drawKey.MeshID,
                    submeshIndex = (ushort)drawKey.SubmeshIndex,
                    splitVisibilityMask = 0xff,
                    flags = BatchDrawCommandFlags.None,
                    sortingPosition = 0
                });
            }
        }
    }

 三,添加/移除渲染物体:

添加Renderer,实际上就是从RendererNode列表中找出空闲的RendererNode用来存放数据。

移除Renderer,就是把RendererNode的active设置为false置为空闲状态

public RendererNodeId AddRenderer(int resourceIdx, float3 pos, quaternion rot, float3 scale)
    {
        if (resourceIdx < 0 || resourceIdx >= m_RendererResources.Count)
        {
            return RendererNodeId.Null;
        }

        var rendererRes = m_RendererResources[resourceIdx];
        var nodesIndexes = m_DrawBatchesNodeIndexes[rendererRes.Key];
        var tempOutputs = new NativeList<int>(Allocator.TempJob);
        var jobs = new GetInvisibleRendererNodeJob
        {
            Nodes = m_AllRendererNodes,
            NodesIndexes = nodesIndexes,
            RequireCount = 1,
            Outputs = tempOutputs
        };
        jobs.Schedule().Complete();
        if (jobs.Outputs.Length < 0)
        {
            Log.Warning("添加Renderer失败, Output Index invaluable");
            tempOutputs.Dispose();
            return RendererNodeId.Null;
        }
        int index = jobs.Outputs[0];
        tempOutputs.Dispose();
        var renderer = m_AllRendererNodes[index];
        renderer.position = pos;
        renderer.rotation = rot;
        renderer.localScale = scale;
        renderer.active = true;
        m_AllRendererNodes[index] = renderer;
        m_BatchesVisibleCount[rendererRes.Key]++;
        m_TotalVisibleCount++;
        return renderer.Id;
    }
    /// <summary>
    /// 移除渲染节点
    /// </summary>
    /// <param name="id"></param>
    public void RemoveRenderer(RendererNodeId id)
    {
        var node = m_AllRendererNodes[id.Index];
        node.active = false;
        m_AllRendererNodes[id.Index] = node;

        m_BatchesVisibleCount[id.BatchKey]--;
        m_TotalVisibleCount--;
    }


[BurstCompile]
    private struct GetInvisibleRendererNodeJob : IJob
    {
        [ReadOnly]
        public NativeArray<RendererNode> Nodes;
        [ReadOnly]
        public NativeList<int> NodesIndexes;
        [ReadOnly]
        public int RequireCount;
        public NativeList<int> Outputs;

        public void Execute()
        {
            int num = 0;
            for (int i = 0; i < NodesIndexes.Length; i++)
            {
                int curIndex = NodesIndexes[i];
                var node = Nodes[curIndex];

                if (!node.Enable)
                {
                    Outputs.Add(curIndex);
                    if (++num >= RequireCount)
                    {
                        break;
                    }
                }
            }
        }
    }

四,同步RVO位置数据到RendererNode:

 由于已经把所有RendererNode组织到了一个NativeArray里,所以可以非常容易得使用Jobs批量同步渲染位置、旋转等信息;

    /// <summary>
    /// 通过JobSystem更新渲染数据
    /// </summary>
    /// <param name="agents"></param>
    internal void SetRendererData(NativeArray<AgentData> agentsData)
    {
        var job = new SyncRendererNodeTransformJob
        {
            AgentDataArr = agentsData,
            Nodes = m_AllRendererNodes
        };
        job.Schedule(agentsData.Length, 64).Complete();
    }


[BurstCompile]
    private struct SyncRendererNodeTransformJob : IJobParallelFor
    {
        [ReadOnly] public NativeArray<AgentData> AgentDataArr;
        [NativeDisableParallelForRestriction]
        public NativeArray<RendererNode> Nodes;

        public void Execute(int index)
        {
            var agentDt = AgentDataArr[index];
            var node = Nodes[agentDt.rendererIndex];
            node.position = agentDt.worldPosition;
            node.rotation = agentDt.worldQuaternion;
            node.animClipId = agentDt.animationIndex;
            Nodes[agentDt.rendererIndex] = node;
        }
    }

未完待续...

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

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

相关文章

[SWPUCTF 2022 新生赛]善哉善哉题目解析

这一题结合的东西挺多的&#xff0c;但也不是说很难。 他先给了压缩包&#xff0c;正常压缩他没有密码卡你压缩出来是一张图片 打开看没什么提示&#xff0c;就按自己的思路走先查看属性&#xff0c;一般属性是最优先查看的&#xff0c;因为他在属性藏东西的地方不多&#xff…

网络安全(黑客)从零开始的自学指南(第二章)

第二章&#xff1a;黑客入门 2.1 什么是黑客 黑客是指具有高超计算机技术和网络知识的个人或组织&#xff0c;通过技术手段侵入他人的计算机系统或网络&#xff0c;获取非法利益或破坏目标系统的行为。黑客可以分为两类&#xff1a;道德黑客&#xff08;白帽黑客&#xff0…

接扫理解.exe文件的结构原理即运行过程

爱像时间&#xff0c;永恒不变而又短暂&#xff1b;爱像流水&#xff0c;浩瀚壮阔却又普普通通。 .exe .exe文件是Windows操作系统中可执行文件的常见格式&#xff0c;它包含了计算机程序的二进制代码以及其他必要的信息&#xff0c;以便操作系统能够加载和执行该程序。下面是…

秒验:可以自定义UI的一键登录服务

一键登录如今成为越来越多移动应用的首选&#xff0c;但千篇一律的登陆界面在引发用户担忧其安全性的同时&#xff0c;也容易让用户在不同APP切换时产生误解。因此&#xff0c;由国内知名移动应用开发服务商MobTech打造的一键登录工具——秒验&#xff0c;通过允许开发者自定义…

【MVDiffusion】完美复刻场景,可多视图设计的生成式模型

文章目录 MVDiffusion1. 自回归 生成 全景图1.1 错误积累1.2 角度变换大 2. 模型结构2.1 多视图潜在扩散模型(mutil-view LDM)2.1.1 Text-conditioned generation model2.1.2 Image&text-conditioned generation model2.1.3 额外的卷积层 2.2 Correspondence-aware Attenti…

使用 TensorFlow 创建 DenseNet 121

一、说明 本篇示意DenseNet如何在tensorflow上实现&#xff0c;DenseNet与ResNet有类似的地方&#xff0c;都有层与层的“短路”方式&#xff0c;但两者对层的短路后处理有所不同&#xff0c;本文遵照原始论文的技术路线&#xff0c;完整复原了DenseNet的全部网络。 图1&#x…

评价指标篇——IOU(交并比)

什么是IoU(Intersection over Union) IoU是一种测量在特定数据集中检测相应物体准确度的一个标准。 即是产生的候选框&#xff08;candidate bound&#xff09;与原标记框&#xff08;ground truth bound&#xff09;的交叠率 即它们的交集与并集的比值。最理想情况是完全重叠…

CVE-2023-5129:libwebp开源库10分漏洞

谷歌为libwebp漏洞分配新的CVE编号&#xff0c;CVSS评分10分。 Libwebp是一个用于处理WebP格式图像编解码的开源库。9月6日&#xff0c;苹果公司安全工程和架构&#xff08;SEAR&#xff09;部门和加拿大多伦多大学研究人员在libwebp库中发现了一个0 day漏洞&#xff0c;随后&…

Linux SSH连接远程服务器(免密登录、scp和sftp传输文件)

1 SSH简介 SSH&#xff08;Secure Shell&#xff0c;安全外壳&#xff09;是一种网络安全协议&#xff0c;通过加密和认证机制实现安全的访问和文件传输等业务。传统远程登录和文件传输方式&#xff0c;例如Telnet、FTP&#xff0c;使用明文传输数据&#xff0c;存在很多的安全…

水果种植与果园监管“智慧化”,AI技术打造智慧果园视频综合解决方案

一、方案背景 我国是水果生产大国&#xff0c;果园种植面积大、产量高。由于果园的位置大都相对偏远、面积较大&#xff0c;值守的工作人员无法顾及到园区每个角落&#xff0c;因此人为偷盗、野生生物偷吃等事件时有发生&#xff0c;并且受极端天气如狂风、雷暴、骤雨等影响&a…

NOSQL Redis 数据持久化 RDB、AOF(二) 恢复

redis 执行flushall 或 flushdb 也会产生dump.rdb文件&#xff0c;但里面是空的。 注意&#xff1a;千万执行&#xff0c;不然rdb文件会被覆盖的。 dump.rdb 文件如何恢复数据 讲备份文件 dump.rdb 移动到redis安装目录并启动服务即可。 dump.rdb 自动触发 和手动触发 自…

Android 更新图标

什么是Android Multidex热更新 • Worktile社区 在不重启app的情况下热更新 &#xff0c;在所有新文件下载完成后&#xff0c;提示用户&#xff0c;是否重启 在不频繁新增图标的情况下可以使用 <adaptive-icon>在AndroidManifest.xml中设置app别名&#xff0c;以实现…

PCB走线的传输延时有多少

信号在PCB上的传输速度虽然很快&#xff0c;但也是存在延时的&#xff0c;一般经验值是6mil/ps。 也就是在PCB上&#xff0c;当信号线走线长度为6mil的时候&#xff0c;信号从驱动端到达接收端需要经过1ps。 信号在PCB上的传输速率&#xff1a; 其中C为信号在真空中的传播速率…

2023年【煤炭生产经营单位(安全生产管理人员)】证考试及煤炭生产经营单位(安全生产管理人员)模拟考试题库

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 煤炭生产经营单位&#xff08;安全生产管理人员&#xff09;证考试是安全生产模拟考试一点通生成的&#xff0c;煤炭生产经营单位&#xff08;安全生产管理人员&#xff09;证模拟考试题库是根据煤炭生产经营单位&…

记一次问题排查

1785年&#xff0c;卡文迪许在实验中发现&#xff0c;把不含水蒸气、二氧化碳的空气除去氧气和氮气后&#xff0c;仍有很少量的残余气体存在。这种现象在当时并没有引起化学家的重视。 一百多年后&#xff0c;英国物理学家瑞利测定氮气的密度时&#xff0c;发现从空气里分离出来…

练[BJDCTF2020]EasySearch

[BJDCTF2020]EasySearch 文章目录 [BJDCTF2020]EasySearch掌握知识解题思路关键paylaod 掌握知识 ​ 目录扫描&#xff0c;index.php.swp文件泄露&#xff0c;代码审计&#xff0c;MD5区块爆破&#xff0c;请求响应包的隐藏信息&#xff0c;.shtml文件RCE漏洞利用 解题思路 …

cpp primer plus笔记01-注意事项

cpp尽量以int main()写函数头而不是以main()或者int main(void)或者void main()写。 cpp尽量上图用第4行的注释而不是用第5行注释。 尽量不要引用命名空间比如:using namespace std; 函数体内引用的命名空间会随着函数生命周期结束而失效&#xff0c;放置在全局引用的命名空…

【LeetCode: 901. 股票价格跨度 | 单调栈】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

【代码随想录】LC 27. 移除元素

文章目录 前言一、题目1、原题链接2、题目描述 二、解题报告1、思路分析2、时间复杂度3、代码详解 三、知识风暴 前言 本专栏文章为《代码随想录》书籍的刷题题解以及读书笔记&#xff0c;如有侵权&#xff0c;立即删除。 一、题目 1、原题链接 27. 移除元素 2、题目描述 二、…

文创行业如何利用软文出圈?媒介盒子告诉你

经济快速发展与社会进步&#xff0c;带来的是人们消费观念的转型&#xff0c;人们的精神需求与文化自信不断增强&#xff0c;随着文化产业和旅游业的不断升级&#xff0c;文创产品凭借独特的概念、创新的形象&#xff0c;吸引许多消费者。那么新时代的文创行业应该如何强势出圈…