Skip to content

Commit

Permalink
Further optimized WavefrontWriter's memory usage by having it write d…
Browse files Browse the repository at this point in the history
…irectly to the output FileStream rather than first to a MemoryStream and then copying.

Optimized some redundant memory allocations in the WavefrontWriter class to avoid OutOfMemory exceptions in 32-bit mode, which fixes some of the existing tests.

Made some more memory optimizations in the WavefrontWriter to limit how many objects are created and stored in memory at once.
  • Loading branch information
MeltyPlayer committed Jan 15, 2023
1 parent ed4450a commit 80d5c3a
Showing 1 changed file with 67 additions and 62 deletions.
129 changes: 67 additions & 62 deletions src/SharpGLTF.Toolkit/IO/WavefrontWriter.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;

using SharpGLTF.Memory;
using SharpGLTF.Schema2;

using static System.FormattableString;
Expand Down Expand Up @@ -55,14 +57,13 @@ public void WriteFiles(string filePath)
{
Guard.NotNullOrEmpty(filePath, nameof(filePath));

var files = GetFiles(System.IO.Path.GetFileNameWithoutExtension(filePath));

var dir = System.IO.Path.GetDirectoryName(filePath);

foreach (var f in files)
foreach (var fileNameAndGenerator in _GetFileGenerators(System.IO.Path.GetFileNameWithoutExtension(filePath)))
{
var fpath = System.IO.Path.Combine(dir, f.Key);
System.IO.File.WriteAllBytes(fpath, f.Value.ToArray());
var fpath = System.IO.Path.Combine(dir, fileNameAndGenerator.Key);
using var fs = File.OpenWrite(fpath);
fileNameAndGenerator.Value(fs);
}
}

Expand All @@ -81,17 +82,33 @@ public IReadOnlyDictionary<String, BYTES> GetFiles(string baseName)
Guard.IsFalse(baseName.Any(c => char.IsWhiteSpace(c)), nameof(baseName), "Whitespace characters not allowed in filename");

var files = new Dictionary<String, BYTES>();
foreach (var fileNameAndGenerator in _GetFileGenerators(baseName))
{
using var mem = new MemoryStream();
fileNameAndGenerator.Value(mem);

var materials = _WriteMaterials(files, baseName, _Mesh.Primitives.Select(item => item.Material));

var geocontent = _GetGeometryContent(materials, baseName + ".mtl");
mem.TryGetBuffer(out var bytes);

_WriteTextContent(files, baseName + ".obj", geocontent);
files[fileNameAndGenerator.Key] = bytes;
}

return files;
}

private static IReadOnlyDictionary<Material, string> _WriteMaterials(IDictionary<String, BYTES> files, string baseName, IEnumerable<Material> materials)
private IReadOnlyDictionary<String, Action<Stream>> _GetFileGenerators(string baseName)
{
Guard.IsFalse(baseName.Any(c => char.IsWhiteSpace(c)), nameof(baseName), "Whitespace characters not allowed in filename");

var fileGenerators = new Dictionary<String, Action<Stream>>();

var materials = _GetMaterialFileGenerator(fileGenerators, baseName, _Mesh.Primitives.Select(item => item.Material));

fileGenerators[baseName + ".obj"] = fs => _GetGeometryContent(new StreamWriter(fs), materials, baseName + ".mtl");

return fileGenerators;
}

private static IReadOnlyDictionary<Material, string> _GetMaterialFileGenerator(IDictionary<String, Action<Stream>> fileGenerators, string baseName, IEnumerable<Material> materials)
{
// write all image files
var images = materials
Expand All @@ -101,76 +118,81 @@ private static IReadOnlyDictionary<Material, string> _WriteMaterials(IDictionary

bool firstImg = true;

var imageNameByImage = new Dictionary<MemoryImage, string>();
foreach (var img in images)
{
var imgName = firstImg
? $"{baseName}.{img.FileExtension}"
: $"{baseName}_{files.Count}.{img.FileExtension}";
: $"{baseName}_{fileGenerators.Count}.{img.FileExtension}";

files[imgName] = new BYTES(img.Content.ToArray());
fileGenerators[imgName] = fs => {
var bytes = img.Content.ToArray();
fs.Write(bytes, 0, bytes.Length);
};
firstImg = false;

imageNameByImage[img] = imgName;
}

// write materials

var mmap = new Dictionary<Material, string>();

var sb = new StringBuilder();

foreach (var m in materials)
foreach (var m in materials)
{
mmap[m] = $"Material_{mmap.Count}";

sb.AppendLine($"newmtl {mmap[m]}");
sb.AppendLine("illum 2");
sb.AppendLine(Invariant($"Ka {m.DiffuseColor.X} {m.DiffuseColor.Y} {m.DiffuseColor.Z}"));
sb.AppendLine(Invariant($"Kd {m.DiffuseColor.X} {m.DiffuseColor.Y} {m.DiffuseColor.Z}"));
sb.AppendLine(Invariant($"Ks {m.SpecularColor.X} {m.SpecularColor.Y} {m.SpecularColor.Z}"));

if (m.DiffuseTexture.IsValid)
{
var imgName = files.FirstOrDefault(kvp => new Memory.MemoryImage(kvp.Value) == m.DiffuseTexture ).Key;
sb.AppendLine($"map_Kd {imgName}");
}

sb.AppendLine();
}

// write material library
_WriteTextContent(files, baseName + ".mtl", sb);
fileGenerators[baseName + ".mtl"] = fs =>
{
var sw = new StreamWriter(fs);
foreach (var m in materials)
{
sw.WriteLine($"newmtl {mmap[m]}");
sw.WriteLine("illum 2");
sw.WriteLine(Invariant($"Ka {m.DiffuseColor.X} {m.DiffuseColor.Y} {m.DiffuseColor.Z}"));
sw.WriteLine(Invariant($"Kd {m.DiffuseColor.X} {m.DiffuseColor.Y} {m.DiffuseColor.Z}"));
sw.WriteLine(Invariant($"Ks {m.SpecularColor.X} {m.SpecularColor.Y} {m.SpecularColor.Z}"));

if (m.DiffuseTexture.IsValid) {
var imgName = imageNameByImage[m.DiffuseTexture];
sw.WriteLine($"map_Kd {imgName}");
}

sw.WriteLine();
}
};

return mmap;
}

private StringBuilder _GetGeometryContent(IReadOnlyDictionary<Material, string> materials, string mtlLib)
private void _GetGeometryContent(StreamWriter sw, IReadOnlyDictionary<Material, string> materials, string mtlLib)
{
var sb = new StringBuilder();

sb.AppendLine($"mtllib {mtlLib}");
sw.WriteLine($"mtllib {mtlLib}");

sb.AppendLine();
sw.WriteLine();

foreach (var p in _Mesh.Primitives)
{
foreach (var v in p.Vertices)
{
var pos = v.Position;
sb.AppendLine(Invariant($"v {pos.X} {pos.Y} {pos.Z}"));
sw.WriteLine(Invariant($"v {pos.X} {pos.Y} {pos.Z}"));
}
}

sb.AppendLine();
sw.WriteLine();

foreach (var p in _Mesh.Primitives)
{
foreach (var v in p.Vertices)
{
var nrm = v.Geometry.Normal;
sb.AppendLine(Invariant($"vn {nrm.X} {nrm.Y} {nrm.Z}"));
sw.WriteLine(Invariant($"vn {nrm.X} {nrm.Y} {nrm.Z}"));
}
}

sb.AppendLine();
sw.WriteLine();

foreach (var p in _Mesh.Primitives)
{
Expand All @@ -179,50 +201,33 @@ private StringBuilder _GetGeometryContent(IReadOnlyDictionary<Material, string>
var uv = v.Material.TexCoord;
uv.Y = 1 - uv.Y;

sb.AppendLine(Invariant($"vt {uv.X} {uv.Y}"));
sw.WriteLine(Invariant($"vt {uv.X} {uv.Y}"));
}
}

sb.AppendLine();
sw.WriteLine();

sb.AppendLine("g default");
sw.WriteLine("g default");

var baseVertexIndex = 1;

foreach (var p in _Mesh.Primitives)
{
var mtl = materials[p.Material];

sb.AppendLine($"usemtl {mtl}");
sw.WriteLine($"usemtl {mtl}");

foreach (var t in p.Triangles)
{
var a = t.A + baseVertexIndex;
var b = t.B + baseVertexIndex;
var c = t.C + baseVertexIndex;

sb.AppendLine(Invariant($"f {a}/{a}/{a} {b}/{b}/{b} {c}/{c}/{c}"));
sw.WriteLine(Invariant($"f {a}/{a}/{a} {b}/{b}/{b} {c}/{c}/{c}"));
}

baseVertexIndex += p.Vertices.Count;
}

return sb;
}

private static void _WriteTextContent(IDictionary<string, BYTES> files, string fileName, StringBuilder sb)
{
using (var mem = new System.IO.MemoryStream())
{
using (var tex = new System.IO.StreamWriter(mem))
{
tex.Write(sb.ToString());
}

mem.TryGetBuffer(out BYTES content);

files[fileName] = content;
}
}

#endregion
Expand Down

0 comments on commit 80d5c3a

Please sign in to comment.