IconForge: rust-g GAGS (250x faster edition) (#89590)

## About The Pull Request

Offloads GAGS generation to rust-g IconForge.

**Key Notes**

- The builtin GAGS editor still uses the 'legacy' generation to allow
for debugging.
- Does not support `color_matrix` layer type, which is currently not
used by any GAGS configs. Will do nothing if used.
- Does not support `or` blending mode, which is currently not used by
any GAGS configs. Will error if used.
- Has some 'quirks' compared to BYOND when it comes to mixing icon
states with different dir/frame amounts. BYOND will just silently handle
these and it's basically undefined behavior because what should you
expect BYOND to do? IconForge will spit errors out instead. So this PR
also fixes a few of those cases.

Functions by writing output to `tmp/gags/gags-[...].dmi`, copying that
output into the RSC and assigning the file object to `icon`.

Saves ~1.7s init by reducing worst-case GAGS icon generation from 250ms
to 1ms.

Also optimizes `icon_exists` by using `rustg_dmi_icon_states` for file
icons, saving ~60ms. Would have more savings if not for json_decode as
well as DMI parsing in rust being somewhat slow. Perhaps having
`rustg_dmi_icon_states` share a cache with IconForge could reduce this
cost, however I'd still recommend limiting these tests to unit tests
(https://github.com/tgstation/dev-cycles-initiative/issues/34),
especially for GAGS configs. I'm not sure they're worth 700ms.

Saves another ~400ms by replacing `md5asfile` with `rustg_hash_file` in
`/datum/greyscale_config/proc/Refresh`

Savings are likely even higher when combined with #89478, due to
spritesheets sharing a parsed DMI cache with GAGS. This means GAGS will
spend less time parsing icons synchronously and can generate output
faster. Tracy tests with this combo seem to yield ~2sec savings instead
of ~1.7sec

Total savings: ~2.16sec to ~2.46sec

- Ports https://github.com/BeeStation/BeeStation-Hornet/pull/10455
- Resolves https://github.com/tgstation/dev-cycles-initiative/issues/9



## Why It's Good For The Game

GAGS go zoooom

<details>
<summary>GAGS Working Ingame</summary>


![image](https://github.com/user-attachments/assets/28df25a5-bdf0-4a63-a6cf-b15f85467b23)


![image](https://github.com/user-attachments/assets/6a9dab46-5814-47ea-ad9b-f5ec84a6333d)

</details>

<details>
<summary>GetColoredIconByType</summary>


![image](https://github.com/user-attachments/assets/1698729f-1101-4413-bfb3-0922b389c347)

</details>

<details>
<summary>icon_exists</summary>


![image](https://github.com/user-attachments/assets/9e72c6aa-287f-4ce3-8dbe-9d3bebf3a762)

</details>


<details>
<summary>Refresh</summary>


![image](https://github.com/user-attachments/assets/18c15073-a294-4db6-bdd0-cdc7d8682221)

</details>


## Changelog
🆑
tweak: Optimized GAGS using rust-g IconForge, reducing worst-case
generation time to 1ms
/🆑
This commit is contained in:
itsmeow
2025-03-13 19:59:12 -05:00
committed by GitHub
parent 94f2519b77
commit 93a2b723da
8 changed files with 75 additions and 10 deletions

View File

@@ -1195,8 +1195,13 @@ GLOBAL_LIST_EMPTY(transformation_animation_objects)
if(isnull(icon_states_cache[file]))
icon_states_cache[file] = list()
for(var/istate in icon_states(file))
icon_states_cache[file][istate] = TRUE
var/file_string = "[file]"
if(length(file_string)) // ensure that it's actually a file, and not a runtime icon
for(var/istate in json_decode(rustg_dmi_icon_states(file_string)))
icon_states_cache[file][istate] = TRUE
else // Otherwise, we have to use the slower BYOND proc
for(var/istate in icon_states(file))
icon_states_cache[file][istate] = TRUE
return !isnull(icon_states_cache[file][state])

View File

@@ -1,3 +1,9 @@
/// Disable to use builtin DM-based generation.
/// IconForge is 250x times faster but requires storing the icons in tmp/ and may result in higher asset transport.
/// Note that the builtin GAGS editor still uses the 'legacy' generation to allow for debugging.
/// IconForge also does not support the color matrix layer type or the 'or' blend_mode, however both are currently unused.
#define USE_RUSTG_ICONFORGE_GAGS
PROCESSING_SUBSYSTEM_DEF(greyscale)
name = "Greyscale"
flags = SS_BACKGROUND
@@ -6,6 +12,10 @@ PROCESSING_SUBSYSTEM_DEF(greyscale)
var/list/datum/greyscale_config/configurations = list()
var/list/datum/greyscale_layer/layer_types = list()
#ifdef USE_RUSTG_ICONFORGE_GAGS
/// Cache containing a list of [UID (config path + colors)] -> [DMI file / RSC object] in the tmp directory from iconforge
var/list/gags_cache = list()
#endif
/datum/controller/subsystem/processing/greyscale/Initialize()
for(var/datum/greyscale_layer/fake_type as anything in subtypesof(/datum/greyscale_layer))
@@ -21,14 +31,35 @@ PROCESSING_SUBSYSTEM_DEF(greyscale)
var/datum/greyscale_config/config = configurations[greyscale_type]
config.Refresh()
#ifdef USE_RUSTG_ICONFORGE_GAGS
var/list/job_ids = list()
#endif
// This final verification step is for things that need other greyscale configurations to be finished loading
for(var/greyscale_type as anything in configurations)
CHECK_TICK
var/datum/greyscale_config/config = configurations[greyscale_type]
config.CrossVerify()
#ifdef USE_RUSTG_ICONFORGE_GAGS
job_ids += rustg_iconforge_load_gags_config_async(greyscale_type, config.raw_json_string, config.string_icon_file)
UNTIL(jobs_completed(job_ids))
#endif
return SS_INIT_SUCCESS
#ifdef USE_RUSTG_ICONFORGE_GAGS
/datum/controller/subsystem/processing/greyscale/proc/jobs_completed(list/job_ids)
for(var/job in job_ids)
var/result = rustg_iconforge_check(job)
if(result == RUSTG_JOB_NO_RESULTS_YET)
return FALSE
if(result != "OK")
stack_trace("Error during rustg_iconforge_load_gags_config job: [result]")
job_ids -= job
return TRUE
#endif
/datum/controller/subsystem/processing/greyscale/proc/RefreshConfigsFromFile()
for(var/i in configurations)
configurations[i].Refresh(TRUE)
@@ -36,12 +67,30 @@ PROCESSING_SUBSYSTEM_DEF(greyscale)
/datum/controller/subsystem/processing/greyscale/proc/GetColoredIconByType(type, list/colors)
if(!ispath(type, /datum/greyscale_config))
CRASH("An invalid greyscale configuration was given to `GetColoredIconByType()`: [type]")
if(!initialized)
CRASH("GetColoredIconByType() called before greyscale subsystem initialized!")
type = "[type]"
if(istype(colors)) // It's the color list format
colors = colors.Join()
else if(!istext(colors))
CRASH("Invalid colors were given to `GetColoredIconByType()`: [colors]")
#ifdef USE_RUSTG_ICONFORGE_GAGS
var/uid = "[replacetext(replacetext(type, "/datum/greyscale_config/", ""), "/", "-")]-[colors]"
var/cached_file = gags_cache[uid]
if(cached_file)
return cached_file
var/output_path = "tmp/gags/gags-[uid].dmi"
var/iconforge_output = rustg_iconforge_gags(type, colors, output_path)
// Handle errors from IconForge
if(iconforge_output != "OK")
CRASH(iconforge_output)
// We'll just explicitly do fcopy_rsc here, so the game doesn't have to do it again later from the cached file.
var/rsc_gags_icon = fcopy_rsc(file(output_path))
gags_cache[uid] = rsc_gags_icon
return rsc_gags_icon
#else
return configurations[type].Generate(colors)
#endif
/datum/controller/subsystem/processing/greyscale/proc/GetColoredIconByTypeUniversalIcon(type, list/colors, target_icon_state)
if(!ispath(type, /datum/greyscale_config))
@@ -58,3 +107,5 @@ PROCESSING_SUBSYSTEM_DEF(greyscale)
var/list/split_colors = splittext(color_string, "#")
for(var/color in 2 to length(split_colors))
. += "#[split_colors[color]]"
#undef USE_RUSTG_ICONFORGE_GAGS

View File

@@ -30,6 +30,9 @@
/// The md5 file hash for the json configuration. Used to check if the file has changed
var/json_config_hash
/// The raw string contents of the JSON config file.
var/raw_json_string
/// String path to the icon file, used for reloading
var/string_icon_file
@@ -105,13 +108,13 @@
var/changed = FALSE
json_config = file(string_json_config)
var/json_hash = md5asfile(json_config)
var/json_hash = rustg_hash_file("md5", string_json_config)
if(json_config_hash != json_hash)
json_config_hash = json_hash
changed = TRUE
icon_file = file(string_icon_file)
var/icon_hash = md5asfile(icon_file)
var/icon_hash = rustg_hash_file("md5", icon_file)
if(icon_file_hash != icon_hash)
icon_file_hash = icon_hash
changed = TRUE
@@ -123,7 +126,8 @@
if(!changed)
return FALSE
var/list/raw = json_decode(file2text(json_config))
raw_json_string = rustg_file_read(string_json_config)
var/list/raw = json_decode(raw_json_string)
ReadIconStateConfiguration(raw)
if(!length(icon_states))

View File

@@ -136,15 +136,18 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests())
var/data_filename = "data/screenshots/[path_prefix]_[name].png"
fcopy(icon, data_filename)
log_test("\t[path_prefix]_[name] was found, putting in data/screenshots")
else if (fexists("code"))
// We are probably running in a local build
fcopy(icon, filename)
TEST_FAIL("Screenshot for [name] did not exist. One has been created.")
else
// We are probably running in real CI, so just pretend it worked and move on
#ifdef CIBUILDING
// We are runing in real CI, so just pretend it worked and move on
fcopy(icon, "data/screenshots_new/[path_prefix]_[name].png")
log_test("\t[path_prefix]_[name] was put in data/screenshots_new")
#else
// We are probably running in a local build
fcopy(icon, filename)
TEST_FAIL("Screenshot for [name] did not exist. One has been created.")
#endif
/// Helper for screenshot tests to take an image of an atom from all directions and insert it into one icon
/datum/unit_test/proc/get_flat_icon_for_all_directions(atom/thing, no_anim = TRUE)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -11,6 +11,7 @@ fi
mkdir -p \
$1/_maps \
$1/code/datums/greyscale/json_configs \
$1/data/spritesheets \
$1/icons \
$1/sound/runtime \
@@ -25,6 +26,7 @@ fi
cp tgstation.dmb tgstation.rsc $1/
cp -r _maps/* $1/_maps/
cp -r code/datums/greyscale/json_configs/* $1/code/datums/greyscale/json_configs/
cp -r icons/* $1/icons/
cp -r sound/runtime/* $1/sound/runtime/
cp -r strings/* $1/strings/