wenmo8 发布的文章

vmstorage如何将原始指标转换为有组织的历史

参考自:
vmstorage-how-it-handles-data-ingestion

vmstorage是VictoriaMetrics中负责处理长期存储的组件。

image

读取和解析数据

在vmstorage接收到数据之后,并不会直接读取这些数据。首先会检查读并发限速器(限制为2倍的CPU cores),如果读操作过多,则最终会排队等候处理。

vmstorage每次会读取一个block,block的结构如下,包含一个表示block大小的8字节的
size
字段以及一个body字段。一个block不能超过100MB。

image

有一个边缘场景值得注意:即当vmstorage在速磁盘空间不足时会转变为只读模式。该模式下vmstorage会对接收到的数据响应"read-only ack",并忽略实际内容,vminsert会识别此种确认类型并重发数据(本文后面讨论)。

本阶段中,vmagent仅使用字节流方式读取原始block,并不会对其解析。这些原始字节最终会被拆分为rows进行处理。

image

如果block过大,vmstorage会以chunks为单位进行处理,每次处理10,000 rows数据并将其写入存储。

查找每条metric的TSID

TSID用于表示不同的时间序列,有如下作用:

数据存储优化

  • TSID 在存储时序数据时起到索引的作用。VictoriaMetrics 使用 TSID 来将时间序列数据映射到磁盘的存储位置,从而避免直接存储复杂的指标名称和标签。
  • 通过 TSID,可以快速找到时间序列对应的样本(samples,时间戳与值的组合)并高效地写入或读取数据。

查询加速

  • 当查询某个指标时,VictoriaMetrics 会解析查询中指定的标签或指标名称,通过倒排索引查找与查询条件匹配的 TSID。

每条metric包含如下几个关键部分:

  • 一个metrics名称
  • 一组metrics labels
  • 一个时间戳(毫秒)
  • 一个浮点数表示的metric value

image

在计算metric的TSID之前,首先需要创建"
规范的指标名称
",即将metric name和labels组合起来,然后按名字的字母顺序排列,目的是防止包含相同labels的metrics,只是因为labels顺序不同而被认为是不同的metric,如
metric{instance="host",job="app"}

metric{job="app",instance="host"}
是相同的metric,只是label顺序不同。

TSID是一种可以表示时间序列的唯一数字,用于快速定位数据。

image

上一节中,vmstorage从block中获取到了原始的metrics(无排序),然后通过查询TSID缓存来判断是否已经存在对应的TSID,如果存在,则直接插入该指标:

image

如果不存在,则需要向IndexDB查询,由于涉及随机磁盘查询,因此这是一个比较慢的过程。在查找成功后缓存结果。

如果不存在,则说明这是一个全新的metric。此时,系统需要生成一个新的TSID,并将其同时注册到内存缓存和IndexDB,包括如下步骤:

  • 将"规范指标名称"映射到新的TSID
  • 设置反向映射,这样TSID可以指向"规范指标名称"
  • 倒排索引包含"规范指标名称"中的每个label,可以帮助系统在使用标签过滤时快速查询时序数据
  • 创建以天为单位的索引,用于优化按时间范围查找数据的场景。

image

注册新的时间序列涉及向磁盘写入与之相关的所有信息,该过程可能会拖慢整个系统,特别是当metric拥有很多labels或非常长的labels时。

这也是为什么要关注
churn rate
的原因。vmstorage可以按小时 (
-storage.maxHourlySeries
)和天(
-storage.maxDailySeries
)限制新创建的时序数据总量。

向内存缓冲插入数据

一旦注册好TSID,VictoriaMetrics就可以处理实际的数据样本,包括TSID、时间戳、值等,并将其放入一个内存缓冲(称为"raw-row shards"),且一个partition(表示一个月的数据)的shards数目等于CPU cores的数目。例如,机器有4 cores,则每个月的数据有4个shards,每个shards最多可以有8 MB的数据,约149,796 rows。

如果shard被填满,则这些rows会被推入一个称为"pending series"的地方,等待被处理成“LSM part”,并最终写入磁盘。只有刷新到LSM part的数据才能被查询到,之后便完成了本block的数据处理,可以开始处理下一个block。

image

数据如何写入磁盘

在如下两种情况中,Shard缓冲会刷新数据:

  1. 当缓冲达到阈值(约120MB),刷新pending series
  2. 如果距上一次刷新超过2s,则系统会自动刷新 pending series和raw-row shards

在刷新过程中,数据会转换为一个LSM part,LSM part中的项会根据TSID和时间戳进行排序。

LSM Parts的类型

每个partition(涵盖一个月的数据)会将其数据组织为3种LSM parts类型:

  • 内存 part:存放raw-row shards首次刷新后的数据,此时数据可以被搜索和查询
  • Small part:比内存part稍大,存储在持久化磁盘上
  • big part:最大的parts,存储在磁盘上

vmstorage同一时间最多可以持有60个内存parts,占用约10%的系统内存。例如,vmstorage内存为10GB,则内存parts占1GB,每个part约1MB~17MB。

image

随着数据的写入,会创建越来越多的parts,当LSM parts过多(无论是内存还是磁盘)时,每个查询(如来自grafana的查询)都需要扫描并合并这些parts,可能会拖慢系统。

为了防止上述问题,vmstorage依赖两个关键处理:刷新和合并。

  • 刷新:将
    所有
    内存parts刷新到磁盘的small parts。每5s,vmstorage(
    -inmemoryDataFlushInterval
    )会将内存parts刷新到基于磁盘或文件的parts上。
  • 合并:将多个parts合并为更高效的存储。这并不意味着将所有small parts合并为big parts,而是将一部分small parts合并为稍大一些的small parts

合并过程

合并并不是固定调度的。只要有parts累积,系统就会尝试合并这些parts,将内存parts合并为较大的内存part,将small parts合并为较大的small part,将big parts合并为较大的big part。

small parts不能超过10MB,big parts最大可以占用大约剩余磁盘空间/4,但不能超过1TB。注意small parts和big parts只是系统在合并过程中评估出来的需要创建的part类型,并不是说small parts一定小于big parts。

small parts合并的结果最终会被写入磁盘,该过程中会执行去重操作。

去重是确认并移除那些几乎相等,但记录时间略微不同的时间点。通常发生在出于冗余或可靠性目的,而使用两个或多个监控系统将相同指标并发往同一个存储的场景。

image

默认关闭去重,可以通过
-dedup.minScrapeInterval
启用去重功能。

Retention, Free Disk Space Guard和 Downsampling

vmstorage的默认回收周期是1个月。需要注意的是,每个part包含很多样本,只要有一个样本在回收周期内,则必须保留整个part。

image

Free Disk Space Watcher: Read Only Mode

一开始提到,在磁盘空间不足的情况下,vmstorage会进入read-only模式,该模式下,vminset会接收到数据发送的确认信息,但vmstorage会忽略掉这些数据。此时vmstorage仍然能够提供查询请求,但停止接收任何写数据。一旦释放了磁盘空间
(-storage.minFreeDiskSpaceBytes
,默认10MB),vmstorage会退出read-only模式。

Partition的结构

无论是内存parts,small parts还是big parts,其数据都是列模式,即TSID、时间戳和值都不会组合在一个记录中,而是被分散到不同的列,每一列都保存在各自的文件中。

  • 所有 TSIDs 都保存在
    index.bin
    .
  • 所有时间戳都保存在
    timestamps.bin
    .
  • 所有值都保存在
    values.bin
    .

对于内存parts,这些列结构已经就绪,可以直接刷新到基于文件的parts中。

image

列模式便于压缩和快速查找。
timestamps.bin

values.bin
中的每个block表示单个TSID行,一个block最多可以有8192 行。

index.bin
的每一行包括多个block首部,一个block首部包含:

  • block的TSID
  • block的行数
  • block在
    timestamps.bin

    values.bin
    中的位置

image

一、环境条件说明:

操作系统:Windows10 64环境

编译工具:用Qt5.12.12自带的mingw730_64构建

构建对象:编译OpenCV4.1.0的Release 64位和Debug 64位动态链接库

构建工具:CMake中的参数配置

二、cmake-3.20.6中的参数配置

1、按照下图配置好OpenCV4.1.0的源代码目录和构建编译输出目录,然后点击Configure按钮,如下图所示:

2、在弹出的界面中,按照下图配置构建工程类型、gcc和g++编译器路径:

3、在使用CMake编译OpenCV_contrib时遇到的错误,由于源文件未成功下载导致编译失败。通过分析CMake的日志文件CMakeDownloadLog.txt,可手动下载缺失的opencv_ffmpeg.dll并放置于正确位置。OpenCV4.1.0所需要的opencv_ffmpeg.dll和opencv_ffmpeg_64.dll下载网盘地址如下:

链接:
https://pan.baidu.com/s/1AWnC_MjuAAWYgwvlHziTqA?pwd=juta
提取码: juta


4、把BUILD_opencv_world、OPENCV_ENABLE_NONFREE选项选中,如下图:

5、分别设置OPENCV_EXTRA_MODULES_PATH为扩展模块的modules目录,如下图:

6、分别开启WITH_QT、WITH_OPENGL、WITH_OPENMP等参数选项,如下图:

7、分别关闭ENABLE_PRECOMPILED_HEADERS、WITH_OPENCL_D3D11_NV、WITH_IPP等参数选项,如下图:

8、在cmake-3.20.6界面中依次搜索example和test,取消例子、测试应用程序的编译。因为在Windows10 64环境下用Qt5.12.12自带的mingw730_64构建编译OpenCV4.1.0时,测试应用test会报编译错误,导致编译OpenCV4.1.0的Debug 64位动态链接库失败。如下图:

9、编译时会提示以下错误:第一个错误提示的是:undefined reference to `cvv:::qtutil::ZoomableImage::updateArea(QRect,double)'之类的,后面还有很多与opencv的cvv模块有关的报错。根据个人编译实践猜测,可能是由于扩展模块中的cvv模块的开启会与BUILD_opencv_world设置有点冲突。参考这篇博文解决:编译opencv 遇到undefined reference to `cvv::XX 错误:
https://blog.csdn.net/panmengjiaa/article/details/114693766
。可在cmake-3.20.6界面中点击“Add Entry”按照下图添加变量BUILD_opencv_cvv,如下图所示:

10、配置完上述参数之后,再次点击Configure,如果没有错误,可点击Generate,没有错误提示之后就可以依次执行以下目录进行编译和安装:

#使用下面命令开始编译opencv,需要20分钟左右。其中,-j 8 代表多核编译

mingw32-make -j 8

#编译完成之后,输入如下指令安装

mingw32-make install

11、注意按照上述步骤默认编译出来的是OpenCV4.1.0的Release 64位动态链接库。可在cmake-3.20.6界面中搜索CMAKE_BUILD_TYPE,将其设置为Debug,保持其他参数值不变,再次按照前一步中的说明编译OpenCV4.1.0的Debug 64位动态链接库,如下图所示:

----------------------------------------------------------------------------------------------------------------------

详细的步骤可参考以下链接:

1)        三、Qt配置opencv环境(详细,简易)https://download.csdn.net/blog/column/11496363/121620850

2)        qtopencv配置https://blog.51cto.com/u_12929/12714843

3)        Qt5.12配置OpenCV教程https://www.cnblogs.com/ybqjymy/p/18070391

4)        【开发实战】QT5 + OpenCV4开发环境配置应用演示https://zhuanlan.zhihu.com/p/719915221

5)        OpenCV4 + Qt5 开发环境配置合集(C++/Python):https://www.bilibili.com/video/BV1Za4y1v7ra/?vd_source=e39e23ac5a7253752edc9b53b94c0c3d

6)        How to setup Qt and openCV on Windows:https://wiki.qt.io/How_to_setup_Qt_and_openCV_on_Windows

7)        Qt5.9.7中使用MinGW32编译OpenCV4.1.0过程https://blog.csdn.net/zhoufoxcn/article/details/103737848

8)        OpenCV使用CMake和MinGW的编译安装及其在Qt配置运行https://blog.csdn.net/huihut/article/details/78701814

9)        OpenCV使用CMake和MinGW-w64的编译安装https://blog.csdn.net/huihut/article/details/81317102

10)     【完美解决】OpenCVError: Insufficient memory (Failed to allocate xxx bytes) 报错的辛酸踩坑史https://blog.csdn.net/qq_52949697/article/details/123033368

11)     opencv库不支持qt qt配置opencv出错https://blog.51cto.com/u_12192/10896803

12)     minGW编译opencv4.1.0时  undefined reference to to cvv::view::MatchVIew

13)     undefined reference to `cvv:::qtutil::ZoomableImage::updateArea(QRect,double)'之类的,后面还有很多与opencv的cvv模块有关的报错。解决办法参考:编译opencv 遇到undefined reference to `cvv::XX 错误:https://blog.csdn.net/panmengjiaa/article/details/114693766

1 前言

​ 本文将介绍 GLSL 中数据类型、数组、结构体、宏、运算符、向量运算、矩阵运算、函数、流程控制、精度限定符、变量限定符(in、out、inout)、函数参数限定符等内容,另外提供了一个 include 工具,方便多文件管理 glsl 代码,实现代码的精简、复用。

​ Unity 中 Shader 介绍详见 →
【Unity3D】Shader常量、变量、结构体、函数
,渲染管线介绍详见 →
【OpenGL ES】渲染管线

2 数据类型

2.1 基本数据类型

2.1.1 基本数据类型

类型 说明 案例
void 空类型,即不返回任何值 void fun()
bool 布尔类型 true、false bool a = true;
int 带符号的整数 int a = 0;
float 带符号的浮点数 float a = 1.0;
float b = 2.;
vec2、vec3、vec4 2 维、3 维、4 维浮点数向量 vec2 a = vec2(1., 2.);
vec2 b = vec2(1.); // ⇔ vec2(1., 1.)
vec3 c = vec3(a, 3.); // ⇔ vec3(1., 2., 3.)
bvec2、bvec3、bvec4 2 维、3 维、4 维布尔向量 bvec2 a = bvec2(true, false);
bvec2 b = bvec2(true); // ⇔ bvec2(true, true)
bec3 c = bvec3(a, true); // ⇔ bvec3(true, false, true)
ivec2、ivec3、ivec4 2 维、3 维、4 维整数向量 ivec2 a = ivec2(1, 2);
ivec2 b = ivec2(1); // ⇔ ivec2(1, 1)
ivec3 c = ivec3(a, 3); // ⇔ ivec3(1, 2, 3)
mat2、mat3、mat4 2x2、3x3、4x4 浮点数矩阵 (列向量) mat2 a = mat2(1., 2., 3., 4.);
mat2 b = mat2(1.); // ⇔ mat2(1., 1., 1., 1.)
sampler2D 2D 纹理 sampler2D sampler;
samplerCube 盒纹理 samplerCube sampler;

​ 说明:mat2、mat3、mat4 中的元素都是按照列向量的顺序排列的,即 mat2 m = mat2(m11, m21, m12, m22) 对应的公式如下。

img

2.1.2 向量分量访问

​ GLSL 中的向量(vec2、vec3、vec4)可以表示一个空间坐标 (x, y, z, w),也可以表示一个颜色 (r, g, b, a),还可以表示一个纹理坐标 (s, t ,p, q),所以 GLSL 提供了多样的分量访问方式。

vec4 v = vec4(1.0, 2.0, 3.0, 4.0);
float x1 = v.x; // 1.0
float x2 = v.r; // 1.0
float x3 = v.s; // 1.0
float x4 = v[0]; // 1.0

vec3 xyz = v.xyz; // vec3(1.0, 2.0, 3.0)
vec3 stq = v.stq; // vec3(1.0, 2.0, 3.0)
vec3 rgb = v.rgb; // vec3(1.0, 2.0, 3.0)
vec3 abc = vec3(v[0], v[1], v[2]); // vec3(1.0, 2.0, 3.0)

2.1.3 数据类型转换

​ GLSL 可以使用构造函数进行显式类型转换。

// 0或0.0转换为false, 非0转换为true
bool a1 = bool(1.0); // true
bool a2 = bool(0); // false

// true转换为1或1.0, false转换为0或0.0
int a3 = int(true); // 1
float a4 = float(false); // 0.0

int a5 = int(2.0); // 2
float a6 = float(1); // 1.0

2.2 数组

​ GLSL 只支持一维数组。

// float 数组
float[3] a = float[] (1.0, 2.0, 3.0);
float b[3] = float[] (1.0, 2.0, 3.0);
float c[3] = float[3] (1.0, 2.0, 3.0);

// vec 数组
vec2[2] d = vec2[] (vec2(0.0), vec2(1.0));

2.3 结构体

struct light {
    vec4 color;
    vec3 pos;
};
const light lgt = light(vec4(1.0), vec3(0.0));

​ 说明:结构体中的字段不可用 const 修饰。

2.4 内置变量

范围 变量 说明
顶点着色器的 Output 变量 highp vec4
gl_Position
;
顶点坐标信息
顶点着色器的 Output 变量 mediump float
gl_PointSize
;
顶点大小 (只在 GL_POINTS 图元模式下有效)
片元着色器的 Input 变量 mediump vec4
gl_FragCoord
;
片元在屏幕空间的坐标,假设屏幕宽高分别为 width、height
x: 片元的x坐标,值域 [0, width - 1]
y: 片元的x坐标,值域 [0, height - 1]
z: 片元的深度坐标,值域 [0, 1]
w: 总是 1,通常用于透视除法
片元着色器的 Input 变量 bool
gl_FrontFacing
;
标志当前图元是否是正面图元的一部分
片元着色器的 Output 变量 mediump vec4
gl_FragColor
;
设置当前片点的颜色

2.5 宏

​ 与 C 语言一样,GLSL 中也可以通过 #define 定义宏,如下。

#define PI 3.14159265359

​ 另外,GLSL 也提供了一些内置宏。

__LINE__ // 当前源码中的行号
__VERSION__ // 当前glsl版本号, 如: 300
GL_ES // 当前运行环境是否是 OPGL ES, 1 表示是
GL_FRAGMENT_PRECISION_HIGH // 当前系统的片元着色器是否支持高浮点精度, 1表示支持

3 运算符

3.1 基础运算符

优先级 (越小越高) 运算符 说明 结合性
1 () 聚组: a * (b + c) N/A
2 []
()
.
++ --
数组下标
方法参数: fun(arg1, arg2)
属性访问
自增 (a++) / 自减 (a--)
L - R
3 ++ --
+ -
!
自增 (++a) / 自减 (--a)
正 (+a) 负 (-a) 号
取反 (!a)
R - L
4 *
/
%
乘法 / 除法运算
整数求余运算 (浮点数求余用 mod 函数)
L - R
5 + - 加法 / 减法运算 L - R
7 < > <= >= 关系运算符 L - R
8 == != 相等性运算符 L - R
12 && 逻辑与 L - R
13 ^^ 逻辑排他或 (用处基本等于 !=) L - R
14 || 逻辑或 L - R
15 ? : 三目运算符 L - R
16 = += -= *= /= 赋值和复合赋值 L - R
17 , 顺序分配运算 L - R

​ 说明:GLSL 中没有隐式类型转换,因此任何表达式左右两侧的类型必须一致,以下表达式都是错误的。

// 以下代码运行时会报错
int a = 2.;
int b = 1. + 2;
float c = 2;
float d = 2. + 1;
bool e = 0;
vec2 f = vec2(1., 2.) * 2;

3.2 向量运算符

// 标量与向量运算
vec2 a = vec2(1., 2.) + 3.; // vec2(4., 5.)
vec2 b = 3. + vec2(1., 2.); // vec2(4., 5.)
vec2 c = vec2(1., 2.) * 3.; // vec2(3., 6.)
vec2 d = 3. * vec2(1., 2.); // vec2(3., 6.)

// 向量与向量运算
vec2 e = vec2(1., 2.) + vec2(3., 4.); // vec2(4., 6.)
vec2 f = vec2(1., 2.) * vec2(3., 4.); // vec2(3., 8.)

3.3 矩阵运算符

// 标量与矩阵运算
mat2 a = mat2(1.) + 2.; // mat2(3.)
mat2 b = 2. + mat2(1.); // mat2(3.)
mat2 c = mat2(1.) * 2.; // mat2(2.)
mat2 d = 2. * mat2(1.); // mat2(2.)

// 向量与矩阵运算
vec2 e = vec2(1., 2.) * mat2(1., 2., 3., 4.); // vec2(5., 11.)
vec2 f = mat2(1., 2., 3., 4.) * vec2(1., 2.); // vec2(7., 10.)

// 矩阵与矩阵运算(矩阵对应元素运算)
mat2 g = mat2(1.) + mat2(2.); // mat2(3.)
mat2 h = matrixCompMult(mat2(1.), mat2(2.)); // mat2(2.)

// 矩阵与矩阵运算(矩阵乘法)
mat2 i = mat2(1., 2., 3., 4.) * mat2(5., 6., 7., 8.); // mat2(23., 34., 31., 46.)
mat2 j = mat2(5., 6., 7., 8.) * mat2(1., 2., 3., 4.); // mat2(19., 22., 43., 50.)

img

4 函数

4.1 自定义函数

​ GLSL 允许在程序的最外部声明函数,函数不能嵌套、不能递归调用,且必须声明返回值类型(无返回值时声明为 void)在其他方面 GLSL 函数与 C 语言函数非常类似。

vec4 getPosition() { 
    vec4 pos = vec4(0.,0.,0.,1.);
    return pos;
}

void doubleSize(inout float size) {
    size = size * 2.0;
}

4.2 内置函数


1)数值运算

sign(x)、abs(x) // 符号、绝对值
min(a, b)、max(a, b) // 最值函数
ceil(x)、floor(x)、round(x) // 取整函数
fract(x) // 取小数部分
mod(x, y) // 取余数
sqrt(x)、pow(x)、inversesqrt(x) // 幂函数, inversesqrt(x)=1/sqrt(x)
exp(x)、exp2(x) // 指数函数(e^x、2^x)
log(x)、log2(x) // 对数函数
degrees(x)、radians(x) // 角度转换函数
sin(x)、cos(x)、tan(x)、asin(x)、acos(x)、atan(x) // 三角函数
sinh(x)、cosh(x)、tanh(x) // 双曲线函数
clamp(x, min, max) // 将x约束在min和max之间, 超过边界就取边界值
smoothstep(min, max, x) // 平滑比例, 公式: k=saturate((x-min)/(max-min)), y=k*k*(3-2*k)
mix(a, b, f) // 混合, 公式: y=(1-f)*a+f*b
step(a, b) // 如果a>b, 返回0; 如果a<=b, 返回1; 当a、b是向量时, 每个分量独立判断, 如: step(fixed2(1,1),fixed(0,2))=(0,1)

​ 说明:以上函数输入的可以是:float、vec2、vec3、vec4,且可以逐分量操作;对于整数求余运算,只能用 %;对于浮点数求余运算,只能用 mod。


2)逻辑运算

bvec z = lessThan(x, y) // 逐分量比较x < y, 将结果写入z的对应位置
bvec z = lessThanEqual(x, y) // 逐分量比较x <= y, 将结果写入z的对应位置
bvec z = greaterThan(x, y) // 逐分量比较x > y, 将结果写入z的对应位置
bvec z = greaterThanEqual(x, y) // 逐分量比较x >= y, 将结果写入z的对应位置
bvec z = equal(x, y) // 逐分量比较x == y, 将结果写入z的对应位置
bvec z = notEqual(x, y) // 逐分量比较x != y, 将结果写入z的对应位置
bvec y = not(x) // bool矢量的逐分量取反
bool y = any(x) // 如果x的任意一个分量是true, 则结果为true
bool y = all(x) // 如果x的所有分量是true, 则结果为true


3)向量运算

distance(pos1, pos2) // 计算pos1与pos2之间的距离
length(vec) // 计算向量的模长
normalize(vec) // 计算向量的单位向量
dot(v1, v2) // 向量点乘
cross(v1, v2) // 向量叉乘
reflect(i, n) // 根据入射向量和法线向量, 计算反射向量(i和n不需要归一化)
refract(i, n, ratio); // 根据入射向量、法线向量、折射率比值, 计算折射向量(i和n需要归一化, ratio为入射介质折射率/折射介质折射率, 或sin(折射角)/sin(入射角))


4)矩阵运算

// 矩阵与矩阵运算(矩阵对应元素运算)
mat2 a = mat2(1.) + mat2(2.); // mat2(3.)
mat2 b = matrixCompMult(mat2(1.), mat2(2.)); // mat2(2.)

// 矩阵与矩阵运算(矩阵乘法)
mat2 c = mat2(1., 2., 3., 4.) * mat2(5., 6., 7., 8.); // mat2(23., 34., 31., 46.)
mat2 d = mat2(5., 6., 7., 8.) * mat2(1., 2., 3., 4.); // mat2(19., 22., 43., 50.)

img


5)纹理查询函数

vec4 texture(sampler2D sampler, vec2 coord);
vec4 texture2D(sampler2D sampler, vec2 coord);
vec4 texture2DProj(sampler2D sampler, vec3 coord);
vec4 texture2DProj(sampler2D sampler, vec4 coord);
vec4 textureCube(samplerCube sampler, vec3 coord);

5 流程控制

​ GLSL 的流控制与 C 语言非常相似,主要有 if、for、while、continue、break,不同的是 GLSL 中多了 discard。使用 discard 会退出片元着色器,不会执行后面的操作,片元也不会写入帧缓冲区。

for (int i = 0; i < 10; i++) {
    sum += a[i];
    if (sum > 5.0)
        break;
}

while (i < 10) {
    sum += a[i];
    if (i % 3 == 1)
        continue;
    i++;
}

do {
    sum += a[i];
    if (sum > 5.0)
        discard;
    i++;
} while (i < 10)

6 限定符

6.1 精度限定符

​ 片元着色器中,对于浮点数 GLSL 有 highp(高)、mediump(中)、lowp(低) 三种精度。

lowp float color;
varying mediump vec2 Coord;
lowp ivec2 foo(lowp mat3);
highp mat4 m;

​ 在片元着色器的第一行加上 precision highp float,表示设定了默认的精度,所有没有显式表明精度的变量都会按照默认精度处理。

precision highp float;

​ 通过判断系统环境,来选择合适的精度。

#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
#endif

6.2 变量限定符

限定符 说明
none 默认的变量限定符,可省略,可读写。
const 修饰的变量为只读类型,变量在定义时必须初始化。
attribute 只能在顶点着色器中使用,修饰全局只读变量,一般用于修饰顶点属性,如:顶点的位置、法线、纹理坐标、颜色等。
uniform 修饰全局只读变量,一般用于修饰程序传递给 shader 的变量,如:屏幕宽高比、光源位置等。
varying 修饰需要进行光栅化插值的变量,在片元着色器中是只读的,如:纹理坐标等。
// 顶点着色器
attribute vec4 a_position;
attribute vec2 a_texCoord0;
varying vec2 v_texCoord0;

void main() {
    gl_Position = vec4(a_position, 1.0);
    v_texCoord0 = a_texCoord0;
}

// 片元着色器
precision highp float;
uniform sampler2D u_texture;
varying vec2 v_texCoord0;

void main() {
  gl_FragColor = texture(u_texture, v_texCoord0);
}

6.3 函数参数限定符

​ 函数的参数默认以拷贝的形式传递,即值传递,我们可以为参数添加限定符实现引用传递,GLSL 中提供的参数限定符如下。

限定符 说明
in 默认的限定符,参数是值传递,在函数中可读写。
out 参数是引用传递,在函数中只能写(write-only)。
inout 参数是引用传递,在函数中可读写(read-write)。

​ 除了函数的参数可以用 in、out、inout 修饰,全局变量也可以用这些限定符修饰,如下。

// 顶点着色器
in vec3 a_position;
in vec2 a_texCoord0;
out vec2 v_texCoord0;

void main() {
    gl_Position = vec4(a_position, 1.0);
    v_texCoord0 = a_texCoord0;
}

// 片元着色器
precision highp float;
uniform sampler2D u_texture;
in vec2 v_texCoord0;
out vec4 o_fragColor;

void main() {
    o_fragColor = texture(u_texture, v_texCoord0);
}

7 include

​ GLSL 中没有提供 #include 功能,如以下代码会运行报错。

#include <shaders/utils/constant.glsl>

​ 要想实现一个 glsl 文件依赖另一个 glsl 文件,可以使用以下工具加载 glsl 字符串。

​ ShaderUtils.java

import android.content.Context;

import java.util.HashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Shader工具类
 * @author little fat sheep
 */
public class ShaderUtils {
    // 匹配: #include <xxx> 或 #include “xxx”, 并且不以"//"开头
    private static final String INCLUDE_REGEX = "(?m)^(?!//\\s*)#include\\s+(<([^>]+)>|\"([^\"]+)\")";

    /**
     * 通过asset加载shader
     * @param vertexShader 顶点着色器路径, 如: "shaders/origin_vert.glsl"
     * @param fragmentShader 片元着色器路径, 如: "shaders/origin_frag.glsl"
     */
    public static String[] loadShader(Context context, String vertexShader, String fragmentShader) {
        String vertex = StringUtils.loadString(context, vertexShader);
        String fragment = StringUtils.loadString(context, fragmentShader);
        String vertex1 = replaceIncludeFiles(context, vertex);
        String fragment1 = replaceIncludeFiles(context, fragment);
        return new String[] { vertex1, fragment1 };
    }

    /**
     * 通过资源id加载shader
     * @param vertexShader 顶点着色器资源id, 如: R.raw.origin_vertex
     * @param fragmentShader 片元着色器资源id, 如: R.raw.origin_fragment
     */
    public static String[] loadShader(Context context, int vertexShader, int fragmentShader) {
        String vertex = StringUtils.loadString(context, vertexShader);
        String fragment = StringUtils.loadString(context, fragmentShader);
        String vertex1 = replaceIncludeFiles(context, vertex);
        String fragment1 = replaceIncludeFiles(context, fragment);
        return new String[] { vertex1, fragment1 };
    }

    /**
     * 将shader字符串中#include的文件替换为文件内容
     */
    public static String replaceIncludeFiles(Context context, String shaderContent) {
        Pattern pattern = Pattern.compile(INCLUDE_REGEX);
        HashSet<String> set = new HashSet<>(); // 用于去掉重复的include
        return replaceIncludeFiles(context, shaderContent, pattern, set);
    }

    /**
     * 将shader字符串中#include的文件替换为文件内容(include的文件中可能也有include, 需要递归调用)
     */
    private static String replaceIncludeFiles(Context context, String shaderContent, Pattern pattern, HashSet<String> set) {
        Matcher matcher = pattern.matcher(shaderContent);
        StringBuffer sb = new StringBuffer(shaderContent.length());
        while (matcher.find()) {
            String angleBrackets = matcher.group(2); // 尖括号内的路径
            String quotationMarks = matcher.group(3); // 引号内的路径
            String file = angleBrackets != null ? angleBrackets : quotationMarks;
            if (set.contains(file)) {
                matcher.appendReplacement(sb, ""); // 删除重复的include
            } else {
                set.add(file);
                String includeShader = StringUtils.loadString(context, file); // 加载include文件中的字符串
                String quoteShader = Matcher.quoteReplacement(includeShader); // 替换字符串中的转义字符
                String wholeShader = replaceIncludeFiles(context, quoteShader, pattern, set); // 递归替换字串中的include
                matcher.appendReplacement(sb, wholeShader); // 将字符串添加到sb中
            }
        }
        matcher.appendTail(sb);
        return sb.toString();
    }
}

​ StringUtils.java

import android.content.Context;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

/**
 * 字符串工具类
 * @author little fat sheep
 */
public class StringUtils {
    /**
     * 根据资源路径读取字符串
     * @param assetPath 资源路径, 如: "shaders/origin_vert.glsl"
     */
    public static String loadString(Context context, String assetPath) {
        String str = "";
        try (InputStream inputStream = context.getAssets().open(assetPath)) {
            str = loadString(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return str;
    }

    /**
     * 根据资源id读取字符串
     * @param rawId 资源id, 如: R.raw.origin_vertex
     */
    public static String loadString(Context context, int rawId) {
        String str = "";
        try (InputStream inputStream = context.getResources().openRawResource(rawId)) {
            str = loadString(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return str;
    }

    private static String loadString(InputStream inputStream) {
        StringBuilder sb = new StringBuilder();
        try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))) {
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line).append("\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sb.toString();
    }
}

​ 声明:本文转自
【OpenGL ES】GLSL基础语法

前言

2024-12-29T03:01:42.png

最新想在vmware虚拟机上玩xf,网上找了不少教程,于是打算自己尝试下。

如果可以修改成功的话,其价值嘛不可估量。

环境

vmware 版本是 16.0.0 ,已安装VMware Tools

vmware
版本是
16.1.2
,已安装VMware Tools

虚拟镜像
cn_windows_7_professional_with_sp1_vl_build_x64_dvd_u_677816

回滚:win7测试失败,改为:cn_windows_10_consumer_editions_version_1909_x64_dvd_76365bf8.iso

用到的工具:1.WINHEX 2.Phoenix BIOS Editor

网上很多都是从卸载vmware开始的,我就不卸载了出问题再说

开始尝试过虚拟化

去掉硬盘虚拟化标识以及光驱虚拟化标识以及修改硬盘序列号


vmware安装目录\x64
下把这两个文件
BIOS.440.ROM

vmware-vmx.exe
备份下,原始文件拖到
winhex

点击寻找16进制数值“3030303030303030303030303031”,因为虚拟机默认硬盘号10000000000000000001,16进制为“3130303030303030303030303030303030303031”。

找到一个,点击替换十六进制,替换为“0000416d6265722073797374656d”,记得勾选在所有打开文件替换。

2024-12-28T05:04:18.png

点确定后,我这边显示一个结果被替换。

继续替换“564D7761726520494445”->“00416d62657220494445”

声卡网卡

继续替换“FFBAAD15000041B87719”->“FFBAEC10000041B88680”,这里替换了2个

网卡 16进制替换
继续替换“66894717B8AD15”->“66894717B88680”

主板的芯片组
“86809071”->“868014A1”

显卡
8002B8AD15
8002B8DE10

0000BA050400
0000BA100700

注意:我在此改完用鲁大师单文件版还是会出现VMware的,因为还有很多没改到的地方,解决方法也很简单,鲁大师显示什么你搜什么然后改掉就行。这个文件改完重启就可以生效,不需要重新创建虚拟机。

修改后保存,替换原始原件。

修改系统制造商以及系统型号

这个需要修改BIOS。
用到Phoenix BIOS Editor工具
安装好虚拟机后找到根目录下的X64文件夹。把“BIOS.440.ROM”用Phoenix BIOS Editor工具打开

打开
DMI String
窗口

依次修改

2024-12-28T04:58:33.png

  • 'LEGEND DragonLENOVO'
  • 'CompaqHewlett-Packard'
  • '123. Inc.'
  • '123 Virtual Piatform'
  • 'IBM CORPORATION'
  • 'Founder PCFUJITSU-PC'
  • 'AcerSystemHP PAVILION'
  • 'Dell SystemTCL123'
  • 'Acer TravelMate'
  • 'GREATWALLEMACHINES'

修改后build一个新的rom出来

将修改后的“BIOS.440.ROM”,复制到你的系统安装文件夹并打开.vmx文件

在里面加入

bios440.filename = "BIOS.440.ROM"

然后虚拟机设置启动项为BIOS启动方式,默认uefi,改主板 一定要改启动方式

2024-12-28T05:10:45.png

改显卡

加两条代码到
.vmx
文件

cpuid.1.ecx = "00000010100111101110001111111111"
cpuid.1.edx = "10111111111010111111101111111111"
monitor_control.restrict_backdoor = "TRUE"

按shift重启-启动设置-重启-7禁用强制签名

先安装VMware Tools,才有显卡驱动

装显卡驱动的方法:
1. 用到的工具WinRAR
2. 驱动人生

点击 虚拟机设置-硬盘-映射,取消勾选只读,
选择盘符
,确定,是

把显卡驱动
SVGA 3d显卡驱动.rar
拖到共享盘

断开连接

确定

这样就把本地显卡驱动挪到虚拟机盘符里面了

打开虚拟机

2024-12-28T06:23:17.png

把高亮的两行的 15AD 改成 10DE , 0405 改成显卡ID0700

改最下面的三行

DiskID = "NVIDIA GeForce"
CompanyName = "NVIDIA, Inc."
SVGA = "NVIDIA GeForce RTX 3080"

全部替换
vmware
->
NVIDIA
,
vm
->
nv

保存

接下来把这个显卡驱动目录的vm开头的文件改为nv开头

批处理

@echo off
setlocal enabledelayedexpansion

rem 遍历当前目录下的所有文件
for %%f in (vm*) do (
    set "filename=%%f"
    set "newname=nv!filename:~2!"
    
    rem 重命名文件
    ren "%%f" "!newname!"
)

echo 所有文件名以 vm 开头的文件已被重命名为以 nv 开头。
endlocal

设备管理器-显示设备-右键更新驱动程序-从计算机的设备驱动列表中选择-从磁盘安装-浏览-打开驱动目录

先安装原版,再安装修改后的版本

2024-12-28T08:24:12.png

打开注册表编辑器

2024-12-28T08:27:58.png

把所有的
VMware SVGA 3D
改成
NVIDIA GeForce RTX 3080


然而还是被检测了。。。

image

参考

什么是内存映射(Memory-Mapped File)?

内存映射(mmap)是一种将文件内容映射到内存中的技术,应用程序可以像操作内存一样对文件内容进行读写,而不需要显式地进行磁盘 I/O 操作。修改的内容会自动由操作系统同步到磁盘。

内存映射需要读取磁盘文件吗?

需要。毕竟,内存中的数据来源于磁盘文件。操作系统会将文件的部分或全部内容加载到内存,供程序访问。

为什么不直接读取文件?

直接读取文件,缓存到用户进程内,这样不也可以随意访问吗?相比这种方式,mmap有何优势?

1.
数据拷贝次数少

内存映射相比直接读取文件的一个主要优势是减少了数据拷贝的次数

  • 内存映射
    :磁盘 => 内核空间
  • 直接读取
    :磁盘 => 内核空间 => 用户空间

正常情况下,应用程序不能直接访问内核空间中的数据。要访问这些数据,通常需要触发系统调用将数据从内核空间拷贝到用户空间。

而内存映射通过将文件内容直接映射到进程的虚拟地址空间,消除了这种额外的拷贝开销,从而提高了效率。

2.
加载范围与按需加载

直接读取文件时,通常需要将整个文件加载到进程的内存缓存中,这对于大文件来说非常低效。而内存映射则更加高效,操作系统会根据需要
按需加载
文件的部分内容。

对于用户来说,内存映射的效果是可以像操作内存一样访问文件内容,而无需担心数据加载的问题。

3.
自动写回磁盘

  • 内存映射
    :修改的内容会自动同步到磁盘,操作系统会处理文件内容的写回。
  • 直接读取
    :如果是直接读取,文件内容的修改要么全部写回磁盘,要么应用程序需要识别哪些区域发生了变化并单独写回磁盘,这样的管理工作相对繁琐。

Kafka在哪里使用了内存映射?

从源码中可以看到,Kafka 只在
索引文件
中使用了内存映射(mmap)。内存映射的优势在于它允许
随机访问
,这与索引文件的应用场景非常匹配。

Kafka的索引文件通过二分法查找消息的存储位置,而内存映射的随机访问特性使得这个过程更加高效。

但是看源码可以发现,日志段则没有使用文件映射,而是直接使用FileChannel.write(buffer)写出数据。

//kafka 3.9.0部分源码

LogSegment.java

package org.apache.kafka.storage.internals.log
...public class LogSegment implementsCloseable {
...
private finalFileRecords log;
...
/*** Append the given messages starting with the given offset. Add
* an entry to the index if needed.
*
* It is assumed this method is being called from within a lock, it is not thread-safe otherwise.
*
*
@paramlargestOffset The last offset in the message set
*
@paramlargestTimestampMs The largest timestamp in the message set.
*
@paramshallowOffsetOfMaxTimestamp The last offset of earliest batch with max timestamp in the messages to append.
*
@paramrecords The log entries to append.
*
@throwsLogSegmentOffsetOverflowException if the largest offset causes index offset overflow*/ public void append(longlargestOffset,longlargestTimestampMs,longshallowOffsetOfMaxTimestamp,
MemoryRecords records)
throwsIOException {if (records.sizeInBytes() > 0) {
LOGGER.trace(
"Inserting {} bytes at end offset {} at position {} with largest timestamp {} at offset {}",
records.sizeInBytes(), largestOffset, log.sizeInBytes(), largestTimestampMs, shallowOffsetOfMaxTimestamp);
int physicalPosition =log.sizeInBytes();if (physicalPosition == 0)
rollingBasedTimestamp
=OptionalLong.of(largestTimestampMs);

ensureOffsetInRange(largestOffset);
//append the messages long appendedBytes =log.append(records);
LOGGER.trace(
"Appended {} to {} at end offset {}", appendedBytes, log.file(), largestOffset);//Update the in memory max timestamp and corresponding offset. if (largestTimestampMs >maxTimestampSoFar()) {
maxTimestampAndOffsetSoFar
= newTimestampOffset(largestTimestampMs, shallowOffsetOfMaxTimestamp);
}
//append an entry to the index (if needed)
// 稀疏索引,有一定的间隔。可以减少索引量 if (bytesSinceLastIndexEntry >indexIntervalBytes) {
offsetIndex().append(largestOffset, physicalPosition);
timeIndex().maybeAppend(maxTimestampSoFar(), shallowOffsetOfMaxTimestampSoFar());
bytesSinceLastIndexEntry
= 0;
}
bytesSinceLastIndexEntry
+=records.sizeInBytes();
}
}

...
}

FileRecords.java

packageorg.apache.kafka.common.record;

...
public class FileRecords extends AbstractRecords implementsCloseable {
...
private finalFileChannel channel;
....
public int append(MemoryRecords records) throwsIOException {if (records.sizeInBytes() > Integer.MAX_VALUE -size.get())throw new IllegalArgumentException("Append of size " + records.sizeInBytes() + " bytes is too large for segment with current file position at " +size.get());int written =records.writeFullyTo(channel);
size.getAndAdd(written);
returnwritten;
}

...


}

MemoryRecords.java

packageorg.apache.kafka.common.record;
....
public class MemoryRecords extendsAbstractRecords {
...
private finalByteBuffer buffer;
...
/*** Write all records to the given channel (including partial records).
*
@paramchannel The channel to write to
*
@returnThe number of bytes written
*
@throwsIOException For any IO errors writing to the channel*/ public int writeFullyTo(GatheringByteChannel channel) throwsIOException {
buffer.mark();
int written = 0;while (written <sizeInBytes())
written
+=channel.write(buffer);
buffer.reset();
returnwritten;
}

....
}

为什么日志段不使用内存映射?

按理说,直接读写内存不是更快吗?日志段为什么不使用内存映射。

1. 内存消耗过大

Kafka 每个主题和分区都有多个日志段文件。如果将所有日志段文件都映射到内存中,将消耗大量的内存资源。尤其是在日志数据量非常大的情况下,这种做法会极大增加内存的负担,可能会在内存受限的环境中不可行。

2. 顺序读写已足够高效

连续区域:
Kafka 的写入和读取操作通常涉及批量消息,这些消息在磁盘上是按顺序存储的。由于数据在物理存储上是连续的,操作系统可以通过一次磁盘寻道就定位到所需的区域,从而减少寻道时间和开销。

页缓存(Page Cache):
操系统的页缓存机制(Page Cache)能够将频繁访问的文件内容缓存到内存中。操作系统也会预读取一部分文件后续内容到缓存中,提高缓存命中的概率,避免频繁从磁盘加载数据。

零拷贝(sendfile)
:Kafka 的日志文件主要由远端消费者触发读取。由于日志在写入文件的时候都已经处理好了,而且读取也是顺序进行的,故Kafka Broker无需进行额外处理,数据可以直接从磁盘通过
sendfile()
系统调用发送到客户端,从内核直接拷贝到 socket 缓冲区,而不需要先载入到用户空间内存中。

总结

内存映射技术通过将文件内容映射到内存,有效避免了多次拷贝和高昂的 I/O 成本,非常适合需要随机访问的场景。然而,对于 Kafka 的日志段文件,顺序写入和读取已经足够高效,因此 Kafka 选择不使用内存映射,而是依赖操作系统的页缓存来提高性能。通过这种设计,Kafka 在内存消耗和 I/O 性能之间实现了良好的平衡。

参考内容

https://stackoverflow.com/questions/2100584/difference-between-sequential-write-and-random-write

https://storedbits.com/sequential-vs-random-data/

https://www.mail-archive.com/users@kafka.apache.org/msg30260.html

https://lists.freebsd.org/pipermail/freebsd-questions/2004-June/050371.html