@@ -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
0 commit comments