Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
debugger_bindings.cpp
Go to the documentation of this file.
1/*
2 * This file defines SkpDebugPlayer, a class which loads a SKP or MSKP file and draws it
3 * to an SkSurface with annotation, and detailed playback controls. It holds as many DebugCanvases
4 * as there are frames in the file.
5 *
6 * It also defines emscripten bindings for SkpDebugPlayer and other classes necessary to us it.
7 *
8 * Copyright 2019 Google LLC
9 *
10 * Use of this source code is governed by a BSD-style license that can be
11 * found in the LICENSE file.
12 */
13
19#include "src/base/SkBase64.h"
22#include "tools/SkSharingProc.h"
27
28#include <memory>
29#include <string>
30#include <string_view>
31#include <vector>
32#include <map>
33#include <emscripten.h>
34#include <emscripten/bind.h>
35
36#ifdef CK_ENABLE_WEBGL
42
43#include <GL/gl.h>
44#include <emscripten/html5.h>
45#endif
46
48
49// file signature for SkMultiPictureDocument
50// TODO(nifong): make public and include from SkMultiPictureDocument.h
51static constexpr char kMultiMagic[] = "Skia Multi-Picture Doc\n\n";
52
54
61
62// TODO(kjlubick) Should this handle colorspace
66
67
69 public:
71 udm(UrlDataManager(SkString("/data"))){}
72
73 /* loadSkp deserializes a skp file that has been copied into the shared WASM memory.
74 * cptr - a pointer to the data to deserialize.
75 * length - length of the data in bytes.
76 * The caller must allocate the memory with M._malloc where M is the wasm module in javascript
77 * and copy the data into M.buffer at the pointer returned by malloc.
78 *
79 * uintptr_t is used here because emscripten will not allow binding of functions with pointers
80 * to primitive types. We can instead pass a number and cast it to whatever kind of
81 * pointer we're expecting.
82 *
83 * Returns an error string which is populated in the case that the file cannot be read.
84 */
85 std::string loadSkp(uintptr_t cptr, int length) {
86 const uint8_t* data = reinterpret_cast<const uint8_t*>(cptr);
87 // Both traditional and multi-frame skp files have a magic word
88 SkMemoryStream stream(data, length);
89 SkDebugf("make stream at %p, with %d bytes\n",data, length);
90 const bool isMulti = memcmp(data, kMultiMagic, sizeof(kMultiMagic) - 1) == 0;
91
92
93 if (isMulti) {
94 SkDebugf("Try reading as a multi-frame skp\n");
95 const auto& error = loadMultiFrame(&stream);
96 if (!error.empty()) { return error; }
97 } else {
98 SkDebugf("Try reading as single-frame skp\n");
99 // TODO(nifong): Rely on SkPicture's return errors once it provides some.
100 std::unique_ptr<DebugCanvas> canvas = loadSingleFrame(&stream);
101 if (!canvas) {
102 return "Error loading single frame";
103 }
104 frames.push_back(std::move(canvas));
105 }
106 return "";
107 }
108
109 /* drawTo asks the debug canvas to draw from the beginning of the picture
110 * to the given command and flush the canvas.
111 */
112 void drawTo(SkSurface* surface, int32_t index) {
113 // Set the command within the frame or layer event being drawn.
114 if (fInspectedLayer >= 0) {
115 fLayerManager->setCommand(fInspectedLayer, fp, index);
116 } else {
117 index = constrainFrameCommand(index);
118 }
119
120 auto* canvas = surface->getCanvas();
121 canvas->clear(SK_ColorTRANSPARENT);
122 if (fInspectedLayer >= 0) {
123 // when it's a layer event we're viewing, we use the layer manager to render it.
124 fLayerManager->drawLayerEventTo(surface, fInspectedLayer, fp);
125 } else {
126 // otherwise, its a frame at the top level.
127 frames[fp]->drawTo(surface->getCanvas(), index);
128 }
129#ifdef CK_ENABLE_WEBGL
131#endif
132 }
133
134 // Draws to the end of the current frame.
136 auto* canvas = surface->getCanvas();
137 canvas->clear(SK_ColorTRANSPARENT);
138 frames[fp]->draw(surface->getCanvas());
139#ifdef CK_ENABLE_WEBGL
141#endif
142 }
143
144 // Gets the bounds for the given frame
145 // (or layer update, assuming there is one at that frame for fInspectedLayer)
147 if (fInspectedLayer < 0) {
148 return fBoundsArray[frame];
149 }
150 auto summary = fLayerManager->event(fInspectedLayer, fp);
151 return SkIRect::MakeWH(summary.layerWidth, summary.layerHeight);
152 }
153
154 // Gets the bounds for the current frame
156 return getBoundsForFrame(fp);
157 }
158
159 // returns the debugcanvas of the current frame, or the current draw event when inspecting
160 // a layer.
162 if (fInspectedLayer >=0) {
163 return fLayerManager->getEventDebugCanvas(fInspectedLayer, fp);
164 } else {
165 return frames[fp].get();
166 }
167 }
168
169 // The following three operations apply to every debugcanvas because they are overdraw features.
170 // There is only one toggle for them on the app, they are global settings.
171 // However, there's not a simple way to make the debugcanvases pull settings from a central
172 // location so we set it on all of them at once.
173 void setOverdrawVis(bool on) {
174 for (size_t i=0; i < frames.size(); i++) {
175 frames[i]->setOverdrawViz(on);
176 }
177 fLayerManager->setOverdrawViz(on);
178 }
179 void setGpuOpBounds(bool on) {
180 for (size_t i=0; i < frames.size(); i++) {
181 frames[i]->setDrawGpuOpBounds(on);
182 }
183 fLayerManager->setDrawGpuOpBounds(on);
184 }
186 for (size_t i=0; i < frames.size(); i++) {
187 frames[i]->setClipVizColor(SkColor(color));
188 }
189 fLayerManager->setClipVizColor(SkColor(color));
190 }
191 void setAndroidClipViz(bool on) {
192 for (size_t i=0; i < frames.size(); i++) {
193 frames[i]->setAndroidClipViz(on);
194 }
195 // doesn't matter in layers
196 }
197 void setOriginVisible(bool on) {
198 for (size_t i=0; i < frames.size(); i++) {
199 frames[i]->setOriginVisible(on);
200 }
201 }
202 // The two operations below only apply to the current frame, because they concern the command
203 // list, which is unique to each frame.
204 void deleteCommand(int index) {
206 }
207 void setCommandVisibility(int index, bool visible) {
208 visibleCanvas()->toggleCommand(index, visible);
209 }
210 int getSize() const {
211 if (fInspectedLayer >=0) {
212 return fLayerManager->event(fInspectedLayer, fp).commandCount;
213 } else {
214 return frames[fp]->getSize();
215 }
216 }
217 int getFrameCount() const {
218 return frames.size();
219 }
220
221 // Return the command list in JSON representation as a string
225 writer.beginObject(); // root
226 visibleCanvas()->toJSON(writer, udm, surface->getCanvas());
227 writer.endObject(); // root
228 writer.flush();
229 auto skdata = stream.detachAsData();
230 // Convert skdata to string_view, which accepts a length
231 std::string_view data_view(reinterpret_cast<const char*>(skdata->data()), skdata->size());
232 // and string_view to string, which emscripten understands.
233 return std::string(data_view);
234 }
235
236 // Gets the clip and matrix of the last command drawn
237 std::string lastCommandInfo() {
240
243 writer.beginObject(); // root
244
245 writer.appendName("ViewMatrix");
247 writer.appendName("ClipRect");
249
250 writer.endObject(); // root
251 writer.flush();
252 auto skdata = stream.detachAsData();
253 // Convert skdata to string_view, which accepts a length
254 std::string_view data_view(reinterpret_cast<const char*>(skdata->data()), skdata->size());
255 // and string_view to string, which emscripten understands.
256 return std::string(data_view);
257 }
258
259 void changeFrame(int index) {
260 fp = index;
261 }
262
263 // Return the png file at the requested index in
264 // the skp file's vector of shared images. this is the set of images referred to by the
265 // filenames like "\\1" in DrawImage commands.
266 // Return type is the PNG data as a base64 encoded string with prepended URI.
267 std::string getImageResource(int index) {
268 sk_sp<SkData> pngData = SkPngEncoder::Encode(nullptr, fImages[index].get(), {});
269 size_t len = SkBase64::EncodedSize(pngData->size());
270 SkString dst;
271 dst.resize(len);
272 SkBase64::Encode(pngData->data(), pngData->size(), dst.data());
273 dst.prepend("data:image/png;base64,");
274 return std::string(dst.c_str());
275 }
276
278 return fImages.size();
279 }
280
281 // Get the image info of one of the resource images.
283 return toImageInfoNoColorspace(fImages[index]->imageInfo());
284 }
285
286 // return data on which commands each image is used in.
287 // (frame, -1) returns info for the given frame,
288 // (frame, nodeid) return info for a layer update
289 // { imageid: [commandid, commandid, ...], ... }
290 JSObject imageUseInfo(int framenumber, int nodeid) {
291 JSObject result = emscripten::val::object();
292 DebugCanvas* debugCanvas = frames[framenumber].get();
293 if (nodeid >= 0) {
294 debugCanvas = fLayerManager->getEventDebugCanvas(nodeid, framenumber);
295 }
296 const auto& map = debugCanvas->getImageIdToCommandMap(udm);
297 for (auto it = map.begin(); it != map.end(); ++it) {
298 JSArray list = emscripten::val::array();
299 for (const int commandId : it->second) {
300 list.call<void>("push", commandId);
301 }
302 result.set(std::to_string(it->first), list);
303 }
304 return result;
305 }
306
307 // Return information on every layer (offscreeen buffer) that is available for drawing at
308 // the current frame.
310 JSArray result = emscripten::val::array();
311 for (auto summary : fLayerManager->summarizeLayers(fp)) {
312 result.call<void>("push", summary);
313 }
314 return result;
315 }
316
318 JSArray result = emscripten::val::array();
319 for (auto key : fLayerManager->getKeys()) {
320 JSObject item = emscripten::val::object();
321 item.set("frame", key.frame);
322 item.set("nodeId", key.nodeId);
323 result.call<void>("push", item);
324 }
325 return result;
326 }
327
328 // When set to a valid layer index, causes this class to playback the layer draw event at nodeId
329 // on frame fp. No validation of nodeId or fp is performed, this must be valid values obtained
330 // from either fLayerManager.listNodesForFrame or fLayerManager.summarizeEvents
331 // Set to -1 to return to viewing the top level animation
332 void setInspectedLayer(int nodeId) {
333 fInspectedLayer = nodeId;
334 }
335
336 // Finds a command that left the given pixel in it's current state.
337 // Note that this method may fail to find the absolute last command that leaves a pixel
338 // the given color, but there is probably only one candidate in most cases, and the log(n)
339 // makes it worth it.
340 int findCommandByPixel(SkSurface* surface, int x, int y, int commandIndex) {
341 // What color is the pixel now?
342 SkColor finalColor = evaluateCommandColor(surface, commandIndex, x, y);
343
344 int lowerBound = 0;
345 int upperBound = commandIndex;
346
347 while (upperBound - lowerBound > 1) {
348 int command = (upperBound - lowerBound) / 2 + lowerBound;
349 auto c = evaluateCommandColor(surface, command, x, y);
350 if (c == finalColor) {
351 upperBound = command;
352 } else {
353 lowerBound = command;
354 }
355 }
356 // clean up after side effects
357 drawTo(surface, commandIndex);
358 return upperBound;
359 }
360
361 private:
362
363 // Helper for findCommandByPixel.
364 // Has side effect of flushing to surface.
365 // TODO(nifong) eliminate side effect.
366 SkColor evaluateCommandColor(SkSurface* surface, int command, int x, int y) {
367 drawTo(surface, command);
368
369 SkColor c;
371 SkPixmap pixmap(info, &c, 4);
372 surface->readPixels(pixmap, x, y);
373 return c;
374 }
375
376 // Loads a single frame (traditional) skp file from the provided data stream and returns
377 // a newly allocated DebugCanvas initialized with the SkPicture that was in the file.
378 std::unique_ptr<DebugCanvas> loadSingleFrame(SkMemoryStream* stream) {
379 // note overloaded = operator that actually does a move
381 if (!picture) {
382 SkDebugf("Unable to deserialze frame.\n");
383 return nullptr;
384 }
385 SkDebugf("Parsed SKP file.\n");
386 // Make debug canvas using bounds from SkPicture
387 fBoundsArray.push_back(picture->cullRect().roundOut());
388 std::unique_ptr<DebugCanvas> debugCanvas = std::make_unique<DebugCanvas>(fBoundsArray.back());
389
390 // Only draw picture to the debug canvas once.
391 debugCanvas->drawPicture(picture);
392 return debugCanvas;
393 }
394
395 std::string loadMultiFrame(SkMemoryStream* stream) {
396 // Attempt to deserialize with an image sharing serial proc.
397 auto deserialContext = std::make_unique<SkSharingDeserialContext>();
398 SkDeserialProcs procs;
400 procs.fImageCtx = deserialContext.get();
401
403 if (!page_count) {
404 // MSKP's have a version separate from the SKP subpictures they contain.
405 return "Not a MultiPictureDocument, MultiPictureDocument file version too old, or MultiPictureDocument contained 0 frames.";
406 }
407 SkDebugf("Expecting %d frames\n", page_count);
408
409 std::vector<SkDocumentPage> pages(page_count);
410 if (!SkMultiPictureDocument::Read(stream, pages.data(), page_count, &procs)) {
411 return "Reading frames from MultiPictureDocument failed";
412 }
413
414 fLayerManager = std::make_unique<DebugLayerManager>();
415
416 int i = 0;
417 for (const auto& page : pages) {
418 // Make debug canvas using bounds from SkPicture
419 fBoundsArray.push_back(page.fPicture->cullRect().roundOut());
420 std::unique_ptr<DebugCanvas> debugCanvas = std::make_unique<DebugCanvas>(fBoundsArray.back());
421 debugCanvas->setLayerManagerAndFrame(fLayerManager.get(), i);
422
423 // Only draw picture to the debug canvas once.
424 debugCanvas->drawPicture(page.fPicture);
425
426 if (debugCanvas->getSize() <=0 ){
427 SkDebugf("Skipped corrupted frame, had %d commands \n", debugCanvas->getSize());
428 continue;
429 }
430 // If you don't set these, they're undefined.
431 debugCanvas->setOverdrawViz(false);
432 debugCanvas->setDrawGpuOpBounds(false);
433 debugCanvas->setClipVizColor(SK_ColorTRANSPARENT);
434 debugCanvas->setAndroidClipViz(false);
435 frames.push_back(std::move(debugCanvas));
436 i++;
437 }
438 fImages = deserialContext->fImages;
439
440 udm.indexImages(fImages);
441 return "";
442 }
443
444 // constrains the draw command index to the frame's command list length.
445 int constrainFrameCommand(int index) {
446 int cmdlen = frames[fp]->getSize();
447 if (index >= cmdlen) {
448 return cmdlen-1;
449 }
450 return index;
451 }
452
453 // A vector of DebugCanvas, each one initialized to a frame of the animation.
454 std::vector<std::unique_ptr<DebugCanvas>> frames;
455 // The index of the current frame (into the vector above)
456 int fp = 0;
457 // The width and height of every frame.
458 // frame sizes are known to change in Android Skia RenderEngine because it interleves pictures from different applications.
459 std::vector<SkIRect> fBoundsArray;
460 // image resources from a loaded file
461 std::vector<sk_sp<SkImage>> fImages;
462
463 // The URLDataManager here is a cache that accepts encoded data (pngs) and puts
464 // numbers on them. We have our own collection of images (fImages) that was populated by the
465 // SkSharingDeserialContext when mskp files are loaded which it can use for IDing images
466 // without having to serialize them.
467 UrlDataManager udm;
468
469 // A structure holding the picture information needed to draw any layers used in an mskp file
470 // individual frames hold a pointer to it, store draw events, and request images from it.
471 // it is stateful and is set to the current frame at all times.
472 std::unique_ptr<DebugLayerManager> fLayerManager;
473
474 // The node id of a layer being inspected, if any.
475 // -1 means we are viewing the top level animation, not a layer.
476 // the exact draw event being inspected depends also on the selected frame `fp`.
477 int fInspectedLayer = -1;
478};
479
480using namespace emscripten;
482
483 function("MinVersion", &MinVersion);
484
485 // The main class that the JavaScript in index.html uses
486 class_<SkpDebugPlayer>("SkpDebugPlayer")
487 .constructor<>()
488 .function("changeFrame", &SkpDebugPlayer::changeFrame)
489 .function("deleteCommand", &SkpDebugPlayer::deleteCommand)
490 .function("draw", &SkpDebugPlayer::draw, allow_raw_pointers())
491 .function("drawTo", &SkpDebugPlayer::drawTo, allow_raw_pointers())
492 .function("findCommandByPixel", &SkpDebugPlayer::findCommandByPixel, allow_raw_pointers())
493 .function("getBounds", &SkpDebugPlayer::getBounds)
494 .function("getBoundsForFrame", &SkpDebugPlayer::getBoundsForFrame)
495 .function("getFrameCount", &SkpDebugPlayer::getFrameCount)
496 .function("getImageResource", &SkpDebugPlayer::getImageResource)
497 .function("getImageCount", &SkpDebugPlayer::getImageCount)
498 .function("getImageInfo", &SkpDebugPlayer::getImageInfo)
499 .function("getLayerKeys", &SkpDebugPlayer::getLayerKeys)
500 .function("getLayerSummariesJs", &SkpDebugPlayer::getLayerSummariesJs)
501 .function("getSize", &SkpDebugPlayer::getSize)
502 .function("imageUseInfo", &SkpDebugPlayer::imageUseInfo)
503 .function("imageUseInfoForFrameJs", optional_override([](SkpDebugPlayer& self, const int frame)->JSObject {
504 // -1 as a node id is used throughout the application to mean no layer inspected.
505 return self.imageUseInfo(frame, -1);
506 }))
507 .function("jsonCommandList", &SkpDebugPlayer::jsonCommandList, allow_raw_pointers())
508 .function("lastCommandInfo", &SkpDebugPlayer::lastCommandInfo)
509 .function("loadSkp", &SkpDebugPlayer::loadSkp, allow_raw_pointers())
510 .function("setClipVizColor", &SkpDebugPlayer::setClipVizColor)
511 .function("setCommandVisibility", &SkpDebugPlayer::setCommandVisibility)
512 .function("setGpuOpBounds", &SkpDebugPlayer::setGpuOpBounds)
513 .function("setInspectedLayer", &SkpDebugPlayer::setInspectedLayer)
514 .function("setOriginVisible", &SkpDebugPlayer::setOriginVisible)
515 .function("setOverdrawVis", &SkpDebugPlayer::setOverdrawVis)
516 .function("setAndroidClipViz", &SkpDebugPlayer::setAndroidClipViz);
517
518 // Structs used as arguments or returns to the functions above
519 // TODO(kjlubick) handle this rect like the ones in CanvasKit
520 value_object<SkIRect>("SkIRect")
521 .field("fLeft", &SkIRect::fLeft)
522 .field("fTop", &SkIRect::fTop)
523 .field("fRight", &SkIRect::fRight)
524 .field("fBottom", &SkIRect::fBottom);
525 // emscripten provided the following convenience function for binding vector<T>
526 // https://emscripten.org/docs/api_reference/bind.h.html#_CPPv415register_vectorPKc
527 register_vector<DebugLayerManager::LayerSummary>("VectorLayerSummary");
528 value_object<DebugLayerManager::LayerSummary>("LayerSummary")
530 .field("frameOfLastUpdate", &DebugLayerManager::LayerSummary::frameOfLastUpdate)
533 .field("layerHeight", &DebugLayerManager::LayerSummary::layerHeight);
534
535 value_object<ImageInfoNoColorspace>("ImageInfoNoColorspace")
536 .field("width", &ImageInfoNoColorspace::width)
537 .field("height", &ImageInfoNoColorspace::height)
538 .field("colorType", &ImageInfoNoColorspace::colorType)
539 .field("alphaType", &ImageInfoNoColorspace::alphaType);
540}
static void info(const char *fmt,...) SK_PRINTF_LIKE(1
Definition DM.cpp:213
SkColor4f color
SkAlphaType
Definition SkAlphaType.h:26
@ kOpaque_SkAlphaType
pixel is opaque
Definition SkAlphaType.h:28
SkColorType
Definition SkColorType.h:19
@ kRGBA_8888_SkColorType
pixel with 8 bits for red, green, blue, alpha; in 32-bit word
Definition SkColorType.h:24
uint32_t SkColor
Definition SkColor.h:37
constexpr SkColor SK_ColorTRANSPARENT
Definition SkColor.h:99
void SK_SPI SkDebugf(const char format[],...) SK_PRINTF_LIKE(1
static SkPath clip(const SkPath &path, const SkHalfPlane &plane)
Definition SkPath.cpp:3824
emscripten::val JSObject
Definition WasmCommon.h:28
int32_t JSColor
Definition WasmCommon.h:26
emscripten::val JSArray
Definition WasmCommon.h:27
const SkIRect & getCurrentClip()
std::map< int, std::vector< int > > getImageIdToCommandMap(UrlDataManager &udm) const
const SkM44 & getCurrentMatrix()
void toggleCommand(int index, bool toggle)
void toJSON(SkJSONWriter &writer, UrlDataManager &urlDataManager, SkCanvas *)
void deleteDrawCommandAt(int index)
static void MakeJsonMatrix44(SkJSONWriter &, const SkM44 &)
static void MakeJsonIRect(SkJSONWriter &, const SkIRect &)
void beginObject(const char *name=nullptr, bool multiline=true)
void appendName(const char *name)
Definition SkM44.h:150
virtual SkRect cullRect() const =0
static sk_sp< SkPicture > MakeFromStream(SkStream *stream, const SkDeserialProcs *procs=nullptr)
std::string jsonCommandList(sk_sp< SkSurface > surface)
void setCommandVisibility(int index, bool visible)
int findCommandByPixel(SkSurface *surface, int x, int y, int commandIndex)
std::string getImageResource(int index)
void setAndroidClipViz(bool on)
void setOriginVisible(bool on)
JSObject imageUseInfo(int framenumber, int nodeid)
void setOverdrawVis(bool on)
std::string loadSkp(uintptr_t cptr, int length)
void draw(SkSurface *surface)
void changeFrame(int index)
const SkIRect getBoundsForFrame(int32_t frame)
JSArray getLayerSummariesJs()
void drawTo(SkSurface *surface, int32_t index)
void deleteCommand(int index)
DebugCanvas * visibleCanvas()
void setClipVizColor(JSColor color)
void setGpuOpBounds(bool on)
const SkIRect getBounds()
std::string lastCommandInfo()
int getFrameCount() const
void setInspectedLayer(int nodeId)
ImageInfoNoColorspace getImageInfo(int index)
void indexImages(const std::vector< sk_sp< SkImage > > &)
ImageInfoNoColorspace toImageInfoNoColorspace(const SkImageInfo &ii)
EMSCRIPTEN_BINDINGS(my_module)
static constexpr char kMultiMagic[]
uint32_t MinVersion()
VkSurfaceKHR surface
Definition main.cc:49
double frame
Definition examples.cpp:31
const uint8_t uint32_t uint32_t GError ** error
GAsyncResult * result
Dart_NativeFunction function
Definition fuchsia.cc:51
size_t length
double y
double x
SK_API bool Read(SkStreamSeekable *src, SkDocumentPage *dstArray, int dstArrayCount, const SkDeserialProcs *=nullptr)
SK_API int ReadPageCount(SkStreamSeekable *src)
SK_API bool Encode(SkWStream *dst, const SkPixmap &src, const Options &options)
sk_sp< const SkPicture > picture
Definition SkRecords.h:299
SK_API GrSemaphoresSubmitted Flush(sk_sp< SkSurface >)
static size_t EncodedSize(size_t srcDataLength)
Definition SkBase64.h:40
static size_t Encode(const void *src, size_t length, void *dst, const char *encode=nullptr)
Definition SkBase64.cpp:113
SkDeserialImageProc fImageProc
int32_t fBottom
larger y-axis bounds
Definition SkRect.h:36
int32_t fTop
smaller y-axis bounds
Definition SkRect.h:34
static constexpr SkIRect MakeWH(int32_t w, int32_t h)
Definition SkRect.h:56
int32_t fLeft
smaller x-axis bounds
Definition SkRect.h:33
int32_t fRight
larger x-axis bounds
Definition SkRect.h:35
int width() const
static SkImageInfo Make(int width, int height, SkColorType ct, SkAlphaType at)
SkAlphaType alphaType() const
SkColorType colorType() const
int height() const
void roundOut(SkIRect *dst) const
Definition SkRect.h:1241
static sk_sp< SkImage > deserializeImage(const void *data, size_t length, void *ctx)