Tistory View
안드로이드 NDK OpenGLES 초기화 및 Shared Context
What should I do? 2021. 3. 15. 02:31OpenGLES 초기화는 자주하는 작업은 아니지만, 다시 하려고 하면 어떻게 하는지 까먹는 작업이라 이렇게 정리를 해둔다. NDK로 코드가 작성되었으며, 이게 우끼게도 Java는 다른 식으로 작성해야 문제가 없다.
초기화 순서
1. eglInitialize
2. eglBindAPI
3. eglChooseConfig
4. eglCreate(Window 또는 Pbuffer)Surface
5. eglCreateContext
6. eglMakeCurrent
초기화는 다음의 순서로 진행한다. 순서가 바뀔 수도 있지만, 필자가 가진 모든 기기에서 위 순서를 사용할 경우 문제 없이 잘 초기화가 되었다. 그리고, 아래의 설명은 필자가 그냥 경험으로 터득?한 것이라 틀린 부분이 많을 수 있다. 어짜피 초기화만 끝나면 신경 쓸일 없으니, 특별한 상황을 제외하고는 자세히 알 필요도 없다고 생각된다.
1. eglInitialize (EGLDisplay dpy, EGLint *major, EGLint *minor)
display[dpy]는 단순히 다음의 코드로 받아 올 수 있다.
display = eglGetDisplay( EGL_DEFAULT_DISPLAY );
eglGetDisplay에서 x11이나 MS Window의 경우는 다른 작업을 해줘야 할 수도 있는 데, 안드로이드에서는 EGL_DEFAULT_DISPLAY만으로 웬만한 것은 다 처리가 된다. 신경쓸 것이 별로없다.
major와 minor는 EGL의 버전정보를 돌려 받을 포인터를 지정하는 곳이다. 필요없을 경우 null값을 넘기면 된다.
2. eglBindAPI
EGLBoolean EGLAPIENTRY eglBindAPI (EGLenum api)
사용할 api를 지정한다. 우리는 Opengles를 사용할 것이니 EGL_OPENGL_ES_API를 넘겨주면 된다.
3. eglChooseConfig
context와 surface를 만들 때 넘겨줄 config값을 알아낸다.
4. Surface생성 eglCreateWindowSurface or eglCreatePbufferSurface
glDrawArrays나 glDrawElements 등 그리기가 수행될 때 그려질 Surface를 만든다.
eglCreateWindowSurface는 실제 기기의 화면에 그리기위한 것이며, eglCreatePbufferSurface는 비디오메모리에 그리도록 한다. eglCreatePixmapSurface도 있는 데, 이는 일반 CPU용 메모리에 생성할 때 주로 사용하지만 안드로이드는 지원하지 않는 것 같다.
기기에서 지원한다면 Surface를 만들지 않고도 EGLContext를 만들 수 있다. 하지만 호환성을 위해 그냥 PBuffer로 Surface를 만들어서 EGLContext를 만드는 것이 편하다.
5. eglCreateContext
eglBindAPI에서 지정한 [EGL_OPENGL_ES_API]의 렌더리 Context를 만든다. 이제 연결된 Surface에 그리기위해서는 이 Context를 이용하게 된다. 아직 Context와 Surface는 연결된 것은 아니다.
6. eglMakeCurrent
EGLContext와 그릴 Surface를 서로 연결한다.
eglMakeCurrent (EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx)
위에서 생성한 값을 넘겨주면 된다.
정리작업
초기화작업을 했으면 이제 없애버리는 작업도 해보자
초기화 작업이 끝났으니 이제 gl***명령을 통해서 작업을 하면 되지만, Native에서 작업을 하기에 메모리 유출을 막기위해 정리하는 코드를 먼저 만들어 보자. 당연한 이야기지만 초기화 작업의 역순으로 하면된다.
1. eglMakeCurrent로 surface와의 연결을 끊어 버린다.
eglMakeCurrent( display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT )
2. eglDestroyContext(display, context)로 EGLContext를 날려 버리고(<--> eglCreateContext)
3. eglDestroySurface( display, surface )로 Surface를 없애고(<--> eglCreate***Surface )
4. eglReleaseThread()로 API를 미지정으로 한다.(<-->eglBindAPI)
5. eglTerminate(display)로 egl도 정리한다.(<-->eglnitialize)
eglDestroyContext를 호출하면 이에 귀속되어있는 vbo,texture등등 모든 것이 알아서 사라진다. 즉, eglDestroyContext이후에는 glDeleteTexture같은 것을 호출해서도 안되며, 호출할 필요도 없다.
초기화작업에 추가내용
지면(?)을 어떻게 할당해야 될지 몰라 eglChooseConfig와 eglCreate***Surface의 좀 더 자세한 사항을 정리해 본다.
eglChooseConfig
EGLBoolean eglChooseConfig
( EGLDisplay dpy,
const EGLint *attrib_list,
EGLConfig *configs,
EGLint config_size,
EGLint *num_config)
eglChooseConfig는 "attrib_list에 설정한데로 config를 만들어라"라는 것이 아니고 "내가 attrib_list에 설정한 값을 쓰고 싶은 데, 거기에 상응하는 config값을 주세요"라는 개념이다. 기기는 지원하는 config들이 정해져 있고, 요청한 attrib_list에 알맞을 것을 골라서 configs에 값을 넣어주게 된다. 보통 안드로이드에서는 다음과 같이 attrib_list값을 지정한다.
EGLint attribsConfig[] = {
EGL_SURFACE_TYPE, EGL_WINDOW_BIT, // PBufffer를 쓸 경우 EGL_PBUFFER_BIT
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, // or 3.x를 사용하려면 EGL_OPENGL_ES3_BIT
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_RED_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_DEPTH_SIZE, 16,
// EGL_STENCIL_SIZE, 8,
EGL_NONE
}
마지막에 EGL_NONE으로 "여기가 끝"표시를 해줘야 한다.
Window Surface 만들기
EGLint format;
eglGetConfigAttrib( display, config, EGL_NATIVE_VISUAL_ID, &format );
ANativeWindow_setBuffersGeometry( window, 0, 0, format );
surface = eglCreateWindowSurface( display, config, window, NULL );
Window Surface를 만드는 코드는 위와 같고 사실상 설명도 필요없다. Native-window의 포맷을 config와 동일하게 바꿔주는 작업이 사실상 전부다.
window값을 받아오려면 SurfaceView계열을 사용하면 다음과 같이 작업하면 된다.
// Java에서
1. surfaceWindow계열, holder쓰는 것들
Surface surface = getHolder().getSurface();
2. TextureView(테스트 안함)
SurfaceTexture st = [TextureView].getSurfaceTexture();
Surface surface = new Surface(st);
// surface변수를 Native로 넘김
// Native에서
ANativeWindow* window = ANativeWindow_fromSurface( env, surface );
window를 다 사용한 경우 ANativeWindow_release로 surface를 해제해 줘야 메모리 유출이 일어나지 않는다.
ANativeWindow_fromSurface가 null를 리턴하는 경우가 있는 데, 우끼게도 Java로만 이 작업을 하면 문제가 없지만 Native로 넘기면 문제가 발생한다(특히 다른 쓰레드로 넘길 때). 실제로 제대로된 window값을 받으려면 surfaceView의 경우 SurfaceHolder.Callback인 surfaceCreated()함수가 호출된 이후에 surface값을 Native로 넘겨줘야 한다.
여담으로 Activity에서 getWindow().takeSurface( SurfaceHolder.Callback2 )를 이용하면
view를 만들 필요도 없이 window의 Surface에 바로 그릴 수도 있다. NativeActivity가
이 방식으로 구현되어 있다.
PBuffer Surface만들기
이 작업은 쉽다.
int surfaceAttribs[] = {
EGL_WIDTH, 1,
EGL_HEIGHT, 1,
EGL_NONE
};
surface = eglCreatePbufferSurface( display, config, surfaceAttribs );
surfaceAttribs에 필요한 크기를 지정하고 만들면 끝이다. 실제 Draw를 할일이 거의 없기에 단순히 1x1의 Surface를 만드는 걸로 충분하다.
구글링을 하다보면 surfaceAttribs에 다른 속성들이 들어가는 경우가 있는 데, 필자의 경우 추가된 다른 속성으로 인해 동작을 하지않는 디바이스 많아 위의 코드만을 사용한다.
초기화 코드
이제 만드는 법을 익혔으니 몽땅 합쳐 보자
struct MYGLES2_CONTEXT {
EGLint eglMajorVer;
EGLint eglMinorVer;
EGLDisplay display;
EGLContext context;
EGLSurface surface;
int width;
int height;
int clientVersion;
};
bool GL_CreateContext( MYGLES2_CONTEXT* pContext, EGLContext sharedContext, ANativeWindow* window, EGLint clientVersion, EGLint red, EGLint green, EGLint blue, EGLint alpha, EGLint depth, EGLint stencil )
{
assert( clientVersion == 3 || clientVersion == 2 );
bool bCleanInitial = false;
bool bCleanRelease = false;
bool bCleanSurface = false;
bool bCleanContext = false;
bool bCleanCurrent = false;
EGLint eglMajorVersion;
EGLint eglMinorVersion;
EGLDisplay display = EGL_NO_DISPLAY; // eglTerminate
EGLSurface surface = EGL_NO_SURFACE; //
EGLContext context = EGL_NO_CONTEXT;
EGLConfig config;
EGLint numConfigs;
EGLint w, h;
int clientVersionQ = 0;
EGLint attribsConfig[32];
const int attribsContext[4] = {
EGL_CONTEXT_CLIENT_VERSION, clientVersion,
EGL_NONE
};
{
int idx = 0;
attribsConfig[idx++] = EGL_SURFACE_TYPE;
attribsConfig[idx++] = window ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT;
attribsConfig[idx++] = EGL_RENDERABLE_TYPE;
attribsConfig[idx++] = clientVersion == 3 ? EGL_OPENGL_ES3_BIT : EGL_OPENGL_ES2_BIT;
attribsConfig[idx++] = EGL_BLUE_SIZE;
attribsConfig[idx++] = blue;
attribsConfig[idx++] = EGL_GREEN_SIZE;
attribsConfig[idx++] = green;
attribsConfig[idx++] = EGL_RED_SIZE;
attribsConfig[idx++] = red;
attribsConfig[idx++] = EGL_ALPHA_SIZE;
attribsConfig[idx++] = alpha;
attribsConfig[idx++] = EGL_DEPTH_SIZE;
attribsConfig[idx++] = depth;
attribsConfig[idx++] = EGL_STENCIL_SIZE;
attribsConfig[idx++] = stencil;
attribsConfig[idx++] = EGL_NONE;
}
// initialize OpenGL ES and EGL
display = eglGetDisplay( EGL_DEFAULT_DISPLAY );
if( display == EGL_NO_DISPLAY ) {
LOGE( TAG, "In CreateContext(...) CreateContext() eglGetDisplay failure" );
return false;
}
LOGD( TAG, "In CreateContext(...) eglGetDisplay(..) display=%p", (void*)display );
if( !eglInitialize( display, &eglMajorVersion, &eglMinorVersion ) ) {
LOGE( TAG, "CreateContext() eglInitialize failure" );
goto fail;
}
bCleanInitial = true;
LOGD( TAG, "In CreateContext(...) eglInitialize(...) success version major=%d minor=%d", eglMajorVersion, eglMinorVersion );
if( !eglBindAPI( EGL_OPENGL_ES_API ) ) {
LOGE( TAG, "CreateContext() eglBindAPI failure" );
goto fail;
}
bCleanRelease = true;
LOGD( TAG, "eglBindAPI() success" );
if( !eglChooseConfig( display, attribsConfig, &config, 1, &numConfigs ) ) {
LOGE( TAG, "CreateContext() eglChooseConfig failure" );
goto fail;
}
LOGD( TAG, "eglChooseConfig() success" );
// EGL_NATIVE_VISUAL_ID is an attribute of the EGLConfig that is
// guaranteed to be accepted by ANativeWindow_setBuffersGeometry().
// We can use it to make the ANativeWindow buffers to match.
if( window ) {
EGLint format;
eglGetConfigAttrib( display, config, EGL_NATIVE_VISUAL_ID, &format );
// Set a native Android window to have the format configured by EGL
ANativeWindow_setBuffersGeometry( window, 0, 0, format );
// Create EGL surface and context
surface = eglCreateWindowSurface( display, config, window, NULL );
if( surface == nullptr ) {
LOGD( TAG, "In CreateContext(...) eglCreateWindowSurface() failure" );
goto fail;
}
LOGD( TAG, "In CreateContext(...) eglCreateWindowSurface() success surface=%p", (void*)surface );
} else {
int surfaceAttribs[] = {
EGL_WIDTH, 1,
EGL_HEIGHT, 1,
EGL_NONE
};
surface = eglCreatePbufferSurface( display, config, surfaceAttribs );
if( surface == nullptr ) {
LOGD( TAG, "In CreateContext(...) eglCreatePbufferSurface() failure" );
goto fail;
}
LOGD( TAG, "In CreateContext(...) eglCreatePbufferSurface() success surface=%p", (void*)surface );
}
assert( surface );
bCleanSurface = true;
context = eglCreateContext( display, config, sharedContext, attribsContext );
if( !context ) {
LOGE( TAG, "CreateContext() eglCreateContext failure" );
goto fail;
}
bCleanContext = true;
LOGD( TAG, "In CreateContext(...) eglCreateContext() success context=%p", (void*)context );
// Use the surface and context we just created and configure the engine
if( !eglMakeCurrent( display, surface, surface, context ) )
{
// TODO
LOGE( TAG, "In CreateContext(...) CreateContext() eglMakeCurrent failure" );
goto fail;
}
bCleanCurrent = true;
// Get width and height of the surface
eglQuerySurface( display, surface, EGL_WIDTH, &w );
eglQuerySurface( display, surface, EGL_HEIGHT, &h );
assert( w > 0 && h > 0 );
eglQueryContext( display, context, EGL_CONTEXT_CLIENT_VERSION, &clientVersionQ );
if( clientVersionQ != clientVersion ) {
LOGE( TAG, "In CreateContext(...) eglQueryContext() client version mismatch request=%d returns=%d", clientVersion, clientVersionQ );
goto fail;
}
// Store the app variables so the callbacks can access the data
pContext->eglMajorVer = eglMajorVersion;
pContext->eglMinorVer = eglMinorVersion;
pContext->display = display;
pContext->context = context;
pContext->surface = surface;
pContext->width = w;
pContext->height = h;
pContext->clientVersion = clientVersionQ;
LOGD( TAG, "In CreateContext(...) eglVersion=%d.%d shared=%s surface=(%s %dx%d) new [display=%p context=%p surface=%p version=%d]",
eglMajorVersion, eglMinorVersion,
sharedContext ? "Y" : "N",
window ? "window" : "pbuffer",
w, h,
display, context, surface, clientVersion
);
return true;
fail:
if( bCleanCurrent ) {
assert( display );
eglMakeCurrent( display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT );
}
if( bCleanContext ) {
assert( display && context );
eglDestroyContext(display, context);
}
if( bCleanSurface ) {
assert( display && surface );
eglDestroySurface( display, surface );
}
if( bCleanRelease ) {
eglReleaseThread();
}
if( bCleanInitial ) {
assert( display );
eglTerminate( display );
}
return false;
}
void GL_DestroyCurrentThreadContextAll( MYGLES2_CONTEXT* gles2Context ) {
LOGD( TAG, "DestroyCurrentThreadContextAll Terminating display=%p context=%p surface=%p",
gles2Context->display,
gles2Context->context,
gles2Context->surface
);
if( gles2Context->display ) {
eglMakeCurrent(gles2Context->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if( gles2Context->context ) {
eglDestroyContext(gles2Context->display, gles2Context->context);
}
if( gles2Context->surface ) {
eglDestroySurface(gles2Context->display, gles2Context->surface);
}
eglReleaseThread();
eglTerminate(gles2Context->display);
}
gles2Context->display = EGL_NO_DISPLAY;
gles2Context->context = EGL_NO_CONTEXT;
gles2Context->surface = EGL_NO_SURFACE;
gles2Context->width = 0;
gles2Context->height = 0;
}
bool GL_CreateContext( MYGLES2_CONTEXT* pContext, EGLContext sharedContext, ANativeWindow* window, EGLint clientVersion, EGLint red, EGLint green, EGLint blue, EGLint alpha, EGLint depth, EGLint stencil );
GL_CreateContext(...)함수는현재 Thread에 GL Context를 생성한다.
MYGLES2_CONTEXT* pContext
올바르게 GL Conext가 생성될 경우 필요한 정보를 모아두는 곳이다.
EGLContext sharedContext
공유할 Context를 지정, 아래의 Context 공유하기 참조
ANativeWindow* window
window를 지정한다. 위에 Window Surface만들기 참조
이 값을 NULL로 지정할 경우, 이 함수는 여느상황에서나 동작하도록 알아서 1x1의 PBuffer Surface(Off-screen)를 생성한다.
EGLint clientVersion
OpenGLES 2.0을 사용할지 3.x를 사용할지를 지정한다. 2[2.0] 또는 3[3.x] 을 넣어주면 된다.
EGLint red, EGLint green, EGLint blue, EGLint alpha, EGLint depth, EGLint stencil
각각의 비트값을 넣어주면 된다.
요즘 장치들은 사실상 OpenGLES 3.x를 기본적으로 지원하기 때문에 clientVersion를 3으로 넣으면 거의 문제 없이 사용할 수 있다. 하지만 오래된 기기를 지원하고 싶은 경우, 이럴 경우가 없다고는 할 수 없으니, 다음과 같이 [3]을 먼저 시도한 후 실패할 경우 2로 다시 시도하면 된다.
bool CreateContextAll( MYGLES2_CONTEXT* pContext, EGLContext sharedContext, ANativeWindow* window, EGLint red, EGLint green, EGLint blue, EGLint alpha, EGLint depth, EGLint stencil ) {
EGLint clientVersion;
for( clientVersion = 3 ; clientVersion >= 2 ; clientVersion-- ) {
if( CGles2::CreateContext( pContext, sharedContext, window, clientVersion, red, green, blue, alpha, depth, stencil ) ) {
return true;
}
}
return false;
}
실제 어떤 버전이 생성되었는 지는 MYGLES2_CONTEXT의 clientVersion 멤버를 확인해보면 알 수 있다.
2.0은 3.x의 하위 호환이 되기 때문에, 3.x로 생성되어도 2.0방식은 그대로 사용할 수 있다.
사용이 다 끝났으면 다시 MYGLES2_CONTEXT변수를 GL_DESGL_DestroyCurrentThreadContextAll 함수로 넘겨주면 된다.
다른 방식으로 OpenGLES 버전 확인하기
혹시나 해서 미리 3.x버전을 지원하는 지 확인하려면
kitkat버전 이후에는 3.0을 지원하도록 되어있다. 물론 kitkat버전이라고 해도 3.0을 지원하지
않는 경우가 있지만, 그런 경우는 거의 없다고 한다면, 다음의 코드로 기기버전을 확인 할수 있다.
if( android_get_device_api_level() >= 19 ) {
// 3.0이상 지원
} else {
// 3.x지원안함
}
api값이 21[Lollipop]이상이면 3.1이상을 지원하도록 google은 권고하고 있다.
GLContext가 생성된 이후에는
const char *version = (char *) glGetString(GL_VERSION);
함수를 통해 버전을 알 수 있다. 문자열을 파싱해야하지만 보통 다음과 같이 체크할 수 있다.
if (strstr(version, "OpenGL ES-CM 1.1")) {
//OpenGLESVersion_1_1;
} else if (strstr(version, "OpenGL ES 2.")) {
//OpenGLESVersion_2_0;
} else if (strstr(version, "OpenGL ES 3.")) {
//OpenGLESVersion_3_0;
}
Context 공유하기
context는 쓰레드마다 따로 존재한다. 다른 말로 Thread A에서 Context를 만들고 Thread B에도 Context를 만든 다음, Thread A에서 texture를 생성한다고 해도, 이 Texture를 ThreadB에서는 사용 할 수 없다. 쓰레드간에 이 GL-Object(texture, vbo 등등)들을 같이 공유해서 쓰려면 Thread B를 생성할 때 Thread A의 Context를 넘겨줘야 한다.
eglCreateContext( display, config, sharedContext, attribsContext )
3번째 파라미터인 sharedContext에 Thread A에서 생성된 Context값을 넘겨주면, GL-Object를 공유해서 사용할 수 있다.
위의 필자가 만든 GL_CreateContext(..)함수에 sharedContext가 이 역할을 한다.
Thread A
MYGLES2_CONTEXT contextA;
CreateContextAll( &contextA, nullptr, window, 8, 8, 8, 8, 16, 0 );
Thread B
MYGLES2_CONTEXT contextB;
CreateContextAll( &contextB, contextA.context, nullptr, 8, 8, 8, 8, 16, 0 );
Thread A는 화면출력을 위해 window값을 넣었다.
Thread B는 공유할 Context인 Thread A의 context인 contextA.context를 넘겨 주었고, Offscreen 버퍼를 만들기위해 window값을 NULL로 넘겨 주었다.
이제 Thread A든 B든 GL-object을을 생성하면 서로 공유해서 사용할 수가 있다.
Context를 공유해도 GL명령어 큐(Queue)는 따로 생성이 된다. 다른 말로 glFlush를 Thread B에서 호출해도 Thread A의 GL명령어 큐(Queue)가 flush되지 않는다.
다음에 쓸 글은 이런 Shared Context를 사용할 경우, Thread를 서로 Sync. 시켜야 하는 데, 그 내용 다음의 링크에 있다.
아.... 정말 글하나 쓰기 어렵다. 며칠 걸린 거냐..ㅠㅠ
'Android Develop > OpenGLES' 카테고리의 다른 글
OpenGL ES : (Texture or FBO) to (Texture or FBO) 복사 정리 (0) | 2021.06.21 |
---|---|
Android OpenGL ES : 올바른 Framebuffer 사용법 (2) | 2021.06.21 |
Android OpenGLES FenceSync 이해하기 (1) | 2021.03.16 |
기본 사용법 : GLSurfaceView (2) | 2020.07.19 |
YUV shader (0) | 2017.10.30 |
- Total
- Today
- Yesterday
- 에어컨
- 적금
- 전기세
- 컴퓨트셰이더
- 티스토리
- 재테크
- 전기요금
- 안드로이드
- 금리
- 아끼는 법
- 사용료
- 애드센스
- OpenGLes
- 전기료
- OpenGL ES
- 에어콘
- 예금
- 컴퓨트쉐이더
- 애드핏
- 공유 컨텍스트
- gpgpu
- 재태크
- Android
- TTS
- choreographer
- 블로그
- ComputeShader
- 텍스처
- texture
- 경제보복
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |