Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7244c68
Remove last MonoModIfFlag("XNA") dependent code
Wartori54 Oct 14, 2025
8bf1c81
Remove superfluous MonoMod patching
Wartori54 Oct 14, 2025
d484b99
Go back to an unoptimized and single threaded texture loading
Wartori54 Oct 20, 2025
1db69f4
Fix some bugs and polish code
Wartori54 Oct 30, 2025
5c88d3d
Make VirtualTexture thread-safe
Wartori54 Nov 2, 2025
c98c1ff
Initial FTL reimpl
Wartori54 Nov 6, 2025
eabee40
Fix out of bounds read and reduce unsafe scopes
Wartori54 Nov 7, 2025
c591480
Fix memory leak, locking and logging
Wartori54 Nov 7, 2025
3953836
Add main thread timeout for unmanaged allocations
Wartori54 Nov 13, 2025
9173b30
Add force lazy load event
Wartori54 Nov 17, 2025
c00ce39
Fix wrong usage of memory limit
Wartori54 Nov 17, 2025
8679173
Add tracing for gc and over budget allocs
Wartori54 Nov 17, 2025
76720b1
Add exception handling to FTL
Wartori54 Nov 18, 2025
ae14123
Fix Stopwatch not being started, and forceQueue not doing anything
Wartori54 Nov 18, 2025
de004e5
Remove FTL timer
Wartori54 Nov 18, 2025
886f192
Move FTL startup into a method so that it can be started earlier/later
Wartori54 Nov 18, 2025
660e7be
Disallow changing Path of VirtualTexture and make resizes thread-safe
Wartori54 Nov 18, 2025
bc793aa
Remove leftover TODO
Wartori54 Nov 18, 2025
4cbf209
Use TextureKind for more clear code and some cleanups
Wartori54 Nov 22, 2025
48c0193
Make texture overrides possible, safe and consistent
Wartori54 Nov 23, 2025
f851c21
(Try to) Get headless right again
Wartori54 Nov 23, 2025
068ac8c
Fix remaining TODOs
Wartori54 Nov 24, 2025
ed32736
Add extra logging preprocessor directives for managed and unmanaged m…
Wartori54 Nov 24, 2025
015f4fa
Add event for lazy loads on texture access
Wartori54 Nov 26, 2025
4d81295
Add documentation to VirtualTexture
Wartori54 Nov 26, 2025
33040ff
Add documentation to TextureContentHelper
Wartori54 Nov 27, 2025
4ad0a29
Fix some typos and leftover comments
Wartori54 Nov 30, 2025
18b309c
Add timing and logging for the MainThreadHelper queue flush
Wartori54 Nov 30, 2025
888e0ca
Make VirtualContent synchronized
Wartori54 Nov 30, 2025
f232dea
Rerun pipeline
maddie480 Dec 1, 2025
5ecc0b4
Make `vTex.Reload(); vTex.Unload();` deterministic
Wartori54 Dec 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Celeste.Mod.mm/Celeste.Mod.mm.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<AssemblyName>Celeste.Mod.mm</AssemblyName>
<RootNamespace>Celeste</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>11</LangVersion>
<LangVersion>12.0</LangVersion>
<ShouldIncludeNativeLua>false</ShouldIncludeNativeLua>
</PropertyGroup>

Expand Down
3 changes: 2 additions & 1 deletion Celeste.Mod.mm/Mod/Core/CoreModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ public override void LoadSettings() {
}

// If we're running in an environment that prefers this flag, forcibly enable them.
Settings.LazyLoading |= Everest.Flags.PreferLazyLoading;
// Used to be used by android, which is not supported currently.
// Settings.LazyLoading |= Everest.Flags.PreferLazyLoading;
}

public override void SaveSettings() {
Expand Down
5 changes: 5 additions & 0 deletions Celeste.Mod.mm/Mod/Core/CoreModuleSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ public bool? FastTextureLoading {
[SettingIgnore] // TODO: Show as advanced setting.
public float FastTextureLoadingMaxMB { get; set; } = 0;

[SettingNeedsRelaunch]
[SettingInGame(false)]
[SettingIgnore] // TODO: Show as advanced setting.
public bool? FastTextureLoadingPoolUseGC { get; set; } = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe FastTextureLoadingPoolUseGC should be a bool instead of a bool?, as both null and false are equivalent when using the property in Celeste.Mod.mm/Mod/Helpers/TextureContentHelper.cs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea behind having a default third state is the ability to change what it actually defaults to if we ever need to, because if the default were to be false we would not be able to switch all the users who didn't manually disable the feature (since we could not distinguish that).
And if you look further _FastTextureLoading and ThreadedGL have been doing this for a while now.


[SettingNeedsRelaunch]
[SettingInGame(false)]
[SettingIgnore] // TODO: Show as advanced setting.
Expand Down
200 changes: 7 additions & 193 deletions Celeste.Mod.mm/Mod/Everest/ContentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,220 +204,37 @@ public static void UnloadTextureRaw(IntPtr dataPtr) {
_UnloadTextureRaw(dataPtr);
}

[MonoModIgnore]
private static extern void _SetTextureDataPtr(Texture2D tex, IntPtr ptr);

[MonoModIgnore]
private static extern Texture2D _LoadTextureLazyPremultiplyFull(GraphicsDevice gd, Stream stream);

[MonoModIgnore]
private static extern void _LoadTextureRaw(GraphicsDevice gd, Stream stream, out int w, out int h, out byte[] data, out IntPtr dataPtr, bool gc);

[MonoModIgnore]
private static extern void _LoadTextureLazyPremultiply(GraphicsDevice gd, Stream stream, out int w, out int h, out byte[] data, out IntPtr dataPtr, bool gc);

[MonoModIgnore]
private static extern void _UnloadTextureRaw(IntPtr dataPtr);

[MonoModIfFlag("XNA")]
[MonoModPatch("_SetTextureDataPtr")]
[MonoModReplace]
private static unsafe void _SetTextureDataPtrXNA(Texture2D tex, IntPtr ptr) {
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new PlatformNotSupportedException();

byte[] copy = new byte[tex.Width * tex.Height * 4];
Marshal.Copy(ptr, copy, 0, copy.Length);
tex.SetData(copy);
}

[MonoModIfFlag("XNA")]
[MonoModPatch("_LoadTextureLazyPremultiplyFull")]
[MonoModReplace]
private static unsafe Texture2D _LoadTextureLazyPremultiplyFullXNA(GraphicsDevice gd, Stream stream) {
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new PlatformNotSupportedException();

_LoadTextureLazyPremultiply(gd, stream, out int w, out int h, out byte[] data, out _, true);
Texture2D tex = new Texture2D(gd, w, h, false, SurfaceFormat.Color);
tex.SetData(data);
return tex;
}

[MonoModIfFlag("XNA")]
[MonoModPatch("_LoadTextureRaw")]
[MonoModReplace]
private static unsafe void _LoadTextureRawXNA(GraphicsDevice gd, Stream stream, out int w, out int h, out byte[] data, out IntPtr dataPtr, bool gc) {
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new PlatformNotSupportedException();

using (SD.Bitmap bmp = new SD.Bitmap(stream)) {
w = bmp.Width;
h = bmp.Height;
int depth = SD.Image.GetPixelFormatSize(bmp.PixelFormat);

SD.Bitmap copy = null;
if (depth != 32)
copy = bmp.Clone(new SD.Rectangle(0, 0, w, h), SDI.PixelFormat.Format32bppArgb);
using (copy) {

SD.Bitmap src = copy ?? bmp;

SDI.BitmapData srcData = src.LockBits(
new SD.Rectangle(0, 0, w, h),
SDI.ImageLockMode.ReadOnly,
src.PixelFormat
);

int length = w * h * 4;
if (gc) {
data = new byte[length];
dataPtr = IntPtr.Zero;
} else {
data = Array.Empty<byte>();
dataPtr = Marshal.AllocHGlobal(length);
}

byte* from = (byte*) srcData.Scan0;
fixed (byte* dataPin = data) {
byte* to = gc ? dataPin : (byte*) dataPtr;
for (int i = length - 1 - 3; i > -1; i -= 4) {
to[i + 0] = from[i + 2];
to[i + 1] = from[i + 1];
to[i + 2] = from[i + 0];
to[i + 3] = from[i + 3];
}
}

src.UnlockBits(srcData);
}
}
}

[MonoModIfFlag("XNA")]
[MonoModPatch("_LoadTextureLazyPremultiply")]
[MonoModReplace]
private static unsafe void _LoadTextureLazyPremultiplyXNA(GraphicsDevice gd, Stream stream, out int w, out int h, out byte[] data, out IntPtr dataPtr, bool gc) {
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new PlatformNotSupportedException();

using (SD.Bitmap bmp = new SD.Bitmap(stream)) {
w = bmp.Width;
h = bmp.Height;
int depth = SD.Image.GetPixelFormatSize(bmp.PixelFormat);

SD.Bitmap copy = null;
if (depth != 32)
copy = bmp.Clone(new SD.Rectangle(0, 0, w, h), SDI.PixelFormat.Format32bppArgb);
using (copy) {

SD.Bitmap src = copy ?? bmp;

SDI.BitmapData srcData = src.LockBits(
new SD.Rectangle(0, 0, w, h),
SDI.ImageLockMode.ReadOnly,
src.PixelFormat
);

int length = w * h * 4;
if (gc) {
data = new byte[length];
dataPtr = IntPtr.Zero;
} else {
data = Array.Empty<byte>();
dataPtr = Marshal.AllocHGlobal(length);
}

byte* from = (byte*) srcData.Scan0;
fixed (byte* dataPin = data) {
byte* to = gc ? dataPin : (byte*) dataPtr;
for (int i = length - 1 - 3; i > -1; i -= 4) {
byte r = from[i + 2];
byte g = from[i + 1];
byte b = from[i + 0];
byte a = from[i + 3];

if (a == 0) {
to[i + 0] = 0;
to[i + 1] = 0;
to[i + 2] = 0;
to[i + 3] = 0;
continue;
}

if (a == 255) {
to[i + 0] = r;
to[i + 1] = g;
to[i + 2] = b;
to[i + 3] = a;
continue;
}

to[i + 0] = (byte) (r * a / 255D);
to[i + 1] = (byte) (g * a / 255D);
to[i + 2] = (byte) (b * a / 255D);
to[i + 3] = a;
}
}

src.UnlockBits(srcData);
}
}
}

[MonoModIfFlag("XNA")]
[MonoModPatch("_UnloadTextureRaw")]
[MonoModReplace]
private static unsafe void _UnloadTextureRawXNA(IntPtr dataPtr) {
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new PlatformNotSupportedException();

Marshal.FreeHGlobal(dataPtr);
}

[MonoModIfFlag("FNA")]
[MonoModPatch("_SetTextureDataPtr")]
[MonoModReplace]
private static unsafe void _SetTextureDataPtrFNA(Texture2D tex, IntPtr ptr) {
private static unsafe void _SetTextureDataPtr(Texture2D tex, IntPtr ptr) {
tex.SetDataPointerEXT(0, null, ptr, tex.Width * tex.Height * 4);
}

[MonoModIfFlag("FNA")]
[MonoModPatch("_LoadTextureLazyPremultiplyFull")]
[MonoModReplace]
private static unsafe Texture2D _LoadTextureLazyPremultiplyFullFNA(GraphicsDevice gd, Stream stream) {
private static unsafe Texture2D _LoadTextureLazyPremultiplyFull(GraphicsDevice gd, Stream stream) {
_LoadTextureLazyPremultiply(gd, stream, out int w, out int h, out _, out IntPtr dataPtr, false);
Texture2D tex = new Texture2D(gd, w, h, false, SurfaceFormat.Color);
tex.SetDataPointerEXT(0, null, dataPtr, w * h * 4);
_UnloadTextureRaw(dataPtr);
return tex;
}

[MonoModIfFlag("FNA")]
[MonoModPatch("_LoadTextureRaw")]
[MonoModReplace]
private static unsafe void _LoadTextureRawFNA(GraphicsDevice gd, Stream stream, out int w, out int h, out byte[] data, out IntPtr dataPtr, bool gc) {
private static unsafe void _LoadTextureRaw(GraphicsDevice gd, Stream stream, out int w, out int h, out byte[] data, out IntPtr dataPtr, bool gc) {
if (gc) {
Texture2D.TextureDataFromStreamEXT(stream, out w, out h, out data);
dataPtr = IntPtr.Zero;
} else {
data = Array.Empty<byte>();
data = [];
dataPtr = FNA3D_ReadImageStream(stream, out w, out h, out _);
}
}


[MonoModIfFlag("FNA")]
[MonoModPatch("_LoadTextureLazyPremultiply")]
[MonoModReplace]
private static unsafe void _LoadTextureLazyPremultiplyFNA(GraphicsDevice gd, Stream stream, out int w, out int h, out byte[] data, out IntPtr dataPtr, bool gc) {
private static unsafe void _LoadTextureLazyPremultiply(GraphicsDevice gd, Stream stream, out int w, out int h, out byte[] data, out IntPtr dataPtr, bool gc) {
int length;
if (gc) {
Texture2D.TextureDataFromStreamEXT(stream, out w, out h, out data);
dataPtr = IntPtr.Zero;
length = data.Length;
} else {
data = Array.Empty<byte>();
data = [];
dataPtr = FNA3D_ReadImageStream(stream, out w, out h, out length);
}

Expand All @@ -438,10 +255,7 @@ private static unsafe void _LoadTextureLazyPremultiplyFNA(GraphicsDevice gd, Str
}
}

[MonoModIfFlag("FNA")]
[MonoModPatch("_UnloadTextureRaw")]
[MonoModReplace]
private static unsafe void _UnloadTextureRawFNA(IntPtr dataPtr) {
private static unsafe void _UnloadTextureRaw(IntPtr dataPtr) {
FNA3D_Image_Free(dataPtr);
}

Expand Down
16 changes: 16 additions & 0 deletions Celeste.Mod.mm/Mod/Everest/Everest.Events.cs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,22 @@ public static class SubHudRenderer {
internal static void BeforeRender(_SubHudRenderer renderer, Scene scene)
=> OnBeforeRender?.Invoke(renderer, scene);
}

public static class VirtualTexture {
public delegate bool ForceLazyLoadHandler(Monocle.VirtualTexture self);

public static event ForceLazyLoadHandler ShouldForceLazyLoad;

internal static bool OnShouldForceLazyLoad(Monocle.VirtualTexture self) {
return ShouldForceLazyLoad.InvokeWhileFalse(self);
}

public delegate void LazyLoadHandler(Monocle.VirtualTexture self);

public static event LazyLoadHandler OnLazyLoad;
internal static void LazyLoad(Monocle.VirtualTexture tex)
=> OnLazyLoad?.Invoke(tex);
}
}
}
}
5 changes: 3 additions & 2 deletions Celeste.Mod.mm/Mod/Everest/Everest.Flags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ public static class Flags {
public static bool AvoidRenderTargets { get; private set; }
/// <summary>
/// Does the environment (platform, ...) prefer lazy loading?
/// Used to be used by android, which is not supported currently.
/// </summary>
public static bool PreferLazyLoading { get; private set; }
[Obsolete("`PreferLazyLoading` is always false on Everest Core")]
public static bool PreferLazyLoading => false;

/// <summary>
/// Does the environment (renderer, framework ,...) prefer threaded GL?
Expand Down Expand Up @@ -80,7 +82,6 @@ internal static void Initialize() {
}

AvoidRenderTargets = Environment.GetEnvironmentVariable("EVEREST_NO_RT") == "1";
PreferLazyLoading = false;

SupportRuntimeMods = true;
SupportUpdatingEverest = true;
Expand Down
11 changes: 6 additions & 5 deletions Celeste.Mod.mm/Mod/Helpers/MainThreadHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ public void ExecuteTasksFor(int timeSlice) {
return;

// Execute tasks during our allocated time slice
_Stopwatch.Reset();
_Stopwatch.Restart();
while (_Stopwatch.ElapsedMilliseconds < timeSlice) {
if (!_TasksQueue.TryDequeue(out Task task))
break;
TryExecuteTask(task);
}
_Stopwatch.Stop();
}

}
Expand Down Expand Up @@ -84,7 +85,7 @@ public static ValueTask Schedule(Action act, bool forceQueue = false) {
if (forceQueue && IsMainThread) {
try {
TaskScheduler.TaskIsForceQueued = true;
return Schedule(act);
return new ValueTask(TaskFactory.StartNew(act));
} finally {
TaskScheduler.TaskIsForceQueued = false;
}
Expand All @@ -101,7 +102,7 @@ public static ValueTask<T> Schedule<T>(Func<T> act, bool forceQueue = false) {
if (forceQueue && IsMainThread) {
try {
TaskScheduler.TaskIsForceQueued = true;
return Schedule(act);
return new ValueTask<T>(TaskFactory.StartNew(act));
} finally {
TaskScheduler.TaskIsForceQueued = false;
}
Expand All @@ -117,7 +118,7 @@ public static Task Schedule(Func<Task> act, bool forceQueue = false) {
if (forceQueue && IsMainThread) {
try {
TaskScheduler.TaskIsForceQueued = true;
return Schedule(act);
return TaskFactory.StartNew(act).Unwrap();
} finally {
TaskScheduler.TaskIsForceQueued = false;
}
Expand All @@ -133,7 +134,7 @@ public static Task<T> Schedule<T>(Func<Task<T>> act, bool forceQueue = false) {
if (forceQueue && IsMainThread) {
try {
TaskScheduler.TaskIsForceQueued = true;
return Schedule(act);
return TaskFactory.StartNew(act).Unwrap();
} finally {
TaskScheduler.TaskIsForceQueued = false;
}
Expand Down
2 changes: 1 addition & 1 deletion Celeste.Mod.mm/Mod/Helpers/SynchronizedZipEntryStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Celeste.Mod.Helpers {
/// </summary>
public class SynchronizedZipEntryStream : Stream {
private readonly ZipArchive archive;
private readonly ZipArchiveEntry entry;
internal readonly ZipArchiveEntry entry;
private Stream wrappedStream;
private long position;

Expand Down
Loading