From b0a2e511cd04a13e862e8cc60183b845c5a05e67 Mon Sep 17 00:00:00 2001 From: SnipUndercover Date: Tue, 24 Mar 2026 00:55:24 +0100 Subject: [PATCH 01/10] Bump `.csproj` language version Everest already needs the .NET 9 SDK to compile, so we might as well utilize its features. --- Celeste.Mod.mm/Celeste.Mod.mm.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Celeste.Mod.mm/Celeste.Mod.mm.csproj b/Celeste.Mod.mm/Celeste.Mod.mm.csproj index 058323b29..dccd11f7a 100644 --- a/Celeste.Mod.mm/Celeste.Mod.mm.csproj +++ b/Celeste.Mod.mm/Celeste.Mod.mm.csproj @@ -5,7 +5,8 @@ Celeste.Mod.mm Celeste true - 11 + + 13 false From 48a7332a260446fd233c3d28b0177a6e84ad24b5 Mon Sep 17 00:00:00 2001 From: SnipUndercover Date: Tue, 24 Mar 2026 01:39:17 +0100 Subject: [PATCH 02/10] Implement temporary SpriteBatches --- .../Mod/Helpers/TemporarySpriteBatch.cs | 244 +++++++++++++++ .../Helpers/TemporarySpriteBatchBuilder.cs | 281 ++++++++++++++++++ 2 files changed, 525 insertions(+) create mode 100644 Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs create mode 100644 Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs diff --git a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs new file mode 100644 index 000000000..bc06d3852 --- /dev/null +++ b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs @@ -0,0 +1,244 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Monocle; +using MonoMod.Utils; +using System.Diagnostics.CodeAnalysis; + +namespace Celeste.Mod.Helpers; + +/// +/// A temporary . +/// +/// +/// When constructed, the current properties and are preserved. +/// Then, the is ended, the is swapped if necessary, +/// and finally the is restarted with the new properties.
+/// When disposed, the previous properties and are restored. +///
+/// Useful when interrupting a mid-render to, for example, render a specific entity to a +/// temporary while applying a custom shader, all while preserving the previous +/// configuration.
+///
+/// Note: Restarting a spritebatch flushes it to the GPU, which is costly. +///
+/// +public ref struct TemporarySpriteBatch : IDisposable +{ + /// + /// The of this . + /// + public readonly SpriteSortMode CurrentSortMode; + + /// + /// The of this . + /// + [NotNull] + public readonly BlendState CurrentBlendState; + + /// + /// The of this . + /// + [NotNull] + public readonly SamplerState CurrentSamplerState; + + /// + /// The of this . + /// + [NotNull] + public readonly DepthStencilState CurrentDepthStencilState; + + /// + /// The of this . + /// + [NotNull] + public readonly RasterizerState CurrentRasterizerState; + + /// + /// The custom of this . + /// + [MaybeNull] + public readonly Effect CurrentCustomEffect; + + /// + /// The transformation of this . + /// + public readonly Matrix CurrentTransformMatrix; + + /// + /// The swapped in for the duration of this + /// or null to draw to the screen if is true; else always null. + /// + [MaybeNull] + public readonly RenderTarget2D CurrentRenderTarget; + + + /// + /// The that was used prior to the start of this + /// . + /// + public readonly SpriteSortMode PreviousSortMode; + + /// + /// The that was used prior to the start of this + /// . + /// + [NotNull] + public readonly BlendState PreviousBlendState; + + /// + /// The that was used prior to the start of this + /// . + /// + [NotNull] + public readonly SamplerState PreviousSamplerState; + + /// + /// The that was used prior to the start of this + /// . + /// + [NotNull] + public readonly DepthStencilState PreviousDepthStencilState; + + /// + /// The that was used prior to the start of this + /// . + /// + [NotNull] + public readonly RasterizerState PreviousRasterizerState; + + /// + /// The custom that was used prior to the start of this + /// . + /// + [MaybeNull] + public readonly Effect PreviousCustomEffect; + + /// + /// The transformation that was used prior to the start of this + /// . + /// + public readonly Matrix PreviousTransformMatrix; + + /// + /// The s that were used prior to the start of this + /// or null to draw to the screen if is true; else always null. + /// + [MaybeNull] + public readonly RenderTargetBinding[] PreviousRenderTargets; + + + /// + /// Whether a was swapped in for the duration of this + /// . + /// + public readonly bool HasRenderTarget; + + /// + /// Whether this is still active. + /// + public bool Active { get; private set; } + + + /// + /// Create and immediately begin a new . + /// + /// + internal TemporarySpriteBatch( + bool hasSortMode, SpriteSortMode? sortMode, + bool hasBlendState, [MaybeNull] BlendState blendState, + bool hasSamplerState, [MaybeNull] SamplerState samplerState, + bool hasDepthStencilState, [MaybeNull] DepthStencilState depthStencilState, + bool hasRasterizerState, [MaybeNull] RasterizerState rasterizerState, + bool hasCustomEffect, [MaybeNull] Effect customEffect, + bool hasTransformMatrix, Matrix? transformMatrix, + bool hasRenderTarget, [MaybeNull] RenderTarget2D renderTarget) + { + GetSpriteBatchFields( + out PreviousSortMode, + out PreviousBlendState, + out PreviousSamplerState, + out PreviousDepthStencilState, + out PreviousRasterizerState, + out PreviousCustomEffect, + out PreviousTransformMatrix); + + CurrentSortMode = hasSortMode ? sortMode!.Value : PreviousSortMode; + CurrentBlendState = hasBlendState ? blendState : PreviousBlendState; + CurrentSamplerState = hasSamplerState ? samplerState : PreviousSamplerState; + CurrentDepthStencilState = hasDepthStencilState ? depthStencilState : PreviousDepthStencilState; + CurrentRasterizerState = hasRasterizerState ? rasterizerState : PreviousRasterizerState; + CurrentCustomEffect = hasCustomEffect ? customEffect : PreviousCustomEffect; + CurrentTransformMatrix = hasTransformMatrix ? transformMatrix!.Value : PreviousTransformMatrix; + + HasRenderTarget = hasRenderTarget; + + GraphicsDevice graphicsDevice = Engine.Graphics.GraphicsDevice; + if (hasRenderTarget) + { + int renderTargetCount = graphicsDevice.GetRenderTargetsNoAllocEXT(null); + if (renderTargetCount > 0) + { + PreviousRenderTargets = new RenderTargetBinding[renderTargetCount]; + graphicsDevice.GetRenderTargetsNoAllocEXT(PreviousRenderTargets); + } + CurrentRenderTarget = renderTarget; + } + + Active = true; + Draw.SpriteBatch.End(); + if (hasRenderTarget) + Engine.Graphics.GraphicsDevice.SetRenderTarget(CurrentRenderTarget); + Draw.SpriteBatch.Begin( + CurrentSortMode, + CurrentBlendState, + CurrentSamplerState, + CurrentDepthStencilState, + CurrentRasterizerState, + CurrentCustomEffect, + CurrentTransformMatrix); + } + + /// + /// End this , restore the previous render targets if necessary, and restore + /// the previous properties. + /// + public void Dispose() + { + ObjectDisposedException.ThrowIf(!Active, typeof(TemporarySpriteBatch)); + + Active = false; + Draw.SpriteBatch.End(); + if (HasRenderTarget) + Engine.Graphics.GraphicsDevice.SetRenderTargets(PreviousRenderTargets); + Draw.SpriteBatch.Begin( + PreviousSortMode, + PreviousBlendState, + PreviousSamplerState, + PreviousDepthStencilState, + PreviousRasterizerState, + PreviousCustomEffect, + PreviousTransformMatrix); + } + + private static void GetSpriteBatchFields( + out SpriteSortMode sortMode, + [NotNull] out BlendState blendState, + [NotNull] out SamplerState samplerState, + [NotNull] out DepthStencilState depthStencilState, + [NotNull] out RasterizerState rasterizerState, + [MaybeNull] out Effect customEffect, + out Matrix transformMatrix) + { + // life would be good if we could just access these directly... + + DynamicData dynData = DynamicData.For(Draw.SpriteBatch); + sortMode = dynData.Get("sortMode"); + blendState = dynData.Get("blendState"); + samplerState = dynData.Get("samplerState"); + depthStencilState = dynData.Get("depthStencilState"); + rasterizerState = dynData.Get("rasterizerState"); + customEffect = dynData.Get("customEffect"); + transformMatrix = dynData.Get("transformMatrix"); + } +} diff --git a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs new file mode 100644 index 000000000..8a1f683f2 --- /dev/null +++ b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs @@ -0,0 +1,281 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System.Diagnostics.CodeAnalysis; + +namespace Celeste.Mod.Helpers; + +/// +/// A builder. +/// +/// +/// This class lets users configure the creation of a new , which allows +/// users to interrupt an existing , optionally swap in a +/// and restart the with custom properties.
+/// When done, the is ended, the previous is restored and the +/// old is resumed.
+///
+/// Note: Restarting a spritebatch flushes it to the GPU, which is costly. +///
+public sealed class TemporarySpriteBatchBuilder +{ + /// + /// Whether the 's + /// should be overridden. + /// + /// + public bool HasSortMode { get; private set; } + + /// + /// Whether the 's + /// should be overridden. + /// + /// + public bool HasBlendState { get; private set; } + + /// + /// Whether the 's + /// should be overridden. + /// + /// + public bool HasSamplerState { get; private set; } + + /// + /// Whether the 's + /// should be overridden. + /// + /// + public bool HasDepthStencilState { get; private set; } + + /// + /// Whether the 's + /// should be overridden. + /// + /// + public bool HasRasterizerState { get; private set; } + + /// + /// Whether the 's custom + /// should be overridden. + /// + /// + public bool HasCustomEffect { get; private set; } + + /// + /// Whether the 's transformation + /// should be overridden. + /// + /// + public bool HasTransformMatrix { get; private set; } + + /// + /// Whether to swap the current + /// in-between es. + /// + /// + public bool HasRenderTarget { get; private set; } + + + /// + /// The that the new + /// should use. + /// + /// + /// Contains a value when is true; null otherwise. + /// + public SpriteSortMode? SortMode { get; private set; } + + /// + /// The that the new + /// should use. + /// + /// + /// Contains a value when is true; null otherwise. + /// + [MaybeNull] + public BlendState BlendState { get; private set; } + + /// + /// The that the new + /// should use. + /// + /// + /// Contains a value when is true; null otherwise. + /// + [MaybeNull] + public SamplerState SamplerState { get; private set; } + + /// + /// The that the new + /// should use. + /// + /// + /// Contains a value when is true; null otherwise. + /// + [MaybeNull] + public DepthStencilState DepthStencilState { get; private set; } + + /// + /// The that the new + /// should use. + /// + /// + /// Contains a value when is true; null otherwise. + /// + [MaybeNull] + public RasterizerState RasterizerState { get; private set; } + + /// + /// The custom that the new + /// should use. + /// + /// + /// Contains a value when is true; null otherwise. + /// + [MaybeNull] + public Effect CustomEffect { get; private set; } + + /// + /// The transformation that the new + /// should use. + /// + /// + /// Contains a value when is true; null otherwise. + /// + public Matrix? TransformMatrix { get; private set; } + + /// + /// The that should be swapped to + /// in-between es. + /// + /// + /// Contains a value when is true; null otherwise. + /// + [MaybeNull] + public RenderTarget2D RenderTarget { get; private set; } + + + // the defaults are the same as the ones in SpriteBatch.Begin + + /// + /// Override the new 's . + /// + /// + /// The new sort mode. + /// + public TemporarySpriteBatchBuilder WithSortMode(SpriteSortMode sortMode) + { + HasSortMode = true; + SortMode = sortMode; + return this; + } + + /// + /// Override the new 's . + /// + /// + /// The new blend state. If null, defaults to . + /// + public TemporarySpriteBatchBuilder WithBlendState([MaybeNull] BlendState blendState) + { + HasBlendState = true; + BlendState = blendState ?? BlendState.AlphaBlend; + return this; + } + + /// + /// Override the new 's . + /// + /// + /// The new sampler state. If null, defaults to . + /// + public TemporarySpriteBatchBuilder WithSamplerState([MaybeNull] SamplerState samplerState) + { + HasSamplerState = true; + SamplerState = samplerState ?? SamplerState.LinearClamp; + return this; + } + + /// + /// Override the new 's . + /// + /// + /// The new depth stencil state. If null, defaults to . + /// + public TemporarySpriteBatchBuilder WithDepthStencilState([MaybeNull] DepthStencilState depthStencilState) + { + HasDepthStencilState = true; + DepthStencilState = depthStencilState ?? DepthStencilState.None; + return this; + } + + /// + /// Override the new 's . + /// + /// + /// The new rasterizer state. If null, defaults to . + /// + public TemporarySpriteBatchBuilder WithRasterizerState([MaybeNull] RasterizerState rasterizerState) + { + HasRasterizerState = true; + RasterizerState = rasterizerState ?? RasterizerState.CullCounterClockwise; + return this; + } + + /// + /// Override the new 's custom . + /// + /// + /// The new custom effect or null if none should be used. + /// + public TemporarySpriteBatchBuilder WithCustomEffect([MaybeNull] Effect customEffect) + { + HasCustomEffect = true; + CustomEffect = customEffect; + return this; + } + + /// + /// Override the new 's transformation . + /// + /// + /// The new transformation matrix. + /// + public TemporarySpriteBatchBuilder WithTransformMatrix(Matrix transformMatrix) + { + HasTransformMatrix = true; + TransformMatrix = transformMatrix; + return this; + } + + /// + /// Override the in-between es. + /// + /// + /// The new render target or null to refer to the screen. + /// + public TemporarySpriteBatchBuilder WithRenderTarget([MaybeNull] RenderTarget2D renderTarget) + { + HasRenderTarget = true; + RenderTarget = renderTarget; + return this; + } + + /// + /// Restart the with the configured properties. + /// + /// + /// A that will restore the previous properties + /// when disposed. Remember to put it in a using block. + /// + public TemporarySpriteBatch Use() + => new( + HasSortMode, SortMode, + HasBlendState, BlendState, + HasSamplerState, SamplerState, + HasDepthStencilState, DepthStencilState, + HasRasterizerState, RasterizerState, + HasCustomEffect, CustomEffect, + HasTransformMatrix, TransformMatrix, + HasRenderTarget, RenderTarget + ); +} From 07aa520c2fac199ae482b7fc95fef680e8497128 Mon Sep 17 00:00:00 2001 From: SnipUndercover Date: Sun, 29 Mar 2026 23:31:42 +0200 Subject: [PATCH 03/10] Adjust GPU upload notes --- Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs | 4 ++-- Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs index bc06d3852..9af6acf7f 100644 --- a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs +++ b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs @@ -19,8 +19,8 @@ namespace Celeste.Mod.Helpers; /// Useful when interrupting a mid-render to, for example, render a specific entity to a /// temporary while applying a custom shader, all while preserving the previous /// configuration.
-///
-/// Note: Restarting a spritebatch flushes it to the GPU, which is costly. +/// Note: Restarting a spritebatch flushes it to the GPU. +/// While the cost is not that significant, it's best to avoid restarting the spritebatch too often per frame. /// /// public ref struct TemporarySpriteBatch : IDisposable diff --git a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs index 8a1f683f2..11c99618d 100644 --- a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs +++ b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs @@ -13,8 +13,8 @@ namespace Celeste.Mod.Helpers; /// and restart the with custom properties.
/// When done, the is ended, the previous is restored and the /// old is resumed.
-///
-/// Note: Restarting a spritebatch flushes it to the GPU, which is costly. +/// Note: Restarting a spritebatch flushes it to the GPU. +/// While the cost is not that significant, it's best to avoid restarting the spritebatch too often per frame. /// public sealed class TemporarySpriteBatchBuilder { From b0d6ee8c4feba1fa6618ea1af88bb33db63f3579 Mon Sep 17 00:00:00 2001 From: SnipUndercover Date: Sun, 29 Mar 2026 23:32:54 +0200 Subject: [PATCH 04/10] Make `TemporarySpriteBatch.Dispose()` idempotent --- Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs index 9af6acf7f..ed5319500 100644 --- a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs +++ b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs @@ -205,7 +205,8 @@ internal TemporarySpriteBatch( /// public void Dispose() { - ObjectDisposedException.ThrowIf(!Active, typeof(TemporarySpriteBatch)); + if (!Active) + return; Active = false; Draw.SpriteBatch.End(); From 5ffbae59ad7257fae57977e0e7abf854450b7479 Mon Sep 17 00:00:00 2001 From: SnipUndercover Date: Sun, 29 Mar 2026 23:50:28 +0200 Subject: [PATCH 05/10] Implicitly implement IDisposable --- Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs index ed5319500..90d95468b 100644 --- a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs +++ b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs @@ -23,7 +23,7 @@ namespace Celeste.Mod.Helpers; /// While the cost is not that significant, it's best to avoid restarting the spritebatch too often per frame. /// /// -public ref struct TemporarySpriteBatch : IDisposable +public ref struct TemporarySpriteBatch { /// /// The of this . From 6d2a866370268b530458f9c9ab296ae37f8ed30a Mon Sep 17 00:00:00 2001 From: SnipUndercover Date: Sun, 29 Mar 2026 23:50:56 +0200 Subject: [PATCH 06/10] Roll back LangVersion to C# 12 (.NET 8 default) --- Celeste.Mod.mm/Celeste.Mod.mm.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/Celeste.Mod.mm/Celeste.Mod.mm.csproj b/Celeste.Mod.mm/Celeste.Mod.mm.csproj index dccd11f7a..7264ff9b3 100644 --- a/Celeste.Mod.mm/Celeste.Mod.mm.csproj +++ b/Celeste.Mod.mm/Celeste.Mod.mm.csproj @@ -5,8 +5,6 @@ Celeste.Mod.mm Celeste true - - 13 false From 5ccdca30b1f777f13d64840d5cb704ce3e3ffff6 Mon Sep 17 00:00:00 2001 From: SnipUndercover Date: Mon, 30 Mar 2026 00:22:30 +0200 Subject: [PATCH 07/10] Avoid allocations by using an ArrayPool --- Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs index 90d95468b..c31f39bf8 100644 --- a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs +++ b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs @@ -1,8 +1,8 @@ -using System; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Monocle; using MonoMod.Utils; +using System.Buffers; using System.Diagnostics.CodeAnalysis; namespace Celeste.Mod.Helpers; @@ -25,6 +25,11 @@ namespace Celeste.Mod.Helpers; /// public ref struct TemporarySpriteBatch { + // we don't really expect the RenderTargetBinding[] array size to exceed 1, + // so a max of 16 seems reasonable + private static readonly ArrayPool _renderTargetPool + = ArrayPool.Create(0x10, 50); + /// /// The of this . /// @@ -179,7 +184,7 @@ internal TemporarySpriteBatch( int renderTargetCount = graphicsDevice.GetRenderTargetsNoAllocEXT(null); if (renderTargetCount > 0) { - PreviousRenderTargets = new RenderTargetBinding[renderTargetCount]; + PreviousRenderTargets = _renderTargetPool.Rent(renderTargetCount); graphicsDevice.GetRenderTargetsNoAllocEXT(PreviousRenderTargets); } CurrentRenderTarget = renderTarget; @@ -220,6 +225,8 @@ public void Dispose() PreviousRasterizerState, PreviousCustomEffect, PreviousTransformMatrix); + + _renderTargetPool.Return(PreviousRenderTargets, clearArray: true); } private static void GetSpriteBatchFields( From 43132eedc7dff41ae69305d650367d8d28a691e5 Mon Sep 17 00:00:00 2001 From: SnipUndercover Date: Mon, 30 Mar 2026 00:46:05 +0200 Subject: [PATCH 08/10] Remove redundant bool properties --- .../Mod/Helpers/TemporarySpriteBatch.cs | 30 ++--- .../Helpers/TemporarySpriteBatchBuilder.cs | 105 ++++-------------- 2 files changed, 37 insertions(+), 98 deletions(-) diff --git a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs index c31f39bf8..cd80d6d46 100644 --- a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs +++ b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs @@ -150,14 +150,16 @@ private static readonly ArrayPool _renderTargetPool /// /// internal TemporarySpriteBatch( - bool hasSortMode, SpriteSortMode? sortMode, - bool hasBlendState, [MaybeNull] BlendState blendState, - bool hasSamplerState, [MaybeNull] SamplerState samplerState, - bool hasDepthStencilState, [MaybeNull] DepthStencilState depthStencilState, - bool hasRasterizerState, [MaybeNull] RasterizerState rasterizerState, - bool hasCustomEffect, [MaybeNull] Effect customEffect, - bool hasTransformMatrix, Matrix? transformMatrix, - bool hasRenderTarget, [MaybeNull] RenderTarget2D renderTarget) + SpriteSortMode? sortMode, + [MaybeNull] BlendState blendState, + [MaybeNull] SamplerState samplerState, + [MaybeNull] DepthStencilState depthStencilState, + [MaybeNull] RasterizerState rasterizerState, + [MaybeNull] Effect customEffect, + Matrix? transformMatrix, + [MaybeNull] RenderTarget2D renderTarget, + bool hasCustomEffect, + bool hasRenderTarget) { GetSpriteBatchFields( out PreviousSortMode, @@ -168,13 +170,13 @@ internal TemporarySpriteBatch( out PreviousCustomEffect, out PreviousTransformMatrix); - CurrentSortMode = hasSortMode ? sortMode!.Value : PreviousSortMode; - CurrentBlendState = hasBlendState ? blendState : PreviousBlendState; - CurrentSamplerState = hasSamplerState ? samplerState : PreviousSamplerState; - CurrentDepthStencilState = hasDepthStencilState ? depthStencilState : PreviousDepthStencilState; - CurrentRasterizerState = hasRasterizerState ? rasterizerState : PreviousRasterizerState; + CurrentSortMode = sortMode ?? PreviousSortMode; + CurrentBlendState = blendState ?? PreviousBlendState; + CurrentSamplerState = samplerState ?? PreviousSamplerState; + CurrentDepthStencilState = depthStencilState ?? PreviousDepthStencilState; + CurrentRasterizerState = rasterizerState ?? PreviousRasterizerState; CurrentCustomEffect = hasCustomEffect ? customEffect : PreviousCustomEffect; - CurrentTransformMatrix = hasTransformMatrix ? transformMatrix!.Value : PreviousTransformMatrix; + CurrentTransformMatrix = transformMatrix ?? PreviousTransformMatrix; HasRenderTarget = hasRenderTarget; diff --git a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs index 11c99618d..4f4784a25 100644 --- a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs +++ b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs @@ -18,41 +18,6 @@ namespace Celeste.Mod.Helpers; /// public sealed class TemporarySpriteBatchBuilder { - /// - /// Whether the 's - /// should be overridden. - /// - /// - public bool HasSortMode { get; private set; } - - /// - /// Whether the 's - /// should be overridden. - /// - /// - public bool HasBlendState { get; private set; } - - /// - /// Whether the 's - /// should be overridden. - /// - /// - public bool HasSamplerState { get; private set; } - - /// - /// Whether the 's - /// should be overridden. - /// - /// - public bool HasDepthStencilState { get; private set; } - - /// - /// Whether the 's - /// should be overridden. - /// - /// - public bool HasRasterizerState { get; private set; } - /// /// Whether the 's custom /// should be overridden. @@ -60,13 +25,6 @@ public sealed class TemporarySpriteBatchBuilder /// public bool HasCustomEffect { get; private set; } - /// - /// Whether the 's transformation - /// should be overridden. - /// - /// - public bool HasTransformMatrix { get; private set; } - /// /// Whether to swap the current /// in-between es. @@ -77,78 +35,62 @@ public sealed class TemporarySpriteBatchBuilder /// /// The that the new - /// should use. + /// should use, or null if the old value should be preserved. /// - /// - /// Contains a value when is true; null otherwise. - /// public SpriteSortMode? SortMode { get; private set; } /// /// The that the new - /// should use. + /// should use, or null if the old value should be preserved. /// - /// - /// Contains a value when is true; null otherwise. - /// [MaybeNull] public BlendState BlendState { get; private set; } /// /// The that the new - /// should use. + /// should use, or null if the old value should be preserved. /// - /// - /// Contains a value when is true; null otherwise. - /// [MaybeNull] public SamplerState SamplerState { get; private set; } /// /// The that the new - /// should use. + /// should use, or null if the old value should be preserved. /// - /// - /// Contains a value when is true; null otherwise. - /// [MaybeNull] public DepthStencilState DepthStencilState { get; private set; } /// /// The that the new - /// should use. + /// should use, or null if the old value should be preserved. /// - /// - /// Contains a value when is true; null otherwise. - /// [MaybeNull] public RasterizerState RasterizerState { get; private set; } /// /// The custom that the new - /// should use. + /// should use, or null if no shader should be used. /// /// - /// Contains a value when is true; null otherwise. + /// When is false, the old value will be preserved + /// and this property will always be null. /// [MaybeNull] public Effect CustomEffect { get; private set; } /// /// The transformation that the new - /// should use. + /// should use, or null if the old value should be preserved. /// - /// - /// Contains a value when is true; null otherwise. - /// public Matrix? TransformMatrix { get; private set; } /// /// The that should be swapped to - /// in-between es. + /// in-between es, or null to render to the screen. /// /// - /// Contains a value when is true; null otherwise. + /// When is false, no render target changes will be done + /// and this property will always be null. /// [MaybeNull] public RenderTarget2D RenderTarget { get; private set; } @@ -164,7 +106,6 @@ public sealed class TemporarySpriteBatchBuilder /// public TemporarySpriteBatchBuilder WithSortMode(SpriteSortMode sortMode) { - HasSortMode = true; SortMode = sortMode; return this; } @@ -177,7 +118,6 @@ public TemporarySpriteBatchBuilder WithSortMode(SpriteSortMode sortMode) /// public TemporarySpriteBatchBuilder WithBlendState([MaybeNull] BlendState blendState) { - HasBlendState = true; BlendState = blendState ?? BlendState.AlphaBlend; return this; } @@ -190,7 +130,6 @@ public TemporarySpriteBatchBuilder WithBlendState([MaybeNull] BlendState blendSt /// public TemporarySpriteBatchBuilder WithSamplerState([MaybeNull] SamplerState samplerState) { - HasSamplerState = true; SamplerState = samplerState ?? SamplerState.LinearClamp; return this; } @@ -203,7 +142,6 @@ public TemporarySpriteBatchBuilder WithSamplerState([MaybeNull] SamplerState sam /// public TemporarySpriteBatchBuilder WithDepthStencilState([MaybeNull] DepthStencilState depthStencilState) { - HasDepthStencilState = true; DepthStencilState = depthStencilState ?? DepthStencilState.None; return this; } @@ -216,7 +154,6 @@ public TemporarySpriteBatchBuilder WithDepthStencilState([MaybeNull] DepthStenci /// public TemporarySpriteBatchBuilder WithRasterizerState([MaybeNull] RasterizerState rasterizerState) { - HasRasterizerState = true; RasterizerState = rasterizerState ?? RasterizerState.CullCounterClockwise; return this; } @@ -242,7 +179,6 @@ public TemporarySpriteBatchBuilder WithCustomEffect([MaybeNull] Effect customEff /// public TemporarySpriteBatchBuilder WithTransformMatrix(Matrix transformMatrix) { - HasTransformMatrix = true; TransformMatrix = transformMatrix; return this; } @@ -269,13 +205,14 @@ public TemporarySpriteBatchBuilder WithRenderTarget([MaybeNull] RenderTarget2D r /// public TemporarySpriteBatch Use() => new( - HasSortMode, SortMode, - HasBlendState, BlendState, - HasSamplerState, SamplerState, - HasDepthStencilState, DepthStencilState, - HasRasterizerState, RasterizerState, - HasCustomEffect, CustomEffect, - HasTransformMatrix, TransformMatrix, - HasRenderTarget, RenderTarget + SortMode, + BlendState, + SamplerState, + DepthStencilState, + RasterizerState, + CustomEffect, + TransformMatrix, + RenderTarget, + HasCustomEffect, HasRenderTarget ); } From 1c048476a941d7790a85744aec6efaf037f8c997 Mon Sep 17 00:00:00 2001 From: SnipUndercover Date: Mon, 30 Mar 2026 00:55:57 +0200 Subject: [PATCH 09/10] Use Nullable Reference Types --- .../Mod/Helpers/TemporarySpriteBatch.cs | 55 ++++++++----------- .../Helpers/TemporarySpriteBatchBuilder.cs | 35 +++++------- 2 files changed, 37 insertions(+), 53 deletions(-) diff --git a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs index cd80d6d46..dc27e1505 100644 --- a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs +++ b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs @@ -1,9 +1,10 @@ -using Microsoft.Xna.Framework; +#nullable enable + +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Monocle; using MonoMod.Utils; using System.Buffers; -using System.Diagnostics.CodeAnalysis; namespace Celeste.Mod.Helpers; @@ -38,32 +39,27 @@ private static readonly ArrayPool _renderTargetPool /// /// The of this . /// - [NotNull] public readonly BlendState CurrentBlendState; /// /// The of this . /// - [NotNull] public readonly SamplerState CurrentSamplerState; /// /// The of this . /// - [NotNull] public readonly DepthStencilState CurrentDepthStencilState; /// /// The of this . /// - [NotNull] public readonly RasterizerState CurrentRasterizerState; /// /// The custom of this . /// - [MaybeNull] - public readonly Effect CurrentCustomEffect; + public readonly Effect? CurrentCustomEffect; /// /// The transformation of this . @@ -74,8 +70,7 @@ private static readonly ArrayPool _renderTargetPool /// The swapped in for the duration of this /// or null to draw to the screen if is true; else always null. /// - [MaybeNull] - public readonly RenderTarget2D CurrentRenderTarget; + public readonly RenderTarget2D? CurrentRenderTarget; /// @@ -88,36 +83,31 @@ private static readonly ArrayPool _renderTargetPool /// The that was used prior to the start of this /// . /// - [NotNull] public readonly BlendState PreviousBlendState; /// /// The that was used prior to the start of this /// . /// - [NotNull] public readonly SamplerState PreviousSamplerState; /// /// The that was used prior to the start of this /// . /// - [NotNull] public readonly DepthStencilState PreviousDepthStencilState; /// /// The that was used prior to the start of this /// . /// - [NotNull] public readonly RasterizerState PreviousRasterizerState; /// /// The custom that was used prior to the start of this /// . /// - [MaybeNull] - public readonly Effect PreviousCustomEffect; + public readonly Effect? PreviousCustomEffect; /// /// The transformation that was used prior to the start of this @@ -129,8 +119,7 @@ private static readonly ArrayPool _renderTargetPool /// The s that were used prior to the start of this /// or null to draw to the screen if is true; else always null. /// - [MaybeNull] - public readonly RenderTargetBinding[] PreviousRenderTargets; + public readonly RenderTargetBinding[]? PreviousRenderTargets; /// @@ -151,13 +140,13 @@ private static readonly ArrayPool _renderTargetPool /// internal TemporarySpriteBatch( SpriteSortMode? sortMode, - [MaybeNull] BlendState blendState, - [MaybeNull] SamplerState samplerState, - [MaybeNull] DepthStencilState depthStencilState, - [MaybeNull] RasterizerState rasterizerState, - [MaybeNull] Effect customEffect, + BlendState? blendState, + SamplerState? samplerState, + DepthStencilState? depthStencilState, + RasterizerState? rasterizerState, + Effect? customEffect, Matrix? transformMatrix, - [MaybeNull] RenderTarget2D renderTarget, + RenderTarget2D? renderTarget, bool hasCustomEffect, bool hasRenderTarget) { @@ -233,21 +222,21 @@ public void Dispose() private static void GetSpriteBatchFields( out SpriteSortMode sortMode, - [NotNull] out BlendState blendState, - [NotNull] out SamplerState samplerState, - [NotNull] out DepthStencilState depthStencilState, - [NotNull] out RasterizerState rasterizerState, - [MaybeNull] out Effect customEffect, + out BlendState blendState, + out SamplerState samplerState, + out DepthStencilState depthStencilState, + out RasterizerState rasterizerState, + out Effect? customEffect, out Matrix transformMatrix) { // life would be good if we could just access these directly... DynamicData dynData = DynamicData.For(Draw.SpriteBatch); sortMode = dynData.Get("sortMode"); - blendState = dynData.Get("blendState"); - samplerState = dynData.Get("samplerState"); - depthStencilState = dynData.Get("depthStencilState"); - rasterizerState = dynData.Get("rasterizerState"); + blendState = dynData.Get("blendState")!; + samplerState = dynData.Get("samplerState")!; + depthStencilState = dynData.Get("depthStencilState")!; + rasterizerState = dynData.Get("rasterizerState")!; customEffect = dynData.Get("customEffect"); transformMatrix = dynData.Get("transformMatrix"); } diff --git a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs index 4f4784a25..1969288ed 100644 --- a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs +++ b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatchBuilder.cs @@ -1,6 +1,7 @@ -using Microsoft.Xna.Framework; +#nullable enable + +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using System.Diagnostics.CodeAnalysis; namespace Celeste.Mod.Helpers; @@ -43,29 +44,25 @@ public sealed class TemporarySpriteBatchBuilder /// The that the new /// should use, or null if the old value should be preserved. /// - [MaybeNull] - public BlendState BlendState { get; private set; } + public BlendState? BlendState { get; private set; } /// /// The that the new /// should use, or null if the old value should be preserved. /// - [MaybeNull] - public SamplerState SamplerState { get; private set; } + public SamplerState? SamplerState { get; private set; } /// /// The that the new /// should use, or null if the old value should be preserved. /// - [MaybeNull] - public DepthStencilState DepthStencilState { get; private set; } + public DepthStencilState? DepthStencilState { get; private set; } /// /// The that the new /// should use, or null if the old value should be preserved. /// - [MaybeNull] - public RasterizerState RasterizerState { get; private set; } + public RasterizerState? RasterizerState { get; private set; } /// /// The custom that the new @@ -75,8 +72,7 @@ public sealed class TemporarySpriteBatchBuilder /// When is false, the old value will be preserved /// and this property will always be null. /// - [MaybeNull] - public Effect CustomEffect { get; private set; } + public Effect? CustomEffect { get; private set; } /// /// The transformation that the new @@ -92,8 +88,7 @@ public sealed class TemporarySpriteBatchBuilder /// When is false, no render target changes will be done /// and this property will always be null. /// - [MaybeNull] - public RenderTarget2D RenderTarget { get; private set; } + public RenderTarget2D? RenderTarget { get; private set; } // the defaults are the same as the ones in SpriteBatch.Begin @@ -116,7 +111,7 @@ public TemporarySpriteBatchBuilder WithSortMode(SpriteSortMode sortMode) /// /// The new blend state. If null, defaults to . /// - public TemporarySpriteBatchBuilder WithBlendState([MaybeNull] BlendState blendState) + public TemporarySpriteBatchBuilder WithBlendState(BlendState? blendState) { BlendState = blendState ?? BlendState.AlphaBlend; return this; @@ -128,7 +123,7 @@ public TemporarySpriteBatchBuilder WithBlendState([MaybeNull] BlendState blendSt /// /// The new sampler state. If null, defaults to . /// - public TemporarySpriteBatchBuilder WithSamplerState([MaybeNull] SamplerState samplerState) + public TemporarySpriteBatchBuilder WithSamplerState(SamplerState? samplerState) { SamplerState = samplerState ?? SamplerState.LinearClamp; return this; @@ -140,7 +135,7 @@ public TemporarySpriteBatchBuilder WithSamplerState([MaybeNull] SamplerState sam /// /// The new depth stencil state. If null, defaults to . /// - public TemporarySpriteBatchBuilder WithDepthStencilState([MaybeNull] DepthStencilState depthStencilState) + public TemporarySpriteBatchBuilder WithDepthStencilState(DepthStencilState? depthStencilState) { DepthStencilState = depthStencilState ?? DepthStencilState.None; return this; @@ -152,7 +147,7 @@ public TemporarySpriteBatchBuilder WithDepthStencilState([MaybeNull] DepthStenci /// /// The new rasterizer state. If null, defaults to . /// - public TemporarySpriteBatchBuilder WithRasterizerState([MaybeNull] RasterizerState rasterizerState) + public TemporarySpriteBatchBuilder WithRasterizerState(RasterizerState? rasterizerState) { RasterizerState = rasterizerState ?? RasterizerState.CullCounterClockwise; return this; @@ -164,7 +159,7 @@ public TemporarySpriteBatchBuilder WithRasterizerState([MaybeNull] RasterizerSta /// /// The new custom effect or null if none should be used. /// - public TemporarySpriteBatchBuilder WithCustomEffect([MaybeNull] Effect customEffect) + public TemporarySpriteBatchBuilder WithCustomEffect(Effect? customEffect) { HasCustomEffect = true; CustomEffect = customEffect; @@ -189,7 +184,7 @@ public TemporarySpriteBatchBuilder WithTransformMatrix(Matrix transformMatrix) /// /// The new render target or null to refer to the screen. /// - public TemporarySpriteBatchBuilder WithRenderTarget([MaybeNull] RenderTarget2D renderTarget) + public TemporarySpriteBatchBuilder WithRenderTarget(RenderTarget2D? renderTarget) { HasRenderTarget = true; RenderTarget = renderTarget; From b6d9d04a558fd8ab7449b83509bba4622654c839 Mon Sep 17 00:00:00 2001 From: SnipUndercover Date: Mon, 30 Mar 2026 00:56:59 +0200 Subject: [PATCH 10/10] Fix possible null dereference in Dispose --- Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs index dc27e1505..5f60c4006 100644 --- a/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs +++ b/Celeste.Mod.mm/Mod/Helpers/TemporarySpriteBatch.cs @@ -207,7 +207,10 @@ public void Dispose() Active = false; Draw.SpriteBatch.End(); if (HasRenderTarget) + { Engine.Graphics.GraphicsDevice.SetRenderTargets(PreviousRenderTargets); + _renderTargetPool.Return(PreviousRenderTargets!, clearArray: true); + } Draw.SpriteBatch.Begin( PreviousSortMode, PreviousBlendState, @@ -216,8 +219,6 @@ public void Dispose() PreviousRasterizerState, PreviousCustomEffect, PreviousTransformMatrix); - - _renderTargetPool.Return(PreviousRenderTargets, clearArray: true); } private static void GetSpriteBatchFields(