我有一个图像缓冲区,我需要转换为另一种格式。原始图像缓冲区是四个通道,每通道8位,Alpha,红色,绿色和蓝色。目标缓冲区是三个通道,每通道8位,蓝色,绿色和红色。
所以蛮力方法是:
// Assume a 32 x 32 pixel image
#define IMAGESIZE (32*32)
typedef struct{ UInt8 Alpha; UInt8 Red; UInt8 Green; UInt8 Blue; } ARGB;
typedef struct{ UInt8 Blue; UInt8 Green; UInt8 Red; } BGR;
ARGB orig[IMAGESIZE];
BGR dest[IMAGESIZE];
for(x = 0; x < IMAGESIZE; x++)
{
dest[x].Red = orig[x].Red;
dest[x].Green = orig[x].Green;
dest[x].Blue = orig[x].Blue;
}
但是,我需要比循环和三字节副本提供的速度更快的速度。我希望可以使用一些技巧来减少内存读写次数,因为我在32位机器上运行。
每张图片都是至少4像素的倍数。因此我们可以处理16个ARGB字节并将它们移动到每个循环12个RGB字节。也许这个事实可以用来加快速度,特别是当它很好地落入32位边界时。
我可以访问OpenCL - 虽然这需要将整个缓冲区移动到GPU内存中,然后将结果移回去,OpenCL可以同时处理图像的许多部分,以及大内存块的事实移动实际上非常有效可能使这个值得探索。
虽然我已经给出了上面的小缓冲区的例子,但我真的正在移动高清视频(1920x1080),有时更大,大多数是更小的缓冲区,所以虽然32x32的情况可能是微不足道的,复制8.3MB的图像数据字节按字节真的非常非常糟糕。
在英特尔处理器(核心2及更高版本)上运行,因此我知道存在流式和数据处理命令,但不知道 - 可能指向寻找专门数据处理指令的指针会很好。
这是进入OS X应用程序,我正在使用XCode 4.如果程序集是无痛的并且显而易见的方法,我可以顺着这条路走下去,但是在这个设置之前没有完成它我担心会把太多时间投入其中。
伪代码很好 - 我不是在寻找一个完整的解决方案,只是算法和任何可能不会立即清楚的诡计的解释。
答案 0 :(得分:55)
我写了4个不同的版本,通过交换字节来工作。我使用带有-O3 -mssse3
的gcc 4.2.1编译它们,在32MB随机数据上运行10次并找到平均值。
第一个版本使用C循环分别转换每个像素,使用OSSwapInt32
函数(使用bswap
编译为-O3
指令。)
void swap1(ARGB *orig, BGR *dest, unsigned imageSize) {
unsigned x;
for(x = 0; x < imageSize; x++) {
*((uint32_t*)(((uint8_t*)dest)+x*3)) = OSSwapInt32(((uint32_t*)orig)[x]);
}
}
第二种方法执行相同的操作,但使用内联汇编循环而不是C循环。
void swap2(ARGB *orig, BGR *dest, unsigned imageSize) {
asm (
"0:\n\t"
"movl (%1),%%eax\n\t"
"bswapl %%eax\n\t"
"movl %%eax,(%0)\n\t"
"addl $4,%1\n\t"
"addl $3,%0\n\t"
"decl %2\n\t"
"jnz 0b"
:: "D" (dest), "S" (orig), "c" (imageSize)
: "flags", "eax"
);
}
第三个版本是just a poseur's answer的修改版本。我将内置函数转换为GCC等价函数并使用lddqu
内置函数,以便输入参数不需要对齐。
typedef uint8_t v16qi __attribute__ ((vector_size (16)));
void swap3(uint8_t *orig, uint8_t *dest, size_t imagesize) {
v16qi mask = __builtin_ia32_lddqu((const char[]){3,2,1,7,6,5,11,10,9,15,14,13,0xFF,0xFF,0xFF,0XFF});
uint8_t *end = orig + imagesize * 4;
for (; orig != end; orig += 16, dest += 12) {
__builtin_ia32_storedqu(dest,__builtin_ia32_pshufb128(__builtin_ia32_lddqu(orig),mask));
}
}
最后,第四个版本是第三个版本的内联汇编。
void swap2_2(uint8_t *orig, uint8_t *dest, size_t imagesize) {
int8_t mask[16] = {3,2,1,7,6,5,11,10,9,15,14,13,0xFF,0xFF,0xFF,0XFF};//{0xFF, 0xFF, 0xFF, 0xFF, 13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3};
asm (
"lddqu (%3),%%xmm1\n\t"
"0:\n\t"
"lddqu (%1),%%xmm0\n\t"
"pshufb %%xmm1,%%xmm0\n\t"
"movdqu %%xmm0,(%0)\n\t"
"add $16,%1\n\t"
"add $12,%0\n\t"
"sub $4,%2\n\t"
"jnz 0b"
:: "r" (dest), "r" (orig), "r" (imagesize), "r" (mask)
: "flags", "xmm0", "xmm1"
);
}
在我的2010 MacBook Pro上,2.4 Ghz i5,4GB内存,这些是每个平均时间:
Version 1: 10.8630 milliseconds Version 2: 11.3254 milliseconds Version 3: 9.3163 milliseconds Version 4: 9.3584 milliseconds
正如您所看到的,编译器在优化时已足够好,您无需编写程序集。此外,矢量函数在32MB数据上的速度仅提高了1.5毫秒,因此如果你想支持最早的不支持SSSE3的英特尔mac,它不会造成太大的伤害。
编辑:liori要求提供标准偏差信息。不幸的是,我没有保存数据点,所以我进行了另外25次迭代的测试。
Average | Standard Deviation Brute force: 18.01956 ms | 1.22980 ms (6.8%) Version 1: 11.13120 ms | 0.81076 ms (7.3%) Version 2: 11.27092 ms | 0.66209 ms (5.9%) Version 3: 9.29184 ms | 0.27851 ms (3.0%) Version 4: 9.40948 ms | 0.32702 ms (3.5%)
此外,这是来自新测试的原始数据,以防任何人想要它。对于每次迭代,随机生成32MB数据集并运行这四个函数。下面列出了每个函数的运行时间(以微秒为单位)。
Brute force: 22173 18344 17458 17277 17508 19844 17093 17116 19758 17395 18393 17075 17499 19023 19875 17203 16996 17442 17458 17073 17043 18567 17285 17746 17845 Version 1: 10508 11042 13432 11892 12577 10587 11281 11912 12500 10601 10551 10444 11655 10421 11285 10554 10334 10452 10490 10554 10419 11458 11682 11048 10601 Version 2: 10623 12797 13173 11130 11218 11433 11621 10793 11026 10635 11042 11328 12782 10943 10693 10755 11547 11028 10972 10811 11152 11143 11240 10952 10936 Version 3: 9036 9619 9341 8970 9453 9758 9043 10114 9243 9027 9163 9176 9168 9122 9514 9049 9161 9086 9064 9604 9178 9233 9301 9717 9156 Version 4: 9339 10119 9846 9217 9526 9182 9145 10286 9051 9614 9249 9653 9799 9270 9173 9103 9132 9550 9147 9157 9199 9113 9699 9354 9314
答案 1 :(得分:25)
显而易见,使用pshufb。
#include <assert.h>
#include <inttypes.h>
#include <tmmintrin.h>
// needs:
// orig is 16-byte aligned
// imagesize is a multiple of 4
// dest has 4 trailing scratch bytes
void convert(uint8_t *orig, size_t imagesize, uint8_t *dest) {
assert((uintptr_t)orig % 16 == 0);
assert(imagesize % 4 == 0);
__m128i mask = _mm_set_epi8(-128, -128, -128, -128, 13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3);
uint8_t *end = orig + imagesize * 4;
for (; orig != end; orig += 16, dest += 12) {
_mm_storeu_si128((__m128i *)dest, _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig), mask));
}
}
答案 2 :(得分:15)
仅结合一个poseur和Jitamaro的答案,如果您假设输入和输出是16字节对齐的,并且如果您一次处理4个像素,则可以使用shuffle,mask,ands和ors的组合来存储使用对齐的商店。主要思想是生成四个中间数据集,然后将它们与掩码一起生成以选择相关的像素值并写出3个16字节的像素数据集。请注意,我没有编译它或尝试运行它。
EDIT2:有关底层代码结构的更多细节:
使用SSE2,16字节对齐读取和写入16字节可以获得更好的性能。由于您的3字节像素每16像素只能对齐16个字节,因此我们一次使用混洗和遮罩以及16个输入像素的组合一次批量处理16个像素。
从LSB到MSB,输入看起来像这样,忽略了特定的组件:
s[0]: 0000 0000 0000 0000
s[1]: 1111 1111 1111 1111
s[2]: 2222 2222 2222 2222
s[3]: 3333 3333 3333 3333
并且ouptuts看起来像这样:
d[0]: 000 000 000 000 111 1
d[1]: 11 111 111 222 222 22
d[2]: 2 222 333 333 333 333
因此要生成这些输出,您需要执行以下操作(稍后我将指定实际的转换):
d[0]= combine_0(f_0_low(s[0]), f_0_high(s[1]))
d[1]= combine_1(f_1_low(s[1]), f_1_high(s[2]))
d[2]= combine_2(f_1_low(s[2]), f_1_high(s[3]))
现在,combine_<x>
应该是什么样的?如果我们假设d
仅仅s
被压缩在一起,我们可以将两个s
连接到一个掩码和一个或:
combine_x(left, right)= (left & mask(x)) | (right & ~mask(x))
其中(1表示选择左侧像素,0表示选择右侧像素): mask(0)= 111 111 111 111 000 0 mask(1)= 11 111 111 000 000 00 mask(2)= 1 111 000 000 000 000
但实际的转换(f_<x>_low
,f_<x>_high
)实际上并非那么简单。由于我们正在从源像素中反转和删除字节,因此实际的转换是(为了简洁起见,第一个目的地):
d[0]=
s[0][0].Blue s[0][0].Green s[0][0].Red
s[0][1].Blue s[0][1].Green s[0][1].Red
s[0][2].Blue s[0][2].Green s[0][2].Red
s[0][3].Blue s[0][3].Green s[0][3].Red
s[1][0].Blue s[1][0].Green s[1][0].Red
s[1][1].Blue
如果将上述内容转换为从源到dest的字节偏移量,则得到:
d [0] =
&amp; s [0] +3&amp; s [0] +2&amp; s [0] +1
&amp; s [0] +7&amp; s [0] +6&amp; s [0] +5
&amp; s [0] +11&amp; s [0] +10&amp; s [0] +9
&amp; s [0] +15&amp; s [0] +14&amp; s [0] +13
&amp; s [1] +3&amp; s [1] +2&amp; s [1] +1
&安培; S [1] 7
(如果您查看所有s [0]偏移,它们只会以相反的顺序匹配一个poseur的shuffle掩码。)
现在,我们可以生成一个shuffle掩码,将每个源字节映射到目标字节(X
意味着我们不关心该值是什么):
f_0_low= 3 2 1 7 6 5 11 10 9 15 14 13 X X X X
f_0_high= X X X X X X X X X X X X 3 2 1 7
f_1_low= 6 5 11 10 9 15 14 13 X X X X X X X X
f_1_high= X X X X X X X X 3 2 1 7 6 5 11 10
f_2_low= 9 15 14 13 X X X X X X X X X X X X
f_2_high= X X X X 3 2 1 7 6 5 11 10 9 15 14 13
我们可以通过查看每个源像素使用的蒙版来进一步优化这一点。如果你看一下我们用于s [1]的shuffle面具:
f_0_high= X X X X X X X X X X X X 3 2 1 7
f_1_low= 6 5 11 10 9 15 14 13 X X X X X X X X
由于两个shuffle蒙版不重叠,我们可以将它们组合起来,然后简单地屏蔽combine_中不相关的像素,我们已经做过了!以下代码执行所有这些优化(此外,它假定源和目标地址是16字节对齐的)。此外,掩码以MSB-> LSB顺序以代码写出,以防您对排序感到困惑。
编辑:将商店更改为_mm_stream_si128
,因为您可能会进行大量写操作,而我们不一定要刷新缓存。另外它应该是对齐的,所以你得到自由的穿孔!
#include <assert.h>
#include <inttypes.h>
#include <tmmintrin.h>
// needs:
// orig is 16-byte aligned
// imagesize is a multiple of 4
// dest has 4 trailing scratch bytes
void convert(uint8_t *orig, size_t imagesize, uint8_t *dest) {
assert((uintptr_t)orig % 16 == 0);
assert(imagesize % 16 == 0);
__m128i shuf0 = _mm_set_epi8(
-128, -128, -128, -128, // top 4 bytes are not used
13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3); // bottom 12 go to the first pixel
__m128i shuf1 = _mm_set_epi8(
7, 1, 2, 3, // top 4 bytes go to the first pixel
-128, -128, -128, -128, // unused
13, 14, 15, 9, 10, 11, 5, 6); // bottom 8 go to second pixel
__m128i shuf2 = _mm_set_epi8(
10, 11, 5, 6, 7, 1, 2, 3, // top 8 go to second pixel
-128, -128, -128, -128, // unused
13, 14, 15, 9); // bottom 4 go to third pixel
__m128i shuf3 = _mm_set_epi8(
13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3, // top 12 go to third pixel
-128, -128, -128, -128); // unused
__m128i mask0 = _mm_set_epi32(0, -1, -1, -1);
__m128i mask1 = _mm_set_epi32(0, 0, -1, -1);
__m128i mask2 = _mm_set_epi32(0, 0, 0, -1);
uint8_t *end = orig + imagesize * 4;
for (; orig != end; orig += 64, dest += 48) {
__m128i a= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig), shuf0);
__m128i b= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 1), shuf1);
__m128i c= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 2), shuf2);
__m128i d= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 3), shuf3);
_mm_stream_si128((__m128i *)dest, _mm_or_si128(_mm_and_si128(a, mask0), _mm_andnot_si128(b, mask0));
_mm_stream_si128((__m128i *)dest + 1, _mm_or_si128(_mm_and_si128(b, mask1), _mm_andnot_si128(c, mask1));
_mm_stream_si128((__m128i *)dest + 2, _mm_or_si128(_mm_and_si128(c, mask2), _mm_andnot_si128(d, mask2));
}
}
答案 3 :(得分:11)
我来参加派对的时间有点晚了,似乎社区已经决定使用poseur的pshufb-answer但是分发2000声望,这是非常慷慨的我必须尝试一下。
这是我的版本没有平台特定的内在函数或特定于机器的asm,我已经包含了一些显示 4x加速的跨平台时序代码,如果你像我一样做这两点的话...... AND 激活编译器优化(寄存器优化,循环展开):
#include "stdlib.h"
#include "stdio.h"
#include "time.h"
#define UInt8 unsigned char
#define IMAGESIZE (1920*1080)
int main() {
time_t t0, t1;
int frames;
int frame;
typedef struct{ UInt8 Alpha; UInt8 Red; UInt8 Green; UInt8 Blue; } ARGB;
typedef struct{ UInt8 Blue; UInt8 Green; UInt8 Red; } BGR;
ARGB* orig = malloc(IMAGESIZE*sizeof(ARGB));
if(!orig) {printf("nomem1");}
BGR* dest = malloc(IMAGESIZE*sizeof(BGR));
if(!dest) {printf("nomem2");}
printf("to start original hit a key\n");
getch();
t0 = time(0);
frames = 1200;
for(frame = 0; frame<frames; frame++) {
int x; for(x = 0; x < IMAGESIZE; x++) {
dest[x].Red = orig[x].Red;
dest[x].Green = orig[x].Green;
dest[x].Blue = orig[x].Blue;
x++;
}
}
t1 = time(0);
printf("finished original of %u frames in %u seconds\n", frames, t1-t0);
// on my core 2 subnotebook the original took 16 sec
// (8 sec with compiler optimization -O3) so at 60 FPS
// (instead of the 1200) this would be faster than realtime
// (if you disregard any other rendering you have to do).
// However if you either want to do other/more processing
// OR want faster than realtime processing for e.g. a video-conversion
// program then this would have to be a lot faster still.
printf("to start alternative hit a key\n");
getch();
t0 = time(0);
frames = 1200;
unsigned int* reader;
unsigned int* end = reader+IMAGESIZE;
unsigned int cur; // your question guarantees 32 bit cpu
unsigned int next;
unsigned int temp;
unsigned int* writer;
for(frame = 0; frame<frames; frame++) {
reader = (void*)orig;
writer = (void*)dest;
next = *reader;
reader++;
while(reader<end) {
cur = next;
next = *reader;
// in the following the numbers are of course the bitmasks for
// 0-7 bits, 8-15 bits and 16-23 bits out of the 32
temp = (cur&255)<<24 | (cur&65280)<<16|(cur&16711680)<<8|(next&255);
*writer = temp;
reader++;
writer++;
cur = next;
next = *reader;
temp = (cur&65280)<<24|(cur&16711680)<<16|(next&255)<<8|(next&65280);
*writer = temp;
reader++;
writer++;
cur = next;
next = *reader;
temp = (cur&16711680)<<24|(next&255)<<16|(next&65280)<<8|(next&16711680);
*writer = temp;
reader++;
writer++;
}
}
t1 = time(0);
printf("finished alternative of %u frames in %u seconds\n", frames, t1-t0);
// on my core 2 subnotebook this alternative took 10 sec
// (4 sec with compiler optimization -O3)
}
结果就是这些(在我的核心2子笔记本上):
F:\>gcc b.c -o b.exe
F:\>b
to start original hit a key
finished original of 1200 frames in 16 seconds
to start alternative hit a key
finished alternative of 1200 frames in 10 seconds
F:\>gcc b.c -O3 -o b.exe
F:\>b
to start original hit a key
finished original of 1200 frames in 8 seconds
to start alternative hit a key
finished alternative of 1200 frames in 4 seconds
答案 4 :(得分:6)
您想使用Duff的设备:http://en.wikipedia.org/wiki/Duff%27s_device。它也在JavaScript中工作。这篇文章但是阅读http://lkml.indiana.edu/hypermail/linux/kernel/0008.2/0171.html有点好笑。想象一下具有512千克移动的Duff设备。
答案 5 :(得分:6)
这个汇编函数应该这样做,但是我不知道你是否想保留旧数据,这个函数会覆盖它。
该代码适用于具有英特尔程序集风格的MinGW GCC ,您必须对其进行修改以适合您的编译器/汇编程序。
extern "C" {
int convertARGBtoBGR(uint buffer, uint size);
__asm(
".globl _convertARGBtoBGR\n"
"_convertARGBtoBGR:\n"
" push ebp\n"
" mov ebp, esp\n"
" sub esp, 4\n"
" mov esi, [ebp + 8]\n"
" mov edi, esi\n"
" mov ecx, [ebp + 12]\n"
" cld\n"
" convertARGBtoBGR_loop:\n"
" lodsd ; load value from [esi] (4byte) to eax, increment esi by 4\n"
" bswap eax ; swap eax ( A R G B ) to ( B G R A )\n"
" stosd ; store 4 bytes to [edi], increment edi by 4\n"
" sub edi, 1; move edi 1 back down, next time we will write over A byte\n"
" loop convertARGBtoBGR_loop\n"
" leave\n"
" ret\n"
);
}
您应该这样称呼它:
convertARGBtoBGR( &buffer, IMAGESIZE );
此功能每个像素/数据包仅访问内存两次(1次读取,1次写入),与暴力方法相比(至少/假设已编译为注册)3读取和3写操作。方法是相同的,但实现使它更有效。
答案 6 :(得分:6)
结合这里的一个快速转换函数,给定对Core 2的访问权限,将转换拆分为线程是明智的,这些线程可以处理它们的第四个数据,就像在这个psudeocode中一样:
void bulk_bgrFromArgb(byte[] dest, byte[] src, int n)
{
thread threads[] = {
create_thread(bgrFromArgb, dest, src, n/4),
create_thread(bgrFromArgb, dest+n/4, src+n/4, n/4),
create_thread(bgrFromArgb, dest+n/2, src+n/2, n/4),
create_thread(bgrFromArgb, dest+3*n/4, src+3*n/4, n/4),
}
join_threads(threads);
}
答案 7 :(得分:4)
你可以用4个像素的块来做,用无符号长指针移动32位。只需要考虑4个32位像素就可以通过移位和OR / AND来构造,3个单词代表4个24位像素,如下所示:
//col0 col1 col2 col3
//ARGB ARGB ARGB ARGB 32bits reading (4 pixels)
//BGRB GRBG RBGR 32 bits writing (4 pixels)
在所有现代32/64位处理器(桶移位技术)中,移位操作总是按1个指令周期完成,因此构建这3个字用于写入,按位AND和OR的最快方法也非常快。
像这样:
//assuming we have 4 ARGB1 ... ARGB4 pixels and 3 32 bits words, W1, W2 and W3 to write
// and *dest its an unsigned long pointer for destination
W1 = ((ARGB1 & 0x000f) << 24) | ((ARGB1 & 0x00f0) << 8) | ((ARGB1 & 0x0f00) >> 8) | (ARGB2 & 0x000f);
*dest++ = W1;
等等......循环中的下一个像素。
您需要对不是4的倍数的图像进行一些调整,但我敢打赌,这是最快的方法,不使用汇编程序。
顺便说一句,忘记使用结构和索引访问,那些是 SLOWER 所有用于移动数据的方法,只需看看已编译的C ++程序的反汇编列表,你就会同意跟我一起。
答案 8 :(得分:3)
typedef struct{ UInt8 Alpha; UInt8 Red; UInt8 Green; UInt8 Blue; } ARGB;
typedef struct{ UInt8 Blue; UInt8 Green; UInt8 Red; } BGR;
除了汇编或编译器内在函数之外,我可能会尝试执行以下操作,而非常仔细地验证结束行为,因为其中一些(涉及联合)可能依赖于编译器实现:
union uARGB
{
struct ARGB argb;
UInt32 x;
};
union uBGRA
{
struct
{
BGR bgr;
UInt8 Alpha;
} bgra;
UInt32 x;
};
然后对于你的代码内核,无论什么循环展开都是合适的:
inline void argb2bgr(BGR* pbgr, ARGB* pargb)
{
uARGB* puargb = (uARGB*)pargb;
uBGRA ubgra;
ubgra.x = __byte_reverse_32(pargb->x);
*pbgr = ubgra.bgra.bgr;
}
其中__byte_reverse_32()
假定存在一个反转32位字的字节的编译器内部函数。
总结基本方法:
答案 9 :(得分:3)
虽然您可以根据CPU使用情况使用一些技巧,
This kind of operations can be done fasted with GPU.
您似乎使用C / C ++ ...所以 GPU编程的替代方案可能是(在Windows平台上)
短暂使用GPU进行此类阵列操作,以便进行更快速的计算。它们是专为它而设计的。
答案 10 :(得分:3)
我还没有看到有人在GPU上展示如何操作的例子。
前段时间我写了类似你问题的内容。我从YUV格式的video4linux2相机接收数据,并希望将其绘制为屏幕上的灰度级(仅Y组件)。我还想绘制蓝色太暗的区域和红色的过饱和区域。
我开始使用freeglut发行版中的smooth_opengl3.c示例。
将数据作为YUV复制到纹理中,然后应用以下GLSL着色器程序。我确信GLSL代码现在可以在所有的mac上运行,并且它将比所有CPU方法快得多。
请注意,我没有关于如何获取数据的经验。从理论上讲,glReadPixels应该读回数据,但我从未测量过它的性能。
OpenCL可能是更简单的方法,但是当我有一个支持它的笔记本时,我只会开始开发。
(defparameter *vertex-shader*
"void main(){
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
gl_FrontColor = gl_Color;
gl_TexCoord[0] = gl_MultiTexCoord0;
}
")
(progn
(defparameter *fragment-shader*
"uniform sampler2D textureImage;
void main()
{
vec4 q=texture2D( textureImage, gl_TexCoord[0].st);
float v=q.z;
if(int(gl_FragCoord.x)%2 == 0)
v=q.x;
float x=0; // 1./255.;
v-=.278431;
v*=1.7;
if(v>=(1.0-x))
gl_FragColor = vec4(255,0,0,255);
else if (v<=x)
gl_FragColor = vec4(0,0,255,255);
else
gl_FragColor = vec4(v,v,v,255);
}
")