MU3 Optimizer is a set of MonoMod patches for MU3 sddt160. The patches cut down the rendering and CPU work the game does during play, avoid a lot of throwaway memory allocations, and shorten the wait at login. Each patch has its own switch in mu3.ini, so you can turn off anything you don't want.
- Game assembly: MU3 sddt160
- Runtime: .NET Framework 3.5
- Patch loader: MonoMod through BepInEx
- GPU text path: Windows Direct3D 11
-
Copy these files to
package/BepInEx/monomod/:Assembly-CSharp.Steroid.mm.dll AMDaemon.NET.Steroid.mm.dll -
Put
mu3.iniin the game's working directory, which is normallypackage/. If you keep it somewhere else, pointMU3_MODS_CONFIG_PATHat it. -
Start the game.
MonoMod reads mu3.ini when it patches the game at startup, so restart the game after you change a setting.
When at least one patch is enabled, the game log shows [Steroid] loaded. shortly after startup. Messages from individual patches start with [Steroid][<Patch>]. A warning means the patch hit something it couldn't handle and fell back to the game's normal behavior. An error means something failed outright. With every patch disabled, the mod doesn't change how the game behaves and doesn't log anything. It never hides messages from the game or BepInEx.
Each switch under [Optimization] turns one patch on (1) or off (0). A switch you leave out uses its default: NoImageBloom, NoUICameraDuringPlay, and PrimitiveMeshEmission are off by default, and everything else is on. Some patches have extra settings in their own [Optimization.<PatchName>] section. The example below matches the defaults.
[Optimization]
NoImageBloom=0
RenderLayers=1
SwingJointPhysics=1
InactiveMirrorIndicator=1
NoUICameraDuringPlay=0
GpuTextScroll=1
LoginRequestsBatching=1
AsyncLoginRequests=1
NoteBatching=1
PrimitiveMeshEmission=0
GameplayPrewarm=1
UVAnimation=1
CollabSocketCaching=1
CollabHeartbeatCaching=1
CollabMemberListCompaction=1
InlinedAMDaemonCalls=1
UnityPlayerHooks=1
[Optimization.RenderLayers]
; How often each layer redraws during normal play:
; negative = every frame
; 0 = only when forced, or when an earlier layer changes
; positive = at most this many times per second
; The layers feed into each other (stage, then background merge, then FX), and
; redrawing one layer also redraws the layers after it. Give FXFPS the highest
; rate, then BGMergeFPS, then StageFPS. At 60, the cabinet's frame rate, nothing
; changes at 60 FPS, and faster displays skip the extra layer redraws. Lower
; values such as StageFPS=0, BGMergeFPS=0, FXFPS=30 save more work but make the
; background less smooth.
StageFPS=60
BGMergeFPS=60
FXFPS=60
; Turns off shadows after the opening cutscene.
DisableShadows=1
[Optimization.SwingJointPhysics]
; Most physics updates per second. 0 or below updates every rendered frame.
SwingJointFPS=60
SwingJointNative=0
[Optimization.UnityPlayerHooks]
GpuFenceWait=1
JobSignalBatching=1Bloom, the soft glow around bright objects, takes several extra rendering passes and temporary textures every time the camera renders. This patch removes the glow and keeps the rest of the image effect the camera needs. The trade-off is visible: bright objects no longer glow.
It is off by default because it changes how the game looks. It is most likely to help when the GPU is the bottleneck.
The game draws the stage, a merged background, and the background effects with separate cameras on every frame. This patch keeps the last image of each layer and redraws a layer only as often as its rate setting allows. The final picture on screen is still drawn every frame. By default each layer redraws at most 60 times a second, the cabinet's frame rate, so the picture doesn't change at 60 FPS and faster displays skip the extra redraws.
Every layer redraws each frame whenever the game needs it: at the start of a battle, during the opening cutscene, around damage, wave-change, and overkill effects, and whenever the game isn't in normal play. DisableShadows=1, which is on by default, also turns off shadows after the opening cutscene.
Rates below your frame rate can make animated backgrounds look less smooth, and turning off shadows changes the scene's lighting.
Hair, costumes, and accessories sway using physics that runs once per rendered frame and doesn't account for frame time. On a display faster than the cabinet's 60 FPS, the sway moves too fast, and the game does more physics work than it needs to. This patch runs the physics at most SwingJointFPS times a second (60 by default), which brings back the intended motion and saves the extra work. It also skips joints on hidden objects and does less repeated work per joint.
SwingJointNative=1 moves the physics into a native helper bundled with the mod. The helper is only used after a live check confirms that it reads and moves the game's objects exactly like the normal code. If that check or a later update fails, the patch keeps using the normal physics code.
With mirror mode off, the game still keeps the mirror indicator active and just shows a blank image, so the UI system keeps processing it. This patch switches the indicator off instead. Nothing on screen changes.
The game draws its menus and play HUD with a separate UI camera. During a song, this patch draws those canvases directly on top of the screen, so the UI camera can be switched off, and it skips the UI system's per-frame update. It stays active from the moment a song is about to start until play ends. With GameplayPrewarm, that includes the time the music waits for preparation to finish. When a dialog appears, the normal UI camera comes back.
This patch is off by default and experimental. Unusual dialog or error sequences can make UI elements draw in the wrong order.
Text that is too long for its box scrolls sideways, and the game rebuilds that text's shape on the CPU every frame to move it. This patch moves the scrolling and clipping into a Direct3D 11 shader, so the CPU only updates the scroll position.
Text the shader can't handle keeps using the normal path. That includes systems without Direct3D 11, dynamic fonts, glowing (emission) text, masked text, and uncommon materials or text effects.
At login the game downloads your characters, items, music, cards, music items, and rival music data in pages of 50, with one request per page. This patch asks for each list in a single request, which removes a lot of round trips for large accounts when the server allows it.
The server can still enforce its own page size, and bigger responses use more memory while they load.
After your basic user record arrives, the game fetches the rest of your data one request at a time and waits for each response before sending the next. This patch sends the requests that don't depend on each other at the same time and keeps the ones that do in their original order.
Everything still runs on the game's main thread. The patch only overlaps the waiting. A network error stops the group it happens in, and how much time you save depends on how many parallel requests the server accepts.
Every note object normally ends up with its own private copy of its material, which stops the GPU from drawing similar notes together. This patch looks at each pooled note once, groups notes that use the same material and mesh, and gives each group one shared material. Notes in a group can then be drawn together with GPU instancing, and reusing a note from the pool no longer repeats the setup.
Notes with different shaders, meshes, or drawing order still need separate groups.
The game rebuilds the lane and hold-note meshes every frame by adding points one at a time. This patch collects the drawing commands for a frame and writes the whole mesh at once through a native helper bundled with the mod. If the helper can't be used, it writes the mesh directly from managed code, and if that isn't possible either, it keeps the game's original method.
It is off by default because testing didn't show a meaningful difference. Leave it off unless it clearly helps on your setup.
The first time a song shows a judgement, updates the score, or plays a wave-change or overkill effect, Unity has to load and prepare everything involved, which can cause a stutter in the middle of play. This patch does that work before the song starts, and the music waits until it is finished. It renders throwaway copies of the judgement and score displays off screen, prepares the wave-change and overkill effects ahead of time and keeps them paused out of sight until the game plays them, and has the runtime compile the code those moments use.
The copies are separate from the real game objects, so the actual score and effects aren't touched. If the prepared effects run out, the game creates new ones as usual. If preparation is cancelled or fails, the patch cleans up after itself and the song doesn't start.
It replaces the old JudgementPrewarm setting, which no longer does anything.
Scrolling textures and sprite-sheet animations normally give every animated object its own copy of its material, which stops those objects from being drawn together. This patch animates them with per-object overrides instead, so they keep sharing the original material.
These patches cover the collaboration network code, which keeps running during normal play, not only in party play. They reuse network objects instead of creating new ones for every packet and heartbeat. With them turned off, normal play has shown serious garbage-collection problems. The network protocol doesn't change.
Reuses network sockets and addresses when the address and port haven't changed, instead of rebuilding them or creating new address objects for every packet. Reopening a connection that hasn't changed does nothing.
Reuses one heartbeat request and one heartbeat response instead of creating new ones for every heartbeat.
Removes disconnected or expired party members from the member lists in place, in fewer passes and without temporary lists. Notifications about members leaving still happen as before.
Every call the game makes into AMDaemon goes through a small wrapper that creates a new delegate object and adds an extra indirect call. This patch rewrites those calls to go straight to the native function. The same error check still runs after every call.
UnityPlayerHooks patches two places inside Unity's own engine code, which a normal MonoMod patch can't reach. The hooks come from a native library embedded in the mod and are installed right after the game starts. Set UnityPlayerHooks=0 to turn both off, or turn each one off separately under [Optimization.UnityPlayerHooks].
The hooks find their targets by scanning for known code patterns, so they support both the regular and the development build of the Unity 5.6.4f1 x64 player. If a hook can't find its target exactly, it is skipped with a warning in the log and Unity's original code runs.
When Unity waits for the GPU to finish a frame, it waits on a GPU query. This hook waits for a fence event from the graphics driver instead and then checks the original query, so Unity still gets the same results and handles errors the same way. It needs a GPU and driver with Direct3D 11 fence support. Without that, or if anything fails, Unity's normal wait is used.
In testing, the fence wait performed the same as waiting with a precise timer, and the precise timer was clearly faster than Unity's normal wait. The fence has not been compared directly with Unity's normal wait.
Unity wakes its worker threads when there is work for them. That code has a counting bug: when many workers are waiting, handing out a little work can wake all of them at once. This hook fixes the check so only as many workers wake as there is work for, and wakes several workers with one system call instead of one call each. The number of workers and the way jobs are handed out don't change.
If a combined wake-up fails, the hook falls back to waking the workers one at a time. It stays installed until the game exits, because Unity can still wake workers while it shuts down.
Requirements:
- Docker
mu3-reference/next to this repository, with its reference source projects and the Unity/.NET DLLs underlib/- Unity credentials in
UNITY_EMAILandUNITY_PASSWORD - Zig 0.16.0 for the
x86_64-windows-gnuplayer hook, and Rust/Cargo with thex86_64-pc-windows-gnutarget for the geometry and swing helpers - MinGW-w64 with
x86_64-w64-mingw32-gcc
Build everything:
UNITY_EMAIL=... UNITY_PASSWORD=... ./build.sh allBuild individual stages:
UNITY_EMAIL=... UNITY_PASSWORD=... ./build.sh build-assets
./build.sh build-native
./build.sh build-main
./build.sh patchbuild-native builds every project under Native/ that has a build.zig or Cargo.toml and collects the Windows DLLs into Native/Build. build-main builds the reference projects and then the optimizer. The container needs write access to the reference projects' generated obj/ and artifacts/ directories. patch applies both optimizer modules to those build outputs and writes portable PDBs next to the patched assemblies.
Outputs:
bin/Release/net35/Assembly-CSharp.Steroid.mm.dll
bin/Release/net35/AMDaemon.NET.Steroid.mm.dll
bin/Release/net35/MONOMODDED_Assembly-CSharp.dll
bin/Release/net35/MONOMODDED_AMDaemon.NET.dll
Install only the two .mm.dll files if you are using BepInEx MonoMod loader. The MONOMODDED_* assemblies can be used on cab environment where MonoMod loader isn't applicable.
Assembly-CSharp/Startup.cs: the[Steroid] loaded.startup hook shared by every switch.Assembly-CSharp/MonoMod/StateHooks.cs:[OnStateEnter("<State>")]and[OnStateLeave("<State>")]attach a switch-gatedstatic void (THolder)method to a game state-machine transition. Patching inserts the call into the holder's finalEnter_<State>orLeave_<State>method, and creates the method if it doesn't exist, so no patch has to own it.Assembly-CSharp/Features/<Switch>/andAMDaemon.NET/Features/<Switch>/: everything that belongs to one[Optimization]switch, in namespaceMU3.Mod.<Switch>. MonoMod rules stay in theMonoModnamespace.Assembly-CSharp/Runtime/: embedded native-module and AssetBundle loading, and the frame-rate limiter, shared by several switches.*/MonoMod/Rules.cs: the MonoMod rules entry points.Common/holds the configuration and patch lifecycle code shared by both projects.