/ src / Ryujinx.HLE / HOS / Applets / SoftwareKeyboard / SoftwareKeyboardRendererBase.cs
SoftwareKeyboardRendererBase.cs
  1  using Ryujinx.HLE.UI;
  2  using Ryujinx.Memory;
  3  using SkiaSharp;
  4  using System;
  5  using System.Diagnostics;
  6  using System.IO;
  7  using System.Reflection;
  8  using System.Runtime.InteropServices;
  9  
 10  namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
 11  {
 12      /// <summary>
 13      /// Base class that generates the graphics for the software keyboard applet during inline mode.
 14      /// </summary>
 15      internal class SoftwareKeyboardRendererBase
 16      {
 17          public const int TextBoxBlinkThreshold = 8;
 18  
 19          const string MessageText = "Please use the keyboard to input text";
 20          const string AcceptText = "Accept";
 21          const string CancelText = "Cancel";
 22          const string ControllerToggleText = "Toggle input";
 23  
 24          private readonly object _bufferLock = new();
 25  
 26          private RenderingSurfaceInfo _surfaceInfo = null;
 27          private SKImageInfo _imageInfo;
 28          private SKSurface _surface = null;
 29          private byte[] _bufferData = null;
 30  
 31          private readonly SKBitmap _ryujinxLogo = null;
 32          private readonly SKBitmap _padAcceptIcon = null;
 33          private readonly SKBitmap _padCancelIcon = null;
 34          private readonly SKBitmap _keyModeIcon = null;
 35  
 36          private readonly float _textBoxOutlineWidth;
 37          private readonly float _padPressedPenWidth;
 38  
 39          private readonly SKColor _textNormalColor;
 40          private readonly SKColor _textSelectedColor;
 41          private readonly SKColor _textOverCursorColor;
 42  
 43          private readonly SKPaint _panelBrush;
 44          private readonly SKPaint _disabledBrush;
 45          private readonly SKPaint _cursorBrush;
 46          private readonly SKPaint _selectionBoxBrush;
 47  
 48          private readonly SKPaint _textBoxOutlinePen;
 49          private readonly SKPaint _cursorPen;
 50          private readonly SKPaint _selectionBoxPen;
 51          private readonly SKPaint _padPressedPen;
 52  
 53          private readonly int _inputTextFontSize;
 54          private SKFont _messageFont;
 55          private SKFont _inputTextFont;
 56          private SKFont _labelsTextFont;
 57  
 58          private SKRect _panelRectangle;
 59          private SKPoint _logoPosition;
 60          private float _messagePositionY;
 61  
 62          public SoftwareKeyboardRendererBase(IHostUITheme uiTheme)
 63          {
 64              int ryujinxLogoSize = 32;
 65  
 66              string ryujinxIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Logo_Ryujinx.png";
 67              _ryujinxLogo = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, ryujinxIconPath, ryujinxLogoSize, ryujinxLogoSize);
 68  
 69              string padAcceptIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnA.png";
 70              string padCancelIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnB.png";
 71              string keyModeIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_KeyF6.png";
 72  
 73              _padAcceptIcon = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, padAcceptIconPath, 0, 0);
 74              _padCancelIcon = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, padCancelIconPath, 0, 0);
 75              _keyModeIcon = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, keyModeIconPath, 0, 0);
 76  
 77              var panelColor = ToColor(uiTheme.DefaultBackgroundColor, 255);
 78              var panelTransparentColor = ToColor(uiTheme.DefaultBackgroundColor, 150);
 79              var borderColor = ToColor(uiTheme.DefaultBorderColor);
 80              var selectionBackgroundColor = ToColor(uiTheme.SelectionBackgroundColor);
 81  
 82              _textNormalColor = ToColor(uiTheme.DefaultForegroundColor);
 83              _textSelectedColor = ToColor(uiTheme.SelectionForegroundColor);
 84              _textOverCursorColor = ToColor(uiTheme.DefaultForegroundColor, null, true);
 85  
 86              float cursorWidth = 2;
 87  
 88              _textBoxOutlineWidth = 2;
 89              _padPressedPenWidth = 2;
 90  
 91              _panelBrush = new SKPaint()
 92              {
 93                  Color = panelColor,
 94                  IsAntialias = true
 95              };
 96              _disabledBrush = new SKPaint()
 97              {
 98                  Color = panelTransparentColor,
 99                  IsAntialias = true
100              };
101              _cursorBrush = new SKPaint() { Color = _textNormalColor, IsAntialias = true };
102              _selectionBoxBrush = new SKPaint() { Color = selectionBackgroundColor, IsAntialias = true };
103  
104              _textBoxOutlinePen = new SKPaint()
105              {
106                  Color = borderColor,
107                  StrokeWidth = _textBoxOutlineWidth,
108                  IsStroke = true,
109                  IsAntialias = true
110              };
111              _cursorPen = new SKPaint() { Color = _textNormalColor, StrokeWidth = cursorWidth, IsStroke = true, IsAntialias = true };
112              _selectionBoxPen = new SKPaint() { Color = selectionBackgroundColor, StrokeWidth = cursorWidth, IsStroke = true, IsAntialias = true };
113              _padPressedPen = new SKPaint() { Color = borderColor, StrokeWidth = _padPressedPenWidth, IsStroke = true, IsAntialias = true };
114  
115              _inputTextFontSize = 20;
116  
117              CreateFonts(uiTheme.FontFamily);
118          }
119  
120          private void CreateFonts(string uiThemeFontFamily)
121          {
122              // Try a list of fonts in case any of them is not available in the system.
123  
124              string[] availableFonts = {
125                  uiThemeFontFamily,
126                  "Liberation Sans",
127                  "FreeSans",
128                  "DejaVu Sans",
129                  "Lucida Grande",
130              };
131  
132              foreach (string fontFamily in availableFonts)
133              {
134                  try
135                  {
136                      using var typeface = SKTypeface.FromFamilyName(fontFamily, SKFontStyle.Normal);
137                      _messageFont = new SKFont(typeface, 26);
138                      _inputTextFont = new SKFont(typeface, _inputTextFontSize);
139                      _labelsTextFont = new SKFont(typeface, 24);
140  
141                      return;
142                  }
143                  catch
144                  {
145                  }
146              }
147  
148              throw new Exception($"None of these fonts were found in the system: {String.Join(", ", availableFonts)}!");
149          }
150  
151          private static SKColor ToColor(ThemeColor color, byte? overrideAlpha = null, bool flipRgb = false)
152          {
153              var a = (byte)(color.A * 255);
154              var r = (byte)(color.R * 255);
155              var g = (byte)(color.G * 255);
156              var b = (byte)(color.B * 255);
157  
158              if (flipRgb)
159              {
160                  r = (byte)(255 - r);
161                  g = (byte)(255 - g);
162                  b = (byte)(255 - b);
163              }
164  
165              return new SKColor(r, g, b, overrideAlpha.GetValueOrDefault(a));
166          }
167  
168          private static SKBitmap LoadResource(Assembly assembly, string resourcePath, int newWidth, int newHeight)
169          {
170              Stream resourceStream = assembly.GetManifestResourceStream(resourcePath);
171  
172              return LoadResource(resourceStream, newWidth, newHeight);
173          }
174  
175          private static SKBitmap LoadResource(Stream resourceStream, int newWidth, int newHeight)
176          {
177              Debug.Assert(resourceStream != null);
178  
179              var bitmap = SKBitmap.Decode(resourceStream);
180  
181              if (newHeight != 0 && newWidth != 0)
182              {
183                  var resized = bitmap.Resize(new SKImageInfo(newWidth, newHeight), SKFilterQuality.High);
184                  if (resized != null)
185                  {
186                      bitmap.Dispose();
187                      bitmap = resized;
188                  }
189              }
190  
191              return bitmap;
192          }
193  
194          private void DrawImmutableElements()
195          {
196              if (_surface == null)
197              {
198                  return;
199              }
200              var canvas = _surface.Canvas;
201  
202              canvas.Clear(SKColors.Transparent);
203              canvas.DrawRect(_panelRectangle, _panelBrush);
204              canvas.DrawBitmap(_ryujinxLogo, _logoPosition);
205  
206              float halfWidth = _panelRectangle.Width / 2;
207              float buttonsY = _panelRectangle.Top + 185;
208  
209              SKPoint disableButtonPosition = new(halfWidth + 180, buttonsY);
210  
211              DrawControllerToggle(canvas, disableButtonPosition);
212          }
213  
214          public void DrawMutableElements(SoftwareKeyboardUIState state)
215          {
216              if (_surface == null)
217              {
218                  return;
219              }
220  
221              using var paint = new SKPaint(_messageFont)
222              {
223                  Color = _textNormalColor,
224                  IsAntialias = true
225              };
226  
227              var canvas = _surface.Canvas;
228              var messageRectangle = MeasureString(MessageText, paint);
229              float messagePositionX = (_panelRectangle.Width - messageRectangle.Width) / 2 - messageRectangle.Left;
230              float messagePositionY = _messagePositionY - messageRectangle.Top;
231              var messagePosition = new SKPoint(messagePositionX, messagePositionY);
232              var messageBoundRectangle = SKRect.Create(messagePositionX, messagePositionY, messageRectangle.Width, messageRectangle.Height);
233  
234              canvas.DrawRect(messageBoundRectangle, _panelBrush);
235  
236              canvas.DrawText(MessageText, messagePosition.X, messagePosition.Y + _messageFont.Metrics.XHeight + _messageFont.Metrics.Descent, paint);
237  
238              if (!state.TypingEnabled)
239              {
240                  // Just draw a semi-transparent rectangle on top to fade the component with the background.
241                  // TODO (caian): This will not work if one decides to add make background semi-transparent as well.
242  
243                  canvas.DrawRect(messageBoundRectangle, _disabledBrush);
244              }
245  
246              DrawTextBox(canvas, state);
247  
248              float halfWidth = _panelRectangle.Width / 2;
249              float buttonsY = _panelRectangle.Top + 185;
250  
251              SKPoint acceptButtonPosition = new(halfWidth - 180, buttonsY);
252              SKPoint cancelButtonPosition = new(halfWidth, buttonsY);
253              SKPoint disableButtonPosition = new(halfWidth + 180, buttonsY);
254  
255              DrawPadButton(canvas, acceptButtonPosition, _padAcceptIcon, AcceptText, state.AcceptPressed, state.ControllerEnabled);
256              DrawPadButton(canvas, cancelButtonPosition, _padCancelIcon, CancelText, state.CancelPressed, state.ControllerEnabled);
257  
258          }
259  
260          public void CreateSurface(RenderingSurfaceInfo surfaceInfo)
261          {
262              if (_surfaceInfo != null)
263              {
264                  return;
265              }
266  
267              _surfaceInfo = surfaceInfo;
268  
269              Debug.Assert(_surfaceInfo.ColorFormat == Services.SurfaceFlinger.ColorFormat.A8B8G8R8);
270  
271              // Use the whole area of the image to draw, even the alignment, otherwise it may shear the final
272              // image if the pitch is different.
273              uint totalWidth = _surfaceInfo.Pitch / 4;
274              uint totalHeight = _surfaceInfo.Size / _surfaceInfo.Pitch;
275  
276              Debug.Assert(_surfaceInfo.Width <= totalWidth);
277              Debug.Assert(_surfaceInfo.Height <= totalHeight);
278              Debug.Assert(_surfaceInfo.Pitch * _surfaceInfo.Height <= _surfaceInfo.Size);
279  
280              _imageInfo = new SKImageInfo((int)totalWidth, (int)totalHeight, SKColorType.Rgba8888);
281              _surface = SKSurface.Create(_imageInfo);
282  
283              ComputeConstants();
284              DrawImmutableElements();
285          }
286  
287          private void ComputeConstants()
288          {
289              int totalWidth = (int)_surfaceInfo.Width;
290              int totalHeight = (int)_surfaceInfo.Height;
291  
292              int panelHeight = 240;
293              int panelPositionY = totalHeight - panelHeight;
294  
295              _panelRectangle = SKRect.Create(0, panelPositionY, totalWidth, panelHeight);
296  
297              _messagePositionY = panelPositionY + 60;
298  
299              int logoPositionX = (totalWidth - _ryujinxLogo.Width) / 2;
300              int logoPositionY = panelPositionY + 18;
301  
302              _logoPosition = new SKPoint(logoPositionX, logoPositionY);
303          }
304          private static SKRect MeasureString(string text, SKPaint paint)
305          {
306              SKRect bounds = SKRect.Empty;
307  
308              if (text == "")
309              {
310                  paint.MeasureText(" ", ref bounds);
311              }
312              else
313              {
314                  paint.MeasureText(text, ref bounds);
315              }
316  
317              return bounds;
318          }
319  
320          private static SKRect MeasureString(ReadOnlySpan<char> text, SKPaint paint)
321          {
322              SKRect bounds = SKRect.Empty;
323  
324              if (text == "")
325              {
326                  paint.MeasureText(" ", ref bounds);
327              }
328              else
329              {
330                  paint.MeasureText(text, ref bounds);
331              }
332  
333              return bounds;
334          }
335  
336          private void DrawTextBox(SKCanvas canvas, SoftwareKeyboardUIState state)
337          {
338              using var textPaint = new SKPaint(_labelsTextFont)
339              {
340                  IsAntialias = true,
341                  Color = _textNormalColor
342              };
343              var inputTextRectangle = MeasureString(state.InputText, textPaint);
344  
345              float boxWidth = (int)(Math.Max(300, inputTextRectangle.Width + inputTextRectangle.Left + 8));
346              float boxHeight = 32;
347              float boxY = _panelRectangle.Top + 110;
348              float boxX = (int)((_panelRectangle.Width - boxWidth) / 2);
349  
350              SKRect boxRectangle = SKRect.Create(boxX, boxY, boxWidth, boxHeight);
351  
352              SKRect boundRectangle = SKRect.Create(_panelRectangle.Left, boxY - _textBoxOutlineWidth,
353                      _panelRectangle.Width, boxHeight + 2 * _textBoxOutlineWidth);
354  
355              canvas.DrawRect(boundRectangle, _panelBrush);
356  
357              canvas.DrawRect(boxRectangle, _textBoxOutlinePen);
358  
359              float inputTextX = (_panelRectangle.Width - inputTextRectangle.Width) / 2 - inputTextRectangle.Left;
360              float inputTextY = boxY + 5;
361  
362              var inputTextPosition = new SKPoint(inputTextX, inputTextY);
363              canvas.DrawText(state.InputText, inputTextPosition.X, inputTextPosition.Y + (_labelsTextFont.Metrics.XHeight + _labelsTextFont.Metrics.Descent), textPaint);
364  
365              // Draw the cursor on top of the text and redraw the text with a different color if necessary.
366  
367              SKColor cursorTextColor;
368              SKPaint cursorBrush;
369              SKPaint cursorPen;
370  
371              float cursorPositionYTop = inputTextY + 1;
372              float cursorPositionYBottom = cursorPositionYTop + _inputTextFontSize + 1;
373              float cursorPositionXLeft;
374              float cursorPositionXRight;
375  
376              bool cursorVisible = false;
377  
378              if (state.CursorBegin != state.CursorEnd)
379              {
380                  Debug.Assert(state.InputText.Length > 0);
381  
382                  cursorTextColor = _textSelectedColor;
383                  cursorBrush = _selectionBoxBrush;
384                  cursorPen = _selectionBoxPen;
385  
386                  ReadOnlySpan<char> textUntilBegin = state.InputText.AsSpan(0, state.CursorBegin);
387                  ReadOnlySpan<char> textUntilEnd = state.InputText.AsSpan(0, state.CursorEnd);
388  
389                  var selectionBeginRectangle = MeasureString(textUntilBegin, textPaint);
390                  var selectionEndRectangle = MeasureString(textUntilEnd, textPaint);
391  
392                  cursorVisible = true;
393                  cursorPositionXLeft = inputTextX + selectionBeginRectangle.Width + selectionBeginRectangle.Left;
394                  cursorPositionXRight = inputTextX + selectionEndRectangle.Width + selectionEndRectangle.Left;
395              }
396              else
397              {
398                  cursorTextColor = _textOverCursorColor;
399                  cursorBrush = _cursorBrush;
400                  cursorPen = _cursorPen;
401  
402                  if (state.TextBoxBlinkCounter < TextBoxBlinkThreshold)
403                  {
404                      // Show the blinking cursor.
405  
406                      int cursorBegin = Math.Min(state.InputText.Length, state.CursorBegin);
407                      ReadOnlySpan<char> textUntilCursor = state.InputText.AsSpan(0, cursorBegin);
408                      var cursorTextRectangle = MeasureString(textUntilCursor, textPaint);
409  
410                      cursorVisible = true;
411                      cursorPositionXLeft = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.Left;
412  
413                      if (state.OverwriteMode)
414                      {
415                          // The blinking cursor is in overwrite mode so it takes the size of a character.
416  
417                          if (state.CursorBegin < state.InputText.Length)
418                          {
419                              textUntilCursor = state.InputText.AsSpan(0, cursorBegin + 1);
420                              cursorTextRectangle = MeasureString(textUntilCursor, textPaint);
421                              cursorPositionXRight = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.Left;
422                          }
423                          else
424                          {
425                              cursorPositionXRight = cursorPositionXLeft + _inputTextFontSize / 2;
426                          }
427                      }
428                      else
429                      {
430                          // The blinking cursor is in insert mode so it is only a line.
431                          cursorPositionXRight = cursorPositionXLeft;
432                      }
433                  }
434                  else
435                  {
436                      cursorPositionXLeft = inputTextX;
437                      cursorPositionXRight = inputTextX;
438                  }
439              }
440  
441              if (state.TypingEnabled && cursorVisible)
442              {
443                  float cursorWidth = cursorPositionXRight - cursorPositionXLeft;
444                  float cursorHeight = cursorPositionYBottom - cursorPositionYTop;
445  
446                  if (cursorWidth == 0)
447                  {
448                      canvas.DrawLine(new SKPoint(cursorPositionXLeft, cursorPositionYTop),
449                          new SKPoint(cursorPositionXLeft, cursorPositionYBottom),
450                          cursorPen);
451                  }
452                  else
453                  {
454                      var cursorRectangle = SKRect.Create(cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight);
455  
456                      canvas.DrawRect(cursorRectangle, cursorPen);
457                      canvas.DrawRect(cursorRectangle, cursorBrush);
458  
459                      using var textOverCursor = SKSurface.Create(new SKImageInfo((int)cursorRectangle.Width, (int)cursorRectangle.Height, SKColorType.Rgba8888));
460                      var textOverCanvas = textOverCursor.Canvas;
461                      var textRelativePosition = new SKPoint(inputTextPosition.X - cursorRectangle.Left, inputTextPosition.Y - cursorRectangle.Top);
462  
463                      using var cursorPaint = new SKPaint(_inputTextFont)
464                      {
465                          Color = cursorTextColor,
466                          IsAntialias = true
467                      };
468  
469                      textOverCanvas.DrawText(state.InputText, textRelativePosition.X, textRelativePosition.Y + _inputTextFont.Metrics.XHeight + _inputTextFont.Metrics.Descent, cursorPaint);
470  
471                      var cursorPosition = new SKPoint((int)cursorRectangle.Left, (int)cursorRectangle.Top);
472                      textOverCursor.Flush();
473                      canvas.DrawSurface(textOverCursor, cursorPosition);
474                  }
475              }
476              else if (!state.TypingEnabled)
477              {
478                  // Just draw a semi-transparent rectangle on top to fade the component with the background.
479                  // TODO (caian): This will not work if one decides to add make background semi-transparent as well.
480  
481                  canvas.DrawRect(boundRectangle, _disabledBrush);
482              }
483          }
484  
485          private void DrawPadButton(SKCanvas canvas, SKPoint point, SKBitmap icon, string label, bool pressed, bool enabled)
486          {
487              // Use relative positions so we can center the entire drawing later.
488  
489              float iconX = 0;
490              float iconY = 0;
491              float iconWidth = icon.Width;
492              float iconHeight = icon.Height;
493  
494              using var paint = new SKPaint(_labelsTextFont)
495              {
496                  Color = _textNormalColor,
497                  IsAntialias = true
498              };
499  
500              var labelRectangle = MeasureString(label, paint);
501  
502              float labelPositionX = iconWidth + 8 - labelRectangle.Left;
503              float labelPositionY = 3;
504  
505              float fullWidth = labelPositionX + labelRectangle.Width + labelRectangle.Left;
506              float fullHeight = iconHeight;
507  
508              // Convert all relative positions into absolute.
509  
510              float originX = (int)(point.X - fullWidth / 2);
511              float originY = (int)(point.Y - fullHeight / 2);
512  
513              iconX += originX;
514              iconY += originY;
515  
516              var iconPosition = new SKPoint((int)iconX, (int)iconY);
517              var labelPosition = new SKPoint(labelPositionX + originX, labelPositionY + originY);
518  
519              var selectedRectangle = SKRect.Create(originX - 2 * _padPressedPenWidth, originY - 2 * _padPressedPenWidth,
520                  fullWidth + 4 * _padPressedPenWidth, fullHeight + 4 * _padPressedPenWidth);
521  
522              var boundRectangle = SKRect.Create(originX, originY, fullWidth, fullHeight);
523              boundRectangle.Inflate(4 * _padPressedPenWidth, 4 * _padPressedPenWidth);
524  
525              canvas.DrawRect(boundRectangle, _panelBrush);
526              canvas.DrawBitmap(icon, iconPosition);
527              canvas.DrawText(label, labelPosition.X, labelPosition.Y + _labelsTextFont.Metrics.XHeight + _labelsTextFont.Metrics.Descent, paint);
528  
529              if (enabled)
530              {
531                  if (pressed)
532                  {
533                      canvas.DrawRect(selectedRectangle, _padPressedPen);
534                  }
535              }
536              else
537              {
538                  // Just draw a semi-transparent rectangle on top to fade the component with the background.
539                  // TODO (caian): This will not work if one decides to add make background semi-transparent as well.
540  
541                  canvas.DrawRect(boundRectangle, _disabledBrush);
542              }
543          }
544  
545          private void DrawControllerToggle(SKCanvas canvas, SKPoint point)
546          {
547              using var paint = new SKPaint(_labelsTextFont)
548              {
549                  IsAntialias = true,
550                  Color = _textNormalColor
551              };
552              var labelRectangle = MeasureString(ControllerToggleText, paint);
553  
554              // Use relative positions so we can center the entire drawing later.
555  
556              float keyWidth = _keyModeIcon.Width;
557              float keyHeight = _keyModeIcon.Height;
558  
559              float labelPositionX = keyWidth + 8 - labelRectangle.Left;
560              float labelPositionY = -labelRectangle.Top - 1;
561  
562              float keyX = 0;
563              float keyY = (int)((labelPositionY + labelRectangle.Height - keyHeight) / 2);
564  
565              float fullWidth = labelPositionX + labelRectangle.Width;
566              float fullHeight = Math.Max(labelPositionY + labelRectangle.Height, keyHeight);
567  
568              // Convert all relative positions into absolute.
569  
570              float originX = (int)(point.X - fullWidth / 2);
571              float originY = (int)(point.Y - fullHeight / 2);
572  
573              keyX += originX;
574              keyY += originY;
575  
576              var labelPosition = new SKPoint(labelPositionX + originX, labelPositionY + originY);
577              var overlayPosition = new SKPoint((int)keyX, (int)keyY);
578  
579              canvas.DrawBitmap(_keyModeIcon, overlayPosition);
580              canvas.DrawText(ControllerToggleText, labelPosition.X, labelPosition.Y + _labelsTextFont.Metrics.XHeight, paint);
581          }
582  
583          public unsafe void CopyImageToBuffer()
584          {
585              lock (_bufferLock)
586              {
587                  if (_surface == null)
588                  {
589                      return;
590                  }
591  
592                  // Convert the pixel format used in the image to the one used in the Switch surface.
593                  _surface.Flush();
594  
595                  var buffer = new byte[_imageInfo.BytesSize];
596                  fixed (byte* bufferPtr = buffer)
597                  {
598                      if (!_surface.ReadPixels(_imageInfo, (nint)bufferPtr, _imageInfo.RowBytes, 0, 0))
599                      {
600                          return;
601                      }
602                  }
603  
604                  _bufferData = buffer;
605  
606                  Debug.Assert(buffer.Length == _surfaceInfo.Size);
607              }
608          }
609  
610          public bool WriteBufferToMemory(IVirtualMemoryManager destination, ulong position)
611          {
612              lock (_bufferLock)
613              {
614                  if (_bufferData == null)
615                  {
616                      return false;
617                  }
618  
619                  try
620                  {
621                      destination.Write(position, _bufferData);
622                  }
623                  catch
624                  {
625                      return false;
626                  }
627  
628                  return true;
629              }
630          }
631      }
632  }