1 //
2 // Copyright (c) 2010-2025 Antmicro
3 //
4 // This file is licensed under the MIT License.
5 // Full license text is available in 'licenses/MIT.txt'.
6 //
7 using System;
8 using System.Linq;
9 using System.Collections.Concurrent;
10 using System.Diagnostics;
11 using System.Drawing;
12 using System.Drawing.Imaging;
13 using System.Runtime.InteropServices;
14 using System.Threading;
15 
16 using Antmicro.Renode.Backends.Display;
17 using Antmicro.Renode.Core;
18 using Antmicro.Renode.Exceptions;
19 using Antmicro.Renode.Peripherals;
20 using Antmicro.Renode.Peripherals.Video;
21 using Antmicro.Renode.Time;
22 using Antmicro.Renode.Utilities;
23 using Antmicro.Migrant.Hooks;
24 using Antmicro.Migrant;
25 
26 namespace Antmicro.Renode.Testing
27 {
28     public static class FrameBufferTesterExtension
29     {
CreateFrameBufferTester(this Emulation emulation, string name, float timeoutInSeconds)30         public static void CreateFrameBufferTester(this Emulation emulation, string name, float timeoutInSeconds)
31         {
32             var tester = new FrameBufferTester(TimeSpan.FromSeconds(timeoutInSeconds));
33             emulation.ExternalsManager.AddExternal(tester, name);
34         }
35     }
36 
37     public class FrameBufferTester : IExternal, IConnectable<IVideo>
38     {
FrameBufferTester(TimeSpan timeout)39         public FrameBufferTester(TimeSpan timeout)
40         {
41             framesQueue = new BlockingCollection<byte[]>();
42             globalTimeout = timeout;
43             newFrameEvent = new AutoResetEvent(false);
44         }
45 
AttachTo(IVideo obj)46         public void AttachTo(IVideo obj)
47         {
48             if(video != null)
49             {
50                 throw new RecoverableException("Cannot attach to the provided video device as it would overwrite the existing configuration.");
51             }
52             video = obj;
53             video.ConfigurationChanged += HandleConfigurationChange;
54             video.FrameRendered += HandleNewFrame;
55         }
56 
DetachFrom(IVideo obj)57         public void DetachFrom(IVideo obj)
58         {
59             video.ConfigurationChanged -= HandleConfigurationChange;
60             video.FrameRendered -= HandleNewFrame;
61             video = null;
62         }
63 
BitmapToByteArray(Bitmap image)64         private static byte[] BitmapToByteArray(Bitmap image)
65         {
66             BitmapData data = null;
67 
68             try
69             {
70                 data = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, image.PixelFormat);
71                 var bytedata = new byte[data.Stride * image.Height];
72 
73                 Marshal.Copy(data.Scan0, bytedata, 0, bytedata.Length);
74 
75                 return bytedata;
76             }
77             finally
78             {
79                 if(data != null)
80                 {
81                     image.UnlockBits(data);
82                 }
83             }
84         }
85 
WaitForFrame(string fileName, float? timeout = null)86         public FrameBufferTester WaitForFrame(string fileName, float? timeout = null)
87         {
88             var image = Image.FromFile(fileName);
89             var bytes = BitmapToByteArray((Bitmap)image);
90             return WaitForFrame(bytes, timeout.HasValue ? TimeSpan.FromSeconds(timeout.Value) : (TimeSpan?)null);
91         }
92 
WaitForFrame(byte[] frame, TimeSpan? timeout = null)93         public FrameBufferTester WaitForFrame(byte[] frame, TimeSpan? timeout = null)
94         {
95             var machine = video.GetMachine();
96             var finalTimeout = timeout ?? globalTimeout;
97             var timeoutEvent = machine.LocalTimeSource.EnqueueTimeoutEvent((ulong)finalTimeout.TotalMilliseconds);
98 
99             var emulation = EmulationManager.Instance.CurrentEmulation;
100             if(!emulation.IsStarted)
101             {
102                 emulation.StartAll();
103             }
104 
105             do
106             {
107                 if(framesQueue.TryTake(out var queuedFrame)
108                     && queuedFrame.Length == frame.Length
109                     && Enumerable.SequenceEqual(queuedFrame, frame))
110                 {
111                     return this;
112                 }
113 
114                 WaitHandle.WaitAny(new [] { timeoutEvent.WaitHandle, newFrameEvent });
115             }
116             while(!timeoutEvent.IsTriggered);
117 
118             throw new ArgumentException();
119         }
120 
WaitForFrameROI(string fileName, uint startX, uint startY, uint width, uint height, float? timeout = null)121         public FrameBufferTester WaitForFrameROI(string fileName, uint startX, uint startY, uint width, uint height, float? timeout = null)
122         {
123             var image = Image.FromFile(fileName);
124             var bytes = BitmapToByteArray((Bitmap)image);
125             return WaitForFrameROI(bytes, startX, startY, width, height, timeout.HasValue ? TimeSpan.FromSeconds(timeout.Value) : (TimeSpan?)null);
126         }
127 
WaitForFrameROI(byte[] frame, uint startX, uint startY, uint width, uint height, TimeSpan? timeout = null)128         public FrameBufferTester WaitForFrameROI(byte[] frame, uint startX, uint startY, uint width, uint height, TimeSpan? timeout = null)
129         {
130             if(width > frameWidth || startX > frameWidth - width || height > frameHeight || startY > frameHeight - height)
131             {
132                 throw new ArgumentException("Region of interest doesn't fit in the frame");
133             }
134 
135             if(height == 0 || width == 0)
136             {
137                 throw new ArgumentException("Width and height can't be equal to 0.");
138             }
139 
140             var machine = video.GetMachine();
141             var finalTimeout = timeout ?? globalTimeout;
142             var timeoutEvent = machine.LocalTimeSource.EnqueueTimeoutEvent((ulong)finalTimeout.TotalMilliseconds);
143 
144             var emulation = EmulationManager.Instance.CurrentEmulation;
145             if(!emulation.IsStarted)
146             {
147                 emulation.StartAll();
148             }
149 
150             do
151             {
152                 if(framesQueue.TryTake(out var queuedFrame)
153                         && queuedFrame.Length == frame.Length)
154                 {
155                     bool roiEqual = true;
156                     for(uint i = startY; roiEqual && i < startY + height; i++)
157                     {
158                         for(uint j = startX; roiEqual && j < startX + width; j++)
159                         {
160                             for(uint k = 0; roiEqual && k < 4; k++)
161                             {
162                                 int index = (int)(i*frameWidth*4 + j*4 + k);
163                                 if(frame[index] != queuedFrame[index])
164                                 {
165                                     roiEqual = false;
166                                 }
167                             }
168                         }
169                     }
170                     if(roiEqual)
171                     {
172                         return this;
173                     }
174                 }
175                 WaitHandle.WaitAny(new [] { timeoutEvent.WaitHandle, newFrameEvent });
176             }
177             while(!timeoutEvent.IsTriggered);
178 
179             throw new ArgumentException();
180         }
181 
HandleConfigurationChange(int width, int height, Backends.Display.PixelFormat format, ELFSharp.ELF.Endianess endianess)182         private void HandleConfigurationChange(int width, int height, Backends.Display.PixelFormat format, ELFSharp.ELF.Endianess endianess)
183         {
184             if(width == 0 || height == 0)
185             {
186                 return;
187             }
188 
189             this.format = format;
190             this.endianess = endianess;
191             InitConverter();
192             frameWidth = width;
193             frameHeight = height;
194         }
195 
HandleNewFrame(byte[] obj)196         private void HandleNewFrame(byte[] obj)
197         {
198             var buffer = new byte[frameWidth * frameHeight * 4];
199             converter.Convert(obj, ref buffer);
200             framesQueue.Add(buffer);
201             newFrameEvent.Set();
202         }
203 
204         [PostDeserialization]
InitConverter()205         private void InitConverter()
206         {
207             if(format != null && endianess != null)
208             {
209                 converter = PixelManipulationTools.GetConverter((Backends.Display.PixelFormat)format, (ELFSharp.ELF.Endianess)endianess, Backends.Display.PixelFormat.ARGB8888, ELFSharp.ELF.Endianess.LittleEndian);
210             }
211         }
212 
213         [Transient]
214         private IPixelConverter converter;
215 
216         private int frameWidth;
217         private int frameHeight;
218         private IVideo video;
219         private Backends.Display.PixelFormat? format;
220         private ELFSharp.ELF.Endianess? endianess;
221 
222         // Even if newFrameEvent was set before saving the emulation it doesn't matter
223         // as we ultimately would have to start the `WaitForFrame` loop from the beginning either way
224         [Constructor(false)]
225         private AutoResetEvent newFrameEvent;
226         private readonly TimeSpan globalTimeout;
227         private readonly BlockingCollection<byte[]> framesQueue;
228     }
229 }
230 
231