From 30b94bb8ab0b51e1ba20b319ed46f43ee2147cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gilles=20Roudi=C3=A8re?= Date: Mon, 18 Sep 2023 16:30:06 +0200 Subject: [PATCH] Improve TileMap Y-sorting performance --- core/templates/self_list.h | 51 ++++++++++++ doc/classes/TileMap.xml | 4 +- scene/2d/tile_map.cpp | 158 +++++++++++++++++++++++-------------- scene/2d/tile_map.h | 16 +++- 4 files changed, 166 insertions(+), 63 deletions(-) diff --git a/core/templates/self_list.h b/core/templates/self_list.h index ff6fa953ae0..fdf91beacca 100644 --- a/core/templates/self_list.h +++ b/core/templates/self_list.h @@ -105,6 +105,57 @@ public: } } + void sort() { + sort_custom>(); + } + + template + void sort_custom() { + if (_first == _last) { + return; + } + + SelfList *from = _first; + SelfList *current = from; + SelfList *to = from; + + while (current) { + SelfList *next = current->_next; + + if (from != current) { + current->_prev = nullptr; + current->_next = from; + + SelfList *find = from; + C less; + while (find && less(*find->_self, *current->_self)) { + current->_prev = find; + current->_next = find->_next; + find = find->_next; + } + + if (current->_prev) { + current->_prev->_next = current; + } else { + from = current; + } + + if (current->_next) { + current->_next->_prev = current; + } else { + to = current; + } + } else { + current->_prev = nullptr; + current->_next = nullptr; + } + + current = next; + } + _first = from; + _last = to; + } + _FORCE_INLINE_ SelfList *first() { return _first; } _FORCE_INLINE_ const SelfList *first() const { return _first; } diff --git a/doc/classes/TileMap.xml b/doc/classes/TileMap.xml index d699121557d..3e071cb744e 100644 --- a/doc/classes/TileMap.xml +++ b/doc/classes/TileMap.xml @@ -469,7 +469,9 @@ Show or hide the TileMap's navigation meshes. If set to [constant VISIBILITY_MODE_DEFAULT], this depends on the show navigation debug settings. - The TileMap's quadrant size. Optimizes drawing by batching, using chunks of this size. + The TileMap's quadrant size. A quadrant is a group of tiles to be drawn together on a single canvas item, for optimization purposes. [member rendering_quadrant_size] defines the length of a square's side, in the map's coordinate system, that forms the quadrant. Thus, the default quandrant size groups together [code]16 * 16 = 256[/code] tiles. + The quadrant size does not apply on Y-sorted layers, as tiles are be grouped by Y position instead in that case. + [b]Note:[/b] As quadrants are created according to the map's coordinate system, the quadrant's "square shape" might not look like square in the TileMap's local coordinate system. The assigned [TileSet]. diff --git a/scene/2d/tile_map.cpp b/scene/2d/tile_map.cpp index 0c1aab6198b..cbcd136438c 100644 --- a/scene/2d/tile_map.cpp +++ b/scene/2d/tile_map.cpp @@ -178,15 +178,6 @@ void TileMapLayer::_debug_quadrants_update_cell(CellData &r_cell_data, SelfList< #endif // DEBUG_ENABLED /////////////////////////////// Rendering ////////////////////////////////////// -Vector2i TileMapLayer::_coords_to_rendering_quadrant_coords(const Vector2i &p_coords) const { - int quad_size = get_effective_quadrant_size(); - - // Rounding down, instead of simply rounding towards zero (truncating). - return Vector2i( - p_coords.x > 0 ? p_coords.x / quad_size : (p_coords.x - (quad_size - 1)) / quad_size, - p_coords.y > 0 ? p_coords.y / quad_size : (p_coords.y - (quad_size - 1)) / quad_size); -} - void TileMapLayer::_rendering_update() { const Ref &tile_set = tile_map_node->get_tileset(); RenderingServer *rs = RenderingServer::get_singleton(); @@ -239,8 +230,11 @@ void TileMapLayer::_rendering_update() { // Check if anything changed that might change the quadrant shape. // If so, recreate everything. - if (forced_cleanup || dirty.flags[DIRTY_FLAGS_LAYER_Y_SORT_ENABLED] || dirty.flags[DIRTY_FLAGS_TILE_MAP_QUADRANT_SIZE]) { - // Free all quadrants. + bool quandrant_shape_changed = dirty.flags[DIRTY_FLAGS_TILE_MAP_QUADRANT_SIZE] || + (tile_map_node->is_y_sort_enabled() && y_sort_enabled && (dirty.flags[DIRTY_FLAGS_LAYER_Y_SORT_ENABLED] || dirty.flags[DIRTY_FLAGS_LAYER_Y_SORT_ORIGIN] || dirty.flags[DIRTY_FLAGS_TILE_MAP_Y_SORT_ENABLED] || dirty.flags[DIRTY_FLAGS_TILE_MAP_LOCAL_XFORM] || dirty.flags[DIRTY_FLAGS_TILE_MAP_TILE_SET])); + + // Free all quadrants. + if (forced_cleanup || quandrant_shape_changed) { for (const KeyValue> &kv : rendering_quadrant_map) { for (int i = 0; i < kv.value->canvas_items.size(); i++) { const RID &ci = kv.value->canvas_items[i]; @@ -295,6 +289,14 @@ void TileMapLayer::_rendering_update() { } rendering_quadrant->canvas_items.clear(); + // Sort the quadrant cells. + if (tile_map_node->is_y_sort_enabled() && is_y_sort_enabled()) { + // For compatibility reasons, we use another comparator for Y-sorted layers. + rendering_quadrant->cells.sort_custom(); + } else { + rendering_quadrant->cells.sort(); + } + // Those allow to group cell per material or z-index. Ref prev_material; int prev_z_index = 0; @@ -317,12 +319,6 @@ void TileMapLayer::_rendering_update() { int tile_z_index = tile_data->get_z_index(); // Quandrant pos. - Vector2i quadrant_coords = _coords_to_rendering_quadrant_coords(cell_data.coords); - Vector2 ci_position = tile_map_node->map_to_local(quadrant_coords * get_effective_quadrant_size()); - if (tile_map_node->is_y_sort_enabled() && y_sort_enabled) { - // When Y-sorting, the quandrant size is sure to be 1, we can thus offset the CanvasItem. - ci_position.y += y_sort_origin + tile_data->get_y_sort_origin(); - } // --- CanvasItems --- RID ci; @@ -337,7 +333,7 @@ void TileMapLayer::_rendering_update() { rs->canvas_item_set_parent(ci, canvas_item); rs->canvas_item_set_use_parent_material(ci, tile_map_node->get_use_parent_material() || tile_map_node->get_material().is_valid()); - Transform2D xform(0, ci_position); + Transform2D xform(0, rendering_quadrant->canvas_items_position); rs->canvas_item_set_transform(ci, xform); rs->canvas_item_set_light_mask(ci, tile_map_node->get_light_mask()); @@ -370,7 +366,7 @@ void TileMapLayer::_rendering_update() { } // Drawing the tile in the canvas item. - tile_map_node->draw_tile(ci, local_tile_pos - ci_position, tile_set, cell_data.cell.source_id, cell_data.cell.get_atlas_coords(), cell_data.cell.alternative_tile, -1, tile_map_node->get_self_modulate(), tile_data, random_animation_offset); + tile_map_node->draw_tile(ci, local_tile_pos - rendering_quadrant->canvas_items_position, tile_set, cell_data.cell.source_id, cell_data.cell.get_atlas_coords(), cell_data.cell.alternative_tile, -1, tile_map_node->get_self_modulate(), tile_data, random_animation_offset); } } else { // Free the quadrant. @@ -465,48 +461,85 @@ void TileMapLayer::_rendering_update() { void TileMapLayer::_rendering_quadrants_update_cell(CellData &r_cell_data, SelfList::List &r_dirty_rendering_quadrant_list) { const Ref &tile_set = tile_map_node->get_tileset(); - Vector2i quadrant_coords = _coords_to_rendering_quadrant_coords(r_cell_data.coords); - if (rendering_quadrant_map.has(quadrant_coords)) { - // Mark the quadrant as dirty. - Ref &rendering_quadrant = rendering_quadrant_map[quadrant_coords]; - if (!rendering_quadrant->dirty_quadrant_list_element.in_list()) { - r_dirty_rendering_quadrant_list.add(&rendering_quadrant->dirty_quadrant_list_element); - } - } else { - // Create a new quadrant and add it to the quadrant lists. - Ref new_quadrant; - new_quadrant.instantiate(); - new_quadrant->quadrant_coords = quadrant_coords; - rendering_quadrant_map[quadrant_coords] = new_quadrant; - } - - // Check if the cell is valid. + // Check if the cell is valid and retrieve its y_sort_origin. bool is_valid = false; + int tile_y_sort_origin = 0; TileSetSource *source; if (tile_set->has_source(r_cell_data.cell.source_id)) { source = *tile_set->get_source(r_cell_data.cell.source_id); TileSetAtlasSource *atlas_source = Object::cast_to(source); if (atlas_source && atlas_source->has_tile(r_cell_data.cell.get_atlas_coords()) && atlas_source->has_alternative_tile(r_cell_data.cell.get_atlas_coords(), r_cell_data.cell.alternative_tile)) { is_valid = true; + tile_y_sort_origin = atlas_source->get_tile_data(r_cell_data.cell.get_atlas_coords(), r_cell_data.cell.alternative_tile)->get_y_sort_origin(); } } - // Add/Remove the cell to/from its quadrant. - Ref &rendering_quadrant = rendering_quadrant_map[quadrant_coords]; - if (r_cell_data.rendering_quadrant_list_element.in_list()) { - if (!is_valid) { + if (is_valid) { + // Get the quadrant coords. + Vector2 canvas_items_position; + Vector2i quadrant_coords; + if (tile_map_node->is_y_sort_enabled() && is_y_sort_enabled()) { + canvas_items_position = Vector2(0, tile_map_node->map_to_local(r_cell_data.coords).y + tile_y_sort_origin + y_sort_origin); + quadrant_coords = canvas_items_position * 100; + } else { + int quad_size = tile_map_node->get_rendering_quadrant_size(); + const Vector2i &coords = r_cell_data.coords; + + // Rounding down, instead of simply rounding towards zero (truncating). + quadrant_coords = Vector2i( + coords.x > 0 ? coords.x / quad_size : (coords.x - (quad_size - 1)) / quad_size, + coords.y > 0 ? coords.y / quad_size : (coords.y - (quad_size - 1)) / quad_size); + canvas_items_position = quad_size * quadrant_coords; + } + + Ref rendering_quadrant; + if (rendering_quadrant_map.has(quadrant_coords)) { + // Reuse existing rendering quadrant. + rendering_quadrant = rendering_quadrant_map[quadrant_coords]; + } else { + // Create a new rendering quadrant. + rendering_quadrant.instantiate(); + rendering_quadrant->quadrant_coords = quadrant_coords; + rendering_quadrant->canvas_items_position = canvas_items_position; + rendering_quadrant_map[quadrant_coords] = rendering_quadrant; + } + + // Mark the old quadrant as dirty (if it exists). + if (r_cell_data.rendering_quadrant.is_valid()) { + if (!r_cell_data.rendering_quadrant->dirty_quadrant_list_element.in_list()) { + r_dirty_rendering_quadrant_list.add(&r_cell_data.rendering_quadrant->dirty_quadrant_list_element); + } + } + + // Remove the cell from that quadrant. + if (r_cell_data.rendering_quadrant_list_element.in_list()) { r_cell_data.rendering_quadrant_list_element.remove_from_list(); } - } else { - if (is_valid) { - rendering_quadrant->cells.add(&r_cell_data.rendering_quadrant_list_element); - } - } - // Add the quadrant to the dirty quadrant list. - if (!rendering_quadrant->dirty_quadrant_list_element.in_list()) { - r_dirty_rendering_quadrant_list.add(&rendering_quadrant->dirty_quadrant_list_element); + // Add the cell to its new quadrant. + r_cell_data.rendering_quadrant = rendering_quadrant; + r_cell_data.rendering_quadrant->cells.add(&r_cell_data.rendering_quadrant_list_element); + + // Add the new quadrant to the dirty quadrant list. + if (!rendering_quadrant->dirty_quadrant_list_element.in_list()) { + r_dirty_rendering_quadrant_list.add(&rendering_quadrant->dirty_quadrant_list_element); + } + } else { + Ref rendering_quadrant = r_cell_data.rendering_quadrant; + + // Remove the cell from its quadrant. + r_cell_data.rendering_quadrant = Ref(); + if (r_cell_data.rendering_quadrant_list_element.in_list()) { + rendering_quadrant->cells.remove(&r_cell_data.rendering_quadrant_list_element); + } + + if (rendering_quadrant.is_valid()) { + // Add the quadrant to the dirty quadrant list. + if (!rendering_quadrant->dirty_quadrant_list_element.in_list()) { + r_dirty_rendering_quadrant_list.add(&rendering_quadrant->dirty_quadrant_list_element); + } + } } } @@ -1831,15 +1864,6 @@ TileMapCell TileMapLayer::get_cell(const Vector2i &p_coords, bool p_use_proxies) } } -int TileMapLayer::get_effective_quadrant_size() const { - // When using YSort, the quadrant size is reduced to 1 to have one CanvasItem per quadrant. - if (tile_map_node->is_y_sort_enabled() && is_y_sort_enabled()) { - return 1; - } else { - return tile_map_node->get_rendering_quadrant_size(); - } -} - void TileMapLayer::set_tile_data(TileMapLayer::DataFormat p_format, const Vector &p_data) { ERR_FAIL_COND(p_format > TileMapLayer::FORMAT_3); @@ -2949,7 +2973,7 @@ void TileMap::_notification(int p_what) { last_valid_transform = new_transform; set_notify_local_transform(false); set_global_transform(new_transform); - set_notify_local_transform(true); + _update_notify_local_transform(); } } break; @@ -2979,7 +3003,7 @@ void TileMap::_notification(int p_what) { // ... but then revert changes. set_notify_local_transform(false); set_global_transform(last_valid_transform); - set_notify_local_transform(true); + _update_notify_local_transform(); } } break; } @@ -3233,6 +3257,7 @@ Color TileMap::get_layer_modulate(int p_layer) const { void TileMap::set_layer_y_sort_enabled(int p_layer, bool p_y_sort_enabled) { TILEMAP_CALL_FOR_LAYER(p_layer, set_y_sort_enabled, p_y_sort_enabled); + _update_notify_local_transform(); } bool TileMap::is_layer_y_sort_enabled(int p_layer) const { @@ -3268,7 +3293,7 @@ void TileMap::set_collision_animatable(bool p_enabled) { return; } collision_animatable = p_enabled; - set_notify_local_transform(p_enabled); + _update_notify_local_transform(); set_physics_process_internal(p_enabled); for (Ref &layer : layers) { layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_COLLISION_ANIMATABLE); @@ -4622,9 +4647,22 @@ void TileMap::_tile_set_changed() { update_configuration_warnings(); } +void TileMap::_update_notify_local_transform() { + bool notify = collision_animatable || is_y_sort_enabled(); + if (!notify) { + for (const Ref &layer : layers) { + if (layer->is_y_sort_enabled()) { + notify = true; + break; + } + } + } + set_notify_local_transform(notify); +} + TileMap::TileMap() { set_notify_transform(true); - set_notify_local_transform(false); + _update_notify_local_transform(); Ref new_layer; new_layer.instantiate(); diff --git a/scene/2d/tile_map.h b/scene/2d/tile_map.h index f804f808cb8..f98fd77a5ca 100644 --- a/scene/2d/tile_map.h +++ b/scene/2d/tile_map.h @@ -121,6 +121,10 @@ struct CellData { // List elements. SelfList dirty_list_element; + bool operator<(const CellData &p_other) const { + return coords < p_other.coords; + } + // For those, copy everything but SelfList elements. void operator=(const CellData &p_other) { coords = p_other.coords; @@ -152,6 +156,13 @@ struct CellData { } }; +// For compatibility reasons, we use another comparator for Y-sorted layers. +struct CellDataYSortedComparator { + _FORCE_INLINE_ bool operator()(const CellData &p_a, const CellData &p_b) const { + return p_a.coords.x == p_b.coords.x ? (p_a.coords.y < p_b.coords.y) : (p_a.coords.x > p_b.coords.x); + } +}; + #ifdef DEBUG_ENABLED class DebugQuadrant : public RefCounted { GDCLASS(DebugQuadrant, RefCounted); @@ -199,6 +210,7 @@ public: Vector2i quadrant_coords; SelfList::List cells; List canvas_items; + Vector2 canvas_items_position; SelfList dirty_quadrant_list_element; @@ -304,7 +316,6 @@ private: #endif // DEBUG_ENABLED HashMap> rendering_quadrant_map; - Vector2i _coords_to_rendering_quadrant_coords(const Vector2i &p_coords) const; bool _rendering_was_cleaned_up = false; void _rendering_update(); void _rendering_quadrants_update_cell(CellData &r_cell_data, SelfList::List &r_dirty_rendering_quadrant_list); @@ -361,7 +372,6 @@ public: // Not exposed to users. TileMapCell get_cell(const Vector2i &p_coords, bool p_use_proxies = false) const; - int get_effective_quadrant_size() const; // For TileMap node's use. void set_tile_data(DataFormat p_format, const Vector &p_data); @@ -455,6 +465,8 @@ private: void _tile_set_changed(); + void _update_notify_local_transform(); + // Polygons. HashMap, int>, Ref, PairHash, int>> polygon_cache; PackedVector2Array _get_transformed_vertices(const PackedVector2Array &p_vertices, int p_alternative_id);