libtcod / python-tcod

A high-performance Python port of libtcod. Includes the libtcodpy module for backwards compatibility with older projects.
BSD 2-Clause "Simplified" License
410 stars 36 forks source link

Option for keeping window aspect ratio #76

Closed Ape closed 4 years ago

Ape commented 5 years ago

Window resizing works nicely with the SDL2 renderer. However, I would like to use letterboxing instead of the current window scaling in order to keep the correct aspect ratio. Is that possible?

HexDecimal commented 5 years ago

Not right now, but this is something that was planned with the newer renderers. I'll try to make it a priority.

Ape commented 5 years ago

Also, I would like to have a scaling mode that does not upscale the tiles at all, but instead centers the picture and adds letterboxing in every direction. This could be used for example with grid size 120x67 with a 16x16 font resulting in 1920x1072 usable area (8 px vertical letterbox on a 1920x1080 monitor). Integer scaling could be allowed because it doesn't negatively affect the looks.

HexDecimal commented 5 years ago

I've already setup a libtcod function that accepts a "viewport" so any rectangle you can make yourself is an option.

It bypasses console_flush so you need to clear and present SDL's renderer manually using python-tcod's FFI functions for SDL2. I'll have an example that shows how this works.

HexDecimal commented 5 years ago

There's now an example that works with the newest release of python-tcod: https://github.com/libtcod/python-tcod/blob/master/examples/experimental/custrender.py

Nothing was added to python-tcod itself yet, but this shows how to have a fixed aspect ratio or integer scaling.

Ape commented 5 years ago

Thanks for implementing this. I tried to run the example and it works just like I wanted. However, I needed to add something like this, or it would just segfault:

tcod.console_set_custom_font(
    "data/fonts/consolas10x10_gs_tc.png",
    tcod.FONT_TYPE_GREYSCALE | tcod.FONT_LAYOUT_TCOD
)

Do you have plans for adding a high level option for integer scaling (or other scaling modes)? It could be for example a parameter for tcod.console_init_root.

HexDecimal commented 5 years ago

However, I needed to add something like this, or it would just segfault.

What's your platform? I'll look into this.

Do you have plans for adding a high level option for integer scaling (or other scaling modes)? It could be for example a parameter for tcod.console_init_root.

In general I'm trying to move away from hidden global values. These kind of options won't be baked into tcod.console_init_root, if they were it'd be less useful than what's already in the example which allows for many things beyond aspect correction.

tcod.console_init_root and the concept of a root console was also the reason it's so hard to change the size of the console in reaction to the window being resized, something you can do easily with this new code.

Ape commented 5 years ago

However, I needed to add something like this, or it would just segfault.

What's your platform? I'll look into this.

I'm on Arch Linux with tcod 10.0.4 installed from pip. I have Python 3.7.3.

EDIT: I tried to debug this thing, but gdb doesn't want to work with Python apparently. Strace tells me the last thing the program did before segfault was

openat(AT_FDCWD, "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", O_RDONLY) = -1 ENOENT (No such file or directory)

So it's probably related to that. I have that font in path /usr/share/fonts/TTF/DejaVuSansMono.ttf instead of the one it tried to open.

Ape commented 5 years ago

Since this bypasses tcod.console_flush it also disables any vsync or frame rate limits. While it's nice to see that my game runs at 2271 FPS, I would prefer to run it at 60 FPS. Is there a way to do this with this new rendering?

HexDecimal commented 5 years ago

Vsync is something I should add to tcod.console_init_root since there doesn't seem to be a way to toggle it once SDL2's window is opened.

Until then you might be able to implement frame limiting in Python.

Ape commented 5 years ago

I installed another version of gdb and managed to get the full stack trace:

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff71c893d in stbtt__find_table (tag=<optimized out>, fontstart=<optimized out>, data=<optimized out>) at libtcod/src/vendor/stb_truetype.h:1291
1291    libtcod/src/vendor/stb_truetype.h: No such file or directory.
(gdb) bt
#0  0x00007ffff71c893d in stbtt__find_table (tag=<optimized out>, fontstart=<optimized out>, data=<optimized out>) at libtcod/src/vendor/stb_truetype.h:1291
#1  stbtt.find_table.lto_priv () at libtcod/src/vendor/stb_truetype.h:2583
#2  0x00007ffff71c3c50 in stbtt_InitFont_internal (fontstart=0, data=0x7fffffffd860 "", info=0x7fffffffd878) at libtcod/src/vendor/stb_truetype.h:1354
#3  stbtt_InitFont (info=0x7fffffffd878, data=0x7fffffffd860 "", offset=0) at libtcod/src/vendor/stb_truetype.h:4777
#4  0x00007ffff718062d in _ZN4tcod7tileset12TTFontLoaderC2ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEii (tile_height=<optimized out>, 
    tile_width=<optimized out>, path=..., this=<optimized out>, this=<optimized out>, path=..., tile_width=<optimized out>, tile_height=<optimized out>)
    at /usr/include/c++/8.3.0/bits/basic_string.h:2302
#5  _ZN4tcod7tileset13load_truetypeERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKSt5arrayIiLm2EE (path=..., tile_size=...)
    at libtcod/src/libtcod/tileset/truetype.cpp:203
#6  0x00007ffff7182616 in _ZN4tcod7tileset20new_fallback_tilesetERKSt5arrayIiLm2EE (tile_size=...) at /usr/include/c++/8.3.0/bits/char_traits.h:352
#7  0x00007ffff718bb92 in init_display<tcod::sdl2::SDL2Display> (fullscreen=0, title="custrender.py", h=4, w=20) at /usr/include/c++/8.3.0/ext/atomicity.h:69
#8  _ZN4tcod7console9init_rootEiiRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEb15TCOD_renderer_t (w=<optimized out>, h=<optimized out>, title=..., 
    fullscreen=<optimized out>, renderer=<optimized out>) at libtcod/src/libtcod/engine/display.cpp:90
#9  0x00007ffff718c315 in TCOD_console_init_root (w=20, h=4, title=<optimized out>, fullscreen=false, renderer=TCOD_RENDERER_SDL2)
    at /usr/include/c++/8.3.0/bits/basic_string.tcc:206
#10 0x00007ffff71f9f3e in _cffi_f_TCOD_console_init_root (self=<optimized out>, args=<optimized out>) at build/temp.linux-x86_64-3.7/tcod._libtcod.c:33106
#11 0x00007ffff7b83e68 in _PyMethodDef_RawFastCallKeywords () from /usr/lib/libpython3.7m.so.1.0
#12 0x00007ffff7b84101 in _PyCFunction_FastCallKeywords () from /usr/lib/libpython3.7m.so.1.0
#13 0x00007ffff7bf4d19 in _PyEval_EvalFrameDefault () from /usr/lib/libpython3.7m.so.1.0
#14 0x00007ffff7b3cd09 in _PyEval_EvalCodeWithName () from /usr/lib/libpython3.7m.so.1.0
#15 0x00007ffff7b83882 in _PyFunction_FastCallKeywords () from /usr/lib/libpython3.7m.so.1.0
#16 0x00007ffff7bf0f9c in _PyEval_EvalFrameDefault () from /usr/lib/libpython3.7m.so.1.0
#17 0x00007ffff7b836db in _PyFunction_FastCallKeywords () from /usr/lib/libpython3.7m.so.1.0
#18 0x00007ffff7bf022d in _PyEval_EvalFrameDefault () from /usr/lib/libpython3.7m.so.1.0
#19 0x00007ffff7b3cd09 in _PyEval_EvalCodeWithName () from /usr/lib/libpython3.7m.so.1.0
#20 0x00007ffff7b3dc64 in PyEval_EvalCodeEx () from /usr/lib/libpython3.7m.so.1.0
#21 0x00007ffff7b3dc8c in PyEval_EvalCode () from /usr/lib/libpython3.7m.so.1.0
#22 0x00007ffff7c66694 in ?? () from /usr/lib/libpython3.7m.so.1.0
#23 0x00007ffff7c67b6e in PyRun_FileExFlags () from /usr/lib/libpython3.7m.so.1.0
#24 0x00007ffff7c6b035 in PyRun_SimpleFileExFlags () from /usr/lib/libpython3.7m.so.1.0
#25 0x00007ffff7c6d2a7 in ?? () from /usr/lib/libpython3.7m.so.1.0
#26 0x00007ffff7c6d4ec in _Py_UnixMain () from /usr/lib/libpython3.7m.so.1.0
#27 0x00007ffff7dcace3 in __libc_start_main () from /usr/lib/libc.so.6
#28 0x000055555555505e in _start ()
Ape commented 5 years ago

Okay, you just cannot assume font paths like this: https://github.com/libtcod/libtcod/blob/b2e1a7f1b496e9eeca16a51f74a4aea3b68441ec/src/libtcod/tileset/fallback.cpp#L52

HexDecimal commented 5 years ago

The fallback fonts will be for prototype projects and not for releases, they should work fine on Windows and maybe even MacOS, but there's no standard for Linux. So if you have a suggestion for that then I'm listening.

This is clearly my C++ inexperience. I suspect the problem is actually this line: https://github.com/libtcod/libtcod/blob/b2e1a7f1b496e9eeca16a51f74a4aea3b68441ec/src/libtcod/tileset/truetype.cpp#L53 Which should really be throwing something instead of returning. The zero width data gets sent to the font loader which segfaults.

Ape commented 5 years ago

Looks like you are correct about the cause of the segfault. Throwing with a proper message would certainly be more helpful than segfaulting.

I think the standard thing to do on Linux is something like fc-match --format=%{file} monospace which returns a path to the default monospace font. There is also the equivalent C function FcFontMatch.

Ape commented 5 years ago

There's one more issue with this rendering example. If you add any console drawing functions that modify the background color it will leak to the background area around the viewport. For example:

        ...
        while True:
            color = [random.randrange(255) for x in range(3)]
            console.print(0, 0, "X", bg=color)

            # Clear background with white.
            clear((255, 255, 255))
            ...

bg_leaking

HexDecimal commented 5 years ago

I think the standard thing to do on Linux is something like fc-match --format=%{file} monospace

Okay, I've added this and it should be tested.

If you add any console drawing functions that modify the background color it will leak to the background area around the viewport.

Turns out the accumulation function was clearing the screen and it was doing so after rendering the console so it had the text background color set when it did. This is now fixed.

Ape commented 5 years ago

I think the standard thing to do on Linux is something like fc-match --format=%{file} monospace

Okay, I've added this and it should be tested.

The fallback font now works on my system.

Ape commented 5 years ago

In the example it says:

You can change to a console of a different size in response to a WINDOWRESIZED event if you want.

But how can this be done? It looks like the console size paramers must be given in console_init_root and are read-only after that. Exiting the console and calling console_init_root doesn't work very well either because it recreates the window.

HexDecimal commented 5 years ago

But how can this be done?

The accumulate function will take any console of any size. You just swap out the root console with a new one make by calling tcod.console.Console with the size you want.

Ape commented 5 years ago

Thanks. console_init_root could be replaced with something like init_window that takes the parameters title, fullscreen, and renderer, but not the console related parameters. Then I could just create the console(s) manually and use this new rendering method to draw them to the screen.

HexDecimal commented 5 years ago

init_window could work for now. The hard part is figuring out where to put it in the API.

console_init_root now has a vsync parameter in the latest release.

I've also added another example that shows how to make a resizable console: https://github.com/libtcod/python-tcod/blob/master/examples/experimental/resizable_console.py

HexDecimal commented 5 years ago

custrender.py has a new initialization function: init_sdl2, this one takes width and height in pixel resolution and you can give it SDL2 window flags.

This should address the features you wanted from an init_window function.

Ape commented 5 years ago

Thanks. What width and height values should I pass to init_sdl2 if I want to use SDL_WINDOW_FULLSCREEN or SDL_WINDOW_MAXIMIZED where the resolution is determined automatically?

Also, can you add the functions in custrender.py to the tcod python library so I can just use them without having to copy-paste them to my project. I can still use modified functions when needed, but some of the functions are quite generic or are at least good for the most common cases.

HexDecimal commented 5 years ago

You might want to use SDL_WINDOW_FULLSCREEN_DESKTOP instead of SDL_WINDOW_FULLSCREEN.

SDL_WINDOW_FULLSCREEN is exclusive mode, you'd need to get video modes from SDL_GetDisplayMode and use that to set the resolution, and few people like having their resolution changed.

For SDL_WINDOW_FULLSCREEN_DESKTOP or SDL_WINDOW_MAXIMIZED the resolution given should be a decent fallback for whenever the window flags are not applied for whatever reason. It would be the resolution for the window if the window was unmaximized or taken off of fullscreen mode.

I was thinking of adding something like a tcod.future package that could hold functions until they're more stable.

HexDecimal commented 4 years ago

I've been able to improve the renderers. Now everything you'd want is available in the tcod.console_flush function since version 11.8. You no longer need custrender.py.

Be sure to mention anything else this should have.