diff --git a/buildsystem/codecompliance/__main__.py b/buildsystem/codecompliance/__main__.py index dded8dd0a9..2a4f8b4084 100644 --- a/buildsystem/codecompliance/__main__.py +++ b/buildsystem/codecompliance/__main__.py @@ -1,4 +1,4 @@ -# Copyright 2014-2023 the openage authors. See copying.md for legal info. +# Copyright 2014-2024 the openage authors. See copying.md for legal info. """ Entry point for the code compliance checker. @@ -231,7 +231,7 @@ def find_all_issues(args, check_files=None): if args.pystyle: from .pystyle import find_issues - yield from find_issues(check_files, ('openage', 'buildsystem')) + yield from find_issues(check_files, ('openage', 'buildsystem', 'etc/gdb_pretty')) if args.cython: from buildsystem.codecompliance.cython import find_issues @@ -243,12 +243,12 @@ def find_all_issues(args, check_files=None): if args.pylint: from .pylint import find_issues - yield from find_issues(check_files, ('openage', 'buildsystem')) + yield from find_issues(check_files, ('openage', 'buildsystem', 'etc/gdb_pretty')) if args.textfiles: from .textfiles import find_issues yield from find_issues( - ('openage', 'libopenage', 'buildsystem', 'doc', 'legal'), + ('openage', 'libopenage', 'buildsystem', 'doc', 'legal', 'etc/gdb_pretty'), ('.pxd', '.pyx', '.pxi', '.py', '.h', '.cpp', '.template', '', '.txt', '.md', '.conf', @@ -257,13 +257,13 @@ def find_all_issues(args, check_files=None): if args.legal: from .legal import find_issues yield from find_issues(check_files, - ('openage', 'buildsystem', 'libopenage'), + ('openage', 'buildsystem', 'libopenage', 'etc/gdb_pretty'), args.test_git_change_years) if args.filemodes: from .modes import find_issues yield from find_issues(check_files, ('openage', 'buildsystem', - 'libopenage')) + 'libopenage', 'etc/gdb_pretty')) if __name__ == '__main__': diff --git a/doc/debug.md b/doc/debug.md index e8f838e0b2..cf6c8880fc 100644 --- a/doc/debug.md +++ b/doc/debug.md @@ -21,10 +21,28 @@ gdb -ex 'set breakpoint pending on' -ex 'b openage::run_game' -ex run --args run ``` The game will be paused at the start of the function run_game() located in `libopenage/main.cpp` -#### Note: -The `run` executable is a compiled version of `run.py` that also embeds the interpreter. +**Note:** The `run` executable is a compiled version of `run.py` that also embeds the interpreter. The game is intended to be run by `run.py` but it is much easier to debug the `./run` file +### Pretty Printers + +Enabling pretty printing will make GDB's output much more readable, so we always recommend +to configure it in your setup. Your [favourite IDE](/doc/ide/) probably an option to enable pretty printers +for the standard library types. If not, you can get them from the [gcc repository](https://github.com/gcc-mirror/gcc/tree/master/libstdc%2B%2B-v3/python/libstdcxx) and register them in your local `.gdbinit` file. + +Additionally, we have created several custom GDB pretty printers for types used in `libopenage`, +the C++ library that contains the openage engine core. To enable them, you have to load the project's +own init file [openage.gdbinit](/etc/openage.gdbinit) when running GDB: + +```gdb +(gdb) source /etc/openage.gdbinit +``` + +Your IDE may be able to do this automatically for a debug run. Alternatively, you can configure +an [auto-loader](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Python-Auto_002dloading.html#Python-Auto_002dloading) +that loads the scripts for you. + + ### GDBGUI [gdbgui](https://github.com/cs01/gdbgui) is a browser-based frontend for GDB. diff --git a/etc/gdb_pretty/__init__.py b/etc/gdb_pretty/__init__.py new file mode 100644 index 0000000000..0e697d0676 --- /dev/null +++ b/etc/gdb_pretty/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2024-2024 the openage authors. See copying.md for legal info. + +""" +GDB pretty printers for openage. +""" diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py new file mode 100644 index 0000000000..e5bbb42d84 --- /dev/null +++ b/etc/gdb_pretty/printers.py @@ -0,0 +1,241 @@ +# Copyright 2024-2024 the openage authors. See copying.md for legal info. + +""" +Pretty printers for GDB. +""" + +import re +import gdb # type: ignore + + +class PrinterControl(gdb.printing.PrettyPrinter): + """ + Exposes a pretty printer for a specific type. + + Printer are searched in the following order: + 1. Exact type name _with_ typedefs + 2. Regex of type name _without_ typedefs + """ + + def __init__(self, name: str): + super().__init__(name) + + self.name_printers = {} + self.regex_printers = {} + + def add_printer(self, type_name: str, printer): + """ + Adds a printer for a specific type name. + """ + self.name_printers[type_name] = printer + + def add_printer_regex(self, regex: str, printer): + """ + Adds a printer for a specific type name. + + :param regex: The regex to match the type name. + :type regex: str + """ + self.regex_printers[re.compile(regex)] = printer + + def __call__(self, val: gdb.Value): + # Check the exact type name with typedefa + type_name = val.type.name + if type_name in self.name_printers: + return self.name_printers[val.type.name](val) + + # Check the type name without typedefs and regex + type_name = val.type.unqualified().strip_typedefs().tag + if type_name is None: + return None + + for regex, printer in self.regex_printers.items(): + if regex.match(type_name): + return printer(val) + + return None + + +pp = PrinterControl('openage') +gdb.printing.register_pretty_printer(None, pp) + + +def printer_typedef(type_name: str): + """ + Decorator for pretty printers. + + :param type_name: The name of the type to register the printer for. + :type type_name: str + """ + def _register_printer(printer): + """ + Registers the printer with GDB. + """ + pp.add_printer(type_name, printer) + + return _register_printer + + +def printer_regex(regex: str): + """ + Decorator for pretty printers. + + :param regex: The regex to match the type name. + :type regex: str + """ + def _register_printer(printer): + """ + Registers the printer with GDB. + """ + pp.add_printer_regex(regex, printer) + + return _register_printer + + +@printer_typedef('openage::time::time_t') +class TimePrinter: + """ + Pretty printer for openage::time::time_t. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the time as a string. + + Format: SS.sss (e.g. 12.345s) + """ + fractional_bits = int(self.__val.type.template_argument(1)) + + # convert the fixed point value to double + to_double_factor = 1 / pow(2, fractional_bits) + seconds = float(self.__val['raw_value']) * to_double_factor + # show as seconds with millisecond precision + return f'{seconds:.3f}s' + + def children(self): + """ + Get the displayed children of the time value. + """ + yield ('raw_value', self.__val['raw_value']) + + +@printer_regex('^openage::util::FixedPoint<.*>') +class FixedPointPrinter: + """ + Pretty printer for openage::util::FixedPoint. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the fixed point value as a string. + + Format: 0.12345 + """ + fractional_bits = int(self.__val.type.template_argument(1)) + + # convert the fixed point value to double + to_double_factor = 1 / pow(2, fractional_bits) + num = float(self.__val['raw_value']) * to_double_factor + return f'{num:.5f}' + + def children(self): + """ + Get the displayed children of the fixed point value. + """ + yield ('raw_value', self.__val['raw_value']) + + # calculate the precision of the fixed point value + # 16 * log10(2) = 16 * 0.30103 = 4.81648 + # do this manualy because it's usually optimized out by the compiler + fractional_bits = int(self.__val.type.template_argument(1)) + + precision = int(fractional_bits * 0.30103 + 1) + yield ('approx_precision', precision) + + +@printer_regex('^openage::util::Vector<.*>') +class VectorPrinter: + """ + Pretty printer for openage::util::Vector. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the vector as a string. + """ + size = self.__val.type.template_argument(0) + int_type = self.__val.type.template_argument(1) + return f'openage::util::Vector<{size}, {int_type}>' + + def children(self): + """ + Get the displayed children of the vector. + """ + size = self.__val.type.template_argument(0) + for i in range(size): + yield (str(i), self.__val['_M_elems'][i]) + + def child(self, index): + """ + Get the child at the given index. + """ + return self.__val['_M_elems'][index] + + def num_children(self): + """ + Get the number of children of the vector. + """ + return self.__val.type.template_argument(0) + + @staticmethod + def display_hint(): + """ + Get the display hint for the vector. + """ + return 'array' + + +@printer_regex('^openage::curve::Keyframe<.*>') +class KeyframePrinter: + """ + Pretty printer for openage::curve::Keyframe. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the keyframe as a string. + """ + return f'openage::curve::Keyframe<{self.__val.type.template_argument(0)}>' + + def children(self): + """ + Get the displayed children of the keyframe. + """ + yield ('time', self.__val['time']) + yield ('value', self.__val['value']) + +# TODO: curve types +# TODO: coord types +# TODO: pathfinding types +# TODO: input event codes +# TODO: eigen types https://github.com/dmillard/eigengdb diff --git a/etc/openage.gdbinit b/etc/openage.gdbinit new file mode 100644 index 0000000000..c0e8b86ce4 --- /dev/null +++ b/etc/openage.gdbinit @@ -0,0 +1,10 @@ +python +import sys, os + +print("Loading openage.gdbinit") +print(f"Adding custom pretty-printers directory to the GDB path: {os.getcwd() + '../../etc'}") + +sys.path.insert(0, "../../etc") + +import gdb_pretty.printers +end diff --git a/libopenage/gamestate/simulation.cpp b/libopenage/gamestate/simulation.cpp index e883bd56f7..ed2627c95e 100644 --- a/libopenage/gamestate/simulation.cpp +++ b/libopenage/gamestate/simulation.cpp @@ -1,4 +1,4 @@ -// Copyright 2013-2023 the openage authors. See copying.md for legal info. +// Copyright 2013-2024 the openage authors. See copying.md for legal info. #include "simulation.h" @@ -45,7 +45,7 @@ GameSimulation::GameSimulation(const util::Path &root_dir, void GameSimulation::run() { this->start(); while (this->running) { - auto current_time = this->time_loop->get_clock()->get_time(); + time::time_t current_time = this->time_loop->get_clock()->get_time(); this->event_loop->reach_time(current_time, this->game->get_state()); } log::log(MSG(info) << "Game simulation loop exited");