Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Seperate console rendering and window present routines. #18

Open
HexDecimal opened this issue Mar 12, 2019 · 22 comments
Open

Seperate console rendering and window present routines. #18

HexDecimal opened this issue Mar 12, 2019 · 22 comments
Assignees
Milestone

Comments

@HexDecimal
Copy link
Collaborator

libtcod can be possessive of the SDL window sometimes, making it hard to add any kind of drawing routine in the middle of libtcod's console_flush function.

Splitting console_flush into a render_console and display_flip function would make any custom drawing onto SDL much easier.

This would also mean libtcod should be able to return the pointer to its SDL window and renderer, so that the user can access them.

@HexDecimal HexDecimal self-assigned this Mar 12, 2019
@davemoore22
Copy link

I would like to support this enhancement. Currently I have a use-case where I'd like to print some libtcod elements (frame, text etc) on top of the SDL surface (that I have accessed via the callback method). To do this currently I have to work out where the libtcod elements will go in advance, and change the alpha of the SDL surface in those regions to fully transparent so that the callback blit won't overwrite the contents.

Doable, but clunky. Ideally, I suppose, I'd like access to the surface either at any point before flush, or alternatively be able to do SDL-based rendering before as well as after libtcod rendering.

@davemoore22
Copy link

davemoore22 commented Mar 29, 2019

Here's an example of what I'd like to be able do (screenshot is from SNES Wizardry): being able to render the scene via a callback, then render the window via libtcod on top!

Untitled

@HexDecimal
Copy link
Collaborator Author

So far I've recently added alpha values to consoles, but the renderers don't support them yet. Once it does have support you should be able to mimic that screenshot with very careful tileset design. For example, the center banner frame tiles would need to be solid black on the inside and fully transparent on the outside.

@davemoore22
Copy link

Oh yes, I'd anticipate a bit of jiggery-pokery with tile positioning and design, but that functionality would be most appreciated and welcomed!

@davemoore22
Copy link

Small issue with the new method when trying it out in a noddy app (using the latest repository version). Or maybe I'm missing something, but I am getting a weird error with mismatched types when using TCOD_sdl2_render_texture. It seems to be wanting the use off an internal type (TCODConsole) as opposed to the exposed version (TCOD_Console)?

error: cannot convert ‘std::unique_ptr<TCODConsole>::pointer’ {aka ‘TCODConsole*’} to ‘const TCOD_Console*’|
/usr/local/include/libtcod/renderer_sdl2.h|147|note:   initializing argument 2 of ‘TCOD_Error TCOD_sdl2_render_texture(const TCOD_TilesetAtlasSDL2*, const TCOD_Console*, TCOD_Console**, SDL_Texture**)’|

Here's the rest of the (mainly boilerplate) code, where I'm doing standard stuff with an offscreen console that I've done dozens of times before.

// Setup the SDL Window
SDL_Init(SDL_INIT_VIDEO);
SDL_window = SDL_CreateWindow("libtcod + SDL", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_width, window_height, SDL_WINDOW_SHOWN);
SDL_surface = SDL_GetWindowSurface(SDL_window);
SDL_renderer = SDL_GetRenderer(SDL_window);
SDL_SetRenderDrawBlendMode(SDL_renderer, SDL_BLENDMODE_NONE);

// Setup the TCOD Tileset and prepare it to be rendered to the window
const std::filesystem::path font_path = {std::filesystem::current_path() / "monaco.ttf"};
TCOD_Tileset* tileset = TCOD_load_truetype_font_(CSTR(font_path.string()), 16, 16);
TCOD_TilesetAtlasSDL2* tileset_atlas = TCOD_sdl2_atlas_new(SDL_renderer, tileset);

// Create a sample TCOD console
std::unique_ptr<TCODConsole> off_console = std::make_unique<TCODConsole>(17, 3);
off_console->setDefaultBackground(TCODColor::black);
off_console->setDefaultForeground(TCODColor::lightRed);
off_console->printFrame(0, 0, 17, 3, true, TCOD_BKGND_OVERLAY, nullptr);
off_console->setDefaultForeground(TCODColor::white);
off_console->printf(1, 1, "libtcod overlay");

// Now render the TCOD console to SQL
SDL_Texture* libtcod_SDL_texture = SDL_CreateTexture(SDL_renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 17 * 16, 3 * 16);
TCOD_sdl2_render_texture(tileset_atlas, (off_console.get()), nullptr, &libtcod_SDL_texture);
SDL_RenderCopy(SDL_renderer, libtcod_SDL_texture, NULL, NULL);

Apologies if I'm doing something dumb, this functionality of libtcod will be wonderful for me when I get it to work.

@HexDecimal
Copy link
Collaborator Author

In the long term TCODConsole will be replaced by smart pointers to TCOD_Console, but for now the C++ API isn't there yet. Until then use TCODConsole::get_data() to get a TCOD_Console* pointer for these new functions.

off_console.get() should be off_console->get_data().

@davemoore22
Copy link

Works wonderfully, thank you. I'll upload some sample code to github so others can see how it is used (ignore the frame character issue in the image below, that's because I'm using a ttf font for the tileset and not a mapped font graphic)

image

One thing I'd like is for the option to make the console appear transparent. Is there any way for libtcod to optionally take into account the keycolour of the console when doing the rendering into a texture? I could read the pixels of the resultant texture with ReadPixels etc and change it there before rendering but that's a very slow operation.

@HexDecimal
Copy link
Collaborator Author

Libtcod supports alpha rather than key colors. You can set the alpha of the tile background directly. I think most of the C++ drawing and printing functions other than blit will remove transparency, so you'll need to change the alpha last:

// Set the background tile at x,y transparent.
off_console->get_data()->at(y, x).bg.a = 0;
// Set all 'magic pink' background tiles transparent.
static const struct TCOD_ColorRGBA MAGIC_PINK{255, 0, 255, 255};
for (struct TCOD_ConsoleTile& tile : *off_console->get_data()) {
  if (tile.bg == MAGIC_PINK) { tile.bg.a = 0; }
}

TCOD_ColorRGBA is from color.h. TCOD_ConsoleTile and the special C++ methods for TCOD_Console are from console.h, but this example shows the most important parts.

@davemoore22
Copy link

Excellent, thank you!

Especially looking forward to libcod v2 now too! Keep going, its coming on wonderfully!

@belug23
Copy link

belug23 commented Feb 22, 2021

I've been looking into doing something like that too, but I had to do multiple modifications to the sample code of Davemoore22 to make it work.

So to anybody with the same wish as me here's a functional demo to print a console on an SDL window base on the information that Davemoore22 gave a few messages up:

#include "libtcod.hpp"
#include <SDL.h>

int main(int argc, char* argv[]) {

    // Setup the SDL Window
    int window_width = 1080;
    int window_height = 720;
    SDL_Init(SDL_INIT_VIDEO);
    SDL_Window* SDL_window = SDL_CreateWindow("libtcod + SDL", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_width, window_height, SDL_WINDOW_SHOWN);
    SDL_Surface* SDL_surface = SDL_GetWindowSurface(SDL_window);
    SDL_Renderer* SDL_renderer = SDL_GetRenderer(SDL_window);
    SDL_SetRenderDrawBlendMode(SDL_renderer, SDL_BLENDMODE_NONE);

    // Setup the TCOD Tileset and prepare it to be rendered to the window
    TCOD_Tileset* tileset = TCOD_load_truetype_font_("font/monaco.ttf", 16, 16);
    TCOD_TilesetAtlasSDL2* tileset_atlas = TCOD_sdl2_atlas_new(SDL_renderer, tileset);

    // Create a sample TCOD console
    std::unique_ptr<TCODConsole> off_console = std::make_unique<TCODConsole>(17, 3);

    while (1) {  // Game loop.
        off_console->setDefaultBackground(TCODColor::black);
        off_console->setDefaultForeground(TCODColor::lightRed);
        off_console->printFrame(0, 0, 17, 3, true, TCOD_BKGND_OVERLAY, nullptr);
        off_console->setDefaultForeground(TCODColor::white);
        off_console->printf(1, 1, "libtcod overlay");

        SDL_SetRenderDrawColor(SDL_renderer, 255, 255, 255, 255);
        SDL_RenderClear(SDL_renderer);

        // Now render the TCOD console to SQL
        SDL_Texture* libtcod_SDL_texture = SDL_CreateTexture(SDL_renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 17 * 16, 3 * 16);
        TCOD_sdl2_render_texture_setup(tileset_atlas, (off_console->get_data()), nullptr, &libtcod_SDL_texture);
        TCOD_sdl2_render_texture(tileset_atlas, (off_console->get_data()), nullptr, libtcod_SDL_texture);
        SDL_Rect dest = {0, 0, 276, 48};

        SDL_RenderCopy(SDL_renderer, libtcod_SDL_texture, NULL, &dest);
        SDL_RenderPresent(SDL_renderer);
        SDL_Event event;
        SDL_WaitEvent(&event);
        switch (event.type) {
            case SDL_QUIT:
                return 0;  // Exit.
        }
    }
}

Adapt the font name for one that you have or change it for an image font with TCOD_tileset_load.

Thanks to @davemoore22 for the base and @HexDecimal for the extra info to fix part of the code!

And good luck to everyone else on your projects!

@davemoore22
Copy link

@belug23, superb! Nice one!

@HexDecimal
Copy link
Collaborator Author

Here's an incremental update of the above. I've tested this version:

#include <SDL.h>
#include <libtcod.h>

int main(int argc, char* argv[]) {
  // Setup the SDL Window
  const int window_width = 1080;
  const int window_height = 720;
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* sdl_window = SDL_CreateWindow(
      "libtcod + SDL",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      window_width,
      window_height,
      SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
  SDL_Renderer* sdl_renderer =
      SDL_CreateRenderer(sdl_window, -1, SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_TARGETTEXTURE);
  SDL_SetRenderDrawBlendMode(sdl_renderer, SDL_BLENDMODE_NONE);

  // Setup the TCOD Tileset and prepare it to be rendered to the window
  TCOD_Tileset* tileset = TCOD_tileset_load("dejavu16x16_gs_tc.png", 32, 8, 256, TCOD_CHARMAP_TCOD);
  TCOD_TilesetAtlasSDL2* tileset_atlas = TCOD_sdl2_atlas_new(sdl_renderer, tileset);

  // Create a sample TCOD console
  tcod::ConsolePtr off_console = tcod::new_console(17, 3);

  // Used for console to texture rendering.
  TCOD_Console* cache_console = nullptr;
  SDL_Texture* libtcod_sdl_texture = nullptr;

  while (1) {  // Game loop.
    tcod::print_frame(
        *off_console, 0, 0, off_console->w, off_console->h, "", &TCOD_light_red, &TCOD_black, TCOD_BKGND_OVERLAY, true);
    tcod::print(*off_console, 1, 1, "libtcod overlay", &TCOD_white, &TCOD_black, TCOD_BKGND_SET, TCOD_LEFT);

    SDL_SetRenderDrawColor(sdl_renderer, 255, 255, 255, 255);
    SDL_RenderClear(sdl_renderer);

    // Now render the TCOD console to SQL
    TCOD_sdl2_render_texture_setup(tileset_atlas, off_console.get(), &cache_console, &libtcod_sdl_texture);
    TCOD_sdl2_render_texture(tileset_atlas, off_console.get(), cache_console, libtcod_sdl_texture);
    const SDL_Rect dest = {0, 0, 276, 48};

    SDL_RenderCopy(sdl_renderer, libtcod_sdl_texture, NULL, &dest);
    SDL_RenderPresent(sdl_renderer);

    SDL_WaitEvent(nullptr);
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
      switch (event.type) {
        case SDL_QUIT:
          return 0;  // Exit.
      }
    }
  }
}

Better handling of the SDL2 renderer. SDL surface stuff was mostly an SDL1 thing.

This font is from libtcod's font directory, which is easier for me to find.

Prevents a memory leak where a new libtcod_SDL_texture was made every loop. Since TCOD_sdl2_render_texture_setup is being used it can create and update this texture automatically.

TCODConsole::print_frame still uses EASCII so it's unsuitable for the Unicode only TCOD_Tileset objects. I've switched this to the experimental tcod::print and tcod::print_frame functions.

The event loop was modified to exhaust all events before the next loop. Reading only one event per grame loop is a common cause of lag issues.

@belug23
Copy link

belug23 commented Feb 23, 2021

Thanks @HexDecimal for all the improvements! I've seen so many examples with the single event loop on the internet so now I'll keep this as the base loop for my future SDL projects.

What is the use of the cache_console?

@HexDecimal
Copy link
Collaborator Author

cache_console keeps track of which glyphs/colors have been drawn on the target texture. It reduces the number of SDL render calls which improves the performance of TCOD_sdl2_render_texture especially when using larger console sizes with fewer changes.

I'm not sure how well other event loops work in SDL, but I've gotten very familiar with event queue issues from how libtcod's event API accesses SDL events. It's a common problem in some libtcod games. So if you've ever experienced keyboard lag in a libtcod game then it was likely related to this.

@davemoore22
Copy link

@HexDecimal , I'd like permission to upload your example to my own github (appropriately credited of course!), as this is incredibly very useful functionality!

@HexDecimal
Copy link
Collaborator Author

@davemoore22, I just modified belug23's example a little. That said you can consider those edits public domain.

Keep in mind that the experimental functions I mentioned are provisional. Although C++ overloads make it easier for me to not break them later.

@belug23
Copy link

belug23 commented Feb 28, 2021

I've found a problem with the revised version, those 2 lines:

    SDL_Surface* SDL_surface = SDL_GetWindowSurface(window);
    SDL_Renderer* SDL_renderer = SDL_GetRenderer(window);

Where replaced by:

  SDL_Renderer* sdl_renderer =
      SDL_CreateRenderer(sdl_window, -1, SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_TARGETTEXTURE);

I found that not using the window renderer would cause rendering order issues, the console would always be rendered over the other SDL elements you render.
And if you don't get the surface while using the window's renderer you would get ghosting issues.

You don't need to assign the Surface to remove the ghosting so I ended up using it like that:

    SDL_GetWindowSurface(window);
    SDL_Renderer* SDL_renderer = SDL_GetRenderer(window);

The source code of SDL indicates that the framebuffer could be created at that point https://github.com/libsdl-org/SDL/blob/main/src/video/SDL_video.c#L2447

I've tried calling SDL_CreateWindowFramebuffer directly but it's not defined in the SDL2 headers so at this point I cannot test the theory unless compiling my own SDL2 lib and debugging it. For the moment I'll keep this strange line of code documenting why it's there.

@HexDecimal
Copy link
Collaborator Author

https://wiki.libsdl.org/SDL_GetWindowSurface Remarks:

You may not combine this with 3D or the rendering API on this window.

Window surfaces are not compatible with the rendering API which libtcod uses. You'd have to use TCOD_tileset_render_to_surface instead if you insist on using this part of the SDL, but I wouldn't recommend that as it seems to be a holdover from SDL1.

I found that not using the window renderer would cause rendering order issues, the console would always be rendered over the other SDL elements you render.

There's not much I can add unless you show what you did. I've followed the SDL2 documentation rather than looking at the sources. The way I initialized the renderer is pretty standard for SDL2.

Be sure to stick to the 2D Accelerated Rendering API. Surfaces should be converted into textures before you render them to the screen.

@belug23
Copy link

belug23 commented Mar 14, 2021

Ok I've been working a lot on a game using this way of rendering the console and I have an extra tip to solve the alpha issue, it's quite simple, but you need to set the blending mode on the texture before copying it:

...
    const SDL_Rect dest = {0, 0, 276, 48};

    // The missing part for copying the alpha values to the renderer
    SDL_SetTextureBlendMode(libtcod_sdl_texture, SDL_BLENDMODE_BLEND);

    SDL_RenderCopy(sdl_renderer, libtcod_sdl_texture, NULL, &dest);
    SDL_RenderPresent(sdl_renderer);
...

It wasn't complex, but if it helps someone.

P.S. HexDecimal Sorry if I didn't answer your previous comment, I suppose my problem came from the fact that I'm using an OpenGL enabled SDL_Window, that's probably the reason I need to call SDL_GetWindowSurface to initialize the buffer for it. I don't use the surface at all, but it's doing something in the background that allows me to render my textures properly.

@HexDecimal
Copy link
Collaborator Author

I really thought SDL_WINDOW_OPENGL was incompatible with the SDL renderer. The documentation implies that being the case. It still feels like undefined behavior.

I'm not sure what to do about supporting OpenGL. OpenGL's early API is really bad and isn't helpful when trying to extend it to support new features.

@giakki
Copy link

giakki commented Jul 30, 2021

Hi!
Thank you for all the comments in this thread, they have been very helpful in sorting some issues I was having.
I support this enancement, it would be great to have a way to do some post processing to the frame before swapping the window.

Have you considered just exposing context->c_accumulate_? That is what i'm using right now, but it would be great to not have to depend on internal functions.
My particular use case is rendering ImGui frames above libtcod: example.

Let me know if I can help or send a PR!

@HexDecimal
Copy link
Collaborator Author

I have thought of it. The issue with accumulate is that it's sensitive to the window size and the method you use to add a custom overlay to it will break if the renderer isn't what you expected. Libtcod will automatically fallback to using OpenGL1 or SDL2 if the faster API's are not supported by the OS. You'd have to force it to use the SDL2 renderer in most cases where you have a manual overlay.

At that point it's best to use TCOD_sdl2_render_texture which lets you handle the console rendering like any other SDL2 texture. Maybe ImGui is an exception since it might be able to handle all of these edge cases. So I'm not entirely opposed to the idea of making accumulate more visible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants