• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orion-ecs / keen-eye / 30113831081

24 Jul 2026 05:39PM UTC coverage: 64.224% (-0.07%) from 64.291%
30113831081

push

github

tyevco
fix(audio): Fix non-looping replay, WAV validation, paused-source recycling, double master volume (#1185,#1186,#1188,#1189)

- #1185: AudioSourceSystem now restarts a stopped backend source when State is set back to Playing (distinguishes a replay request from a natural finish via CurrentSound validity), so non-looping sources are replayable per the AudioSource.State contract.
- #1186: WavDecoder validates each chunk size against the remaining bytes and throws AudioLoadException on negative/oversized sizes, preventing the ArgumentOutOfRangeException slice crash and the negative-size infinite loop on untrusted files.
- #1188: SourcePool.Update only recycles Stopped sources (Paused treated as still-active), so Pause/PauseAll no longer kills pooled one-shots on the next frame.
- #1189: SilkAudioContext applies master + Master-channel volume only via the listener gain; ComputeEffectiveVolume no longer re-multiplies them into the per-source gain, eliminating the squared master volume and the new-vs-playing inconsistency.

Adds KeenEyes.Audio.Tests (AudioSourceSystem replay/finish, via fake IAudioDevice/IAudioContext) and KeenEyes.Audio.Silk.Tests (WavDecoder malformed-input validation). #1188/#1189 are OpenAL-device-bound and verified by code trace.

Closes #1185
Closes #1186
Closes #1188
Closes #1189

8612 of 12731 branches covered (67.65%)

Branch coverage included in aggregate %.

10 of 15 new or added lines in 4 files covered. (66.67%)

368 existing lines in 26 files now uncovered.

51312 of 80574 relevant lines covered (63.68%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.19
/src/KeenEyes.Animation/Systems/SkinnedMeshBoneSystem.cs
1
using System.Numerics;
2

3
using KeenEyes.Animation.Components;
4
using KeenEyes.Animation.Rendering;
5
using KeenEyes.Common;
6

7
namespace KeenEyes.Animation.Systems;
8

9
/// <summary>
10
/// System that computes bone matrices for skinned mesh rendering.
11
/// </summary>
12
/// <remarks>
13
/// <para>
14
/// This system queries entities with <see cref="SkinnedMesh"/> components and computes
15
/// the final bone matrices needed for GPU skinning. For each skinned mesh:
16
/// </para>
17
/// <list type="number">
18
///   <item><description>Reads world transforms from bone entities</description></item>
19
///   <item><description>Gets inverse bind matrices from the SkinnedMesh component</description></item>
20
///   <item><description>Computes: boneMatrix = inverseBindMatrix × boneWorldTransform (row-vector order)</description></item>
21
///   <item><description>Stores results in a <see cref="BoneMatrixBuffer"/> for GPU upload</description></item>
22
/// </list>
23
/// <para>
24
/// The computed bone matrices are stored in a dictionary keyed by entity ID for the
25
/// rendering system to access. Use <see cref="GetBoneMatrixBuffer"/> to retrieve the
26
/// buffer for a specific skinned mesh entity.
27
/// </para>
28
/// </remarks>
29
public sealed class SkinnedMeshBoneSystem : SystemBase
30
{
31
    private readonly Dictionary<int, BoneMatrixBuffer> boneBuffers = [];
1✔
32

33
    // Rebuilt each frame: maps entity id -> the live entity handle (with its current
34
    // version) so bone ids stored on the SkinnedMesh component can be resolved without
35
    // fabricating a Version-0 handle (which IsAlive always rejects).
36
    private readonly Dictionary<int, Entity> entityLookup = [];
1✔
37
    private ulong currentGeneration;
38

39
    /// <summary>
40
    /// Gets the bone matrix buffer for a skinned mesh entity.
41
    /// </summary>
42
    /// <param name="entityId">The skinned mesh entity ID.</param>
43
    /// <returns>The bone matrix buffer, or null if not found.</returns>
44
    public BoneMatrixBuffer? GetBoneMatrixBuffer(int entityId)
45
    {
46
        return boneBuffers.TryGetValue(entityId, out var buffer) ? buffer : null;
1✔
47
    }
48

49
    /// <inheritdoc />
50
    public override void Update(float deltaTime)
51
    {
52
        // Increment generation for dirty tracking
53
        currentGeneration++;
1✔
54

55
        // Resolve bone ids to live entity handles (with correct versions) for this frame.
56
        entityLookup.Clear();
1✔
57
        foreach (var entity in World.Query<Transform3D>())
1✔
58
        {
59
            entityLookup[entity.Id] = entity;
1✔
60
        }
61

62
        // Track which entities we've seen this frame
63
        var activeEntities = new HashSet<int>();
1✔
64

65
        // Process all skinned mesh entities
66
        foreach (var entity in World.Query<SkinnedMesh, Transform3D>())
1✔
67
        {
68
            activeEntities.Add(entity.Id);
1✔
69

70
            ref readonly var skinnedMesh = ref World.Get<SkinnedMesh>(entity);
1✔
71

72
            // Get or create bone matrix buffer for this entity
73
            if (!boneBuffers.TryGetValue(entity.Id, out var boneBuffer))
1✔
74
            {
75
                boneBuffer = new BoneMatrixBuffer();
1✔
76
                boneBuffers[entity.Id] = boneBuffer;
1✔
77
            }
78

79
            // Compute bone matrices using the inverse bind matrices on the component
80
            ComputeBoneMatrices(skinnedMesh, boneBuffer);
1✔
81
        }
82

83
        // Clean up buffers for despawned entities
84
        var entitiesToRemove = new List<int>();
1✔
85
        foreach (var entityId in boneBuffers.Keys)
1✔
86
        {
87
            if (!activeEntities.Contains(entityId))
1✔
88
            {
89
                entitiesToRemove.Add(entityId);
×
90
            }
91
        }
92

93
        foreach (var entityId in entitiesToRemove)
1✔
94
        {
UNCOV
95
            if (boneBuffers.TryGetValue(entityId, out var buffer))
×
96
            {
UNCOV
97
                buffer.Dispose();
×
UNCOV
98
                boneBuffers.Remove(entityId);
×
99
            }
100
        }
101
    }
1✔
102

103
    private void ComputeBoneMatrices(in SkinnedMesh skinnedMesh, BoneMatrixBuffer buffer)
104
    {
105
        if (skinnedMesh.BoneEntityIds is null || skinnedMesh.BoneEntityIds.Length == 0 ||
1✔
106
            skinnedMesh.InverseBindMatrices is null || skinnedMesh.InverseBindMatrices.Length == 0)
1✔
107
        {
UNCOV
108
            return;
×
109
        }
110

111
        var boneCount = Math.Min(skinnedMesh.BoneEntityIds.Length, skinnedMesh.InverseBindMatrices.Length);
1✔
112
        boneCount = Math.Min(boneCount, buffer.MaxBones);
1✔
113

114
        for (var i = 0; i < boneCount; i++)
1✔
115
        {
116
            var boneEntityId = skinnedMesh.BoneEntityIds[i];
1✔
117

118
            // Get world transform of the bone entity
119
            var boneWorldMatrix = GetBoneWorldMatrix(boneEntityId);
1✔
120

121
            // Compute final skinning matrix. System.Numerics uses the row-vector convention
122
            // (v' = v * M), so a vertex is first pulled into bone space by the inverse bind
123
            // matrix, then pushed to the animated pose by the bone's world matrix:
124
            // finalMatrix = inverseBindMatrix * boneWorldMatrix.
125
            var inverseBindMatrix = skinnedMesh.InverseBindMatrices[i];
1✔
126
            var finalMatrix = inverseBindMatrix * boneWorldMatrix;
1✔
127

128
            // Store in buffer with current generation for dirty tracking
129
            buffer.SetBoneMatrix(i, finalMatrix, currentGeneration);
1✔
130
        }
131
    }
1✔
132

133
    private Matrix4x4 GetBoneWorldMatrix(int boneEntityId)
134
    {
135
        // Resolve the id to the live entity handle (correct version) for this frame.
136
        return entityLookup.TryGetValue(boneEntityId, out var entity)
1✔
137
            ? GetBoneWorldMatrix(entity)
1✔
138
            : Matrix4x4.Identity;
1✔
139
    }
140

141
    private Matrix4x4 GetBoneWorldMatrix(Entity entity)
142
    {
143
        // Check if entity is alive
144
        if (!World.IsAlive(entity))
1✔
145
        {
146
            return Matrix4x4.Identity;
×
147
        }
148

149
        // Check if entity has Transform3D
150
        if (!World.Has<Transform3D>(entity))
1✔
151
        {
UNCOV
152
            return Matrix4x4.Identity;
×
153
        }
154

155
        ref readonly var transform = ref World.Get<Transform3D>(entity);
1✔
156

157
        // Build the local matrix from transform components
158
        var localMatrix = Matrix4x4.CreateScale(transform.Scale) *
1✔
159
                         Matrix4x4.CreateFromQuaternion(transform.Rotation) *
1✔
160
                         Matrix4x4.CreateTranslation(transform.Position);
1✔
161

162
        // If entity has a parent, compose with the parent's world matrix. GetParent returns
163
        // a live handle, so recursion keeps the correct version without an id round-trip.
164
        var parentEntity = World.GetParent(entity);
1✔
165
        if (parentEntity.IsValid)
1✔
166
        {
167
            var parentWorld = GetBoneWorldMatrix(parentEntity);
1✔
168
            return localMatrix * parentWorld;
1✔
169
        }
170

171
        return localMatrix;
1✔
172
    }
173

174
    /// <inheritdoc />
175
    protected override void Dispose(bool disposing)
176
    {
177
        if (disposing)
1✔
178
        {
179
            foreach (var buffer in boneBuffers.Values)
1✔
180
            {
181
                buffer.Dispose();
1✔
182
            }
183

184
            boneBuffers.Clear();
1✔
185
        }
186

187
        base.Dispose(disposing);
1✔
188
    }
1✔
189
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc