VtgAsComputeContext.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. using Ryujinx.Common;
  2. using Ryujinx.Graphics.GAL;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Runtime.InteropServices;
  6. namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
  7. {
  8. /// <summary>
  9. /// Vertex, tessellation and geometry as compute shader context.
  10. /// </summary>
  11. class VtgAsComputeContext : IDisposable
  12. {
  13. private const int DummyBufferSize = 16;
  14. private readonly GpuContext _context;
  15. /// <summary>
  16. /// Cache of buffer textures used for vertex and index buffers.
  17. /// </summary>
  18. private class BufferTextureCache : IDisposable
  19. {
  20. private readonly Dictionary<Format, ITexture> _cache;
  21. /// <summary>
  22. /// Creates a new instance of the buffer texture cache.
  23. /// </summary>
  24. public BufferTextureCache()
  25. {
  26. _cache = new();
  27. }
  28. /// <summary>
  29. /// Gets a cached or creates and caches a buffer texture with the specified format.
  30. /// </summary>
  31. /// <param name="renderer">Renderer where the texture will be used</param>
  32. /// <param name="format">Format of the buffer texture</param>
  33. /// <returns>Buffer texture</returns>
  34. public ITexture Get(IRenderer renderer, Format format)
  35. {
  36. if (!_cache.TryGetValue(format, out ITexture bufferTexture))
  37. {
  38. bufferTexture = renderer.CreateTexture(new TextureCreateInfo(
  39. 1,
  40. 1,
  41. 1,
  42. 1,
  43. 1,
  44. 1,
  45. 1,
  46. 1,
  47. format,
  48. DepthStencilMode.Depth,
  49. Target.TextureBuffer,
  50. SwizzleComponent.Red,
  51. SwizzleComponent.Green,
  52. SwizzleComponent.Blue,
  53. SwizzleComponent.Alpha));
  54. _cache.Add(format, bufferTexture);
  55. }
  56. return bufferTexture;
  57. }
  58. protected virtual void Dispose(bool disposing)
  59. {
  60. if (disposing)
  61. {
  62. foreach (var texture in _cache.Values)
  63. {
  64. texture.Release();
  65. }
  66. _cache.Clear();
  67. }
  68. }
  69. public void Dispose()
  70. {
  71. Dispose(true);
  72. GC.SuppressFinalize(this);
  73. }
  74. }
  75. /// <summary>
  76. /// Buffer state.
  77. /// </summary>
  78. private struct Buffer
  79. {
  80. /// <summary>
  81. /// Buffer handle.
  82. /// </summary>
  83. public BufferHandle Handle;
  84. /// <summary>
  85. /// Current free buffer offset.
  86. /// </summary>
  87. public int Offset;
  88. /// <summary>
  89. /// Total buffer size in bytes.
  90. /// </summary>
  91. public int Size;
  92. }
  93. /// <summary>
  94. /// Index buffer state.
  95. /// </summary>
  96. private readonly struct IndexBuffer
  97. {
  98. /// <summary>
  99. /// Buffer handle.
  100. /// </summary>
  101. public BufferHandle Handle { get; }
  102. /// <summary>
  103. /// Index count.
  104. /// </summary>
  105. public int Count { get; }
  106. /// <summary>
  107. /// Size in bytes.
  108. /// </summary>
  109. public int Size { get; }
  110. /// <summary>
  111. /// Creates a new index buffer state.
  112. /// </summary>
  113. /// <param name="handle">Buffer handle</param>
  114. /// <param name="count">Index count</param>
  115. /// <param name="size">Size in bytes</param>
  116. public IndexBuffer(BufferHandle handle, int count, int size)
  117. {
  118. Handle = handle;
  119. Count = count;
  120. Size = size;
  121. }
  122. /// <summary>
  123. /// Creates a full range starting from the beggining of the buffer.
  124. /// </summary>
  125. /// <returns>Range</returns>
  126. public readonly BufferRange ToRange()
  127. {
  128. return new BufferRange(Handle, 0, Size);
  129. }
  130. /// <summary>
  131. /// Creates a range starting from the beggining of the buffer, with the specified size.
  132. /// </summary>
  133. /// <param name="size">Size in bytes of the range</param>
  134. /// <returns>Range</returns>
  135. public readonly BufferRange ToRange(int size)
  136. {
  137. return new BufferRange(Handle, 0, size);
  138. }
  139. }
  140. private readonly BufferTextureCache[] _bufferTextures;
  141. private BufferHandle _dummyBuffer;
  142. private Buffer _vertexDataBuffer;
  143. private Buffer _geometryVertexDataBuffer;
  144. private Buffer _geometryIndexDataBuffer;
  145. private BufferHandle _sequentialIndexBuffer;
  146. private int _sequentialIndexBufferCount;
  147. private readonly Dictionary<PrimitiveTopology, IndexBuffer> _topologyRemapBuffers;
  148. /// <summary>
  149. /// Vertex information buffer updater.
  150. /// </summary>
  151. public VertexInfoBufferUpdater VertexInfoBufferUpdater { get; }
  152. /// <summary>
  153. /// Creates a new instance of the vertex, tessellation and geometry as compute shader context.
  154. /// </summary>
  155. /// <param name="context"></param>
  156. public VtgAsComputeContext(GpuContext context)
  157. {
  158. _context = context;
  159. _bufferTextures = new BufferTextureCache[Constants.TotalVertexBuffers + 2];
  160. _topologyRemapBuffers = new();
  161. VertexInfoBufferUpdater = new(context.Renderer);
  162. }
  163. /// <summary>
  164. /// Gets the number of complete primitives that can be formed with a given vertex count, for a given topology.
  165. /// </summary>
  166. /// <param name="primitiveType">Topology</param>
  167. /// <param name="count">Vertex count</param>
  168. /// <returns>Total of complete primitives</returns>
  169. public static int GetPrimitivesCount(PrimitiveTopology primitiveType, int count)
  170. {
  171. return primitiveType switch
  172. {
  173. PrimitiveTopology.Lines => count / 2,
  174. PrimitiveTopology.LinesAdjacency => count / 4,
  175. PrimitiveTopology.LineLoop => count > 1 ? count : 0,
  176. PrimitiveTopology.LineStrip => Math.Max(count - 1, 0),
  177. PrimitiveTopology.LineStripAdjacency => Math.Max(count - 3, 0),
  178. PrimitiveTopology.Triangles => count / 3,
  179. PrimitiveTopology.TrianglesAdjacency => count / 6,
  180. PrimitiveTopology.TriangleStrip or
  181. PrimitiveTopology.TriangleFan or
  182. PrimitiveTopology.Polygon => Math.Max(count - 2, 0),
  183. PrimitiveTopology.TriangleStripAdjacency => Math.Max(count - 2, 0) / 2,
  184. PrimitiveTopology.Quads => (count / 4) * 2, // In triangles.
  185. PrimitiveTopology.QuadStrip => Math.Max((count - 2) / 2, 0) * 2, // In triangles.
  186. _ => count,
  187. };
  188. }
  189. /// <summary>
  190. /// Gets the total of vertices that a single primitive has, for the specified topology.
  191. /// </summary>
  192. /// <param name="primitiveType">Topology</param>
  193. /// <returns>Vertex count</returns>
  194. private static int GetVerticesPerPrimitive(PrimitiveTopology primitiveType)
  195. {
  196. return primitiveType switch
  197. {
  198. PrimitiveTopology.Lines or
  199. PrimitiveTopology.LineLoop or
  200. PrimitiveTopology.LineStrip => 2,
  201. PrimitiveTopology.LinesAdjacency or
  202. PrimitiveTopology.LineStripAdjacency => 4,
  203. PrimitiveTopology.Triangles or
  204. PrimitiveTopology.TriangleStrip or
  205. PrimitiveTopology.TriangleFan or
  206. PrimitiveTopology.Polygon => 3,
  207. PrimitiveTopology.TrianglesAdjacency or
  208. PrimitiveTopology.TriangleStripAdjacency => 6,
  209. PrimitiveTopology.Quads or
  210. PrimitiveTopology.QuadStrip => 3, // 2 triangles.
  211. _ => 1,
  212. };
  213. }
  214. /// <summary>
  215. /// Gets a cached or creates a new buffer that can be used to map linear indices to ones
  216. /// of a specified topology, and build complete primitives.
  217. /// </summary>
  218. /// <param name="topology">Topology</param>
  219. /// <param name="count">Number of input vertices that needs to be mapped using that buffer</param>
  220. /// <returns>Remap buffer range</returns>
  221. public BufferRange GetOrCreateTopologyRemapBuffer(PrimitiveTopology topology, int count)
  222. {
  223. if (!_topologyRemapBuffers.TryGetValue(topology, out IndexBuffer buffer) || buffer.Count < count)
  224. {
  225. if (buffer.Handle != BufferHandle.Null)
  226. {
  227. _context.Renderer.DeleteBuffer(buffer.Handle);
  228. }
  229. buffer = CreateTopologyRemapBuffer(topology, count);
  230. _topologyRemapBuffers[topology] = buffer;
  231. return buffer.ToRange();
  232. }
  233. return buffer.ToRange(Math.Max(GetPrimitivesCount(topology, count) * GetVerticesPerPrimitive(topology), 1) * sizeof(uint));
  234. }
  235. /// <summary>
  236. /// Creates a new topology remap buffer.
  237. /// </summary>
  238. /// <param name="topology">Topology</param>
  239. /// <param name="count">Maximum of vertices that will be accessed</param>
  240. /// <returns>Remap buffer range</returns>
  241. private IndexBuffer CreateTopologyRemapBuffer(PrimitiveTopology topology, int count)
  242. {
  243. // Size can't be zero as creating zero sized buffers is invalid.
  244. Span<int> data = new int[Math.Max(GetPrimitivesCount(topology, count) * GetVerticesPerPrimitive(topology), 1)];
  245. switch (topology)
  246. {
  247. case PrimitiveTopology.Points:
  248. case PrimitiveTopology.Lines:
  249. case PrimitiveTopology.LinesAdjacency:
  250. case PrimitiveTopology.Triangles:
  251. case PrimitiveTopology.TrianglesAdjacency:
  252. case PrimitiveTopology.Patches:
  253. for (int index = 0; index < data.Length; index++)
  254. {
  255. data[index] = index;
  256. }
  257. break;
  258. case PrimitiveTopology.LineLoop:
  259. data[^1] = 0;
  260. for (int index = 0; index < ((data.Length - 1) & ~1); index += 2)
  261. {
  262. data[index] = index >> 1;
  263. data[index + 1] = (index >> 1) + 1;
  264. }
  265. break;
  266. case PrimitiveTopology.LineStrip:
  267. for (int index = 0; index < ((data.Length - 1) & ~1); index += 2)
  268. {
  269. data[index] = index >> 1;
  270. data[index + 1] = (index >> 1) + 1;
  271. }
  272. break;
  273. case PrimitiveTopology.TriangleStrip:
  274. int tsTrianglesCount = data.Length / 3;
  275. int tsOutIndex = 3;
  276. if (tsTrianglesCount > 0)
  277. {
  278. data[0] = 0;
  279. data[1] = 1;
  280. data[2] = 2;
  281. }
  282. for (int tri = 1; tri < tsTrianglesCount; tri++)
  283. {
  284. int baseIndex = tri * 3;
  285. if ((tri & 1) != 0)
  286. {
  287. data[baseIndex] = tsOutIndex - 1;
  288. data[baseIndex + 1] = tsOutIndex - 2;
  289. data[baseIndex + 2] = tsOutIndex++;
  290. }
  291. else
  292. {
  293. data[baseIndex] = tsOutIndex - 2;
  294. data[baseIndex + 1] = tsOutIndex - 1;
  295. data[baseIndex + 2] = tsOutIndex++;
  296. }
  297. }
  298. break;
  299. case PrimitiveTopology.TriangleFan:
  300. case PrimitiveTopology.Polygon:
  301. int tfTrianglesCount = data.Length / 3;
  302. int tfOutIndex = 1;
  303. for (int index = 0; index < tfTrianglesCount * 3; index += 3)
  304. {
  305. data[index] = 0;
  306. data[index + 1] = tfOutIndex;
  307. data[index + 2] = ++tfOutIndex;
  308. }
  309. break;
  310. case PrimitiveTopology.Quads:
  311. int qQuadsCount = data.Length / 6;
  312. for (int quad = 0; quad < qQuadsCount; quad++)
  313. {
  314. int index = quad * 6;
  315. int qIndex = quad * 4;
  316. data[index] = qIndex;
  317. data[index + 1] = qIndex + 1;
  318. data[index + 2] = qIndex + 2;
  319. data[index + 3] = qIndex;
  320. data[index + 4] = qIndex + 2;
  321. data[index + 5] = qIndex + 3;
  322. }
  323. break;
  324. case PrimitiveTopology.QuadStrip:
  325. int qsQuadsCount = data.Length / 6;
  326. if (qsQuadsCount > 0)
  327. {
  328. data[0] = 0;
  329. data[1] = 1;
  330. data[2] = 2;
  331. data[3] = 0;
  332. data[4] = 2;
  333. data[5] = 3;
  334. }
  335. for (int quad = 1; quad < qsQuadsCount; quad++)
  336. {
  337. int index = quad * 6;
  338. int qIndex = quad * 2;
  339. data[index] = qIndex + 1;
  340. data[index + 1] = qIndex;
  341. data[index + 2] = qIndex + 2;
  342. data[index + 3] = qIndex + 1;
  343. data[index + 4] = qIndex + 2;
  344. data[index + 5] = qIndex + 3;
  345. }
  346. break;
  347. case PrimitiveTopology.LineStripAdjacency:
  348. for (int index = 0; index < ((data.Length - 3) & ~3); index += 4)
  349. {
  350. int lIndex = index >> 2;
  351. data[index] = lIndex;
  352. data[index + 1] = lIndex + 1;
  353. data[index + 2] = lIndex + 2;
  354. data[index + 3] = lIndex + 3;
  355. }
  356. break;
  357. case PrimitiveTopology.TriangleStripAdjacency:
  358. int tsaTrianglesCount = data.Length / 6;
  359. int tsaOutIndex = 6;
  360. if (tsaTrianglesCount > 0)
  361. {
  362. data[0] = 0;
  363. data[1] = 1;
  364. data[2] = 2;
  365. data[3] = 3;
  366. data[4] = 4;
  367. data[5] = 5;
  368. }
  369. for (int tri = 1; tri < tsaTrianglesCount; tri++)
  370. {
  371. int baseIndex = tri * 6;
  372. if ((tri & 1) != 0)
  373. {
  374. data[baseIndex] = tsaOutIndex - 2;
  375. data[baseIndex + 1] = tsaOutIndex - 1;
  376. data[baseIndex + 2] = tsaOutIndex - 4;
  377. data[baseIndex + 3] = tsaOutIndex - 3;
  378. data[baseIndex + 4] = tsaOutIndex++;
  379. data[baseIndex + 5] = tsaOutIndex++;
  380. }
  381. else
  382. {
  383. data[baseIndex] = tsaOutIndex - 4;
  384. data[baseIndex + 1] = tsaOutIndex - 3;
  385. data[baseIndex + 2] = tsaOutIndex - 2;
  386. data[baseIndex + 3] = tsaOutIndex - 1;
  387. data[baseIndex + 4] = tsaOutIndex++;
  388. data[baseIndex + 5] = tsaOutIndex++;
  389. }
  390. }
  391. break;
  392. }
  393. ReadOnlySpan<byte> dataBytes = MemoryMarshal.Cast<int, byte>(data);
  394. BufferHandle buffer = _context.Renderer.CreateBuffer(dataBytes.Length);
  395. _context.Renderer.SetBufferData(buffer, 0, dataBytes);
  396. return new IndexBuffer(buffer, count, dataBytes.Length);
  397. }
  398. /// <summary>
  399. /// Gets a buffer texture with a given format, for the given index.
  400. /// </summary>
  401. /// <param name="index">Index of the buffer texture</param>
  402. /// <param name="format">Format of the buffer texture</param>
  403. /// <returns>Buffer texture</returns>
  404. public ITexture EnsureBufferTexture(int index, Format format)
  405. {
  406. return (_bufferTextures[index] ??= new()).Get(_context.Renderer, format);
  407. }
  408. /// <summary>
  409. /// Gets the offset and size of usable storage on the output vertex buffer.
  410. /// </summary>
  411. /// <param name="size">Size in bytes that will be used</param>
  412. /// <returns>Usable offset and size on the buffer</returns>
  413. public (int, int) GetVertexDataBuffer(int size)
  414. {
  415. return EnsureBuffer(ref _vertexDataBuffer, size);
  416. }
  417. /// <summary>
  418. /// Gets the offset and size of usable storage on the output geometry shader vertex buffer.
  419. /// </summary>
  420. /// <param name="size">Size in bytes that will be used</param>
  421. /// <returns>Usable offset and size on the buffer</returns>
  422. public (int, int) GetGeometryVertexDataBuffer(int size)
  423. {
  424. return EnsureBuffer(ref _geometryVertexDataBuffer, size);
  425. }
  426. /// <summary>
  427. /// Gets the offset and size of usable storage on the output geometry shader index buffer.
  428. /// </summary>
  429. /// <param name="size">Size in bytes that will be used</param>
  430. /// <returns>Usable offset and size on the buffer</returns>
  431. public (int, int) GetGeometryIndexDataBuffer(int size)
  432. {
  433. return EnsureBuffer(ref _geometryIndexDataBuffer, size);
  434. }
  435. /// <summary>
  436. /// Gets a range of the output vertex buffer for binding.
  437. /// </summary>
  438. /// <param name="offset">Offset of the range</param>
  439. /// <param name="size">Size of the range in bytes</param>
  440. /// <returns>Range</returns>
  441. public BufferRange GetVertexDataBufferRange(int offset, int size)
  442. {
  443. return new BufferRange(_vertexDataBuffer.Handle, offset, size);
  444. }
  445. /// <summary>
  446. /// Gets a range of the output geometry shader vertex buffer for binding.
  447. /// </summary>
  448. /// <param name="offset">Offset of the range</param>
  449. /// <param name="size">Size of the range in bytes</param>
  450. /// <returns>Range</returns>
  451. public BufferRange GetGeometryVertexDataBufferRange(int offset, int size)
  452. {
  453. return new BufferRange(_geometryVertexDataBuffer.Handle, offset, size);
  454. }
  455. /// <summary>
  456. /// Gets a range of the output geometry shader index buffer for binding.
  457. /// </summary>
  458. /// <param name="offset">Offset of the range</param>
  459. /// <param name="size">Size of the range in bytes</param>
  460. /// <returns>Range</returns>
  461. public BufferRange GetGeometryIndexDataBufferRange(int offset, int size)
  462. {
  463. return new BufferRange(_geometryIndexDataBuffer.Handle, offset, size);
  464. }
  465. /// <summary>
  466. /// Gets the range for a dummy 16 bytes buffer, filled with zeros.
  467. /// </summary>
  468. /// <returns>Dummy buffer range</returns>
  469. public BufferRange GetDummyBufferRange()
  470. {
  471. if (_dummyBuffer == BufferHandle.Null)
  472. {
  473. _dummyBuffer = _context.Renderer.CreateBuffer(DummyBufferSize);
  474. _context.Renderer.Pipeline.ClearBuffer(_dummyBuffer, 0, DummyBufferSize, 0);
  475. }
  476. return new BufferRange(_dummyBuffer, 0, DummyBufferSize);
  477. }
  478. /// <summary>
  479. /// Gets the range for a sequential index buffer, with ever incrementing index values.
  480. /// </summary>
  481. /// <param name="count">Minimum number of indices that the buffer should have</param>
  482. /// <returns>Buffer handle</returns>
  483. public BufferHandle GetSequentialIndexBuffer(int count)
  484. {
  485. if (_sequentialIndexBufferCount < count)
  486. {
  487. if (_sequentialIndexBuffer != BufferHandle.Null)
  488. {
  489. _context.Renderer.DeleteBuffer(_sequentialIndexBuffer);
  490. }
  491. _sequentialIndexBuffer = _context.Renderer.CreateBuffer(count * sizeof(uint));
  492. _sequentialIndexBufferCount = count;
  493. Span<int> data = new int[count];
  494. for (int index = 0; index < count; index++)
  495. {
  496. data[index] = index;
  497. }
  498. _context.Renderer.SetBufferData(_sequentialIndexBuffer, 0, MemoryMarshal.Cast<int, byte>(data));
  499. }
  500. return _sequentialIndexBuffer;
  501. }
  502. /// <summary>
  503. /// Ensure that a buffer exists, is large enough, and allocates a sub-region of the specified size inside the buffer.
  504. /// </summary>
  505. /// <param name="buffer">Buffer state</param>
  506. /// <param name="size">Required size in bytes</param>
  507. /// <returns>Allocated offset and size</returns>
  508. private (int, int) EnsureBuffer(ref Buffer buffer, int size)
  509. {
  510. int newSize = buffer.Offset + size;
  511. if (buffer.Size < newSize)
  512. {
  513. if (buffer.Handle != BufferHandle.Null)
  514. {
  515. _context.Renderer.DeleteBuffer(buffer.Handle);
  516. }
  517. buffer.Handle = _context.Renderer.CreateBuffer(newSize);
  518. buffer.Size = newSize;
  519. }
  520. int offset = buffer.Offset;
  521. buffer.Offset = BitUtils.AlignUp(newSize, _context.Capabilities.StorageBufferOffsetAlignment);
  522. return (offset, size);
  523. }
  524. /// <summary>
  525. /// Frees all buffer sub-regions that were previously allocated.
  526. /// </summary>
  527. public void FreeBuffers()
  528. {
  529. _vertexDataBuffer.Offset = 0;
  530. _geometryVertexDataBuffer.Offset = 0;
  531. _geometryIndexDataBuffer.Offset = 0;
  532. }
  533. protected virtual void Dispose(bool disposing)
  534. {
  535. if (disposing)
  536. {
  537. for (int index = 0; index < _bufferTextures.Length; index++)
  538. {
  539. _bufferTextures[index]?.Dispose();
  540. _bufferTextures[index] = null;
  541. }
  542. DestroyIfNotNull(ref _dummyBuffer);
  543. DestroyIfNotNull(ref _vertexDataBuffer.Handle);
  544. DestroyIfNotNull(ref _geometryVertexDataBuffer.Handle);
  545. DestroyIfNotNull(ref _geometryIndexDataBuffer.Handle);
  546. DestroyIfNotNull(ref _sequentialIndexBuffer);
  547. foreach (var indexBuffer in _topologyRemapBuffers.Values)
  548. {
  549. _context.Renderer.DeleteBuffer(indexBuffer.Handle);
  550. }
  551. _topologyRemapBuffers.Clear();
  552. }
  553. }
  554. /// <summary>
  555. /// Deletes a buffer if the handle is valid (not null), then sets the handle to null.
  556. /// </summary>
  557. /// <param name="handle">Buffer handle</param>
  558. private void DestroyIfNotNull(ref BufferHandle handle)
  559. {
  560. if (handle != BufferHandle.Null)
  561. {
  562. _context.Renderer.DeleteBuffer(handle);
  563. handle = BufferHandle.Null;
  564. }
  565. }
  566. public void Dispose()
  567. {
  568. Dispose(true);
  569. GC.SuppressFinalize(this);
  570. }
  571. }
  572. }