diff --git a/LibDgf.Graphics/Mesh/ObjConverter.cs b/LibDgf.Graphics/Mesh/ObjConverter.cs new file mode 100644 index 0000000..92e7b79 --- /dev/null +++ b/LibDgf.Graphics/Mesh/ObjConverter.cs @@ -0,0 +1,395 @@ +using LibDgf.Dat; +using LibDgf.Mesh; +using LibDgf.Ps2.Gs; +using LibDgf.Ps2.Gs.Registers; +using LibDgf.Txm; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LibDgf.Graphics.Mesh +{ + public class ObjConverter : IDisposable + { + Dictionary textureCache = new Dictionary(); + DatReader textureDat; + private bool disposedValue; + int numWrittenTextures = 0; + + public ObjConverter(DatReader textureDat) + { + this.textureDat = textureDat ?? throw new ArgumentNullException(nameof(textureDat)); + } + + public void ConvertObj(Pdb pdb, StreamWriter sw) + { + int startVert = 1; + if (pdb.Specular != null) + { + sw.WriteLine("o specular"); + startVert = WriteObj(pdb.Specular, sw, startVert); + } + if (pdb.Diffuse != null) + { + sw.WriteLine("o diffuse"); + startVert = WriteObj(pdb.Diffuse, sw, startVert); + } + if (pdb.Metallic != null) + { + sw.WriteLine("o metallic"); + startVert = WriteObj(pdb.Metallic, sw, startVert); + } + } + + TxmHeader CreateTexture(Tdb tdb, int index, out ulong textureId) + { + var tdbTexture = tdb.Textures[index]; + textureId = ((ulong)tdbTexture.DatIndex << 32) | ((ulong)tdbTexture.ImageBufferBase << 16) | tdbTexture.ClutBufferBase; + if (!textureCache.ContainsKey(textureId)) + { + TxmHeader txm = new TxmHeader(); + txm.ImageBufferBase = tdbTexture.ImageBufferBase; + txm.ClutBufferBase = tdbTexture.ClutBufferBase; + textureCache.Add(textureId, txm); + return txm; + } + return null; + } + + void FillTxm(Db2Element elem, TxmHeader txm) + { + Tex0 tex0 = new Tex0 { Packed = BitConverter.ToUInt64(elem.GsRegs, 0x10) }; + txm.ImageSourcePixelFormat = (TxmPixelFormat)tex0.Psm; + txm.ImageVideoPixelFormat = txm.ImageSourcePixelFormat; + txm.ImageWidth = (short)(1 << tex0.Tw); + txm.ImageHeight = (short)(1 << tex0.Th); + txm.Misc = tex0.Tbw; + if (txm.ImageSourcePixelFormat == TxmPixelFormat.PSMT4 || txm.ImageSourcePixelFormat == TxmPixelFormat.PSMT8) + { + txm.ClutPixelFormat = (TxmPixelFormat)tex0.Cpsm; + if (txm.ImageSourcePixelFormat == TxmPixelFormat.PSMT4) + { + txm.ClutWidth = 8; + txm.ClutHeight = 2; + } + else + { + txm.ClutWidth = 16; + txm.ClutHeight = 16; + } + } + else + { + txm.ClutPixelFormat = TxmPixelFormat.None; + } + } + + int WriteObj(Tdb tdb, StreamWriter sw, int startVert) + { + for (int i = 0; i < tdb.Mesh.Elements.Count; ++i) + { + var elem = tdb.Mesh.Elements[i]; + sw.WriteLine($"g elem_{i}"); + + TxmHeader txm = CreateTexture(tdb, elem.TextureIndex, out var textureId); + sw.WriteLine($"usemtl tex_{textureId:x12}"); + if (txm != null) FillTxm(elem, txm); + + // Write vertices + foreach (var vert in elem.Vertices) + { + sw.WriteLine($"v {(double)vert.X} {(double)vert.Y} {(double)vert.Z}"); + } + + foreach (var norm in elem.VertexNormals) + { + sw.WriteLine($"vn {norm.Item1} {norm.Item2} {norm.Item3}"); + } + + foreach (var uv in elem.STCoordinates) + { + sw.WriteLine($"vt {uv.Item1} {1 - uv.Item2}"); + } + + // Write faces + int[] initVerts = new int[2]; + bool clockwise = true; + if (elem.GifTagIndex == 3) // Triangle fans + { + int initVertPos = 0; + for (int j = 0; j < elem.Vertices.Count; ++j) + { + if ((elem.Vertices[j].W.Packed & 0x00008000) != 0) + { + initVerts[initVertPos++] = startVert + j; + } + else + { + int currVert = startVert + j; + if (clockwise) + { + sw.WriteLine($"f {initVerts[0]}/{initVerts[0]}/{initVerts[0]} {initVerts[1]}/{initVerts[1]}/{initVerts[1]} {currVert}/{currVert}/{currVert}"); + } + else + { + sw.WriteLine($"f {currVert}/{currVert}/{currVert} {initVerts[1]}/{initVerts[1]}/{initVerts[1]} {initVerts[0]}/{initVerts[0]}/{initVerts[0]}"); + } + initVerts[1] = currVert; + initVertPos = 0; + } + clockwise = !clockwise; + } + } + else if (elem.GifTagIndex == 4) // Triangle strips + { + for (int j = 0; j < elem.Vertices.Count; ++j) + { + int currVert = startVert + j; + if ((elem.Vertices[j].W.Packed & 0x00008000) == 0) + { + if (clockwise) + { + sw.WriteLine($"f {initVerts[0]}/{initVerts[0]}/{initVerts[0]} {initVerts[1]}/{initVerts[1]}/{initVerts[1]} {currVert}/{currVert}/{currVert}"); + } + else + { + sw.WriteLine($"f {currVert}/{currVert}/{currVert} {initVerts[1]}/{initVerts[1]}/{initVerts[1]} {initVerts[0]}/{initVerts[0]}/{initVerts[0]}"); + } + } + initVerts[0] = initVerts[1]; + initVerts[1] = currVert; + clockwise = !clockwise; + } + } + else + { + throw new NotSupportedException("Unknown face construction type"); + } + startVert += elem.Vertices.Count; + sw.WriteLine(); + } + + return startVert; + } + + void CopyTexelsClut(BinaryReader br, BinaryWriter bw, TxmHeader pakTxm, TxmHeader textureTxm) + { + if (pakTxm.ClutPixelFormat != TxmPixelFormat.None) + throw new ArgumentException("Cannot operate on source TXM with CLUT.", nameof(pakTxm)); + if (textureTxm.ClutPixelFormat == TxmPixelFormat.None) return; + + var destColumnParams = GsMemoryUtils.GetColumnParams(textureTxm.ClutPixelFormat); + int copyLength = textureTxm.GetClutByteSize(); + int baseBlockNumber = textureTxm.ClutBufferBase - pakTxm.ImageBufferBase; + int srcBase = 0x10 + pakTxm.GetClutByteSize(); + var destBase = 0x10; + int bytesPerSrcLine = pakTxm.GetImageByteSize() / pakTxm.ImageHeight; + int bytesPerDestLine = textureTxm.GetClutByteSize() / textureTxm.ClutHeight; + + bw.Write(new byte[copyLength]); + int numXBlocks = textureTxm.ClutWidth / destColumnParams.Width; + if (numXBlocks == 0) numXBlocks = 1; + int numYBlocks = textureTxm.ClutHeight / (destColumnParams.Height * GsMemoryUtils.COLUMNS_PER_BLOCK); + if (numYBlocks == 0) numYBlocks = 1; + int destBlock = 0; + for (int blockY = 0; blockY < numYBlocks; ++blockY) + { + for (int blockX = 0; blockX < numXBlocks; ++blockX) + { + int blockNumber = baseBlockNumber + GsMemoryUtils.CalcBlockNumber(textureTxm.ClutPixelFormat, blockX, blockY, 1); + br.BaseStream.Seek(srcBase + GsMemoryUtils.CalcBlockMemoryOffset(pakTxm.ImageSourcePixelFormat, blockNumber), + SeekOrigin.Begin); + bw.BaseStream.Seek(destBase + GsMemoryUtils.CalcTxmImageOffset(destColumnParams, destBlock, textureTxm.ClutWidth), + SeekOrigin.Begin); + for (int i = 0; i < GsMemoryUtils.COLUMNS_PER_BLOCK; ++i) + { + byte[] col = GsMemoryUtils.ReadColumn(br, pakTxm.ImageSourcePixelFormat, bytesPerSrcLine); + GsMemoryUtils.WriteColumn(bw, textureTxm.ClutPixelFormat, bytesPerDestLine, col); + } + ++destBlock; + } + } + + // Dump palette + //bw.BaseStream.Seek(destBase, SeekOrigin.Begin); + //BinaryReader palBr = new BinaryReader(bw.BaseStream); + //using (var palette = TxmConversion.ConvertTxmRgba32(palBr, textureTxm.ClutWidth, textureTxm.ClutHeight)) + //{ + // palette.SaveAsPng($"palette_{numWrittenTextures}.png"); + //} + } + + void CopyTexels(BinaryReader br, BinaryWriter bw, TxmHeader pakTxm, TxmHeader textureTxm) + { + if (pakTxm.ClutPixelFormat != TxmPixelFormat.None) + throw new ArgumentException("Cannot operate on source TXM with CLUT.", nameof(pakTxm)); + + var destColumnParams = GsMemoryUtils.GetColumnParams(textureTxm.ImageSourcePixelFormat); + int copyLength = textureTxm.GetImageByteSize(); + int srcBase = 0x10 + pakTxm.GetClutByteSize(); + int baseBlockNumber = textureTxm.ImageBufferBase - pakTxm.ImageBufferBase; + int destBase = 0x10 + textureTxm.GetClutByteSize(); + int bytesPerSrcLine = pakTxm.GetImageByteSize() / pakTxm.ImageHeight; + int bytesPerDestLine = copyLength / textureTxm.ImageHeight; + + bw.Write(new byte[copyLength]); + int numXBlocks = textureTxm.ImageWidth / destColumnParams.Width; + if (numXBlocks == 0) numXBlocks = 1; + int numYBlocks = textureTxm.ImageHeight / (destColumnParams.Height * GsMemoryUtils.COLUMNS_PER_BLOCK); + if (numYBlocks == 0) numYBlocks = 1; + int destBlock = 0; + for (int blockY = 0; blockY < numYBlocks; ++blockY) + { + for (int blockX = 0; blockX < numXBlocks; ++blockX) + { + int blockNumber = baseBlockNumber + GsMemoryUtils.CalcBlockNumber(textureTxm.ImageSourcePixelFormat, blockX, blockY, textureTxm.Misc); + br.BaseStream.Seek(srcBase + GsMemoryUtils.CalcBlockMemoryOffset(pakTxm.ImageSourcePixelFormat, blockNumber), + SeekOrigin.Begin); + bw.BaseStream.Seek(destBase + GsMemoryUtils.CalcTxmImageOffset(destColumnParams, destBlock, textureTxm.ImageWidth), + SeekOrigin.Begin); + for (int i = 0; i < GsMemoryUtils.COLUMNS_PER_BLOCK; ++i) + { + byte[] col = GsMemoryUtils.ReadColumn(br, pakTxm.ImageSourcePixelFormat, bytesPerSrcLine); + if (pakTxm.ImageSourcePixelFormat != textureTxm.ImageSourcePixelFormat) + { + col = PsmtMixer.MixColumn(col, pakTxm.ImageSourcePixelFormat, textureTxm.ImageSourcePixelFormat, i % 2 != 0); + } + GsMemoryUtils.WriteColumn(bw, textureTxm.ImageSourcePixelFormat, bytesPerDestLine, col); + } + + ++destBlock; + } + } + } + + public void ExportTextures(StreamWriter mtlWriter, string outputPath) + { + if (disposedValue) throw new ObjectDisposedException(GetType().FullName); + + int i = 0; + numWrittenTextures = 0; + foreach (var pair in textureCache.OrderBy(p => p.Key)) + { + string pngPath = $"{outputPath}{i}.png"; + string alphaPath = $"{outputPath}{i}_alpha.png"; + TxmHeader textureTxm = pair.Value; + + int txmIndex = (int)(pair.Key >> 32); + using (var txmMs = new MemoryStream(textureDat.GetData(txmIndex))) + { + BinaryReader txmBr = new BinaryReader(txmMs); + TxmHeader pakTxm = new TxmHeader(); + pakTxm.Read(txmBr); + + Image img = null; + try + { + // Check if TXM is already suitable + if (/*pakTxm.ImageSourcePixelFormat == textureTxm.ImageSourcePixelFormat &&*/ + pakTxm.ImageBufferBase == textureTxm.ImageBufferBase && + pakTxm.ClutPixelFormat == textureTxm.ClutPixelFormat && + pakTxm.ClutBufferBase == textureTxm.ClutBufferBase) + { + // Use TXM as-is + txmMs.Seek(0, SeekOrigin.Begin); + img = TxmConversion.ConvertTxmToImage(txmMs); + + // Dump palette + //if (pakTxm.ClutPixelFormat != TxmPixelFormat.None) + //{ + // txmMs.Seek(0x10, SeekOrigin.Begin); + // using (var palette = TxmConversion.ConvertTxmRgba32(txmBr, pakTxm.ClutWidth, pakTxm.ClutHeight)) + // { + // palette.SaveAsPng($"palette_{numWrittenTextures}.png"); + // } + //} + } + else + { + // Generate new TXM + using (MemoryStream ms = new MemoryStream()) + { + BinaryWriter bw = new BinaryWriter(ms); + textureTxm.Write(bw); + CopyTexelsClut(txmBr, bw, pakTxm, textureTxm); + CopyTexels(txmBr, bw, pakTxm, textureTxm); + bw.Flush(); + ms.Seek(0, SeekOrigin.Begin); + img = TxmConversion.ConvertTxmToImage(ms); + } + } + + // Save out color texture + using (var img24bpp = img.CloneAs()) + { + img24bpp.SaveAsPng(pngPath); + } + + // Extract alpha channel as a separate image + using (var alphaImg = new Image(img.Width, img.Height)) + { + for (int y = 0; y < alphaImg.Height; ++y) + { + var srcSpan = img.GetPixelRowSpan(y); + var destSpan = alphaImg.GetPixelRowSpan(y); + for (int x = 0; x < alphaImg.Width; ++x) + { + var srcAlpha = srcSpan[x].A; + destSpan[x] = new L8(srcAlpha); + } + } + alphaImg.SaveAsPng(alphaPath); + } + } + finally + { + if (img != null) img.Dispose(); + } + } + + mtlWriter.WriteLine($"newmtl tex_{pair.Key:x12}"); + mtlWriter.WriteLine("Kd 0.80000000 0.80000000 0.80000000"); + mtlWriter.WriteLine("Ka 0 0 0"); + mtlWriter.WriteLine("Ke 0 0 0"); + mtlWriter.WriteLine("Ks 0 0 0"); + mtlWriter.WriteLine("d 1"); + mtlWriter.WriteLine("illum 2"); + mtlWriter.WriteLine($"map_Kd {Path.GetFileName(pngPath)}"); + mtlWriter.WriteLine($"map_d {Path.GetFileName(alphaPath)}"); + mtlWriter.WriteLine(); + + ++i; + ++numWrittenTextures; + } + } + + public void Reset() + { + textureCache.Clear(); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + textureDat.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/LibDgf.Graphics/TxmConversion.cs b/LibDgf.Graphics/TxmConversion.cs index a6f958c..624a4e8 100644 --- a/LibDgf.Graphics/TxmConversion.cs +++ b/LibDgf.Graphics/TxmConversion.cs @@ -10,7 +10,7 @@ namespace LibDgf.Graphics { public static class TxmConversion { - public static void ConvertImageToTxm(string inPath, Stream outStream, byte level = 1, short bufferBase = 0, short paletteBufferBase = 0) + public static void ConvertImageToTxm(string inPath, Stream outStream, byte level = 1, ushort bufferBase = 0, ushort paletteBufferBase = 0) { using (var image = Image.Load(inPath)) { @@ -161,14 +161,22 @@ namespace LibDgf.Graphics } public static void ConvertTxmToPng(Stream stream, string outPath) + { + using (var image = ConvertTxmToImage(stream)) + { + image.SaveAsPng(outPath); + } + } + + public static Image ConvertTxmToImage(Stream stream) { BinaryReader br = new BinaryReader(stream); TxmHeader imageHeader = new TxmHeader(); imageHeader.Read(br); Console.WriteLine(imageHeader); - if (imageHeader.Misc != 1) - Console.WriteLine("Different level!"); + //if (imageHeader.Misc != 1) + // Console.WriteLine("Different level!"); Image image; if (imageHeader.ImageSourcePixelFormat == TxmPixelFormat.PSMT8 || imageHeader.ImageSourcePixelFormat == TxmPixelFormat.PSMT4) @@ -209,8 +217,7 @@ namespace LibDgf.Graphics throw new NotSupportedException("Unsupported pixel format"); } - image.SaveAsPng(outPath); - image.Dispose(); + return image; } public static Image ConvertTxmIndexed8bpp(BinaryReader br, int width, int height, Rgba32[] palette) diff --git a/LibDgf/Mesh/Db2.cs b/LibDgf/Mesh/Db2.cs new file mode 100644 index 0000000..de84280 --- /dev/null +++ b/LibDgf/Mesh/Db2.cs @@ -0,0 +1,157 @@ +using LibDgf.Ps2.Vif; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Mesh +{ + public class Db2 + { + BinaryReader br; + + public List Elements { get; set; } = new List(); + + public void Read(BinaryReader br) + { + uint vifStreamLength = br.ReadUInt32(); + uint version = br.ReadUInt32(); + if (version != 2) throw new InvalidDataException("DB version is not 2."); + uint reserved1 = br.ReadUInt32(); + uint reserved2 = br.ReadUInt32(); + //if (reserved1 != 0 || reserved2 != 0) + // System.Diagnostics.Debugger.Break(); + + Elements.Clear(); + var startPos = br.BaseStream.Position; + var endPos = startPos + vifStreamLength; + this.br = br; + try + { + ExpectNop(); + ExpectNop(); + ExpectStCycl(); + ExpectUnpackV4_32(); + + while (br.BaseStream.Position < endPos) + { + Db2Element element = new Db2Element(); + element.ElementLength = br.ReadInt32(); + element.Flags = br.ReadUInt32(); + //if ((element.Flags & 0xFFFFE0E0) != 0) + // System.Diagnostics.Debugger.Break(); + element.TextureIndex = br.ReadInt32(); + element.Reserved = br.ReadUInt32(); + //if (element.Reserved != 0) + // System.Diagnostics.Debugger.Break(); + + int numReglistWords = (int)(element.Flags & 0xf); + if (numReglistWords != 0) + { + element.GsRegs = br.ReadBytes((numReglistWords + 1) * 16); + } + + element.GifTagFan = br.ReadBytes(16); + element.GifTagStrip = br.ReadBytes(16); + + // Retrieve number of vertices from GIFtag NLOOP + int numVertices = element.VertexCount; + for (int i = 0; i < numVertices; ++i) + { + element.Vertices.Add(br.ReadV4_32()); + } + + ExpectUnpackV3_16((byte)numVertices); + for (int i = 0; i < numVertices; ++i) + { + element.VertexNormals.Add(new Tuple( + Utils.Convert12BitFixedToDouble(br.ReadInt16()), + Utils.Convert12BitFixedToDouble(br.ReadInt16()), + Utils.Convert12BitFixedToDouble(br.ReadInt16()) + )); + } + ReadAlign(startPos, 4); + + // Do STs exist if no texture? + ExpectUnpackV2_16((byte)numVertices); + for (int i = 0; i < numVertices; ++i) + { + element.STCoordinates.Add(new Tuple( + Utils.Convert12BitFixedToDouble(br.ReadInt16()), + Utils.Convert12BitFixedToDouble(br.ReadInt16()) + )); + } + ReadAlign(startPos, 16); // Skipping over some NOPs in the process, assume that's what they are + + ExpectMsCnt(); + + Elements.Add(element); + + ExpectNop(); + if (br.BaseStream.Position == endPos - 8) + { + ExpectNop(); + ExpectNop(); + } + else + { + ExpectStCycl(); + ExpectUnpackV4_32(); + } + } + } + finally + { + this.br = null; + } + } + + void ReadAlign(long startPos, int bytes) + { + var aligned = (br.BaseStream.Position - startPos + bytes - 1) / bytes * bytes; + br.BaseStream.Seek(startPos + aligned - br.BaseStream.Position, SeekOrigin.Current); + } + + #region Sanity checking functions + + VifCode ExpectNop() + { + return ExpectVifCode(0x00000000, 0x7f000000); + } + + VifCode ExpectStCycl() + { + return ExpectVifCode(0x01000404, 0x7f00ffff); + } + + VifCodeUnpack ExpectUnpackV4_32() + { + return (VifCodeUnpack)ExpectVifCode(0x6c008000, 0x6f00c000); + } + + VifCodeUnpack ExpectUnpackV3_16(byte num) + { + return (VifCodeUnpack)ExpectVifCode(0x69008000 | ((uint)num << 16), 0x6fffc000); + } + + VifCodeUnpack ExpectUnpackV2_16(byte num) + { + return (VifCodeUnpack)ExpectVifCode(0x65008000 | ((uint)num << 16), 0x6fffc000); + } + + VifCode ExpectMsCnt() + { + return ExpectVifCode(0x17000000, 0x7f000000); + } + + VifCode ExpectVifCode(uint value, uint mask) + { + uint read = br.ReadUInt32(); + if ((read & mask) != value) + throw new InvalidDataException($"VIFcode expectation failed at {br.BaseStream.Position - 4:x8}."); + return new VifCode { Value = read }; + } + + #endregion + } +} diff --git a/LibDgf/Mesh/Db2Element.cs b/LibDgf/Mesh/Db2Element.cs new file mode 100644 index 0000000..01aa159 --- /dev/null +++ b/LibDgf/Mesh/Db2Element.cs @@ -0,0 +1,89 @@ +using LibDgf.Ps2.Vif; +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Mesh +{ + public class Db2Element + { + // Size of element in 128-bit words, including header, excluding surrounding VIFcodes + public int ElementLength { get; set; } + // 0x000f: number of REGLIST words (excluding leading GIFtag) + // 0x0010: has texture + // 0x00e0: reserved + // 0x0100: has vertex color (deprecated, probably) + // 0x0e00: GIFtag index - with standard REGLIST primitive: 3 = triangle fan, 4 = triangle strip + // 0x1000: disable lighting + // 0x80000: textures enabled (only set at runtime) + public uint Flags { get; set; } + public int TextureIndex { get; set; } // Actually byte + public uint Reserved { get; set; } + + public byte[] GsRegs { get; set; } // One GS primitive, CLAMP_1 and TEX0_1 + public byte[] GifTagFan { get; set; } + public byte[] GifTagStrip { get; set; } + + public List Vertices { get; } = new List(); + public List> VertexNormals { get; } = new List>(); + public List> STCoordinates { get; } = new List>(); + + // Flags broken out + + public bool HasTexture + { + get + { + return (Flags & 0x10) != 0; + } + set + { + Flags = (uint)((Flags & ~0x10) | ((value ? 1u : 0) << 4)); + } + } + + public int GifTagIndex + { + get + { + return (int)((Flags >> 9) & 7); + } + set + { + Flags = (uint)((Flags & ~0xe00) | (((uint)value & 7) << 9)); + } + } + + public bool DisableLighting + { + get + { + return (Flags & 0x1000) != 0; + } + set + { + Flags = (uint)((Flags & ~0x1000) | ((value ? 1u : 0) << 12)); + } + } + + public int VertexCount + { + get + { + return BitConverter.ToInt32(GifTagStrip, 0) & 0x7ff; + } + set + { + int num = BitConverter.ToInt32(GifTagStrip, 0); + num = (num & ~0x7ff) | (value & 0x7ff); + byte[] bytes = BitConverter.GetBytes(num); + Buffer.BlockCopy(bytes, 0, GifTagStrip, 0, bytes.Length); + + num = BitConverter.ToInt32(GifTagFan, 0); + num = (num & ~0x7ff) | (value & 0x7ff); + bytes = BitConverter.GetBytes(num); + Buffer.BlockCopy(bytes, 0, GifTagFan, 0, bytes.Length); + } + } + } +} diff --git a/LibDgf/Mesh/Pdb.cs b/LibDgf/Mesh/Pdb.cs new file mode 100644 index 0000000..cf11feb --- /dev/null +++ b/LibDgf/Mesh/Pdb.cs @@ -0,0 +1,92 @@ +using LibDgf.Ps2.Vif; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Mesh +{ + public class Pdb + { + VuVector[] boundingBox; + + public int BoundingBoxType { get; set; } + public VuFloat BoundingBallSize { get; set; } + public VuVector[] BoundingBox + { + get + { + return boundingBox; + } + set + { + if (value != null && value.Length != 8) throw new ArgumentException("Wrong number of vertices for bounding box.", nameof(value)); + boundingBox = value; + } + } + public Tdb Specular { get; set; } + public Tdb Diffuse { get; set; } + public Tdb Metallic { get; set; } + + public void Read(BinaryReader br) + { + var startOffset = br.BaseStream.Position; + BoundingBoxType = br.ReadInt32(); + BoundingBallSize = br.ReadPs2Float(); + uint boundingBoxOffset = br.ReadUInt32(); + uint boundingBoxLength = br.ReadUInt32(); + uint[] tdbOffsets = new uint[3]; + uint[] tdbLengths = new uint[tdbOffsets.Length]; + for (int i = 0; i < tdbOffsets.Length; ++i) + { + tdbOffsets[i] = br.ReadUInt32(); + tdbLengths[i] = br.ReadUInt32(); + } + + // Junk bytes 0xc8 to 0xcf here + + if (boundingBoxLength != 0) + { + br.BaseStream.Seek(startOffset + boundingBoxOffset, SeekOrigin.Begin); + BoundingBox = br.ReadBoundingBox(); + } + else + { + BoundingBox = null; + } + + if (tdbLengths[0] != 0) + { + br.BaseStream.Seek(startOffset + tdbOffsets[0], SeekOrigin.Begin); + Specular = new Tdb(); + Specular.Read(br); + } + else + { + Specular = null; + } + + if (tdbLengths[1] != 0) + { + br.BaseStream.Seek(startOffset + tdbOffsets[1], SeekOrigin.Begin); + Diffuse = new Tdb(); + Diffuse.Read(br); + } + else + { + Diffuse = null; + } + + if (tdbLengths[2] != 0) + { + br.BaseStream.Seek(startOffset + tdbOffsets[2], SeekOrigin.Begin); + Metallic = new Tdb(); + Metallic.Read(br); + } + else + { + Metallic = null; + } + } + } +} diff --git a/LibDgf/Mesh/Tdb.cs b/LibDgf/Mesh/Tdb.cs new file mode 100644 index 0000000..9033433 --- /dev/null +++ b/LibDgf/Mesh/Tdb.cs @@ -0,0 +1,54 @@ +using LibDgf.Ps2.Vif; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Mesh +{ + public class Tdb + { + VuVector[] boundingBox; + + public TdbFlags Flags { get; set; } + public List Textures { get; } = new List(); + public VuVector[] BoundingBox + { + get + { + return boundingBox; + } + set + { + if (value != null && value.Length != 8) throw new ArgumentException("Wrong number of vertices for bounding box.", nameof(value)); + boundingBox = value; + } + } + public Db2 Mesh { get; set; } + + public void Read(BinaryReader br) + { + var startPos = br.BaseStream.Position; + Flags = (TdbFlags)br.ReadByte(); + Textures.Clear(); + byte numTextures = br.ReadByte(); + for (int i = 0; i < numTextures; ++i) + { + var tex = new TdbTexture(); + tex.Read(br); + Textures.Add(tex); + } + var read = br.BaseStream.Position - startPos; + var aligned = (read + 15) & ~15; + br.BaseStream.Seek(aligned - read, SeekOrigin.Current); + + if ((Flags & TdbFlags.SkipBoundingBox) == 0) + { + BoundingBox = br.ReadBoundingBox(); + } + + Mesh = new Db2(); + Mesh.Read(br); + } + } +} diff --git a/LibDgf/Mesh/TdbFlags.cs b/LibDgf/Mesh/TdbFlags.cs new file mode 100644 index 0000000..70c938b --- /dev/null +++ b/LibDgf/Mesh/TdbFlags.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Mesh +{ + [Flags] + public enum TdbFlags : byte + { + None = 0, + SkipBoundingBox = 1, + } +} diff --git a/LibDgf/Mesh/TdbTexture.cs b/LibDgf/Mesh/TdbTexture.cs new file mode 100644 index 0000000..bad05d7 --- /dev/null +++ b/LibDgf/Mesh/TdbTexture.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Mesh +{ + public class TdbTexture + { + public ushort DatIndex { get; set; } + public ushort ImageBufferBase { get; set; } + public ushort ClutBufferBase { get; set; } + + public void Read(BinaryReader br) + { + DatIndex = br.ReadUInt16(); + ImageBufferBase = br.ReadUInt16(); + ClutBufferBase = br.ReadUInt16(); + } + } +} diff --git a/LibDgf/Ps2/Gs/GsMemoryUtils.cs b/LibDgf/Ps2/Gs/GsMemoryUtils.cs new file mode 100644 index 0000000..6bedd0b --- /dev/null +++ b/LibDgf/Ps2/Gs/GsMemoryUtils.cs @@ -0,0 +1,207 @@ +using LibDgf.Txm; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Ps2.Gs +{ + public static class GsMemoryUtils + { + public const int BYTES_PER_COLUMN = 64; + public const int COLUMNS_PER_BLOCK = 4; + public const int BYTES_PER_BLOCK = BYTES_PER_COLUMN * COLUMNS_PER_BLOCK; + public const int BLOCKS_PER_PAGE = 32; + public const int BYTES_PER_PAGE = BYTES_PER_BLOCK * BLOCKS_PER_PAGE; + public const int TOTAL_PAGES = 512; + public const int TOTAL_BYTES = BYTES_PER_PAGE * TOTAL_PAGES; + + public class ColumnParams + { + public int Width { get; } + public int Height { get; } + public int BitsPerPixel { get; } + public int ColsPerPage { get; } + public int RowsPerPage { get; } + + internal ColumnParams(int width, int height, int bitsPerPixel, int colsPerPage, int rowsPerPage) + { + Width = width; + Height = height; + BitsPerPixel = bitsPerPixel; + ColsPerPage = colsPerPage; + RowsPerPage = rowsPerPage; + } + } + + static readonly ColumnParams PSMCT32_COLUMN_PARAMS = new ColumnParams(8, 2, 32, 8, 4); + static readonly ColumnParams PSMT8_COLUMN_PARAMS = new ColumnParams(16, 4, 8, 8, 4); + static readonly ColumnParams PSMT4_COLUMN_PARAMS = new ColumnParams(32, 4, 4, 4, 8); + + public static ColumnParams GetColumnParams(TxmPixelFormat format) + { + switch (format) + { + case TxmPixelFormat.PSMCT32: + case TxmPixelFormat.PSMCT24: + return PSMCT32_COLUMN_PARAMS; + case TxmPixelFormat.PSMT8: + return PSMT8_COLUMN_PARAMS; + case TxmPixelFormat.PSMT4: + return PSMT4_COLUMN_PARAMS; + default: + throw new NotSupportedException($"{format} not supported."); + } + } + + public static byte[] ReadColumn(BinaryReader br, TxmPixelFormat format, int bytesPerLine) + { + ColumnParams colParams = GetColumnParams(format); + byte[] data = new byte[colParams.Width * colParams.Height * colParams.BitsPerPixel / 8]; + int bytesPerDestLine = colParams.Width * colParams.BitsPerPixel / 8; + int skipLength = bytesPerLine - bytesPerDestLine; + for (int i = 0; i < colParams.Height; ++i) + { + byte[] lineData = br.ReadBytes(bytesPerDestLine); + br.BaseStream.Seek(skipLength, SeekOrigin.Current); + Buffer.BlockCopy(lineData, 0, data, i * bytesPerDestLine, lineData.Length); + } + return data; + } + + public static void WriteColumn(BinaryWriter bw, TxmPixelFormat format, int bytesPerLine, byte[] data) + { + ColumnParams colParams = GetColumnParams(format); + int bytesPerDestLine = colParams.Width * colParams.BitsPerPixel / 8; + int skipLength = bytesPerLine - bytesPerDestLine; + for (int i = 0; i < colParams.Height; ++i) + { + bw.Write(data, i * bytesPerDestLine, bytesPerDestLine); + bw.Seek(skipLength, SeekOrigin.Current); + } + } + + // -------------------------------------------------------------------- + + // Block address to linear offset lookup for PSMCT32 + static readonly int[,] PSMCT32_BLOCK_LOOKUP = new[,] + { + { 0, 1, 4, 5, 16, 17, 20, 21 }, + { 2, 3, 6, 7, 18, 19, 22, 23 }, + { 8, 9, 12, 13, 24, 25, 28, 29 }, + { 10, 11, 14, 15, 26, 27, 30, 31 } + }; + + static readonly int[,] PSMT8_BLOCK_LOOKUP = new[,] + { + { 0, 1, 4, 5, 16, 17, 20, 21 }, + { 2, 3, 6, 7, 18, 19, 22, 23 }, + { 8, 9, 12, 13, 24, 25, 28, 29 }, + { 10, 11, 14, 15, 26, 27, 30, 31 } + }; + + static readonly int[,] PSMT4_BLOCK_LOOKUP = new[,] + { + { 0, 2, 8, 10 }, + { 1, 3, 9, 11 }, + { 4, 6, 12, 14 }, + { 5, 7, 13, 15 }, + { 16, 18, 24, 26 }, + { 17, 19, 25, 27 }, + { 20, 22, 28, 30 }, + { 21, 23, 29, 31 } + }; + + static readonly int[] PSMCT32_BLOCK_REVERSE_LOOKUP; + static readonly int[] PSMT8_BLOCK_REVERSE_LOOKUP; + static readonly int[] PSMT4_BLOCK_REVERSE_LOOKUP; + + static GsMemoryUtils() + { + PSMCT32_BLOCK_REVERSE_LOOKUP = MakeReverseLookup(PSMCT32_BLOCK_LOOKUP); + PSMT8_BLOCK_REVERSE_LOOKUP = MakeReverseLookup(PSMT8_BLOCK_LOOKUP); + PSMT4_BLOCK_REVERSE_LOOKUP = MakeReverseLookup(PSMT4_BLOCK_LOOKUP); + } + + static int[] MakeReverseLookup(int[,] lut) + { + int[] reverse = new int[lut.Length]; + int i = 0; + foreach (var n in lut) + { + reverse[n] = i++; + } + return reverse; + } + + public static int CalcBlockNumber(TxmPixelFormat format, int blockX, int blockY, int texBufWidth) + { + ColumnParams colParams = GetColumnParams(format); + switch (format) + { + case TxmPixelFormat.PSMCT32: + return CalcBlockNumber(colParams, PSMCT32_BLOCK_LOOKUP,blockX, blockY, texBufWidth); + case TxmPixelFormat.PSMT8: + return CalcBlockNumber(colParams, PSMT8_BLOCK_LOOKUP, blockX, blockY, texBufWidth); + case TxmPixelFormat.PSMT4: + return CalcBlockNumber(colParams, PSMT4_BLOCK_LOOKUP, blockX, blockY, texBufWidth); + default: + throw new NotSupportedException($"{format} not supported"); + } + } + + public static int CalcBlockMemoryOffset(TxmPixelFormat format, int index) + { + ColumnParams colParams = GetColumnParams(format); + switch (format) + { + case TxmPixelFormat.PSMCT32: + return CalcBlockMemoryOffset(colParams, PSMCT32_BLOCK_REVERSE_LOOKUP, index); + case TxmPixelFormat.PSMT8: + return CalcBlockMemoryOffset(colParams, PSMT8_BLOCK_REVERSE_LOOKUP, index); + case TxmPixelFormat.PSMT4: + return CalcBlockMemoryOffset(colParams, PSMT4_BLOCK_REVERSE_LOOKUP, index); + default: + throw new NotSupportedException($"{format} not supported"); + } + } + + static int CalcBlockNumber(ColumnParams colParams, int[,] lut, int blockX, int blockY, int texBufWidth) + { + int pageX = blockX / colParams.ColsPerPage; + int pageY = blockY / colParams.RowsPerPage; + int blockXInPage = blockX % colParams.ColsPerPage; + int blockYInPage = blockY % colParams.RowsPerPage; + + return (pageY * texBufWidth + pageX) * BLOCKS_PER_PAGE + lut[blockYInPage, blockXInPage]; + } + + static int CalcBlockMemoryOffset(ColumnParams colParams, int[] lut, int index) + { + int pageIndex = index / BLOCKS_PER_PAGE; + int rem = index % BLOCKS_PER_PAGE; + int pixelsPerBlock = colParams.Width * colParams.Height * COLUMNS_PER_BLOCK; + int memBlockNum = lut[rem]; + int fullBlockRows = memBlockNum / colParams.ColsPerPage; + int partialBlocks = memBlockNum % colParams.ColsPerPage; + + return ( + pageIndex * BLOCKS_PER_PAGE * pixelsPerBlock + // Full pages + fullBlockRows * pixelsPerBlock * colParams.ColsPerPage + // Full row of blocks + partialBlocks * colParams.Width // Partial row of blocks + ) * colParams.BitsPerPixel / 8; + } + + public static int CalcTxmImageOffset(ColumnParams colParams, int blockIndex, int imageWidth) + { + int blocksPerRow = imageWidth / colParams.Width; + if (blocksPerRow == 0) blocksPerRow = 1; + int fullRows = blockIndex / blocksPerRow; + int rowBlockIndex = blockIndex % blocksPerRow; + return ( + fullRows * imageWidth * colParams.Height * 4 + + rowBlockIndex * colParams.Width + ) * colParams.BitsPerPixel / 8; + } + } +} diff --git a/LibDgf/Ps2/Gs/PsmtMixer.cs b/LibDgf/Ps2/Gs/PsmtMixer.cs new file mode 100644 index 0000000..f21b0ce --- /dev/null +++ b/LibDgf/Ps2/Gs/PsmtMixer.cs @@ -0,0 +1,141 @@ +using LibDgf.Txm; +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Ps2.Gs +{ + public static class PsmtMixer + { + static int[,] PSMCT32_TO_PSMT4_COL02_LOOKUP = new int[16, 8]; + static int[,] PSMCT32_TO_PSMT4_COL13_LOOKUP = new int[16, 8]; + static int[,] PSMCT32_TO_PSMT8_COL02_LOOKUP = new int[16, 4]; + static int[,] PSMCT32_TO_PSMT8_COL13_LOOKUP = new int[16, 4]; + + static PsmtMixer() + { + FillLookup(PSMCT32_TO_PSMT4_COL02_LOOKUP, false); + FillOddLookup(PSMCT32_TO_PSMT4_COL02_LOOKUP, PSMCT32_TO_PSMT4_COL13_LOOKUP); + FillLookup(PSMCT32_TO_PSMT8_COL02_LOOKUP, false); + FillOddLookup(PSMCT32_TO_PSMT8_COL02_LOOKUP, PSMCT32_TO_PSMT8_COL13_LOOKUP); + + //PrintLookup(PSMCT32_TO_PSMT4_COL02_LOOKUP, nameof(PSMCT32_TO_PSMT4_COL02_LOOKUP)); + //PrintLookup(PSMCT32_TO_PSMT4_COL13_LOOKUP, nameof(PSMCT32_TO_PSMT4_COL13_LOOKUP)); + //PrintLookup(PSMCT32_TO_PSMT8_COL02_LOOKUP, nameof(PSMCT32_TO_PSMT8_COL02_LOOKUP)); + //PrintLookup(PSMCT32_TO_PSMT8_COL13_LOOKUP, nameof(PSMCT32_TO_PSMT8_COL13_LOOKUP)); + } + + static void PrintLookup(int[,] lookup, string name) + { + Console.WriteLine(name); + int rowNum = lookup.GetLength(0); + int colNum = lookup.GetLength(1); + for (int row = 0; row < rowNum; ++row) + { + for (int col = colNum - 1; col >= 0; --col) + { + Console.Write("{0,3} ", lookup[row, col]); + } + Console.WriteLine(); + } + Console.WriteLine(); + } + + static void FillLookup(int[,] lookup, bool skipNonConsec) + { + int numCols = lookup.GetLength(1); + int num = 0; + + // Phase 1: consecutive numbers + // Fill every second column + // Top half then bottom half + for (int half = 0; half < 2; ++half) + { + for (int col = 0; col < numCols; col += skipNonConsec ? 1 : 2) + { + for (int row = 0; row < 8; ++row) + { + lookup[half * 8 + row, col] = num++; + } + } + } + + // Phase 2: wrapped numbers + if (!skipNonConsec) + { + for (int half = 0; half < 2; ++half) + { + for (int col = 1; col < numCols; col += 2) + { + for (int row = 4; row < 12; ++row) + { + lookup[half * 8 + (row % 8), col] = num++; + } + } + } + } + } + + static void FillOddLookup(int[,] evenLookup, int[,] oddLookup) + { + int numCols = evenLookup.GetLength(1); + for (int half = 0; half < 2; ++half) + { + for (int i = 0; i < 8; ++i) + { + for (int j = 0; j < numCols; ++j) + { + oddLookup[half * 8 + i, j] = evenLookup[half * 8 + ((i + 4) % 8), j]; + } + } + } + } + + public static byte[] MixColumn(byte[] column, TxmPixelFormat srcFormat, TxmPixelFormat destFormat, bool isOdd) + { + if (srcFormat != TxmPixelFormat.PSMCT32) + throw new NotSupportedException("Only PSMCT32 supported as source format."); + switch (destFormat) + { + case TxmPixelFormat.PSMT4: + return MixColumn32To4(column, isOdd); + case TxmPixelFormat.PSMT8: + return MixColumn32To8(column, isOdd); + default: + throw new NotSupportedException($"{destFormat} not supported as destination format."); + } + } + + static byte[] MixColumn32To4(byte[] column, bool isOdd) + { + int[,] lookup = isOdd ? PSMCT32_TO_PSMT4_COL13_LOOKUP : PSMCT32_TO_PSMT4_COL02_LOOKUP; + int numCol = lookup.GetLength(1); + byte[] dest = new byte[column.Length]; + byte b = 0; + for (int i = 0; i < column.Length * 2; ++i) + { + if (i % 2 == 0) + b = column[i / 2]; + else + b >>= 4; + + int index = lookup[i / numCol, i % numCol]; + dest[index / 2] |= (byte)((b & 0x0f) << (index % 2 * 4)); + } + return dest; + } + + static byte[] MixColumn32To8(byte[] column, bool isOdd) + { + int[,] lookup = isOdd ? PSMCT32_TO_PSMT8_COL13_LOOKUP : PSMCT32_TO_PSMT8_COL02_LOOKUP; + int numCol = lookup.GetLength(1); + byte[] dest = new byte[column.Length]; + for (int i = 0; i < column.Length; ++i) + { + int index = lookup[i / numCol, i % numCol]; + dest[index] = column[i]; + } + return dest; + } + } +} diff --git a/LibDgf/Ps2/Gs/Registers/Tex0.cs b/LibDgf/Ps2/Gs/Registers/Tex0.cs new file mode 100644 index 0000000..db3595a --- /dev/null +++ b/LibDgf/Ps2/Gs/Registers/Tex0.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Ps2.Gs.Registers +{ + public struct Tex0 + { + public ulong Packed; + + public ushort Tbp0 + { + get + { + return (ushort)(Packed & 0x3fff); + } + } + + public byte Tbw + { + get + { + return (byte)((Packed >> 14) & 0x3f); + } + } + + public byte Psm + { + get + { + return (byte)((Packed >> 20) & 0x3f); + } + } + + public byte Tw + { + get + { + return (byte)((Packed >> 26) & 0xf); + } + } + + public byte Th + { + get + { + return (byte)((Packed >> 30) & 0xf); + } + } + + public byte Tcc + { + get + { + return (byte)((Packed >> 34) & 0x1); + } + } + + public byte Tfx + { + get + { + return (byte)((Packed >> 35) & 0x3); + } + } + + public ushort Cbp + { + get + { + return (byte)((Packed >> 37) & 0x3ff); + } + } + + public byte Cpsm + { + get + { + return (byte)((Packed >> 51) & 0xf); + } + } + + public byte Csm + { + get + { + return (byte)((Packed >> 55) & 0x1); + } + } + + public byte Csa + { + get + { + return (byte)((Packed >> 56) & 0x1f); + } + } + + public byte Cld + { + get + { + return (byte)((Packed >> 61) & 0x7); + } + } + } +} diff --git a/LibDgf/Ps2/Vif/BinaryReaderVifExtensions.cs b/LibDgf/Ps2/Vif/BinaryReaderVifExtensions.cs new file mode 100644 index 0000000..b0d6141 --- /dev/null +++ b/LibDgf/Ps2/Vif/BinaryReaderVifExtensions.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Ps2.Vif +{ + public static class BinaryReaderVifExtensions + { + public static VuFloat ReadPs2Float(this BinaryReader br) + { + return new VuFloat { Packed = br.ReadUInt32() }; + } + + public static VuVector[] ReadBoundingBox(this BinaryReader br) + { + var box = new VuVector[8]; + for (int i = 0; i < box.Length; ++i) + { + box[i] = br.ReadV4_32(); + } + return box; + } + + public static VuVector ReadOneVifCodeUnpack(this BinaryReader br, VifCodeUnpack u) + { + switch (u.Vn) + { + case VifUnpackVnType.S: + switch (u.Vl) + { + case VifUnpackVlType.L_32: + return br.ReadS_32(); + case VifUnpackVlType.L_16: + return u.Unsigned ? br.ReadS_16U() : br.ReadS_16S(); + case VifUnpackVlType.L_8: + return u.Unsigned ? br.ReadS_8U() : br.ReadS_8S(); + default: + break; + } + break; + case VifUnpackVnType.V2: + switch (u.Vl) + { + case VifUnpackVlType.L_32: + return br.ReadV2_32(); + case VifUnpackVlType.L_16: + return u.Unsigned ? br.ReadV2_16U() : br.ReadV2_16S(); + case VifUnpackVlType.L_8: + return u.Unsigned ? br.ReadV2_8U() : br.ReadV2_8S(); + default: + break; + } + break; + case VifUnpackVnType.V3: + switch (u.Vl) + { + case VifUnpackVlType.L_32: + return br.ReadV3_32(); + case VifUnpackVlType.L_16: + return u.Unsigned ? br.ReadV3_16U() : br.ReadV3_16S(); + case VifUnpackVlType.L_8: + return u.Unsigned ? br.ReadV3_8U() : br.ReadV3_8S(); + default: + break; + } + break; + case VifUnpackVnType.V4: + switch (u.Vl) + { + case VifUnpackVlType.L_32: + return br.ReadV4_32(); + case VifUnpackVlType.L_16: + return u.Unsigned ? br.ReadV4_16U() : br.ReadV4_16S(); + case VifUnpackVlType.L_8: + return u.Unsigned ? br.ReadV4_8U() : br.ReadV4_8S(); + case VifUnpackVlType.L_5: + return br.ReadV4_5(); + default: + break; + } + break; + default: + break; + } + + throw new ArgumentException("Invalid vn/vl combination", nameof(u)); + } + + public static VuVector ReadV4_32(this BinaryReader br) + { + return new VuVector + { + X = new VuFloat { Packed = br.ReadUInt32() }, + Y = new VuFloat { Packed = br.ReadUInt32() }, + Z = new VuFloat { Packed = br.ReadUInt32() }, + W = new VuFloat { Packed = br.ReadUInt32() } + }; + } + + public static VuVector ReadV4_16U(this BinaryReader br) + { + return new VuVector + { + X = new VuFloat { Packed = br.ReadUInt16() }, + Y = new VuFloat { Packed = br.ReadUInt16() }, + Z = new VuFloat { Packed = br.ReadUInt16() }, + W = new VuFloat { Packed = br.ReadUInt16() }, + }; + } + + public static VuVector ReadV4_16S(this BinaryReader br) + { + return new VuVector + { + X = new VuFloat { Packed = unchecked((uint)br.ReadInt16()) }, + Y = new VuFloat { Packed = unchecked((uint)br.ReadInt16()) }, + Z = new VuFloat { Packed = unchecked((uint)br.ReadInt16()) }, + W = new VuFloat { Packed = unchecked((uint)br.ReadInt16()) }, + }; + } + + public static VuVector ReadV4_8U(this BinaryReader br) + { + return new VuVector + { + X = new VuFloat { Packed = br.ReadByte() }, + Y = new VuFloat { Packed = br.ReadByte() }, + Z = new VuFloat { Packed = br.ReadByte() }, + W = new VuFloat { Packed = br.ReadByte() }, + }; + } + + public static VuVector ReadV4_8S(this BinaryReader br) + { + return new VuVector + { + X = new VuFloat { Packed = unchecked((uint)br.ReadSByte()) }, + Y = new VuFloat { Packed = unchecked((uint)br.ReadSByte()) }, + Z = new VuFloat { Packed = unchecked((uint)br.ReadSByte()) }, + W = new VuFloat { Packed = unchecked((uint)br.ReadSByte()) }, + }; + } + + public static VuVector ReadV3_32(this BinaryReader br) + { + return new VuVector + { + X = new VuFloat { Packed = br.ReadUInt32() }, + Y = new VuFloat { Packed = br.ReadUInt32() }, + Z = new VuFloat { Packed = br.ReadUInt32() }, + }; + } + + public static VuVector ReadV3_16U(this BinaryReader br) + { + return new VuVector + { + X = new VuFloat { Packed = br.ReadUInt16() }, + Y = new VuFloat { Packed = br.ReadUInt16() }, + Z = new VuFloat { Packed = br.ReadUInt16() }, + }; + } + + public static VuVector ReadV3_16S(this BinaryReader br) + { + return new VuVector + { + X = new VuFloat { Packed = unchecked((uint)br.ReadInt16()) }, + Y = new VuFloat { Packed = unchecked((uint)br.ReadInt16()) }, + Z = new VuFloat { Packed = unchecked((uint)br.ReadInt16()) }, + }; + } + + public static VuVector ReadV3_8U(this BinaryReader br) + { + return new VuVector + { + X = new VuFloat { Packed = br.ReadByte() }, + Y = new VuFloat { Packed = br.ReadByte() }, + Z = new VuFloat { Packed = br.ReadByte() }, + }; + } + + public static VuVector ReadV3_8S(this BinaryReader br) + { + return new VuVector + { + X = new VuFloat { Packed = unchecked((uint)br.ReadSByte()) }, + Y = new VuFloat { Packed = unchecked((uint)br.ReadSByte()) }, + Z = new VuFloat { Packed = unchecked((uint)br.ReadSByte()) }, + }; + } + + public static VuVector ReadV2_32(this BinaryReader br) + { + var x = new VuFloat { Packed = br.ReadUInt32() }; + var y = new VuFloat { Packed = br.ReadUInt32() }; + return new VuVector + { + X = x, + Y = y, + Z = x, + W = y + }; + } + + public static VuVector ReadV2_16U(this BinaryReader br) + { + var x = new VuFloat { Packed = br.ReadUInt16() }; + var y = new VuFloat { Packed = br.ReadUInt16() }; + return new VuVector + { + X = x, + Y = y, + Z = x, + W = y + }; + } + + public static VuVector ReadV2_16S(this BinaryReader br) + { + var x = new VuFloat { Packed = unchecked((uint)br.ReadInt16()) }; + var y = new VuFloat { Packed = unchecked((uint)br.ReadInt16()) }; + return new VuVector + { + X = x, + Y = y, + Z = x, + W = y + }; + } + + public static VuVector ReadV2_8U(this BinaryReader br) + { + var x = new VuFloat { Packed = (uint)br.ReadByte() }; + var y = new VuFloat { Packed = (uint)br.ReadByte() }; + return new VuVector + { + X = x, + Y = y, + Z = x, + W = y + }; + } + + public static VuVector ReadV2_8S(this BinaryReader br) + { + var x = new VuFloat { Packed = unchecked((uint)br.ReadSByte()) }; + var y = new VuFloat { Packed = unchecked((uint)br.ReadSByte()) }; + return new VuVector + { + X = x, + Y = y, + Z = x, + W = y + }; + } + + public static VuVector ReadS_32(this BinaryReader br) + { + var s = new VuFloat { Packed = br.ReadUInt32() }; + return new VuVector + { + X = s, + Y = s, + Z = s, + W = s + }; + } + + public static VuVector ReadS_16U(this BinaryReader br) + { + var s = new VuFloat { Packed = (uint)br.ReadUInt16() }; + return new VuVector + { + X = s, + Y = s, + Z = s, + W = s + }; + } + + public static VuVector ReadS_16S(this BinaryReader br) + { + var s = new VuFloat { Packed = unchecked((uint)br.ReadInt16()) }; + return new VuVector + { + X = s, + Y = s, + Z = s, + W = s + }; + } + + public static VuVector ReadS_8U(this BinaryReader br) + { + var s = new VuFloat { Packed = (uint)br.ReadByte() }; + return new VuVector + { + X = s, + Y = s, + Z = s, + W = s + }; + } + + public static VuVector ReadS_8S(this BinaryReader br) + { + var s = new VuFloat { Packed = unchecked((uint)br.ReadSByte()) }; + return new VuVector + { + X = s, + Y = s, + Z = s, + W = s + }; + } + + public static VuVector ReadV4_5(this BinaryReader br) + { + uint rgba = br.ReadUInt16(); + return new VuVector + { + X = new VuFloat { Packed = (rgba << 3) & 0xf8 }, + Y = new VuFloat { Packed = (rgba >> 2) & 0xf8 }, + Z = new VuFloat { Packed = (rgba >> 7) & 0xf8 }, + W = new VuFloat { Packed = (rgba >> 8) & 0x80 }, + }; + } + } +} diff --git a/LibDgf/Ps2/Vif/VifCode.cs b/LibDgf/Ps2/Vif/VifCode.cs new file mode 100644 index 0000000..5649895 --- /dev/null +++ b/LibDgf/Ps2/Vif/VifCode.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Ps2.Vif +{ + public struct VifCode + { + public uint Value; + + public bool Interrupt + { + get + { + return (Cmd & VifCodeCmd.Interrupt) != 0; + } + } + + public VifCodeCmd Cmd + { + get + { + return (VifCodeCmd)(byte)(Value >> 24); + } + } + + public VifCodeCmd CmdWithoutInterrupt + { + get + { + return Cmd & ~VifCodeCmd.Interrupt; + } + } + + public byte Num + { + get + { + return (byte)(Value >> 16); + } + } + + public ushort Immediate + { + get + { + return (ushort)Value; + } + } + + public bool IsUnpack => (Cmd & VifCodeCmd.Unpack) == VifCodeCmd.Unpack; + } +} diff --git a/LibDgf/Ps2/Vif/VifCodeCmd.cs b/LibDgf/Ps2/Vif/VifCodeCmd.cs new file mode 100644 index 0000000..bf3d11f --- /dev/null +++ b/LibDgf/Ps2/Vif/VifCodeCmd.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Ps2.Vif +{ + public enum VifCodeCmd : byte + { + Nop = 0b000_0000, + StCycl = 0b000_0001, + Offset = 0b000_0010, + Base = 0b000_0011, + Itop = 0b000_0100, + StMod = 0b000_0101, + MskPath3 = 0b000_0110, + Mark = 0b000_0111, + FlushE = 0b001_0000, + Flush = 0b001_0001, + FlushA = 0b001_0011, + MsCal = 0b001_0100, + MsCnt = 0b001_0111, + MsCalF = 0b001_0101, + StMask = 0b010_0000, + StRow = 0b011_0000, + StCol = 0b011_0001, + Mpg = 0b100_1010, + Direct = 0b101_0000, + DirectHl = 0b101_0001, + Unpack = 0b11_00000, + Interrupt = 0b1000_0000 + } +} diff --git a/LibDgf/Ps2/Vif/VifCodeUnpack.cs b/LibDgf/Ps2/Vif/VifCodeUnpack.cs new file mode 100644 index 0000000..6eeda08 --- /dev/null +++ b/LibDgf/Ps2/Vif/VifCodeUnpack.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Ps2.Vif +{ + public struct VifCodeUnpack + { + private uint Value; + + public bool Interrupt + { + get + { + return (Value & 0x80000000) != 0; + } + } + + public bool Mask + { + get + { + return (Value & 0x10000000) != 0; + } + } + + public VifUnpackVnType Vn + { + get + { + return (VifUnpackVnType)((Value >> 26) & 0x3); + } + } + + public VifUnpackVlType Vl + { + get + { + return (VifUnpackVlType)((Value >> 24) & 0x3); + } + } + + public byte Num + { + get + { + return (byte)(Value >> 16); + } + } + + public bool Flag + { + get + { + return (Value & 0x8000) != 0; + } + } + + public bool Unsigned + { + get + { + return (Value & 0x4000) != 0; + } + } + + public ushort Address + { + get + { + return (ushort)(Value & 0x3ff); + } + } + + public static implicit operator VifCode(VifCodeUnpack v) + { + return new VifCode { Value = v.Value | ((uint)VifCodeCmd.Unpack << 24) }; + } + + public static explicit operator VifCodeUnpack(VifCode v) + { + if ((v.Cmd & VifCodeCmd.Unpack) != VifCodeCmd.Unpack) + throw new ArgumentException("Not an UNPACK code", nameof(v)); + return new VifCodeUnpack { Value = v.Value }; + } + } +} diff --git a/LibDgf/Ps2/Vif/VifEmulator.cs b/LibDgf/Ps2/Vif/VifEmulator.cs new file mode 100644 index 0000000..382ade1 --- /dev/null +++ b/LibDgf/Ps2/Vif/VifEmulator.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace LibDgf.Ps2.Vif +{ + public class VifEmulator + { + public delegate void VuMpgActivate(ushort? address, bool waitGif); + + VuVector[] memory; + VifRegisters registers; + bool maskPath3; + + public VifEmulator() + { + memory = new VuVector[1024]; + for (int i = 0; i < memory.Length; ++i) + { + memory[i] = new VuVector(); + } + registers = new VifRegisters(); + } + + public VuVector[] VuMemory => memory; + public VifRegisters Registers => registers; + + public void Process(BinaryReader br, int dataLength, VuMpgActivate onVuMpgActivate) + { + var startPos = br.BaseStream.Position; + var endPos = startPos + dataLength; + while (br.BaseStream.Position < endPos) + { + var vifcode = new VifCode { Value = br.ReadUInt32() }; + registers.Code = vifcode; + //if ((vifcode.CmdWithoutInterrupt & VifCodeCmd.Unpack) != VifCodeCmd.Unpack) + //{ + // Console.WriteLine($"VIFcode: {vifcode.CmdWithoutInterrupt} NUM={vifcode.Num} IMMEDIATE={vifcode.Immediate:x4}"); + //} + switch (vifcode.CmdWithoutInterrupt) + { + case VifCodeCmd.Nop: + break; + case VifCodeCmd.StCycl: + registers.Cycle = vifcode.Immediate; + break; + case VifCodeCmd.Offset: + registers.Ofst = (uint)(vifcode.Immediate & 0x3ff); + registers.Stat_Dbf = false; + registers.TopS = registers.Base; + break; + case VifCodeCmd.Base: + registers.Base = (uint)(vifcode.Immediate & 0x3ff); + break; + case VifCodeCmd.Itop: + registers.ITopS = (uint)(vifcode.Immediate & 0x3ff); + break; + case VifCodeCmd.StMod: + registers.Mode = (uint)(vifcode.Immediate & 3); + break; + case VifCodeCmd.MskPath3: + maskPath3 = (vifcode.Immediate & 0x8000) != 0; + break; + case VifCodeCmd.Mark: + registers.Mark = vifcode.Immediate; + break; + case VifCodeCmd.FlushE: + case VifCodeCmd.Flush: + case VifCodeCmd.FlushA: + // Microprogram is run synchronously in emulation + break; + case VifCodeCmd.MsCal: + registers.DoubleBufferSwap(); + onVuMpgActivate?.Invoke(vifcode.Immediate, false); + break; + case VifCodeCmd.MsCnt: + registers.DoubleBufferSwap(); + onVuMpgActivate?.Invoke(null, false); + break; + case VifCodeCmd.MsCalF: + registers.DoubleBufferSwap(); + onVuMpgActivate?.Invoke(vifcode.Immediate, true); + break; + case VifCodeCmd.StMask: + registers.Mask = br.ReadUInt32(); + break; + case VifCodeCmd.StRow: + registers.R[0] = br.ReadUInt32(); + registers.R[1] = br.ReadUInt32(); + registers.R[2] = br.ReadUInt32(); + registers.R[3] = br.ReadUInt32(); + break; + case VifCodeCmd.StCol: + registers.C[0] = br.ReadUInt32(); + registers.C[1] = br.ReadUInt32(); + registers.C[2] = br.ReadUInt32(); + registers.C[3] = br.ReadUInt32(); + break; + case VifCodeCmd.Mpg: + { + if (!CheckAlignment(br, startPos, 8)) + throw new InvalidDataException("MPG data is not aligned."); + //Console.WriteLine($"MPG load at 0x{vifcode.Immediate * 8:x4}"); + // Skip MPG since we don't have a VU to execute it on + int skipLength = vifcode.Num; + if (skipLength == 0) skipLength = 256; + skipLength *= 8; + br.BaseStream.Seek(skipLength, SeekOrigin.Current); + break; + } + case VifCodeCmd.Direct: + case VifCodeCmd.DirectHl: + { + if (!CheckAlignment(br, startPos, 16)) + throw new InvalidDataException("Direct data is not aligned."); + //Console.WriteLine($"Direct transfer"); + // TODO: handle GIFtag + int skipLength = vifcode.Immediate; + if (skipLength == 0) skipLength = 65536; + skipLength *= 16; + br.BaseStream.Seek(skipLength, SeekOrigin.Current); + break; + } + default: + if ((vifcode.Cmd & VifCodeCmd.Unpack) == VifCodeCmd.Unpack) + { + ProcessVifCodeUnpack(br); + AlignReader(br, startPos, 4); + break; + } + else + { + throw new InvalidDataException("Invalid VIFcode command"); + } + } + } + } + + void ProcessVifCodeUnpack(BinaryReader br) + { + var vifcode = (VifCodeUnpack)registers.Code; + registers.Num = vifcode.Num; + //Console.WriteLine($"VIFcode: {VifCodeCmd.Unpack} vn={vifcode.Vn} vl={vifcode.Vl} NUM={vifcode.Num} ADDR={vifcode.Address:x4} FLG={vifcode.Flag} USN={vifcode.Unsigned} m={vifcode.Mask}"); + int addr = (int)((vifcode.Flag ? registers.TopS : 0) + vifcode.Address); + int cycle = 0; + bool isV4_5 = vifcode.Vn == VifUnpackVnType.V4 && vifcode.Vl == VifUnpackVlType.L_5; + while (registers.Num > 0) + { + VuVector result = default; + bool doSkip = false; + bool doMode; + bool doMask; + if (registers.CycleCl >= registers.CycleWl) + { + doMode = true; + doMask = false; + // Skipping write + if (cycle < registers.CycleWl) + { + // Write when under write limit + result = br.ReadOneVifCodeUnpack(vifcode); + } + + if (cycle == registers.CycleWl - 1) + { + doSkip = true; + } + } + else + { + // Filling write + throw new NotImplementedException("Filling write not implemented"); + } + + // Write result + result = ApplyMaskAndMode(result, doMode && !isV4_5, doMask); + memory[addr++] = result; + --registers.Num; + + // TODO: figure out the proper behavior for filling write + if (doSkip) + { + addr += registers.CycleCl - registers.CycleWl; + cycle = 0; + } + else + { + ++cycle; + } + } + } + + VuVector ApplyMaskAndMode(VuVector vector, bool doMode, bool doMask) + { + if (!doMode && !doMask) return vector; + + uint x = vector.X.Packed; + uint y = vector.Y.Packed; + uint z = vector.Z.Packed; + uint w = vector.W.Packed; + + if (doMask) + { + throw new NotImplementedException("Masking not implemented"); + } + + if (doMode) + { + switch (registers.ModeMod) + { + case VifMode.None: + break; + default: + throw new NotImplementedException("Addition decompression write not implemented"); + } + } + + return new VuVector + { + X = new VuFloat { Packed = x }, + Y = new VuFloat { Packed = y }, + Z = new VuFloat { Packed = z }, + W = new VuFloat { Packed = w }, + }; + } + + // Alignment in bytes + static void AlignReader(BinaryReader br, long startPos, int alignment) + { + var read = br.BaseStream.Position - startPos; + var aligned = (read + alignment - 1) / alignment * alignment; + br.BaseStream.Seek(aligned - read, SeekOrigin.Current); + } + + static bool CheckAlignment(BinaryReader br, long startPos, int alignment) + { + var read = br.BaseStream.Position - startPos; + var aligned = (read + alignment - 1) / alignment * alignment; + return read == aligned; + } + } +} diff --git a/LibDgf/Ps2/Vif/VifMode.cs b/LibDgf/Ps2/Vif/VifMode.cs new file mode 100644 index 0000000..9d79cff --- /dev/null +++ b/LibDgf/Ps2/Vif/VifMode.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Ps2.Vif +{ + public enum VifMode : byte + { + None, + Offset, + Difference + } +} diff --git a/LibDgf/Ps2/Vif/VifRegisters.cs b/LibDgf/Ps2/Vif/VifRegisters.cs new file mode 100644 index 0000000..b945a40 --- /dev/null +++ b/LibDgf/Ps2/Vif/VifRegisters.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Ps2.Vif +{ + public class VifRegisters + { + public uint[] R { get; } = new uint[4]; + public uint[] C { get; } = new uint[4]; + public uint Cycle { get; set; } + public uint Mask { get; set; } + public uint Mode { get; set; } + public uint ITop { get; set; } + public uint ITopS { get; set; } + public uint Base { get; set; } + public uint Ofst { get; set; } + public uint Top { get; set; } + public uint TopS { get; set; } + public uint Mark { get; set; } + public uint Num { get; set; } + public VifCode Code { get; set; } + + // Just this flag because the other ones are not that interesting + public bool Stat_Dbf { get; set; } + + public byte CycleCl + { + get + { + return (byte)Cycle; + } + set + { + Cycle = (Cycle & 0xffffff00) | value; + } + } + + public byte CycleWl + { + get + { + return (byte)(Cycle >> 8); + } + set + { + Cycle = (Cycle & 0xffff00ff) | ((uint)value << 8); + } + } + + public VifMode ModeMod + { + get + { + return (VifMode)(Mode & 3); + } + set + { + Mode = (uint)value & 3; + } + } + + public void DoubleBufferSwap() + { + ITop = ITopS; + Top = TopS; + TopS = Base + (Stat_Dbf ? Ofst : 0); + Stat_Dbf = !Stat_Dbf; + } + } +} diff --git a/LibDgf/Ps2/Vif/VifUnpackVlType.cs b/LibDgf/Ps2/Vif/VifUnpackVlType.cs new file mode 100644 index 0000000..092fe0e --- /dev/null +++ b/LibDgf/Ps2/Vif/VifUnpackVlType.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Ps2.Vif +{ + public enum VifUnpackVlType + { + L_32, + L_16, + L_8, + L_5 + } +} diff --git a/LibDgf/Ps2/Vif/VifUnpackVnType.cs b/LibDgf/Ps2/Vif/VifUnpackVnType.cs new file mode 100644 index 0000000..6c5af5c --- /dev/null +++ b/LibDgf/Ps2/Vif/VifUnpackVnType.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Ps2.Vif +{ + public enum VifUnpackVnType + { + S, + V2, + V3, + V4 + } +} diff --git a/LibDgf/Ps2/Vif/VuFloat.cs b/LibDgf/Ps2/Vif/VuFloat.cs new file mode 100644 index 0000000..2447e30 --- /dev/null +++ b/LibDgf/Ps2/Vif/VuFloat.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Ps2.Vif +{ + public struct VuFloat + { + public uint Packed; + + public static implicit operator double(VuFloat f) + { + ulong sign = (f.Packed >> 31) & 1; + ulong exponent = (f.Packed >> 23) & 0xff; + ulong mantissa = f.Packed & 0x7fffff; + ulong doubleValue; + if (exponent == 0) + { + doubleValue = sign << 63; + } + else + { + doubleValue = (sign << 63) | ((exponent + 1023 - 127) << 52) | (mantissa << 29); + } + return BitConverter.ToDouble(BitConverter.GetBytes(doubleValue), 0); + } + + public override string ToString() + { + return ((double)this).ToString(); + } + } +} diff --git a/LibDgf/Ps2/Vif/VuVector.cs b/LibDgf/Ps2/Vif/VuVector.cs new file mode 100644 index 0000000..889a672 --- /dev/null +++ b/LibDgf/Ps2/Vif/VuVector.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibDgf.Ps2.Vif +{ + public struct VuVector + { + public VuFloat X; + public VuFloat Y; + public VuFloat Z; + public VuFloat W; + + public override string ToString() + { + return $"<{X}, {Y}, {Z}, {W}>"; + } + } +} diff --git a/LibDgf/Txm/TxmHeader.cs b/LibDgf/Txm/TxmHeader.cs index 04c574f..05c6758 100644 --- a/LibDgf/Txm/TxmHeader.cs +++ b/LibDgf/Txm/TxmHeader.cs @@ -11,12 +11,12 @@ namespace LibDgf.Txm public TxmPixelFormat ImageVideoPixelFormat { get; set; } public short ImageWidth { get; set; } public short ImageHeight { get; set; } - public short ImageBufferBase { get; set; } + public ushort ImageBufferBase { get; set; } public TxmPixelFormat ClutPixelFormat { get; set; } public byte Misc { get; set; } // 0x0f = level, 0x70 = count, 0x80 = fast count public short ClutWidth { get; set; } public short ClutHeight { get; set; } - public short ClutBufferBase { get; set; } + public ushort ClutBufferBase { get; set; } public void Read(BinaryReader br) { @@ -24,12 +24,12 @@ namespace LibDgf.Txm ImageVideoPixelFormat = (TxmPixelFormat)br.ReadByte(); ImageWidth = br.ReadInt16(); ImageHeight = br.ReadInt16(); - ImageBufferBase = br.ReadInt16(); + ImageBufferBase = br.ReadUInt16(); ClutPixelFormat = (TxmPixelFormat)br.ReadByte(); Misc = br.ReadByte(); ClutWidth = br.ReadInt16(); ClutHeight = br.ReadInt16(); - ClutBufferBase = br.ReadInt16(); + ClutBufferBase = br.ReadUInt16(); } public void Write(BinaryWriter bw) diff --git a/LibDgf/Utils.cs b/LibDgf/Utils.cs index e7b4ca1..6d15dfb 100644 --- a/LibDgf/Utils.cs +++ b/LibDgf/Utils.cs @@ -25,5 +25,10 @@ namespace LibDgf } return stream; } + + public static double Convert12BitFixedToDouble(int value) + { + return value * 0.000244140625; // value / 2^12 + } } } diff --git a/README.md b/README.md index c9a2aad..cfe0fee 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ Currently implemented - font/*.pak: font pack - Sound effects catalog (read support only) - TXM: texture files +- PDB/TDB/DB2: Mesh formats (typically contained inside DAT, read support only) + +**PlayStation 2** +- VIF emulator (partial) **Aqualead (Plug & Play)** (read support only currently)