# HG changeset patch # User Niels Martin Hansen # Date 2020-01-26 12:45:51 # Node ID 49625df81342ee09e08a17b2235a1f8b5a0b83a1 # Parent b04878c663c752adf9749a1a3895f940ff157ebd Feature: NewGRF callback profiling (#7868) Adds a console command newgrf_profile to collect some profiling data about NewGRF action 2 callbacks and produce a CSV file. diff --git a/docs/logging_and_performance_metrics.md b/docs/logging_and_performance_metrics.md --- a/docs/logging_and_performance_metrics.md +++ b/docs/logging_and_performance_metrics.md @@ -99,3 +99,43 @@ The following is an explanation of the d If the frame rate window is shaded, the title bar will instead show just the current simulation rate and the game speed factor. +## 3.0) NewGRF callback profiling + +NewGRF developers can profile callback chains via the `newgrf_profile` +console command. The command controls a profiling mode where every sprite +request is measured and logged, and written to a CSV file in the end. + +The NewGRF developer tools need to be enabled for the command to function. + +View the syntax for the command in-game with the console command +`help newgrf_profile`. + +Profiling only works during game or in the editor, it's not possible to +profile across the main menu, world generation, or loading savegames. + +The CSV files contain one line per sprite request during the profiling. +They can get very large, especially on large games with many objects from +the GRF. Start profiling short periods such as 3 or 7 days, and watch the +file sizes. + +The produced CSV file contains the following fields: + +- *Tick* - Game tick counter, this may wrap to zero during recording. + Mainly useful to distinguish events from separate ticks. +- *Sprite* - Index of the root Action 2 sprite in the GRF file. This is + the sprite group being resolved. +- *Feature* - NewGRF feature number the sprite group is being resolved for. + This will be 0xFF for AI purchase selection and ambient sound callbacks. +- *Item* - The id of the item within the GRF. For cargotypes, railtypes, + roadtypes, and tramtypes, this is the integer representation of the label. +- *CallbackID* - The type of callback being resolved. ID 0 is regular graphics + lookup. See the `newgrf_callbacks.h` file in the OpenTTD source code for the + full list of callback IDs. +- *Microseconds* - Total time spent to resolve the Action 2, in microseconds. +- *Depth* - Number of recursive Action 2 lookups were made during resolution. + Value zero means the sprite group resolved directly. +- *Result* - Result of the callback resolution. For lookups that result in + a sprite, this is the index of the base action 2 in the GRF file. For + callbacks that give a numeric result, this is the callback result value. + For lookups that result in an industry production or tilelayout, this + is the sprite index of the action 2 defining the production/tilelayout. diff --git a/projects/openttd_vs140.vcxproj b/projects/openttd_vs140.vcxproj --- a/projects/openttd_vs140.vcxproj +++ b/projects/openttd_vs140.vcxproj @@ -583,6 +583,7 @@ + @@ -1236,6 +1237,7 @@ + diff --git a/projects/openttd_vs140.vcxproj.filters b/projects/openttd_vs140.vcxproj.filters --- a/projects/openttd_vs140.vcxproj.filters +++ b/projects/openttd_vs140.vcxproj.filters @@ -837,6 +837,9 @@ Header Files + + Header Files + Header Files @@ -2796,6 +2799,9 @@ NewGRF + + NewGRF + NewGRF diff --git a/projects/openttd_vs141.vcxproj b/projects/openttd_vs141.vcxproj --- a/projects/openttd_vs141.vcxproj +++ b/projects/openttd_vs141.vcxproj @@ -583,6 +583,7 @@ + @@ -1236,6 +1237,7 @@ + diff --git a/projects/openttd_vs141.vcxproj.filters b/projects/openttd_vs141.vcxproj.filters --- a/projects/openttd_vs141.vcxproj.filters +++ b/projects/openttd_vs141.vcxproj.filters @@ -837,6 +837,9 @@ Header Files + + Header Files + Header Files @@ -2796,6 +2799,9 @@ NewGRF + + NewGRF + NewGRF diff --git a/projects/openttd_vs142.vcxproj b/projects/openttd_vs142.vcxproj --- a/projects/openttd_vs142.vcxproj +++ b/projects/openttd_vs142.vcxproj @@ -583,6 +583,7 @@ + @@ -1236,6 +1237,7 @@ + diff --git a/projects/openttd_vs142.vcxproj.filters b/projects/openttd_vs142.vcxproj.filters --- a/projects/openttd_vs142.vcxproj.filters +++ b/projects/openttd_vs142.vcxproj.filters @@ -837,6 +837,9 @@ Header Files + + Header Files + Header Files @@ -2796,6 +2799,9 @@ NewGRF + + NewGRF + NewGRF diff --git a/source.list b/source.list --- a/source.list +++ b/source.list @@ -270,6 +270,7 @@ newgrf_house.h newgrf_industries.h newgrf_industrytiles.h newgrf_object.h +newgrf_profiling.h newgrf_properties.h newgrf_railtype.h newgrf_roadtype.h @@ -986,6 +987,7 @@ newgrf_house.cpp newgrf_industries.cpp newgrf_industrytiles.cpp newgrf_object.cpp +newgrf_profiling.cpp newgrf_railtype.cpp newgrf_roadtype.cpp newgrf_sound.cpp diff --git a/src/console_cmds.cpp b/src/console_cmds.cpp --- a/src/console_cmds.cpp +++ b/src/console_cmds.cpp @@ -33,6 +33,7 @@ #include "ai/ai.hpp" #include "ai/ai_config.hpp" #include "newgrf.h" +#include "newgrf_profiling.h" #include "console_func.h" #include "engine_base.h" #include "game/game.hpp" @@ -1877,6 +1878,135 @@ DEF_CONSOLE_CMD(ConNewGRFReload) return true; } +DEF_CONSOLE_CMD(ConNewGRFProfile) +{ + if (argc == 0) { + IConsoleHelp("Collect performance data about NewGRF sprite requests and callbacks. Sub-commands can be abbreviated."); + IConsoleHelp("Usage: newgrf_profile [list]"); + IConsoleHelp(" List all NewGRFs that can be profiled, and their status."); + IConsoleHelp("Usage: newgrf_profile select ..."); + IConsoleHelp(" Select one or more GRFs for profiling."); + IConsoleHelp("Usage: newgrf_profile unselect ..."); + IConsoleHelp(" Unselect one or more GRFs from profiling. Use the keyword \"all\" instead of a GRF number to unselect all. Removing an active profiler aborts data collection."); + IConsoleHelp("Usage: newgrf_profile start []"); + IConsoleHelp(" Begin profiling all selected GRFs. If a number of days is provided, profiling stops after that many in-game days."); + IConsoleHelp("Usage: newgrf_profile stop"); + IConsoleHelp(" End profiling and write the collected data to CSV files."); + IConsoleHelp("Usage: newgrf_profile abort"); + IConsoleHelp(" End profiling and discard all collected data."); + return true; + } + + extern const std::vector &GetAllGRFFiles(); + const std::vector &files = GetAllGRFFiles(); + + /* "list" sub-command */ + if (argc == 1 || strncasecmp(argv[1], "lis", 3) == 0) { + IConsolePrint(CC_INFO, "Loaded GRF files:"); + int i = 1; + for (GRFFile *grf : files) { + auto profiler = std::find_if(_newgrf_profilers.begin(), _newgrf_profilers.end(), [&](NewGRFProfiler &pr) { return pr.grffile == grf; }); + bool selected = profiler != _newgrf_profilers.end(); + bool active = selected && profiler->active; + TextColour tc = active ? TC_LIGHT_BLUE : selected ? TC_GREEN : CC_INFO; + const char *statustext = active ? " (active)" : selected ? " (selected)" : ""; + IConsolePrintF(tc, "%d: [%08X] %s%s", i, BSWAP32(grf->grfid), grf->filename, statustext); + i++; + } + return true; + } + + /* "select" sub-command */ + if (strncasecmp(argv[1], "sel", 3) == 0 && argc >= 3) { + for (size_t argnum = 2; argnum < argc; ++argnum) { + int grfnum = atoi(argv[argnum]); + if (grfnum < 1 || grfnum > (int)files.size()) { // safe cast, files.size() should not be larger than a few hundred in the most extreme cases + IConsolePrintF(CC_WARNING, "GRF number %d out of range, not added.", grfnum); + continue; + } + GRFFile *grf = files[grfnum - 1]; + if (std::any_of(_newgrf_profilers.begin(), _newgrf_profilers.end(), [&](NewGRFProfiler &pr) { return pr.grffile == grf; })) { + IConsolePrintF(CC_WARNING, "GRF number %d [%08X] is already selected for profiling.", grfnum, BSWAP32(grf->grfid)); + continue; + } + _newgrf_profilers.emplace_back(grf); + } + return true; + } + + /* "unselect" sub-command */ + if (strncasecmp(argv[1], "uns", 3) == 0 && argc >= 3) { + for (size_t argnum = 2; argnum < argc; ++argnum) { + if (strcasecmp(argv[argnum], "all") == 0) { + _newgrf_profilers.clear(); + break; + } + int grfnum = atoi(argv[argnum]); + if (grfnum < 1 || grfnum > (int)files.size()) { + IConsolePrintF(CC_WARNING, "GRF number %d out of range, not removing.", grfnum); + continue; + } + GRFFile *grf = files[grfnum - 1]; + auto pos = std::find_if(_newgrf_profilers.begin(), _newgrf_profilers.end(), [&](NewGRFProfiler &pr) { return pr.grffile == grf; }); + if (pos != _newgrf_profilers.end()) _newgrf_profilers.erase(pos); + } + return true; + } + + /* "start" sub-command */ + if (strncasecmp(argv[1], "sta", 3) == 0) { + std::string grfids; + size_t started = 0; + for (NewGRFProfiler &pr : _newgrf_profilers) { + if (!pr.active) { + pr.Start(); + started++; + + if (!grfids.empty()) grfids += ", "; + char grfidstr[12]{ 0 }; + seprintf(grfidstr, lastof(grfidstr), "[%08X]", BSWAP32(pr.grffile->grfid)); + grfids += grfidstr; + } + } + if (started > 0) { + IConsolePrintF(CC_DEBUG, "Started profiling for GRFID%s %s", (started > 1) ? "s" : "", grfids.c_str()); + if (argc >= 3) { + int days = max(atoi(argv[2]), 1); + _newgrf_profile_end_date = _date + days; + + char datestrbuf[32]{ 0 }; + SetDParam(0, _newgrf_profile_end_date); + GetString(datestrbuf, STR_JUST_DATE_ISO, lastof(datestrbuf)); + IConsolePrintF(CC_DEBUG, "Profiling will automatically stop on game date %s", datestrbuf); + } else { + _newgrf_profile_end_date = MAX_DAY; + } + } else if (_newgrf_profilers.empty()) { + IConsolePrintF(CC_WARNING, "No GRFs selected for profiling, did not start."); + } else { + IConsolePrintF(CC_WARNING, "Did not start profiling for any GRFs, all selected GRFs are already profiling."); + } + return true; + } + + /* "stop" sub-command */ + if (strncasecmp(argv[1], "sto", 3) == 0) { + NewGRFProfiler::FinishAll(); + return true; + } + + /* "abort" sub-command */ + if (strncasecmp(argv[1], "abo", 3) == 0) { + for (NewGRFProfiler &pr : _newgrf_profilers) { + pr.Abort(); + } + _newgrf_profile_end_date = MAX_DAY; + return true; + } + + return false; +} + #ifdef _DEBUG /****************** * debug commands @@ -2056,4 +2186,5 @@ void IConsoleStdLibRegister() /* NewGRF development stuff */ IConsoleCmdRegister("reload_newgrfs", ConNewGRFReload, ConHookNewGRFDeveloperTool); + IConsoleCmdRegister("newgrf_profile", ConNewGRFProfile, ConHookNewGRFDeveloperTool); } diff --git a/src/date.cpp b/src/date.cpp --- a/src/date.cpp +++ b/src/date.cpp @@ -18,6 +18,7 @@ #include "rail_gui.h" #include "linkgraph/linkgraph.h" #include "saveload/saveload.h" +#include "newgrf_profiling.h" #include "safeguards.h" @@ -245,6 +246,10 @@ static void OnNewMonth() */ static void OnNewDay() { + if (!_newgrf_profilers.empty() && _newgrf_profile_end_date <= _date) { + NewGRFProfiler::FinishAll(); + } + if (_network_server) NetworkServerDailyLoop(); DisasterDailyLoop(); diff --git a/src/misc.cpp b/src/misc.cpp --- a/src/misc.cpp +++ b/src/misc.cpp @@ -29,6 +29,7 @@ #include "station_kdtree.h" #include "town_kdtree.h" #include "viewport_kdtree.h" +#include "newgrf_profiling.h" #include "safeguards.h" @@ -69,6 +70,8 @@ void InitializeGame(uint size_x, uint si _thd.redsq = INVALID_TILE; if (reset_settings) MakeNewgameSettingsLive(); + _newgrf_profilers.clear(); + if (reset_date) { SetDate(ConvertYMDToDate(_settings_game.game_creation.starting_year, 0, 1), 0); InitializeOldNames(); diff --git a/src/newgrf.cpp b/src/newgrf.cpp --- a/src/newgrf.cpp +++ b/src/newgrf.cpp @@ -66,6 +66,11 @@ /** List of all loaded GRF files */ static std::vector _grf_files; +const std::vector &GetAllGRFFiles() +{ + return _grf_files; +} + /** Miscellaneous GRF features, set by Action 0x0D, parameter 0x9E */ byte _misc_grf_features = 0; @@ -5000,6 +5005,7 @@ static void NewSpriteGroup(ByteReader *b assert(DeterministicSpriteGroup::CanAllocateItem()); DeterministicSpriteGroup *group = new DeterministicSpriteGroup(); + group->nfo_line = _cur.nfo_line; act_group = group; group->var_scope = HasBit(type, 1) ? VSG_SCOPE_PARENT : VSG_SCOPE_SELF; @@ -5116,6 +5122,7 @@ static void NewSpriteGroup(ByteReader *b { assert(RandomizedSpriteGroup::CanAllocateItem()); RandomizedSpriteGroup *group = new RandomizedSpriteGroup(); + group->nfo_line = _cur.nfo_line; act_group = group; group->var_scope = HasBit(type, 1) ? VSG_SCOPE_PARENT : VSG_SCOPE_SELF; @@ -5164,6 +5171,7 @@ static void NewSpriteGroup(ByteReader *b assert(RealSpriteGroup::CanAllocateItem()); RealSpriteGroup *group = new RealSpriteGroup(); + group->nfo_line = _cur.nfo_line; act_group = group; group->num_loaded = num_loaded; @@ -5197,6 +5205,7 @@ static void NewSpriteGroup(ByteReader *b assert(TileLayoutSpriteGroup::CanAllocateItem()); TileLayoutSpriteGroup *group = new TileLayoutSpriteGroup(); + group->nfo_line = _cur.nfo_line; act_group = group; /* On error, bail out immediately. Temporary GRF data was already freed */ @@ -5212,6 +5221,7 @@ static void NewSpriteGroup(ByteReader *b assert(IndustryProductionSpriteGroup::CanAllocateItem()); IndustryProductionSpriteGroup *group = new IndustryProductionSpriteGroup(); + group->nfo_line = _cur.nfo_line; act_group = group; group->version = type; if (type == 0) { diff --git a/src/newgrf_airport.cpp b/src/newgrf_airport.cpp --- a/src/newgrf_airport.cpp +++ b/src/newgrf_airport.cpp @@ -58,6 +58,9 @@ struct AirportResolverObject : public Re } const SpriteGroup *ResolveReal(const RealSpriteGroup *group) const override; + + GrfSpecFeature GetFeature() const override; + uint32 GetDebugID() const override; }; /** @@ -226,6 +229,16 @@ void AirportOverrideManager::SetEntitySp return nullptr; } +GrfSpecFeature AirportResolverObject::GetFeature() const +{ + return GSF_AIRPORTS; +} + +uint32 AirportResolverObject::GetDebugID() const +{ + return AirportSpec::Get(this->airport_scope.airport_id)->grf_prop.local_id; +} + /* virtual */ uint32 AirportScopeResolver::GetRandomBits() const { return this->st == nullptr ? 0 : this->st->random_bits; diff --git a/src/newgrf_airporttiles.cpp b/src/newgrf_airporttiles.cpp --- a/src/newgrf_airporttiles.cpp +++ b/src/newgrf_airporttiles.cpp @@ -220,6 +220,16 @@ AirportTileResolverObject::AirportTileRe this->root_spritegroup = ats->grf_prop.spritegroup[0]; } +GrfSpecFeature AirportTileResolverObject::GetFeature() const +{ + return GSF_AIRPORTTILES; +} + +uint32 AirportTileResolverObject::GetDebugID() const +{ + return this->tiles_scope.ats->grf_prop.local_id; +} + uint16 GetAirportTileCallback(CallbackID callback, uint32 param1, uint32 param2, const AirportTileSpec *ats, Station *st, TileIndex tile, int extra_data = 0) { AirportTileResolverObject object(ats, tile, st, callback, param1, param2); diff --git a/src/newgrf_airporttiles.h b/src/newgrf_airporttiles.h --- a/src/newgrf_airporttiles.h +++ b/src/newgrf_airporttiles.h @@ -22,6 +22,7 @@ struct AirportTileScopeResolver : public struct Station *st; ///< %Station of the airport for which the callback is run, or \c nullptr for build gui. byte airport_id; ///< Type of airport for which the callback is run. TileIndex tile; ///< Tile for the callback, only valid for airporttile callbacks. + const AirportTileSpec *ats; /** * Constructor of the scope resolver specific for airport tiles. @@ -30,7 +31,7 @@ struct AirportTileScopeResolver : public * @param st Station of the airport for which the callback is run, or \c nullptr for build gui. */ AirportTileScopeResolver(ResolverObject &ro, const AirportTileSpec *ats, TileIndex tile, Station *st) - : ScopeResolver(ro), st(st), tile(tile) + : ScopeResolver(ro), st(st), tile(tile), ats(ats) { assert(st != nullptr); this->airport_id = st->airport.type; @@ -54,6 +55,9 @@ struct AirportTileResolverObject : publi default: return ResolverObject::GetScope(scope, relative); } } + + GrfSpecFeature GetFeature() const override; + uint32 GetDebugID() const override; }; /** diff --git a/src/newgrf_canal.cpp b/src/newgrf_canal.cpp --- a/src/newgrf_canal.cpp +++ b/src/newgrf_canal.cpp @@ -13,6 +13,7 @@ #include "newgrf_canal.h" #include "water.h" #include "water_map.h" +#include "spritecache.h" #include "safeguards.h" @@ -35,6 +36,7 @@ struct CanalScopeResolver : public Scope /** Resolver object for canals. */ struct CanalResolverObject : public ResolverObject { CanalScopeResolver canal_scope; + CanalFeature feature; CanalResolverObject(CanalFeature feature, TileIndex tile, CallbackID callback = CBID_NO_CALLBACK, uint32 callback_param1 = 0, uint32 callback_param2 = 0); @@ -48,6 +50,9 @@ struct CanalResolverObject : public Reso } const SpriteGroup *ResolveReal(const RealSpriteGroup *group) const override; + + GrfSpecFeature GetFeature() const override; + uint32 GetDebugID() const override; }; /* virtual */ uint32 CanalScopeResolver::GetRandomBits() const @@ -111,6 +116,16 @@ struct CanalResolverObject : public Reso return group->loaded[0]; } +GrfSpecFeature CanalResolverObject::GetFeature() const +{ + return GSF_CANALS; +} + +uint32 CanalResolverObject::GetDebugID() const +{ + return this->feature; +} + /** * Canal resolver constructor. * @param feature Which canal feature we want. @@ -121,7 +136,7 @@ struct CanalResolverObject : public Reso */ CanalResolverObject::CanalResolverObject(CanalFeature feature, TileIndex tile, CallbackID callback, uint32 callback_param1, uint32 callback_param2) - : ResolverObject(_water_feature[feature].grffile, callback, callback_param1, callback_param2), canal_scope(*this, tile) + : ResolverObject(_water_feature[feature].grffile, callback, callback_param1, callback_param2), canal_scope(*this, tile), feature(feature) { this->root_spritegroup = _water_feature[feature].group; } diff --git a/src/newgrf_cargo.cpp b/src/newgrf_cargo.cpp --- a/src/newgrf_cargo.cpp +++ b/src/newgrf_cargo.cpp @@ -15,9 +15,14 @@ /** Resolver of cargo. */ struct CargoResolverObject : public ResolverObject { + const CargoSpec *cargospec; + CargoResolverObject(const CargoSpec *cs, CallbackID callback = CBID_NO_CALLBACK, uint32 callback_param1 = 0, uint32 callback_param2 = 0); const SpriteGroup *ResolveReal(const RealSpriteGroup *group) const override; + + GrfSpecFeature GetFeature() const override; + uint32 GetDebugID() const override; }; /* virtual */ const SpriteGroup *CargoResolverObject::ResolveReal(const RealSpriteGroup *group) const @@ -30,6 +35,16 @@ struct CargoResolverObject : public Reso return nullptr; } +GrfSpecFeature CargoResolverObject::GetFeature() const +{ + return GSF_CARGOES; +} + +uint32 CargoResolverObject::GetDebugID() const +{ + return this->cargospec->label; +} + /** * Constructor of the cargo resolver. * @param cs Cargo being resolved. @@ -38,7 +53,7 @@ struct CargoResolverObject : public Reso * @param callback_param2 Second parameter (var 18) of the callback. */ CargoResolverObject::CargoResolverObject(const CargoSpec *cs, CallbackID callback, uint32 callback_param1, uint32 callback_param2) - : ResolverObject(cs->grffile, callback, callback_param1, callback_param2) + : ResolverObject(cs->grffile, callback, callback_param1, callback_param2), cargospec(cs) { this->root_spritegroup = cs->group; } diff --git a/src/newgrf_engine.cpp b/src/newgrf_engine.cpp --- a/src/newgrf_engine.cpp +++ b/src/newgrf_engine.cpp @@ -946,6 +946,22 @@ static uint32 VehicleGetVariable(Vehicle return in_motion ? group->loaded[set] : group->loading[set]; } +GrfSpecFeature VehicleResolverObject::GetFeature() const +{ + switch (Engine::Get(this->self_scope.self_type)->type) { + case VEH_TRAIN: return GSF_TRAINS; + case VEH_ROAD: return GSF_ROADVEHICLES; + case VEH_SHIP: return GSF_SHIPS; + case VEH_AIRCRAFT: return GSF_AIRCRAFT; + default: return GSF_INVALID; + } +} + +uint32 VehicleResolverObject::GetDebugID() const +{ + return Engine::Get(this->self_scope.self_type)->grf_prop.local_id; +} + /** * Get the grf file associated with an engine type. * @param engine_type Engine to query. diff --git a/src/newgrf_engine.h b/src/newgrf_engine.h --- a/src/newgrf_engine.h +++ b/src/newgrf_engine.h @@ -65,6 +65,9 @@ struct VehicleResolverObject : public Re ScopeResolver *GetScope(VarSpriteGroupScope scope = VSG_SCOPE_SELF, byte relative = 0) override; const SpriteGroup *ResolveReal(const RealSpriteGroup *group) const override; + + GrfSpecFeature GetFeature() const override; + uint32 GetDebugID() const override; }; static const uint TRAININFO_DEFAULT_VEHICLE_WIDTH = 29; diff --git a/src/newgrf_generic.cpp b/src/newgrf_generic.cpp --- a/src/newgrf_generic.cpp +++ b/src/newgrf_generic.cpp @@ -29,6 +29,8 @@ struct GenericScopeResolver : public Sco uint8 count; uint8 station_size; + uint8 feature; + /** * Generic scope resolver. * @param ro Surrounding resolver. @@ -36,7 +38,7 @@ struct GenericScopeResolver : public Sco */ GenericScopeResolver(ResolverObject &ro, bool ai_callback) : ScopeResolver(ro), cargo_type(0), default_selection(0), src_industry(0), dst_industry(0), distance(0), - event(), count(0), station_size(0), ai_callback(ai_callback) + event(), count(0), station_size(0), feature(GSF_INVALID), ai_callback(ai_callback) { } @@ -62,6 +64,16 @@ struct GenericResolverObject : public Re } const SpriteGroup *ResolveReal(const RealSpriteGroup *group) const override; + + GrfSpecFeature GetFeature() const override + { + return (GrfSpecFeature)this->generic_scope.feature; + } + + uint32 GetDebugID() const override + { + return 0; + } }; struct GenericCallback { @@ -226,6 +238,7 @@ uint16 GetAiPurchaseCallbackResult(uint8 object.generic_scope.event = event; object.generic_scope.count = count; object.generic_scope.station_size = station_size; + object.generic_scope.feature = feature; uint16 callback = GetGenericCallbackResult(feature, object, 0, 0, file); if (callback != CALLBACK_FAILED) callback = GB(callback, 0, 8); @@ -247,6 +260,7 @@ void AmbientSoundEffectCallback(TileInde /* Prepare resolver object. */ GenericResolverObject object(false, CBID_SOUNDS_AMBIENT_EFFECT); + object.generic_scope.feature = GSF_SOUNDFX; uint32 param1_v7 = GetTileType(tile) << 28 | Clamp(TileHeight(tile), 0, 15) << 24 | GB(r, 16, 8) << 16 | GetTerrainType(tile); uint32 param1_v8 = GetTileType(tile) << 24 | GetTileZ(tile) << 16 | GB(r, 16, 8) << 8 | (HasTileWaterClass(tile) ? GetWaterClass(tile) : 0) << 3 | GetTerrainType(tile); diff --git a/src/newgrf_house.cpp b/src/newgrf_house.cpp --- a/src/newgrf_house.cpp +++ b/src/newgrf_house.cpp @@ -62,6 +62,16 @@ HouseResolverObject::HouseResolverObject this->root_spritegroup = HouseSpec::Get(house_id)->grf_prop.spritegroup[0]; } +GrfSpecFeature HouseResolverObject::GetFeature() const +{ + return GSF_HOUSES; +} + +uint32 HouseResolverObject::GetDebugID() const +{ + return HouseSpec::Get(this->house_scope.house_id)->grf_prop.local_id; +} + HouseClassID AllocateHouseClassID(byte grf_class_id, uint32 grfid) { /* Start from 1 because 0 means that no class has been assigned. */ diff --git a/src/newgrf_house.h b/src/newgrf_house.h --- a/src/newgrf_house.h +++ b/src/newgrf_house.h @@ -64,6 +64,9 @@ struct HouseResolverObject : public Reso default: return ResolverObject::GetScope(scope, relative); } } + + GrfSpecFeature GetFeature() const override; + uint32 GetDebugID() const override; }; /** diff --git a/src/newgrf_industries.cpp b/src/newgrf_industries.cpp --- a/src/newgrf_industries.cpp +++ b/src/newgrf_industries.cpp @@ -489,6 +489,16 @@ TownScopeResolver *IndustriesResolverObj return this->town_scope; } +GrfSpecFeature IndustriesResolverObject::GetFeature() const +{ + return GSF_INDUSTRIES; +} + +uint32 IndustriesResolverObject::GetDebugID() const +{ + return GetIndustrySpec(this->industries_scope.type)->grf_prop.local_id; +} + /** * Perform an industry callback. * @param callback The callback to perform. diff --git a/src/newgrf_industries.h b/src/newgrf_industries.h --- a/src/newgrf_industries.h +++ b/src/newgrf_industries.h @@ -63,6 +63,9 @@ struct IndustriesResolverObject : public return ResolverObject::GetScope(scope, relative); } } + + GrfSpecFeature GetFeature() const override; + uint32 GetDebugID() const override; }; /** When should the industry(tile) be triggered for random bits? */ diff --git a/src/newgrf_industrytiles.cpp b/src/newgrf_industrytiles.cpp --- a/src/newgrf_industrytiles.cpp +++ b/src/newgrf_industrytiles.cpp @@ -139,11 +139,22 @@ IndustryTileResolverObject::IndustryTile CallbackID callback, uint32 callback_param1, uint32 callback_param2) : ResolverObject(GetIndTileGrffile(gfx), callback, callback_param1, callback_param2), indtile_scope(*this, indus, tile), - ind_scope(*this, tile, indus, indus->type) + ind_scope(*this, tile, indus, indus->type), + gfx(gfx) { this->root_spritegroup = GetIndustryTileSpec(gfx)->grf_prop.spritegroup[0]; } +GrfSpecFeature IndustryTileResolverObject::GetFeature() const +{ + return GSF_INDUSTRYTILES; +} + +uint32 IndustryTileResolverObject::GetDebugID() const +{ + return GetIndustryTileSpec(gfx)->grf_prop.local_id; +} + static void IndustryDrawTileLayout(const TileInfo *ti, const TileLayoutSpriteGroup *group, byte rnd_colour, byte stage, IndustryGfx gfx) { const DrawTileSprites *dts = group->ProcessRegisters(&stage); diff --git a/src/newgrf_industrytiles.h b/src/newgrf_industrytiles.h --- a/src/newgrf_industrytiles.h +++ b/src/newgrf_industrytiles.h @@ -39,6 +39,7 @@ struct IndustryTileScopeResolver : publi struct IndustryTileResolverObject : public ResolverObject { IndustryTileScopeResolver indtile_scope; ///< Scope resolver for the industry tile. IndustriesScopeResolver ind_scope; ///< Scope resolver for the industry owning the tile. + IndustryGfx gfx; IndustryTileResolverObject(IndustryGfx gfx, TileIndex tile, Industry *indus, CallbackID callback = CBID_NO_CALLBACK, uint32 callback_param1 = 0, uint32 callback_param2 = 0); @@ -51,6 +52,9 @@ struct IndustryTileResolverObject : publ default: return ResolverObject::GetScope(scope, relative); } } + + GrfSpecFeature GetFeature() const override; + uint32 GetDebugID() const override; }; bool DrawNewIndustryTile(TileInfo *ti, Industry *i, IndustryGfx gfx, const IndustryTileSpec *inds); diff --git a/src/newgrf_object.cpp b/src/newgrf_object.cpp --- a/src/newgrf_object.cpp +++ b/src/newgrf_object.cpp @@ -352,7 +352,7 @@ unhandled: */ ObjectResolverObject::ObjectResolverObject(const ObjectSpec *spec, Object *obj, TileIndex tile, uint8 view, CallbackID callback, uint32 param1, uint32 param2) - : ResolverObject(spec->grf_prop.grffile, callback, param1, param2), object_scope(*this, obj, tile, view) + : ResolverObject(spec->grf_prop.grffile, callback, param1, param2), object_scope(*this, obj, spec, tile, view) { this->town_scope = nullptr; this->root_spritegroup = (obj == nullptr && spec->grf_prop.spritegroup[CT_PURCHASE_OBJECT] != nullptr) ? @@ -384,6 +384,16 @@ TownScopeResolver *ObjectResolverObject: return this->town_scope; } +GrfSpecFeature ObjectResolverObject::GetFeature() const +{ + return GSF_OBJECTS; +} + +uint32 ObjectResolverObject::GetDebugID() const +{ + return this->object_scope.spec->grf_prop.local_id; +} + /** * Perform a callback for an object. * @param callback The callback to perform. diff --git a/src/newgrf_object.h b/src/newgrf_object.h --- a/src/newgrf_object.h +++ b/src/newgrf_object.h @@ -98,9 +98,10 @@ struct ObjectSpec { /** Object scope resolver. */ struct ObjectScopeResolver : public ScopeResolver { - struct Object *obj; ///< The object the callback is ran for. - TileIndex tile; ///< The tile related to the object. - uint8 view; ///< The view of the object. + struct Object *obj; ///< The object the callback is ran for. + const ObjectSpec *spec; ///< Specification of the object type. + TileIndex tile; ///< The tile related to the object. + uint8 view; ///< The view of the object. /** * Constructor of an object scope resolver. @@ -109,8 +110,8 @@ struct ObjectScopeResolver : public Scop * @param tile %Tile of the object. * @param view View of the object. */ - ObjectScopeResolver(ResolverObject &ro, Object *obj, TileIndex tile, uint8 view = 0) - : ScopeResolver(ro), obj(obj), tile(tile), view(view) + ObjectScopeResolver(ResolverObject &ro, Object *obj, const ObjectSpec *spec, TileIndex tile, uint8 view = 0) + : ScopeResolver(ro), obj(obj), spec(spec), tile(tile), view(view) { } @@ -144,6 +145,9 @@ struct ObjectResolverObject : public Res } } + GrfSpecFeature GetFeature() const override; + uint32 GetDebugID() const override; + private: TownScopeResolver *GetTown(); }; diff --git a/src/newgrf_profiling.cpp b/src/newgrf_profiling.cpp new file mode 100644 --- /dev/null +++ b/src/newgrf_profiling.cpp @@ -0,0 +1,162 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + + /** @file newgrf_profiling.cpp Profiling of NewGRF action 2 handling. */ + +#include "newgrf_profiling.h" +#include "date_func.h" +#include "fileio_func.h" +#include "string_func.h" +#include "console_func.h" +#include "spritecache.h" + +#include +#include + + +std::vector _newgrf_profilers; +Date _newgrf_profile_end_date; + + +/** + * Create profiler object and begin profiling session. + * @param grffile The GRF file to collect profiling data on + * @param end_date Game date to end profiling on + */ +NewGRFProfiler::NewGRFProfiler(const GRFFile *grffile) : grffile{ grffile }, active{ false }, cur_call{} +{ +} + +/** + * Complete profiling session and write data to file + */ +NewGRFProfiler::~NewGRFProfiler() +{ +} + +/** + * Capture the start of a sprite group resolution. + * @param resolver Data about sprite group being resolved + */ +void NewGRFProfiler::BeginResolve(const ResolverObject &resolver) +{ + using namespace std::chrono; + this->cur_call.root_sprite = resolver.root_spritegroup->nfo_line; + this->cur_call.subs = 0; + this->cur_call.time = (uint32)time_point_cast(high_resolution_clock::now()).time_since_epoch().count(); + this->cur_call.tick = _tick_counter; + this->cur_call.cb = resolver.callback; + this->cur_call.feat = resolver.GetFeature(); + this->cur_call.item = resolver.GetDebugID(); +} + +/** + * Capture the completion of a sprite group resolution. + */ +void NewGRFProfiler::EndResolve(const SpriteGroup *result) +{ + using namespace std::chrono; + this->cur_call.time = (uint32)time_point_cast(high_resolution_clock::now()).time_since_epoch().count() - this->cur_call.time; + + if (result == nullptr) { + this->cur_call.result = 0; + } else if (result->type == SGT_CALLBACK) { + this->cur_call.result = static_cast(result)->result; + } else if (result->type == SGT_RESULT) { + this->cur_call.result = GetSpriteLocalID(static_cast(result)->sprite); + } else { + this->cur_call.result = result->nfo_line; + } + + this->calls.push_back(this->cur_call); +} + +/** + * Capture a recursive sprite group resolution. + */ +void NewGRFProfiler::RecursiveResolve() +{ + this->cur_call.subs += 1; +} + +void NewGRFProfiler::Start() +{ + this->Abort(); + this->active = true; + this->start_tick = _tick_counter; +} + +uint32 NewGRFProfiler::Finish() +{ + if (!this->active) return 0; + + if (this->calls.empty()) { + IConsolePrintF(CC_DEBUG, "Finished profile of NewGRF [%08X], no events collected, not writing a file", BSWAP32(this->grffile->grfid)); + return 0; + } + + std::string filename = this->GetOutputFilename(); + IConsolePrintF(CC_DEBUG, "Finished profile of NewGRF [%08X], writing %u events to %s", BSWAP32(this->grffile->grfid), (uint)this->calls.size(), filename.c_str()); + + FILE *f = FioFOpenFile(filename.c_str(), "wt", Subdirectory::NO_DIRECTORY); + FileCloser fcloser(f); + + uint32 total_microseconds = 0; + + fputs("Tick,Sprite,Feature,Item,CallbackID,Microseconds,Depth,Result\n", f); + for (const Call &c : this->calls) { + fprintf(f, "%u,%u,0x%X,%d,0x%X,%u,%u,%u\n", c.tick, c.root_sprite, c.feat, c.item, (uint)c.cb, c.time, c.subs, c.result); + total_microseconds += c.time; + } + + this->Abort(); + + return total_microseconds; +} + +void NewGRFProfiler::Abort() +{ + this->active = false; + this->calls.clear(); +} + +/** + * Get name of the file that will be written. + * @return File name of profiling output file. + */ +std::string NewGRFProfiler::GetOutputFilename() const +{ + time_t write_time = time(nullptr); + + char timestamp[16] = {}; + strftime(timestamp, lengthof(timestamp), "%Y%m%d-%H%M", localtime(&write_time)); + + char filepath[MAX_PATH] = {}; + seprintf(filepath, lastof(filepath), "%sgrfprofile-%s-%08X.csv", FiosGetScreenshotDir(), timestamp, BSWAP32(this->grffile->grfid)); + + return std::string(filepath); +} + +uint32 NewGRFProfiler::FinishAll() +{ + int max_ticks = 0; + uint32 total_microseconds = 0; + for (NewGRFProfiler &pr : _newgrf_profilers) { + if (pr.active) { + total_microseconds += pr.Finish(); + max_ticks = max(max_ticks, _tick_counter - pr.start_tick); + } + } + + if (total_microseconds > 0 && max_ticks > 0) { + IConsolePrintF(CC_DEBUG, "Total NewGRF callback processing: %u microseconds over %d ticks", total_microseconds, max_ticks); + } + + _newgrf_profile_end_date = MAX_DAY; + + return total_microseconds; +} diff --git a/src/newgrf_profiling.h b/src/newgrf_profiling.h new file mode 100644 --- /dev/null +++ b/src/newgrf_profiling.h @@ -0,0 +1,63 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + + /** @file newgrf_profiling.h Profiling of NewGRF action 2 handling. */ + +#ifndef NEWGRF_PROFILING_H +#define NEWGRF_PROFILING_H + +#include "stdafx.h" +#include "date_type.h" +#include "newgrf.h" +#include "newgrf_callbacks.h" +#include "newgrf_spritegroup.h" + +#include +#include +#include + +/** + * Callback profiler for NewGRF development + */ +struct NewGRFProfiler { + NewGRFProfiler(const GRFFile *grffile); + ~NewGRFProfiler(); + + void BeginResolve(const ResolverObject &resolver); + void EndResolve(const SpriteGroup *result); + void RecursiveResolve(); + + void Start(); + uint32 Finish(); + void Abort(); + std::string GetOutputFilename() const; + + static uint32 FinishAll(); + + /** Measurement of a single sprite group resolution */ + struct Call { + uint32 root_sprite; ///< Pseudo-sprite index in GRF file + uint32 item; ///< Local ID of item being resolved for + uint32 result; ///< Result of callback + uint32 subs; ///< Sub-calls to other sprite groups + uint32 time; ///< Time taken for resolution (microseconds) + uint16 tick; ///< Game tick + CallbackID cb; ///< Callback ID + GrfSpecFeature feat; ///< GRF feature being resolved for + }; + + const GRFFile *grffile; ///< Which GRF is being profiled + bool active; ///< Is this profiler collecting data + uint16 start_tick; ///< Tick number this profiler was started on + Call cur_call; ///< Data for current call in progress + std::vector calls; ///< All calls collected so far +}; + +extern std::vector _newgrf_profilers; +extern Date _newgrf_profile_end_date; + +#endif /* NEWGRF_PROFILING_H */ diff --git a/src/newgrf_railtype.cpp b/src/newgrf_railtype.cpp --- a/src/newgrf_railtype.cpp +++ b/src/newgrf_railtype.cpp @@ -65,6 +65,16 @@ return nullptr; } +GrfSpecFeature RailTypeResolverObject::GetFeature() const +{ + return GSF_RAILTYPES; +} + +uint32 RailTypeResolverObject::GetDebugID() const +{ + return this->railtype_scope.rti->label; +} + /** * Resolver object for rail types. * @param rti Railtype. nullptr in NewGRF Inspect window. @@ -75,7 +85,7 @@ * @param param2 Extra parameter (second parameter of the callback, except railtypes do not have callbacks). */ RailTypeResolverObject::RailTypeResolverObject(const RailtypeInfo *rti, TileIndex tile, TileContext context, RailTypeSpriteGroup rtsg, uint32 param1, uint32 param2) - : ResolverObject(rti != nullptr ? rti->grffile[rtsg] : nullptr, CBID_NO_CALLBACK, param1, param2), railtype_scope(*this, tile, context) + : ResolverObject(rti != nullptr ? rti->grffile[rtsg] : nullptr, CBID_NO_CALLBACK, param1, param2), railtype_scope(*this, rti, tile, context) { this->root_spritegroup = rti != nullptr ? rti->group[rtsg] : nullptr; } diff --git a/src/newgrf_railtype.h b/src/newgrf_railtype.h --- a/src/newgrf_railtype.h +++ b/src/newgrf_railtype.h @@ -18,6 +18,7 @@ struct RailTypeScopeResolver : public ScopeResolver { TileIndex tile; ///< Tracktile. For track on a bridge this is the southern bridgehead. TileContext context; ///< Are we resolving sprites for the upper halftile, or on a bridge? + const RailtypeInfo *rti; /** * Constructor of the railtype scope resolvers. @@ -25,8 +26,8 @@ struct RailTypeScopeResolver : public Sc * @param tile %Tile containing the track. For track on a bridge this is the southern bridgehead. * @param context Are we resolving sprites for the upper halftile, or on a bridge? */ - RailTypeScopeResolver(ResolverObject &ro, TileIndex tile, TileContext context) - : ScopeResolver(ro), tile(tile), context(context) + RailTypeScopeResolver(ResolverObject &ro, const RailtypeInfo *rti, TileIndex tile, TileContext context) + : ScopeResolver(ro), tile(tile), context(context), rti(rti) { } @@ -49,6 +50,9 @@ struct RailTypeResolverObject : public R } const SpriteGroup *ResolveReal(const RealSpriteGroup *group) const override; + + GrfSpecFeature GetFeature() const override; + uint32 GetDebugID() const override; }; SpriteID GetCustomRailSprite(const RailtypeInfo *rti, TileIndex tile, RailTypeSpriteGroup rtsg, TileContext context = TCX_NORMAL, uint *num_results = nullptr); diff --git a/src/newgrf_roadtype.cpp b/src/newgrf_roadtype.cpp --- a/src/newgrf_roadtype.cpp +++ b/src/newgrf_roadtype.cpp @@ -65,16 +65,32 @@ return nullptr; } +GrfSpecFeature RoadTypeResolverObject::GetFeature() const +{ + RoadType rt = GetRoadTypeByLabel(this->roadtype_scope.rti->label, false); + switch (GetRoadTramType(rt)) { + case RTT_ROAD: return GSF_ROADTYPES; + case RTT_TRAM: return GSF_TRAMTYPES; + default: return GSF_INVALID; + } +} + +uint32 RoadTypeResolverObject::GetDebugID() const +{ + return this->roadtype_scope.rti->label; +} + /** * Constructor of the roadtype scope resolvers. * @param ro Surrounding resolver. * @param tile %Tile containing the track. For track on a bridge this is the southern bridgehead. * @param context Are we resolving sprites for the upper halftile, or on a bridge? */ -RoadTypeScopeResolver::RoadTypeScopeResolver(ResolverObject &ro, TileIndex tile, TileContext context) : ScopeResolver(ro) +RoadTypeScopeResolver::RoadTypeScopeResolver(ResolverObject &ro, const RoadTypeInfo *rti, TileIndex tile, TileContext context) : ScopeResolver(ro) { this->tile = tile; this->context = context; + this->rti = rti; } /** @@ -87,7 +103,7 @@ RoadTypeScopeResolver::RoadTypeScopeReso * @param param2 Extra parameter (second parameter of the callback, except roadtypes do not have callbacks). */ RoadTypeResolverObject::RoadTypeResolverObject(const RoadTypeInfo *rti, TileIndex tile, TileContext context, RoadTypeSpriteGroup rtsg, uint32 param1, uint32 param2) - : ResolverObject(rti != nullptr ? rti->grffile[rtsg] : nullptr, CBID_NO_CALLBACK, param1, param2), roadtype_scope(*this, tile, context) + : ResolverObject(rti != nullptr ? rti->grffile[rtsg] : nullptr, CBID_NO_CALLBACK, param1, param2), roadtype_scope(*this, rti, tile, context) { this->root_spritegroup = rti != nullptr ? rti->group[rtsg] : nullptr; } diff --git a/src/newgrf_roadtype.h b/src/newgrf_roadtype.h --- a/src/newgrf_roadtype.h +++ b/src/newgrf_roadtype.h @@ -18,8 +18,9 @@ struct RoadTypeScopeResolver : public ScopeResolver { TileIndex tile; ///< Tracktile. For track on a bridge this is the southern bridgehead. TileContext context; ///< Are we resolving sprites for the upper halftile, or on a bridge? + const RoadTypeInfo *rti; - RoadTypeScopeResolver(ResolverObject &ro, TileIndex tile, TileContext context); + RoadTypeScopeResolver(ResolverObject &ro, const RoadTypeInfo *rti, TileIndex tile, TileContext context); /* virtual */ uint32 GetRandomBits() const; /* virtual */ uint32 GetVariable(byte variable, uint32 parameter, bool *available) const; @@ -40,6 +41,9 @@ struct RoadTypeResolverObject : public R } /* virtual */ const SpriteGroup *ResolveReal(const RealSpriteGroup *group) const; + + GrfSpecFeature GetFeature() const override; + uint32 GetDebugID() const override; }; SpriteID GetCustomRoadSprite(const RoadTypeInfo *rti, TileIndex tile, RoadTypeSpriteGroup rtsg, TileContext context = TCX_NORMAL, uint *num_results = nullptr); diff --git a/src/newgrf_spritegroup.cpp b/src/newgrf_spritegroup.cpp --- a/src/newgrf_spritegroup.cpp +++ b/src/newgrf_spritegroup.cpp @@ -11,6 +11,7 @@ #include #include "debug.h" #include "newgrf_spritegroup.h" +#include "newgrf_profiling.h" #include "core/pool_func.hpp" #include "safeguards.h" @@ -34,10 +35,23 @@ TemporaryStorageArray _tem /* static */ const SpriteGroup *SpriteGroup::Resolve(const SpriteGroup *group, ResolverObject &object, bool top_level) { if (group == nullptr) return nullptr; - if (top_level) { + + const GRFFile *grf = object.grffile; + auto profiler = std::find_if(_newgrf_profilers.begin(), _newgrf_profilers.end(), [&](const NewGRFProfiler &pr) { return pr.grffile == grf; }); + + if (profiler == _newgrf_profilers.end() || !profiler->active) { + if (top_level) _temp_store.ClearChanges(); + return group->Resolve(object); + } else if (top_level) { + profiler->BeginResolve(object); _temp_store.ClearChanges(); + const SpriteGroup *result = group->Resolve(object); + profiler->EndResolve(result); + return result; + } else { + profiler->RecursiveResolve(); + return group->Resolve(object); } - return group->Resolve(object); } RealSpriteGroup::~RealSpriteGroup() diff --git a/src/newgrf_spritegroup.h b/src/newgrf_spritegroup.h --- a/src/newgrf_spritegroup.h +++ b/src/newgrf_spritegroup.h @@ -56,13 +56,14 @@ extern SpriteGroupPool _spritegroup_pool /* Common wrapper for all the different sprite group types */ struct SpriteGroup : SpriteGroupPool::PoolItem<&_spritegroup_pool> { protected: - SpriteGroup(SpriteGroupType type) : type(type) {} + SpriteGroup(SpriteGroupType type) : nfo_line(0), type(type) {} /** Base sprite group resolver */ virtual const SpriteGroup *Resolve(ResolverObject &object) const { return this; }; public: virtual ~SpriteGroup() {} + uint32 nfo_line; SpriteGroupType type; virtual SpriteID GetResult() const { return 0; } @@ -398,6 +399,18 @@ struct ResolverObject { this->used_triggers = 0; memset(this->reseed, 0, sizeof(this->reseed)); } + + /** + * Get the feature number being resolved for. + * This function is mainly intended for the callback profiling feature. + */ + virtual GrfSpecFeature GetFeature() const { return GSF_INVALID; } + /** + * Get an identifier for the item being resolved. + * This function is mainly intended for the callback profiling feature, + * and should return an identifier recognisable by the NewGRF developer. + */ + virtual uint32 GetDebugID() const { return 0; } }; #endif /* NEWGRF_SPRITEGROUP_H */ diff --git a/src/newgrf_station.cpp b/src/newgrf_station.cpp --- a/src/newgrf_station.cpp +++ b/src/newgrf_station.cpp @@ -527,6 +527,16 @@ uint32 Waypoint::GetNewGRFVariable(const return group->loading[0]; } +GrfSpecFeature StationResolverObject::GetFeature() const +{ + return GSF_STATIONS; +} + +uint32 StationResolverObject::GetDebugID() const +{ + return this->station_scope.statspec->grf_prop.local_id; +} + /** * Resolver for stations. * @param statspec Station (type) specification. diff --git a/src/newgrf_station.h b/src/newgrf_station.h --- a/src/newgrf_station.h +++ b/src/newgrf_station.h @@ -75,6 +75,9 @@ struct StationResolverObject : public Re } const SpriteGroup *ResolveReal(const RealSpriteGroup *group) const override; + + GrfSpecFeature GetFeature() const override; + uint32 GetDebugID() const override; }; enum StationClassID : byte { diff --git a/src/spritecache.cpp b/src/spritecache.cpp --- a/src/spritecache.cpp +++ b/src/spritecache.cpp @@ -146,6 +146,17 @@ uint GetOriginFileSlot(SpriteID sprite) } /** + * Get the GRF-local sprite id of a given sprite. + * @param sprite The sprite to look at. + * @return The GRF-local sprite id. + */ +uint32 GetSpriteLocalID(SpriteID sprite) +{ + if (!SpriteExists(sprite)) return 0; + return GetSpriteCache(sprite)->id; +} + +/** * Count the sprites which originate from a specific file slot in a range of SpriteIDs. * @param file_slot FIOS file slot. * @param begin First sprite in range. diff --git a/src/spritecache.h b/src/spritecache.h --- a/src/spritecache.h +++ b/src/spritecache.h @@ -30,6 +30,7 @@ bool SpriteExists(SpriteID sprite); SpriteType GetSpriteType(SpriteID sprite); uint GetOriginFileSlot(SpriteID sprite); +uint32 GetSpriteLocalID(SpriteID sprite); uint GetSpriteCountForSlot(uint file_slot, SpriteID begin, SpriteID end); uint GetMaxSpriteID();