2023. 10. 21. 17:31ㆍ실험실(C++)
서문
정점 셰이더를 통해 객체를 직접 배치하는 경우 보통 논리적 공간 내 배치를 위한 행렬(주로 World 또는 Model Matrix라고 불림), 공간 중 보이고자 하는 위치를 정하기 위한 카메라 행렬(주로 Camera 또는 View Matrix라고 불림), 해당 방향을 기준으로 보이고자 하는 영역을 정하기 위한 투영 행렬(주로 Projection Matrix라고 불림)을 통해 셰이더 내에서 모델을 변환시키는 것으로 수행합니다.
셰이딩을 안 한다면 뷰 행렬과 투영 행렬을 분리해서 GPU에 넘길 필요가 줄어들고, 보이고자 하는 공간이 2D라면 투영 행렬의 역할은 종횡비 맞춤 및 확대 정도가 되고 카메라의 역할은 평행이동이 전부.
이때 객체의 회전이 없으면 뷰포트 세팅만으로 객체 배치를 할 수 있는 셈인데요, 여기서 만약 대부분의 객체에 회전이 가해지지 않는다면, 뷰포트 세팅만으로 객체를 배치하는 파이프라인을 사용할 가치가 생기는 거 아닐까요?
라고 생각했는데, 특별히 검색 결과가 나오지 않아 직접 해 보는 수밖에 없게 됐습니다.
테스트
테스트는 Vulkan 1.0, OpenGL 4.6, D3D 11에서 수행합니다. (제 YERM에 지금 그것밖에 없어서)
재현을 위한 테스트 기준점은 여기서 클론 받으면 됩니다. 수정하는 부분은 기반 api마다 다른데, 대부분 yr_game.cpp만 수정됩니다.
GitHub - onart/YERM: Windows, X11, Android Target Vulkan 2/3D Interactive Graphics framework
Windows, X11, Android Target Vulkan 2/3D Interactive Graphics framework - GitHub - onart/YERM: Windows, X11, Android Target Vulkan 2/3D Interactive Graphics framework
github.com
방식은 다음과 같습니다.
1. 빌드 타임에 100개 정도의 배치 영역을 준비합니다. 이들은 모두 화면 안에 그려집니다. (래스터화된 픽셀이 많을수록 draw가 느려지기 때문에 이 조건은 모든 케이스에서 동일하게 맞출 필요가 있습니다.)
// 각각 좌측 상단 좌표, 가로/세로입니다.
{ 0.206f, 0.274f },{ 0.107f, 0.206f },
{ 0.119f, 0.406f },{ 0.609f, 0.178f },
{ 0.080f, 0.903f },{ 0.401f, 0.662f },
{ 0.958f, 0.288f },{ 0.126f, 0.635f },
{ 0.023f, 0.861f },{ 0.469f, 0.464f },
{ 0.023f, 0.986f },{ 0.173f, 0.848f },
{ 0.875f, 0.726f },{ 0.327f, 0.453f },
{ 0.933f, 0.367f },{ 0.120f, 0.328f },
{ 0.497f, 0.628f },{ 0.194f, 0.677f },
{ 0.970f, 0.764f },{ 0.855f, 0.043f },
{ 0.496f, 0.869f },{ 0.203f, 0.703f },
{ 0.093f, 0.681f },{ 0.440f, 0.204f },
{ 0.974f, 0.344f },{ 0.896f, 0.882f },
{ 0.693f, 0.344f },{ 0.620f, 0.098f },
{ 0.282f, 0.453f },{ 0.206f, 0.577f },
{ 0.533f, 0.832f },{ 0.372f, 0.131f },
{ 0.730f, 0.184f },{ 0.509f, 0.659f },
{ 0.172f, 0.363f },{ 0.558f, 0.522f },
{ 0.967f, 0.576f },{ 0.213f, 0.210f },
{ 0.155f, 0.807f },{ 0.665f, 0.716f },
{ 0.117f, 0.472f },{ 0.582f, 0.294f },
{ 0.835f, 0.978f },{ 0.361f, 0.314f },
{ 0.872f, 0.916f },{ 0.478f, 0.699f },
{ 0.500f, 0.193f },{ 0.962f, 0.587f },
{ 0.646f, 0.243f },{ 0.826f, 0.080f },
{ 0.648f, 0.426f },{ 0.676f, 0.135f },
{ 0.742f, 0.091f },{ 0.249f, 0.477f },
{ 0.852f, 0.839f },{ 0.598f, 0.348f },
{ 0.520f, 0.726f },{ 0.760f, 0.458f },
{ 0.859f, 0.702f },{ 0.863f, 0.264f },
{ 0.302f, 0.409f },{ 0.537f, 0.479f },
{ 0.282f, 0.209f },{ 0.969f, 0.107f },
{ 0.284f, 0.712f },{ 0.881f, 0.845f },
{ 0.709f, 0.642f },{ 0.213f, 0.849f },
{ 0.667f, 0.284f },{ 0.674f, 0.867f },
{ 0.077f, 0.265f },{ 0.467f, 0.318f },
{ 0.609f, 0.134f },{ 0.096f, 0.512f },
{ 0.727f, 0.612f },{ 0.012f, 0.891f },
{ 0.785f, 0.039f },{ 0.159f, 0.673f },
{ 0.475f, 0.726f },{ 0.452f, 0.802f },
{ 0.188f, 0.818f },{ 0.679f, 0.046f },
{ 0.979f, 0.221f },{ 0.326f, 0.570f },
{ 0.016f, 0.709f },{ 0.994f, 0.124f },
{ 0.572f, 0.032f },{ 0.473f, 0.205f },
{ 0.209f, 0.251f },{ 0.728f, 0.265f },
{ 0.110f, 0.990f },{ 0.557f, 0.549f },
{ 0.358f, 0.735f },{ 0.137f, 0.257f },
{ 0.378f, 0.345f },{ 0.575f, 0.196f },
{ 0.401f, 0.456f },{ 0.417f, 0.503f },
{ 0.195f, 0.936f },{ 0.944f, 0.082f },
{ 0.280f, 0.439f },{ 0.626f, 0.772f },
{ 0.860f, 0.485f },{ 0.742f, 0.863f },
{ 0.516f, 0.306f },{ 0.148f, 0.798f },
{ 0.301f, 0.613f },{ 0.229f, 0.622f },
{ 0.338f, 0.376f },{ 0.168f, 0.604f },
{ 0.131f, 0.461f },{ 0.674f, 0.737f },
{ 0.253f, 0.686f },{ 0.074f, 0.447f },
{ 0.905f, 0.768f },{ 0.218f, 0.297f },
{ 0.186f, 0.642f },{ 0.822f, 0.470f },
{ 0.256f, 0.589f },{ 0.444f, 0.340f },
{ 0.987f, 0.619f },{ 0.040f, 0.208f },
{ 0.538f, 0.110f },{ 0.689f, 0.365f },
{ 0.703f, 0.819f },{ 0.391f, 0.559f },
{ 0.473f, 0.499f },{ 0.754f, 0.200f },
{ 0.084f, 0.617f },{ 0.159f, 0.632f },
{ 0.865f, 0.599f },{ 0.223f, 0.097f },
{ 0.919f, 0.359f },{ 0.496f, 0.089f },
{ 0.141f, 0.638f },{ 0.857f, 0.492f },
{ 0.906f, 0.131f },{ 0.961f, 0.745f },
{ 0.292f, 0.010f },{ 0.167f, 0.456f },
{ 0.257f, 0.872f },{ 0.477f, 0.706f },
{ 0.553f, 0.447f },{ 0.207f, 0.024f },
{ 0.169f, 0.562f },{ 0.286f, 0.089f },
{ 0.300f, 0.865f },{ 0.648f, 0.343f },
{ 0.919f, 0.355f },{ 0.822f, 0.025f },
{ 0.749f, 0.555f },{ 0.480f, 0.198f },
{ 0.258f, 0.569f },{ 0.352f, 0.233f },
{ 0.302f, 0.094f },{ 0.521f, 0.346f },
{ 0.295f, 0.456f },{ 0.169f, 0.611f },
{ 0.507f, 0.357f },{ 0.466f, 0.879f },
{ 0.350f, 0.708f },{ 0.361f, 0.267f },
{ 0.434f, 0.951f },{ 0.399f, 0.489f },
{ 0.611f, 0.401f },{ 0.711f, 0.553f },
{ 0.604f, 0.161f },{ 0.277f, 0.665f },
{ 0.705f, 0.327f },{ 0.250f, 0.979f },
{ 0.561f, 0.547f },{ 0.801f, 0.371f },
{ 0.655f, 0.254f },{ 0.970f, 0.253f },
{ 0.968f, 0.725f },{ 0.598f, 0.907f },
{ 0.001f, 0.064f },{ 0.141f, 0.990f },
{ 0.979f, 0.405f },{ 0.787f, 0.769f },
{ 0.590f, 0.058f },{ 0.545f, 0.617f },
{ 0.506f, 0.344f },{ 0.111f, 0.147f },
{ 0.652f, 0.705f },{ 0.373f, 0.752f },
{ 0.679f, 0.414f },{ 0.408f, 0.255f },
{ 0.168f, 0.986f },{ 0.777f, 0.020f },
{ 0.749f, 0.302f },{ 0.089f, 0.813f },
{ 0.326f, 0.230f },{ 0.040f, 0.474f },
{ 0.431f, 0.139f },{ 0.926f, 0.918f },
{ 0.649f, 0.009f },{ 0.924f, 0.167f },
{ 0.073f, 0.881f },{ 0.518f, 0.569f },
* 이걸로 제 텍스처를 렌더링하면 이렇게 되어야 합니다. 실제로 돌려 보면 배치는 같지만 Vulkan이랑 d3d만 이렇게 나오고 GL은 색이 약간 어둡게 나올 텐데, 이는 텍스처 포맷에 대해 SRGB 공간을 사용하지 않기 때문이며 Vulkan에서 동일한 결과를 보려면 surface에서 format 선택 시 SRGB 대신 UNORM 포맷을 고르게 바꾸면 됩니다. 제 디바이스에서는 경험상 성능의 차이는 없었습니다.
2. depth/stencil 버퍼링은 하지 않습니다.
3. 텍스처는 1종류만 사용합니다.
4. 블렌딩은 src_alpha, 1 - src_alpha로 수행합니다.
5. 수직 동기화를 하지 않고 프레임 시간 측정합니다.
6. view/projection의 세팅은 일반적으로 프레임당 1회이므로 어떤 케이스에도 넣지 않습니
7. 타겟 크기 1366x768로 합니다.
8. Vertex buffer를 직접 변경하지 않는 케이스에 대해서 Vertex buffer는 사용하지 않습니다. 모두 정점 셰이더에서 정의됩니다.
9. 테스트는 외장 그래픽(Nvidia GeForce GTX 1050)으로만 진행했습니다.
Vulkan 테스트
Vulkan의 렌더링은 비동기. 그래서 당연히 정점 버퍼를 업데이트하는 선택지는 없습니다. 여기선 비교 대상은 다음과 같겠네요.
기대되는 좋은 특징 | 특성상 추가되는 CPU 작업 | |
뷰포트를 바꿔 가며 배치 | GPU 측에 접근하는 규모가 줄어들어 성능 향상이 기대됨 | 평행이동과 줌 |
푸시 상수로 배치 | GPU 측에 접근하는 횟수가 적음 모델 행렬로서 꽤 검증됐다고 볼 수 있는 방법 |
없음 (평행이동과 줌은 값 자체가 셰이더로 넘어감) |
동적 유니폼 버퍼로 배치 | 용도만 치면 가장 자유로움 | 없음 (같은 이유) |
맨 아래는 딱 봐도 가장 안 좋을 것 같은데 그냥 가능하니까 해 보겠습니다.
공통 fragment shader는 이렇게 합니다.
#version 450
layout(location = 0) in vec2 tc;
layout(location = 0) out vec4 outColor;
layout(set = 0, binding = 0) uniform sampler2D tex;
void main() {
outColor = texture(tex, tc);
}
spir-v로 바꾸면 이렇게 됩니다.
const uint32_t THIS_SHADER[119]={119734787,65536,851979,20,0,131089,1,393227,1,1280527431,1685353262,808793134,0,196622,0,1,458767,4,4,1852399981,0,9,17,196624,4,7,262215,9,30,0,262215,13,34,0,262215,13,33,0,262215,17,30,0,131091,2,196641,3,2,196630,6,32,262167,7,6,4,262176,8,3,7,262203,8,9,3,589849,10,6,1,0,0,0,1,0,196635,11,10,262176,12,0,11,262203,12,13,0,262167,15,6,2,262176,16,1,15,262203,16,17,1,327734,2,4,0,3,131320,5,262205,11,14,13,262205,15,18,17,327767,7,19,14,18,196670,9,19,65789,65592};
이건 뷰포트 파이프라인용 veretx shader입니다.
#version 450
// #extension GL_KHR_vulkan_glsl: enable
const vec2 pos[6] = {vec2(-1, -1), vec2(-1, 1), vec2(1, -1), vec2(1, 1), vec2(1, -1), vec2(-1, 1)};
layout(location = 0) out vec2 tc;
void main() {
gl_Position = vec4(pos[gl_VertexIndex], 0.0f, 1.0f);
tc = max(pos[gl_VertexIndex], 0);
}
spir-v로 바꾸면 이렇게 됩니다.
const uint32_t THIS_SHADER[267]={119734787,65536,851979,49,0,131089,1,393227,1,1280527431,1685353262,808793134,0,196622,0,1,524303,0,4,1852399981,0,13,27,41,327752,11,0,11,0,327752,11,1,11,1,327752,11,2,11,3,327752,11,3,11,4,196679,11,2,262215,27,11,42,262215,41,30,0,131091,2,196641,3,2,196630,6,32,262167,7,6,4,262165,8,32,0,262187,8,9,1,262172,10,6,9,393246,11,7,6,10,10,262176,12,3,11,262203,12,13,3,262165,14,32,1,262187,14,15,0,262167,16,6,2,262187,8,17,6,262172,18,16,17,262187,6,19,3212836864,327724,16,20,19,19,262187,6,21,1065353216,327724,16,22,19,21,327724,16,23,21,19,327724,16,24,21,21,589868,18,25,20,22,23,24,23,22,262176,26,1,14,262203,26,27,1,262176,29,7,18,262176,31,7,16,262187,6,34,0,262176,38,3,7,262176,40,3,16,262203,40,41,3,327724,16,48,34,34,327734,2,4,0,3,131320,5,262203,29,30,7,262203,29,43,7,262205,14,28,27,196670,30,25,327745,31,32,30,28,262205,16,33,32,327761,6,35,33,0,327761,6,36,33,1,458832,7,37,35,36,34,21,327745,38,39,13,15,196670,39,37,196670,43,25,327745,31,44,43,28,262205,16,45,44,458764,16,47,1,40,45,48,196670,41,47,65789,65592};
렌더링 코드는 다음과 같습니다.
rp2s->start();
rp2s->bind(0, tx);
vec2 size((float)x, (float)y); // 타겟 크기.
for (int i = 0; i < 100; i++) {
vec2 sz = rects[2 * i + 1] * size;
vec2 lt = rects[2 * i] * size;
rp2s->setViewport(sz.x, sz.y, lt.x, lt.y, true);
rp2s->invoke(vb); // 내용물 없는 vertex buffer
}
rp2s->execute();
다음은 푸시 파이프라인용 vertex shader입니다.
#version 450
// #extension GL_KHR_vulkan_glsl: enable
layout(std140, push_constant) uniform ui{
vec2 offset;
vec2 zoom;
};
const vec2 pos[6] = {vec2(0, 0), vec2(0, 1), vec2(1, 0), vec2(1, 1), vec2(1, 0), vec2(0, 1)};
layout(location = 0) out vec2 tc;
void main() {
gl_Position = vec4((pos[gl_VertexIndex] * zoom + offset)*2.0 - vec2(1.0), 0.0f, 1.0f);
tc = pos[gl_VertexIndex];
}
spir-v로 바꾸면 이렇게 됩니다.
const uint32_t THIS_SHADER[324]={119734787,65536,851979,59,0,131089,1,393227,1,1280527431,1685353262,808793134,0,196622,0,1,524303,0,4,1852399981,0,13,27,54,327752,11,0,11,0,327752,11,1,11,1,327752,11,2,11,3,327752,11,3,11,4,196679,11,2,262215,27,11,42,327752,34,0,35,0,327752,34,1,35,8,196679,34,2,262215,54,30,0,131091,2,196641,3,2,196630,6,32,262167,7,6,4,262165,8,32,0,262187,8,9,1,262172,10,6,9,393246,11,7,6,10,10,262176,12,3,11,262203,12,13,3,262165,14,32,1,262187,14,15,0,262167,16,6,2,262187,8,17,6,262172,18,16,17,262187,6,19,0,327724,16,20,19,19,262187,6,21,1065353216,327724,16,22,19,21,327724,16,23,21,19,327724,16,24,21,21,589868,18,25,20,22,23,24,23,22,262176,26,1,14,262203,26,27,1,262176,29,7,18,262176,31,7,16,262174,34,16,16,262176,35,9,34,262203,35,36,9,262187,14,37,1,262176,38,9,16,262187,6,45,1073741824,262176,51,3,7,262176,53,3,16,262203,53,54,3,327734,2,4,0,3,131320,5,262203,29,30,7,262203,29,56,7,262205,14,28,27,196670,30,25,327745,31,32,30,28,262205,16,33,32,327745,38,39,36,37,262205,16,40,39,327745,38,42,36,15,262205,16,43,42,524300,16,44,1,50,33,40,43,327822,16,46,44,45,327811,16,47,46,24,327761,6,48,47,0,327761,6,49,47,1,458832,7,50,48,49,19,21,327745,51,52,13,15,196670,52,50,196670,56,25,327745,31,57,56,28,262205,16,58,57,196670,54,58,65789,65592};
렌더링 코드는 다음과 같습니다.
rp2s->start();
rp2s->bind(0, tx);
for (int i = 0; i < 100; i++) {
vec4 pushed(rects[2 * i].xyyy()); // y가 2개 더 들어간 특별한 의미는 없습니다. 이게 제 엔진의 vec2 클래스를 vec4로 옮기는 가장 빠른 방법
pushed.z = rects[2 * i + 1].x;
pushed.w = rects[2 * i + 1].y;
rp2s->push(pushed.entry, 0, 16);
rp2s->invoke(vb);
}
rp2s->execute();
동적 유니폼 버퍼 파이프라인용 정점 셰이더는 코드가 거의 똑같습니다. 뒤에서 자원에 접근하는 방식이 다를 뿐입니다.
#version 450
// #extension GL_KHR_vulkan_glsl: enable
layout(std140, binding = 1, set=1) uniform UBO{
vec2 offset;
vec2 zoom;
}ui;
const vec2 pos[6] = {vec2(0, 0), vec2(0, 1), vec2(1, 0), vec2(1, 1), vec2(1, 0), vec2(0, 1)};
layout(location = 0) out vec2 tc;
void main() {
gl_Position = vec4((pos[gl_VertexIndex] * ui.zoom + ui.offset)*2.0 - vec2(1.0), 0.0f, 1.0f);
tc = pos[gl_VertexIndex];
}
spir-v로 바꾸면 이렇게 됩니다.
const uint32_t THIS_SHADER[332]={119734787,65536,851979,59,0,131089,1,393227,1,1280527431,1685353262,808793134,0,196622,0,1,524303,0,4,1852399981,0,13,27,54,327752,11,0,11,0,327752,11,1,11,1,327752,11,2,11,3,327752,11,3,11,4,196679,11,2,262215,27,11,42,327752,34,0,35,0,327752,34,1,35,8,196679,34,2,262215,36,34,1,262215,36,33,1,262215,54,30,0,131091,2,196641,3,2,196630,6,32,262167,7,6,4,262165,8,32,0,262187,8,9,1,262172,10,6,9,393246,11,7,6,10,10,262176,12,3,11,262203,12,13,3,262165,14,32,1,262187,14,15,0,262167,16,6,2,262187,8,17,6,262172,18,16,17,262187,6,19,0,327724,16,20,19,19,262187,6,21,1065353216,327724,16,22,19,21,327724,16,23,21,19,327724,16,24,21,21,589868,18,25,20,22,23,24,23,22,262176,26,1,14,262203,26,27,1,262176,29,7,18,262176,31,7,16,262174,34,16,16,262176,35,2,34,262203,35,36,2,262187,14,37,1,262176,38,2,16,262187,6,45,1073741824,262176,51,3,7,262176,53,3,16,262203,53,54,3,327734,2,4,0,3,131320,5,262203,29,30,7,262203,29,56,7,262205,14,28,27,196670,30,25,327745,31,32,30,28,262205,16,33,32,327745,38,39,36,37,262205,16,40,39,327745,38,42,36,15,262205,16,43,42,524300,16,44,1,50,33,40,43,327822,16,46,44,45,327811,16,47,46,24,327761,6,48,47,0,327761,6,49,47,1,458832,7,50,48,49,19,21,327745,51,52,13,15,196670,52,50,196670,56,25,327745,31,57,56,28,262205,16,58,57,196670,54,58,65789,65592};
렌더링 코드는 다음과 같습니다.
rp2s->start();
rp2s->bind(0, tx);
for (int i = 0; i < 100; i++) {
vec4 pushed(rects[2 * i].xyyy());
pushed.z = rects[2 * i + 1].x;
pushed.w = rects[2 * i + 1].y;
ubo->update(pushed.entry, i, 0, 16);
}
for (int i = 0; i < 100; i++){
rp2s->bind(1, ubo, i);
rp2s->invoke(vb);
}
rp2s->execute();
측정 결과는 이렇습니다.
256프레임마다 측정한 평균 fps | 렌더링 타임 비교 (중앙값 기준) | |
렌더패스 시작 (타겟 클리어), present만 반복 (draw 없음) | 2800~3600 | 0 |
뷰포트 기반 배치로 렌더링 | 1498~1502 | 약 1/2823.53 |
푸시 기반 배치로 렌더링 | 1479~1485 | 약 1/2760.41 |
동적 공유 버퍼 기반 배치로 렌더링 | 1498~1502 | 약 1/2823.53 |
뷰포트 및 동적 공유 버퍼 버전이 푸시보다 1~2%정도 빠르게 나왔습니다. 푸시를 잘못 쓰고 있다기에는 해당 함수 안에 있는 유의미한 동작이 vkCmdPushConstant 밖에 없어서.. 푸시 상수를 많이 쓰면 명령 버퍼가 가장 많이 커지기 때문일까요?
참고로 저 100개짜리 draw 루프를 3번 반복시키면 차이가 약 3~4% 정도로 벌어집니다.
제가 제대로 측정했다면 재미있는 결과네요. 차이가 크진 않지만 동적 공유 버퍼가 안 느립니다. 뷰포트는 예상대로 빨랐고요.
그대로 테스트하려면 위에서 클론받아 온 것에서 YERM_PC/yr_game.cpp을 다음으로 대체하고, 해당 파일의 constexpr int LOCATE_TYPE = 2; 를 0, 1, 2중 하나로 세팅합니다. 그리고 YERM_PC/yr_vulkan.cpp에서 "FIFO"가 있는 스왑체인 속성 부분을 "IMMEDIATE"로 수정하면 됩니다. (수직동기화 off) 창 크기는 자유롭게 설정하면 됩니다.
OpenGL 테스트
OpenGL은 4.6 버전을 기준으로 테스트합니다. 제 엔진이 vulkan이랑 인터페이스 맞추려고 셰이더에서는 uniform buffer에 바인딩 넘버로만 접근하기 때문에요. 여기선 기본적으로 동기 draw를 하니, 이에 따라 vertex buffer를 수정하는 선택지가 생깁니다. 다만 이건 6개뿐이긴 해도 GPU가 할 걸 CPU로 끌어오면서 GPU 로컬 메모리를 사용하지 않을 가능성도 생기니 역시 기대는 되지 않습니다. 단, 애초에 픽셀 영역이 정해져 있는 경우부터 스타트한다면 CPU 측 부담은 덜어지겠네요.
기대되는 좋은 특징 | 특성상 추가되는 CPU 작업 | |
뷰포트를 바꿔 가며 배치 | GPU 측에 접근하는 규모가 줄어들어 성능 향상이 기대됨 | 평행이동과 줌 |
정점 값 수정으로 배치 | ?? | 정점 좌표를 계산 |
유니폼 버퍼로 배치 | 용도만 치면 가장 자유로움 | 없음 (평행이동과 줌은 값 자체가 셰이더로 넘어감) |
공통 fragment shader는 이렇게 합니다.
#version 450
layout(location = 0) in vec2 tc;
out vec4 outColor;
layout(binding = 0) uniform sampler2D tex;
void main() {
outColor = texture(tex, tc);
}
컴파일은 런타임에 합니다.
OpenGL은 화면에서 보이는 좌표계에서 +y가 위쪽 방향인 점이 다릅니다. 이는 뷰포트랑 텍스처 좌표도 마찬가지입니다. 우선 이 점은 루프 타임이 아닌 프로그램 시작 시 반영하도록 합니다.
for (int i = 0; i < 100; i++) {
rects[i * 2].y = 1.0f - rects[i * 2].y - rects[i * 2 + 1].y;
}
다음은 뷰포트 파이프라인용 vertex shader입니다.
#version 450
const vec2 pos[6] = { vec2(-1, -1), vec2(-1, 1), vec2(1, -1), vec2(1, 1), vec2(1, -1), vec2(-1, 1) };
layout(location = 0) out vec2 tc;
void main() {
gl_Position = vec4(pos[gl_VertexID], 0.0f, 1.0f);
tc = max(pos[gl_VertexID], 0);
tc.y = 1 - tc.y;
}
전처리를 했기 때문에 렌더링 코드는 Vulkan 때랑 다를 바 없습니다.
rp2s->start();
rp2s->bind(0, tx);
vec2 size((float)x, (float)y); // 타겟 크기.
for (int i = 0; i < 100; i++) {
vec2 sz = rects[2 * i + 1] * size;
vec2 lt = rects[2 * i] * size;
rp2s->setViewport(sz.x, sz.y, lt.x, lt.y, true);
rp2s->invoke(vb); // 내용물 없는 vertex buffer
}
rp2s->execute();
공유 버퍼 파이프라인용 vertex shader도 Vulkan때랑 거의 똑같습니다.
#version 450
const vec2 pos[6] = {vec2(0, 0), vec2(0, 1), vec2(1, 0), vec2(1, 1), vec2(1, 0), vec2(0, 1)};
layout(std140, binding=11) uniform ui{
vec2 offset;
vec2 zoom;
};
layout(location = 0) out vec2 tc;
void main() {
gl_Position = vec4((pos[gl_VertexID] * zoom + offset) * 2.0 - vec2(1.0), 0.0f, 1.0f);
tc = max(pos[gl_VertexID], 0);
tc.y = 1 - tc.y;
}
렌더링 코드는 Vulkan때와 동일합니다.
rp2s->start();
rp2s->bind(0, tx);
for (int i = 0; i < 100; i++) {
vec4 pushed(rects[2 * i].xyyy()); // y가 2개 더 들어간 특별한 의미는 없습니다. 이게 제 엔진의 vec2 클래스를 vec4로 옮기는 가장 빠른 방법
pushed.z = rects[2 * i + 1].x;
pushed.w = rects[2 * i + 1].y;
rp2s->push(pushed.entry, 0, 16); // GL에는 푸시 상수가 없으며, 이 함수는 렌더패스에 무관하게 바인딩 11번 uniform buffer로 연결됨.
rp2s->invoke(vb);
}
rp2s->execute();
마지막으로 정점값 수정 vertex shader는 이렇습니다.
#version 450
layout(location = 0) in vec4 xyuv;
layout(location = 0) out vec2 tc;
void main() {
gl_Position = vec4(xyuv.xy, 0.0f, 1.0f);
tc = xyuv.zw;
}
렌더링 코드는 앞의 것의 vertex shader랑 유사합니다. 위치를 구하는 cpu 측 코드는 연산 횟수상 행렬을 만들어 곱하는 것보다 저게 빠르다고 판단했습니다.
rp2s->start();
rp2s->bind(0, tx);
for (int i = 0; i < 100; i++) {
vec4 verts[6];
const vec2 pos[6] = { vec2(0, 0), vec2(0, 1), vec2(1, 0), vec2(1, 1), vec2(1, 0), vec2(0, 1) };
for (int j = 0; j < 6; j++) {
vec2 transformed = (pos[j] * rects[2 * i + 1] + rects[2 * i]) * 2.0f - vec2(1.0f);
verts[j].x = transformed.x;
verts[j].y = transformed.y;
verts[j].z = pos[j].x;
verts[j].w = 1.0f - pos[j].y;
}
vb->update(verts, 0, sizeof(verts));
rp2s->invoke(vb);
}
rp2s->execute();
측정 결과는 다음과 같습니다.
256프레임마다 측정한 평균 fps | 렌더링 타임 비교 (중앙값 기준) | |
타겟 클리어, present만 반복 (draw 없음) | 4700~4900 | 0 |
뷰포트 기반 배치로 렌더링 | 1039~1046 | 약 1/1331.74 |
공유 버퍼 기반 배치로 렌더링 | 1020~1040 | 약 1/1311.40 |
정점 값 수정 기반 배치로 렌더링 | 690~697 | 약 1/810.62 |
뷰포트 기반이 공유 버퍼 기반에 비해 약 1~2% 빠르게 측정됐고, 정점 값 수정은 척 봐도 엄청 느린데(약 1.6배), cpu 연산 때문은 아닌 것이 공유 버퍼 기반 배치에서 넘길 데이터에 위의 정점 값을 0.0001배만큼 반영하여 동일 연산을 매 프레임 적용하여도 평균 fps에 큰 변화가 없었습니다.
코드 구현부에서 업데이트 가능한 정점 버퍼의 경우 실제 의도를 추론하여 GL_DYNAMIC_DRAW 버퍼를 사용하게 했습니다만 거기를 GL_STREAM_DRAW로 수정해도 별로 안 빨라집니다. (그래픽 장치의 구현상 차이인지 STATIC 해도 거의 똑같네요..)
정말 확연히 느린 것은 제 장치에서 BC7이 안 돼서 그렇습니다. (측정상 BC7이랑 RGBA랑 성능 차이가 상당히 큽니다.)
그대로 테스트하려면 위에서 클론받아 온 것에서 YERM_PC/yr_game.cpp을 다음으로 대체하고, 해당 파일의 constexpr int LOCATE_TYPE = 2; 를 0, 1, 2중 하나로 세팅합니다. 그리고 YERM_PC/yr_opengl.h에서 USE_OPENGL_DEBUG를 false로 만들고 빌드합니다. 창 크기는 자유롭게 설정하면 됩니다. CMAKE 구성 시 opengl로 빌드할 수 있을 것 같은 옵션을 선택하여 체크하고 vulkan 건 해제해야 opengl로 빌드됩니다.
D3D11 테스트
케이스는 OpenGL이랑 똑같습니다.
기대되는 좋은 특징 | 특성상 추가되는 CPU 작업 | |
뷰포트를 바꿔 가며 배치 | GPU 측에 접근하는 규모가 줄어들어 성능 향상이 기대됨 | 평행이동과 줌 |
정점 값 수정으로 배치 | ?? | 정점 좌표를 계산 |
상수 버퍼로 배치 | 용도만 치면 가장 자유로움 | 없음 (평행이동과 줌은 값 자체가 셰이더로 넘어감) |
D3D11은 기본적으로 화면 위쪽이 +y 방향입니다. 텍스처 좌표랑 뷰포트는 좌측 상단이 (0,0)이고요. +y 방향과 관련된 보정이 정점 셰이더에 약간 반영됩니다.
공통으로 사용되는 픽셀 셰이더입니다.
struct PS_INPUT{
float4 pos: SV_POSITION;
float2 tc: TEXCOORD0;
};
Texture2D tex: register(t0);
SamplerState spr: register(s0);
float4 main(PS_INPUT input): SV_TARGET {
return tex.Sample(spr, input.tc);
}
D3D11의 경우 크로스플랫폼 용도는 아니고 이에 따라 그냥 컴파일된 바이너리를 여기 변수 형태로 적는 게 가능하겠지만 아직 그러는 직관적 툴을 안 만든 관계로 런타임 컴파일로 해 놓았습니다.
뷰포트 기반 배치 정점 셰이더입니다.
struct VS_INPUT{
float _: _0_;
uint vid: SV_VertexID;
};
struct PS_INPUT{
float4 pos: SV_POSITION;
float2 tc: TEXCOORD0;
};
static const float2 pos[6] = {float2(-1, -1), float2(-1, 1), float2(1, -1), float2(1, 1), float2(1, -1), float2(-1, 1)};
PS_INPUT main(VS_INPUT input) {
PS_INPUT ret = (PS_INPUT)0;
ret.pos = float4(pos[input.vid], 0.0, 1.0);
ret.tc = max(pos[input.vid],float2(0.0,0.0));
ret.tc.y = 1-ret.tc.y;
return ret;
}
렌더링 코드는 변하지 않습니다.
rp2s->start();
rp2s->bind(0, tx);
vec2 size((float)x, (float)y); // 타겟 크기.
for (int i = 0; i < 100; i++) {
vec2 sz = rects[2 * i + 1] * size;
vec2 lt = rects[2 * i] * size;
rp2s->setViewport(sz.x, sz.y, lt.x, lt.y, true);
rp2s->invoke(vb); // 내용물 없는 vertex buffer
}
rp2s->execute();
상수 버퍼 기반 배치 정점 셰이더입니다.
struct VS_INPUT{
float _: _0_;
uint vid: SV_VertexID;
};
cbuffer _0: register(b13){
float2 offset;
float2 zoom;
};
const static float2 pos[6] = {float2(0, -1), float2(0, 0), float2(1, -1), float2(1, 0), float2(1, -1), float2(0, 0)};
struct PS_INPUT{
float4 pos: SV_POSITION;
float2 tc: TEXCOORD0;
};
PS_INPUT main(VS_INPUT input) {
PS_INPUT ret = (PS_INPUT)0;
ret.pos = float4((pos[input.vid] * zoom + float2(offset.x,-offset.y)) * 2.0 - float2(1.0,-1.0), 0.0, 1.0);
ret.tc = pos[input.vid] + float2(0,1);
ret.tc.y = 1.0 - ret.tc.y;
return ret;
}
정점 값 수정 기반 배치 정점 셰이더입니다.
struct VS_INPUT{
float4 xyuv: _0_;
};
struct PS_INPUT{
float4 pos: SV_POSITION;
float2 tc: TEXCOORD0;
};
PS_INPUT main(VS_INPUT input) {
PS_INPUT ret = (PS_INPUT)0;
ret.pos = float4(input.xyuv.xy, 0, 1);
ret.tc = input.xyuv.zw;
return ret;
}
렌더링 코드는 GL때와, 그리고 상수 버퍼의 동작을 CPU로 가져온 것과 유사합니다.
측정 결과는 다음과 같습니다.
256프레임마다 측정한 평균 fps | 렌더링 타임 비교 (중앙값 기준) | |
타겟 클리어, present만 반복 (draw 없음) | 8000~9500 | 0 |
뷰포트 기반 배치로 렌더링 | 1960~1970 | 1/2534.08 |
공유 버퍼 기반 배치로 렌더링 | 1955~1963 | 1/2524.11 |
정점 값 수정 기반 배치로 렌더링 | 1957~1966 | 1/2528.26 |
D3D11 상당히 빠르네요. 아직까지 현역인 이유가 있어요. 마치 신토불이(?)를 몸소 보여주는 녀석 같습니다. 테스트 프로그램은 구조가 간단해서 이미 vulkan 쪽도 최적화할 거리가 거의 없을 텐데요. 그것보다 무려 30%나 빠르게 나왔습니다.
측정이 맞다면 3가지 방식의 차이는 1% 미만입니다. 여기서는 뷰포트 기반으로 배치하는 게 코드가 쉬운 것(해상도와 무관하게 동일 크기를 유지하기 쉬운..) 말고는 장점이 없다고 봐도 무방하겠습니다.
그대로 테스트하려면 위에서 클론받아 온 것에서 YERM_PC/yr_game.cpp을 다음으로 대체하고, 해당 파일의 constexpr int LOCATE_TYPE = 2; 를 0, 1, 2중 하나로 세팅합니다. 창 크기는 자유롭게 설정하면 됩니다. CMAKE 구성 시 d3d11로 빌드할 수 있을 것 같은 옵션을 선택하여 체크하고 vulkan 건 해제해야 d3d11로 빌드됩니다.
결론
최대한 많은 변인을 통제했을 때 뷰포트 세팅이 자체 오버헤드는 셰이더 리소스 수정보다 적다는 게 미세하게 느껴지는데요, 실제 draw 콜에서 다른 state들(ex: 텍스처 바인딩, 파이프라인 등이 변경되는 경우)이 변하는 걸로 더 많은 시간 비중이 생길 걸 생각하면 실질적으로 영향력은 별로 없겠네요. 기존에 하던 대로 하는 게 더 많은 기능을 한다고 볼 수 있으므로 특별한 이유가 없다면 그쪽을 계속 사용하는 편이 더 좋겠습니다.
그리고 전 가능하면 Vulkan 최적화를 더 해야겠네요.