[SOLVED] OPENGL(ES) with SDL2 and "rendering to texture"

Hi there,

I added SDL2 support to OpenLara, an opensource 3D engine for Tomb Raider 1, 2 and 3 (1 is currently completable!).

The problem is that performance is not exactly stellar on things like the Raspberry Pi.
However, I have an idea for getting rock-solid 60FPS: rendering the GL stuff to the original 320x240 resolution, and then upscaling each frame to physical fullscreen resolution., instead of letting the engine render the GL stuff at full physical screen resolution.
SO, this is what I do:

  1. I create a fullscreen SDL2 window, the corresponding renderer, and a 320x240 texture:

    SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO|SDL_INIT_EVENTS|SDL_INIT_GAMECONTROLLER);

    SDL_GetCurrentDisplayMode(0, &sdl_displaymode);

    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);

    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); 
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); 
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); 

    sdl_window = SDL_CreateWindow(WND_TITLE, 0, 0,
        sdl_displaymode.w, sdl_displaymode.h, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_FULLSCREEN_DESKTOP);

    SDL_GLContext context = SDL_GL_CreateContext(sdl_window);
    SDL_GL_SetSwapInterval(0);

    sdl_renderer = SDL_CreateRenderer(sdl_window, -1,
          SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE);

    sdl_texture = SDL_CreateTexture(sdl_renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 320, 240);
  1. I set the rendering target to the texture, my understanding is that the GL stuff would be rendered to sdl_texture, right?:

    SDL_SetRenderTarget(sdl_renderer, sdl_texture);
  1. I tell the engine to render the GL stuff at 320x240 (it makes glViewport() to 320x240, etc):
    Core::width  = 320;
    Core::height = 240;
  1. I use two rects during rendercopy so the target texture gets rendered to fullscreen physical resolution, and then renderpresent as usual:

    SDL_GetWindowSize(sdl_window, &w, &h);

    src_rect.x = 0;
    src_rect.y = 0;
    src_rect.w = 320;
    src_rect.h = 240;

    dst_rect.x = 0;
    dst_rect.y = 0;
    dst_rect.w = w;
    dst_rect.h = h;

    SDL_RenderCopy(sdl_renderer, sdl_texture, &src_rect, &dst_rect);
    SDL_RenderPresent(sdl_renderer);

But all I get is a black screen. Do you guys know what could be going on here?

Come on, guys! Is it even possible to use SDL_SetRenderTarget() with an OpenGL SDL2 program?

I think mixing SDL_Renderer and OpenGL is not a very good idea (even though SDL2 has some functions to somehow support this)…

As far as I understand it, SDL_SetRenderTarget() sets the render target of the SDL_Renderer, not the render target for generic OpenGL code.

It should be possible to do all this in OpenGL directly (render to OpenGL texture or sth and then render that scaled to screen), see e.g. https://www.opengl-tutorial.org/intermediate-tutorials/tutorial-14-render-to-texture/

Mixing SDL_Renderer and OpenGL can cause problems if one is not carefull.
The problem here probably is that 1) your program uses a different opengl context than the render and 2) that i dont see you actually switching the render target to the screen (SDL_SetRenderTarget(renderer, NULL)).

First of all, use

SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengles2");

This forces the rendere to gles2 mode. Now you dont need the SDL_GL* functions for setup anymore, as the renderer should handle this.

Up next: dont create a opengl context yourself, otherwise you end up with two, as the renderer has its own context that it switches to when needed.
All opengl functions will work on that context however. I dont know if this is a documented feature or just works™, but that is how i run IMGUI on windows, linux and android, so i guess its fine?

Then the flow is

  • switch target to your small texture
  • draw stuff
  • switch target to NULL (so that you draw to the screen (backbuffer))
  • RenderCopy the target texture to the screen
  • call SDL_RenderPreset to actually present the result to the screen.
  • repeat

Thanks a lot for your response, mkalte!
This is what I am currently doing, and it is exactly as you said:

0) To init the GL context, I now do as you say:

SDL_SetHint(SDL_HINT_RENDER_DRIVER, “opengles2”);

sdl_window = SDL_CreateWindow(WND_TITLE, 0, 0, 640, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);

1) I set the target to my small texture:

sdl_renderer = SDL_CreateRenderer(sdl_window, -1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE);

sdl_texture = SDL_CreateTexture(sdl_renderer, SDL_PIXELFORMAT_RGB565,
SDL_TEXTUREACCESS_TARGET, 320, 240);

SDL_SetRenderTarget(sdl_renderer, sdl_texture);

2) After the OpenGL commands complete (there is NOT custom render target setting on that code to my knowledge), I set the target to NULL, and then rendercopy to the texture and then renderpresent:

SDL_SetRenderTarget(sdl_renderer, NULL);
SDL_RenderCopy(sdl_renderer, sdl_texture, &src_rect, &dst_rect);
SDL_RenderPresent(sdl_renderer);

But the resulting image is not scaled full-window: it is shown on a small 320x240 area inside the window (which is 640x480 on my tests).
After this sequence, glGetError() always returns 0, so apparently it is right.

Some notes:
-I am forcing the engine to work at 320x240 by setting:
glViewport(vp.x, vp.y, 320, 240);
glScissor(vp.x, vp.y, 320, 240);

-The SDL_Rects for rendercopy are like set like this:
src_rect.x = 0;
src_rect.y = 0;
src_rect.w = 320;
src_rect.h = 240;

dst_rect.x = 0;
dst_rect.y = 0;
dst_rect.w = 640;
dst_rect.h = 480;

Are you resetting the viewport afterwards? I don’t think, before SDL 2.0.10, calling SetRenderTarget will force a viewport reset (why though? anyway). Try setting viewport and scissorrect back to your window size before calling the renderer functions to see if that helps.

Not helping you, but interestingly enough and correct me if im wrong, it looks like there a few reasons for wich the viewport is reset aside from switching gl contexts before 2.0.10

@mkalte: I have done so, look:

        Game::render();
        Core::waitVBlank();

        glViewport(0, 0, 640, 480);
        glScissor(0, 0, 640, 480);

        SDL_SetRenderTarget(sdl_renderer, NULL);
        SDL_RenderCopy(sdl_renderer, sdl_texture, &src_rect, &dst_rect);
        SDL_RenderPresent(sdl_renderer);

And all I get is some strange upscale parts of the screen on the full window:

Just to be clear - your plan is to render the scene to a SDL_Texture and then render that specific texture to the screen, while down/upscaling it?

If that’s the case, shouldn’t the code then look like this:

SDL_SetRenderTarget(sdl_renderer, sdl_texture);

Game::render();

SDL_SetRenderTarget(sdl_renderer, NULL);
SDL_RenderCopy(sdl_renderer, sdl_texture, &src_rect, &dst_rect);
SDL_RenderPresent(sdl_renderer);

This will render the content inside Game::render(); to the render target texture, which is then rendered to the screen/backbuffer after the render target has been reset back to the default render target.

I have tried exactly that, and I get a black screen…

I created a small test program with a render target texture, which I render some primitives to and the render target texture are then rendered to the default render target. It works for me at least and everything is being rendered as it’s supposed to.
What you might forget to do on your render target texture, when it is set as a render target, is to actually clear it before rendering to it. In the code example below you can see how I render everything.

Code example
void Render(void)
{
	// Clear the render target texture to black
	SDL_SetRenderTarget(pRenderer, pRenderTarget);
	SDL_SetRenderDrawColor(pRenderer, 0, 0, 0, 255);
	SDL_RenderClear(pRenderer);

	// Render a red square to the render target texture
	SDL_SetRenderDrawColor(pRenderer, 255, 0, 0, 255);
	const SDL_Rect Square = {20, 20, 60, 60};
	SDL_RenderFillRect(pRenderer, &Square);

	// Render a diagonal yellow line to the render target texture
	SDL_SetRenderDrawColor(pRenderer, 255, 255, 0, 255);
	SDL_RenderDrawLine(pRenderer, 20, 100, 180, 180);

	// Reset the render target to the default render target and clear it to white
	SDL_SetRenderTarget(pRenderer, nullptr);
	SDL_SetRenderDrawColor(pRenderer, 255, 255, 255, 255);
	SDL_RenderClear(pRenderer);

	const SDL_Rect SrcQuad = { 0,  0, RENDER_TARGET_WIDTH, RENDER_TARGET_HEIGHT};
	const SDL_Rect DstQuad = {50, 50, RENDER_TARGET_WIDTH, RENDER_TARGET_HEIGHT};

	// Render the actual render target texture to the default render target
	SDL_RenderCopy(pRenderer, pRenderTarget, &SrcQuad, &DstQuad);

	SDL_RenderPresent(pRenderer);
}

Result:

@Daniel1985
Thanks a lot for that! I finally understood the issue :slight_smile:
Also, OpenLara author has added scaled rendering to texture himself!