Vulkan Tutorial 5 顶点缓冲区

目录

16 顶点缓冲区

顶点着色器

顶点数据

管道顶点输入

17  顶点缓冲区创建

缓冲区创建

内存要求

内存分配

填充顶点缓冲区

 18  暂存缓冲区

传输队列

使用暂存缓冲区

19 索引缓冲区

索引缓冲区创建

使用索引缓冲区


16 顶点缓冲区

我们将用内存中的顶点缓冲区替换顶点着色器中的硬编码顶点数据。我们将从最简单的方法开始,即创建 CPU 可见缓冲区并使用memcpy直接将顶点数据复制到其中。

顶点着色器

首先更改顶点着色器,使其不再在着色器代码本身中包含顶点数据。顶点着色器使用关键字从顶点缓冲区获取输入 in

#version 450

//layout(location = x)注释将索引分配给我们稍后可以用来引用它们的输入
layout(location = 0) in vec2 inPosition;
//inPosition变量和inColor是顶点属性
layout(location = 1) in vec3 inColor;

layout(location = 0) out vec3 fragColor;

void main() {
    gl_Position = vec4(inPosition, 0.0, 1.0);
    fragColor = inColor;
}

顶点数据

我们将顶点数据从着色器代码移动到程序代码中的数组。首先包括 GLM 库,它为我们提供线性代数相关类型,如向量和矩阵。我们将使用这些类型来指定位置和颜色向量。

#include <glm/glm.hpp>
struct Vertex {
    glm::vec2 pos;
    glm::vec3 color;

//顶点绑定描述了在整个顶点中从内存加载数据的速率

    static VkVertexInputBindingDescription getBindingDescription() {
        VkVertexInputBindingDescription bindingDescription{};
//我们所有的逐顶点数据都打包在一个数组中,因此我们只需要一个绑定

bindingDescription.binding = 0;
bindingDescription.stride = sizeof(Vertex);
bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
        return bindingDescription;
    }

//另一个辅助函数来Vertex填充这些结构
static std::array<VkVertexInputAttributeDescription, 2> getAttributeDescriptions() {
        std::array<VkVertexInputAttributeDescription, 2> attributeDescriptions{};
//属性描述结构描述了如何从源自绑定描述的顶点数据块中提取顶点属性。
//我们有两个属性,位置和颜色,所以我们需要两个属性描述结构。
        attributeDescriptions[0].binding = 0;
        attributeDescriptions[0].location = 0;
        attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT;
        attributeDescriptions[0].offset = offsetof(Vertex, pos);

        attributeDescriptions[1].binding = 0;
        attributeDescriptions[1].location = 1;
//该format参数描述属性的数据类型
        attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT;
        attributeDescriptions[1].offset = offsetof(Vertex, color);

        return attributeDescriptions;
    }


};

//GLM 方便地为我们提供了与着色器语言中使用的矢量类型完全匹配的 C++ 类型。

const std::vector<Vertex> vertices = {
    {{0.0f, -0.5f}, {1.0f, 0.0f, 0.0f}},
    {{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}},
    {{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}}
};

stride参数指定从一个条目到下一个条目的字节数,该inputRate参数可以具有以下值之一:

  • VK_VERTEX_INPUT_RATE_VERTEX:移动到每个顶点后的下一个数据条目
  • VK_VERTEX_INPUT_RATE_INSTANCE:移动到每个实例后的下一个数据条目

管道顶点输入

我们现在需要设置图形管道以通过引用createGraphicsPipeline. 找到 vertexInputInfo结构并修改它以引用两个描述:

auto bindingDescription = Vertex::getBindingDescription();
auto attributeDescriptions = Vertex::getAttributeDescriptions();

vertexInputInfo.vertexBindingDescriptionCount = 1;
vertexInputInfo.vertexAttributeDescriptionCount = static_cast<uint32_t>(attributeDescriptions.size());
vertexInputInfo.pVertexBindingDescriptions = &bindingDescription;
vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data();

17  顶点缓冲区创建

Vulkan 中的缓冲区是用于存储可由显卡读取的任意数据的内存区域。它们可以用来存储顶点数据.与我们目前处理的 Vulkan 对象不同,缓冲区不会自动为自己分配内存。

缓冲区创建

创建一个新函数createVertexBuffer调用它。

void createVertexBuffer() {
//创建缓冲区需要我们填充一个VkBufferCreateInfo结构。

VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
//以字节为单位指定缓冲区的大小
bufferInfo.size = sizeof(vertices[0]) * vertices.size();
//指示将要使用缓冲区中的数据的目的
bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
 if (vkCreateBuffer(device, &bufferInfo, nullptr, &vertexBuffer) != VK_SUCCESS) {
        throw std::runtime_error("failed to create vertex buffer!");
    }

}

我们现在可以创建缓冲区了vkCreateBuffer。定义一个类成员来保存缓冲区句柄并调用它vertexBuffer

VkBuffer vertexBuffer;

vkDestroyBuffer(device, vertexBuffer, nullptr);

内存要求

缓冲区已创建,但实际上尚未分配任何内存。为缓冲区分配内存的第一步是使用适当命名的

VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(device, vertexBuffer, &memRequirements);
//缓冲区已创建,但实际上尚未分配任何内存。
//为缓冲区分配内存的第一步是使用适当命名的vkGetBufferMemoryRequirements 函数查询其内存需求。

该VkMemoryRequirements结构具有三个字段:

  • size:所需内存量的大小(以字节为单位)可能与 不同 bufferInfo.size
  • alignment: 缓冲区在分配的内存区域中开始的字节偏移量,取决于bufferInfo.usagebufferInfo.flags
  • memoryTypeBits:适合缓冲区的内存类型的位字段。

显卡可以提供不同类型的内存供分配。每种类型的内存在允许的操作和性能特征方面各不相同。我们需要结合缓冲区的要求和我们自己的应用程序要求来找到合适的内存类型来使用。让我们findMemoryType为此创建一个新函数。

uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) {

    VkPhysicalDeviceMemoryProperties memProperties;
    vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
//结构有两个数组memoryTypes 和memoryHeaps。
//内存堆是不同的内存资源,例如专用 VRAM 和 RAM 中用于 VRAM 耗尽时的交换空间。
//这些堆中存在不同类型的内存。
for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
    if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
//该typeFilter参数将用于指定适合的内存类型的位域
        return i;
    }
}

throw std::runtime_error("failed to find suitable memory type!");
    
}

内存分配

我们现在有办法确定正确的内存类型,所以我们可以通过填充结构来实际分配内存VkMemoryAllocateInfo。

VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);

内存分配现在就像指定大小和类型一样简单,这两者都源自顶点缓冲区的内存要求和所需的属性。创建一个类成员将句柄存储到内存中,并使用vkAllocateMemory.

VkDeviceMemory vertexBufferMemory;

如果内存分配成功,那么我们现在可以使用以下方法将此内存与缓冲区相关联vkBindBufferMemory:前三个参数是不言自明的,第四个参数是内存区域内的偏移量。由于此内存是专门为此顶点缓冲区分配的,因此偏移量很简单0。如果偏移量不为零,则它需要被 整除memRequirements.alignment

vkBindBufferMemory(device, vertexBuffer, vertexBufferMemory, 0);

填充顶点缓冲区

现在是将顶点数据复制到缓冲区的时候了。

void* data;
vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data);
//允许我们访问由偏移量和大小定义的指定内存资源区域。这里的偏移量和大小分别是0和 bufferInfo.size
//倒数第二个参数可用于指定标志,但当前 API 中尚无可用标志。它必须设置为值0。
//最后一个参数指定指向映射内存的指针的输出。

 memcpy(data, vertices.data(), (size_t) bufferInfo.size);
//v\简单地memcpy将顶点数据存储到映射的内存中,然后使用vkUnmapMemor
vkUnmapMemory(device, vertexBufferMemory);

//在渲染操作期间绑定顶点缓冲区
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);

VkBuffer vertexBuffers[] = {vertexBuffer};
VkDeviceSize offsets[] = {0};
vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);
//除了命令缓冲区,还指定偏移量和我们要为其指定顶点缓冲区的绑定数

vkCmdDraw(commandBuffer, static_cast<uint32_t>(vertices.size()), 1, 0, 0);
//调用更改为vkCmdDraw传递缓冲区中的顶点数,而不是硬编码的数字3。

 18  暂存缓冲区

我们现在拥有的顶点缓冲区可以正常工作,但允许我们从 CPU 访问它的内存类型可能不是显卡本身读取的最佳内存类型。最佳内存有 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT标志,通常不能被专用显卡上的 CPU 访问。

传输队列

缓冲区复制命令需要一个支持传输操作的队列系列,使用 表示VK_QUEUE_TRANSFER_BIT任何具有VK_QUEUE_GRAPHICS_BITVK_QUEUE_COMPUTE_BIT 功能的队列系列都已经隐含地支持VK_QUEUE_TRANSFER_BIT操作。我们将创建两个顶点缓冲区。 CPU 可访问内存中的一个暂存缓冲区,用于将数据从顶点数组上传到,最后一个顶点缓冲区位于设备本地内存中。然后,我们将使用缓冲区复制命令将数据从暂存缓冲区移动到实际的顶点缓冲区。

  • 修改QueueFamilyIndicesfindQueueFamilies显式查找带有 bit 的队列系列VK_QUEUE_TRANSFER_BIT,而不是 VK_QUEUE_GRAPHICS_BIT.
  • 修改createLogicalDevice以请求传输队列的句柄
  • 为在传输队列系列上提交的命令缓冲区创建第二个命令池
  • 将资源更改sharingModeVK_SHARING_MODE_CONCURRENT并指定图形和传输队列系列
  • 将任何传输命令vkCmdCopyBuffer(我们将在本章中使用)提交到传输队列而不是图形队列

因为我们将在本章中创建多个缓冲区,所以将缓冲区创建移至辅助函数是一个好主意。创建一个新函数 createBuffer 并将 createVertexBuffer 中的代码(映射除外)移动到它。

void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, VkDeviceMemory& bufferMemory) {
    VkBufferCreateInfo bufferInfo{};
    bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
    bufferInfo.size = size;
    bufferInfo.usage = usage;
    bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

    if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) {
        throw std::runtime_error("failed to create buffer!");
    }

    VkMemoryRequirements memRequirements;
    vkGetBufferMemoryRequirements(device, buffer, &memRequirements);

    VkMemoryAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
    allocInfo.allocationSize = memRequirements.size;
    allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties);

    if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) {
        throw std::runtime_error("failed to allocate buffer memory!");
    }

    vkBindBufferMemory(device, buffer, bufferMemory, 0);

确保为缓冲区大小、内存属性和用法添加参数,以便我们可以使用此函数创建许多不同类型的缓冲区。最后两个参数是要写入句柄的输出变量。

使用暂存缓冲区

我们现在要更改createVertexBuffer为仅使用主机可见缓冲区作为临时缓冲区,并使用设备本地缓冲区作为实际顶点缓冲区。

void createVertexBuffer() {
    VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size();

//使用新的stagingBuffer、stagingBufferMemory来映射和复制顶点数据
    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;
    createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);

    void* data;
    vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data);
        memcpy(data, vertices.data(), (size_t) bufferSize);
    vkUnmapMemory(device, stagingBufferMemory);

    createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory);
}
  • VK_BUFFER_USAGE_TRANSFER_SRC_BIT:缓冲区可以用作内存传输操作中的源。
  • VK_BUFFER_USAGE_TRANSFER_DST_BIT:缓冲区可用作内存传输操作中的目标。

vertexBuffer从设备本地的内存类型分配的,这通常意味着我们无法使用vkMapMemory. 但是,我们可以将数据从 stagingBuffer复制到vertexBuffer。必须通过指定 stagingBuffer的传输源标志和vertexBuffer 的传输目标标志以及顶点缓冲区使用标志来表明 。

我们现在要编写一个函数,将内容从一个缓冲区复制到另一个缓冲区,称为copyBuffer.

void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
    VkCommandBufferAllocateInfo allocInfo{};
//内存传输操作是使用命令缓冲区执行的,就像绘图命令一样。因此我们必须首先分配一个临时命令缓冲区。
    allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    allocInfo.commandPool = commandPool;//短期缓冲区创建一个单独的命令池
    allocInfo.commandBufferCount = 1;
//
    VkCommandBuffer commandBuffer;
    vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);
//立即开始记录命令缓冲区
    VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

vkBeginCommandBuffer(commandBuffer, &beginInfo);
//只打算使用命令缓冲区一次,然后等待从函数返回,直到复制操作完成执行
    VkBufferCopy copyRegion{};
copyRegion.srcOffset = 0; // Optional
copyRegion.dstOffset = 0; // Optional
copyRegion.size = size;
//使用命令传输缓冲区的内容vkCmdCopyBuffer。它以源缓冲区和目标缓冲区作为参数,以及要复制的区域数组。
vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);

    vkEndCommandBuffer(commandBuffer);
//命令缓冲区仅包含复制命令,因此我们可以立即停止记录。现在执行命令缓冲区以完成传输:

VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
//与绘制命令不同,这次我们不需要等待任何事件。我们只想立即在缓冲区上执行传输
vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
vkQueueWaitIdle(graphicsQueue);

vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);


}

我们现在可以调用copyBuffer函数createVertexBuffer将顶点数据移动到设备本地缓冲区,将数据从暂存缓冲区复制到设备缓冲区后,我们应该清理它:

copyBuffer(stagingBuffer, vertexBuffer, bufferSize);
        vkDestroyBuffer(device, stagingBuffer, nullptr);
        vkFreeMemory(device, stagingBufferMemory, nullptr);

19 索引缓冲区

在真实世界应用程序中渲染的 3D 网格通常会在多个三角形之间共享顶点。

绘制一个矩形需要两个三角形,这意味着我们需要一个有 6 个顶点的顶点缓冲区。问题是需要复制两个顶点的数据,导致 50% 的冗余。对于更复杂的网格,情况只会变得更糟,其中顶点在平均 3 个三角形中重复使用。这个问题的解决方案是使用索引缓冲区

索引缓冲区本质上是指向顶点缓冲区的指针数组。它允许您重新排序顶点数据,并为多个顶点重用现有数据。上图演示了如果我们有一个包含四个唯一顶点中的每一个的顶点缓冲区,则索引缓冲区对于矩形来说会是什么样子。前三个索引定义右上三角形,后三个索引定义左下三角形的顶点。

索引缓冲区创建

修改顶点数据并添加索引数据以绘制图中所示的矩形。修改顶点数据以表示四个角:左上角是红色,右上角是绿色,右下角是蓝色,左下角是白色。

const std::vector<Vertex> vertices = {
    {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}},
    {{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}},
    {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}},
    {{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}}
};

 添加一个新数组indices来表示索引缓冲区的内容。

const std::vector<uint16_t> indices = {
    0, 1, 2, 2, 3, 0
};

//定义两个新的类成员来保存索引缓冲区的资源:

VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;
VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;

void createIndexBuffer() {
    VkDeviceSize bufferSize = sizeof(indices[0]) * indices.size();//索引数bufferSize乘以索引类型的大小

    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;
//
    createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);

    void* data;
    vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data);
    memcpy(data, indices.data(), (size_t) bufferSize);
    vkUnmapMemory(device, stagingBufferMemory);
//VK_BUFFER_USAGE_INDEX_BUFFER_BIT而不是 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT

    createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, indexBuffer, indexBufferMemory);

    copyBuffer(stagingBuffer, indexBuffer, bufferSize);

    vkDestroyBuffer(device, stagingBuffer, nullptr);
    vkFreeMemory(device, stagingBufferMemory, nullptr);
}

使用索引缓冲区

使用索引缓冲区进行绘图涉及对 recordCommandBuffer. 我们首先需要绑定索引缓冲区,就像我们为顶点缓冲区所做的那样。不同之处在于您只能有一个索引缓冲区。不幸的是,不可能为每个顶点属性使用不同的索引,因此即使只有一个属性发生变化,我们仍然必须完全复制顶点数据。

vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT16);

仅绑定索引缓冲区还不会改变任何内容,我们还需要更改绘图命令以告知 Vulkan 使用索引缓冲区。删除该 vkCmdDraw行并将其替换为vkCmdDrawIndexed:

vkCmdDrawIndexed(commandBuffer, static_cast<uint32_t>(indices.size()), 1, 0, 0, 0);

对此函数的调用与vkCmdDraw. 前两个参数指定索引数和实例数。我们没有使用实例化,所以只需指定1实例。索引数表示将传递给顶点着色器的顶点数。下一个参数指定索引缓冲区的偏移量,使用值1将导致图形卡在第二个索引处开始读取。倒数第二个参数指定要添加到索引缓冲区中的索引的偏移量。最后一个参数指定实例化的偏移量,我们没有使用它。

现在运行你的程序,你应该看到以下内容:

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

5。STM32裸机开发(4)

嵌入式软件开发学习过程记录&#xff0c;本部分结合本人的学习经验撰写&#xff0c;系统描述各类基础例程的程序撰写逻辑。构建裸机开发的思维&#xff0c;为RTOS做铺垫&#xff08;本部分基于库函数版实现&#xff09;&#xff0c;如有不足之处&#xff0c;敬请批评指正。 &…

拥抱 Spring 全新 OAuth 解决方案

以下全文 Spring Authorization Server 简称为: SAS 背景 Spring 团队正式宣布 Spring Security OAuth 停止维护&#xff0c;该项目将不会再进行任何的迭代 目前 Spring 生态中的 OAuth2 授权服务器是 Spring Authorization Server 已经可以正式生产使用 作为 SpringBoot 3.0…

FastThreadLocal 原理解析

FastThreadLocal 每个 FastThread 包含一个 FastThreadLocalMap&#xff0c;每个 FastThreadLocalThread 中的多个 FastThreadLocal 占用不同的索引。每个 InternalThreadLocalMap 的第一个元素保存了所有的 ThreadLocal 对象。之后的元素保存了每个 ThreadLocal 对应的 value …

SpringBoot 之 Tomcat 与 Undertow 容器性能对比

一、前言&#x1f525; 环境说明&#xff1a;Windows10 Idea2021.3.2 Jdk1.8 SpringBoot 2.3.1.RELEASE 在上一篇《SpringBoot 之配置 Undertow 容器》一文中写道&#xff1a;“Undertow 的性能和内存使用方面都要优于 Tomcat 容器”, 这一期&#xff0c;我就要给大家来求证…

批处理文件(.bat)启动redis及任何软件(同理)

批处理文件 每次从文件根目录用配置文件格式来启动redis太麻烦了 可以在桌面上使用批处理文件&#xff08;.bat&#xff09;启动Redis&#xff0c;请按照以下步骤进行操作&#xff1a; 打开文本编辑器&#xff0c;如记事本。 在编辑器中输入以下内容&#xff1a; 将文件保存…

【JavaSE】Java基础语法(三十六):File IO流

文章目录 1. File1.1 File类概述和构造方法1.2 绝对路径和相对路径1.3 File 类的常用方法1.4 递归删除文件夹及其下面的文件 2. IO2.1 分类2.2 字节输出流2.3 字节输入流2.4 文件的拷贝2.5 文件拷贝效率优化2.6 释放资源2.7 缓冲流2.8 编码表 3. commons-io 工具包3.1 API 1. F…

gitlab搭建与认证登录

gitlab搭建与认证登录 gitlab的安装配置gitlab中Ldap认证配置 gitlab的安装配置 参考链接&#xff1a; Gitlab 仓库搭建&#xff08;详细版&#xff09; 以下4项注意点&#xff1a; gitlab安装包&#xff0c;直接访问在浏览器上下载速度很慢&#xff0c;可复制链接到迅雷中进…

怎样用一周时间研究 ChatGPT

我是怎样用一周时间研究 ChatGPT 的&#xff1f; 上周大概开了 20 多个会&#xff0c;其中有一些是见了觉得今年可能会比较活跃出手的机构&#xff0c;其余见的绝大多数是和 ChatGPT 相关。 我后面就以 ChatGPT 为例&#xff0c;讲下我是如何快速一周 cover 一个赛道的&#x…

机器视觉怎么对陶瓷板的外观尺寸进行自动检测?

随着工业自动化的不断发展&#xff0c;机器视觉技术在制造业中的应用越来越广泛。在陶瓷板行业中&#xff0c;机器视觉技术可以用于自动检测陶瓷板的外观尺寸&#xff0c;提高生产效率和产品质量。下面我们来介绍机器视觉如何对陶瓷板的外观尺寸进行自动检测。 一、检测原理 …

vue常用指令

vue是前端框架&#xff0c;使用vue指令时需要导入vue.js文件&#xff1b;vue的常用指令有以下这些&#xff1a; v-bind、v-model&#xff1a;双向绑定数据、链接 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8">&…

SAP-MM-采购申请字段解析

采购申请抬头以及行项目字段解析 1、采购申请类型&#xff1a; 对PR进行分类&#xff1b; 控制PR行项目的编号间隔&#xff1b; 控制PR编号范围&#xff0c;以及是否内/外部给号&#xff1b; 控制PR的屏幕选择格式&#xff1b; 控制PR是否允许凭证抬头审批&#xff0c;如果允…

什么是MQTT?mqtt协议和http协议区别

摘要&#xff1a; 什么是MQTT&#xff1f;MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;译为&#xff1a;消息队列遥测传输&#xff0c;是一种轻量级的通讯协议&#xff0c;用于在网络上传输消息。MQTT 最初由 IBM 发布&#xff0c;后来成为 OASIS&#xf…

会话跟踪cookie和session

什么是会话跟踪技术 会话&#xff1a;用户打开浏览器&#xff0c;访问web服务器的资源&#xff0c;会话建立&#xff0c;直到有一方断开连接&#xff0c;会话结束。在一次会话中可能包含多次请求和响应。 会话跟踪&#xff1a;一种维护浏览器状态的方法&#xff0c;服务器需…

vivo互联网视频播放体验优化的探索与实践

随着vivo互联网在视频业务领域的不断扩展&#xff0c;在多样化的业务场景下&#xff0c;如何提升每个用户的视频播放体验&#xff0c;保障最优的播放流畅度和清晰度&#xff0c;vivo互联网技术团队做了很多尝试与突破。LiveVideoStackCon 2022北京站邀请vivo互联网研发经理王道…

python接口自动化测试之unittest自动化测试框架基本使用

目录 unittest简单介绍 unittest基础使用 unittest.Testcase setUp tearDown setUpClass tearDownClass 测试用例 unittest.main() unitteest提供的各种断言方式 unittest测试用例跳过执行 跳过执行测试用例共有四种写法 self.skipTest(reason) 跳过执行测试用例注…

eBay如何实现多账号登录以及防关联?

随着跨境电商的快速发展&#xff0c;亚马逊&#xff0c;eBay已成为人们熟知的电商平台。“不把鸡蛋放在同一个篮子里”&#xff0c;多账号运营店铺有许多显而易见的好处。 但由于亚马逊平台封号状况愈演愈烈&#xff0c;不少卖家把战线转移到了eBay平台。随着入驻人数的增加&a…

Solidity拓展:数学运算过程中数据长度溢出的问题

在数学运算过程中假如超过了长度则值会变成该类型的最小值&#xff0c;如果小于了该长度则变成最大值 数据上溢 uint8 numA 255; numA;uint8的定义域为[0,255]&#xff0c;现在numA已经到顶了&#xff0c;numA会使num变成0(由于256已经超过定义域&#xff0c;它会越过256&…

Redis事务及网络处理

一 Redis事务 redis开启事务后&#xff0c;会把接下来的所有命令缓存到一个单独的队列中&#xff0c;在提交事务时&#xff0c;使这些命令不可被分割的一起执行完成。 如果使用了watch命令监视某一个key&#xff0c;如果在开启事务之后&#xff0c;提交事务之前&#xff0c;有…

FreeRTOS:队列

目录 前言一、队列简介1.1数据存储1.2多任务访问1.3出队阻塞1.4入队阻塞1.5队列操作过程图示1.5.1创建队列1.5.2向队列发送第一个消息1.5.3向队列发送第二个消息1.5.4从队列中读取消息 二、队列结构体三、队列创建3.1创建函数3.2函数xQueueCreateStatic()3.3函数xQueueCreate()…

Spring Security入门

1. Spring Security 简介 Spring Security 是一个高度可定制的身份验证和访问控制框架&#xff0c;它基于 Spring 框架&#xff0c;并可与 Spring 全家桶无缝集成。该框架可以精确控制用户对应用程序的访问&#xff0c;控制用户的角色和权限等。 Spring Security 最早是由 Be…