Browse Source

Refactoring validation to align it closer with gltf-validator (WIP)

Vicente Penades 5 years ago
parent
commit
b0840e82dd
44 changed files with 1366 additions and 1094 deletions
  1. 1 0
      SharpGLTF.sln
  2. 2 2
      src/Analyzers.props
  3. 24 0
      src/Shared/Guard.cs
  4. 73 9
      src/Shared/_Extensions.cs
  5. 17 13
      src/SharpGLTF.Core/IO/JsonSerializable.cs
  6. 36 30
      src/SharpGLTF.Core/IO/ReadContext.cs
  7. 7 1
      src/SharpGLTF.Core/IO/Serialization.cs
  8. 3 3
      src/SharpGLTF.Core/IO/WriteContext.cs
  9. 121 0
      src/SharpGLTF.Core/Memory/AttributeFormat.cs
  10. 80 8
      src/SharpGLTF.Core/Memory/MemoryAccessor.Validation.cs
  11. 13 2
      src/SharpGLTF.Core/Memory/MemoryAccessor.cs
  12. 31 33
      src/SharpGLTF.Core/Memory/MemoryImage.cs
  13. 9 20
      src/SharpGLTF.Core/Schema2/_Extensions.cs
  14. 28 33
      src/SharpGLTF.Core/Schema2/gltf.AccessorSparse.cs
  15. 106 165
      src/SharpGLTF.Core/Schema2/gltf.Accessors.cs
  16. 23 29
      src/SharpGLTF.Core/Schema2/gltf.Animations.cs
  17. 5 7
      src/SharpGLTF.Core/Schema2/gltf.Asset.cs
  18. 17 18
      src/SharpGLTF.Core/Schema2/gltf.Buffer.cs
  19. 36 59
      src/SharpGLTF.Core/Schema2/gltf.BufferView.cs
  20. 39 0
      src/SharpGLTF.Core/Schema2/gltf.Camera.cs
  21. 3 3
      src/SharpGLTF.Core/Schema2/gltf.ExtensionsFactory.cs
  22. 13 8
      src/SharpGLTF.Core/Schema2/gltf.ExtraProperties.cs
  23. 21 13
      src/SharpGLTF.Core/Schema2/gltf.Images.cs
  24. 14 0
      src/SharpGLTF.Core/Schema2/gltf.Material.cs
  25. 21 28
      src/SharpGLTF.Core/Schema2/gltf.Mesh.cs
  26. 15 27
      src/SharpGLTF.Core/Schema2/gltf.MeshPrimitive.cs
  27. 62 51
      src/SharpGLTF.Core/Schema2/gltf.Node.cs
  28. 13 55
      src/SharpGLTF.Core/Schema2/gltf.Root.cs
  29. 9 3
      src/SharpGLTF.Core/Schema2/gltf.Scene.cs
  30. 19 10
      src/SharpGLTF.Core/Schema2/gltf.Skin.cs
  31. 11 0
      src/SharpGLTF.Core/Schema2/gltf.TextureInfo.cs
  32. 7 6
      src/SharpGLTF.Core/Schema2/gltf.Textures.cs
  33. 37 3
      src/SharpGLTF.Core/Schema2/khr.lights.cs
  34. 7 5
      src/SharpGLTF.Core/SharpGLTF.Core.csproj
  35. 332 0
      src/SharpGLTF.Core/Validation/ValidationContext.Guards.cs
  36. 31 352
      src/SharpGLTF.Core/Validation/ValidationContext.cs
  37. 2 10
      src/SharpGLTF.Core/Validation/ValidationResult.cs
  38. 0 1
      src/SharpGLTF.Toolkit/Geometry/PackedEncoding.cs
  39. 2 2
      src/SharpGLTF.Toolkit/Scenes/SceneBuilder.Schema2.cs
  40. 4 6
      src/SharpGLTF.Toolkit/SharpGLTF.Toolkit.csproj
  41. 35 3
      tests/SharpGLTF.NUnit/TestFiles.cs
  42. 1 0
      tests/SharpGLTF.NUnit/ValidationResult.cs
  43. 25 63
      tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadGeneratedTests.cs
  44. 11 13
      tests/SharpGLTF.Tests/Validation/InvalidFilesTests.cs

+ 1 - 0
SharpGLTF.sln

@@ -5,6 +5,7 @@ VisualStudioVersion = 16.0.29709.97
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{29566B60-311D-42A0-9E8D-C48DECDD587F}"
 	ProjectSection(SolutionItems) = preProject
+		.editorconfig = .editorconfig
 		Analyzers.targets = Analyzers.targets
 		README.md = README.md
 		SharpGLTF.ruleset = SharpGLTF.ruleset

+ 2 - 2
Analyzers.targets → src/Analyzers.props

@@ -2,11 +2,11 @@
 <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
 
   <PropertyGroup>
-    <CodeAnalysisRuleSet>$(MsBuildThisFileDirectory)SharpGLTF.ruleset</CodeAnalysisRuleSet>
+    <CodeAnalysisRuleSet>$(MsBuildThisFileDirectory)..\SharpGLTF.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
 
   <ItemGroup>
-    <AdditionalFiles Include="$(MsBuildThisFileDirectory)stylecop.json" />
+    <AdditionalFiles Include="$(MsBuildThisFileDirectory)..\stylecop.json" />
   </ItemGroup>
 
   <ItemGroup>    

+ 24 - 0
src/Shared/Guard.cs

@@ -179,6 +179,30 @@ namespace SharpGLTF
 
         #region specialised
 
+        public static void IsValidURI(string parameterName, string gltfURI, params string[] validHeaders)
+        {
+            if (string.IsNullOrEmpty(gltfURI)) return;
+
+            foreach (var hdr in validHeaders)
+            {
+                if (gltfURI.StartsWith(hdr, StringComparison.OrdinalIgnoreCase))
+                {
+                    string value = hdr + ",";
+                    if (gltfURI.StartsWith(value, StringComparison.OrdinalIgnoreCase)) return;
+                    if (gltfURI.StartsWith(hdr + ";base64,", StringComparison.OrdinalIgnoreCase)) return;
+
+                    throw new ArgumentException($"{parameterName} has invalid URI '{gltfURI}'.");
+                }
+            }
+
+            if (gltfURI.StartsWith("data:")) throw new ArgumentException($"Invalid URI '{gltfURI}'.");
+
+            if (!Uri.IsWellFormedUriString(gltfURI, UriKind.RelativeOrAbsolute)) throw new ArgumentException($"Invalid URI '{gltfURI}'.");
+            if (!Uri.TryCreate(gltfURI, UriKind.RelativeOrAbsolute, out Uri xuri)) throw new ArgumentException($"Invalid URI '{gltfURI}'.");
+
+            return;
+        }
+
         public static void MustShareLogicalParent(Schema2.LogicalChildOfRoot a, Schema2.LogicalChildOfRoot b, string parameterName)
         {
             MustShareLogicalParent(a?.LogicalParent, nameof(a.LogicalParent), b, parameterName);

+ 73 - 9
src/Shared/_Extensions.cs

@@ -17,8 +17,8 @@ namespace SharpGLTF
 
         // constants from: https://github.com/KhronosGroup/glTF-Validator/blob/master/lib/src/errors.dart
 
-        private const float unitLengthThresholdVec3 = 0.00674f;
-        private const float unitLengthThresholdVec4 = 0.00769f;
+        private const float _UnitLengthThresholdVec3 = 0.00674f;
+        private const float _UnitLengthThresholdVec4 = 0.00769f;
 
         // This value is slightly greater
         // than the maximum error from unsigned 8-bit quantization
@@ -26,7 +26,7 @@ namespace SharpGLTF
         // 3..4 elements - 1 * step
         // 5..6 elements - 2 * step
         // ...
-        private const float unitSumThresholdStep = 0.0039216f;
+        private const float _UnitSumThresholdStep = 0.0039216f;
 
         #endregion
 
@@ -87,16 +87,14 @@ namespace SharpGLTF
         {
             if (!normal._IsFinite()) return false;
 
-            return Math.Abs(normal.Length() - 1) <= unitLengthThresholdVec3;
+            return Math.Abs(normal.Length() - 1) <= _UnitLengthThresholdVec3;
         }
 
-        internal static Boolean IsNormalized(this Quaternion q)
+        internal static Boolean IsNormalized(this Quaternion rotation)
         {
-            // As per: https://github.com/KhronosGroup/glTF-Validator/issues/33 , quaternions need to be normalized.
+            if (!rotation._IsFinite()) return false;
 
-            if (!q._IsFinite()) return false;
-
-            return Math.Abs(q.Length() - 1) <= 0.000005;
+            return Math.Abs(rotation.Length() - 1) <= _UnitLengthThresholdVec4;
         }
 
         internal static Quaternion AsQuaternion(this Vector4 v)
@@ -179,6 +177,17 @@ namespace SharpGLTF
             return dst;
         }
 
+        internal static bool IsValid(this in Matrix4x4 matrix)
+        {
+            if (!matrix._IsFinite()) return false;
+
+            if (!Matrix4x4.Decompose(matrix, out Vector3 s, out Quaternion r, out Vector3 t)) return false;
+
+            if (!Matrix4x4.Invert(matrix, out Matrix4x4 inverse)) return false;
+
+            return true;
+        }
+
         #endregion
 
         #region linq
@@ -347,6 +356,22 @@ namespace SharpGLTF
             return collection.Concat(instances.Where(item => item != null));
         }
 
+        public static void SanitizeNormals(this IList<Vector3> normals)
+        {
+            for (int i = 0; i < normals.Count; ++i)
+            {
+                if (!normals[i].IsNormalized()) normals[i] = normals[i].SanitizeNormal();
+            }
+        }
+
+        public static void SanitizeTangents(this IList<Vector4> tangents)
+        {
+            for (int i = 0; i < tangents.Count; ++i)
+            {
+                if (!tangents[i].IsValidTangent()) tangents[i] = tangents[i].SanitizeTangent();
+            }
+        }
+
         #endregion
 
         #region vertex & index accessors
@@ -633,6 +658,45 @@ namespace SharpGLTF
             return paddedContent;
         }
 
+        public static Byte[] TryParseBase64Unchecked(this string uri, params string[] prefixes)
+        {
+            if (uri == null) return null;
+
+            if (!uri.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) return null;
+
+            foreach (var prefix in prefixes)
+            {
+                var data = _TryParseBase64Unchecked(uri, prefix);
+                if (data != null) return data;
+            }
+
+            return null;
+        }
+
+        private static Byte[] _TryParseBase64Unchecked(string uri, string prefix)
+        {
+            if (!uri.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) return null;
+
+            var content = uri.Substring(prefix.Length);
+
+            if (content.StartsWith(";base64,", StringComparison.OrdinalIgnoreCase))
+            {
+                content = content.Substring(";base64,".Length);
+                return Convert.FromBase64String(content);
+            }
+
+            if (content.StartsWith(",", StringComparison.OrdinalIgnoreCase))
+            {
+                content = content.Substring(",".Length);
+
+                if (content.Length == 1) return new Byte[] { Byte.Parse(content, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture) };
+
+                throw new NotImplementedException();
+            }
+
+            throw new NotImplementedException();
+        }
+
         #endregion
     }
 }

+ 17 - 13
src/SharpGLTF.Core/IO/JsonSerializable.cs

@@ -15,24 +15,24 @@ namespace SharpGLTF.IO
     {
         #region validation
 
-        internal void ValidateReferences(Validation.ValidationContext context)
+        internal void ValidateReferences(Validation.ValidationContext validate)
         {
-            context = context.Result.GetContext(this);
-            OnValidateReferences(context);
-        }
-
-        protected virtual void OnValidateReferences(Validation.ValidationContext result) { }
+            validate = validate.GetContext(this);
 
-        internal void Validate(Validation.ValidationContext context)
-        {
-            context = context.Result.GetContext(this);
-            OnValidate(context);
+            OnValidateReferences(validate);
         }
 
-        protected virtual void OnValidate(Validation.ValidationContext result)
+        internal void ValidateContent(Validation.ValidationContext validate)
         {
+            validate = validate.GetContext(this);
+
+            OnValidateContent(validate);
         }
 
+        protected virtual void OnValidateReferences(Validation.ValidationContext validate) { }
+
+        protected virtual void OnValidateContent(Validation.ValidationContext validate) { }
+
         #endregion
 
         #region serialization
@@ -400,6 +400,8 @@ namespace SharpGLTF.IO
                 // System.Diagnostics.Debug.Assert(reader.TokenType != JsonToken.StartConstructor);
             }
 
+            if (list.Count == 0) throw new JsonException("Empty array found.");
+
             System.Diagnostics.Debug.Assert(reader.TokenType == JSONTOKEN.EndArray);
         }
 
@@ -429,6 +431,8 @@ namespace SharpGLTF.IO
                     // System.Diagnostics.Debug.Assert(reader.TokenType != JsonToken.StartConstructor);
                 }
             }
+
+            if (dict.Count == 0) throw new JsonException("Empty dictionary found.");
         }
 
         private static bool _TryCastValue(ref Utf8JsonReader reader, Type vtype, out Object value)
@@ -510,9 +514,9 @@ namespace SharpGLTF.IO
             {
                 var item = Activator.CreateInstance(vtype, true) as JsonSerializable;
 
-                System.Diagnostics.Debug.Assert(reader.TokenType == JSONTOKEN.StartObject);
+                // System.Diagnostics.Debug.Assert(reader.TokenType == JSONTOKEN.StartObject);
                 item.Deserialize(ref reader);
-                System.Diagnostics.Debug.Assert(reader.TokenType == JSONTOKEN.EndObject);
+                // System.Diagnostics.Debug.Assert(reader.TokenType == JSONTOKEN.EndObject);
 
                 value = item;
 

+ 36 - 30
src/SharpGLTF.Core/IO/ReadContext.cs

@@ -191,7 +191,7 @@ namespace SharpGLTF.IO
         {
             Guard.NotNull(stream, nameof(stream));
 
-            IReadOnlyDictionary<uint, Byte[]> chunks = null;
+            IReadOnlyDictionary<uint, byte[]> chunks;
 
             try
             {
@@ -200,13 +200,13 @@ namespace SharpGLTF.IO
             catch (System.IO.EndOfStreamException ex)
             {
                 var vr = new Validation.ValidationResult(null, this.Validation);
-                vr.AddError(new Validation.SchemaException(null, "Unexpected EOF"));
+                vr.SetError(new Validation.SchemaException(null, ex.Message));
                 return (null, vr);
             }
             catch (Validation.SchemaException ex)
             {
                 var vr = new Validation.ValidationResult(null, this.Validation);
-                vr.AddError(ex);
+                vr.SetError(ex);
                 return (null, vr);
             }
 
@@ -228,54 +228,60 @@ namespace SharpGLTF.IO
         private (SCHEMA2 Model, Validation.ValidationResult Validation) _Read(ReadOnlyMemory<Byte> jsonUtf8Bytes)
         {
             var root = new SCHEMA2();
-
             var vcontext = new Validation.ValidationResult(root, this.Validation);
 
-            if (jsonUtf8Bytes.IsEmpty)
+            try
             {
-                vcontext.AddError(new Validation.SchemaException(null, "JSon is empty."));
-            }
+                if (jsonUtf8Bytes.IsEmpty) throw new System.Text.Json.JsonException("JSon is empty.");
 
-            var reader = new Utf8JsonReader(jsonUtf8Bytes.Span);
+                var reader = new Utf8JsonReader(jsonUtf8Bytes.Span);
 
-            try
-            {
                 if (!reader.Read())
                 {
-                    vcontext.AddError(new Validation.SchemaException(root, "Json is empty"));
+                    vcontext.SetError(new Validation.SchemaException(root, "Json is empty"));
                     return (null, vcontext);
                 }
 
                 root.Deserialize(ref reader);
                 root.OnDeserializationCompleted();
-            }
-            catch (JsonException rex)
-            {
-                vcontext.AddError(new Validation.SchemaException(root, rex));
-                return (null, vcontext);
-            }
 
-            // binary chunk check
+                // binary chunk check
 
-            foreach (var b in root.LogicalBuffers) b.OnValidateBinaryChunk(vcontext.GetContext(root), this._BinaryChunk);
+                foreach (var b in root.LogicalBuffers) b.OnValidateBinaryChunk(vcontext.GetContext(), this._BinaryChunk);
 
-            // schema validation
+                // schema validation
 
-            root.ValidateReferences(vcontext.GetContext());
-            var ex = vcontext.Errors.FirstOrDefault();
-            if (ex != null) return (null, vcontext);
+                root.ValidateReferences(vcontext.GetContext());
+                var ex = vcontext.Errors.FirstOrDefault();
+                if (ex != null) return (null, vcontext);
 
-            // resolve external dependencies
+                // resolve external dependencies
 
-            root._ResolveSatelliteDependencies(this);
+                root._ResolveSatelliteDependencies(this);
 
-            // full validation
+                // full validation
 
-            if (this.Validation != VALIDATIONMODE.Skip)
+                if (this.Validation != VALIDATIONMODE.Skip)
+                {
+                    root.ValidateContent(vcontext.GetContext());
+                    ex = vcontext.Errors.FirstOrDefault();
+                    if (ex != null) return (null, vcontext);
+                }
+            }
+            catch (JsonException rex)
             {
-                root.Validate(vcontext.GetContext());
-                ex = vcontext.Errors.FirstOrDefault();
-                if (ex != null) return (null, vcontext);
+                vcontext.SetError(new Validation.SchemaException(root, rex));
+                return (null, vcontext);
+            }
+            catch (System.FormatException fex)
+            {
+                vcontext.SetError(new Validation.ModelException(null, fex));
+                return (null, vcontext);
+            }
+            catch (Validation.ModelException mex)
+            {
+                vcontext.SetError(mex);
+                return (null, vcontext);
             }
 
             return (root, vcontext);

+ 7 - 1
src/SharpGLTF.Core/IO/Serialization.cs

@@ -38,7 +38,13 @@ namespace SharpGLTF.IO
         {
             if (reader.TokenType == JsonTokenType.String)
             {
-                return Enum.Parse(enumType, reader.GetString(), true);
+                var jsonVal = reader.GetString();
+
+                try { return Enum.Parse(enumType, jsonVal, true); }
+                catch (System.ArgumentException ex)
+                {
+                    throw new System.Text.Json.JsonException($"Value {jsonVal} not found int {enumType}", ex);
+                }
             }
 
             if (reader.TokenType == JsonTokenType.Number)

+ 3 - 3
src/SharpGLTF.Core/IO/WriteContext.cs

@@ -207,9 +207,9 @@ namespace SharpGLTF.IO
 
         /// <summary>
         /// This needs to be called immediately before writing to json,
-        /// but immediately after preprocessing and buffer setup, so we have a valid model.
+        /// but immediately after preprocessing and buffer setup, so the model can be correctly validated.
         /// </summary>
-        /// <param name="model"></param>
+        /// <param name="model">The model to validate.</param>
         private void _ValidateBeforeWriting(SCHEMA2 model)
         {
             if (_NoCloneWatchdog) return;
@@ -220,7 +220,7 @@ namespace SharpGLTF.IO
             var ex = vcontext.Errors.FirstOrDefault();
             if (ex != null) throw ex;
 
-            model.Validate(vcontext.GetContext());
+            model.ValidateContent(vcontext.GetContext());
             ex = vcontext.Errors.FirstOrDefault();
             if (ex != null) throw ex;
         }

+ 121 - 0
src/SharpGLTF.Core/Memory/AttributeFormat.cs

@@ -0,0 +1,121 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SharpGLTF.Memory
+{
+    using DIMENSIONS = SharpGLTF.Schema2.DimensionType;
+    using ENCODING = SharpGLTF.Schema2.EncodingType;
+
+    /// <summary>
+    /// Defines the formatting in which a byte sequence can be encoded/decoded to attribute elements.
+    /// </summary>
+    [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
+    public readonly struct AttributeFormat : IEquatable<AttributeFormat>
+    {
+        #region debug
+
+        internal string _GetDebuggerDisplay()
+        {
+            var txt = $"{Encoding}";
+
+            switch (Dimensions)
+            {
+                case DIMENSIONS.SCALAR: break;
+                case DIMENSIONS.VEC2: txt += "2"; break;
+                case DIMENSIONS.VEC3: txt += "3"; break;
+                case DIMENSIONS.VEC4: txt += "4"; break;
+                case DIMENSIONS.MAT2: txt += "2x2"; break;
+                case DIMENSIONS.MAT3: txt += "3x3"; break;
+                case DIMENSIONS.MAT4: txt += "4x4"; break;
+                default: txt += "?"; break;
+            }
+
+            if (Normalized) txt += " Normalized";
+            return txt;
+        }
+
+        #endregion
+
+        #region constructors
+
+        public static implicit operator AttributeFormat(ENCODING enc) { return new AttributeFormat(enc); }
+
+        public static implicit operator AttributeFormat(DIMENSIONS dim) { return new AttributeFormat(dim); }
+
+        public static implicit operator AttributeFormat((DIMENSIONS dim, ENCODING enc) fmt) { return new AttributeFormat(fmt.dim, fmt.enc); }
+
+        public static implicit operator AttributeFormat((DIMENSIONS dim, ENCODING enc, Boolean nrm) fmt) { return new AttributeFormat(fmt.dim, fmt.enc, fmt.nrm); }
+
+        public AttributeFormat(ENCODING enc)
+        {
+            Dimensions = DIMENSIONS.SCALAR;
+            Encoding = enc;
+            Normalized = false;
+            ByteSize = Dimensions.DimCount() * Encoding.ByteLength();
+        }
+
+        public AttributeFormat(DIMENSIONS dim)
+        {
+            Dimensions = dim;
+            Encoding = ENCODING.FLOAT;
+            Normalized = false;
+            ByteSize = Dimensions.DimCount() * Encoding.ByteLength();
+        }
+
+        public AttributeFormat(DIMENSIONS dim, ENCODING enc)
+        {
+            Dimensions = dim;
+            Encoding = enc;
+            Normalized = false;
+            ByteSize = Dimensions.DimCount() * Encoding.ByteLength();
+        }
+
+        public AttributeFormat(DIMENSIONS dim, ENCODING enc, Boolean nrm)
+        {
+            Dimensions = dim;
+            Encoding = enc;
+            Normalized = nrm;
+            ByteSize = Dimensions.DimCount() * Encoding.ByteLength();
+        }
+
+        #endregion
+
+        #region data
+
+        public readonly ENCODING Encoding;
+        public readonly DIMENSIONS Dimensions;
+        public readonly Boolean Normalized;
+        public readonly Int32 ByteSize;
+
+        public override int GetHashCode()
+        {
+            return Dimensions.GetHashCode()
+                ^ Encoding.GetHashCode()
+                ^ Normalized.GetHashCode();
+        }
+
+        public static bool AreEqual(AttributeFormat a, AttributeFormat b)
+        {
+            if (a.Encoding != b.Encoding) return false;
+            if (a.Dimensions != b.Dimensions) return false;
+            if (a.Normalized != b.Normalized) return false;
+            return true;
+        }
+
+        public Int32 ByteSizePadded => ByteSize.WordPadded();
+
+        #endregion
+
+        #region API
+
+        public override bool Equals(object obj) { return obj is AttributeFormat other ? AreEqual(this, other) : false; }
+
+        public bool Equals(AttributeFormat other) { return AreEqual(this, other); }
+
+        public static bool operator ==(AttributeFormat a, AttributeFormat b) { return AreEqual(a, b); }
+        public static bool operator !=(AttributeFormat a, AttributeFormat b) { return !AreEqual(a, b); }
+
+        #endregion
+    }
+}

+ 80 - 8
src/SharpGLTF.Core/Memory/MemoryAccessor.Validation.cs

@@ -130,7 +130,7 @@ namespace SharpGLTF.Memory
 
         #region validate weights sum
 
-        public static void ValidateWeightsSum(Validation.ValidationContext result, MemoryAccessor weights0, MemoryAccessor weights1)
+        public static void VerifyWeightsSum(MemoryAccessor weights0, MemoryAccessor weights1)
         {
             int idx = 0;
 
@@ -142,7 +142,7 @@ namespace SharpGLTF.Memory
                 {
                     if (!_CheckWeightSum(item, weights0.Attribute.Encoding))
                     {
-                        result.AddDataError($"Weight Sum invalid at Index {idx}");
+                        throw new ArgumentException($"Weight Sum invalid at Index {idx}", nameof(weights0));
                     }
 
                     ++idx;
@@ -151,11 +151,7 @@ namespace SharpGLTF.Memory
                 return;
             }
 
-            if (weights0 == null)
-            {
-                result.AddLinkError("");
-                return;
-            }
+            if (weights0 == null) throw new ArgumentNullException(nameof(weights0));
 
             var len = weights0.Attribute.ItemByteLength;
             Span<Byte> dst = stackalloc byte[len * 2];
@@ -169,7 +165,7 @@ namespace SharpGLTF.Memory
 
                 if (!_CheckWeightSum(dst, weights0.Attribute.Encoding))
                 {
-                    result.AddDataError($"Weight Sum invalid at Index {idx}");
+                    throw new ArgumentException($"Weight Sum invalid at Index {idx}", nameof(weights1));
                 }
 
                 ++idx;
@@ -226,5 +222,81 @@ namespace SharpGLTF.Memory
         }
 
         #endregion
+
+        #region bounds validation
+
+        public static void VerifyAccessorBounds(MemoryAccessor memory, IReadOnlyList<double> min, IReadOnlyList<double> max)
+        {
+            Guard.NotNull(memory, nameof(memory));
+            Guard.NotNull(min, nameof(min));
+            Guard.NotNull(max, nameof(max));
+
+            if (min.Count == 0 && max.Count == 0) return;
+
+            var dimensions = memory.Attribute.Dimensions.DimCount();
+
+            if (min.Count != dimensions) throw new ArgumentException($"min size mismatch; expected {dimensions} but found {min.Count}", nameof(min));
+            if (max.Count != dimensions) throw new ArgumentException($"max size mismatch; expected {dimensions} but found {max.Count}", nameof(max));
+
+            for (int i = 0; i < min.Count; ++i)
+            {
+                // if (_min[i] > _max[i]) result.AddError(this, $"min[{i}] is larger than max[{i}]");
+            }
+
+            var minimum = min.Select(item => (float)item).ToArray();
+            var maximum = max.Select(item => (float)item).ToArray();
+
+            var xinfo = memory.Attribute;
+            xinfo.Dimensions = DIMENSIONS.SCALAR;
+            memory = new MemoryAccessor(memory.Data, xinfo);
+
+            var array = new MultiArray(memory.Data, memory.Attribute.ByteOffset, memory.Attribute.ItemsCount, memory.Attribute.ByteStride, dimensions, memory.Attribute.Encoding, memory.Attribute.Normalized);
+
+            var current = new float[dimensions];
+
+            for (int i = 0; i < array.Count; ++i)
+            {
+                array.CopyItemTo(i, current);
+
+                for (int j = 0; j < current.Length; ++j)
+                {
+                    var v = current[j];
+
+                    // if (!v._IsFinite()) result.AddError(this, $"Item[{j}][{i}] is not a finite number: {v}");
+
+                    var axisMin = minimum[j];
+                    var axisMax = maximum[j];
+
+                    if (v < axisMin || v > axisMax) throw new ArgumentException($"Value[{i}] is out of bounds. {axisMin} <= {v} <= {axisMax}", nameof(memory));
+
+                    // if (v < min || v > max) result.AddError(this, $"Item[{j}][{i}] is out of bounds. {min} <= {v} <= {max}");
+                }
+            }
+        }
+
+        #endregion
+
+        #region vertex index validation
+
+        public static void VerifyVertexIndices(MemoryAccessor memory, uint vertexCount)
+        {
+            uint restart_value = 0xff;
+            if (memory.Attribute.Encoding == ENCODING.UNSIGNED_SHORT) restart_value = 0xffff;
+            if (memory.Attribute.Encoding == ENCODING.UNSIGNED_INT) restart_value = 0xffffffff;
+
+            memory.AsIntegerArray();
+
+            var indices = memory.AsIntegerArray();
+
+            for (int i = 0; i < indices.Count; ++i)
+            {
+                var idx = indices[i];
+
+                if (idx >= vertexCount) throw new ArgumentException($"Value[{i}] is out of bounds {vertexCount}.", nameof(memory));
+                if (idx == restart_value) throw new ArgumentException($"Value[{i}] is restart value.", nameof(memory));
+            }
+        }
+
+        #endregion
     }
 }

+ 13 - 2
src/SharpGLTF.Core/Memory/MemoryAccessor.cs

@@ -3,11 +3,11 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Numerics;
 
+using BYTES = System.ArraySegment<System.Byte>;
+
 using DIMENSIONS = SharpGLTF.Schema2.DimensionType;
 using ENCODING = SharpGLTF.Schema2.EncodingType;
 
-using BYTES = System.ArraySegment<System.Byte>;
-
 namespace SharpGLTF.Memory
 {
     /// <summary>
@@ -62,6 +62,17 @@ namespace SharpGLTF.Memory
             throw new NotImplementedException();
         }
 
+        public MemoryAccessInfo(string name, int byteOffset, int itemsCount, int byteStride, AttributeFormat format)
+        {
+            this.Name = name;
+            this.ByteOffset = byteOffset;
+            this.ItemsCount = itemsCount;
+            this.ByteStride = byteStride;
+            this.Dimensions = format.Dimensions;
+            this.Encoding = format.Encoding;
+            this.Normalized = format.Normalized;
+        }
+
         public MemoryAccessInfo(string name, int byteOffset, int itemsCount, int byteStride, DIMENSIONS dimensions, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
         {
             this.Name = name;

+ 31 - 33
src/SharpGLTF.Core/Memory/MemoryImage.cs

@@ -13,12 +13,12 @@ namespace SharpGLTF.Memory
     {
         #region constants
 
-        const string EMBEDDED_OCTET_STREAM = "data:application/octet-stream;base64,";
-        const string EMBEDDED_GLTF_BUFFER = "data:application/gltf-buffer;base64,";
-        const string EMBEDDED_JPEG_BUFFER = "data:image/jpeg;base64,";
-        const string EMBEDDED_PNG_BUFFER = "data:image/png;base64,";
-        const string EMBEDDED_DDS_BUFFER = "data:image/vnd-ms.dds;base64,";
-        const string EMBEDDED_WEBP_BUFFER = "data:image/webp;base64,";
+        const string EMBEDDED_OCTET_STREAM = "data:application/octet-stream";
+        const string EMBEDDED_GLTF_BUFFER = "data:application/gltf-buffer";
+        const string EMBEDDED_JPEG_BUFFER = "data:image/jpeg";
+        const string EMBEDDED_PNG_BUFFER = "data:image/png";
+        const string EMBEDDED_DDS_BUFFER = "data:image/vnd-ms.dds";
+        const string EMBEDDED_WEBP_BUFFER = "data:image/webp";
 
         const string MIME_PNG = "image/png";
         const string MIME_JPG = "image/jpeg";
@@ -33,12 +33,13 @@ namespace SharpGLTF.Memory
         internal static Byte[] DefaultPngImage => Convert.FromBase64String(DEFAULT_PNG_IMAGE);
 
         internal static readonly string[] _EmbeddedHeaders =
-                { EMBEDDED_OCTET_STREAM
-                , EMBEDDED_GLTF_BUFFER
-                , EMBEDDED_JPEG_BUFFER
-                , EMBEDDED_PNG_BUFFER
-                , EMBEDDED_DDS_BUFFER
-                , EMBEDDED_WEBP_BUFFER };
+                { EMBEDDED_OCTET_STREAM,
+                EMBEDDED_GLTF_BUFFER,
+                EMBEDDED_JPEG_BUFFER,
+                EMBEDDED_PNG_BUFFER,
+                EMBEDDED_DDS_BUFFER,
+                EMBEDDED_WEBP_BUFFER
+                };
 
         public static MemoryImage Empty => default;
 
@@ -141,6 +142,12 @@ namespace SharpGLTF.Memory
             return new System.IO.MemoryStream(_Image.Array, _Image.Offset, _Image.Count, false);
         }
 
+        /// <summary>
+        /// Gets the internal buffer.
+        /// </summary>
+        /// <returns>An array buffer.</returns>
+        public BYTES GetBuffer() { return _Image; }
+
         /// <summary>
         /// Returns this image file, enconded as a Mime64 string.
         /// </summary>
@@ -157,17 +164,13 @@ namespace SharpGLTF.Memory
                 if (this.IsJpg) mimeContent = EMBEDDED_JPEG_BUFFER;
                 if (this.IsDds) mimeContent = EMBEDDED_DDS_BUFFER;
                 if (this.IsWebp) mimeContent = EMBEDDED_WEBP_BUFFER;
+
+                mimeContent += ";base64,";
             }
 
             return mimeContent + Convert.ToBase64String(_Image.Array, _Image.Offset, _Image.Count, Base64FormattingOptions.None);
         }
 
-        /// <summary>
-        /// Gets the internal buffer.
-        /// </summary>
-        /// <returns>An array buffer.</returns>
-        public BYTES GetBuffer() { return _Image; }
-
         /// <summary>
         /// Tries to parse a Mime64 string to a Byte array.
         /// </summary>
@@ -175,12 +178,16 @@ namespace SharpGLTF.Memory
         /// <returns>A byte array representing an image file, or null if the image was not identified.</returns>
         public static Byte[] TryParseBytes(string mime64content)
         {
-            return _TryParseBase64Unchecked(mime64content, EMBEDDED_GLTF_BUFFER)
-                ?? _TryParseBase64Unchecked(mime64content, EMBEDDED_OCTET_STREAM)
-                ?? _TryParseBase64Unchecked(mime64content, EMBEDDED_JPEG_BUFFER)
-                ?? _TryParseBase64Unchecked(mime64content, EMBEDDED_PNG_BUFFER)
-                ?? _TryParseBase64Unchecked(mime64content, EMBEDDED_DDS_BUFFER)
-                ?? null;
+            if (mime64content == null) return null;
+
+            var bytes = mime64content.TryParseBase64Unchecked(_EmbeddedHeaders);
+
+            if (mime64content.StartsWith(EMBEDDED_PNG_BUFFER, StringComparison.Ordinal) && !_IsPngImage(bytes)) throw new ArgumentException("Invalid PNG Content", nameof(mime64content));
+            if (mime64content.StartsWith(EMBEDDED_JPEG_BUFFER, StringComparison.Ordinal) && !_IsJpgImage(bytes)) throw new ArgumentException("Invalid JPG Content", nameof(mime64content));
+            if (mime64content.StartsWith(EMBEDDED_DDS_BUFFER, StringComparison.Ordinal) && !_IsDdsImage(bytes)) throw new ArgumentException("Invalid DDS Content", nameof(mime64content));
+            if (mime64content.StartsWith(EMBEDDED_WEBP_BUFFER, StringComparison.Ordinal) && !_IsWebpImage(bytes)) throw new ArgumentException("Invalid WEBP Content", nameof(mime64content));
+
+            return bytes;
         }
 
         /// <summary>
@@ -207,15 +214,6 @@ namespace SharpGLTF.Memory
 
         #region internals
 
-        private static Byte[] _TryParseBase64Unchecked(string uri, string prefix)
-        {
-            if (uri == null) return null;
-            if (!uri.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) return null;
-
-            var content = uri.Substring(prefix.Length);
-            return Convert.FromBase64String(content);
-        }
-
         private static bool _IsPngImage(IReadOnlyList<Byte> data)
         {
             if (data[0] != 0x89) return false;

+ 9 - 20
src/SharpGLTF.Core/Schema2/_Extensions.cs

@@ -10,31 +10,20 @@ namespace SharpGLTF.Schema2
     /// </summary>
     static class _Schema2Extensions
     {
-        #region base64
+        #region morph weights
 
-        internal static Byte[] _TryParseBase64Unchecked(this string uri, string prefix)
+        public static void SetMorphWeights(this IList<Double> list, int maxCount, Transforms.SparseWeight8 weights)
         {
-            if (uri == null) return null;
-            if (!uri.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) return null;
+            while (list.Count > maxCount) list.RemoveAt(list.Count - 1);
+            while (list.Count < maxCount) list.Add(0);
 
-            var content = uri.Substring(prefix.Length);
-
-            if (content.StartsWith(";base64,", StringComparison.OrdinalIgnoreCase))
+            if (list.Count > 0)
             {
-                content = content.Substring(";base64,".Length);
-                return Convert.FromBase64String(content);
+                foreach (var (index, weight) in weights.GetIndexedWeights())
+                {
+                    list[index] = weight;
+                }
             }
-
-            if (content.StartsWith(",", StringComparison.OrdinalIgnoreCase))
-            {
-                content = content.Substring(",".Length);
-
-                if (content.Length == 1) return new Byte[] { Byte.Parse(content,System.Globalization.NumberStyles.HexNumber) };
-
-                throw new NotImplementedException();
-            }
-
-            throw new NotImplementedException();
         }
 
         #endregion

+ 28 - 33
src/SharpGLTF.Core/Schema2/gltf.AccessorSparse.cs

@@ -49,28 +49,25 @@ namespace SharpGLTF.Schema2
 
         #region validation
 
-        protected override void OnValidateReferences(ValidationContext result)
+        protected override void OnValidateReferences(ValidationContext validate)
         {
-            result = result.GetContext(this);
+            base.OnValidateReferences(validate);
 
-            base.OnValidateReferences(result);
+            validate
+                .IsInRange(nameof(Count), _count, _countMinimum, int.MaxValue)
+                .IsDefined("Indices", _indices)
+                .IsDefined("Values", _values);
 
-            result.CheckIsInRange(nameof(Count), _count, _countMinimum, int.MaxValue);
-            result.CheckSchemaIsDefined("Indices", _indices);
-            result.CheckSchemaIsDefined("Values", _values);
-
-            _indices?.ValidateReferences(result);
-            _values?.ValidateReferences(result);
+            _indices?.ValidateReferences(validate);
+            _values?.ValidateReferences(validate);
         }
 
-        protected override void OnValidate(ValidationContext result)
+        protected override void OnValidateContent(ValidationContext validate)
         {
-            result = result.GetContext(this);
-
-            base.OnValidate(result);
+            base.OnValidateContent(validate);
 
-            _indices.Validate(result, _count);
-            _values.Validate(result, _count);
+            _indices.ValidateIndices(validate, _count);
+            _values.ValidateValues(validate, _count);
         }
 
         #endregion
@@ -106,23 +103,22 @@ namespace SharpGLTF.Schema2
 
         #region validation
 
-        protected override void OnValidateReferences(ValidationContext result)
+        protected override void OnValidateReferences(ValidationContext validate)
         {
-            result = result.GetContext(this);
+            base.OnValidateReferences(validate);
 
-            base.OnValidateReferences(result);
-
-            result.CheckSchemaNonNegative("ByteOffset", _byteOffset);
-            result.CheckArrayIndexAccess("BufferView", _bufferView, result.Root.LogicalBufferViews);
+            validate
+                .NonNegative("ByteOffset", _byteOffset)
+                .IsNullOrIndex("BufferView", _bufferView, validate.Root.LogicalBufferViews);
         }
 
-        internal void Validate(ValidationContext result, int count)
+        internal void ValidateIndices(ValidationContext validate, int count)
         {
-            result = result.GetContext(this);
+            validate = validate.GetContext(this);
 
-            var bv = result.Root.LogicalBufferViews[_bufferView];
+            var bv = validate.Root.LogicalBufferViews[_bufferView];
 
-            BufferView.CheckAccess(result, bv, _byteOffset ?? _byteOffsetDefault, DimensionType.SCALAR, _componentType.ToComponent(), false, count);
+            BufferView.VerifyAccess(validate, bv, _byteOffset ?? _byteOffsetDefault, (DimensionType.SCALAR, _componentType.ToComponent()), count);
         }
 
         #endregion
@@ -158,19 +154,18 @@ namespace SharpGLTF.Schema2
 
         #region validation
 
-        protected override void OnValidateReferences(ValidationContext result)
+        protected override void OnValidateReferences(ValidationContext validate)
         {
-            result = result.GetContext(this);
-
-            base.OnValidateReferences(result);
+            base.OnValidateReferences(validate);
 
-            result.CheckSchemaNonNegative("ByteOffset", _byteOffset);
-            result.CheckArrayIndexAccess("BufferView", _bufferView, result.Root.LogicalBufferViews);
+            validate
+                .NonNegative("ByteOffset", _byteOffset)
+                .IsNullOrIndex("BufferView", _bufferView, validate.Root.LogicalBufferViews);
         }
 
-        internal void Validate(ValidationContext result, int count)
+        internal void ValidateValues(ValidationContext validate, int count)
         {
-            var bv = result.Root.LogicalBufferViews[_bufferView];
+            var bv = validate.Root.LogicalBufferViews[_bufferView];
 
             // we need the accessor's settings to properly check this.
             // result.CheckAccess(bv, _byteOffset, DimensionType.SCALAR, _componentType, count);

+ 106 - 165
src/SharpGLTF.Core/Schema2/gltf.Accessors.cs

@@ -61,7 +61,7 @@ namespace SharpGLTF.Schema2
         /// <summary>
         /// Gets the number of bytes, starting at <see cref="ByteOffset"/> use by this <see cref="Accessor"/>
         /// </summary>
-        public int ByteLength                   => SourceBufferView.GetAccessorByteLength(Dimensions, Encoding, Count);
+        public int ByteLength                   => SourceBufferView.GetAccessorByteLength(Format, Count);
 
         /// <summary>
         /// Gets the <see cref="DimensionType"/> of an item.
@@ -83,10 +83,13 @@ namespace SharpGLTF.Schema2
         /// </summary>
         public Boolean IsSparse                 => this._sparse != null;
 
+        public AttributeFormat Format => new AttributeFormat(_type, _componentType, this._normalized.AsValue(false));
+
         /// <summary>
         /// Gets the number of bytes required to encode a single item in <see cref="SourceBufferView"/>
         /// Given the current <see cref="Dimensions"/> and <see cref="Encoding"/> states.
         /// </summary>
+        [Obsolete("Use Format.ByteSize instead")]
         public int ElementByteSize                 => Encoding.ByteLength() * Dimensions.DimCount();
 
         #endregion
@@ -96,7 +99,7 @@ namespace SharpGLTF.Schema2
         internal MemoryAccessor _GetMemoryAccessor(string name = null)
         {
             var view = SourceBufferView;
-            var info = new MemoryAccessInfo(name, ByteOffset, Count, view.ByteStride, Dimensions, Encoding, Normalized);
+            var info = new MemoryAccessInfo(name, ByteOffset, Count, view.ByteStride, Format);
             return new MemoryAccessor(view.Content, info);
         }
 
@@ -337,103 +340,60 @@ namespace SharpGLTF.Schema2
 
         #region Validation
 
-        protected override void OnValidateReferences(VALIDATIONCTX result)
+        protected override void OnValidateReferences(VALIDATIONCTX validate)
         {
-            base.OnValidateReferences(result);
-
-            result.CheckSchemaIsDefined("BufferView", _bufferView);
-            result.CheckArrayIndexAccess("BufferView", _bufferView, this.LogicalParent.LogicalBufferViews);
+            base.OnValidateReferences(validate);
 
-            result.CheckSchemaNonNegative("ByteOffset", _byteOffset);
-            result.CheckSchemaIsInRange("Count", _count, _countMinimum, int.MaxValue);
-
-            _sparse?.ValidateReferences(result);
+            validate
+                .IsDefined(nameof(_bufferView), _bufferView)
+                .NonNegative(nameof(_byteOffset), _byteOffset)
+                .IsGreaterOrEqual(nameof(_count), _count, _countMinimum)
+                .IsNullOrIndex(nameof(_bufferView), _bufferView, this.LogicalParent.LogicalBufferViews);
         }
 
-        protected override void OnValidate(VALIDATIONCTX result)
+        protected override void OnValidateContent(VALIDATIONCTX validate)
         {
-            base.OnValidate(result);
-
-            _sparse?.Validate(result);
+            base.OnValidateContent(validate);
 
-            BufferView.CheckAccess(result, this.SourceBufferView, this.ByteOffset, this.Dimensions, this.Encoding, this.Normalized, this.Count);
+            BufferView.VerifyAccess(validate, this.SourceBufferView, this.ByteOffset, this.Format, this.Count);
 
-            ValidateBounds(result);
+            try
+            {
+                MemoryAccessor.VerifyAccessorBounds(_GetMemoryAccessor(), _min, _max);
+            }
+            catch (ArgumentException ex)
+            {
+                validate._DataThrow(ex.ParamName, ex.Message);
+            }
 
             // at this point we don't know which kind of data we're accessing, so it's up to the components
             // using this accessor to validate the data.
         }
 
-        private void ValidateBounds(VALIDATIONCTX result)
+        internal void ValidateIndices(VALIDATIONCTX validate, uint vertexCount, PrimitiveType drawingType)
         {
-            result = result.GetContext(this);
-
-            if (_min.Count != _max.Count) result.AddDataError("Max", $"Min and Max bounds dimension mismatch Min:{_min.Count} Max:{_max.Count}");
-
-            if (_min.Count == 0 && _max.Count == 0) return;
-
-            var dimensions = this.Dimensions.DimCount();
-
-            if (_min.Count != dimensions) { result.AddLinkError("Min", $"size mismatch; expected {dimensions} but found {_min.Count}"); return; }
-            if (_max.Count != dimensions) { result.AddLinkError("Max", $"size mismatch; expected {dimensions} but found {_max.Count}"); return; }
-
-            for (int i = 0; i < _min.Count; ++i)
-            {
-                // if (_min[i] > _max[i]) result.AddError(this, $"min[{i}] is larger than max[{i}]");
-            }
+            validate = validate.GetContext(this);
 
-            if (this.Encoding != EncodingType.FLOAT) return;
-
-            var current = new float[dimensions];
-            var minimum = this._min.ConvertAll(item => (float)item);
-            var maximum = this._max.ConvertAll(item => (float)item);
+            SourceBufferView.ValidateBufferUsageGPU(validate, BufferMode.ELEMENT_ARRAY_BUFFER);
+            validate.IsAnyOf("Format", Format, (DimensionType.SCALAR, EncodingType.UNSIGNED_BYTE), (DimensionType.SCALAR, EncodingType.UNSIGNED_SHORT), (DimensionType.SCALAR, EncodingType.UNSIGNED_INT));
 
-            var array = new MultiArray(this.SourceBufferView.Content, this.ByteOffset, this.Count, this.SourceBufferView.ByteStride, dimensions, this.Encoding, false);
+            validate.AreEqual(nameof(SourceBufferView.ByteStride), SourceBufferView.ByteStride, 0); // "bufferView.byteStride must not be defined for indices accessor.";
 
-            for (int i = 0; i < array.Count; ++i)
+            try
             {
-                array.CopyItemTo(i, current);
-
-                for (int j = 0; j < current.Length; ++j)
-                {
-                    var v = current[j];
-
-                    // if (!v._IsFinite()) result.AddError(this, $"Item[{j}][{i}] is not a finite number: {v}");
-
-                    var min = minimum[j];
-                    var max = maximum[j];
-
-                    // if (v < min || v > max) result.AddError(this, $"Item[{j}][{i}] is out of bounds. {min} <= {v} <= {max}");
-                }
+                MemoryAccessor.VerifyVertexIndices(_GetMemoryAccessor(), vertexCount);
             }
-        }
-
-        internal void ValidateIndices(VALIDATIONCTX result, uint vertexCount, PrimitiveType drawingType)
-        {
-            result = result.GetContext(this);
-
-            SourceBufferView.ValidateBufferUsageGPU(result, BufferMode.ELEMENT_ARRAY_BUFFER);
-            result.CheckLinkMustBeAnyOf(nameof(Normalized), Normalized, false);
-            result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.UNSIGNED_BYTE, EncodingType.UNSIGNED_SHORT, EncodingType.UNSIGNED_INT);
-            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.SCALAR);
-
-            uint restart_value = 0xff;
-            if (this.Encoding == EncodingType.UNSIGNED_SHORT) restart_value = 0xffff;
-            if (this.Encoding == EncodingType.UNSIGNED_INT) restart_value = 0xffffffff;
-
-            var indices = this.AsIndicesArray();
-
-            for (int i = 0; i < indices.Count; ++i)
+            catch (ArgumentException ex)
             {
-                result.CheckVertexIndex(i, indices[i], vertexCount, restart_value);
+                validate._DataThrow(ex.ParamName, ex.Message);
             }
         }
 
-        internal static void ValidateVertexAttributes(VALIDATIONCTX result, IReadOnlyDictionary<string, Accessor> attributes, int skinsMaxJointCount)
+        internal static void ValidateVertexAttributes(VALIDATIONCTX validate, IReadOnlyDictionary<string, Accessor> attributes, int skinsMaxJointCount)
         {
-            if (result.TryFix)
+            if (validate.TryFix)
             {
-                foreach(var kvp in attributes.Where(item => item.Key != "POSITION"))
+                foreach (var kvp in attributes.Where(item => item.Key != "POSITION"))
                 {
                     // remove unnecessary bounds
                     kvp.Value._min.Clear();
@@ -441,167 +401,148 @@ namespace SharpGLTF.Schema2
                 }
             }
 
-            if (attributes.TryGetValue("POSITION", out Accessor positions)) positions._ValidatePositions(result);
-            else result.AddSemanticWarning("No POSITION attribute found.");
+            if (attributes.TryGetValue("POSITION", out Accessor positions)) positions._ValidatePositions(validate);
 
-            if (attributes.TryGetValue("NORMAL", out Accessor normals)) normals._ValidateNormals(result);
-            if (attributes.TryGetValue("TANGENT", out Accessor tangents)) tangents._ValidateTangents(result);
-            if (normals == null && tangents != null) result.AddSemanticWarning("TANGENT", "attribute without NORMAL found.");
+            if (attributes.TryGetValue("NORMAL", out Accessor normals)) normals._ValidateNormals(validate);
+            if (attributes.TryGetValue("TANGENT", out Accessor tangents)) tangents._ValidateTangents(validate);
 
-            if (attributes.TryGetValue("JOINTS_0", out Accessor joints0)) joints0._ValidateJoints(result, "JOINTS_0", skinsMaxJointCount);
-            if (attributes.TryGetValue("JOINTS_1", out Accessor joints1)) joints0._ValidateJoints(result, "JOINTS_1", skinsMaxJointCount);
+            if (attributes.TryGetValue("JOINTS_0", out Accessor joints0)) joints0._ValidateJoints(validate, "JOINTS_0", skinsMaxJointCount);
+            if (attributes.TryGetValue("JOINTS_1", out Accessor joints1)) joints0._ValidateJoints(validate, "JOINTS_1", skinsMaxJointCount);
 
             attributes.TryGetValue("WEIGHTS_0", out Accessor weights0);
             attributes.TryGetValue("WEIGHTS_1", out Accessor weights1);
-            _ValidateWeights(result, weights0, weights1);
+            _ValidateWeights(validate, weights0, weights1);
         }
 
-        private void _ValidatePositions(VALIDATIONCTX result)
+        private void _ValidatePositions(VALIDATIONCTX validate)
         {
-            result = result.GetContext(this);
+            validate = validate.GetContext(this);
+
+            SourceBufferView.ValidateBufferUsageGPU(validate, BufferMode.ARRAY_BUFFER);
 
-            SourceBufferView.ValidateBufferUsageGPU(result, BufferMode.ARRAY_BUFFER);
-            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.VEC3);
             if (!this.LogicalParent.MeshQuantizationAllowed)
             {
-                result.CheckLinkMustBeAnyOf(nameof(Normalized), Normalized, false);
-                result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.FLOAT);
+                validate.IsAnyOf(nameof(Format), Format, DimensionType.VEC3);
             }
-
-            var positions = this.AsVector3Array();
-
-            for (int i = 0; i < positions.Count; ++i)
+            else
             {
-                result.CheckIsFinite(i, positions[i]);
+                validate.IsAnyOf(nameof(Dimensions), Dimensions, DimensionType.VEC3);
             }
+
+            validate.ArePositions("POSITION", this.AsVector3Array());
         }
 
-        private void _ValidateNormals(VALIDATIONCTX result)
+        private void _ValidateNormals(VALIDATIONCTX validate)
         {
-            result = result.GetContext(this);
+            validate = validate.GetContext(this);
+
+            SourceBufferView.ValidateBufferUsageGPU(validate, BufferMode.ARRAY_BUFFER);
 
-            SourceBufferView.ValidateBufferUsageGPU(result, BufferMode.ARRAY_BUFFER);
-            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.VEC3);
             if (!this.LogicalParent.MeshQuantizationAllowed)
             {
-                result.CheckLinkMustBeAnyOf(nameof(Normalized), Normalized, false);
-                result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.FLOAT);
+                validate.IsAnyOf(nameof(Format), Format, DimensionType.VEC3);
             }
             else
             {
-                if (Normalized) result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.BYTE, EncodingType.SHORT);
-                else result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.FLOAT);
+                validate.IsAnyOf(nameof(Dimensions), Dimensions, DimensionType.VEC3);
             }
 
-            var normals = this.AsVector3Array();
+            if (validate.TryFix) this.AsVector3Array().SanitizeNormals();
 
-            for (int i = 0; i < normals.Count; ++i)
-            {
-                if (result.TryFixUnitLengthOrError(i, normals[i]))
-                {
-                    normals[i] = normals[i].SanitizeNormal();
-                }
-            }
+            validate.AreNormals("NORMAL", this.AsVector3Array());
         }
 
-        private void _ValidateTangents(VALIDATIONCTX result)
+        private void _ValidateTangents(VALIDATIONCTX validate)
         {
-            result = result.GetContext(this);
+            validate = validate.GetContext(this);
+
+            SourceBufferView.ValidateBufferUsageGPU(validate, BufferMode.ARRAY_BUFFER);
 
-            SourceBufferView.ValidateBufferUsageGPU(result, BufferMode.ARRAY_BUFFER);
-            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.VEC3, DimensionType.VEC4);
             if (!this.LogicalParent.MeshQuantizationAllowed)
             {
-                result.CheckLinkMustBeAnyOf(nameof(Normalized), Normalized, false);
-                result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.FLOAT);
+                validate.IsAnyOf(nameof(Format), Format, DimensionType.VEC3, DimensionType.VEC4);
             }
             else
             {
-                if (Normalized) result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.BYTE, EncodingType.SHORT);
-                else result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.FLOAT);
+                validate.IsAnyOf(nameof(Dimensions), Dimensions, DimensionType.VEC3, DimensionType.VEC4);
             }
 
-            // when Dimensions == VEC3, its morph target tangent deltas
-
-            if (Dimensions == DimensionType.VEC4)
+            if (validate.TryFix)
             {
-                var tangents = this.AsVector4Array();
-
-                for (int i = 0; i < tangents.Count; ++i)
-                {
-                    if (result.TryFixTangentOrError(i, tangents[i]))
-                    {
-                        tangents[i] = tangents[i].SanitizeTangent();
-                    }
-                }
+                if (Dimensions == DimensionType.VEC3) this.AsVector3Array().SanitizeNormals();
+                if (Dimensions == DimensionType.VEC4) this.AsVector4Array().SanitizeTangents();
             }
+
+            if (Dimensions == DimensionType.VEC3) validate.AreNormals("TANGENT", this.AsVector3Array());
+            if (Dimensions == DimensionType.VEC4) validate.AreTangents("TANGENT", this.AsVector4Array());
         }
 
-        private void _ValidateJoints(VALIDATIONCTX result, string attributeName, int skinsMaxJointCount)
+        private void _ValidateJoints(VALIDATIONCTX validate, string attributeName, int skinsMaxJointCount)
         {
-            result = result.GetContext(this);
+            validate = validate.GetContext(this);
 
-            SourceBufferView.ValidateBufferUsageGPU(result, BufferMode.ARRAY_BUFFER);
-            result.CheckLinkMustBeAnyOf(nameof(Normalized), Normalized, false);
-            result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.UNSIGNED_BYTE, EncodingType.UNSIGNED_SHORT, EncodingType.FLOAT);
-            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.VEC4);
+            SourceBufferView.ValidateBufferUsageGPU(validate, BufferMode.ARRAY_BUFFER);
 
-            var joints = this.AsVector4Array();
-
-            for (int i = 0; i < joints.Count; ++i)
-            {
-                var jidx = joints[i];
-
-                result.CheckIsFinite(i, jidx);
-                result.CheckIsInRange(i, jidx, 0, skinsMaxJointCount);
-            }
+            validate
+                .IsAnyOf(nameof(Format), Format, (DimensionType.VEC4, EncodingType.UNSIGNED_BYTE), (DimensionType.VEC4, EncodingType.UNSIGNED_SHORT), DimensionType.VEC4)
+                .AreJoints(attributeName, this.AsVector4Array(), skinsMaxJointCount);
         }
 
-        private static void _ValidateWeights(VALIDATIONCTX result, Accessor weights0, Accessor weights1)
+        private static void _ValidateWeights(VALIDATIONCTX validate, Accessor weights0, Accessor weights1)
         {
-            weights0?._ValidateWeights(result);
-            weights1?._ValidateWeights(result);
+            weights0?._ValidateWeights(validate);
+            weights1?._ValidateWeights(validate);
 
             var memory0 = weights0?._GetMemoryAccessor("WEIGHTS_0");
             var memory1 = weights1?._GetMemoryAccessor("WEIGHTS_1");
 
-            MemoryAccessor.ValidateWeightsSum(result, memory0, memory1);
+            try
+            {
+                MemoryAccessor.VerifyWeightsSum(memory0, memory1);
+            }
+            catch (ArgumentException ex)
+            {
+                validate._DataThrow(ex.ParamName, ex.Message);
+            }
         }
 
-        private void _ValidateWeights(VALIDATIONCTX result)
+        private void _ValidateWeights(VALIDATIONCTX validate)
         {
-            result = result.GetContext(this);
+            validate = validate.GetContext(this);
 
-            SourceBufferView.ValidateBufferUsageGPU(result, BufferMode.ARRAY_BUFFER);
-            result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.UNSIGNED_BYTE, EncodingType.UNSIGNED_SHORT, EncodingType.FLOAT);
-            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.VEC4);
+            SourceBufferView.ValidateBufferUsageGPU(validate, BufferMode.ARRAY_BUFFER);
+
+            validate.IsAnyOf(nameof(Format), Format, (DimensionType.VEC4, EncodingType.UNSIGNED_BYTE, true), (DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true), DimensionType.VEC4);
         }
 
-        internal void ValidateMatrices(VALIDATIONCTX result)
+        internal void ValidateMatrices(VALIDATIONCTX validate)
         {
-            result = result.GetContext(this);
+            validate = validate.GetContext(this);
+
+            SourceBufferView.ValidateBufferUsagePlainData(validate);
 
-            SourceBufferView.ValidateBufferUsagePlainData(result);
-            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.MAT4);
+            validate.IsAnyOf(nameof(Format), Format, (DimensionType.MAT4, EncodingType.BYTE, true), (DimensionType.MAT4, EncodingType.SHORT, true), DimensionType.MAT4);
 
             var matrices = this.AsMatrix4x4Array();
 
             for (int i = 0; i < matrices.Count; ++i)
             {
-                result.CheckIsMatrix(i, matrices[i]);
+                validate.IsNullOrMatrix("Matrices", matrices[i]);
             }
         }
 
-        internal void ValidateAnimationInput(VALIDATIONCTX result)
+        internal void ValidateAnimationInput(VALIDATIONCTX validate)
         {
-            SourceBufferView.ValidateBufferUsagePlainData(result);
-            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.SCALAR);
+            SourceBufferView.ValidateBufferUsagePlainData(validate);
+
+            validate.IsAnyOf(nameof(Dimensions), Dimensions, DimensionType.SCALAR);
         }
 
-        internal void ValidateAnimationOutput(VALIDATIONCTX result)
+        internal void ValidateAnimationOutput(VALIDATIONCTX validate)
         {
-            SourceBufferView.ValidateBufferUsagePlainData(result);
-            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.SCALAR, DimensionType.VEC3, DimensionType.VEC4);
+            SourceBufferView.ValidateBufferUsagePlainData(validate);
+
+            validate.IsAnyOf(nameof(Dimensions), Dimensions, DimensionType.SCALAR, DimensionType.VEC3, DimensionType.VEC4);
         }
 
         #endregion

+ 23 - 29
src/SharpGLTF.Core/Schema2/gltf.Animations.cs

@@ -219,23 +219,18 @@ namespace SharpGLTF.Schema2
 
         #region Validation
 
-        protected override void OnValidateReferences(ValidationContext result)
+        protected override void OnValidateReferences(ValidationContext validate)
         {
-            base.OnValidateReferences(result);
+            validate
+                .IsSetCollection("Samplers", _samplers)
+                .IsSetCollection("Channels", _channels);
 
-            result.CheckLinksInCollection("Samplers", _samplers);
-            result.CheckLinksInCollection("Channels", _channels);
-
-            foreach (var s in _samplers) s.ValidateReferences(result);
-            foreach (var c in _channels) c.ValidateReferences(result);
+            base.OnValidateReferences(validate);
         }
 
-        protected override void OnValidate(ValidationContext result)
+        protected override void OnValidateContent(ValidationContext validate)
         {
-            base.OnValidate(result);
-
-            foreach (var s in _samplers) s.Validate(result);
-            foreach (var c in _channels) c.Validate(result);
+            base.OnValidateContent(validate);
         }
 
         #endregion
@@ -267,11 +262,11 @@ namespace SharpGLTF.Schema2
 
         #region Validation
 
-        protected override void OnValidateReferences(ValidationContext result)
+        protected override void OnValidateReferences(ValidationContext validate)
         {
-            base.OnValidateReferences(result);
+            base.OnValidateReferences(validate);
 
-            result.CheckArrayIndexAccess("Node", _node, result.Root.LogicalNodes);
+            validate.IsNullOrIndex("Node", _node, validate.Root.LogicalNodes);
         }
 
         #endregion
@@ -331,13 +326,11 @@ namespace SharpGLTF.Schema2
 
         #region Validation
 
-        protected override void OnValidateReferences(ValidationContext result)
+        protected override void OnValidateReferences(ValidationContext validate)
         {
-            base.OnValidateReferences(result);
-
-            result.CheckArrayIndexAccess("Sampler", _sampler, this.LogicalParent._Samplers);
+            base.OnValidateReferences(validate);
 
-            _target.ValidateReferences(result);
+            validate.IsNullOrIndex("Sampler", _sampler, this.LogicalParent._Samplers);
         }
 
         #endregion
@@ -728,27 +721,28 @@ namespace SharpGLTF.Schema2
 
         #region validation
 
-        protected override void OnValidateReferences(ValidationContext result)
+        protected override void OnValidateReferences(ValidationContext validate)
         {
-            base.OnValidateReferences(result);
+            base.OnValidateReferences(validate);
 
-            result.CheckArrayIndexAccess("Input", _input, this.LogicalParent.LogicalParent.LogicalAccessors);
-            result.CheckArrayIndexAccess("Output", _output, this.LogicalParent.LogicalParent.LogicalAccessors);
+            validate
+                .IsNullOrIndex("Input", _input, this.LogicalParent.LogicalParent.LogicalAccessors)
+                .IsNullOrIndex("Output", _output, this.LogicalParent.LogicalParent.LogicalAccessors);
         }
 
-        protected override void OnValidate(ValidationContext result)
+        protected override void OnValidateContent(ValidationContext validate)
         {
-            base.OnValidate(result);
+            base.OnValidateContent(validate);
 
             if (Output.Dimensions != DimensionType.SCALAR)
             {
                 var outMult = InterpolationMode == AnimationInterpolationMode.CUBICSPLINE ? 3 : 1;
 
-                if (Input.Count * outMult != Output.Count) result.AddLinkError("Output", $"Input and Output count mismatch; Input: {Input.Count * outMult} Output:{Output.Count}");
+                validate.AreEqual("Output", Output.Count, Input.Count * outMult);
             }
 
-            Input.ValidateAnimationInput(result);
-            Output.ValidateAnimationOutput(result);
+            Input.ValidateAnimationInput(validate);
+            Output.ValidateAnimationOutput(validate);
         }
 
         #endregion

+ 5 - 7
src/SharpGLTF.Core/Schema2/gltf.Asset.cs

@@ -89,20 +89,18 @@ namespace SharpGLTF.Schema2
 
         #region Validation
 
-        protected override void OnValidate(Validation.ValidationContext result)
+        protected override void OnValidateContent(Validation.ValidationContext validate)
         {
-            base.OnValidate(result);
+            base.OnValidateContent(validate);
 
             if (!Version.TryParse(_version, out Version ver))
             {
-                result.AddSemanticError("Version", $"Unknown glTF major asset version: {_version}.");
+                validate._SchemaThrow("Version", $"Unknown glTF major asset version: {_version}.");
                 return;
             }
 
-            if (Version < MINVERSION) result.AddSemanticError($"Minimum supported version is {MINVERSION} but found:{MinVersion}");
-            if (MinVersion > MAXVERSION) result.AddSemanticError( $"Maximum supported version is {MAXVERSION} but found:{MinVersion}");
-
-            if (MinVersion > Version) result.AddSemanticWarning("Version", $"Asset minVersion '{MinVersion}' is greater than version '{Version}'.");
+            if (Version < MINVERSION) validate._SchemaThrow("Version", $"Minimum supported version is {MINVERSION} but found:{MinVersion}");
+            // if (MinVersion > MAXVERSION) result.AddSemanticError( $"Maximum supported version is {MAXVERSION} but found:{MinVersion}");
         }
 
         #endregion

+ 17 - 18
src/SharpGLTF.Core/Schema2/gltf.Buffer.cs

@@ -55,9 +55,7 @@ namespace SharpGLTF.Schema2
 
         private static Byte[] _LoadBinaryBufferUnchecked(string uri, IO.ReadContext context)
         {
-            return uri._TryParseBase64Unchecked(EMBEDDEDGLTFBUFFER)
-                ?? uri._TryParseBase64Unchecked(EMBEDDEDOCTETSTREAM)
-                ?? context.ReadAllBytesToEnd(uri).ToArray();
+            return uri.TryParseBase64Unchecked(EMBEDDEDGLTFBUFFER, EMBEDDEDOCTETSTREAM) ?? context.ReadAllBytesToEnd(uri).ToArray();
         }
 
         #endregion
@@ -114,29 +112,30 @@ namespace SharpGLTF.Schema2
 
         #region validation
 
-        internal void OnValidateBinaryChunk(Validation.ValidationContext result, Byte[] binaryChunk)
+        internal void OnValidateBinaryChunk(Validation.ValidationContext validate, Byte[] binaryChunk)
         {
+            validate = validate.GetContext(this);
+
             if (_uri == null)
             {
-                if (binaryChunk == null) { result.GetContext(this).AddSchemaError("Binary chunk not found"); return; }
-                if (_byteLength > binaryChunk.Length) result.GetContext(this).AddSchemaError("Buffer length larger than Binary chunk");
+                validate
+                    .NotNull(nameof(binaryChunk), binaryChunk)
+                    .IsGreaterOrEqual(nameof(_byteLength), _byteLength, _byteLengthMinimum)
+                    .IsLessOrEqual(nameof(_byteLength), _byteLength, binaryChunk.Length);
+                    // result.CheckSchemaIsMultipleOf("ByteLength", _byteLength, 4);
+            }
+            else
+            {
+                validate
+                    .IsValidURI(nameof(_uri), _uri, EMBEDDEDGLTFBUFFER, EMBEDDEDOCTETSTREAM);
             }
         }
 
-        protected override void OnValidateReferences(Validation.ValidationContext result)
-        {
-            base.OnValidateReferences(result);
-
-            result.CheckSchemaIsValidURI("Uri", this._uri, EMBEDDEDGLTFBUFFER, EMBEDDEDOCTETSTREAM);
-            result.CheckSchemaIsInRange("ByteLength", _byteLength, _byteLengthMinimum, int.MaxValue);
-            // result.CheckSchemaIsMultipleOf("ByteLength", _byteLength, 4);
-        }
-
-        protected override void OnValidate(Validation.ValidationContext result)
+        protected override void OnValidateContent(Validation.ValidationContext validate)
         {
-            base.OnValidate(result);
+            base.OnValidateContent(validate);
 
-            if (_Content.Length < _byteLength) result.AddDataError("ByteLength", $"Actual data length {_Content.Length} is less than the declared buffer byteLength {_byteLength}.");
+            validate.IsGreaterOrEqual("ByteLength", _Content.Length, _byteLength); // $"Actual data length {_Content.Length} is less than the declared buffer byteLength {_byteLength}.");
         }
 
         #endregion

+ 36 - 59
src/SharpGLTF.Core/Schema2/gltf.BufferView.cs

@@ -172,9 +172,9 @@ namespace SharpGLTF.Schema2
         /// taking into account if the source <see cref="BufferView"/> is strided.
         /// </summary>
         /// <returns>The number of bytes to access.</returns>
-        internal int GetAccessorByteLength(DimensionType dim, EncodingType enc, int count)
+        internal int GetAccessorByteLength(in Memory.AttributeFormat fmt, int count)
         {
-            var elementByteSize = dim.DimCount() * enc.ByteLength();
+            var elementByteSize = fmt.ByteSize;
             if (this.ByteStride == 0) return elementByteSize * count;
             return (this.ByteStride * (count - 1)) + elementByteSize;
         }
@@ -183,92 +183,70 @@ namespace SharpGLTF.Schema2
 
         #region Validation
 
-        internal static void CheckAccess(Validation.ValidationContext result, BufferView bv, int accessorByteOffset, DimensionType dim, EncodingType enc, bool nrm, int count)
+        internal static void VerifyAccess(Validation.ValidationContext validate, BufferView bv, int accessorByteOffset, Memory.AttributeFormat format, int count)
         {
-            if (nrm)
+            if (format.Normalized)
             {
-                if (enc != EncodingType.UNSIGNED_BYTE && enc != EncodingType.UNSIGNED_SHORT)
-                {
-                    result.AddDataError("Normalized", "Only (u)byte and (u)short accessors can be normalized.");
-                }
+                validate.IsAnyOf("Encoding", format.Encoding, EncodingType.UNSIGNED_BYTE, EncodingType.UNSIGNED_SHORT, EncodingType.BYTE, EncodingType.SHORT);
             }
 
-            var elementByteSize = dim.DimCount() * enc.ByteLength();
-
             if (bv.IsVertexBuffer)
             {
-                if (bv.ByteStride == 0) result.CheckSchemaIsMultipleOf("ElementByteSize", elementByteSize, 4);
+                if (bv.ByteStride == 0) validate.IsMultipleOf("ElementByteSize", format.ByteSize, 4);
             }
 
             if (bv.IsIndexBuffer)
             {
-                if (bv._byteStride.HasValue) result.AddSemanticError("bufferView: Invalid ByteStride.");
-
-                if (dim != DimensionType.SCALAR) result.AddLinkError(("BufferView", bv.LogicalIndex), $"is an IndexBuffer, but accessor dimensions is: {dim}");
-
-                // TODO: these could by fixed by replacing BYTE by UBYTE, SHORT by USHORT, etc
-                if (enc == EncodingType.BYTE)    result.AddLinkError(("BufferView", bv.LogicalIndex), $"is an IndexBuffer, but accessor encoding is (s)byte");
-                if (enc == EncodingType.SHORT)   result.AddLinkError(("BufferView", bv.LogicalIndex), $"is an IndexBuffer, but accessor encoding is (s)short");
-                if (enc == EncodingType.FLOAT)   result.AddLinkError(("BufferView", bv.LogicalIndex), $"is an IndexBuffer, but accessor encoding is float");
-                if (nrm)                         result.AddLinkError(("BufferView", bv.LogicalIndex), $"is an IndexBuffer, but accessor is normalized");
+                validate.GetContext(bv).IsUndefined("ByteStride", bv._byteStride);
+                validate.IsAnyOf("Format", format, (DimensionType.SCALAR, EncodingType.UNSIGNED_BYTE), (DimensionType.SCALAR, EncodingType.UNSIGNED_SHORT), (DimensionType.SCALAR, EncodingType.UNSIGNED_INT));
             }
 
-            if (bv.ByteStride > 0)
-            {
-                if (bv.ByteStride < elementByteSize) result.AddLinkError("ElementByteSize", $"Referenced bufferView's byteStride value {bv.ByteStride} is less than accessor element's length {elementByteSize}.");
-
-                return;
-            }
+            if (bv.ByteStride > 0) validate.IsGreaterOrEqual("ElementByteSize", bv.ByteStride, format.ByteSize);
 
-            var accessorByteLength = bv.GetAccessorByteLength(dim, enc, count);
+            var accessorByteLength = bv.GetAccessorByteLength(format, count);
 
             // "Accessor(offset: {0}, length: {1}) does not fit referenced bufferView[% 3] length %4.";
-            result.CheckArrayRangeAccess(("BufferView", bv.LogicalIndex), accessorByteOffset, accessorByteLength, bv.Content);
+            validate.IsNullOrInRange(("BufferView", bv.LogicalIndex), accessorByteOffset, accessorByteLength, bv.Content);
         }
 
-        protected override void OnValidateReferences(Validation.ValidationContext result)
+        protected override void OnValidateReferences(Validation.ValidationContext validate)
         {
-            base.OnValidateReferences(result);
-
-            result.CheckArrayIndexAccess(nameof(Buffer), _buffer, this.LogicalParent.LogicalBuffers);
+            base.OnValidateReferences(validate);
 
-            result.CheckSchemaNonNegative("ByteOffset", _byteOffset);
+            validate
+                .IsNullOrIndex(nameof(Buffer), _buffer, this.LogicalParent.LogicalBuffers)
+                .NonNegative("ByteOffset", _byteOffset)
+                .IsGreaterOrEqual("ByteLength", _byteLength, _byteLengthMinimum);
 
-            result.CheckSchemaIsInRange("ByteLength", _byteLength, _byteLengthMinimum, int.MaxValue);
+            // ByteStride must defined only with BufferMode.ARRAY_BUFFER, be multiple of 4, and between 4 and 252
+            if (!_byteStride.HasValue) return;
 
-            // ByteStride must be multiple of 4, between 4 and 252
-            if (_byteStride.HasValue)
-            {
-                result.CheckSchemaIsInRange(nameof(ByteStride), _byteStride.Value, _byteStrideMinimum, _byteStrideMaximum);
-                result.CheckSchemaIsMultipleOf(nameof(ByteStride), _byteStride.Value, 4);
-            }
+            validate
+                .IsAnyOf("Target", _target, null,  BufferMode.ARRAY_BUFFER)
+                .IsInRange(nameof(ByteStride), _byteStride.Value, _byteStrideMinimum, _byteStrideMaximum)
+                .IsMultipleOf(nameof(ByteStride), _byteStride.Value, 4);
         }
 
-        protected override void OnValidate(Validation.ValidationContext result)
+        protected override void OnValidateContent(Validation.ValidationContext validate)
         {
-            base.OnValidate(result);
+            base.OnValidateContent(validate);
 
             var buffer = this.LogicalParent.LogicalBuffers[this._buffer];
             var bcontent = buffer.Content;
 
-            result.CheckArrayRangeAccess("ByteOffset", _byteOffset, _byteLength, buffer.Content);
-
-            if (ByteStride > _byteLength) result.AddSemanticError(nameof(ByteStride), $"value ({ByteStride}) is larger than byteLength ({_byteLength}).");
-
-            // if (this.DeviceBufferTarget.HasValue && this.FindAccessors().Any(item => item.IsSparse)) result.AddError()
+            validate
+                .IsNullOrInRange("ByteOffset", _byteOffset, _byteLength, buffer.Content)
+                .IsLessOrEqual(nameof(ByteStride), ByteStride, _byteLength);
         }
 
-        internal void ValidateBufferUsageGPU(Validation.ValidationContext result, BufferMode usingMode)
+        internal void ValidateBufferUsageGPU(Validation.ValidationContext validate, BufferMode usingMode)
         {
-            result = result.GetContext(this);
+            validate = validate.GetContext(this);
 
-            if (!this._target.HasValue) return;
-            if (usingMode == this._target.Value) return;
-
-            result.AddLinkError("Device Buffer Target", $"is set as {this._target.Value}. But an accessor wants to use it as '{usingMode}'.");
+            if (this._target.HasValue) validate.EnumsAreEqual(nameof(_target), _target.Value, usingMode);
         }
 
-        internal void ValidateBufferUsagePlainData(Validation.ValidationContext result)
+        internal void ValidateBufferUsagePlainData(Validation.ValidationContext validate)
         {
             /*
             if (this._byteStride.HasValue)
@@ -279,14 +257,13 @@ namespace SharpGLTF.Schema2
                 }
             }*/
 
-            result = result.GetContext(this);
+            validate = validate.GetContext(this);
 
             if (!this._target.HasValue) return;
 
-            if (result.TryFixLinkOrError("Device Buffer Target", $"is set as {this._target.Value}. But an accessor wants to use it as a plain data buffer."))
-            {
-                this._target = null;
-            }
+            if (validate.TryFix) this._target = null;
+
+            validate.IsUndefined(nameof(_target), this._target);
         }
 
         #endregion

+ 39 - 0
src/SharpGLTF.Core/Schema2/gltf.Camera.cs

@@ -89,6 +89,23 @@ namespace SharpGLTF.Schema2
         }
 
         #endregion
+
+        #region validation
+
+        protected override void OnValidateReferences(Validation.ValidationContext validate)
+        {
+            base.OnValidateReferences(validate);
+
+            if (_orthographic == null && _perspective == null) validate._LinkThrow("perspective", "Missing orthographic or perspective");
+
+            if (_orthographic != null && _perspective != null)
+            {
+                if (validate.TryFix) _orthographic = null;
+                else validate._LinkThrow("perspective", "orthographic and perspective are mutually exclusive");
+            }
+        }
+
+        #endregion
     }
 
     /// <summary>
@@ -166,6 +183,17 @@ namespace SharpGLTF.Schema2
         }
 
         #endregion
+
+        #region validation
+
+        protected override void OnValidateContent(Validation.ValidationContext validate)
+        {
+            base.OnValidateContent(validate);
+
+            validate.IsGreater(nameof(ZFar), ZFar, ZNear); // "ZFar must be greater than ZNear");
+        }
+
+        #endregion
     }
 
     [System.Diagnostics.DebuggerDisplay("Perspective {AspectRatio} {VerticalFOV}   {ZNear} < {ZFar}")]
@@ -237,6 +265,17 @@ namespace SharpGLTF.Schema2
         }
 
         #endregion
+
+        #region validation
+
+        protected override void OnValidateContent(Validation.ValidationContext validate)
+        {
+            base.OnValidateContent(validate);
+
+            validate.IsGreater(nameof(ZFar), ZFar, ZNear); // "ZFar must be greater than ZNear");
+        }
+
+        #endregion
     }
 
     public partial class ModelRoot

+ 3 - 3
src/SharpGLTF.Core/Schema2/gltf.ExtensionsFactory.cs

@@ -52,13 +52,13 @@ namespace SharpGLTF.Schema2
         {
             var ptype = parent.GetType();
 
-            var entry = _Extensions.FirstOrDefault(item => item.Name == key && item.ParentType == ptype);
+            var (name, parentType, extType) = _Extensions.FirstOrDefault(item => item.Name == key && item.ParentType == ptype);
 
-            if (entry.Name == null) return null;
+            if (name == null) return null;
 
             var instance = Activator.CreateInstance
                 (
-                entry.ExtType,
+                extType,
                 System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
                 null,
                 new Object[] { parent },

+ 13 - 8
src/SharpGLTF.Core/Schema2/gltf.ExtraProperties.cs

@@ -126,23 +126,28 @@ namespace SharpGLTF.Schema2
         {
             base.OnValidateReferences(result);
 
+            foreach (var lc in this.GetLogicalChildren())
+            {
+                lc.ValidateReferences(result);
+            }
+
             foreach (var ext in this.Extensions) ext.ValidateReferences(result);
 
             if (this._extras is JsonSerializable js) js.ValidateReferences(result);
         }
 
-        protected override void OnValidate(Validation.ValidationContext result)
+        protected override void OnValidateContent(Validation.ValidationContext validate)
         {
-            base.OnValidate(result);
-
-            foreach (var ext in this.Extensions) ext.Validate(result);
+            base.OnValidateContent(validate);
 
-            if (this._extras is JsonSerializable js) js.Validate(result);
-
-            if (this._extras != null)
+            foreach (var lc in this.GetLogicalChildren())
             {
-                result.CheckSchemaIsJsonSerializable("Extras", this._extras);
+                lc.ValidateContent(validate);
             }
+
+            if (this._extras is JsonSerializable js) js.ValidateContent(validate);
+
+            if (this._extras != null) validate.IsJsonSerializable("Extras", this._extras);
         }
 
         #endregion

+ 21 - 13
src/SharpGLTF.Core/Schema2/gltf.Images.cs

@@ -150,8 +150,6 @@ namespace SharpGLTF.Schema2
             var imimg = new Memory.MemoryImage(content);
             if (!imimg.IsValid) throw new ArgumentException($"{nameof(content)} must be a PNG, JPG, DDS or WEBP image", nameof(content));
 
-            string imageType = imimg.MimeType;
-
             _DiscardContent();
 
             this._SatelliteImageContent = content;
@@ -183,7 +181,16 @@ namespace SharpGLTF.Schema2
         {
             if (String.IsNullOrWhiteSpace(_uri)) return;
 
-            var data = Memory.MemoryImage.TryParseBytes(_uri);
+            byte[] data = null;
+
+            try
+            {
+                data = Memory.MemoryImage.TryParseBytes(_uri);
+            }
+            catch (ArgumentException argex)
+            {
+                throw new Validation.DataException(this, argex.Message);
+            }
 
             if (data == null)
             {
@@ -270,24 +277,25 @@ namespace SharpGLTF.Schema2
 
         #region Validation
 
-        protected override void OnValidateReferences(Validation.ValidationContext result)
+        protected override void OnValidateReferences(Validation.ValidationContext validate)
         {
-            base.OnValidateReferences(result);
-
-            result.CheckSchemaIsValidURI("Uri", this._uri, Memory.MemoryImage._EmbeddedHeaders);
+            base.OnValidateReferences(validate);
 
-            result.CheckArrayIndexAccess("BufferView", _bufferView, this.LogicalParent.LogicalBufferViews);
+            validate
+                .IsNullOrValidURI(nameof(_uri), this._uri, Memory.MemoryImage._EmbeddedHeaders)
+                .IsNullOrIndex("BufferView", _bufferView, validate.Root.LogicalBufferViews);
         }
 
-        protected override void OnValidate(ValidationContext result)
+        protected override void OnValidateContent(ValidationContext validate)
         {
-            base.OnValidate(result);
-
             if (this._bufferView.HasValue)
             {
-                var bv = result.Root.LogicalBufferViews[this._bufferView ?? 0];
-                if (!bv.IsDataBuffer) result.AddLinkError(("BufferView", this._bufferView), "is a GPU target.");
+                var bv = validate.Root.LogicalBufferViews[this._bufferView ?? 0];
+
+                validate.IsTrue("BufferView", bv.IsDataBuffer, "is a GPU target.");
             }
+
+            validate.IsTrue("MemoryImage", MemoryImage.IsValid, "Invalid image");
         }
 
         #endregion

+ 14 - 0
src/SharpGLTF.Core/Schema2/gltf.Material.cs

@@ -95,6 +95,20 @@ namespace SharpGLTF.Schema2
         }
 
         #endregion
+
+        #region validation
+
+        protected override void OnValidateContent(Validation.ValidationContext result)
+        {
+            base.OnValidateContent(result);
+
+            var shaderCount = 0;
+            // if (_pbrMetallicRoughness != null) ++shaderCount; this is the fallback
+            if (this.GetExtension<MaterialPBRSpecularGlossiness>() != null) ++shaderCount;
+            if (this.GetExtension<MaterialUnlit>() != null) ++shaderCount;
+        }
+
+        #endregion
     }
 
     public partial class ModelRoot

+ 21 - 28
src/SharpGLTF.Core/Schema2/gltf.Mesh.cs

@@ -46,7 +46,7 @@ namespace SharpGLTF.Schema2
 
         public IReadOnlyList<MeshPrimitive> Primitives => _primitives;
 
-        public IReadOnlyList<Single> MorphWeights => _weights.Count == 0 ? null : _weights.Select(item => (Single)item).ToList();
+        public IReadOnlyList<Single> MorphWeights => GetMorphWeights();
 
         public bool AllPrimitivesHaveJoints => Primitives.All(p => p.GetVertexAccessor("JOINTS_0") != null);
 
@@ -54,20 +54,18 @@ namespace SharpGLTF.Schema2
 
         #region API
 
+        public IReadOnlyList<Single> GetMorphWeights()
+        {
+            if (_weights == null || _weights.Count == 0) return Array.Empty<Single>();
+
+            return _weights.Select(item => (float)item).ToList();
+        }
+
         public void SetMorphWeights(Transforms.SparseWeight8 weights)
         {
             int count = _primitives.Max(item => item.MorphTargetsCount);
 
-            while (_weights.Count > count) _weights.RemoveAt(_weights.Count - 1);
-            while (_weights.Count < count) _weights.Add(0);
-
-            if (_weights.Count > 0)
-            {
-                foreach (var kw in weights.GetIndexedWeights())
-                {
-                    _weights[kw.Index] = kw.Weight;
-                }
-            }
+            _weights.SetMorphWeights(count, weights);
         }
 
         protected override IEnumerable<ExtraProperties> GetLogicalChildren()
@@ -93,34 +91,29 @@ namespace SharpGLTF.Schema2
 
         #region Validation
 
-        protected override void OnValidateReferences(Validation.ValidationContext result)
+        protected override void OnValidateReferences(Validation.ValidationContext validate)
         {
-            base.OnValidateReferences(result);
-            
-            if (this.Primitives.Count == 0)
-            {
-                result.AddSchemaError("Primitives must be defined");
-                return;
-            }
-
-            result.CheckLinksInCollection("Primitives", _primitives);
+            validate
+                .IsGreater(nameof(Primitives), this.Primitives.Count, 0)
+                .IsSetCollection(nameof(Primitives), _primitives);
 
-            foreach (var p in this.Primitives) p.ValidateReferences(result);
+            base.OnValidateReferences(validate);
         }
 
-        protected override void OnValidate(Validation.ValidationContext result)
+        protected override void OnValidateContent(Validation.ValidationContext validate)
         {
-            base.OnValidate(result);
-
-            foreach (var p in this.Primitives) p.Validate(result);
+            base.OnValidateContent(validate);
 
             var morphTargetsCount = this.Primitives
                 .Select(item => item.MorphTargetsCount)
                 .Distinct();
 
-            if (morphTargetsCount.Count() != 1) result.AddSemanticError("Count", "All primitives must have the same number of morph targets.");
+            validate.AreEqual("Morph targets", morphTargetsCount.Count(), 1);
 
-            if (_weights.Count != 0 && morphTargetsCount.First() != _weights.Count) result.AddSemanticError("Weights", $"The length of weights array ({_weights.Count}) does not match the number of morph targets({morphTargetsCount.First()}).");
+            if (_weights.Count != 0 && this.Primitives.Count > 0)
+            {
+                validate.AreEqual("Morph targets", _weights.Count, this.Primitives[0].MorphTargetsCount);
+            }
         }
 
         #endregion

+ 15 - 27
src/SharpGLTF.Core/Schema2/gltf.MeshPrimitive.cs

@@ -252,29 +252,30 @@ namespace SharpGLTF.Schema2
 
         #region validation
 
-        protected override void OnValidateReferences(Validation.ValidationContext result)
+        protected override void OnValidateReferences(Validation.ValidationContext validate)
         {
-            base.OnValidateReferences(result);
+            base.OnValidateReferences(validate);
 
             var root = this.LogicalParent.LogicalParent;
 
-            result.CheckArrayIndexAccess("Material", _material, root.LogicalMaterials);
-            result.CheckArrayIndexAccess("Indices", _indices, root.LogicalAccessors);
+            validate
+                .IsNullOrIndex("Material", _material, root.LogicalMaterials)
+                .IsNullOrIndex("Indices", _indices, root.LogicalAccessors);
 
             foreach (var idx in _attributes.Values)
             {
-                result.CheckArrayIndexAccess("Attributes", idx, root.LogicalAccessors);
+                validate.IsNullOrIndex("Attributes", idx, root.LogicalAccessors);
             }
 
             foreach (var idx in _targets.SelectMany(item => item.Values))
             {
-                result.CheckArrayIndexAccess("Targets", idx, root.LogicalAccessors);
+                validate.IsNullOrIndex("Targets", idx, root.LogicalAccessors);
             }
         }
 
-        protected override void OnValidate(Validation.ValidationContext result)
+        protected override void OnValidateContent(Validation.ValidationContext validate)
         {
-            base.OnValidate(result);
+            base.OnValidateContent(validate);
 
             // all vertices must have the same vertex count
 
@@ -284,7 +285,7 @@ namespace SharpGLTF.Schema2
 
             if (vertexCounts.Skip(1).Any())
             {
-                result.AddLinkError("Attributes", "All accessors of the same primitive must have the same count.");
+                validate._LinkThrow("Attributes", "All accessors of the same primitive must have the same count.");
                 return;
             }
 
@@ -294,8 +295,7 @@ namespace SharpGLTF.Schema2
 
             if (IndexAccessor != null)
             {
-                if (IndexAccessor.SourceBufferView.ByteStride != 0) result.AddLinkError("bufferView.byteStride must not be defined for indices accessor.");
-                IndexAccessor.ValidateIndices(result, (uint)vertexCount, DrawPrimitiveType);
+                IndexAccessor.ValidateIndices(validate, (uint)vertexCount, DrawPrimitiveType);
 
                 var incompatibleMode = false;
 
@@ -319,8 +319,6 @@ namespace SharpGLTF.Schema2
                         if (!IndexAccessor.Count.IsMultipleOf(3)) incompatibleMode = true;
                         break;
                 }
-
-                if (incompatibleMode) result.AddLinkWarning("Indices", $"Number of vertices or indices({IndexAccessor.Count}) is not compatible with used drawing mode('{this.DrawPrimitiveType}').");
             }
 
             // check vertex attributes accessors
@@ -331,11 +329,7 @@ namespace SharpGLTF.Schema2
                 {
                     // if more than one accessor shares a BufferView, it must define a ByteStride
 
-                    if (group.Key.ByteStride == 0)
-                    {
-                        result.GetContext(group.Key).AddLinkError("ByteStride", " must be defined when two or more accessors use the same BufferView.");
-                        return;
-                    }
+                    validate.IsGreater("ByteStride", group.Key.ByteStride, 0); // " must be defined when two or more accessors use the same BufferView."
 
                     // now, there's two possible outcomes:
                     // - sequential accessors: all accessors's ElementByteStride should be EQUAL to BufferView's ByteStride
@@ -352,20 +346,14 @@ namespace SharpGLTF.Schema2
                     else
                     {
                         var accessors = string.Join(" ", group.Select(item => item.LogicalIndex));
-                        result.AddLinkError("Attributes", $"Inconsistent accessors configuration: {accessors}");
+                        validate._LinkThrow("Attributes", $"Inconsistent accessors configuration: {accessors}");
                     }
                 }
             }
 
-            if (DrawPrimitiveType == PrimitiveType.POINTS)
-            {
-                if (this.VertexAccessors.Keys.Contains("NORMAL")) result.AddSemanticWarning("NORMAL", $"attribute defined for {DrawPrimitiveType} rendering mode.");
-                if (this.VertexAccessors.Keys.Contains("TANGENT")) result.AddSemanticWarning("TANGENT", $"attribute defined for {DrawPrimitiveType} rendering mode.");
-            }
-
             // check vertex attributes
 
-            if (result.TryFix)
+            if (validate.TryFix)
             {
                 var vattributes = this.VertexAccessors
                 .Select(item => item.Value._GetMemoryAccessor(item.Key))
@@ -386,7 +374,7 @@ namespace SharpGLTF.Schema2
 
             var maxJoints = skins.Any() ? skins.Max() : 0;
 
-            Accessor.ValidateVertexAttributes(result, this.VertexAccessors, maxJoints);
+            Accessor.ValidateVertexAttributes(validate, this.VertexAccessors, maxJoints);
         }
 
         #endregion

+ 62 - 51
src/SharpGLTF.Core/Schema2/gltf.Node.cs

@@ -177,17 +177,9 @@ namespace SharpGLTF.Schema2
         }
 
         /// <summary>
-        /// Gets or sets the Morph Weights of this <see cref="Node"/>.
+        /// Gets the Morph Weights of this <see cref="Node"/>.
         /// </summary>
-        public IReadOnlyList<Single> MorphWeights
-        {
-            get => _weights.Count == 0 ? Mesh?.MorphWeights : _weights.Select(item => (float)item).ToList();
-            set
-            {
-                _weights.Clear();
-                if (value != null) _weights.AddRange(value.Select(item => (Double)item));
-            }
-        }
+        public IReadOnlyList<Single> MorphWeights => GetMorphWeights();
 
         #endregion
 
@@ -281,6 +273,24 @@ namespace SharpGLTF.Schema2
             return vs == null ? lm : Transforms.AffineTransform.LocalToWorld(vs.GetWorldMatrix(animation, time), lm);
         }
 
+        public IReadOnlyList<Single> GetMorphWeights()
+        {
+            if (!_mesh.HasValue) return Array.Empty<Single>();
+
+            if (_weights == null || _weights.Count == 0) return Mesh.MorphWeights;
+
+            return _weights.Select(item => (float)item).ToList();
+        }
+
+        public void SetMorphWeights(Transforms.SparseWeight8 weights)
+        {
+            Guard.IsTrue(_mesh.HasValue, nameof(weights), "Nodes with no mesh cannot have morph weights");
+
+            int count = Mesh.Primitives.Max(item => item.MorphTargetsCount);
+
+            _weights.SetMorphWeights(count, weights);
+        }
+
         #endregion
 
         #region API - hierarchy
@@ -372,52 +382,51 @@ namespace SharpGLTF.Schema2
 
         #region validation
 
-        protected override void OnValidateReferences(Validation.ValidationContext result)
+        protected override void OnValidateReferences(Validation.ValidationContext validate)
         {
-            base.OnValidateReferences(result);
+            base.OnValidateReferences(validate);
 
             // check out of range indices
             foreach (var idx in this._children)
             {
-                result.CheckArrayIndexAccess(nameof(VisualChildren), idx, this.LogicalParent.LogicalNodes);
+                validate.IsNullOrIndex(nameof(VisualChildren), idx, this.LogicalParent.LogicalNodes);
             }
 
-            result.CheckArrayIndexAccess(nameof(Mesh), _mesh, this.LogicalParent.LogicalMeshes);
-            result.CheckArrayIndexAccess(nameof(Skin), _skin, this.LogicalParent.LogicalSkins);
-            result.CheckArrayIndexAccess(nameof(Camera), _camera, this.LogicalParent.LogicalCameras);
+            validate
+                .IsNullOrIndex(nameof(Mesh), _mesh, this.LogicalParent.LogicalMeshes)
+                .IsNullOrIndex(nameof(Skin), _skin, this.LogicalParent.LogicalSkins)
+                .IsNullOrIndex(nameof(Camera), _camera, this.LogicalParent.LogicalCameras);
         }
 
-        protected override void OnValidate(Validation.ValidationContext result)
+        protected override void OnValidateContent(Validation.ValidationContext validate)
         {
-            base.OnValidate(result);
+            base.OnValidateContent(validate);
 
-            _ValidateHierarchy(result);
-            _ValidateTransforms(result);
-            _ValidateMeshAndSkin(result, Mesh, Skin);
+            _ValidateHierarchy(validate);
+            _ValidateTransforms(validate);
+            _ValidateMeshAndSkin(validate, Mesh, Skin, _weights);
         }
 
-        private void _ValidateHierarchy(Validation.ValidationContext result)
+        private void _ValidateHierarchy(Validation.ValidationContext validate)
         {
             var allNodes = this.LogicalParent.LogicalNodes;
 
-            var thisIndex = this.LogicalIndex;
-
-            var pidx = thisIndex;
+            var pidx = this.LogicalIndex;
 
             var sequence = new List<int>();
 
             while (true)
             {
+                validate = validate.GetContext(allNodes[pidx]);
+
                 if (sequence.Contains(pidx))
                 {
-                    result.AddLinkError("is a part of a node loop.");
+                    validate._LinkThrow("Node", "is a part of a node loop.");
                     break;
                 }
 
                 sequence.Add(pidx);
 
-                result = result.GetContext(result.Root.LogicalNodes[pidx]);
-
                 // every node must have 0 or 1 parents.
 
                 var allParents = allNodes
@@ -426,44 +435,46 @@ namespace SharpGLTF.Schema2
 
                 if (allParents.Count == 0) break; // we're already root
 
-                if (allParents.Count > 1)
-                {
-                    var parents = string.Join(" ", allParents);
-
-                    result.AddLinkError($"is child of nodes {parents}. A node can only have one parent.");
-                    break;
-                }
+                validate.IsLessOrEqual((nameof(Node), pidx), allParents.Count, 1); // must have 1 parent
 
-                if (allParents[0].LogicalIndex == pidx)
-                {
-                    result.AddLinkError("is a part of a node loop.");
-                    break;
-                }
+                // validate.AreEqual((nameof(Node), pidx), allParents[0].LogicalIndex, pidx); // part of node loop
 
                 pidx = allParents[0].LogicalIndex;
             }
         }
 
-        private void _ValidateTransforms(Validation.ValidationContext result)
+        private void _ValidateTransforms(Validation.ValidationContext validate)
         {
-            result.CheckIsFinite("Scale", _scale);
-            result.CheckIsFinite("Rotation", _rotation);
-            result.CheckIsFinite("Translation", _translation);
-            result.CheckIsMatrix("Matrix", _matrix);
+            if (_matrix.HasValue)
+            {
+                validate
+                    .IsUndefined(nameof(_scale), _scale)
+                    .IsUndefined(nameof(_rotation), _rotation)
+                    .IsUndefined(nameof(_translation), _translation);
+            }
+
+            validate
+                .IsNullOrPosition("Scale", _scale)
+                .IsNullOrRotation("Rotation", _rotation)
+                .IsNullOrPosition("Translation", _translation)
+                .IsNullOrMatrix("Rotation", _matrix);
         }
 
-        private static void _ValidateMeshAndSkin(Validation.ValidationContext result, Mesh mesh, Skin skin)
+        private static void _ValidateMeshAndSkin(Validation.ValidationContext validate, Mesh mesh, Skin skin, List<Double> weights)
         {
-            if (mesh == null && skin == null) return;
+            var wcount = weights == null ? 0 : weights.Count;
+
+            if (mesh == null && skin == null && wcount == 0) return;
 
-            if (mesh != null)
+            if (skin != null)
             {
-                if (skin == null && mesh.AllPrimitivesHaveJoints) result.AddLinkWarning("Skin", "Node uses skinned mesh, but has no skin defined.");
+                validate.IsDefined("Mesh", mesh);
+                validate.IsTrue("Mesh", mesh.AllPrimitivesHaveJoints, "Node has skin defined, but mesh has no joints data.");
             }
 
-            if (skin != null)
+            if (mesh == null)
             {
-                if (mesh == null || !mesh.AllPrimitivesHaveJoints) result.AddLinkError("Mesh", "Node has skin defined, but mesh has no joints data.");
+                validate.AreEqual("weights", wcount, 0); // , "Morph weights require a mesh."
             }
         }
 

+ 13 - 55
src/SharpGLTF.Core/Schema2/gltf.Root.cs

@@ -149,71 +149,29 @@ namespace SharpGLTF.Schema2
 
         #region validation
 
-        protected override void OnValidateReferences(Validation.ValidationContext result)
+        protected override void OnValidateReferences(Validation.ValidationContext validate)
         {
-            if (Asset == null) result.AddSchemaError(nameof(Asset), "is missing");
+            validate
+                .NotNull(nameof(Asset), this.Asset)
+                .IsNullOrIndex(nameof(DefaultScene), _scene, this.LogicalScenes);
 
-            result.CheckArrayIndexAccess(nameof(DefaultScene), _scene, this.LogicalScenes);
-
-            foreach (var b in _buffers) b.ValidateReferences(result);
-            foreach (var v in _bufferViews) v.ValidateReferences(result);
-            foreach (var a in _accessors) a.ValidateReferences(result);
-
-            foreach (var i in _images) i.ValidateReferences(result);
-            foreach (var s in _samplers) s.ValidateReferences(result);
-            foreach (var t in _textures) t.ValidateReferences(result);
-            foreach (var m in _materials) m.ValidateReferences(result);
-
-            foreach (var m in _meshes) m.ValidateReferences(result);
-            foreach (var s in _skins) s.ValidateReferences(result);
-            foreach (var c in _cameras) c.ValidateReferences(result);
-
-            foreach (var n in _nodes) n.ValidateReferences(result);
-            foreach (var s in _scenes) s.ValidateReferences(result);
-            foreach (var a in _animations) a.ValidateReferences(result);
-
-            base.OnValidateReferences(result);
-        }
-
-        protected override void OnValidate(Validation.ValidationContext result)
-        {
-            // 1st check version number
-
-            Asset.Validate(result);
-
-            if (result.Result.HasErrors) return;
-
-            // 2nd check incompatible extensions
+            // check incompatible extensions
 
             foreach (var iex in this.IncompatibleExtensions)
             {
-                result.UnsupportedExtensionError(iex);
+                validate._LinkThrow("Extensions", iex);
             }
 
-            if (result.Result.HasErrors) return;
-
-            // 3rd check base class
-
-            base.OnValidate(result);
-
-            // 4th check contents
-
-            foreach (var b in _buffers) b.Validate(result);
-            foreach (var v in _bufferViews) v.Validate(result);
-            foreach (var a in _accessors) a.Validate(result);
+            base.OnValidateReferences(validate);
+        }
 
-            foreach (var i in _images) i.Validate(result);
-            foreach (var s in _samplers) s.Validate(result);
-            foreach (var t in _textures) t.Validate(result);
-            foreach (var m in _materials) m.Validate(result);
+        protected override void OnValidateContent(Validation.ValidationContext validate)
+        {
+            // 1st check version number
 
-            foreach (var m in _meshes) m.Validate(result);
-            foreach (var s in _skins) s.Validate(result);
-            foreach (var c in _cameras) c.Validate(result);
+            Asset.ValidateContent(validate);
 
-            foreach (var n in _nodes) n.Validate(result);
-            foreach (var s in _scenes) s.Validate(result);
-            foreach (var a in _animations) a.Validate(result);
+            base.OnValidateContent(validate);
         }
 
         #endregion

+ 9 - 3
src/SharpGLTF.Core/Schema2/gltf.Scene.cs

@@ -61,14 +61,20 @@ namespace SharpGLTF.Schema2
 
         #region Validation
 
-        protected override void OnValidateReferences(Validation.ValidationContext result)
+        protected override void OnValidateReferences(Validation.ValidationContext validate)
         {
-            base.OnValidateReferences(result);
+            base.OnValidateReferences(validate);
 
             // check out of range indices
             foreach (var idx in this._nodes)
             {
-                result.CheckArrayIndexAccess( nameof(VisualChildren), idx, this.LogicalParent.LogicalNodes);
+                validate.IsNullOrIndex(nameof(VisualChildren), idx, this.LogicalParent.LogicalNodes);
+            }
+
+            // checks if a root node is being used as a child.
+            foreach (var node in this.LogicalParent.LogicalNodes)
+            {
+                if (this._nodes.Any(ridx => node._HasVisualChild(ridx))) validate.GetContext(node)._LinkThrow("Children", "Root nodes cannot be children.");
             }
 
             // check duplicated indices

+ 19 - 10
src/SharpGLTF.Core/Schema2/gltf.Skin.cs

@@ -238,13 +238,13 @@ namespace SharpGLTF.Schema2
 
         #region validation
 
-        protected override void OnValidateReferences(Validation.ValidationContext result)
+        protected override void OnValidateReferences(Validation.ValidationContext validate)
         {
-            base.OnValidateReferences(result);
+            base.OnValidateReferences(validate);
 
-            result.CheckArrayIndexAccess("Skeleton", _skeleton, this.LogicalParent.LogicalNodes);
-
-            result.CheckArrayIndexAccess("InverseBindMatrices", _inverseBindMatrices, this.LogicalParent.LogicalAccessors);
+            validate
+                .IsNullOrIndex("Skeleton", _skeleton, this.LogicalParent.LogicalNodes)
+                .IsNullOrIndex("InverseBindMatrices", _inverseBindMatrices, this.LogicalParent.LogicalAccessors);
 
             if (_joints.Count < _jointsMinItems)
             {
@@ -252,25 +252,34 @@ namespace SharpGLTF.Schema2
                 return;
             }
 
+            Node commonRoot = null;
+
             for (int i = 0; i < _joints.Count; ++i)
             {
                 var jidx = _joints[i];
 
-                result.CheckArrayIndexAccess("Joints", _joints[i], this.LogicalParent.LogicalNodes);
+                validate.IsNullOrIndex("Joints", jidx, this.LogicalParent.LogicalNodes);
+
+                var jnode = this.LogicalParent.LogicalNodes[jidx];
+                var jroot = jnode.VisualRoot;
+
+                if (commonRoot == null) { commonRoot = jroot; continue; }
+
+                validate.GetContext(jroot).AreSameReference("Root", commonRoot, jroot);
             }
         }
 
-        protected override void OnValidate(Validation.ValidationContext result)
+        protected override void OnValidateContent(Validation.ValidationContext validate)
         {
-            base.OnValidate(result);
+            base.OnValidateContent(validate);
 
             var ibxAccessor = GetInverseBindMatricesAccessor();
 
             if (ibxAccessor != null)
             {
-                if (_joints.Count != ibxAccessor.Count) result.AddLinkError("InverseBindMatrices", $"has {ibxAccessor.Count} matrices. But expected {_joints.Count}.");
+                validate.AreEqual("InverseBindMatrices", _joints.Count, ibxAccessor.Count);
 
-                ibxAccessor.ValidateMatrices(result);
+                ibxAccessor.ValidateMatrices(validate);
             }
 
             if (_skeleton.HasValue)

+ 11 - 0
src/SharpGLTF.Core/Schema2/gltf.TextureInfo.cs

@@ -47,6 +47,17 @@ namespace SharpGLTF.Schema2
         }
 
         #endregion
+
+        #region Validation
+
+        protected override void OnValidateReferences(Validation.ValidationContext validate)
+        {
+            validate.IsNullOrIndex("Index", _index, validate.Root.LogicalTextures);
+
+            base.OnValidateReferences(validate);
+        }
+
+        #endregion
     }
 
     [System.Diagnostics.DebuggerDisplay("TextureTransform {Offset} {Scale} {Rotation} {TextureCoordinate}")]

+ 7 - 6
src/SharpGLTF.Core/Schema2/gltf.Textures.cs

@@ -87,7 +87,7 @@ namespace SharpGLTF.Schema2
             Guard.NotNull(primaryImage, nameof(primaryImage));
             Guard.MustShareLogicalParent(this, primaryImage, nameof(primaryImage));
 
-            if (primaryImage.IsDds || primaryImage.IsWebp)
+            if (primaryImage.MemoryImage.IsDds || primaryImage.MemoryImage.IsWebp)
             {
                 var fallback = LogicalParent.UseImage(Memory.MemoryImage.DefaultPngImage.Slice(0));
                 SetImages(primaryImage, fallback);
@@ -152,12 +152,13 @@ namespace SharpGLTF.Schema2
 
         #region Validation
 
-        protected override void OnValidateReferences(ValidationContext result)
+        protected override void OnValidateReferences(ValidationContext validate)
         {
-            base.OnValidateReferences(result);
+            base.OnValidateReferences(validate);
 
-            result.CheckArrayIndexAccess("Source", _source, this.LogicalParent.LogicalImages);
-            result.CheckArrayIndexAccess("Sampler", _sampler, this.LogicalParent.LogicalTextureSamplers);
+            validate
+                .IsNullOrIndex("Source", _source, this.LogicalParent.LogicalImages)
+                .IsNullOrIndex("Sampler", _sampler, this.LogicalParent.LogicalTextureSamplers);
         }
 
         #endregion
@@ -205,7 +206,7 @@ namespace SharpGLTF.Schema2
                 if (value != null)
                 {
                     Guard.MustShareLogicalParent(_Parent, value, nameof(value));
-                    Guard.IsTrue(value.IsWebp, nameof(value));
+                    Guard.IsTrue(value.MemoryImage.IsWebp, nameof(value));
                 }
 
                 _source = value?.LogicalIndex;

+ 37 - 3
src/SharpGLTF.Core/Schema2/khr.lights.cs

@@ -128,7 +128,7 @@ namespace SharpGLTF.Schema2
         /// <summary>
         /// Gets the type of light.
         /// </summary>
-        public PunctualLightType LightType => (PunctualLightType)Enum.Parse(typeof(PunctualLightType), _type, true);
+        public PunctualLightType LightType => string.IsNullOrEmpty(_type) ? PunctualLightType.Directional : (PunctualLightType)Enum.Parse(typeof(PunctualLightType), _type, true);
 
         /// <summary>
         /// Gets the Angle, in radians, from centre of spotlight where falloff begins.
@@ -179,6 +179,28 @@ namespace SharpGLTF.Schema2
         }
 
         #endregion
+
+        #region Validation
+
+        protected override void OnValidateReferences(Validation.ValidationContext validate)
+        {
+            base.OnValidateReferences(validate);
+
+            if (string.IsNullOrEmpty(_type)) { validate._SchemaThrow("Type", "light Type must be defined"); return; }
+
+            if (LightType == PunctualLightType.Spot) validate.IsDefined("Spot", _spot);
+
+            _spot?.ValidateReferences(validate);
+        }
+
+        protected override void OnValidateContent(Validation.ValidationContext validate)
+        {
+            base.OnValidateContent(validate);
+
+            _spot?.ValidateContent(validate);
+        }
+
+        #endregion
     }
 
     partial class PunctualLightSpot
@@ -186,13 +208,25 @@ namespace SharpGLTF.Schema2
         public Single InnerConeAngle
         {
             get => (Single)_innerConeAngle.AsValue(_innerConeAngleDefault);
-            set => _innerConeAngle = ((Double)value).AsNullable(_innerConeAngleDefault, _innerConeAngleMinimum, _innerConeAngleMaximum);
+            set => _innerConeAngle = value.AsNullable((Single)_innerConeAngleDefault, (Single)_innerConeAngleMinimum, (Single)_innerConeAngleMaximum);
         }
 
         public Single OuterConeAngle
         {
             get => (Single)_outerConeAngle.AsValue(_outerConeAngleDefault);
-            set => _outerConeAngle = ((Double)value).AsNullable(_outerConeAngleDefault, _outerConeAngleMinimum, _outerConeAngleMaximum);
+            set => _outerConeAngle = value.AsNullable((Single)_outerConeAngleDefault, (Single)_outerConeAngleMinimum, (Single)_outerConeAngleMaximum);
+        }
+
+        protected override void OnValidateContent(Validation.ValidationContext validate)
+        {
+            validate
+                .IsGreaterOrEqual(nameof(InnerConeAngle), InnerConeAngle, (Single)_innerConeAngleMinimum)
+                .IsLessOrEqual(nameof(InnerConeAngle), InnerConeAngle, (Single)_innerConeAngleMaximum)
+                .IsGreaterOrEqual(nameof(OuterConeAngle), OuterConeAngle, (Single)_outerConeAngleMinimum)
+                .IsLessOrEqual(nameof(OuterConeAngle), OuterConeAngle, (Single)_outerConeAngleMaximum)
+                .IsLess(nameof(InnerConeAngle), InnerConeAngle, OuterConeAngle);
+
+            base.OnValidateContent(validate);
         }
     }
 

+ 7 - 5
src/SharpGLTF.Core/SharpGLTF.Core.csproj

@@ -8,16 +8,16 @@
     <DebugSymbols>true</DebugSymbols>    
   </PropertyGroup>  
   
-  <Import Project="..\Version.props" />
-  
+  <Import Project="..\Version.props" />  
   <Import Project="..\PackageInfo.props" />
+  <Import Project="..\Analyzers.props" />
   
   <ItemGroup>
     <Compile Include="..\Shared\Guard.cs" Link="Debug\Guard.cs" />
     <Compile Include="..\Shared\_Extensions.cs" Link="_Extensions.cs" />
   </ItemGroup>
 
-  <ItemGroup>
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
     <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
       <_Parameter1>SharpGLTF.Core.Tests</_Parameter1>
     </AssemblyAttribute>
@@ -35,7 +35,9 @@
       <ExcludeFromStyleCop>true</ExcludeFromStyleCop>
     </None>
   </ItemGroup>
-  
-  <Import Project="..\..\Analyzers.targets" />
+
+  <ItemGroup>
+    <None Include="..\..\.editorconfig" Link=".editorconfig" />
+  </ItemGroup>  
 
 </Project>

+ 332 - 0
src/SharpGLTF.Core/Validation/ValidationContext.Guards.cs

@@ -0,0 +1,332 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace SharpGLTF.Validation
+{
+    using OUTTYPE = ValidationContext;
+
+    using PARAMNAME = ValueLocation;
+
+    [System.Diagnostics.DebuggerDisplay("{_Current}")]
+    partial struct ValidationContext
+    {
+        private readonly IO.JsonSerializable _Current;
+
+        #region schema
+
+        [System.Diagnostics.DebuggerStepThrough]
+        internal void _SchemaThrow(PARAMNAME pname, string msg) { throw new SchemaException(_Current, $"{pname}: {msg}"); }
+
+        public OUTTYPE NotNull(PARAMNAME parameterName, object target)
+        {
+            if (target == null) _SchemaThrow(parameterName, "must not be null.");
+            return this;
+        }
+
+        public OUTTYPE IsDefined<T>(PARAMNAME parameterName, T value)
+            where T : class
+        {
+            if (value == null) _SchemaThrow(parameterName, "must be defined.");
+            return this;
+        }
+
+        public OUTTYPE IsDefined<T>(PARAMNAME parameterName, T? value)
+            where T : struct
+        {
+            if (!value.HasValue) _SchemaThrow(parameterName, "must be defined.");
+            return this;
+        }
+
+        public OUTTYPE IsTrue(PARAMNAME parameterName, bool value, string msg)
+        {
+            if (!value) _SchemaThrow(parameterName, msg);
+            return this;
+        }
+
+        public OUTTYPE IsUndefined<T>(PARAMNAME parameterName, T? value)
+            where T : struct
+        {
+            if (value.HasValue) _SchemaThrow(parameterName, "must NOT be defined.");
+            return this;
+        }
+
+        public OUTTYPE AreSameReference<TRef>(PARAMNAME parameterName, TRef value, TRef expected)
+            where TRef : class
+        {
+            if (!Object.ReferenceEquals(value, expected)) _SchemaThrow(parameterName, $"{value} and {expected} must be the same.");
+            return this;
+        }
+
+        public OUTTYPE AreEqual<TValue>(PARAMNAME parameterName, TValue value, TValue expected)
+                where TValue : IEquatable<TValue>
+        {
+            if (!value.Equals(expected)) _SchemaThrow(parameterName, $"{value} must be equal to {expected}.");
+            return this;
+        }
+
+        public OUTTYPE IsLess<TValue>(PARAMNAME parameterName, TValue value, TValue max)
+                where TValue : IComparable<TValue>
+        {
+            if (value.CompareTo(max) >= 0) _SchemaThrow(parameterName, $"{value} must be less than {max}.");
+            return this;
+        }
+
+        public OUTTYPE IsLessOrEqual<TValue>(PARAMNAME parameterName, TValue value, TValue max)
+                where TValue : IComparable<TValue>
+        {
+            if (value.CompareTo(max) > 0) _SchemaThrow(parameterName, $"{value} must be less or equal to {max}.");
+            return this;
+        }
+
+        public OUTTYPE IsGreater<TValue>(PARAMNAME parameterName, TValue value, TValue min)
+                where TValue : IComparable<TValue>
+        {
+            if (value.CompareTo(min) <= 0) _SchemaThrow(parameterName, $"{value} must be greater than {min}.");
+            return this;
+        }
+
+        public OUTTYPE IsGreaterOrEqual<TValue>(PARAMNAME parameterName, TValue value, TValue min)
+                where TValue : IComparable<TValue>
+        {
+            if (value.CompareTo(min) < 0) _SchemaThrow(parameterName, $"{value} must be greater or equal to {min}.");
+            return this;
+        }
+
+        public OUTTYPE IsMultipleOf(PARAMNAME parameterName, int value, int multiple)
+        {
+            if ((value % multiple) != 0) _SchemaThrow(parameterName, $"Value {value} is not a multiple of {multiple}.");
+            return this;
+        }
+
+        public OUTTYPE NonNegative(PARAMNAME parameterName, int? value)
+        {
+            if ((value ?? 0) < 0) _SchemaThrow(parameterName, "must be a non-negative integer.");
+            return this;
+        }
+
+        public OUTTYPE IsNullOrValidURI(PARAMNAME parameterName, string gltfURI, params string[] validHeaders)
+        {
+            if (gltfURI == null) return this;
+            return IsValidURI(parameterName, gltfURI, validHeaders);
+        }
+
+        public OUTTYPE IsValidURI(PARAMNAME parameterName, string gltfURI, params string[] validHeaders)
+        {
+            try { Guard.IsValidURI(parameterName, gltfURI, validHeaders); return this; }
+            catch (ArgumentException ex) { _SchemaThrow(parameterName, ex.Message); }
+            return this;
+        }
+
+        public OUTTYPE IsJsonSerializable(PARAMNAME parameterName, Object value)
+        {
+            if (!IO.JsonUtils.IsJsonSerializable(value)) _SchemaThrow(parameterName, "cannot be serialized to Json");
+            return this;
+        }
+
+        #endregion
+
+        #region link
+
+        [System.Diagnostics.DebuggerStepThrough]
+        internal void _LinkThrow(PARAMNAME pname, string msg) { throw new LinkException(_Current, $"{pname.ToString()}: {msg}"); }
+
+        public OUTTYPE EnumsAreEqual<TValue>(PARAMNAME parameterName, TValue value, TValue expected)
+                where TValue : Enum
+        {
+            if (!value.Equals(expected)) _LinkThrow(parameterName, $"{value} must be equal to {expected}.");
+            return this;
+        }
+
+        public OUTTYPE IsNullOrIndex<T>(PARAMNAME parameterName, int? index, IReadOnlyList<T> array)
+        {
+            return IsNullOrInRange(parameterName, index, 1, array);
+        }
+
+        public OUTTYPE IsNullOrInRange<T>(PARAMNAME parameterName, int? offset, int length, IReadOnlyList<T> array)
+        {
+            if (!offset.HasValue) return this;
+
+            this.NonNegative($"{parameterName}.offset", offset.Value);
+            this.IsGreater($"{parameterName}.length", length, 0);
+
+            if (array == null) _LinkThrow(parameterName, $".{offset} exceeds the number of available items (null).");
+
+            if (offset > array.Count - length)
+            {
+                if (length == 1) _LinkThrow(parameterName, $".{offset} exceeds the number of available items ({array.Count}).");
+                else _LinkThrow(parameterName, $".{offset}+{length} exceeds the number of available items ({array.Count}).");
+            }
+
+            return this;
+        }
+
+        public OUTTYPE IsAnyOf<T>(PARAMNAME parameterName, T value, params T[] values)
+        {
+            if (!values.Contains(value)) _LinkThrow(parameterName, $"value {value} is invalid.");
+
+            return this;
+        }
+
+        public OUTTYPE IsAnyOf(PARAMNAME parameterName, Memory.AttributeFormat value, params Memory.AttributeFormat[] values)
+        {
+            if (!values.Contains(value)) _LinkThrow(parameterName, $"value {value} is invalid.");
+
+            return this;
+        }
+
+        public OUTTYPE IsSetCollection<T>(PARAMNAME parameterName, IEnumerable<T> collection)
+            where T : class
+        {
+            int idx = 0;
+
+            if (collection == null) _LinkThrow(parameterName, "must not be null.");
+
+            var uniqueInstances = new HashSet<T>();
+
+            foreach (var v in collection)
+            {
+                if (v == null) _LinkThrow((parameterName, idx), "Is NULL.");
+
+                if (uniqueInstances.Contains(v)) _LinkThrow((parameterName, idx), "Is duplicated.");
+
+                uniqueInstances.Add(v);
+
+                ++idx;
+            }
+
+            return this;
+        }
+
+        #endregion
+
+        #region data
+
+        [System.Diagnostics.DebuggerStepThrough]
+        internal void _DataThrow(PARAMNAME pname, string msg) { throw new DataException(_Current, $"{pname}: {msg}"); }
+
+        public OUTTYPE IsInRange<T>(PARAMNAME pname, T value, T minInclusive, T maxInclusive)
+            where T : IComparable<T>
+        {
+            if (value.CompareTo(minInclusive) == -1) _DataThrow(pname, $"is below minimum {minInclusive} value: {value}");
+            if (value.CompareTo(maxInclusive) == +1) _DataThrow(pname, $"is above maximum {maxInclusive} value: {value}");
+            return this;
+        }
+
+        public OUTTYPE IsInRange(PARAMNAME pname, double? value, double minInclusive, double maxInclusive)
+        {
+            if (!value.HasValue) return this;
+            if (value.Value < minInclusive) _DataThrow(pname, $"is below minimum {minInclusive} value: {value}");
+            if (value.Value > maxInclusive) _DataThrow(pname, $"is above maximum {maxInclusive} value: {value}");
+            return this;
+        }
+
+        public OUTTYPE IsNullOrPosition(PARAMNAME pname, System.Numerics.Vector3? position)
+        {
+            if (!position.HasValue) return this;
+            return IsPosition(pname, position.Value);
+        }
+
+        public OUTTYPE IsNullOrRotation(PARAMNAME pname, System.Numerics.Quaternion? rotation)
+        {
+            if (!rotation.HasValue) return this;
+            return IsRotation(pname, rotation.Value);
+        }
+
+        public OUTTYPE IsNullOrMatrix(PARAMNAME pname, System.Numerics.Matrix4x4? matrix)
+        {
+            if (!matrix.HasValue) return this;
+            return IsMatrix(pname, matrix.Value);
+        }
+
+        public OUTTYPE IsPosition(PARAMNAME pname, in System.Numerics.Vector3 position)
+        {
+            if (!position._IsFinite()) _DataThrow(pname, "Invalid Position");
+            return this;
+        }
+
+        public OUTTYPE IsNormal(PARAMNAME pname, in System.Numerics.Vector3 normal)
+        {
+            if (!normal.IsNormalized()) _DataThrow(pname, "Invalid Normal");
+            return this;
+        }
+
+        public OUTTYPE IsRotation(PARAMNAME pname, in System.Numerics.Quaternion rotation)
+        {
+            if (!rotation.IsNormalized()) _DataThrow(pname, "Invalid Rotation");
+            return this;
+        }
+
+        public OUTTYPE IsNormal(PARAMNAME pname, in System.Numerics.Vector4 tangent)
+        {
+            if (!tangent.IsValidTangent()) _DataThrow(pname, "Invalid Tangent");
+            return this;
+        }
+
+        public OUTTYPE IsMatrix(PARAMNAME pname, in System.Numerics.Matrix4x4 matrix)
+        {
+            if (!matrix.IsValid()) _DataThrow(pname, "Invalid Matrix");
+            return this;
+        }
+
+        public OUTTYPE ArePositions(PARAMNAME pname, IList<System.Numerics.Vector3> positions)
+        {
+            for (int i = 0; i < positions.Count; ++i)
+            {
+                IsPosition((pname, i), positions[i]);
+            }
+
+            return this;
+        }
+
+        public OUTTYPE AreNormals(PARAMNAME pname, IList<System.Numerics.Vector3> normals)
+        {
+            for (int i = 0; i < normals.Count; ++i)
+            {
+                IsNormal((pname, i), normals[i]);
+            }
+
+            return this;
+        }
+
+        public OUTTYPE AreTangents(PARAMNAME pname, IList<System.Numerics.Vector4> tangents)
+        {
+            for (int i = 0; i < tangents.Count; ++i)
+            {
+                if (!tangents[i].IsValidTangent()) _DataThrow((pname, i), "Invalid Tangent");
+            }
+
+            return this;
+        }
+
+        public OUTTYPE AreRotations(PARAMNAME pname, IList<System.Numerics.Quaternion> rotations)
+        {
+            for (int i = 0; i < rotations.Count; ++i)
+            {
+                if (!rotations[i].IsNormalized()) _DataThrow((pname, i), "Invalid Rotation");
+            }
+
+            return this;
+        }
+
+        public OUTTYPE AreJoints(PARAMNAME pname, IList<System.Numerics.Vector4> joints, int skinsMaxJointCount)
+        {
+            for (int i = 0; i < joints.Count; ++i)
+            {
+                var jjjj = joints[i];
+
+                if (!jjjj._IsFinite()) _DataThrow((pname, i), "Is not finite");
+
+                if (jjjj.X < 0 || jjjj.X >= skinsMaxJointCount) _DataThrow((pname, i), "Is out of bounds");
+                if (jjjj.Y < 0 || jjjj.Y >= skinsMaxJointCount) _DataThrow((pname, i), "Is out of bounds");
+                if (jjjj.Z < 0 || jjjj.Z >= skinsMaxJointCount) _DataThrow((pname, i), "Is out of bounds");
+                if (jjjj.W < 0 || jjjj.W >= skinsMaxJointCount) _DataThrow((pname, i), "Is out of bounds");
+            }
+
+            return this;
+        }
+
+        #endregion
+    }
+}

+ 31 - 352
src/SharpGLTF.Core/Validation/ValidationContext.cs

@@ -10,385 +10,54 @@ namespace SharpGLTF.Validation
     /// <summary>
     /// Utility class used in the process of model validation.
     /// </summary>
-    [System.Diagnostics.DebuggerStepThrough]
-    public struct ValidationContext
+    // [System.Diagnostics.DebuggerStepThrough]
+    public readonly partial struct ValidationContext
     {
         #region constructor
 
-        public ValidationContext(ValidationResult result, TARGET target)
+        public ValidationContext(ValidationResult result)
         {
-            _Result = result;
-            _Target = target;
+            _Root = result.Root;
+            _Mode = result.Mode;
+            _Current = null;
+        }
+
+        internal ValidationContext(ValidationContext context, TARGET target)
+        {
+            _Root = context._Root;
+            _Mode = context._Mode;
+            _Current = target;
         }
 
         #endregion
 
         #region data
 
-        private readonly TARGET _Target;
-        private readonly ValidationResult _Result;
+        private readonly Schema2.ModelRoot _Root;
+        private readonly ValidationMode _Mode;
 
         #endregion
 
         #region properties
 
-        public Schema2.ModelRoot Root => _Result.Root;
+        public Schema2.ModelRoot Root => _Root;
 
-        public ValidationResult Result => _Result;
-
-        public bool TryFix => Result.Mode == Validation.ValidationMode.TryFix;
+        public bool TryFix => _Mode == ValidationMode.TryFix;
 
         #endregion
 
         #region API
 
-        public ValidationContext GetContext(TARGET target) { return _Result.GetContext(target); }
-
-        public void AddSchemaError(ValueLocation location, string message) { AddSchemaError(location.ToString(_Target, message)); }
-
-        public bool TryFixLinkOrError(ValueLocation location, string message)
-        {
-            if (TryFix) AddLinkWarning(location.ToString(_Target, message));
-            else AddLinkError(location.ToString(_Target, message));
-
-            return TryFix;
-        }
-
-        public bool TryFixDataOrError(ValueLocation location, string message)
-        {
-            if (TryFix) AddDataWarning(location.ToString(_Target, message));
-            else AddDataError(location.ToString(_Target, message));
-
-            return TryFix;
-        }
-
-        public void AddLinkError(ValueLocation location, string message) { AddLinkError(location.ToString(_Target, message)); }
-
-        public void AddLinkWarning(String format, params object[] args) { AddLinkWarning(String.Format(format, args)); }
-
-        public void AddDataError(ValueLocation location, string message) { AddDataError(location.ToString(_Target, message)); }
-
-        public void AddDataWarning(ValueLocation location, string message) { AddDataWarning(location.ToString(_Target, message)); }
-
-        public void AddSemanticWarning(String format, params object[] args) { AddSemanticWarning(String.Format(format, args)); }
-
-        public void AddSemanticError(String format, params object[] args) { AddSemanticError(String.Format(format, args)); }
-
-        public void AddLinkWarning(string message)
-        {
-            var ex = new LinkException(_Target, message);
-
-            _Result.AddWarning(ex);
-        }
-
-        public void AddLinkError(string message)
-        {
-            var ex = new LinkException(_Target, message);
-
-            _Result.AddError(ex);
-        }
-
-        public void AddSchemaError(string message)
-        {
-            var ex = new SchemaException(_Target, message);
-
-            _Result.AddError(ex);
-        }
-
-        public void AddDataWarning(string message)
-        {
-            var ex = new DataException(_Target, message);
-            _Result.AddWarning(ex);
-        }
-
-        public void AddDataError(string message)
-        {
-            var ex = new DataException(_Target, message);
-            _Result.AddError(ex);
-        }
-
-        public void AddSemanticError(String message)
-        {
-            var ex = new SemanticException(_Target, message);
-
-            _Result.AddError(ex);
-        }
-
-        public void AddSemanticWarning(String message)
-        {
-            var ex = new SemanticException(_Target, message);
-
-            _Result.AddWarning(ex);
-        }
-
-        #endregion
-
-        #region schema errors
-
-        public bool CheckSchemaIsDefined<T>(ValueLocation location, T value)
-            where T : class
-        {
-            if (value != null) return true;
-
-            AddSchemaError(location, "must be defined.");
-
-            return false;
-        }
-
-        public bool CheckSchemaIsDefined<T>(ValueLocation location, T? value)
-            where T : struct
-        {
-            if (value.HasValue) return true;
-
-            AddSchemaError(location, "must be defined.");
-
-            return false;
-        }
-
-        public bool CheckSchemaNonNegative(ValueLocation location, int? value)
-        {
-            if ((value ?? 0) >= 0) return true;
-            AddSchemaError(location, "must be a non-negative integer.");
-            return false;
-        }
-
-        public void CheckSchemaIsInRange<T>(ValueLocation location, T value, T minInclusive, T maxInclusive)
-            where T : IComparable<T>
-        {
-            if (value.CompareTo(minInclusive) == -1) AddSchemaError(location, $"is below minimum {minInclusive} value: {value}");
-            if (value.CompareTo(maxInclusive) == +1) AddSchemaError(location, $"is above maximum {maxInclusive} value: {value}");
-        }
-
-        public void CheckSchemaIsMultipleOf(ValueLocation location,  int value, int multiple)
-        {
-            if ((value % multiple) == 0) return;
-
-            AddSchemaError(location, $"Value {value} is not a multiple of {multiple}.");
-        }
-
-        public void CheckSchemaIsJsonSerializable(ValueLocation location, Object value)
-        {
-            if (IO.JsonUtils.IsJsonSerializable(value)) return;
-
-            AddSchemaError(location, "Invalid JSON data.");
-        }
-
-        #pragma warning disable CA1054 // Uri parameters should not be strings
-
-        public void CheckSchemaIsValidURI(ValueLocation location, string gltfURI, params string[] validHeaders)
-        {
-            if (string.IsNullOrEmpty(gltfURI)) return;
-
-            foreach (var hdr in validHeaders)
-            {
-                if (gltfURI.StartsWith(hdr)) return;
-            }
-
-            if (Uri.TryCreate(gltfURI, UriKind.Relative, out Uri xuri)) return;
-
-            AddSchemaError(location, $"Invalid URI '{gltfURI}'.");
-        }
-
-        #pragma warning restore CA1054 // Uri parameters should not be strings
-
-        #endregion
-
-        #region semantic errors
-
-        #endregion
-
-        #region data errors
-
-        public void CheckVertexIndex(ValueLocation location, UInt32 vertexIndex, UInt32 vertexCount, UInt32 vertexRestart)
-        {
-            if (vertexIndex == vertexRestart)
-            {
-                AddDataError(location, $"is a primitive restart value ({vertexIndex})");
-                return;
-            }
-
-            if (vertexIndex >= vertexCount)
-            {
-                AddDataError(location, $"has a value ({vertexIndex}) that exceeds number of available vertices ({vertexCount})");
-                return;
-            }
-        }
-
-        public bool CheckIsFinite(ValueLocation location, System.Numerics.Vector2? value)
-        {
-            if (!value.HasValue) return true;
-            if (value.Value._IsFinite()) return true;
-            AddDataError(location, $"is NaN or Infinity.");
-            return false;
-        }
-
-        public bool CheckIsFinite(ValueLocation location, System.Numerics.Vector3? value)
-        {
-            if (!value.HasValue) return true;
-            if (value.Value._IsFinite()) return true;
-            AddDataError(location, "is NaN or Infinity.");
-            return false;
-        }
-
-        public bool CheckIsFinite(ValueLocation location, System.Numerics.Vector4? value)
-        {
-            if (!value.HasValue) return true;
-            if (value.Value._IsFinite()) return true;
-            AddDataError(location, "is NaN or Infinity.");
-            return false;
-        }
-
-        public bool CheckIsFinite(ValueLocation location, System.Numerics.Quaternion? value)
-        {
-            if (!value.HasValue) return true;
-            if (value.Value._IsFinite()) return true;
-            AddDataError(location, "is NaN or Infinity.");
-            return false;
-        }
-
-        public bool TryFixUnitLengthOrError(ValueLocation location, System.Numerics.Vector3? value)
-        {
-            if (!value.HasValue) return false;
-            if (!CheckIsFinite(location, value)) return false;
-            if (value.Value.IsNormalized()) return false;
-
-            return TryFixDataOrError(location, $"is not of unit length: {value.Value.Length()}.");
-        }
-
-        public bool TryFixTangentOrError(ValueLocation location, System.Numerics.Vector4 tangent)
-        {
-            if (TryFixUnitLengthOrError(location, new System.Numerics.Vector3(tangent.X, tangent.Y, tangent.Z))) return true;
-
-            if (tangent.W == 1 || tangent.W == -1) return false;
-
-            return TryFixDataOrError(location, $"has invalid value: {tangent.W}. Must be 1.0 or -1.0.");
-        }
-
-        public void CheckIsInRange(ValueLocation location, System.Numerics.Vector4 v, float minInclusive, float maxInclusive)
-        {
-            CheckIsInRange(location, v.X, minInclusive, maxInclusive);
-            CheckIsInRange(location, v.Y, minInclusive, maxInclusive);
-            CheckIsInRange(location, v.Z, minInclusive, maxInclusive);
-            CheckIsInRange(location, v.W, minInclusive, maxInclusive);
-        }
-
-        public void CheckIsInRange(ValueLocation location, float value, float minInclusive, float maxInclusive)
-        {
-            if (value < minInclusive) AddDataError(location, $"is below minimum {minInclusive} value: {value}");
-            if (value > maxInclusive) AddDataError(location, $"is above maximum {maxInclusive} value: {value}");
-        }
-
-        public bool CheckIsMatrix(ValueLocation location, System.Numerics.Matrix4x4? matrix)
-        {
-            if (matrix == null) return true;
-
-            if (!matrix.Value._IsFinite())
-            {
-                AddDataError(location, "is NaN or Infinity.");
-                return false;
-            }
-
-            if (!System.Numerics.Matrix4x4.Decompose(matrix.Value, out System.Numerics.Vector3 s, out System.Numerics.Quaternion r, out System.Numerics.Vector3 t))
-            {
-                AddDataError(location, "is not decomposable to TRS.");
-                return false;
-            }
-
-            return true;
-        }
+        public ValidationContext GetContext(TARGET target) { return new ValidationContext(this, target); }
 
         #endregion
 
-        #region link errors
-
-        public bool CheckArrayIndexAccess<T>(ValueLocation location, int? index, IReadOnlyList<T> array)
-        {
-            return CheckArrayRangeAccess(location, index, 1, array);
-        }
-
-        public bool CheckArrayRangeAccess<T>(ValueLocation location, int? offset, int length, IReadOnlyList<T> array)
-        {
-            if (!offset.HasValue) return true;
-
-            if (!CheckSchemaNonNegative(location, offset)) return false;
-
-            if (length <= 0)
-            {
-                AddSchemaError(location, "Invalid length");
-                return false;
-            }
-
-            if (array == null)
-            {
-                AddLinkError(location, $"Index {offset} exceeds the number of available items (null).");
-                return false;
-            }
-
-            if (offset > array.Count - length)
-            {
-                if (length == 1) AddLinkError(location, $"Index {offset} exceeds the number of available items ({array.Count}).");
-                else AddLinkError(location, $"Index {offset}+{length} exceeds the number of available items ({array.Count}).");
-                return false;
-            }
-
-            return true;
-        }
-
-        public bool CheckLinkMustBeAnyOf<T>(ValueLocation location, T value, params T[] values)
-        {
-            if (values.Contains(value)) return true;
-
-            var validValues = string.Join(" ", values);
-
-            AddLinkError(location, $"value {value} is invalid. Must be one of {validValues}");
-
-            return false;
-        }
-
-        public bool CheckLinksInCollection<T>(ValueLocation location, IEnumerable<T> collection)
-            where T : class
-        {
-            int idx = 0;
-
-            if (collection == null)
-            {
-                AddLinkError(location, "Is NULL.");
-                return false;
-            }
-
-            var uniqueInstances = new HashSet<T>();
-
-            foreach (var v in collection)
-            {
-                if (v == null)
-                {
-                    AddLinkError((location, idx), "Is NULL.");
-                    return false;
-                }
-                else if (uniqueInstances.Contains(v))
-                {
-                    AddSchemaError((location, idx), "Is duplicated.");
-                    return false;
-                }
-
-                uniqueInstances.Add(v);
-
-                ++idx;
-            }
-
-            return true;
-        }
-
-        public void UnsupportedExtensionError(String message)
-        {
-            AddLinkError(message);
-        }
-
-        #endregion
     }
 
-    public struct ValueLocation
+    public readonly struct ValueLocation
     {
+        #region constructors
+
         public static implicit operator ValueLocation(int index) { return new ValueLocation(string.Empty, index); }
 
         public static implicit operator ValueLocation(int? index) { return new ValueLocation(string.Empty, index ?? 0); }
@@ -407,9 +76,17 @@ namespace SharpGLTF.Validation
             _Index = idx1;
         }
 
+        #endregion
+
+        #region
+
         private readonly string _Name;
         private readonly int _Index;
 
+        #endregion
+
+        #region API
+
         public override string ToString()
         {
             if (_Index >= 0) return $"{_Name}[{_Index}]";
@@ -438,5 +115,7 @@ namespace SharpGLTF.Validation
 
             return name + this.ToString();
         }
+
+        #endregion
     }
 }

+ 2 - 10
src/SharpGLTF.Core/Validation/ValidationResult.cs

@@ -27,7 +27,6 @@ namespace SharpGLTF.Validation
         private readonly bool _InstantThrow;
 
         private readonly List<Exception> _Errors = new List<Exception>();
-        private readonly List<Exception> _Warnings = new List<Exception>();
 
         #endregion
 
@@ -45,16 +44,9 @@ namespace SharpGLTF.Validation
 
         #region API
 
-        public ValidationContext GetContext() { return new ValidationContext(this, _Root); }
+        public ValidationContext GetContext() { return new ValidationContext(this); }
 
-        public ValidationContext GetContext(TARGET target) { return new ValidationContext(this, target); }
-
-        public void AddWarning(ModelException ex)
-        {
-            _Warnings.Add(ex);
-        }
-
-        public void AddError(ModelException ex)
+        public void SetError(ModelException ex)
         {
             if (_InstantThrow) throw ex;
 

+ 0 - 1
src/SharpGLTF.Toolkit/Geometry/PackedEncoding.cs

@@ -7,7 +7,6 @@ using ENCODING = SharpGLTF.Schema2.EncodingType;
 
 namespace SharpGLTF.Geometry
 {
-
     class PackedEncoding
     {
         public ENCODING? JointsEncoding;

+ 2 - 2
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.Schema2.cs

@@ -178,7 +178,7 @@ namespace SharpGLTF.Scenes
         /// Convertes a collection of <see cref="SceneBuilder"/> instances to a single <see cref="ModelRoot"/> instance.
         /// </summary>
         /// <param name="srcScenes">A collection of scenes</param>
-        /// <param name="useStridedBuffers">True to generate strided vertex buffers whenever possible.</param>
+        /// <param name="settings">Conversion settings.</param>
         /// <returns>A new <see cref="ModelRoot"/> instance.</returns>
         public static ModelRoot ToSchema2(IEnumerable<SceneBuilder> srcScenes, SceneBuilderSchema2Settings settings)
         {
@@ -213,7 +213,7 @@ namespace SharpGLTF.Scenes
         /// <summary>
         /// Converts this <see cref="SceneBuilder"/> instance into a <see cref="ModelRoot"/> instance.
         /// </summary>
-        /// <param name="useStridedBuffers">True to generate strided vertex buffers whenever possible.</param>
+        /// <param name="settings">Conversion settings.</param>
         /// <returns>A new <see cref="ModelRoot"/> instance.</returns>
         public ModelRoot ToGltf2(SceneBuilderSchema2Settings settings)
         {

+ 4 - 6
src/SharpGLTF.Toolkit/SharpGLTF.Toolkit.csproj

@@ -10,10 +10,10 @@
   </PropertyGroup>
 
   <Import Project="..\Version.props" />
-
   <Import Project="..\PackageInfo.props" />
-  
-  <ItemGroup>
+  <Import Project="..\Analyzers.props" />
+
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
     <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
       <_Parameter1>SharpGLTF.Core.Tests</_Parameter1>
     </AssemblyAttribute>
@@ -29,8 +29,6 @@
 
   <ItemGroup>
     <ProjectReference Include="..\SharpGLTF.Core\SharpGLTF.Core.csproj" />
-  </ItemGroup>
-  
-  <Import Project="..\..\Analyzers.targets" />
+  </ItemGroup>  
   
 </Project>

+ 35 - 3
tests/SharpGLTF.NUnit/TestFiles.cs

@@ -90,7 +90,7 @@ namespace SharpGLTF
             return GetModelPathsInDirectory(_SchemaDir, "extensions", "2.0");         
         }
 
-        public static IEnumerable<(string Path, bool ShouldLoad)> GetReferenceModelPaths(bool useNegative = false)
+        public static IEnumerable<string> GetReferenceModelPaths(bool useNegative = false)
         {
             _Check();
 
@@ -122,7 +122,7 @@ namespace SharpGLTF
 
                     mdlPath = System.IO.Path.Combine(d, mdlPath);
 
-                    yield return (mdlPath, loadable);
+                    yield return mdlPath;
                 }
             }
 
@@ -145,7 +145,39 @@ namespace SharpGLTF
         {
             _Check();
 
-            var skip = new string[] { "misplaced_bin_chunk.glb", "valid_placeholder.glb" };
+            var skip = new string[]
+            {
+                "empty_object.gltf", // need to look further
+                "custom_property.gltf",
+                "integer_written_as_float.gltf",
+                "unknown_type.gltf",
+                "valid.gltf", // valid just because objects are unused
+                "get_elements_sparse.gltf", // valid just because objects are unused
+                "invalid_elements_float.gltf", // sure, it has invalid floats, but then the accessor is not used.
+                "not_found.gltf", // it fails at a tricky time
+                "non_relative_uri.gltf", // absolute path pointing to a http which is not supported.
+                "unrecognized_format.gltf", // might require to dig into the image
+                "multiple_extensions.gltf", // it's theoretically tracked (it should give a warning) but then, objects should not be empty...
+                "invalid_tangent.gltf", // it's theoretically tracked (it should give a warning) but then, objects should not be empty...
+                "primitive_incompatible_mode.gltf", // it's theoretically tracked (it should give a warning) but then, objects should not be empty...
+                "primitive_no_position.gltf", // it's theoretically tracked (it should give a warning) but then, objects should not be empty...
+                "index_buffer_degenerate_triangle.gltf", // it's theoretically tracked (it should give a warning) but then, objects should not be empty...
+                "node_skinned_mesh_without_skin.gltf", // it's theoretically tracked (it should give a warning) but then, objects should not be empty...
+                "duplicate_extension_entry.gltf",
+                "named_objects.gltf", // gltf-validator says valid, but Buffer should not be.
+                "unused_objects.gltf",
+                "ignored_animated_transform.gltf", // an channel animated a node with a skin has no effect (warning) since nodes with skin have no transform
+                "ignored_local_transform.gltf", // a transform in a node with a skin has no effect (warning) since nodes with skin have no transform
+                "ignored_parent_transform.gltf", // a transform in a node with a skin has no effect (warning) since nodes with skin have no transform
+                "misplaced_bin_chunk.glb",
+                "valid_placeholder.glb",
+                "undeclared_extension.gltf",
+                "unexpected_extension.gltf",
+                "unresolved_source.gltf",
+                "unresolved_light_empty_root_ext.gltf",
+                "unresolved_light_no_root_ext.gltf",
+                "invalid_image_mime_type.gltf", // actual images cannot be validated
+            };
 
             var files = GetModelPathsInDirectory(_ValidationDir, "test")
                 .Where(item => skip.All(f=>!item.EndsWith(f)));

+ 1 - 0
tests/SharpGLTF.NUnit/ValidationResult.cs

@@ -42,6 +42,7 @@ namespace SharpGLTF
         public bool Truncated { get; set; }
     }
 
+    [System.Diagnostics.DebuggerDisplay("{Code} {Severity} {Message}")]
     public sealed class ValidationMessage
     {
         public string Code { get; set; }

+ 25 - 63
tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadGeneratedTests.cs

@@ -24,104 +24,66 @@ namespace SharpGLTF.Schema2.LoadAndSave
 
         #endregion        
 
-        [Test]
-        public void LoadPositiveModels()
+        [TestCase(true)]
+        [TestCase(false)]
+        public void LoadGeneratedModels(bool isNegativeCase)
         {
             TestContext.CurrentContext.AttachShowDirLink();
 
-            var files = TestFiles.GetReferenceModelPaths();
+            var files = TestFiles.GetReferenceModelPaths(isNegativeCase);
 
             bool passed = true;
 
-            foreach (var f in files)
+            foreach (var filePath in files)
             {
-                try
-                {
-                    var model = ModelRoot.Load(f.Item1);
+                // System.Diagnostics.Debug.Assert(!filePath.EndsWith("Compatibility_05.gltf"));
 
-                    if (!f.ShouldLoad)
-                    {
-                        TestContext.Error.WriteLine($"{f.Path.ToShortDisplayPath()} 👎😦 Should not load!");
-                        passed = false;
-                    }
-                    else
-                    {
-                        TestContext.WriteLine($"{f.Path.ToShortDisplayPath()} 🙂👍");                        
-                    }                    
-                }
-                catch (Exception ex)
-                {
-                    if (f.ShouldLoad)
-                    {
-                        TestContext.Error.WriteLine($"{f.Path.ToShortDisplayPath()} 👎😦 Should load!");
-                        TestContext.Error.WriteLine($"   ERROR: {ex.Message}");
-                        passed = false;
-                    }
-                    else
-                    {
-                        TestContext.WriteLine($"{f.Path.ToShortDisplayPath()} 🙂👍");
-                        TestContext.WriteLine($"   Exception: {ex.Message}");
-                    }                    
-                }
-
-                if (f.ShouldLoad && !f.Path.ToLower().Contains("compatibility"))
-                {
-                    var model = ModelRoot.Load(f.Path);
-                    model.AttachToCurrentTest(System.IO.Path.ChangeExtension(System.IO.Path.GetFileName(f.Path), ".obj"));
-                }
-            }
+                var gltfJson = filePath.EndsWith(".gltf") ? System.IO.File.ReadAllText(filePath) : string.Empty;
 
-            Assert.IsTrue(passed);
-        }
-
-        [Test]
-        public void LoadNegativeModels()
-        {
-            TestContext.CurrentContext.AttachShowDirLink();
+                var report = gltf_validator.ValidateFile(filePath);                
 
-            var files = TestFiles.GetReferenceModelPaths(true);
+                if (report == null) continue; // ??
 
-            bool passed = true;
+                if (report.Warnings.Any(item => item.Contains("Cannot validate an extension"))) continue;
 
-            foreach (var f in files)
-            {
                 try
                 {
-                    var model = ModelRoot.Load(f.Path);
+                    var model = ModelRoot.Load(filePath);
 
-                    if (!f.ShouldLoad)
+                    if (report.HasErrors)
                     {
-                        TestContext.Error.WriteLine($"{f.Path.ToShortDisplayPath()} 👎😦 Should not load!");
+                        TestContext.Error.WriteLine($"{filePath.ToShortDisplayPath()} 👎😦 Should not load!");
                         passed = false;
                     }
                     else
                     {
-                        TestContext.WriteLine($"{f.Path.ToShortDisplayPath()} 🙂👍");
-                    }
+                        TestContext.WriteLine($"{filePath.ToShortDisplayPath()} 🙂👍");                        
+                    }                    
                 }
                 catch (Exception ex)
                 {
-                    if (f.ShouldLoad)
+                    if (!report.HasErrors)
                     {
-                        TestContext.Error.WriteLine($"{f.Path.ToShortDisplayPath()} 👎😦 Should load!");
+                        TestContext.Error.WriteLine($"{filePath.ToShortDisplayPath()} 👎😦 Should load!");
                         TestContext.Error.WriteLine($"   ERROR: {ex.Message}");
                         passed = false;
                     }
                     else
                     {
-                        TestContext.WriteLine($"{f.Path.ToShortDisplayPath()} 🙂👍");
+                        TestContext.WriteLine($"{filePath.ToShortDisplayPath()} 🙂👍");
                         TestContext.WriteLine($"   Exception: {ex.Message}");
-                    }
+                    }                    
                 }
 
-                if (f.ShouldLoad && !f.Path.ToLower().Contains("compatibility"))
+                /*
+                if (ShouldLoad && !filePath.ToLower().Contains("compatibility"))
                 {
-                    var model = ModelRoot.Load(f.Path);
-                    model.AttachToCurrentTest(System.IO.Path.ChangeExtension(System.IO.Path.GetFileName(f.Path), ".obj"));
-                }
+                    var model = ModelRoot.Load(filePath);
+                    model.AttachToCurrentTest(System.IO.Path.ChangeExtension(System.IO.Path.GetFileName(filePath), ".obj"));
+                }*/
             }
 
             Assert.IsTrue(passed);
-        }
+        }        
     }
 }

+ 11 - 13
tests/SharpGLTF.Tests/Validation/InvalidFilesTests.cs

@@ -52,7 +52,7 @@ namespace SharpGLTF.Validation
         }
 
         [Test]
-        public void CheckExceptionOnInvalidFiles()
+        public void CheckInvalidFiles()
         {
             var files = TestFiles
                 .GetKhronosValidationPaths()
@@ -60,23 +60,21 @@ namespace SharpGLTF.Validation
 
             foreach (var f in files)
             {
-                var report = ValidationReport.Load(f + ".report.json");
+                // System.Diagnostics.Debug.Assert(!f.EndsWith("unresolved_source.gltf"));
 
-                TestContext.Progress.WriteLine($"{f}...");
+                var gltfJson = f.EndsWith(".gltf") ? System.IO.File.ReadAllText(f) : string.Empty;
 
-                TestContext.Write($"{f}...");
+                var report = ValidationReport.Load(f + ".report.json");
+                
+                var result = Schema2.ModelRoot.Validate(f);
 
-                try
+                if (result.HasErrors != report.Issues.NumErrors > 0)
                 {
-
-                    var result = Schema2.ModelRoot.Validate(f);
-
-                    TestContext.WriteLine($"{result.HasErrors == report.Issues.NumErrors > 0}");
+                    TestContext.WriteLine($"Failed: {f}");
+                    foreach (var e in report.Issues.Messages.Where(item => item.Severity == 0)) TestContext.WriteLine($"    {e.Message}");
                 }
-                catch(Exception ex)
-                {
-                    TestContext.WriteLine("THROW!");
-                }                
+
+                Assert.AreEqual(report.Issues.NumErrors > 0, result.HasErrors);                                
             }
         }