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