Skip to content
19 changes: 2 additions & 17 deletions DarkUI/DarkUI/Collections/ObservableList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,13 @@ public class ObservableList<T> : List<T>, IDisposable

#endregion

#region Destructor Region

~ObservableList()
Comment thread
Lwmte marked this conversation as resolved.
{
Dispose(false);
}

#endregion

#region Dispose Region

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
public virtual void Dispose()
{
if (_disposed)
return;

ItemsAdded = null;
ItemsRemoved = null;

Expand Down
9 changes: 7 additions & 2 deletions TombEditor/Controls/ImportedGeometryManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ public void Set(SetValue setValue)

private readonly Color _correctColor;
private readonly Color _wrongColor;
private ListChangedEventHandler _listChangedHandler;

public ImportedGeometryManager()
{
Expand Down Expand Up @@ -193,7 +194,7 @@ public ImportedGeometryManager()
};
dataGridView.DataSource = _dataGridViewDataSource;
dataGridViewControls.DeleteRowCheckIfCancel = MessageUserAboutHimDeletingRows;
_dataGridViewDataSource.ListChanged += delegate (object sender, ListChangedEventArgs e)
_listChangedHandler = delegate (object sender, ListChangedEventArgs e)
{
switch (e.ListChangedType)
{
Expand All @@ -203,6 +204,7 @@ public ImportedGeometryManager()
break;
}
};
_dataGridViewDataSource.ListChanged += _listChangedHandler;

Enabled = true;

Expand All @@ -214,8 +216,11 @@ protected override void Dispose(bool disposing)
{
if (disposing)
{
_dataGridViewDataSource.ListChanged -= _listChangedHandler;
components?.Dispose();
Editor.Instance.EditorEventRaised -= EditorEventRaised;

if (Editor.Instance is not null)
Editor.Instance.EditorEventRaised -= EditorEventRaised;
}

base.Dispose(disposing);
Expand Down
4 changes: 4 additions & 0 deletions TombEditor/Controls/Panel3D/Panel3D.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ public bool DisablePickingForHiddenRooms
private Buffer<SolidVertex> _objectHeightLineVertexBuffer;
private Buffer<SolidVertex> _flybyPathVertexBuffer;
private Buffer<SolidVertex> _ghostBlockVertexBuffer;
private SolidVertex[] _ghostBlockVertices = new SolidVertex[84];
private float[] _roomsDistanceCache;
private Buffer<SolidVertex> _boxVertexBuffer;

// Flyby stuff
Expand Down Expand Up @@ -243,6 +245,7 @@ protected override void Dispose(bool disposing)
_rasterizerStateDepthBias?.Dispose();
_currentContextMenu?.Dispose();
_wadRenderer?.Dispose();
_fontDefault?.Dispose();
}
Comment thread
Nickelony marked this conversation as resolved.
base.Dispose(disposing);
}
Expand Down Expand Up @@ -397,6 +400,7 @@ obj is Editor.HideSelectionEvent ||
// Stop camera animation if level is changing
if (obj is Editor.LevelChangedEvent)
{
_roomsDistanceCache = null;
_movementTimer.Stop(true);

if (_editor.CameraPreviewMode != CameraPreviewType.None)
Expand Down
5 changes: 3 additions & 2 deletions TombEditor/Controls/Panel3D/Panel3DDraw.cs
Original file line number Diff line number Diff line change
Expand Up @@ -713,8 +713,9 @@ private void DrawGhostBlockBodies(Effect effect, List<GhostBlockInstance> ghostB
if (!instance.Valid)
continue;

// Create a vertex array
SolidVertex[] vtxs = new SolidVertex[84]; // 78 with diagonal steps
// Reuse cached vertex array
SolidVertex[] vtxs = _ghostBlockVertices;
Array.Clear(vtxs, 0, vtxs.Length);

// Derive base sector colours
var p1c = new Vector4(baseColor.To3() * (selected ? 0.8f : 0.4f), selected ? 0.7f : 0.5f);
Expand Down
8 changes: 5 additions & 3 deletions TombEditor/Controls/Panel3D/Panel3DDrawCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,14 @@ Room[] CollectRoomsToDraw()
// Collect rooms to draw
var camPos = Camera.GetPosition();
var roomsToDraw = CollectRoomsToDraw(_editor.SelectedRoom).ToArray();
var roomsToDrawDistanceSquared = new float[roomsToDraw.Length];

if (_roomsDistanceCache == null || _roomsDistanceCache.Length < roomsToDraw.Length)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If I understand correctly, even if now _roomsDistanceCache is not recreated every draw call, it can grow infinitely, depending on the overall room count, and is never cleared. For example, if I load a level with 700 rooms and turn on "Draw all rooms" mode, _roomsDistanceCache will now grow to 700 units and will stay like that, even if I will close the level and make a new one with just 1 room.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

You're right, I've pushed a fix for this.

_roomsDistanceCache = new float[roomsToDraw.Length];

for (int i = 0; i < roomsToDraw.Length; ++i)
roomsToDrawDistanceSquared[i] = Vector3.DistanceSquared(camPos, roomsToDraw[i].WorldPos + roomsToDraw[i].GetLocalCenter());
_roomsDistanceCache[i] = Vector3.DistanceSquared(camPos, roomsToDraw[i].WorldPos + roomsToDraw[i].GetLocalCenter());

Array.Sort(roomsToDrawDistanceSquared, roomsToDraw);
Array.Sort(_roomsDistanceCache, roomsToDraw, 0, roomsToDraw.Length);
Array.Reverse(roomsToDraw);

return roomsToDraw;
Expand Down
19 changes: 13 additions & 6 deletions TombLib/TombLib.Rendering/Rendering/RenderingFont.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,23 +315,30 @@ public class GlyphRenderInfo

public void Dispose()
{
if (!_disposed)
if (_disposed)
return;
_disposed = true;

TextureAllocator?.Dispose();
Comment thread
Nickelony marked this conversation as resolved.
ReleaseUnmanagedResources();

GC.SuppressFinalize(this);
}

~RenderingFont()
{
ReleaseUnmanagedResources();
}

private void ReleaseUnmanagedResources()
{
GDI.DeleteObject(_gdiFont);
GDI.DeleteDC(_gdiHdc);
Marshal.FreeHGlobal(_gdiGetCharacterPlacementOrder);
Marshal.FreeHGlobal(_gdiGetCharacterPlacementDx);
Marshal.FreeHGlobal(_gdiGetCharacterPlacementGlpyhs);
}

~RenderingFont()
{
Dispose();
}

private static class GDI
{
public class GDIException : Exception
Expand Down
2 changes: 2 additions & 0 deletions TombLib/TombLib/IO/BinaryWriterFast.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ public void Dispose()
_ptr = null;
_startPtr = null;
_baseStream = null;

GC.SuppressFinalize(this);
Comment thread
Lwmte marked this conversation as resolved.
}

public Stream BaseStream
Expand Down
27 changes: 23 additions & 4 deletions TombLib/TombLib/LevelData/ImportedGeometry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

namespace TombLib.LevelData
{
public class ImportedGeometryTexture : Texture
public class ImportedGeometryTexture : Texture, IDisposable
{
public Texture2D DirectXTexture { get; private set; }

private bool _disposed;

Comment thread
Nickelony marked this conversation as resolved.
public ImportedGeometryTexture(string absolutePath)
{
AbsolutePath = absolutePath;
Expand All @@ -31,22 +33,39 @@ public ImportedGeometryTexture(string absolutePath)
if (SynchronizationContext.Current == null)
DirectXTexture = TextureLoad.Load(ImportedGeometry.Device, Image);
else
SynchronizationContext.Current.Post(unused => // Synchronize DirectX, we can't 'send' because that may deadlock with the level settings reloader
DirectXTexture = TextureLoad.Load(ImportedGeometry.Device, Image), null);
SynchronizationContext.Current.Post(unused => {
Comment thread
Lwmte marked this conversation as resolved.
if (_disposed)
return;

DirectXTexture = TextureLoad.Load(ImportedGeometry.Device, Image);
Comment thread
Nickelony marked this conversation as resolved.
}, null);
}

private ImportedGeometryTexture(ImportedGeometryTexture other)
{
DirectXTexture = other.DirectXTexture;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this assignment now happening elsewhere, or not needed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is an ownership problem, since DirectXTexture needs to be disposed, you can't make 2 instances of ImportedGeometryTexture share the same reference. I remember checking this and found no issues with this being removed, but I will check again later.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@copilot From my initial analysis, the clones of the textures are purely for data driven operations, and the DX texture itself doesn't need to be copied, but I please double-check this claim and whether the DX texture not being copied causes any new issues/regressions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified - removing DirectXTexture from the clone copy constructor is safe and does not cause regressions.

Analysis:

  • The Clone() method on ImportedGeometryTexture is inherited from the abstract Texture class
  • After exhaustive search, ImportedGeometryTexture.Clone() is never called anywhere in the codebase
  • All explicit texture.Clone() calls use (LevelTexture)texture.Clone() cast, targeting only LevelTexture objects
  • The LevelSettings.Textures list only contains LevelTexture, not ImportedGeometryTexture
  • ImportedGeometryTexture is stored in ImportedGeometry.Textures list, and ImportedGeometry.Clone() uses MemberwiseClone which copies the list reference, not the individual textures

Key points:

  1. Rendering paths (Panel3DDraw, WadObjectRenderHelper) access DirectXTexture directly via Material.Texture - no cloning involved
  2. Compilers access the texture's Image property for data operations - DirectXTexture is GPU-only
  3. Even if Clone() were called, the cloned texture would be data-only which is correct since DirectXTexture is uploaded lazily in the constructor

The change is correct and improves ownership semantics.

AbsolutePath = other.AbsolutePath;
Image = other.Image;
}

public void Assign(ImportedGeometryTexture other)
{
// Dispose old GPU texture if it differs from the new one.
if (DirectXTexture != null && DirectXTexture != other.DirectXTexture)
DirectXTexture.Dispose();

AbsolutePath = other.AbsolutePath;
Image = other.Image;
DirectXTexture = other.DirectXTexture;

_disposed = false;
}

public void Dispose()
{
_disposed = true;

DirectXTexture?.Dispose();
DirectXTexture = null;
}
Comment thread
Nickelony marked this conversation as resolved.

public override Texture Clone() => new ImportedGeometryTexture(this);
Comment thread
Nickelony marked this conversation as resolved.
Expand Down
24 changes: 16 additions & 8 deletions TombLib/TombLib/Wad/WadTexture.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Buffers.Binary;
using System.IO;
using TombLib.Graphics;
using TombLib.Utils;
using Blake3Hasher = Blake3.Hasher;

namespace TombLib.Wad
{
Expand All @@ -20,14 +22,20 @@ public WadTexture(ImageC image)
{
Image = image;

using (var ms = new MemoryStream())
{
var writer = new BinaryWriter(ms);
writer.Write(Image.Size.X);
writer.Write(Image.Size.Y);
Image.WriteToStreamRaw(ms);
Hash = Hash.FromByteArray(ms.ToArray());
}
// Hash image dimensions and pixel data directly without intermediate copies.
using var hasher = Blake3Hasher.New();
Comment thread
Lwmte marked this conversation as resolved.
Span<byte> header = stackalloc byte[8];
BinaryPrimitives.WriteInt32LittleEndian(header, Image.Size.X);
BinaryPrimitives.WriteInt32LittleEndian(header.Slice(4), Image.Size.Y);
hasher.Update(header);
hasher.Update(Image.ToByteArray());
Comment thread
Nickelony marked this conversation as resolved.

Span<byte> digest = stackalloc byte[32];
hasher.Finalize(digest);

ulong low = BinaryPrimitives.ReadUInt64LittleEndian(digest.Slice(0, 8));
ulong high = BinaryPrimitives.ReadUInt64LittleEndian(digest.Slice(8, 8));
Hash = new Hash { HashLow = low, HashHigh = high };
}

public override Texture Clone() => this;
Expand Down
1 change: 0 additions & 1 deletion WadTool/Controls/PanelRenderingAnimationEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ protected override void Dispose(bool disposing)
{
if (disposing)
{
_fontTexture?.Dispose();
_fontDefault?.Dispose();
_gizmo?.Dispose();
_plane?.Dispose();
Expand Down
1 change: 1 addition & 0 deletions WadTool/Controls/PanelRenderingMesh.cs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ protected override void Dispose(bool disposing)
_bigSphere?.Dispose();
_plane?.Dispose();
_wadRenderer?.Dispose();
_fontDefault?.Dispose();
}
base.Dispose(disposing);
}
Expand Down
1 change: 0 additions & 1 deletion WadTool/Controls/PanelRenderingSkeleton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ protected override void Dispose(bool disposing)
{
if (disposing)
{
_fontTexture?.Dispose();
_fontDefault?.Dispose();
_rasterizerWireframe?.Dispose();
_vertexBufferVisibility?.Dispose();
Expand Down
1 change: 0 additions & 1 deletion WadTool/Controls/PanelRenderingStaticEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ protected override void Dispose(bool disposing)
{
if (disposing)
{
_fontTexture?.Dispose();
_fontDefault?.Dispose();
_gizmo?.Dispose();
_gizmoLight?.Dispose();
Expand Down