1 /*
2 * Copyright (c) 2023 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 #include "recording/draw_cmd_list.h"
17
18 #include <cstddef>
19 #include <memory>
20
21 #include "recording/draw_cmd.h"
22 #include "recording/recording_canvas.h"
23 #include "utils/log.h"
24 #include "utils/performanceCaculate.h"
25
26 namespace OHOS {
27 namespace Rosen {
28 namespace Drawing {
29
CreateFromData(const CmdListData & data,bool isCopy)30 std::shared_ptr<DrawCmdList> DrawCmdList::CreateFromData(const CmdListData& data, bool isCopy)
31 {
32 auto cmdList = std::make_shared<DrawCmdList>(DrawCmdList::UnmarshalMode::DEFERRED);
33 if (isCopy) {
34 cmdList->opAllocator_.BuildFromDataWithCopy(data.first, data.second);
35 } else {
36 cmdList->opAllocator_.BuildFromData(data.first, data.second);
37 }
38
39 int32_t* width = static_cast<int32_t*>(cmdList->opAllocator_.OffsetToAddr(0, sizeof(int32_t)));
40 int32_t* height = static_cast<int32_t*>(cmdList->opAllocator_.OffsetToAddr(sizeof(int32_t), sizeof(int32_t)));
41 if (width && height) {
42 cmdList->width_ = *width;
43 cmdList->height_ = *height;
44 } else {
45 cmdList->width_ = 0;
46 cmdList->height_ = 0;
47 }
48 return cmdList;
49 }
50
DrawCmdList(DrawCmdList::UnmarshalMode mode)51 DrawCmdList::DrawCmdList(DrawCmdList::UnmarshalMode mode) : width_(0), height_(0), mode_(mode) {}
52
DrawCmdList(int32_t width,int32_t height,DrawCmdList::UnmarshalMode mode)53 DrawCmdList::DrawCmdList(int32_t width, int32_t height, DrawCmdList::UnmarshalMode mode)
54 : width_(width), height_(height), mode_(mode)
55 {
56 opAllocator_.Add(&width_, sizeof(int32_t));
57 opAllocator_.Add(&height_, sizeof(int32_t));
58 }
59
~DrawCmdList()60 DrawCmdList::~DrawCmdList()
61 {
62 if (drawOpItems_.size() == 0) {
63 UnmarshallingDrawOps();
64 }
65 ClearOp();
66 }
67
AddDrawOp(std::shared_ptr<DrawOpItem> && drawOpItem)68 bool DrawCmdList::AddDrawOp(std::shared_ptr<DrawOpItem>&& drawOpItem)
69 {
70 if (mode_ != DrawCmdList::UnmarshalMode::DEFERRED) {
71 return false;
72 }
73 std::lock_guard<std::recursive_mutex> lock(mutex_);
74 drawOpItems_.emplace_back(drawOpItem);
75 return true;
76 }
77
ClearOp()78 void DrawCmdList::ClearOp()
79 {
80 {
81 std::lock_guard<std::recursive_mutex> lock(mutex_);
82 opAllocator_.ClearData();
83 opAllocator_.Add(&width_, sizeof(int32_t));
84 opAllocator_.Add(&height_, sizeof(int32_t));
85 imageAllocator_.ClearData();
86 bitmapAllocator_.ClearData();
87 imageMap_.clear();
88 imageHandleVec_.clear();
89 drawOpItems_.clear();
90 lastOpGenSize_ = 0;
91 lastOpItemOffset_ = std::nullopt;
92 opCnt_ = 0;
93 }
94 {
95 std::lock_guard<std::mutex> lock(recordCmdMutex_);
96 recordCmdVec_.clear();
97 }
98 {
99 std::lock_guard<std::mutex> lock(imageObjectMutex_);
100 imageObjectVec_.clear();
101 }
102 {
103 std::lock_guard<std::mutex> lock(imageBaseObjMutex_);
104 imageBaseObjVec_.clear();
105 }
106 }
107
GetWidth() const108 int32_t DrawCmdList::GetWidth() const
109 {
110 return width_;
111 }
112
GetHeight() const113 int32_t DrawCmdList::GetHeight() const
114 {
115 return height_;
116 }
117
SetWidth(int32_t width)118 void DrawCmdList::SetWidth(int32_t width)
119 {
120 width_ = width;
121 }
122
SetHeight(int32_t height)123 void DrawCmdList::SetHeight(int32_t height)
124 {
125 height_ = height;
126 }
127
IsEmpty() const128 bool DrawCmdList::IsEmpty() const
129 {
130 if (mode_ == DrawCmdList::UnmarshalMode::DEFERRED) {
131 return drawOpItems_.empty();
132 }
133 size_t offset = 2 * sizeof(int32_t); // 2 is width and height.Offset of first OpItem is behind the w and h
134 if (opAllocator_.GetSize() <= offset && drawOpItems_.size() == 0) {
135 return true;
136 }
137 return false;
138 }
139
GetOpItemSize() const140 size_t DrawCmdList::GetOpItemSize() const
141 {
142 return mode_ == DrawCmdList::UnmarshalMode::DEFERRED ? drawOpItems_.size() : opCnt_;
143 }
144
GetOpsWithDesc() const145 std::string DrawCmdList::GetOpsWithDesc() const
146 {
147 std::string desc;
148 for (auto& item : drawOpItems_) {
149 if (item == nullptr) {
150 continue;
151 }
152 desc += item->GetOpDesc();
153 desc += "\n";
154 }
155 LOGD("DrawCmdList::GetOpsWithDesc %{public}s, opitem sz: %{public}zu", desc.c_str(), drawOpItems_.size());
156 return desc;
157 }
158
Dump(std::string & out)159 void DrawCmdList::Dump(std::string& out)
160 {
161 std::lock_guard<std::recursive_mutex> lock(mutex_);
162 for (auto& item : drawOpItems_) {
163 if (item == nullptr) {
164 continue;
165 }
166 item->Dump(out);
167 out += " ";
168 }
169 if (drawOpItems_.size() > 0) {
170 out.pop_back();
171 }
172 }
173
MarshallingDrawOps()174 void DrawCmdList::MarshallingDrawOps()
175 {
176 if (mode_ == DrawCmdList::UnmarshalMode::IMMEDIATE) {
177 return;
178 }
179 std::lock_guard<std::recursive_mutex> lock(mutex_);
180 if (replacedOpListForVector_.empty()) {
181 for (auto& op : drawOpItems_) {
182 if (op) {
183 op->Marshalling(*this);
184 }
185 }
186 return;
187 }
188 for (auto& [index, op] : replacedOpListForVector_) {
189 op.swap(drawOpItems_[index]);
190 }
191 std::vector<uint32_t> opIndexForCache(replacedOpListForVector_.size());
192 uint32_t opReplaceIndex = 0;
193 for (auto index = 0u; index < drawOpItems_.size(); ++index) {
194 if (drawOpItems_[index]) {
195 drawOpItems_[index]->Marshalling(*this);
196 }
197 if (index == static_cast<size_t>(replacedOpListForVector_[opReplaceIndex].first)) {
198 opIndexForCache[opReplaceIndex] = lastOpItemOffset_.value();
199 ++opReplaceIndex;
200 }
201 }
202 for (auto index = 0u; index < replacedOpListForVector_.size(); ++index) {
203 if (replacedOpListForVector_[index].second) {
204 replacedOpListForVector_[index].second->Marshalling(*this);
205 }
206 replacedOpListForBuffer_.emplace_back(opIndexForCache[index], lastOpItemOffset_.value());
207 }
208 }
209
CaculatePerformanceOpType()210 void DrawCmdList::CaculatePerformanceOpType()
211 {
212 size_t offset = offset_;
213 const int caculatePerformaceCount = 500; // 被测单接口用例至少出现500次以上
214 std::map<uint32_t, uint32_t> opTypeCountMap;
215 do {
216 void* itemPtr = opAllocator_.OffsetToAddr(offset, sizeof(OpItem));
217 auto* curOpItemPtr = static_cast<OpItem*>(itemPtr);
218 if (curOpItemPtr == nullptr) {
219 break;
220 }
221 uint32_t type = curOpItemPtr->GetType();
222 if (opTypeCountMap.find(type) != opTypeCountMap.end()) {
223 if (++opTypeCountMap[type] > caculatePerformaceCount) {
224 performanceCaculateOpType_ = type;
225 DRAWING_PERFORMANCE_START_CACULATE;
226 return;
227 }
228 } else {
229 opTypeCountMap[type] = 1; // 记录出现的第1次
230 }
231 offset = curOpItemPtr->GetNextOpItemOffset();
232 } while (offset != 0);
233 }
234
UnmarshallingDrawOps()235 void DrawCmdList::UnmarshallingDrawOps()
236 {
237 if (PerformanceCaculate::GetDrawingTestRecordingEnabled()) {
238 CaculatePerformanceOpType();
239 }
240 if (performanceCaculateOpType_ != 0) {
241 LOGI("Drawing Performance UnmarshallingDrawOps begin %{public}lld", PerformanceCaculate::GetUpTime());
242 }
243
244 if (opAllocator_.GetSize() <= offset_ || width_ <= 0 || height_ <= 0) {
245 return;
246 }
247
248 UnmarshallingPlayer player = { *this };
249 drawOpItems_.clear();
250 lastOpGenSize_ = 0;
251 uint32_t opReplaceIndex = 0;
252 size_t offset = offset_;
253 do {
254 void* itemPtr = opAllocator_.OffsetToAddr(offset, sizeof(OpItem));
255 auto* curOpItemPtr = static_cast<OpItem*>(itemPtr);
256 if (curOpItemPtr == nullptr) {
257 LOGE("DrawCmdList::UnmarshallingOps failed, opItem is nullptr");
258 break;
259 }
260 uint32_t type = curOpItemPtr->GetType();
261 auto op = player.Unmarshalling(type, itemPtr, opAllocator_.GetSize() - offset);
262 if (!op) {
263 offset = curOpItemPtr->GetNextOpItemOffset();
264 continue;
265 }
266 if (opReplaceIndex < replacedOpListForBuffer_.size() &&
267 replacedOpListForBuffer_[opReplaceIndex].first == offset) {
268 auto* replacePtr = opAllocator_.OffsetToAddr(
269 replacedOpListForBuffer_[opReplaceIndex].second, sizeof(OpItem));
270 if (replacePtr == nullptr) {
271 LOGE("DrawCmdList::Unmarshalling replace Ops failed, replace op is nullptr");
272 break;
273 }
274 auto* replaceOpItemPtr = static_cast<OpItem*>(replacePtr);
275 size_t avaliableSize = opAllocator_.GetSize() - replacedOpListForBuffer_[opReplaceIndex].second;
276 auto replaceOp = player.Unmarshalling(replaceOpItemPtr->GetType(), replacePtr, avaliableSize);
277 if (replaceOp) {
278 drawOpItems_.emplace_back(replaceOp);
279 replacedOpListForVector_.emplace_back((drawOpItems_.size() - 1), op);
280 } else {
281 drawOpItems_.emplace_back(op);
282 }
283 opReplaceIndex++;
284 } else {
285 drawOpItems_.emplace_back(op);
286 }
287 offset = curOpItemPtr->GetNextOpItemOffset();
288 if (!replacedOpListForBuffer_.empty() && offset >= replacedOpListForBuffer_[0].second) {
289 LOGD("DrawCmdList::UnmarshallingOps seek end by cache textOps");
290 break;
291 }
292 } while (offset != 0);
293 lastOpGenSize_ = opAllocator_.GetSize();
294
295 if ((int)imageAllocator_.GetSize() > 0) {
296 imageAllocator_.ClearData();
297 }
298
299 if (performanceCaculateOpType_ != 0) {
300 LOGI("Drawing Performance UnmarshallingDrawOps end %{public}lld", PerformanceCaculate::GetUpTime());
301 }
302 }
303
Playback(Canvas & canvas,const Rect * rect)304 void DrawCmdList::Playback(Canvas& canvas, const Rect* rect)
305 {
306 if (width_ <= 0 || height_ <= 0) {
307 return;
308 }
309 if (performanceCaculateOpType_ != 0) {
310 LOGI("Drawing Performance Playback begin %{public}lld", PerformanceCaculate::GetUpTime());
311 }
312 if (canvas.GetDrawingType() == DrawingType::RECORDING) {
313 PlaybackToDrawCmdList(static_cast<RecordingCanvas&>(canvas).GetDrawCmdList());
314 return;
315 }
316 std::lock_guard<std::recursive_mutex> lock(mutex_);
317 #ifdef ROSEN_OHOS
318 // invalidate cache if high contrast flag changed
319 if (isCached_ && canvas.isHighContrastEnabled() != cachedHighContrast_) {
320 ClearCache();
321 }
322 // Generate or clear cache if cache state changed
323 if (canvas.GetCacheType() == Drawing::CacheType::ENABLED && !isCached_) {
324 GenerateCache(&canvas, rect);
325 } else if (canvas.GetCacheType() == Drawing::CacheType::DISABLED && isCached_) {
326 ClearCache();
327 }
328 #endif
329 Rect tmpRect;
330 if (rect != nullptr) {
331 tmpRect = *rect;
332 }
333 if (mode_ == DrawCmdList::UnmarshalMode::IMMEDIATE) {
334 PlaybackByBuffer(canvas, &tmpRect);
335 } else if (mode_ == DrawCmdList::UnmarshalMode::DEFERRED) {
336 PlaybackByVector(canvas, &tmpRect);
337 }
338 if (performanceCaculateOpType_ != 0) {
339 DRAWING_PERFORMANCE_STOP_CACULATE;
340 performanceCaculateOpType_ = 0;
341 LOGI("Drawing Performance Playback end %{public}lld", PerformanceCaculate::GetUpTime());
342 }
343 }
344
GenerateCache(Canvas * canvas,const Rect * rect)345 void DrawCmdList::GenerateCache(Canvas* canvas, const Rect* rect)
346 {
347 #ifdef ROSEN_OHOS
348 if (isCached_) {
349 LOGD("DrawCmdList::GenerateCache Invoke multiple times");
350 return;
351 }
352
353 std::lock_guard<std::recursive_mutex> lock(mutex_);
354 if (mode_ == DrawCmdList::UnmarshalMode::IMMEDIATE) {
355 GenerateCacheByBuffer(canvas, rect);
356 } else if (mode_ == DrawCmdList::UnmarshalMode::DEFERRED) {
357 GenerateCacheByVector(canvas, rect);
358 }
359 #endif
360 }
361
GetIsCache() const362 bool DrawCmdList::GetIsCache() const
363 {
364 return isCached_;
365 }
366
SetIsCache(bool isCached)367 void DrawCmdList::SetIsCache(bool isCached)
368 {
369 isCached_ = isCached;
370 }
371
GetCachedHighContrast() const372 bool DrawCmdList::GetCachedHighContrast() const
373 {
374 return cachedHighContrast_;
375 }
376
SetCachedHighContrast(bool cachedHighContrast)377 void DrawCmdList::SetCachedHighContrast(bool cachedHighContrast)
378 {
379 cachedHighContrast_ = cachedHighContrast;
380 }
381
GetReplacedOpList()382 std::vector<std::pair<size_t, size_t>> DrawCmdList::GetReplacedOpList()
383 {
384 return replacedOpListForBuffer_;
385 }
386
SetReplacedOpList(std::vector<std::pair<size_t,size_t>> replacedOpList)387 void DrawCmdList::SetReplacedOpList(std::vector<std::pair<size_t, size_t>> replacedOpList)
388 {
389 replacedOpListForBuffer_ = replacedOpList;
390 }
391
UpdateNodeIdToPicture(NodeId nodeId)392 void DrawCmdList::UpdateNodeIdToPicture(NodeId nodeId)
393 {
394 if (drawOpItems_.size() == 0) {
395 return;
396 }
397 for (size_t i = 0; i < drawOpItems_.size(); ++i) {
398 auto opItem = drawOpItems_[i];
399 if (!opItem) {
400 continue;
401 }
402 opItem->SetNodeId(nodeId);
403 }
404 }
405
ClearCache()406 void DrawCmdList::ClearCache()
407 {
408 #ifdef ROSEN_OHOS
409 // restore the original op
410 for (auto& [index, op] : replacedOpListForVector_) {
411 op.swap(drawOpItems_[index]);
412 }
413 replacedOpListForVector_.clear();
414 replacedOpListForBuffer_.clear();
415 isCached_ = false;
416 #endif
417 }
418
GenerateCacheByVector(Canvas * canvas,const Rect * rect)419 void DrawCmdList::GenerateCacheByVector(Canvas* canvas, const Rect* rect)
420 {
421 #ifdef ROSEN_OHOS
422 if (drawOpItems_.size() == 0) {
423 return;
424 }
425 uint32_t opSize = drawOpItems_.size();
426 for (auto index = 0u; index < opSize; ++index) {
427 std::shared_ptr<DrawOpItem> op = drawOpItems_[index];
428 if (!op || op->GetType() != DrawOpItem::TEXT_BLOB_OPITEM) {
429 continue;
430 }
431 DrawTextBlobOpItem* textBlobOp = static_cast<DrawTextBlobOpItem*>(op.get());
432 auto replaceCache = textBlobOp->GenerateCachedOpItem(canvas);
433 if (replaceCache) {
434 replacedOpListForVector_.emplace_back(index, op);
435 drawOpItems_[index] = replaceCache;
436 }
437 }
438 isCached_ = true;
439 cachedHighContrast_ = canvas && canvas->isHighContrastEnabled();
440 #endif
441 }
442
GenerateCacheByBuffer(Canvas * canvas,const Rect * rect)443 void DrawCmdList::GenerateCacheByBuffer(Canvas* canvas, const Rect* rect)
444 {
445 #ifdef ROSEN_OHOS
446 if (opAllocator_.GetSize() <= offset_) {
447 return;
448 }
449
450 size_t offset = offset_;
451 GenerateCachedOpItemPlayer player = { *this, canvas, rect };
452 uint32_t maxOffset = opAllocator_.GetSize();
453 do {
454 void* itemPtr = opAllocator_.OffsetToAddr(offset, sizeof(OpItem));
455 auto* curOpItemPtr = static_cast<OpItem*>(itemPtr);
456 if (curOpItemPtr == nullptr) {
457 LOGE("DrawCmdList::GenerateCacheByBuffer failed, opItem is nullptr");
458 break;
459 }
460 size_t avaliableSize = opAllocator_.GetSize() - offset;
461 bool replaceSuccess = player.GenerateCachedOpItem(curOpItemPtr->GetType(), itemPtr, avaliableSize);
462 if (replaceSuccess) {
463 replacedOpListForBuffer_.push_back({offset, lastOpItemOffset_.value()});
464 itemPtr = opAllocator_.OffsetToAddr(offset, sizeof(OpItem));
465 curOpItemPtr = static_cast<OpItem*>(itemPtr);
466 if (curOpItemPtr == nullptr) {
467 LOGE("DrawCmdList::GenerateCache failed, opItem is nullptr");
468 break;
469 }
470 }
471 offset = curOpItemPtr->GetNextOpItemOffset();
472 } while (offset != 0 && offset < maxOffset);
473 isCached_ = true;
474 cachedHighContrast_ = canvas && canvas->isHighContrastEnabled();
475 #endif
476 }
477
PlaybackToDrawCmdList(std::shared_ptr<DrawCmdList> drawCmdList)478 void DrawCmdList::PlaybackToDrawCmdList(std::shared_ptr<DrawCmdList> drawCmdList)
479 {
480 if (!drawCmdList) {
481 return;
482 }
483 std::lock_guard<std::recursive_mutex> lock(mutex_);
484 if (mode_ == DrawCmdList::UnmarshalMode::DEFERRED) {
485 std::lock_guard<std::recursive_mutex> lock(drawCmdList->mutex_);
486 drawCmdList->drawOpItems_.insert(drawCmdList->drawOpItems_.end(), drawOpItems_.begin(), drawOpItems_.end());
487 return;
488 }
489
490 void* addr = opAllocator_.OffsetToAddr(offset_, 0);
491 if (addr == nullptr) {
492 return;
493 }
494
495 {
496 std::lock_guard<std::mutex> lock(drawCmdList->recordCmdMutex_);
497 drawCmdList->recordCmdVec_.swap(recordCmdVec_);
498 }
499 #ifdef SUPPORT_OHOS_PIXMAP
500 {
501 std::lock_guard<std::mutex> lock(drawCmdList->imageObjectMutex_);
502 drawCmdList->imageObjectVec_.swap(imageObjectVec_);
503 }
504 #endif
505 {
506 std::lock_guard<std::mutex> lock(drawCmdList->imageBaseObjMutex_);
507 drawCmdList->imageBaseObjVec_.swap(imageBaseObjVec_);
508 }
509 size_t size = opAllocator_.GetSize() - offset_;
510 auto imageData = GetAllImageData();
511 auto bitmapData = GetAllBitmapData();
512 drawCmdList->opAllocator_.Add(addr, size);
513 if (imageData.first != nullptr && imageData.second != 0) {
514 drawCmdList->AddImageData(imageData.first, imageData.second);
515 }
516
517 if (bitmapData.first != nullptr && bitmapData.second != 0) {
518 drawCmdList->AddBitmapData(bitmapData.first, bitmapData.second);
519 }
520 }
521
PlaybackByVector(Canvas & canvas,const Rect * rect)522 void DrawCmdList::PlaybackByVector(Canvas& canvas, const Rect* rect)
523 {
524 if (drawOpItems_.empty()) {
525 return;
526 }
527 for (auto op : drawOpItems_) {
528 if (op) {
529 op->Playback(&canvas, rect);
530 }
531 }
532 canvas.DetachPaint();
533 }
534
PlaybackByBuffer(Canvas & canvas,const Rect * rect)535 void DrawCmdList::PlaybackByBuffer(Canvas& canvas, const Rect* rect)
536 {
537 if (opAllocator_.GetSize() <= offset_) {
538 return;
539 }
540 size_t offset = offset_;
541 if (lastOpGenSize_ != opAllocator_.GetSize()) {
542 UnmarshallingPlayer player = { *this };
543 drawOpItems_.clear();
544 do {
545 void* itemPtr = opAllocator_.OffsetToAddr(offset, sizeof(OpItem));
546 auto* curOpItemPtr = static_cast<OpItem*>(itemPtr);
547 if (curOpItemPtr == nullptr) {
548 break;
549 }
550 uint32_t type = curOpItemPtr->GetType();
551 if (auto op = player.Unmarshalling(type, itemPtr, opAllocator_.GetSize() - offset)) {
552 drawOpItems_.emplace_back(op);
553 }
554 offset = curOpItemPtr->GetNextOpItemOffset();
555 } while (offset != 0);
556 lastOpGenSize_ = opAllocator_.GetSize();
557 }
558 for (auto op : drawOpItems_) {
559 if (op) {
560 op->Playback(&canvas, rect);
561 }
562 }
563 canvas.DetachPaint();
564 }
565
CountTextBlobNum()566 size_t DrawCmdList::CountTextBlobNum()
567 {
568 size_t textBlobCnt = 0;
569 if (mode_ == DrawCmdList::UnmarshalMode::IMMEDIATE) {
570 size_t offset = offset_;
571 size_t maxOffset = opAllocator_.GetSize();
572 do {
573 void* itemPtr = opAllocator_.OffsetToAddr(offset, sizeof(OpItem));
574 auto* curOpItemPtr = static_cast<OpItem*>(itemPtr);
575 if (curOpItemPtr == nullptr) {
576 break;
577 }
578 uint32_t type = curOpItemPtr->GetType();
579 if (type == DrawOpItem::TEXT_BLOB_OPITEM) {
580 textBlobCnt++;
581 }
582 offset = curOpItemPtr->GetNextOpItemOffset();
583 } while (offset != 0 && offset < maxOffset);
584 }
585 return textBlobCnt;
586 }
587
Purge()588 void DrawCmdList::Purge()
589 {
590 std::lock_guard<std::recursive_mutex> lock(mutex_);
591 for (auto op : drawOpItems_) {
592 if (!op) {
593 continue;
594 }
595 auto type = op->GetType();
596 if (type == DrawOpItem::PIXELMAP_RECT_OPITEM ||
597 type == DrawOpItem::PIXELMAP_WITH_PARM_OPITEM) {
598 op->Purge();
599 }
600 }
601 }
602 } // namespace Drawing
603 } // namespace Rosen
604 } // namespace OHOS
605