Replaces our lighting system with CM's. (#21465)

Depends on #21458.

Ports https://github.com/cmss13-devs/cmss13/pull/4229, with the original
authors as:

- https://github.com/tgstation/TerraGov-Marine-Corps/pull/1964 for the
lighting controller (A-lexa)
- https://github.com/tgstation/TerraGov-Marine-Corps/pull/4747 and
https://github.com/tgstation/TerraGov-Marine-Corps/pull/7263 for the
lighting (TiviPlus)
- https://github.com/tgstation/tgstation/pull/54520 for the dir lighting
component
- https://github.com/tgstation/tgstation/pull/75018 for the out of
bounds fix in lighting
- https://github.com/tgstation/TerraGov-Marine-Corps/pull/6678 for the
emissives (TiviPlus)

The main driving reason behind this is that current lighting consumes
way too much processing power, especially for things like odysseys/away
sites where a billion light sources are processing/moving at once and
the game slows down to a crawl. Hopefully this improves the situation by
a good margin, but we will need some testmerging to confirm that.
<img width="1349" height="1349" alt="image"
src="https://github.com/user-attachments/assets/1059ba2b-c0c5-495a-9c76-2d75d0c42bf2"
/>
<img width="1349" height="1349" alt="image"
src="https://github.com/user-attachments/assets/9704b0f6-4cf6-4dfd-a6cb-5702ad07d677"
/>


- [x] Resolve todos
- [x] Look into open space fuckery (border objects)

---------

Co-authored-by: Matt Atlas <liermattia@gmail.com>
Co-authored-by: JohnWildkins <john.wildkins@gmail.com>
This commit is contained in:
Matt Atlas
2025-11-04 21:27:42 +00:00
committed by GitHub
co-authored by Matt Atlas JohnWildkins
parent 8ebd7c9ad5
commit 94d92803b4
395 changed files with 3906 additions and 3299 deletions
-45
View File
@@ -1,45 +0,0 @@
SUBSYSTEM_DEF(ao)
name = "Ambient Occlusion"
init_order = SS_INIT_AO
wait = 1
priority = SS_PRIORITY_LIGHTING
runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY
var/list/queue = list()
/datum/controller/subsystem/ao/stat_entry(msg)
msg = "P:[queue.len]"
return ..()
/datum/controller/subsystem/ao/Initialize()
fire(FALSE, TRUE)
return SS_INIT_SUCCESS
/datum/controller/subsystem/ao/fire(resumed = 0, no_mc_tick = FALSE)
var/list/curr = queue
while (curr.len)
var/turf/target = curr[curr.len]
curr.len--
if (!QDELETED(target))
if (target.ao_queued == AO_UPDATE_REBUILD)
var/old_n = target.ao_neighbors
var/old_z = target.ao_neighbors_mimic
target.calculate_ao_neighbors()
if (old_n != target.ao_neighbors || old_z != target.ao_neighbors_mimic)
target.update_ao()
else
target.update_ao()
target.ao_queued = AO_UPDATE_NONE
if (no_mc_tick)
CHECK_TICK
else if (MC_TICK_CHECK)
return
/datum/controller/subsystem/ao/ExplosionStart()
can_fire = FALSE
/datum/controller/subsystem/ao/ExplosionEnd()
can_fire = TRUE
+1 -8
View File
@@ -305,13 +305,6 @@ SUBSYSTEM_DEF(explosives)
act_turfs[current_turf] = current_power
current_power -= current_turf.explosion_resistance
// Attempt to shortcut on empty tiles: if a turf only has a LO on it, we don't need to check object resistance. Some turfs might not have LOs, so we need to check it actually has one.
if (current_turf.contents.len > !!current_turf.lighting_overlay)
for (var/thing in current_turf)
var/atom/movable/AM = thing
if (AM.simulated && AM.explosion_resistance)
current_power -= AM.explosion_resistance
if (current_power <= 0)
CHECK_TICK
continue
@@ -395,7 +388,7 @@ SUBSYSTEM_DEF(explosives)
if (T.simulated)
T.ex_act(severity)
if (T.contents.len > !!T.lighting_overlay)
if (T.contents.len)
for (var/subthing in T)
var/atom/movable/AM = subthing
if (AM.simulated)
+88 -182
View File
@@ -1,217 +1,123 @@
GLOBAL_VAR_INIT(lighting_profiling, FALSE)
GLOBAL_VAR_INIT(lighting_overlays_initialized, FALSE)
SUBSYSTEM_DEF(lighting)
name = "Lighting"
wait = LIGHTING_INTERVAL
runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY
priority = SS_PRIORITY_LIGHTING
wait = 2
init_order = INIT_ORDER_LIGHTING
var/total_lighting_overlays = 0
var/total_lighting_sources = 0
var/list/lighting_corners = list() // List of all lighting corners in the world.
//debug var for tracking updates before init is complete
var/duplicate_shadow_updates_in_init = 0
///Total times shadows were updated, debug
var/total_shadow_calculations = 0
var/list/light_queue = list() // lighting sources queued for update.
var/lq_idex = 1
var/list/corner_queue = list() // lighting corners queued for update.
var/cq_idex = 1
var/list/overlay_queue = list() // lighting overlays queued for update.
var/oq_idex = 1
///Whether the SS has begun setting up yet
var/started = FALSE
var/tmp/processed_lights = 0
var/tmp/processed_corners = 0
var/tmp/processed_overlays = 0
var/static/list/static_sources_queue = list() //! List of static lighting sources queued for update.
var/static/list/corners_queue = list() //! List of lighting corners queued for update.
var/static/list/objects_queue = list() //! List of lighting objects queued for update.
var/total_ss_updates = 0
var/total_instant_updates = 0
#ifdef USE_INTELLIGENT_LIGHTING_UPDATES
var/force_queued = TRUE
var/force_override = FALSE // For admins.
#endif
/datum/controller/subsystem/lighting/stat_entry(msg)
var/list/out = list(
#ifdef USE_INTELLIGENT_LIGHTING_UPDATES
"IUR: [total_ss_updates ? round(total_instant_updates/(total_instant_updates+total_ss_updates)*100, 0.1) : "NaN"]%\n",
#endif
"\tT:{L:[total_lighting_sources] C:[lighting_corners.len] O:[total_lighting_overlays]}\n",
"\tP:{L:[light_queue.len - (lq_idex - 1)]|C:[corner_queue.len - (cq_idex - 1)]|O:[overlay_queue.len - (oq_idex - 1)]}\n",
"\tL:{L:[processed_lights]|C:[processed_corners]|O:[processed_overlays]}\n"
)
msg = out.Join()
return ..()
#ifdef USE_INTELLIGENT_LIGHTING_UPDATES
/datum/controller/subsystem/lighting/ExplosionStart()
force_queued = TRUE
can_fire = FALSE
/datum/controller/subsystem/lighting/ExplosionEnd()
can_fire = TRUE
if (!force_override)
force_queued = FALSE
/datum/controller/subsystem/lighting/proc/handle_roundstart()
force_queued = FALSE
total_ss_updates = 0
total_instant_updates = 0
#endif
var/static/list/mask_queue = list() //! List of hybrid lighting sources queued for update.
/datum/controller/subsystem/lighting/Initialize(timeofday)
var/overlaycount = 0
var/starttime = REALTIMEOFDAY
// Generate overlays.
var/turf/T
var/thing
for (var/zlevel = 1 to world.maxz)
for (thing in Z_TURFS(zlevel))
T = thing
if(GLOB.config.starlight)
var/turf/space/S = T
if(istype(S) && S.use_starlight)
S.update_starlight()
if (!T.dynamic_lighting)
continue
var/area/A = T.loc
if (!A.dynamic_lighting)
continue
T.lighting_build_overlay()
overlaycount++
CHECK_TICK
GLOB.lighting_overlays_initialized = TRUE
admin_notice(SPAN_DANGER("Created [overlaycount] lighting overlays in [(REALTIMEOFDAY - starttime)/10] seconds."), R_DEBUG)
starttime = REALTIMEOFDAY
// Tick once to clear most lights.
started = TRUE
if(!initialized)
//Handle static lightnig
create_all_lighting_objects()
fire(FALSE, TRUE)
admin_notice(SPAN_DANGER("Processed [processed_lights] light sources."), R_DEBUG)
admin_notice(SPAN_DANGER("Processed [processed_corners] light corners."), R_DEBUG)
admin_notice(SPAN_DANGER("Processed [processed_overlays] light overlays."), R_DEBUG)
admin_notice(SPAN_DANGER("Lighting pre-bake completed in [(REALTIMEOFDAY - starttime)/10] seconds."), R_DEBUG)
log_subsystem("lighting", "NOv:[overlaycount] L:[processed_lights] C:[processed_corners] O:[processed_overlays]")
#ifdef USE_INTELLIGENT_LIGHTING_UPDATES
SSticker.OnRoundstart(CALLBACK(src, PROC_REF(handle_roundstart)))
#endif
return SS_INIT_SUCCESS
/datum/controller/subsystem/lighting/fire(resumed = FALSE, no_mc_tick = FALSE)
if (!resumed)
processed_lights = 0
processed_corners = 0
processed_overlays = 0
/datum/controller/subsystem/lighting/stat_entry()
. = ..("ShCalcs:[total_shadow_calculations]|SourcQ:[length(static_sources_queue)]|CcornQ:[length(corners_queue)]|ObjQ:[length(objects_queue)]|HybrQ:[length(mask_queue)]")
/datum/controller/subsystem/lighting/fire(resumed, init_tick_checks)
MC_SPLIT_TICK_INIT(3)
if (!no_mc_tick)
if(!init_tick_checks)
MC_SPLIT_TICK
var/updators_num = 0
while(updators_num < length(static_sources_queue))
updators_num += 1
var/list/curr_lights = light_queue
var/list/curr_corners = corner_queue
var/list/curr_overlays = overlay_queue
while (lq_idex <= curr_lights.len)
var/datum/light_source/L = curr_lights[lq_idex++]
if(QDELETED(L))
continue
if (L.needs_update != LIGHTING_NO_UPDATE)
total_ss_updates += 1
L.update_corners()
var/datum/static_light_source/L = static_sources_queue[updators_num]
L.update_corners()
if(!QDELETED(L))
L.needs_update = LIGHTING_NO_UPDATE
processed_lights++
if (no_mc_tick)
CHECK_TICK
else
updators_num -= 1
if(init_tick_checks)
if(!TICK_CHECK)
continue
static_sources_queue.Cut(1, updators_num + 1)
updators_num = 0
stoplag()
else if (MC_TICK_CHECK)
break
if(updators_num)
static_sources_queue.Cut(1, updators_num + 1)
updators_num = 0
if (lq_idex > 1)
curr_lights.Cut(1, lq_idex)
lq_idex = 1
if (!no_mc_tick)
if(!init_tick_checks)
MC_SPLIT_TICK
while (cq_idex <= curr_corners.len)
var/datum/lighting_corner/C = curr_corners[cq_idex++]
if(QDELETED(C))
while(updators_num < length(corners_queue))
updators_num += 1
var/datum/static_lighting_corner/C = corners_queue[updators_num]
C.needs_update = FALSE //update_objects() can call qdel if the corner is storing no data
C.update_objects()
if(init_tick_checks)
if(!TICK_CHECK)
continue
corners_queue.Cut(1, updators_num + 1)
updators_num = 0
stoplag()
else if (MC_TICK_CHECK)
break
if(updators_num)
corners_queue.Cut(1, updators_num + 1)
updators_num = 0
if(!init_tick_checks)
MC_SPLIT_TICK
while(updators_num < length(objects_queue))
updators_num += 1
var/datum/static_lighting_object/O = objects_queue[updators_num]
if (QDELETED(O))
continue
O.update()
O.needs_update = FALSE
if (C.needs_update)
C.update_overlays()
C.needs_update = FALSE
processed_corners++
if (no_mc_tick)
CHECK_TICK
if(init_tick_checks)
if(!TICK_CHECK)
continue
objects_queue.Cut(1, updators_num + 1)
updators_num = 0
else if (MC_TICK_CHECK)
break
if (cq_idex > 1)
curr_corners.Cut(1, cq_idex)
cq_idex = 1
if (!no_mc_tick)
if(updators_num)
objects_queue.Cut(1, updators_num + 1)
updators_num = 0
if(!init_tick_checks)
MC_SPLIT_TICK
while (oq_idex <= curr_overlays.len)
var/atom/movable/lighting_overlay/O = curr_overlays[oq_idex++]
while(updators_num > length(mask_queue))
updators_num += 1
if (!QDELETED(O) && O.needs_update)
O.update_overlay()
O.needs_update = FALSE
var/atom/movable/lighting_mask/mask_to_update = mask_queue[updators_num]
mask_to_update.calculate_lighting_shadows()
processed_overlays++
if (no_mc_tick)
CHECK_TICK
if(init_tick_checks)
if(!TICK_CHECK)
continue
mask_queue.Cut(1, updators_num + 1)
updators_num = 0
stoplag()
else if (MC_TICK_CHECK)
break
if (oq_idex > 1)
curr_overlays.Cut(1, oq_idex)
oq_idex = 1
if(updators_num)
mask_queue.Cut(1, updators_num + 1)
/datum/controller/subsystem/lighting/Recover()
lighting_corners = SSlighting.lighting_corners
total_lighting_overlays = SSlighting.total_lighting_overlays
total_lighting_sources = SSlighting.total_lighting_sources
light_queue = SSlighting.light_queue
corner_queue = SSlighting.corner_queue
overlay_queue = SSlighting.overlay_queue
lq_idex = SSlighting.lq_idex
cq_idex = SSlighting.cq_idex
oq_idex = SSlighting.oq_idex
if (lq_idex > 1)
light_queue.Cut(1, lq_idex)
lq_idex = 1
if (cq_idex > 1)
corner_queue.Cut(1, cq_idex)
cq_idex = 1
if (oq_idex > 1)
overlay_queue.Cut(1, oq_idex)
oq_idex = 1
initialized = SSlighting.initialized
return ..()
-3
View File
@@ -16,9 +16,6 @@ SUBSYSTEM_DEF(mobs)
var/list/greatworms = list()
var/list/greatasses = list()
var/list/ghost_darkness_images = list() //this is a list of images for things ghosts should still be able to see when they toggle darkness
var/list/ghost_sightless_images = list() //this is a list of images for things ghosts should still be able to see even without ghost sight
// Devour types (these are typecaches). Only simple_animals check these, other types are handled specially.
var/list/mtl_synthetic = list(
/mob/living/simple_animal/hostile/hivebot
-1
View File
@@ -53,7 +53,6 @@ SUBSYSTEM_DEF(sunlight)
/atom/movable/sunobj
name = "sunlight emitter"
desc = DESC_PARENT
light_novis = TRUE
light_range = 16
simulated = FALSE
mouse_opacity = FALSE
+1 -6
View File
@@ -7,9 +7,7 @@
///The time at which the next automatic transfer vote will be called
GLOBAL_VAR_INIT(next_transfer_time, null)
var/datum/controller/subsystem/ticker/SSticker
/datum/controller/subsystem/ticker
SUBSYSTEM_DEF(ticker)
// -- Subsystem stuff --
name = "Ticker"
@@ -73,9 +71,6 @@ var/datum/controller/subsystem/ticker/SSticker
var/total_players_ready = 0
var/list/ready_player_jobs
/datum/controller/subsystem/ticker/New()
NEW_SS_GLOBAL(SSticker)
/datum/controller/subsystem/ticker/Initialize(timeofday)
pregame()
restart_timeout = GLOB.config.restart_timeout
-101
View File
@@ -250,8 +250,6 @@ SUBSYSTEM_DEF(zcopy)
TO.plane = t_target
TO.mouse_opacity = initial(TO.mouse_opacity)
T.queue_ao(T.ao_neighbors_mimic == null) // If ao_neighbors hasn't been set yet, we need to do a rebuild
// Explicitly copy turf delegates so they show up properly on below levels.
// I think it's possible to get this to work without discrete delegate copy objects, but I'd rather this just work.
if ((T.below.z_flags & (ZM_MIMIC_BELOW|ZM_MIMIC_OVERWRITE)) == ZM_MIMIC_BELOW)
@@ -275,11 +273,6 @@ SUBSYSTEM_DEF(zcopy)
// Don't queue deleted stuff, stuff that's not visible, blacklisted stuff, or stuff that's centered on another tile but intersects ours.
continue
// Special case: these are merged into the shadower to reduce memory usage.
if (object.type == /atom/movable/lighting_overlay)
//T.shadower.copy_lighting(object)
continue
if (!object.bound_overlay) // Generate a new overlay if the atom doesn't already have one.
object.bound_overlay = new(T)
object.bound_overlay.associated_atom = object
@@ -393,13 +386,6 @@ SUBSYSTEM_DEF(zcopy)
if (OO.bound_overlay) // If we have a bound overlay, queue it too.
OO.update_above()
// If an atom has explicit plane sets on its overlays/underlays, we need to replace the appearance so they can be mangled to work with our planing.
if (OO.z_flags & ZMM_MANGLE_PLANES)
var/new_appearance = fixup_appearance_planes(OO.appearance)
if (new_appearance)
OO.appearance = new_appearance
OO.have_performed_fixup = TRUE
if (no_mc_tick)
CHECK_TICK
else if (MC_TICK_CHECK)
@@ -439,93 +425,6 @@ SUBSYSTEM_DEF(zcopy)
if (TO.plane == 0 && target_plane)
TO.plane = target_plane
/// Generate a new appearance from `appearance` with planes mangled to work with Z-Mimic. Do not pass a depth.
/datum/controller/subsystem/zcopy/proc/fixup_appearance_planes(appearance, depth = 0)
// Adding this to guard against a reported runtime - supposed to be impossible, so cause is unclear.
if(!appearance)
return null
if (fixup_known_good[appearance])
fixup_hit += 1
return null
if (fixup_cache[appearance])
fixup_hit += 1
return fixup_cache[appearance]
// If you have more than 4 layers of overlays within overlays, I dunno what to say.
if (depth > 4)
var/icon_name = "[appearance:icon]"
WARNING("Fixup of appearance with icon [icon_name || "<unknown file>"] exceeded maximum recursion limit, bailing")
return null
var/plane_needs_fix = FALSE
// Don't fixup the root object's plane.
if (depth > 0)
switch (appearance:plane)
if (GAME_PLANE, FLOAT_PLANE)
plane_needs_fix = FALSE //For lint
else
plane_needs_fix = TRUE
// Scan & fix overlays
var/list/fixed_overlays
if (appearance:overlays:len)
var/mutated = FALSE
var/fixed_appearance
for (var/i in 1 to appearance:overlays:len)
if ((fixed_appearance = .(appearance:overlays[i], depth + 1)))
mutated = TRUE
if (!fixed_overlays)
fixed_overlays = new( length(appearance:overlays))
fixed_overlays[i] = fixed_appearance
if (mutated)
for (var/i in 1 to length(fixed_overlays))
if (fixed_overlays[i] == null)
fixed_overlays[i] = appearance:overlays[i]
// Scan & fix underlays
var/list/fixed_underlays
if (appearance:underlays:len)
var/mutated = FALSE
var/fixed_appearance
for (var/i in 1 to appearance:underlays:len)
if ((fixed_appearance = .(appearance:underlays[i], depth + 1)))
mutated = TRUE
if (!fixed_underlays)
fixed_underlays = new( length(appearance:underlays))
fixed_underlays[i] = fixed_appearance
if (mutated)
for (var/i in 1 to length(fixed_overlays))
if (fixed_underlays[i] == null)
fixed_underlays[i] = appearance:underlays[i]
// If we did nothing (no violations), don't bother creating a new appearance
if (!plane_needs_fix && !fixed_overlays && !fixed_underlays)
fixup_noop += 1
fixup_known_good[appearance] = TRUE
return null
fixup_miss += 1
var/mutable_appearance/MA = new(appearance)
if (plane_needs_fix)
MA.plane = depth == 0 ? GAME_PLANE : FLOAT_PLANE
MA.layer = FLY_LAYER // probably fine
if (fixed_overlays)
MA.overlays = fixed_overlays
if (fixed_underlays)
MA.underlays = fixed_underlays
fixup_cache[appearance] = MA.appearance
return MA
#define FMT_DEPTH(X) (X == null ? "(null)" : X)
// This is a dummy object used so overlays can be shown in the analyzer.