namespace Mibo.Elmish.Graphics2D open System.Collections.Generic open Microsoft.Xna.Framework.Graphics open Mibo.Elmish /// /// Provides pooled render targets to avoid per-frame allocation and disposal /// of resources. /// /// /// Acquired targets remain in use until /// is called, typically once per frame. Targets are keyed by dimensions and reused /// across frames without being disposed, avoiding GPU allocation overhead. /// type IRenderTargetPool = /// /// Acquires a render target matching the given dimensions. /// Reuses a previously released target if available, otherwise creates a new one. /// /// A render target with the specified width and height. abstract Acquire: width: int * height: int -> RenderTarget2D /// /// Returns all currently held targets to the pool. Call once per frame /// after rendering is complete. Targets are retained for future reuse. /// abstract ReleaseAll: unit -> unit /// /// Default implementation of /// using a dictionary keyed by (width, height) dimensions. /// Stores targets in per-dimension queues for FIFO reuse. /// /// /// Dispose the pool when the application shuts down to dispose all pooled targets. /// Idle targets kept per dimension are capped by /// so that repeated window resizes (which produce many distinct dimensions) don't /// retain GPU memory for sizes that may never be requested again. Excess idle /// targets are disposed at time. /// type RenderTargetPool(gd: GraphicsDevice, ?maxIdlePerDimension: int) = // Maximum idle targets retained per (width, height) key. Anything beyond this // is disposed at ReleaseAll rather than kept, bounding memory growth when the // app sees many distinct dimensions over its lifetime (e.g. during window // resizing). 2 is enough for ping-pong post-processing chains. let maxIdle = defaultArg maxIdlePerDimension 2 let pool = Dictionary>() let inUse = ResizeArray() interface IRenderTargetPool with member _.Acquire(width, height) = let key = struct (width, height) match Dictionary.tryGetValue key pool with | ValueSome queue when queue.Count > 0 -> let rt = queue.Dequeue() inUse.Add(rt) rt | _ -> let rt = new RenderTarget2D( gd, width, height, false, SurfaceFormat.Color, DepthFormat.None, 0, RenderTargetUsage.DiscardContents ) inUse.Add(rt) rt member _.ReleaseAll() = for rt in inUse do let key = struct (rt.Width, rt.Height) match Dictionary.tryGetValue key pool with | ValueSome queue when queue.Count < maxIdle -> queue.Enqueue(rt) | ValueSome _ -> // Per-dimension idle cap reached: dispose the excess rather than // retaining it, so dimensions seen only during transient resizes // don't leak GPU memory. rt.Dispose() | ValueNone -> let queue = Queue() queue.Enqueue(rt) pool[key] <- queue inUse.Clear() interface System.IDisposable with member _.Dispose() = for rt in inUse do rt.Dispose() inUse.Clear() for KeyValueV(_, queue) in pool do for rt in queue do rt.Dispose() queue.Clear() pool.Clear()