From 0eaed1f4fe42144c85eaad4b6cc32265fdb03138 Mon Sep 17 00:00:00 2001
From: adfriz <76892624+adfriz@users.noreply.github.com>
Date: Mon, 20 Jul 2026 14:15:04 +0700
Subject: [PATCH 1/2] First try, just deleting the vertex limits.
---
source/Plugins/Object.CsvB3d/Plugin.Parser.cs | 4 ----
1 file changed, 4 deletions(-)
diff --git a/source/Plugins/Object.CsvB3d/Plugin.Parser.cs b/source/Plugins/Object.CsvB3d/Plugin.Parser.cs
index 88bfec787..f98e85fb4 100644
--- a/source/Plugins/Object.CsvB3d/Plugin.Parser.cs
+++ b/source/Plugins/Object.CsvB3d/Plugin.Parser.cs
@@ -395,10 +395,6 @@ private static StaticObject ReadObject(string FileName, Encoding Encoding) {
currentHost.AddMessage(MessageType.Error, false, "v" + j.ToString(Culture) + " references a non-existing vertex in " + cmd + " at line " + (i + 1).ToString(Culture) + " in file " + FileName);
q = false;
break;
- } else if (a[j] > 65535) {
- currentHost.AddMessage(MessageType.Error, false, "v" + j.ToString(Culture) + " indexes a vertex above 65535 which is not currently supported in " + cmd + " at line " + (i + 1).ToString(Culture) + " in file " + FileName);
- q = false;
- break;
}
}
From b07b092e2843ed236cb76a6c6ba9da1aaa43f550 Mon Sep 17 00:00:00 2001
From: adfriz <76892624+adfriz@users.noreply.github.com>
Date: Mon, 20 Jul 2026 18:03:05 +0700
Subject: [PATCH 2/2] Reduce mesh load RAM and fix vertex normals overlay
- Build the normals debug VAO lazily and render it with a
single DrawArrays call, avoiding a duplicate index buffer in RAM.
- Show normals in solid blue
- Force a GC collect when normals are hidden so RAM drops immediately.
- Add LibRenderVertex constructor and a
DrawArrays overload for the normals
---
source/LibRender2/BaseRenderer.cs | 80 ++++++-
source/LibRender2/openGL/Vertex.cs | 10 +
source/LibRender2/openGL/VertexArrayObject.cs | 203 +++++++++++++-----
3 files changed, 239 insertions(+), 54 deletions(-)
diff --git a/source/LibRender2/BaseRenderer.cs b/source/LibRender2/BaseRenderer.cs
index 1055f9da7..8f9a404c7 100644
--- a/source/LibRender2/BaseRenderer.cs
+++ b/source/LibRender2/BaseRenderer.cs
@@ -170,7 +170,59 @@ public Vector2 ScaleFactor
public bool OptionLighting = true;
/// Whether normals rendering is enabled in the debug options
- public bool OptionNormals = false;
+ private bool optionNormals = false;
+
+ /// Whether normals rendering is enabled in the debug options
+ /// Disposing the normals VAO when disabled frees the duplicate vertex buffer held in RAM.
+ public bool OptionNormals
+ {
+ get => optionNormals;
+ set
+ {
+ if (value == optionNormals)
+ {
+ return;
+ }
+
+ optionNormals = value;
+ if (!value)
+ {
+ DisposeNormalsVAOs();
+ // Force the GC so the freed normals buffers are gone from RAM immediately.
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+ }
+ }
+
+ private void DisposeNormalsVAOs()
+ {
+ if (StaticObjectStates != null)
+ {
+ foreach (var state in StaticObjectStates)
+ {
+ DisposeNormalsVAO(state);
+ }
+ }
+
+ if (DynamicObjectStates != null)
+ {
+ foreach (var state in DynamicObjectStates)
+ {
+ DisposeNormalsVAO(state);
+ }
+ }
+ }
+
+ private static void DisposeNormalsVAO(ObjectState state)
+ {
+ if (state?.Prototype?.Mesh?.NormalsVAO is VertexArrayObject normalsVao)
+ {
+ normalsVao.UnBind();
+ normalsVao.Dispose();
+ state.Prototype.Mesh.NormalsVAO = null;
+ }
+ }
/// Whether back face culling is enabled
public bool OptionBackFaceCulling = true;
@@ -1429,10 +1481,28 @@ public void RenderFace(Shader shader, ObjectState state, MeshFace face, bool deb
shader.DisableTexturing();
shader.SetBrightness(1.0f);
shader.SetOpacity(1.0f);
- VertexArrayObject normalsVao = (VertexArrayObject)state.Prototype.Mesh.NormalsVAO;
- normalsVao.Bind();
- lastVAO = normalsVao.handle;
- normalsVao.Draw(PrimitiveType.Lines, face.NormalsIboStartIndex, face.Vertices.Length * 2);
+ Mesh normalsMesh = state.Prototype.Mesh;
+ if (normalsMesh.NormalsVAO == null)
+ {
+ // Build the normals VAO lazily on first use to avoid holding a duplicate vertex buffer in RAM
+ VAOExtensions.CreateNormalsVAO(normalsMesh, state.Prototype.Dynamic, DefaultShader.VertexLayout, this);
+ }
+ VertexArrayObject normalsVao = (VertexArrayObject)normalsMesh.NormalsVAO;
+ if (normalsVao != null && face.IboStartIndex == 0)
+ {
+ // Draw all normals for this mesh in a single call (the normals VAO is a contiguous list of line pairs).
+ // Solid purple overlay: disable lighting and force a white, emissive material so the shader
+ // outputs the baked per-vertex purple colour unchanged (finalColor *= oLightResult == white).
+ shader.SetIsLight(false);
+ shader.SetMaterialAmbient(Color32.White);
+ shader.SetMaterialFlags(MaterialFlags.Emissive);
+ normalsVao.Bind();
+ lastVAO = normalsVao.handle;
+ normalsVao.DrawArrays(PrimitiveType.Lines, 0, normalsMesh.Vertices.Length > 0 ? normalsMesh.Faces.Sum(f => f.Vertices.Length) * 2 : 0);
+ shader.SetIsLight(OptionLighting);
+ shader.SetMaterialFlags(material.Flags);
+ shader.SetMaterialAmbient(material.Color);
+ }
}
// finalize
diff --git a/source/LibRender2/openGL/Vertex.cs b/source/LibRender2/openGL/Vertex.cs
index fc25cf67c..82f791897 100644
--- a/source/LibRender2/openGL/Vertex.cs
+++ b/source/LibRender2/openGL/Vertex.cs
@@ -39,6 +39,16 @@ public LibRenderVertex(Vector3f position)
MatrixChain = new Vector3i(-1, 0, 0);
}
+ /// Creates a LibRenderVertex from a position, normal and a solid color (used by the debug normals overlay)
+ public LibRenderVertex(Vector3 position, Vector3 normal, Color128 color)
+ {
+ Position = new Vector3f(position.X, position.Y, position.Z);
+ Normal = new Vector3f(normal.X, normal.Y, normal.Z);
+ UV = Vector2f.Null;
+ Color = color;
+ MatrixChain = new Vector3i(-1, 0, 0);
+ }
+
/// Creates a LibRenderVertex from a template vertex and a normal
public LibRenderVertex(VertexTemplate template, Vector3f normal)
{
diff --git a/source/LibRender2/openGL/VertexArrayObject.cs b/source/LibRender2/openGL/VertexArrayObject.cs
index 6d20cbae3..879537952 100644
--- a/source/LibRender2/openGL/VertexArrayObject.cs
+++ b/source/LibRender2/openGL/VertexArrayObject.cs
@@ -118,6 +118,20 @@ public void Draw(PrimitiveType DrawMode, int Start, int Count)
ibo.Draw(DrawMode, Start, Count);
}
+ ///
+ /// Draws the VAO using non-indexed rendering (no IBO required).
+ /// Specifies the primitive or primitives that will be created from vertices.
+ /// Start position of the first vertex.
+ /// Number of vertices to render.
+ ///
+ /// Used by the debug normals overlay, where the vertex buffer is already laid out as contiguous line pairs.
+ /// Avoids holding a duplicate index buffer in RAM.
+ ///
+ public void DrawArrays(PrimitiveType DrawMode, int Start, int Count)
+ {
+ GL.DrawArrays(DrawMode, Start, Count);
+ }
+
///
/// Unbinds the VAO deactivating the VAO from use
///
@@ -201,72 +215,163 @@ private static void createVAO(Mesh mesh, bool isDynamic, VertexLayout vertexLayo
* This marginally helps loading very large objects (as default C# list starts at a capacity of 4 then doubles exponentially)
*/
- var vertexData = new List(mesh.Vertices.Length);
- var indexData = new List();
+ int totalFaceVertices = 0;
+ for (int i = 0; i < mesh.Faces.Length; i++)
+ {
+ totalFaceVertices += mesh.Faces[i].Vertices.Length;
+ }
+
+ var vertexData = new List(totalFaceVertices);
+ var indexData = new List(totalFaceVertices);
- var normalsVertexData = new List(mesh.Vertices.Length * 2);
- var normalsIndexData = new List();
+ for (int i = 0; i < mesh.Faces.Length; i++)
+ {
+ mesh.Faces[i].IboStartIndex = indexData.Count;
- for (int i = 0; i < mesh.Faces.Length; i++)
+ foreach (var vertex in mesh.Faces[i].Vertices)
{
- mesh.Faces[i].IboStartIndex = indexData.Count;
- mesh.Faces[i].NormalsIboStartIndex = normalsIndexData.Count;
+ vertexData.Add(new LibRenderVertex(mesh.Vertices[vertex.Index], vertex.Normal));
+ }
- foreach (var vertex in mesh.Faces[i].Vertices)
- {
- vertexData.Add(new LibRenderVertex(mesh.Vertices[vertex.Index], vertex.Normal));
- normalsVertexData.Add(new LibRenderVertex(mesh.Vertices[vertex.Index].Coordinates));
- normalsVertexData.Add(new LibRenderVertex(mesh.Vertices[vertex.Index].Coordinates + vertex.Normal));
- }
+ indexData.AddRange(Enumerable.Range(mesh.Faces[i].IboStartIndex, mesh.Faces[i].Vertices.Length).Select(x => (uint)x));
+ }
- indexData.AddRange(Enumerable.Range(mesh.Faces[i].IboStartIndex, mesh.Faces[i].Vertices.Length).Select(x => (uint)x));
- normalsIndexData.AddRange(Enumerable.Range(mesh.Faces[i].NormalsIboStartIndex, mesh.Faces[i].Vertices.Length * 2).Select(x => (uint)x));
- }
+ VertexArrayObject VAO = (VertexArrayObject)mesh.VAO;
+ VAO?.UnBind();
+ VAO?.Dispose();
- VertexArrayObject VAO = (VertexArrayObject)mesh.VAO;
- VAO?.UnBind();
- VAO?.Dispose();
+ VAO = new VertexArrayObject();
+ VAO.Bind();
+ VAO.SetVBO(new VertexBufferObject(vertexData.ToArray(), hint));
+ if (indexData.Count > 65530)
+ {
+ //Marginal headroom, although it probably doesn't matter
+ VAO.SetIBO(new IndexBufferObjectUI(indexData.ToArray(), hint));
+ }
+ else
+ {
+ VAO.SetIBO(new IndexBufferObjectUS(indexData.Select(x => (ushort)x).ToArray(), hint));
+ }
- VAO = new VertexArrayObject();
- VAO.Bind();
- VAO.SetVBO(new VertexBufferObject(vertexData.ToArray(), hint));
- if (indexData.Count > 65530)
- {
- //Marginal headroom, although it probably doesn't matter
- VAO.SetIBO(new IndexBufferObjectUI(indexData.ToArray(), hint));
- }
- else
+ VAO.SetAttributes(vertexLayout);
+ VAO.UnBind();
+ mesh.VAO = VAO;
+
+ /*
+ * The normals VAO is only used for the debug normals overlay (OptionNormals).
+ * It is built lazily via CreateNormalsVAO to avoid holding a second large
+ * vertex buffer in RAM for every loaded mesh.
+ */
+ VertexArrayObject NormalsVAO = (VertexArrayObject)mesh.NormalsVAO;
+ NormalsVAO?.UnBind();
+ NormalsVAO?.Dispose();
+ mesh.NormalsVAO = null;
+ }
+ catch (Exception e)
+ {
+ renderer.currentHost.AddMessage(MessageType.Error, false, $"Creating VAO failed with the following error: {e}");
+ }
+ }
+
+ /// Creates the OpenGL/OpenTK VAO for the normals overlay of a mesh.
+ /// This is built lazily on first use of the normals debug view, as it duplicates the vertex data.
+ public static void CreateNormalsVAO(Mesh mesh, bool isDynamic, VertexLayout vertexLayout, BaseRenderer renderer)
+ {
+ if (mesh == null || mesh.NormalsVAO is VertexArrayObject)
+ {
+ return;
+ }
+ if (!renderer.GameWindow.Context.IsCurrent)
+ {
+ renderer.RunInRenderThread(() =>
{
- VAO.SetIBO(new IndexBufferObjectUS(indexData.Select(x => (ushort)x).ToArray(), hint));
- }
+ createNormalsVAO(mesh, isDynamic, vertexLayout, renderer);
+ }, 2000);
+ }
+ else
+ {
+ createNormalsVAO(mesh, isDynamic, vertexLayout, renderer);
+ }
+ }
- VAO.SetAttributes(vertexLayout);
- VAO.UnBind();
- mesh.VAO = VAO;
- VertexArrayObject NormalsVAO = (VertexArrayObject)mesh.NormalsVAO;
- NormalsVAO?.UnBind();
- NormalsVAO?.Dispose();
-
- NormalsVAO = new VertexArrayObject();
- NormalsVAO.Bind();
- NormalsVAO.SetVBO(new VertexBufferObject(normalsVertexData.ToArray(), hint));
- if (normalsIndexData.Count > 65530)
+ private static void createNormalsVAO(Mesh mesh, bool isDynamic, VertexLayout vertexLayout, BaseRenderer renderer)
+ {
+ if (mesh == null)
+ {
+ return;
+ }
+ try
+ {
+ var hint = isDynamic ? BufferUsageHint.DynamicDraw : BufferUsageHint.StaticDraw;
+
+ int totalFaceVertices = 0;
+ for (int i = 0; i < mesh.Faces.Length; i++)
+ {
+ totalFaceVertices += mesh.Faces[i].Vertices.Length;
+ }
+
+ // Debug normals overlay: a contiguous list of line pairs (vertex -> vertex + normal).
+ // Rendered with DrawArrays (no IBO), so we do not hold a duplicate index buffer in RAM.
+ // Colour is baked per-vertex (purple) because the shader multiplies the uniform colour by the
+ // (zeroed) vertex colour attribute when colour is disabled, which would otherwise render black.
+ var normalsVertexData = new List(totalFaceVertices * 2);
+ var normalColor = new Color128(0.6f, 0.2f, 1.0f, 1.0f); // purple
+
+ // Scale normal lines relative to the mesh bounding box so they stay a sensible on-screen size.
+ // A fixed 1-unit length blows up into huge overdraw when the camera is close to a small mesh.
+ double scale = 0.01;
+ if (mesh.Vertices.Length > 0)
+ {
+ Vector3 min = mesh.Vertices[0].Coordinates;
+ Vector3 max = mesh.Vertices[0].Coordinates;
+ for (int v = 1; v < mesh.Vertices.Length; v++)
{
- //Marginal headroom, although it probably doesn't matter
- NormalsVAO.SetIBO(new IndexBufferObjectUI(normalsIndexData.ToArray(), hint));
+ Vector3 c = mesh.Vertices[v].Coordinates;
+ min.X = Math.Min(min.X, c.X); min.Y = Math.Min(min.Y, c.Y); min.Z = Math.Min(min.Z, c.Z);
+ max.X = Math.Max(max.X, c.X); max.Y = Math.Max(max.Y, c.Y); max.Z = Math.Max(max.Z, c.Z);
}
- else
+ double size = Math.Max(max.X - min.X, Math.Max(max.Y - min.Y, max.Z - min.Z));
+ if (size > 0.0)
{
- NormalsVAO.SetIBO(new IndexBufferObjectUS(normalsIndexData.Select(x => (ushort)x).ToArray(), hint));
+ scale = size * 0.01;
}
+ }
- NormalsVAO.SetAttributes(vertexLayout);
- NormalsVAO.UnBind();
- mesh.NormalsVAO = NormalsVAO;
+ for (int i = 0; i < mesh.Faces.Length; i++)
+ {
+ foreach (var vertex in mesh.Faces[i].Vertices)
+ {
+ Vector3 coordinates = mesh.Vertices[vertex.Index].Coordinates;
+ Vector3 tip = coordinates + vertex.Normal * scale;
+ // Pass the normal so the shader's lighting produces a visible (non-black) colour.
+ // UV/MatrixChain are omitted so the shader cannot sample any texture.
+ normalsVertexData.Add(new LibRenderVertex(coordinates, vertex.Normal, normalColor));
+ normalsVertexData.Add(new LibRenderVertex(tip, vertex.Normal, normalColor));
+ }
}
+
+ VertexArrayObject NormalsVAO = (VertexArrayObject)mesh.NormalsVAO;
+ NormalsVAO?.UnBind();
+ NormalsVAO?.Dispose();
+
+ NormalsVAO = new VertexArrayObject();
+ NormalsVAO.Bind();
+ NormalsVAO.SetVBO(new VertexBufferObject(normalsVertexData.ToArray(), hint));
+ // Position + Normal + Colour: UV/matrix omitted so the shader cannot sample any texture,
+ // but the normal is supplied so lighting yields a visible (non-black) colour.
+ var normalsLayout = new VertexLayout
+ {
+ Position = vertexLayout.Position,
+ Normal = vertexLayout.Normal,
+ Color = vertexLayout.Color
+ };
+ NormalsVAO.SetAttributes(normalsLayout);
+ NormalsVAO.UnBind();
+ mesh.NormalsVAO = NormalsVAO;
+ }
catch (Exception e)
{
- renderer.currentHost.AddMessage(MessageType.Error, false, $"Creating VAO failed with the following error: {e}");
+ renderer.currentHost.AddMessage(MessageType.Error, false, $"Creating normals VAO failed with the following error: {e}");
}
}