Atmos refactor & speedup by utilizing BYOND 516 vector functions (#96448)

## About The Pull Request

### Summary
This PR changes internal structure of `/datum/gas_mixture`:
`gases[gas_id][MOLES]` refactored into `moles[gas_id]`,
`gases[gas_id][ARCHIVE]` into `moles_archive[gas_id]` and
`gases[gas_id][GAS_META]` into `gas_meta` static variable. This allows
us to use BYOND 516 vector functions for calculating total moles and
heat capacity. Also it simplifies some parts of the code, allowing us to
get rid of macros `ADD_GAS` and `ASSERT_GAS`. According to the profiler
`/turf/open/process_cell` time is reduced by ~20%.

### Details
`gas_mixture.gases` was a nested 2d-list with MOLES, ARCHIVE and
GAS_META for each gas_id. For example, to get gas moles you had to do
`gases[gas_id][MOLES]`. I've changed this structure to be as follows:
`moles[gas_id]` - moles for the gas, `moles_archive[gas_id]` - archived
version of moles, `gas_meta[KEY][gas_id]` - static var with meta
information for the gas.

Since I removed key GAS_META from the gases, `gas_meta` was moved to the
static variable and the order of keys in the array was changed from
`gas_meta[gas_id][META_KEY]' to 'gas_meta[META_KEY][gas_id]`. This was
done to allow using it in vector calculations (for example heat capacity
or fusion power). Static variable access is very fast and it is
considered as accessing a global in the bytecode.

Byond 516 introduced new vector functions: `values_sum`, `values_dot`
and others. These functions are very fast, but operate only on
associative lists. This allows us to change the way we calculate
total_moles and heat_capacity - very hot and heavily used functions.
`total_moles()` became just `values_sum(moles)`, and `heat_capacity` is
just a dot product: `values_dot(moles,
gas_meta[META_GAS_SPECIFIC_HEAT])`.

As a side bonus, since `moles` is just an associative list, you don't
really need old macros `ADD_GAS` and `ASSERT_GAS` - all they did was to
make a copy of a list[3] with default value [0, 0, gas_meta] for
specific gas. Now when you're adding gas you can just use `moles[gas_id]
+= amount` and when you query amount of gas you can just query the key
(for example `moles[/datum/gas/oxygen]`) if the key does not exist, it
returns null and works as 0 for all arithmetic and logic operations. For
example, old code would be `if (!air.gases[/datum/gas/oxygen] ||
air.gases[/datum/gas/oxygen][MOLES] < 1)` and now it is `if
(moles[/datum/gas/oxygen] < 1)`. This simplifies some parts of the code
and also speeds things up.

For the performance comparison I used Tracy profiler. I've done many
different tests, and they all show slightly different numbers, but
overall speedup for `process_cell` is about 20%. (-20% to average time
per call from ). My testing setup was as follows:
Load Icebox, drop 30/60/90 radius bomb in the middle of the bridge, set
code to blue, wait 10 minutes until the round ends.
Also I fixed random seed in the master controller and in the planetary
gas randomization so generated maps are the same between tests.
Althought it's not very realistic, it generates a lot of samples for the
`process_cell` (around ~3.5M per 10 minutes).
Another test I did was a plasmafire in an 8x8 space, on runtime station,
it showed (-24% time on process_cell).
Another test was a emagged holodeck burn test, it showed (-13% time)
As for other functions of gas_mixture: `total_moles`: -50%(2x speedup),
`heat_capacity`: -65%(3x speedup), `share`: -30%, `react`: -20%. Timings
of all those functions is in microseconds range and they are very hot
(call count is in the same order as process_cell)

<details><summary>Some pictures from profiler</summary>
<img width="569" height="642" alt="process_cell"
src="https://github.com/user-attachments/assets/76fa0c27-719d-485d-9bfc-859fef788999"
/>
<img width="572" height="315" alt="image"
src="https://github.com/user-attachments/assets/f68497e9-4db8-4a9c-b43f-ad04e6dc5cac"
/>
<img width="569" height="317" alt="image"
src="https://github.com/user-attachments/assets/d7249e7b-f344-47a6-8b39-1bab0521182d"
/>
<img width="541" height="316" alt="image"
src="https://github.com/user-attachments/assets/1bef71fd-533d-40fa-a85e-7a803ad322f7"
/>
<img width="519" height="409" alt="image"
src="https://github.com/user-attachments/assets/51165289-174e-403d-a09c-787dd9af136a"
/>
<img width="523" height="318" alt="image"
src="https://github.com/user-attachments/assets/fe4b1db0-17d8-47f9-8fd0-e2ecef2ee66a"
/>
</details>

<details><summary>Setting up a profiler</summary>
If you wanna to reproduce my results here is a list of steps

1. download: https://github.com/goonstation/byond-tracy-writer (this one
has offsets for my version 1677)
2. build the dll, drop in the tgstation/ folder
3. download rtracy https://github.com/Dimach/rtracy
4. download Tracy profiler (0.13.1) https://github.com/wolfpld/tracy
5. uncomment `#define USE_BYOND_TRACY` in `_compile_options.dm`
6. build tgstation
7. open dream daemon, run the desired test, after round end dream daemon
closes
8. navigate to tgstation/data/profiler, find the `123412341234.utracy`
file
9. run `rtracty 123412341234.utracy`
10. open tracy-profiler.exe, press Connect, save the profiler data
11. repeat steps 5-10 with another branch, save another profiler data
12. open tracy-profiler, open first data, press compare, open second
data
</details>

## Why It's Good For The Game

## Changelog
🆑
refactor: Atmos refactor & speedup by utilizing BYOND 516 vector
functions
/🆑

---------

Co-authored-by: san7890 <the@san7890.com>
This commit is contained in:
antropod
2026-07-14 20:34:31 -07:00
committed by GitHub
co-authored by san7890
parent c4194f8b4d
commit 0716e3fff4
69 changed files with 818 additions and 906 deletions
@@ -239,7 +239,7 @@
// Not safe to check the old pp because of can_breath_vacuum
breather.throw_alert(ALERT_NOT_ENOUGH_OXYGEN, /atom/movable/screen/alert/not_enough_oxy)
var/gas_breathed = handle_suffocation(breather, o2_pp, safe_oxygen_min, breath.gases[/datum/gas/oxygen][MOLES])
var/gas_breathed = handle_suffocation(breather, o2_pp, safe_oxygen_min, breath.moles[/datum/gas/oxygen])
if(o2_pp)
breathe_gas_volume(breath, /datum/gas/oxygen, /datum/gas/carbon_dioxide, volume = gas_breathed)
return
@@ -263,7 +263,7 @@
return BREATH_LOST
return
var/ratio = (breath.gases[/datum/gas/oxygen][MOLES] / safe_oxygen_max) * 10
var/ratio = (breath.moles[/datum/gas/oxygen] / safe_oxygen_max) * 10
breather.apply_damage(clamp(ratio, oxy_breath_dam_min, oxy_breath_dam_max), oxy_damage_type, spread_damage = TRUE)
if(!HAS_TRAIT(breather, TRAIT_ANOSMIA))
breather.throw_alert(ALERT_TOO_MUCH_OXYGEN, /atom/movable/screen/alert/too_much_oxy)
@@ -287,7 +287,7 @@
// Not safe to check the old pp because of can_breath_vacuum
if(!HAS_TRAIT(breather, TRAIT_ANOSMIA))
breather.throw_alert(ALERT_NOT_ENOUGH_NITRO, /atom/movable/screen/alert/not_enough_nitro)
var/gas_breathed = handle_suffocation(breather, nitro_pp, safe_nitro_min, breath.gases[/datum/gas/nitrogen][MOLES])
var/gas_breathed = handle_suffocation(breather, nitro_pp, safe_nitro_min, breath.moles[/datum/gas/nitrogen])
if(nitro_pp)
breathe_gas_volume(breath, /datum/gas/nitrogen, /datum/gas/carbon_dioxide, volume = gas_breathed)
return
@@ -342,7 +342,7 @@
if(!HAS_TRAIT(breather, TRAIT_ANOSMIA))
breather.throw_alert(ALERT_NOT_ENOUGH_PLASMA, /atom/movable/screen/alert/not_enough_plas)
// Breathe insufficient amount of Plasma, exhale CO2.
var/gas_breathed = handle_suffocation(breather, plasma_pp, safe_plasma_min, breath.gases[/datum/gas/plasma][MOLES])
var/gas_breathed = handle_suffocation(breather, plasma_pp, safe_plasma_min, breath.moles[/datum/gas/plasma])
if(plasma_pp)
breathe_gas_volume(breath, /datum/gas/plasma, /datum/gas/carbon_dioxide, volume = gas_breathed)
return
@@ -368,7 +368,7 @@
if(!HAS_TRAIT(breather, TRAIT_ANOSMIA))
breather.throw_alert(ALERT_TOO_MUCH_PLASMA, /atom/movable/screen/alert/too_much_plas)
var/ratio = (breath.gases[/datum/gas/plasma][MOLES] / safe_plasma_max) * 10
var/ratio = (breath.moles[/datum/gas/plasma] / safe_plasma_max) * 10
breather.apply_damage(clamp(ratio, plas_breath_dam_min, plas_breath_dam_max), plas_damage_type, spread_damage = TRUE)
/// Resets plasma side effects
@@ -558,7 +558,7 @@
/// Radioactive, green gas. Toxin damage, and a radiation chance
/obj/item/organ/lungs/proc/too_much_tritium(mob/living/carbon/breather, datum/gas_mixture/breath, trit_pp, old_trit_pp)
var/gas_breathed = breathe_gas_volume(breath, /datum/gas/tritium)
var/moles_visible = GLOB.meta_gas_info[/datum/gas/tritium][META_GAS_MOLES_VISIBLE] * BREATH_PERCENTAGE
var/moles_visible = GLOB.meta_gas_info[META_GAS_MOLES_VISIBLE][/datum/gas/tritium] * BREATH_PERCENTAGE
// Tritium side-effects.
if(gas_breathed > moles_visible)
var/ratio = gas_breathed * 15
@@ -625,7 +625,7 @@
breather.failed_last_breath = TRUE
// The list of gases in the breath.
var/list/breath_gases = breath.gases
var/list/breath_moles = breath.moles
// Copy the breath's temperature into breath_out to avoid cooling the output breath down unfairly
breath_out.temperature = breath.temperature
@@ -639,8 +639,8 @@
// Build out our partial pressures, for use as we go
var/list/partial_pressures = list()
for(var/gas_id in breath_gases)
partial_pressures[gas_id] = breath.get_breath_partial_pressure(breath_gases[gas_id][MOLES] * received_pressure_mult)
for(var/gas_id, amount in breath_moles)
partial_pressures[gas_id] = breath.get_breath_partial_pressure(amount * received_pressure_mult)
// Treat gas as other types of gas
for(var/list/conversion_packet in treat_as)
@@ -658,32 +658,31 @@
var/partial_pressure = partial_pressures[breath_id] || 0
var/old_partial_pressure = last_partial_pressures[breath_id] || 0
// Ensures the gas will always be instanciated, so people can interact with it safely
ASSERT_GAS(breath_id, breath)
var/inhale = breathe_always[breath_id]
call(src, inhale)(breather, breath, partial_pressure, old_partial_pressure)
// Now we'll handle the callbacks that want to be run conditionally off our current breath
for(var/breath_id in breath_gases)
var/when_present = breath_present[breath_id]
for(var/gas_id in breath_moles)
var/when_present = breath_present[gas_id]
if(!when_present)
continue
var/reaction = call(src, when_present)(breather, breath, partial_pressures[breath_id], last_partial_pressures[breath_id])
var/reaction = call(src, when_present)(breather, breath, partial_pressures[gas_id], last_partial_pressures[gas_id])
if(reaction == BREATH_LOST)
var/on_lose = breath_lost[breath_id]
var/on_lose = breath_lost[gas_id]
if(on_lose)
call(src, on_lose)(breather, breath, partial_pressures[breath_id], last_partial_pressures[breath_id])
call(src, on_lose)(breather, breath, partial_pressures[gas_id], last_partial_pressures[gas_id])
// Finally, we'll run the callbacks that aren't in breath_gases, but WERE in our last breath
for(var/gas_lost in last_partial_pressures)
for(var/gas_id in last_partial_pressures)
// If we still have it, go away
if(breath_gases[gas_lost])
if(breath_moles[gas_id])
continue
var/on_loss = breath_lost[gas_lost]
var/on_loss = breath_lost[gas_id]
if(!on_loss)
continue
call(src, on_loss)(breather, breath, last_partial_pressures[gas_lost])
call(src, on_loss)(breather, breath, last_partial_pressures[gas_id])
src.last_partial_pressures = partial_pressures
@@ -711,12 +710,11 @@
/// Removes 100% of the given gas type unless given a volume argument.
/// Returns the amount of gas theoretically removed.
/obj/item/organ/lungs/proc/breathe_gas_volume(datum/gas_mixture/breath, remove_id, exchange_id = null, volume = INFINITY)
var/list/breath_gases = breath.gases
volume = min(volume, breath_gases[remove_id][MOLES])
breath_gases[remove_id][MOLES] -= volume
var/list/breath_moles = breath.moles
volume = min(volume, breath_moles[remove_id])
breath_moles[remove_id] -= volume
if(exchange_id)
ASSERT_GAS(exchange_id, breath_out)
breath_out.gases[exchange_id][MOLES] += volume
breath_out.moles[exchange_id] += volume
return volume
/// Applies suffocation side-effects to a given Human, scaling based on ratio of required pressure VS "true" pressure.
@@ -924,8 +922,8 @@
/obj/item/organ/lungs/slime/check_breath(datum/gas_mixture/breath, mob/living/carbon/human/breather_slime)
. = ..()
if (breath?.gases[/datum/gas/plasma])
var/plasma_pp = breath.get_breath_partial_pressure(breath.gases[/datum/gas/plasma][MOLES])
if (breath?.moles[/datum/gas/plasma])
var/plasma_pp = breath.get_breath_partial_pressure(breath.moles[/datum/gas/plasma])
breather_slime.adjust_blood_volume(0.2 * plasma_pp) // 10/s when breathing literally nothing but plasma, which will suffocate you.
/obj/item/organ/lungs/smoker_lungs
@@ -1023,7 +1021,7 @@
// Take a "breath" of the air
var/datum/gas_mixture/breath = volumed_mix.remove(volumed_mix.total_moles() * BREATH_PERCENTAGE)
var/list/breath_gases = breath.gases
var/list/breath_moles = breath.moles
breath.assert_gases(
/datum/gas/oxygen,
@@ -1034,12 +1032,12 @@
/datum/gas/miasma,
)
var/oxygen_pp = breath.get_breath_partial_pressure(breath_gases[/datum/gas/oxygen][MOLES])
var/nitrogen_pp = breath.get_breath_partial_pressure(breath_gases[/datum/gas/nitrogen][MOLES])
var/plasma_pp = breath.get_breath_partial_pressure(breath_gases[/datum/gas/plasma][MOLES])
var/carbon_dioxide_pp = breath.get_breath_partial_pressure(breath_gases[/datum/gas/carbon_dioxide][MOLES])
var/bz_pp = breath.get_breath_partial_pressure(breath_gases[/datum/gas/bz][MOLES])
var/miasma_pp = breath.get_breath_partial_pressure(breath_gases[/datum/gas/miasma][MOLES])
var/oxygen_pp = breath.get_breath_partial_pressure(breath_moles[/datum/gas/oxygen])
var/nitrogen_pp = breath.get_breath_partial_pressure(breath_moles[/datum/gas/nitrogen])
var/plasma_pp = breath.get_breath_partial_pressure(breath_moles[/datum/gas/plasma])
var/carbon_dioxide_pp = breath.get_breath_partial_pressure(breath_moles[/datum/gas/carbon_dioxide])
var/bz_pp = breath.get_breath_partial_pressure(breath_moles[/datum/gas/bz])
var/miasma_pp = breath.get_breath_partial_pressure(breath_moles[/datum/gas/miasma])
safe_oxygen_min = max(0, oxygen_pp - GAS_TOLERANCE)
safe_nitro_min = max(0, nitrogen_pp - GAS_TOLERANCE)
@@ -1120,7 +1118,7 @@
/// H2O electrolysis
/obj/item/organ/lungs/ethereal/proc/consume_water(mob/living/carbon/breather, datum/gas_mixture/breath, h2o_pp, old_h2o_pp)
var/gas_breathed = breath.gases[/datum/gas/water_vapor][MOLES]
var/gas_breathed = breath.moles[/datum/gas/water_vapor]
breath.adjust_gas(/datum/gas/water_vapor, -gas_breathed)
var/list/new_gases = list(/datum/gas/oxygen = gas_breathed, /datum/gas/hydrogen = gas_breathed * 2)
breath_out.adjust_multiple_gases(new_gases)