diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 4f182d132..1cba8284e 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -2594,11 +2594,18 @@ invalidates the ``texpos`` value that used to point to that texture. The ``textures`` module solves this problem by providing a stable handle instead of a raw ``texpos``. When we need to draw a particular tile, we can look up the current ``texpos`` value via the handle. +Texture module can register textures in two ways: to reserved and dynamic ranges. +Reserved range is a limit buffer in a game texture vector, that will never be wiped. +It is good for static assets, which need to be loaded at the very beginning and will be used during the process running. +In other cases, it is better to use dynamic range. +If reserved range buffer limit has been reached, dynamic range will be used by default. -* ``loadTileset(file, tile_px_w, tile_px_h)`` +* ``loadTileset(file, tile_px_w, tile_px_h[, reserved])`` Loads a tileset from the image ``file`` with give tile dimensions in pixels. The image will be sliced in row major order. Returns an array of ``TexposHandle``. + ``reserved`` is optional boolean argument, which indicates texpos range. + ``true`` - reserved, ``false`` - dynamic (default). Example usage:: @@ -2611,18 +2618,22 @@ raw ``texpos``. When we need to draw a particular tile, we can look up the curre get the ``texpos`` for your texture. ``texpos`` can change when game textures are reset, but the handle will be the same. -* ``createTile(pixels, tile_px_w, tile_px_h)`` +* ``createTile(pixels, tile_px_w, tile_px_h[, reserved])`` Create and register a new texture with the given tile dimensions and an array of ``pixels`` in row major order. Each pixel is an integer representing color in packed RBGA format (for example, #0022FF11). Returns a ``TexposHandle``. + ``reserved`` is optional boolean argument, which indicates texpos range. + ``true`` - reserved, ``false`` - dynamic (default). -* ``createTileset(pixels, texture_px_w, texture_px_h, tile_px_w, tile_px_h)`` +* ``createTileset(pixels, texture_px_w, texture_px_h, tile_px_w, tile_px_h[, reserved])`` Create and register a new texture with the given texture dimensions and an array of ``pixels`` in row major order. Then slice it into tiles with the given tile dimensions. Each pixel is an integer representing color in packed RBGA format (for example #0022FF11). Returns an array of ``TexposHandle``. + ``reserved`` is optional boolean argument, which indicates texpos range. + ``true`` - reserved, ``false`` - dynamic (default). * ``deleteHandle(handle)`` diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index f792cff90..5faec6d88 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1760,7 +1760,8 @@ static int textures_loadTileset(lua_State *state) std::string file = luaL_checkstring(state, 1); auto tile_w = luaL_checkint(state, 2); auto tile_h = luaL_checkint(state, 3); - auto handles = Textures::loadTileset(file, tile_w, tile_h); + bool reserved = lua_isboolean(state, 4) ? lua_toboolean(state, 4) : false; + auto handles = Textures::loadTileset(file, tile_w, tile_h, reserved); Lua::PushVector(state, handles); return 1; } @@ -1798,7 +1799,8 @@ static int textures_createTile(lua_State *state) Lua::GetVector(state, pixels); auto tile_w = luaL_checkint(state, 2); auto tile_h = luaL_checkint(state, 3); - auto handle = Textures::createTile(pixels, tile_w, tile_h); + bool reserved = lua_isboolean(state, 4) ? lua_toboolean(state, 4) : false; + auto handle = Textures::createTile(pixels, tile_w, tile_h, reserved); Lua::Push(state, handle); return 1; } @@ -1811,7 +1813,8 @@ static int textures_createTileset(lua_State *state) auto texture_h = luaL_checkint(state, 3); auto tile_w = luaL_checkint(state, 4); auto tile_h = luaL_checkint(state, 5); - auto handles = Textures::createTileset(pixels, texture_w, texture_h, tile_w, tile_h); + bool reserved = lua_isboolean(state, 6) ? lua_toboolean(state, 6) : false; + auto handles = Textures::createTileset(pixels, texture_w, texture_h, tile_w, tile_h, reserved); Lua::PushVector(state, handles); return 1; } diff --git a/library/include/modules/Textures.h b/library/include/modules/Textures.h index 5b238a4fd..b820c3332 100644 --- a/library/include/modules/Textures.h +++ b/library/include/modules/Textures.h @@ -26,7 +26,7 @@ const uint32_t TILE_HEIGHT_PX = 12; * Load texture and get handle. * Keep it to obtain valid texpos. */ -DFHACK_EXPORT TexposHandle loadTexture(SDL_Surface* surface); +DFHACK_EXPORT TexposHandle loadTexture(SDL_Surface* surface, bool reserved = false); /** * Load tileset from image file. @@ -34,7 +34,8 @@ DFHACK_EXPORT TexposHandle loadTexture(SDL_Surface* surface); */ DFHACK_EXPORT std::vector loadTileset(const std::string& file, int tile_px_w = TILE_WIDTH_PX, - int tile_px_h = TILE_HEIGHT_PX); + int tile_px_h = TILE_HEIGHT_PX, + bool reserved = false); /** * Get texpos by handle. @@ -53,7 +54,7 @@ DFHACK_EXPORT void deleteHandle(TexposHandle handle); * Register this texture and return TexposHandle. */ DFHACK_EXPORT TexposHandle createTile(std::vector& pixels, int tile_px_w = TILE_WIDTH_PX, - int tile_px_h = TILE_HEIGHT_PX); + int tile_px_h = TILE_HEIGHT_PX, bool reserved = false); /** * Create new textures as tileset with RGBA32 format and pixels as data in row major order. @@ -62,7 +63,8 @@ DFHACK_EXPORT TexposHandle createTile(std::vector& pixels, int tile_px DFHACK_EXPORT std::vector createTileset(std::vector& pixels, int texture_px_w, int texture_px_h, int tile_px_w = TILE_WIDTH_PX, - int tile_px_h = TILE_HEIGHT_PX); + int tile_px_h = TILE_HEIGHT_PX, + bool reserved = false); /** * Call this on DFHack init just once to setup interposed handlers and diff --git a/library/lua/gui/textures.lua b/library/lua/gui/textures.lua index 6557b6e02..6dac234e0 100644 --- a/library/lua/gui/textures.lua +++ b/library/lua/gui/textures.lua @@ -6,18 +6,18 @@ local _ENV = mkmodule('gui.textures') -- Preloaded DFHack Assets. -- Use this handles if you need to get dfhack standard textures. ----@type table +---@type table local texpos_handles = { - green_pin = dfhack.textures.loadTileset('hack/data/art/green-pin.png', 8, 12), - red_pin = dfhack.textures.loadTileset('hack/data/art/red-pin.png', 8, 12), - icons = dfhack.textures.loadTileset('hack/data/art/icons.png', 8, 12), - on_off = dfhack.textures.loadTileset('hack/data/art/on-off.png', 8, 12), - control_panel = dfhack.textures.loadTileset('hack/data/art/control-panel.png', 8, 12), - border_thin = dfhack.textures.loadTileset('hack/data/art/border-thin.png', 8, 12), - border_medium = dfhack.textures.loadTileset('hack/data/art/border-medium.png', 8, 12), - border_bold = dfhack.textures.loadTileset('hack/data/art/border-bold.png', 8, 12), - border_panel = dfhack.textures.loadTileset('hack/data/art/border-panel.png', 8, 12), - border_window = dfhack.textures.loadTileset('hack/data/art/border-window.png', 8, 12), + green_pin = dfhack.textures.loadTileset('hack/data/art/green-pin.png', 8, 12, true), + red_pin = dfhack.textures.loadTileset('hack/data/art/red-pin.png', 8, 12, true), + icons = dfhack.textures.loadTileset('hack/data/art/icons.png', 8, 12, true), + on_off = dfhack.textures.loadTileset('hack/data/art/on-off.png', 8, 12, true), + control_panel = dfhack.textures.loadTileset('hack/data/art/control-panel.png', 8, 12, true), + border_thin = dfhack.textures.loadTileset('hack/data/art/border-thin.png', 8, 12, true), + border_medium = dfhack.textures.loadTileset('hack/data/art/border-medium.png', 8, 12, true), + border_bold = dfhack.textures.loadTileset('hack/data/art/border-bold.png', 8, 12, true), + border_panel = dfhack.textures.loadTileset('hack/data/art/border-panel.png', 8, 12, true), + border_window = dfhack.textures.loadTileset('hack/data/art/border-window.png', 8, 12, true), } -- Get valid texpos for preloaded texture in tileset diff --git a/library/modules/Textures.cpp b/library/modules/Textures.cpp index c957bd878..8b485a3a6 100644 --- a/library/modules/Textures.cpp +++ b/library/modules/Textures.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -28,9 +30,35 @@ namespace DFHack { DBG_DECLARE(core, textures, DebugCategory::LINFO); } +struct ReservedRange { + void init(int32_t start) { + this->start = start; + this->end = start + ReservedRange::size; + this->current = start; + this->is_installed = true; + } + long get_new_texpos() { + if (this->current == this->end) + return -1; + return this->current++; + } + + static const int32_t size = 10000; // size of reserved texpos buffer + int32_t start = -1; + int32_t end = -1; + long current = -1; + bool is_installed = false; +}; + +static ReservedRange reserved_range{}; static std::unordered_map g_handle_to_texpos; +static std::unordered_map g_handle_to_reserved_texpos; static std::unordered_map g_handle_to_surface; +static std::unordered_map> g_tileset_to_handles; +static std::vector g_delayed_regs; static std::mutex g_adding_mutex; +static std::atomic loading_state = false; +static SDL_Surface* dummy_surface = NULL; // Converts an arbitrary Surface to something like the display format // (32-bit RGBA), and converts magenta to transparency if convert_magenta is set @@ -71,6 +99,12 @@ static long add_texture(SDL_Surface* surface) { return texpos; } +// register surface in texture raws to specific texpos +static void insert_texture(SDL_Surface* surface, long texpos) { + std::lock_guard lg_add_texture(g_adding_mutex); + enabler->textures.raws[texpos] = surface; +} + // delete surface from texture raws static void delete_texture(long texpos) { std::lock_guard lg_add_texture(g_adding_mutex); @@ -94,7 +128,8 @@ SDL_Surface* create_texture(std::vector& pixels, int texture_px_w, int // convert single surface into tiles according w/h // register tiles in texture raws and return handles -std::vector slice_tileset(SDL_Surface* surface, int tile_px_w, int tile_px_h) { +std::vector slice_tileset(SDL_Surface* surface, int tile_px_w, int tile_px_h, + bool reserved) { std::vector handles{}; if (!surface) return handles; @@ -102,6 +137,12 @@ std::vector slice_tileset(SDL_Surface* surface, int tile_px_w, int int dimx = surface->w / tile_px_w; int dimy = surface->h / tile_px_h; + if (reserved && (dimx * dimy > reserved_range.end - reserved_range.current)) { + WARN(textures).print( + "there is not enough space in reserved range for whole tileset, using dynamic range\n"); + reserved = false; + } + for (int y = 0; y < dimy; y++) { for (int x = 0; x < dimx; x++) { SDL_Surface* tile = DFSDL_CreateRGBSurface( @@ -109,7 +150,7 @@ std::vector slice_tileset(SDL_Surface* surface, int tile_px_w, int surface->format->Bmask, surface->format->Amask); SDL_Rect vp{tile_px_w * x, tile_px_h * y, tile_px_w, tile_px_h}; DFSDL_UpperBlit(surface, &vp, tile, NULL); - auto handle = Textures::loadTexture(tile); + auto handle = Textures::loadTexture(tile, reserved); handles.push_back(handle); } } @@ -118,20 +159,46 @@ std::vector slice_tileset(SDL_Surface* surface, int tile_px_w, int return handles; } -TexposHandle Textures::loadTexture(SDL_Surface* surface) { +TexposHandle Textures::loadTexture(SDL_Surface* surface, bool reserved) { if (!surface || !enabler) return 0; // should be some error, i guess + if (loading_state) + reserved = true; // use reserved range during loading for all textures auto handle = reinterpret_cast(surface); g_handle_to_surface.emplace(handle, surface); surface->refcount++; // prevent destruct on next FreeSurface by game - auto texpos = add_texture(surface); - g_handle_to_texpos.emplace(handle, texpos); + + if (reserved && reserved_range.is_installed) { + auto texpos = reserved_range.get_new_texpos(); + if (texpos != -1) { + insert_texture(surface, texpos); + g_handle_to_reserved_texpos.emplace(handle, texpos); + dummy_surface->refcount--; + return handle; + } + + if (loading_state) { // if we in loading state and reserved range is full -> error + ERR(textures).printerr("reserved range limit has been reached, use dynamic range\n"); + return 0; + } + } + + // if we here in loading state = true, then it should be dynamic range -> delay reg + if (loading_state) { + g_delayed_regs.push_back(handle); + } else { + auto texpos = add_texture(surface); + g_handle_to_texpos.emplace(handle, texpos); + } + return handle; } std::vector Textures::loadTileset(const std::string& file, int tile_px_w, - int tile_px_h) { + int tile_px_h, bool reserved) { + if (g_tileset_to_handles.contains(file)) + return g_tileset_to_handles[file]; if (!enabler) return std::vector{}; @@ -142,9 +209,10 @@ std::vector Textures::loadTileset(const std::string& file, int til } surface = canonicalize_format(surface); - auto handles = slice_tileset(surface, tile_px_w, tile_px_h); + auto handles = slice_tileset(surface, tile_px_w, tile_px_h, reserved); DEBUG(textures).print("loaded %zd textures from '%s'\n", handles.size(), file.c_str()); + g_tileset_to_handles[file] = handles; return handles; } @@ -153,11 +221,18 @@ long Textures::getTexposByHandle(TexposHandle handle) { if (!handle || !enabler) return -1; + if (g_handle_to_reserved_texpos.contains(handle)) + return g_handle_to_reserved_texpos[handle]; if (g_handle_to_texpos.contains(handle)) return g_handle_to_texpos[handle]; - + if (std::find(g_delayed_regs.begin(), g_delayed_regs.end(), handle) != g_delayed_regs.end()) + return 0; if (g_handle_to_surface.contains(handle)) { g_handle_to_surface[handle]->refcount++; // prevent destruct on next FreeSurface by game + if (loading_state) { // reinit dor dynamic range during loading -> delayed + g_delayed_regs.push_back(handle); + return 0; + } auto texpos = add_texture(g_handle_to_surface[handle]); g_handle_to_texpos.emplace(handle, texpos); return texpos; @@ -166,22 +241,24 @@ long Textures::getTexposByHandle(TexposHandle handle) { return -1; } -TexposHandle Textures::createTile(std::vector& pixels, int tile_px_w, int tile_px_h) { +TexposHandle Textures::createTile(std::vector& pixels, int tile_px_w, int tile_px_h, + bool reserved) { if (!enabler) return 0; auto texture = create_texture(pixels, tile_px_w, tile_px_h); - auto handle = Textures::loadTexture(texture); + auto handle = Textures::loadTexture(texture, reserved); return handle; } std::vector Textures::createTileset(std::vector& pixels, int texture_px_w, - int texture_px_h, int tile_px_w, int tile_px_h) { + int texture_px_h, int tile_px_w, int tile_px_h, + bool reserved) { if (!enabler) return std::vector{}; auto texture = create_texture(pixels, texture_px_w, texture_px_h); - auto handles = slice_tileset(texture, tile_px_w, tile_px_h); + auto handles = slice_tileset(texture, tile_px_w, tile_px_h, reserved); return handles; } @@ -192,8 +269,13 @@ void Textures::deleteHandle(TexposHandle handle) { auto texpos = Textures::getTexposByHandle(handle); if (texpos > 0) delete_texture(texpos); + if (g_handle_to_reserved_texpos.contains(handle)) + g_handle_to_reserved_texpos.erase(handle); if (g_handle_to_texpos.contains(handle)) g_handle_to_texpos.erase(handle); + if (auto it = std::find(g_delayed_regs.begin(), g_delayed_regs.end(), handle); + it != g_delayed_regs.end()) + g_delayed_regs.erase(it); if (g_handle_to_surface.contains(handle)) { auto surface = g_handle_to_surface[handle]; while (surface->refcount) @@ -207,20 +289,45 @@ static void reset_texpos() { g_handle_to_texpos.clear(); } +static void reset_reserved_texpos() { + DEBUG(textures).print("resetting reserved texture mappings\n"); + g_handle_to_reserved_texpos.clear(); +} + +static void reset_tilesets() { + DEBUG(textures).print("resetting tileset to handle mappings\n"); + g_tileset_to_handles.clear(); +} + static void reset_surface() { + DEBUG(textures).print("deleting cached surfaces\n"); for (auto& entry : g_handle_to_surface) { DFSDL_FreeSurface(entry.second); } g_handle_to_surface.clear(); } +static void register_delayed_handles() { + DEBUG(textures).print("register delayed handles, size %zd\n", g_delayed_regs.size()); + for (auto& handle : g_delayed_regs) { + auto texpos = add_texture(g_handle_to_surface[handle]); + g_handle_to_texpos.emplace(handle, texpos); + } + g_delayed_regs.clear(); +} + // reset point on New Game struct tracking_stage_new_region : df::viewscreen_new_regionst { typedef df::viewscreen_new_regionst interpose_base; DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_raw_load_stage != this->raw_load_stage) { - TRACE(textures).print("raw_load_stage %d -> %d\n", this->m_raw_load_stage, this->raw_load_stage); + TRACE(textures).print("raw_load_stage %d -> %d\n", this->m_raw_load_stage, + this->raw_load_stage); + bool tmp_state = loading_state; + loading_state = this->raw_load_stage >= 0 && this->raw_load_stage < 3 ? true : false; + if (tmp_state != loading_state && !loading_state) + register_delayed_handles(); this->m_raw_load_stage = this->raw_load_stage; if (this->m_raw_load_stage == 1) reset_texpos(); @@ -240,6 +347,10 @@ struct tracking_stage_adopt_region : df::viewscreen_adopt_regionst { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_cur_step != this->cur_step) { TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step); + bool tmp_state = loading_state; + loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; + if (tmp_state != loading_state && !loading_state) + register_delayed_handles(); this->m_cur_step = this->cur_step; if (this->m_cur_step == 1) reset_texpos(); @@ -259,6 +370,10 @@ struct tracking_stage_load_region : df::viewscreen_loadgamest { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_cur_step != this->cur_step) { TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step); + bool tmp_state = loading_state; + loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; + if (tmp_state != loading_state && !loading_state) + register_delayed_handles(); this->m_cur_step = this->cur_step; if (this->m_cur_step == 1) reset_texpos(); @@ -278,6 +393,10 @@ struct tracking_stage_new_arena : df::viewscreen_new_arenast { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_cur_step != this->cur_step) { TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step); + bool tmp_state = loading_state; + loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; + if (tmp_state != loading_state && !loading_state) + register_delayed_handles(); this->m_cur_step = this->cur_step; if (this->m_cur_step == 0) reset_texpos(); @@ -304,12 +423,31 @@ static void uninstall_reset_point() { INTERPOSE_HOOK(tracking_stage_new_arena, logic).remove(); } +static void reserve_static_range() { + if (static_cast(enabler->textures.init_texture_size) != enabler->textures.raws.size()) { + WARN(textures).print( + "reserved range can't be installed! all textures will be loaded to dynamic range!"); + return; + } + reserved_range.init(enabler->textures.init_texture_size); + dummy_surface = + DFSDL_CreateRGBSurfaceWithFormat(0, 0, 0, 32, SDL_PixelFormatEnum::SDL_PIXELFORMAT_RGBA32); + dummy_surface->refcount += ReservedRange::size; + for (int32_t i = 0; i < ReservedRange::size; i++) { + add_texture(dummy_surface); + } + enabler->textures.init_texture_size += ReservedRange::size; +} + void Textures::init(color_ostream& out) { if (!enabler) return; + reserve_static_range(); install_reset_point(); - DEBUG(textures, out).print("dynamic texture loading ready"); + DEBUG(textures, out) + .print("dynamic texture loading ready, reserved range %d-%d\n", reserved_range.start, + reserved_range.end); } void Textures::cleanup() { @@ -317,6 +455,8 @@ void Textures::cleanup() { return; reset_texpos(); + reset_reserved_texpos(); + reset_tilesets(); reset_surface(); uninstall_reset_point(); } diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index fae138353..8eff17aae 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -5,8 +5,8 @@ local helpdb = require('helpdb') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') -local logo_textures = dfhack.textures.loadTileset('hack/data/art/logo.png', 8, 12) -local logo_hovered_textures = dfhack.textures.loadTileset('hack/data/art/logo_hovered.png', 8, 12) +local logo_textures = dfhack.textures.loadTileset('hack/data/art/logo.png', 8, 12, true) +local logo_hovered_textures = dfhack.textures.loadTileset('hack/data/art/logo_hovered.png', 8, 12, true) local function get_command(cmdline) local first_word = cmdline:trim():split(' +')[1] diff --git a/plugins/pathable.cpp b/plugins/pathable.cpp index 12852a894..be6537d32 100644 --- a/plugins/pathable.cpp +++ b/plugins/pathable.cpp @@ -36,7 +36,7 @@ namespace DFHack { static std::vector textures; DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - textures = Textures::loadTileset("hack/data/art/pathable.png", 32, 32); + textures = Textures::loadTileset("hack/data/art/pathable.png", 32, 32, true); return CR_OK; }