Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

'Features' #14

Open
Sewer56 opened this issue Dec 29, 2023 · 2 comments
Open

'Features' #14

Sewer56 opened this issue Dec 29, 2023 · 2 comments

Comments

@Sewer56
Copy link
Member

Sewer56 commented Dec 29, 2023

[Another Self Reminder]

Context

This functionality is based on Rust 'features' in cargo:

Reloaded-III will be built based on a modular model of Pay for What You Need.

Quite simply, if you don't use a feature, then you should not pay the cost for it in RAM / CPU / Startup Time.

We can give the illusion of larger monolithic mods that the user expects by grouping together settings from various mods and then conditionally loading in additional mods at runtime as needed.

Also note, features can only be activated via mod config changes.

Problems

Features via Settings

User does not want settings to be split between 20 million different mods.

This problem is already visible with Reloaded-II.

Many end users don't know, for example that in Sonic Heroes, you need a separate mod Post Process for Controller Hook than the main controller mod ('main' mod being XInput for Controller Hook or Custom Mapping for Controller Hook) to adjust deadzones so their stick doesn't drift in freecam. These settings should ideally be displayed as part of the same menu, even if the functionality exists in different mods.

Proposed solution for this is to split the config into multiple sections with an Enable toggle. If a section is enabled, a dependency (or set of dependencies) that act according to the settings of that section will be enabled at load time.

Handling cases of mods with separate binary for their settings from UX perspective is TBD.

Feature Auto Enabling

i.e. For mod authors, most basic mods should only need to specify a dependency on 'X Game Support for Reloaded III', a.k.a. Layer 2 Mods.

These mods should not be monolithic in nature, and need to use the 'pay for what you need' model too.

However, you cannot expect mod authors to know which 'features' their mod needs, that would make it difficult for mod authors to create mods. Instead, at least one of the following should be true:

  • Inspect other mods' contents at load time, conditionally enable features.
  • Enable a feature if another mod is enabled for whatever reason.
  • Provide 'diagnostics' in launcher (server) that tell users which features they're missing.
    • Maybe even auto-enable/disable features based on file path globs.

Some further planning is required here, but I'd like to try going with the auto-enable/disable approach for this.

File path globs to auto enable features in dependants and watching filesystem for changes should be sufficient.


Although some platforms may not support FileSystem events (e.g. embedded/console servers); scanning for file changes before boot on those platforms may not be the end of the world.

If we use my SATA SSD laptop on Microsoft Windows as a rough ballpark, we can discover around 100000 files per second. With a naive implementation of comparing modify dates of all files, it is expected for the process to take no longer than 1 second, even with a ton of mods on a 2016 phone.

However, as the system is based on globs, adding/renaming/modifying a file should update the folder's last modify date, on just about every common system; so that can be used to greatly accelerate the search on those platforms.

@Sewer56
Copy link
Member Author

Sewer56 commented Feb 29, 2024

Something I had in mind yesterday.


Delivering Features in Code Mods

In the past, I originally thought of making it so additional 'features' are their own packages.
However, that very greatly makes deployment much more complex and difficult to deal with.

Instead, we could pre-ship every single possible feature combination as part of the main package.

An Example

A mod has the following features:

  • debug (debug build, including logging & PDB)
  • dump (e.g. dumping files out of file emulator)

With the following architecture targets:

  • i686
  • aarch64
  • x86-64-v1
  • x86-64-v2
  • x86-64-v3

This yields us with the following combinations:

  • bin/i686/none
  • bin/i686/debug
  • bin/i686/debug-dump
  • bin/i686/dump
  • bin/x86-64-v1/none
  • bin/x86-64-v1/debug
  • bin/x86-64-v1/debug-dump
  • bin/x86-64-v1/dump
  • bin/x86-64-v2/none
  • bin/x86-64-v2/debug
  • bin/x86-64-v2/debug-dump
  • bin/x86-64-v2/dump
  • bin/x86-64-v3/none
  • bin/x86-64-v3/debug
  • bin/x86-64-v3/debug-dump
  • bin/x86-64-v3/dump
  • bin/aarch64/none
  • bin/aarch64/debug
  • bin/aarch64/debug-dump
  • bin/aarch64/dump

(20 builds).

Different platforms, e.g. win, mac, linux will have separate packages.

Side Effects

Note

We sacrifice negligible amount of Disk Space for improvements in load time and CPU usage.

Assuming that each build is approx 100kB, having 20 builds yields a package of 2.0MB.
By all accounts that isn't that bad, it's same as the size of a single 2048x2048 BC7 texture.

In the future, I hope to use File Emulation tech to simulate files inside mods while they are actually located inside Nx Archives.

By grouping all of the different feature builds for each architecture into a single SOLID block, and using deterministic builds, the duplicate code between the different builds should be eliminated during compression. The end result should roughly be equivalent compared to having just 1 build as opposed to a build for each combination of features.

Counting All Feature Permutations

Both the debug and dump features have two states, 'enabled' or 'disabled'.

So counting and calculating all possibilities is a binary operation:

  • 2 ^ (num_features)

Same as counting max number in binary, i.e. 1, 2, 4, 8, 16 etc.

Intent

'Features' are intended to be used to deliver functionality that may be undesirable to have in regular release builds of mods.

Examples:

  • debug: Debug Logging / Debug Builds (w/ Symbols)
  • modder: Adds features only useful to mod authors.
  • dev: Adds features only useful to developers.

A More Specific Example:

You're a mod that hooks into DirectX and allows for texture replacement.
DirectX' SetTexture runs at approx ~3000 times per frame.

Shaving code from SetTexture improves performance, so rather than checking at runtime during every call, you make it a 'feature'.

  • animated: Activated when any enabled texture mod has an animated texture.
    • And also your DLL is smaller, as you don't carry dead code anymore.

Expectation

Mod Loader and Core Layer 0 & 1 Mods, i.e.

  • Backends
  • Middleware
  • Libraries

Are expected to ship architecture specific optimized binaries and feature flags (where beneficial).
This also applies to complex 'game support' mods. It's expected the more 'smart' people will probably work on game cores.

Regular game specific mods (Layer 3) have no expectations, it's unfair to expect things of random people.

It's expected that in a user's typical more complex mod/game setup, there will probably be around 10 mods using 'features' with code. Amounting to ~20MB on Disk (using earlier example).

@Sewer56
Copy link
Member Author

Sewer56 commented May 10, 2024

Merging Mods through Features

In an effort to maximize the efficiency of Reloaded-III and completely obliterate binary size, I've been considering the idea of 'merging' mods through the use of 'features' to essentially build a monolithic loader.

For example, let's consider the following additional 'core' mods/packages:

  • Hooks Library: reloaded3.utility.hooks.s56
  • Signature Scanner: reloaded3.utility.sigscan.s56
  • Merged File Cache: reloaded3.utility.mergedfilecache.s56
  • Virtual FileSystem: reloaded3.api.windows.vfs.s56
  • File Emulation Framework: reloaded3.api.windows.fef.s56

If a user has both the core loader reloaded3.loader.s56, hooks and signature scanning enabled (a very common setup), we could instead load a version of the loader (reloaded3.loader.s56) with the two dependencies enabled. i.e. This would be the loader with features hooks and scanner enabled.

This would help minimize binary sizes and improve load times. In particular, binary size would be reduced thanks to:

How it works?

Because we obtain dependencies through loader's built in DI

_redirector = _injector.GetService<IRedirector>();

We can be more flexible. The caller does not need to specify where exactly this named dependency comes from.

Problems

Code Design

The code should be designed such that the different embedded packages can still function as standalone mods. The process should involve creating the standalone mods first, and then embedding them into the loader based on the user's needs, without modifying the mods' code in any way.

This idea/feature should be expressed as I have a specialised copy of the loader that includes these features, so I will disable the originals.

UX/Communication

Loader must make it clear in the log which dependencies were merged; and that we're loading a merged dependency as opposed to the standalone one.

For example, the loader could read:

This loader was built with features: hooks, sigscan

Testing

The system needs to be designed in a way where the standalone components can still be ran separately without the monolithic loader build, i.e. something like 'Use a version without me embedded'.

Versioning

We must not use the embedded components if they are out of date with upstream.

If the user has a newer version of an embedded component installed, the system should prioritize the standalone component and disable the embedded feature to ensure the most up-to-date version is used.

API Compatibility

Any item embedded into the loader must never break its API.

Replacing the system outright is ok though, so hooks could be deprecated for hooks2, if there was hypothetically a need to do that though.

The recommended approach however is to provide a backcompat layer that tarnslates hooks API to hooks2.

Nested Features

Embedded mods might have features of their own.

For instance, if the 'hooks' mod has a feature called 'wrappers', it would be propagated as 'hooks/wrappers' when embedded into the loader.

In some cases a feature of the nested project may map to a feature of the parent, for example debug and hooks/debug. That should be unified under debug simply.

No Special Casing

We're not Rust's std.

This system should be designed in a generic way, allowing not only the loader but also other mods to be merged. For example, users might want to merge all the 'core mods' for a specific game into a single package.

Something that says

  • 'My Mod has feature X which replaces Mod Y with version >= Z'

This relationship might be 1 to many, so 1 feature could enable/disable many mods.

Other Notes

Build Times

This should not be a concern.

While having a large number of builds (e.g., 256) might seem concerning, the process should be relatively quick as each build is essentially an incremental build from the previous one. The primary focus is on minimizing the final binary size rather than the build time.

Groupings

In order to prevent the build count exploding, different embedded mods could be bundled, for example hooks and sigscan could be hooks+sigscan.

In addition, you should measure the actual savings after merging, rather than blindly providing a new feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant