Skip to content

Commit

Permalink
fast turf reservation lookups & aligned spatial grid lookups (#6532)
Browse files Browse the repository at this point in the history
# why?

non-player facing

we're going to need fast lookups for "is this thing in range of this
other thing" at some point, for things like telecomms

this current solution consumes about 16KB per reservation level

we also add the capability to use spatial grids to quickly get atoms on
a level.
spatial grids are also at 16KB per grid, per world level

all in all worth it in my opinion.
  • Loading branch information
silicons committed Jun 20, 2024
1 parent 83bc9a7 commit 7b5c263
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 60 deletions.
8 changes: 5 additions & 3 deletions code/__DEFINES/mapping/system.dm
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
#define RESERVED_TURF_TYPE /turf/space/basic
/// reserved area type
#define RESERVED_AREA_TYPE /area/space
/// reservation resolution - this drastically decreases the amount of time used to search for a spot for reservation.
/// set this to a value that's slightly above average of a common multiplier for reservation sizes.
#define RESERVED_TURF_RESOLUTION 8

/// Turf chunk resolution
///
/// * this is the most granular a turf reservation alloc can be (e.g. 8x8 for '8')
/// * this is the resolution of spatial gridmaps. why? this way spatial queries are aligned and super fast.
#define TURF_CHUNK_RESOLUTION 8
5 changes: 5 additions & 0 deletions code/controllers/subsystem/mapping/allocation.dm
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
// todo: recover()
// todo: zclear system will be in this later, for now, this is just a wrapper

/datum/controller/subsystem/mapping/on_max_z_changed(old_z_count, new_z_count)
. = ..()
// just to make sure order of ops / assumptions are right
ASSERT(length(ordered_levels) == world.maxz)

/**
* gets an reusable level, or increments world.maxz
* WARNING: AFTER THIS, YOU NEED TO USE THE LEVEL, OR READD TO REUSABLE, OR THIS IS A MEMORY LEAK!
Expand Down
26 changes: 22 additions & 4 deletions code/controllers/subsystem/mapping/boot.dm
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,31 @@
* so we snowflaked it by compiling a single empty space level
* this is called during initial world loading to expand that space level to the size of the world,
* and init it as our first reserved level.
*
* width and height exist to init the world's dimensions based on the map being loaded
* this must be done before anything like spatial hashes are made, as those depend on world dimensions!
*
* this proc is hilariously, hilariously unstable and changes as backend changes
* why?
*
* because the backend is generally extremely tightly coupled
* as an example, the backend API assumes all level allocs are done through SSmapping,
* so it doesn't even allow for the existnece of an unmanaged level already being there;
* such a thing is impossible outside of severe bugs
*
* so in this proc, we're basically hard-setting variables - with potential issues, because
* this can get desynced with the rest of the subsystem's code - to 'fake' such a proper init cycle.
*
* at some point, SSmapping will be better coded, but for now, it's pretty messy.
*/
/datum/controller/subsystem/mapping/proc/load_server_initial_reservation_area(width, height)
ASSERT(world.maxz == 1)
ASSERT(length(reserve_levels) == 0)
reserved_level_count = 1
reserve_levels = list(1)
world.maxx = width
world.maxy = height
ordered_levels = list(new /datum/map_level/reserved)
ASSERT(length(reserve_levels) == 0)
// basically makes allocate_level() grab the first one
reusable_levels += 1
ordered_levels += null
world.max_z_changed(0, 1)
synchronize_datastructures()
allocate_reserved_level()
45 changes: 24 additions & 21 deletions code/controllers/subsystem/mapping/levels.dm
Original file line number Diff line number Diff line change
Expand Up @@ -233,27 +233,30 @@
SSplanets.legacy_planet_assert(z_index, level_or_path.planet_path)

//! LEGACY
if((level_or_path.flags & LEGACY_LEVEL_STATION) || level_or_path.has_trait(ZTRAIT_STATION))
loaded_station.station_levels += z_index
if((level_or_path.flags & LEGACY_LEVEL_ADMIN) || level_or_path.has_trait(ZTRAIT_ADMIN))
loaded_station.admin_levels += z_index
if((level_or_path.flags & LEGACY_LEVEL_CONTACT) || level_or_path.has_trait(ZTRAIT_STATION))
loaded_station.contact_levels += z_index
if((level_or_path.flags & LEGACY_LEVEL_SEALED))
loaded_station.sealed_levels += z_index
if((level_or_path.flags & LEGACY_LEVEL_CONSOLES) || level_or_path.has_trait(ZTRAIT_STATION))
loaded_station.map_levels += z_index
// Holomaps
// Auto-center the map if needed (Guess based on maxx/maxy)
if (level_or_path.holomap_offset_x < 0)
level_or_path.holomap_offset_x = ((HOLOMAP_ICON_SIZE - world.maxx) / 2)
if (level_or_path.holomap_offset_x < 0)
level_or_path.holomap_offset_y = ((HOLOMAP_ICON_SIZE - world.maxy) / 2)
// Assign them to the map lists
LIST_NUMERIC_SET(loaded_station.holomap_offset_x, z_index, level_or_path.holomap_offset_x)
LIST_NUMERIC_SET(loaded_station.holomap_offset_y, z_index, level_or_path.holomap_offset_y)
LIST_NUMERIC_SET(loaded_station.holomap_legend_x, z_index, level_or_path.holomap_legend_x)
LIST_NUMERIC_SET(loaded_station.holomap_legend_y, z_index, level_or_path.holomap_legend_y)
// the fact that this exists is stupid but this check
// make sure we're not loading system maps like reserved levels before station loads.
if(loaded_station)
if((level_or_path.flags & LEGACY_LEVEL_STATION) || level_or_path.has_trait(ZTRAIT_STATION))
loaded_station.station_levels += z_index
if((level_or_path.flags & LEGACY_LEVEL_ADMIN) || level_or_path.has_trait(ZTRAIT_ADMIN))
loaded_station.admin_levels += z_index
if((level_or_path.flags & LEGACY_LEVEL_CONTACT) || level_or_path.has_trait(ZTRAIT_STATION))
loaded_station.contact_levels += z_index
if((level_or_path.flags & LEGACY_LEVEL_SEALED))
loaded_station.sealed_levels += z_index
if((level_or_path.flags & LEGACY_LEVEL_CONSOLES) || level_or_path.has_trait(ZTRAIT_STATION))
loaded_station.map_levels += z_index
// Holomaps
// Auto-center the map if needed (Guess based on maxx/maxy)
if (level_or_path.holomap_offset_x < 0)
level_or_path.holomap_offset_x = ((HOLOMAP_ICON_SIZE - world.maxx) / 2)
if (level_or_path.holomap_offset_x < 0)
level_or_path.holomap_offset_y = ((HOLOMAP_ICON_SIZE - world.maxy) / 2)
// Assign them to the map lists
LIST_NUMERIC_SET(loaded_station.holomap_offset_x, z_index, level_or_path.holomap_offset_x)
LIST_NUMERIC_SET(loaded_station.holomap_offset_y, z_index, level_or_path.holomap_offset_y)
LIST_NUMERIC_SET(loaded_station.holomap_legend_x, z_index, level_or_path.holomap_legend_x)
LIST_NUMERIC_SET(loaded_station.holomap_legend_y, z_index, level_or_path.holomap_legend_y)
//! END

/**
Expand Down
39 changes: 35 additions & 4 deletions code/controllers/subsystem/mapping/reservations.dm
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,22 @@
/// doing some blocking op on reservation system
var/static/reservation_blocking_op = FALSE
/// singleton area holding all free reservation turfs
var/static/area/unused_reservation_area/unallocated_reserve_area = new
var/static/area/unused_reservation_area/reservation_unallocated_area = new
/// spatial grid of turf reservations. the owner of a chunk is the bottom left tile's owner.
///
/// this is a list with length of world.maxz, with the actual spatial grid list being at the index of the z
/// e.g. to grab a reserved level's lookup, do `reservation_spatia_lookups[z_index]`
///
/// * null means that a level isn't a reservation level
/// * this also means that we can't zclear / 'free' reserved levels; they're effectively immovable due to this datastructure
/// * if it is a reserved level, it returns the spatial grid
/// * to get a chunk, do `spatial_lookup[ceil(where.x / TURF_CHUNK_RESOLUTION) + (ceil(where.y / TURF_CHUNK_RESOLUTION) - 1) * ceil(world.maxx / TURF_CHUNK_RESOLUTION)]`
var/static/list/reservation_spatial_lookups = list()

/datum/controller/subsystem/mapping/on_max_z_changed(old_z_count, new_z_count)
. = ..()
if(length(reservation_spatial_lookups) < new_z_count)
reservation_spatial_lookups.len = new_z_count

/datum/controller/subsystem/mapping/Recover()
. = ..()
Expand All @@ -29,12 +44,15 @@
if(reserved_level_count && ((world.maxx * world.maxy * (reserved_level_count + 1)) > reserved_turfs_max))
log_and_message_admins(SPAN_USERDANGER("Out of dynamic reservation allocations. Is there a memory leak with turf reservations?"))
return FALSE
log_and_message_admins(SPAN_USERDANGER("Allocating new reserved level. Now at [reserved_level_count]. This is probably not a good thing if the server is not at high load right now."))
if(reserved_level_count)
log_and_message_admins(SPAN_USERDANGER("Allocating new reserved level. Now at [reserved_level_count + 1]. This is probably not a good thing if the server is not at high load right now."))
reserved_level_count++
var/datum/map_level/reserved/level_struct = new
ASSERT(allocate_level(level_struct))
reserved_level_count++
initialize_reserved_level(level_struct.z_index)
reserve_levels |= level_struct.z_index
// make a list with a predetermined size for the lookup
reservation_spatial_lookups[level_struct.z_index] = new /list(ceil(world.maxx / TURF_CHUNK_RESOLUTION) * ceil(world.maxy / TURF_CHUNK_RESOLUTION))
return level_struct.z_index

/**
Expand Down Expand Up @@ -71,7 +89,20 @@
T.turf_flags |= UNUSED_RESERVATION_TURF
CHECK_TICK
// todo: area.assimilate_turfs?
unallocated_reserve_area.contents.Add(turfs)
reservation_unallocated_area.contents.Add(turfs)

/**
* @return turf reservation someone's in, or null if they're not in a reservation
*/
/datum/controller/subsystem/mapping/proc/get_turf_reservation(atom/where)
where = get_turf(where)
if(!where)
return
// this doubles as 'is this a reserved level'
var/list/spatial_lookup = reservation_spatial_lookups[where.z]
if(!spatial_lookup)
return
return spatial_lookup[ceil(where.x / TURF_CHUNK_RESOLUTION) + (ceil(where.y / TURF_CHUNK_RESOLUTION) - 1) * ceil(world.maxx / TURF_CHUNK_RESOLUTION)]

/area/unused_reservation_area
name = "Unused Reservation Area"
Expand Down
76 changes: 61 additions & 15 deletions code/controllers/subsystem/spatial_grids.dm
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ SUBSYSTEM_DEF(spatial_grids)
living.sync_world_z(new_z_count)

/**
* index = ceil(x / resolution) + width * ceil(y / resolution)
* index = ceil(x / resolution) + width * (ceil(y / resolution) - 1)
*
* * at time of writing, spatial grids are intentionally aligned with turf reservation resolution. this is intentional so looking stuff up in grids on a reservation is lightning-fast.
*/
/datum/spatial_grid
/// our grid; list[z] = grid: list()
Expand All @@ -36,21 +38,14 @@ SUBSYSTEM_DEF(spatial_grids)
var/width
/// our grid height
var/height
/// our grid resolution in tiles
var/resolution = 16
/// expected type
var/expected_type = /atom/movable

/datum/spatial_grid/New(expected_type, resolution)
// make sure resolution is reasonable
if(resolution <= 8 || resolution >= 128)
stack_trace("invalid resolution: [resolution]")
resolution = 16
/datum/spatial_grid/New(expected_type)
// initialize grid
src.width = ceil(world.maxx / resolution)
src.height = ceil(world.maxy / resolution)
src.width = ceil(world.maxx / TURF_CHUNK_RESOLUTION)
src.height = ceil(world.maxy / TURF_CHUNK_RESOLUTION)
src.grids = list()
src.resolution = resolution
src.expected_type = expected_type

sync_world_z(world.maxz)
Expand Down Expand Up @@ -105,10 +100,10 @@ SUBSYSTEM_DEF(spatial_grids)
*/
/datum/spatial_grid/proc/range_query(turf/epicenter, distance)
. = list()
var/min_x = ceil((epicenter.x - distance) / src.resolution)
var/min_y = floor((epicenter.y - distance) / src.resolution)
var/max_x = ceil((epicenter.x + distance) / src.resolution)
var/max_y = floor((epicenter.y + distance) / src.resolution)
var/min_x = ceil((epicenter.x - distance) / TURF_CHUNK_RESOLUTION)
var/min_y = ceil((epicenter.y - distance) / TURF_CHUNK_RESOLUTION)
var/max_x = ceil((epicenter.x + distance) / TURF_CHUNK_RESOLUTION)
var/max_y = ceil((epicenter.y + distance) / TURF_CHUNK_RESOLUTION)
var/list/grid = src.grids[epicenter.z]
for(var/x in max(1, min_x) to min(src.width, max_x))
for(var/y in max(1, min_y) to min(src.height, max_y))
Expand Down Expand Up @@ -142,3 +137,54 @@ SUBSYSTEM_DEF(spatial_grids)
if(!grid[i])
continue
. += grid[i]

//* basically the above but only within a certain turf reservation *//

/datum/spatial_grid/proc/reservation_range_query(datum/turf_reservation/reservation, turf/epicenter, distance)
ASSERT(reservation.spatial_z == epicenter.z)
. = list()
var/min_x = ceil((epicenter.x - distance) / TURF_CHUNK_RESOLUTION)
var/min_y = ceil((epicenter.y - distance) / TURF_CHUNK_RESOLUTION)
var/max_x = ceil((epicenter.x + distance) / TURF_CHUNK_RESOLUTION)
var/max_y = ceil((epicenter.y + distance) / TURF_CHUNK_RESOLUTION)
var/list/grid = src.grids[epicenter.z]
for(var/x in max(1, min_x, reservation.spatial_bl_x) to min(src.width, max_x, reservation.spatial_tr_x))
for(var/y in max(1, min_y, reservation.spatial_bl_y) to min(src.height, max_y, reservation.spatial_tr_y))
var/index = x + src.width * (y - 1)
if(grid[index])
var/entry = grid[index]
if(islist(entry))
for(var/atom/movable/AM as anything in entry)
if(get_dist(AM, epicenter) <= distance)
. += AM
else if(get_dist(entry, epicenter) <= distance)
. += entry

/datum/spatial_grid/proc/reservation_all_atoms(datum/turf_reservation/reservation)
. = list()
var/list/grid = src.grids[reservation.spatial_z]
for(var/x in reservation.spatial_bl_x to reservation.spatial_tr_x)
for(var/y in reservation.spatial_bl_y to reservation.spatial_tr_y)
var/index = x + src.width * (y - 1)
if(!grid[index])
continue
. += grid[index]

//* basically the above but only within a certain turf reservation, if reservation exists; otherwise, proceed as normal *//
//* if on a reservation level, but no reservation, we return nothing. *//

/datum/spatial_grid/proc/automatic_range_query(turf/epicenter, distance)
// check if we're on a reserved level
var/list/spatial_lookup = SSmapping.reservation_spatial_lookups[epicenter.z]
if(!spatial_lookup)
// we're not on a reserved level, use normal
return range_query(epicenter, distance)
// we're on a reserve level
var/datum/turf_reservation/reservation = spatial_lookup[ceil(epicenter.x / TURF_CHUNK_RESOLUTION) + (ceil(epicenter.y / TURF_CHUNK_RESOLUTION) - 1) * ceil(world.maxx / TURF_CHUNK_RESOLUTION)]
// check if reservation exists
if(reservation)
// it does, get stuff in reservation
return reservation_range_query(reservation, epicenter, distance)
else
// it doesn't, return nothing
return list()
7 changes: 2 additions & 5 deletions code/datums/components/movable/spatial_grid.dm
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
registered_type = /datum/component/spatial_grid
/// target spatial grid
var/datum/spatial_grid/grid
/// target grid resolution
var/grid_resolution
/// target grid width
var/grid_width
/// last grid index
Expand All @@ -24,7 +22,6 @@
return COMPONENT_INCOMPATIBLE

src.grid = grid
src.grid_resolution = grid.resolution
src.grid_width = grid.width

/datum/component/spatial_grid/RegisterWithParent()
Expand All @@ -43,7 +40,7 @@
RegisterSignal(root, COMSIG_MOVABLE_MOVED, PROC_REF(update))
root = root.loc
if(isturf(root))
var/idx = ceil(root.x / grid_resolution) + grid_width * floor(root.y / grid_resolution)
var/idx = ceil(root.x / TURF_CHUNK_RESOLUTION) + grid_width * (ceil(root.y / TURF_CHUNK_RESOLUTION) - 1)
grid.direct_insert(parent, root.z, idx)
current_index = idx

Expand All @@ -61,7 +58,7 @@
return
// turf --> turf, try to do an optimized, lazy update
if(isturf(oldloc) && isturf(newloc) && (oldloc.z == newloc.z))
var/new_index = ceil(newloc.x / grid_resolution) + grid_width * floor(newloc.y / grid_resolution)
var/new_index = ceil(newloc.x / TURF_CHUNK_RESOLUTION) + grid_width * (ceil(newloc.y / TURF_CHUNK_RESOLUTION) - 1)
var/z = oldloc.z
if(new_index != current_index)
grid.direct_remove(parent, z, current_index)
Expand Down
Loading

0 comments on commit 7b5c263

Please sign in to comment.