Skip to content

Commit db59092

Browse files
committed
Add mesh relation disk (persistent) cache
- Dramatically speeds up reload time for mods, especially those that use a lot of exclusion/inclusion groups where the build time is the worst - See module comment in MeshRelDiskCache in MeshRelation.fs for more info - This also locks to FSharp.Core 4.4.3.0 for reasons noted there
1 parent 28e3f36 commit db59092

File tree

18 files changed

+11894
-15
lines changed

18 files changed

+11894
-15
lines changed
2.53 MB
Binary file not shown.
419 KB
Binary file not shown.
559 KB
Binary file not shown.

FSharp.Core.4.4.3.0/FSharp.Core.xml

Lines changed: 11732 additions & 0 deletions
Large diffs are not rendered by default.

MMLaunch/MMLaunch.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
</ProjectReference>
103103
<Reference Include="Accessibility" />
104104
<Reference Include="FSharp.Core">
105-
<HintPath>..\packages\FSharp.Core\lib\net45\FSharp.Core.dll</HintPath>
105+
<HintPath>..\FSharp.Core.4.4.3.0\FSharp.Core.dll</HintPath>
106106
</Reference>
107107
<Reference Include="Microsoft.VisualBasic" />
108108
<Reference Include="MonoGame.Framework">

MMLaunch/PreviewHost.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type PreviewHost() =
4747
CamPosition = Some(Vec3F(0.f,3.75f,10.0f))
4848
MeshReadFlags = { ReadMaterialFile = true; ReverseTransform = false }
4949
})
50+
Conf.BinCacheDir = ""
5051
}
5152
control <- Some(new MeshView.Main.MeshViewControl(conf, x.GraphicsDevice))
5253
()

MMManaged/CoreTypes.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ module CoreTypes =
300300

301301
/// Storage for a named Reference object.
302302
/// The Name of a reference is its base file name (no extension).
303+
/// The "DB" prefix naming convention alludes to it being an entity in a "Database" (the mod db).
303304
type DBReference = {
304305
Name : string
305306
Mesh : Lazy<Mesh>
@@ -322,6 +323,9 @@ module CoreTypes =
322323

323324
/// Storage for a named mod.
324325
/// The Name of a mod is its base file name (no extension).
326+
/// The "DB" prefix naming convention alludes to it being an entity in a "Database" (the mod db).
327+
/// Do not confuse this type with the `ModDB` type which is the actual database of all the mods and references.
328+
/// (A bit of a naming fail there, alas).
325329
type DBMod = {
326330
RefName: string option
327331
Type: ModType

MMManaged/MMManaged.fsproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,14 @@
8383
<None Include="LoadInFSI.fsx" />
8484
<None Include="TestFSI.fsx" />
8585
<Compile Include="Program.fs" />
86+
<Content Include="packages.config" />
8687
</ItemGroup>
8788
<ItemGroup>
8889
<Reference Include="FSharp.Core">
89-
<HintPath>..\packages\FSharp.Core\lib\net45\FSharp.Core.dll</HintPath>
90+
<HintPath>..\FSharp.Core.4.4.3.0\FSharp.Core.dll</HintPath>
91+
</Reference>
92+
<Reference Include="FsPickler">
93+
<HintPath>..\packages\FsPickler.5.3.2\lib\net45\FsPickler.dll</HintPath>
9094
</Reference>
9195
<Reference Include="mscorlib" />
9296
<Reference Include="SharpDX">

MMManaged/MeshRelation.fs

Lines changed: 135 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,100 @@ module MeshRelation =
6868
CpuSkinningData: CPUSkinningData option
6969
}
7070

71-
type MeshRelation(md:DBMod, ref:DBReference) =
71+
/// Utility module to cache mesh relations to disk. Because these
72+
/// are so inefficient to build (see code below) this dramatically speeds
73+
/// up reload times since most mods and references aren't changing
74+
/// from run-to-run.
75+
///
76+
/// This uses FsPickler which is apparently very sensitive to FSharp.Core
77+
/// versions - it was failing until I locked everything to use 4.4.3.0
78+
/// (which is what VS2019 wants to use) everywhere. This is also fixable
79+
/// by using a .config file next to the injected exe, which contains a
80+
/// binding redirect, but since that requires dropping extra conf files
81+
/// everywhere I chose not to do that.
82+
/// Note this code was mostly generated by LLM (OpenAI GPT 5.2-Thinking)
83+
module private MeshRelDiskCache =
84+
open System.IO.Compression
85+
open MBrace.FsPickler
86+
87+
let private ser = FsPickler.CreateBinarySerializer()
88+
let private cacheVersion = 1
89+
90+
type MeshSig = {
91+
Path: string
92+
Ticks: int64
93+
Size: int64
94+
ModType: ModType
95+
Flags: MeshReadFlags
96+
}
97+
98+
type Entry = {
99+
Version: int
100+
Mod: MeshSig
101+
Ref: MeshSig
102+
VertRels: VertRel[]
103+
}
104+
105+
let private fileSig (path: string) =
106+
let fi = FileInfo(path)
107+
fi.LastWriteTimeUtc.Ticks, fi.Length
108+
109+
let private mkModSig (md: DBMod) =
110+
let t,s = fileSig md.MeshPath
111+
{ Path = md.MeshPath; Ticks = t; Size = s; ModType = md.Type; Flags = md.MeshReadFlags }
112+
113+
let private mkRefSig (r: DBReference) =
114+
let t,s = fileSig r.MeshPath
115+
{ Path = r.MeshPath; Ticks = t; Size = s; ModType = ModType.Reference; Flags = r.MeshReadFlags }
116+
117+
let private key (modName: string) (refName: string) =
118+
let s = modName.ToLowerInvariant() + "|" + refName.ToLowerInvariant()
119+
use sha = System.Security.Cryptography.SHA256.Create()
120+
sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(s))
121+
|> Seq.map (fun b -> b.ToString("x2"))
122+
|> String.concat ""
123+
124+
let relPath (cacheDir: string) modName refName =
125+
Path.Combine(cacheDir, "MeshRelations", key modName refName + ".bin.gz")
126+
127+
let tryLoad (cacheDir: string) (md: DBMod) (r: DBReference) : VertRel[] option =
128+
let p = relPath cacheDir md.Name r.Name
129+
if not (File.Exists(p)) then None
130+
else
131+
use fs = File.OpenRead(p)
132+
use gz = new GZipStream(fs, CompressionMode.Decompress)
133+
let e = ser.Deserialize<Entry>(gz)
134+
135+
if e.Version <> cacheVersion then None
136+
else
137+
let ms = mkModSig md
138+
let rs = mkRefSig r
139+
if e.Mod = ms && e.Ref = rs then Some e.VertRels else None
140+
141+
let save (cacheDir: string) (md: DBMod) (r: DBReference) (vrs: VertRel[]) =
142+
let dir = Path.Combine(cacheDir, "MeshRelations")
143+
Directory.CreateDirectory(dir) |> ignore
144+
let p = relPath cacheDir md.Name r.Name
145+
let tmp = p + ".tmp"
146+
147+
log.Info "[meshrelcache]: creating bincache entry: %A for mod=%A ref=%A" tmp md.Name r.Name
148+
149+
let e =
150+
{
151+
Entry.Version = cacheVersion
152+
Mod = mkModSig md
153+
Ref = mkRefSig r
154+
VertRels = vrs
155+
}
156+
157+
use fs = File.Create(tmp)
158+
use gz = new GZipStream(fs, CompressionMode.Compress)
159+
ser.Serialize(gz, e)
160+
161+
if File.Exists(p) then File.Delete(p)
162+
File.Move(tmp, p)
163+
164+
type MeshRelation(md:DBMod, ref:DBReference, binCacheDir:string) =
72165
let verifyAndGet(name:string) (mo:Lazy<Mesh> option) =
73166
match mo with
74167
| None -> failwithf "cannot build vertrel for mod/ref with no mesh: %A" name
@@ -273,16 +366,48 @@ module MeshRelation =
273366

274367
modVertRels
275368

276-
let buildIt() =
277-
//log.Info "Starting build of meshrelation for mod: %A" md.Name
278-
modMesh <- Some ((verifyAndGet md.Name md.Mesh).Force())
279-
refMesh <- Some ((ref.Mesh.Force()))
369+
let buildIt() =
370+
let binCacheDir = binCacheDir.Trim()
371+
let useBinCache =
372+
if binCacheDir = "" then false
373+
else
374+
if not (Directory.Exists binCacheDir) then Directory.CreateDirectory binCacheDir |> ignore
375+
true
376+
377+
let loadFresh() =
378+
let sw = new Util.StopwatchTracker("MeshRel:" + md.Name + "/" + ref.Name)
379+
let vertRels = buildVertRels()
380+
log.Info "built mesh relation from mod '%s' to ref '%s'" md.Name ref.Name
381+
sw.StopAndPrint()
382+
383+
vertRels
280384

281-
let sw = new Util.StopwatchTracker("MeshRel:" + md.Name + "/" + ref.Name)
282-
let vertRels = buildVertRels()
283-
log.Info "built mesh relation from mod '%s' to ref '%s'" md.Name ref.Name
284-
sw.StopAndPrint()
285-
vertRels
385+
modMesh <- Some ((verifyAndGet md.Name md.Mesh).Force())
386+
refMesh <- Some (ref.Mesh.Force())
387+
388+
let cacheVROpt =
389+
if useBinCache then
390+
try
391+
MeshRelDiskCache.tryLoad binCacheDir md ref
392+
with e ->
393+
log.Error "%A" e
394+
None
395+
else None
396+
397+
match cacheVROpt with
398+
| Some cachedVertRels ->
399+
log.Info "[meshrelcache]: loaded mesh relation from cache for mod '%s' to ref '%s' (cache file: %A)" md.Name ref.Name
400+
(MeshRelDiskCache.relPath binCacheDir md.Name ref.Name )
401+
cachedVertRels
402+
| None ->
403+
let vertRels = loadFresh()
404+
405+
if useBinCache then
406+
try
407+
MeshRelDiskCache.save binCacheDir md ref vertRels
408+
with e ->
409+
log.Error "%A" e
410+
vertRels
286411

287412
let vertRels = lazy (buildIt())
288413

MMManaged/ModDB.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,7 @@ module ModDB =
698698

699699
let newMeshRel() =
700700
nBuilt <- nBuilt + 1
701-
new MeshRelation(dbmod, Option.get dbmod.Ref)
701+
new MeshRelation(dbmod, Option.get dbmod.Ref, conf.BinCacheDir)
702702

703703
// if both of the meshes are unchanged (or if loading them would use a cache entry, meaning they are unchanged)
704704
// we can reuse the mesh relation. the case where the mesh relation exists but the meshes aren't loaded, yet they are cached,

0 commit comments

Comments
 (0)