Web Inspector: Canvas: missing icons for WebGL2 contexts
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Models / Canvas.js
1 /*
2  * Copyright (C) 2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.Canvas = class Canvas extends WI.Object
27 {
28     constructor(identifier, contextType, {domNode, cssCanvasName, contextAttributes, memoryCost, backtrace} = {})
29     {
30         super();
31
32         console.assert(identifier);
33         console.assert(contextType);
34
35         this._identifier = identifier;
36         this._contextType = contextType;
37         this._domNode = domNode || null;
38         this._cssCanvasName = cssCanvasName || "";
39         this._contextAttributes = contextAttributes || {};
40         this._extensions = new Set;
41         this._memoryCost = memoryCost || NaN;
42         this._backtrace = backtrace || [];
43
44         this._cssCanvasClientNodes = null;
45         this._shaderProgramCollection = new WI.ShaderProgramCollection;
46         this._recordingCollection = new WI.RecordingCollection;
47
48         this._nextShaderProgramDisplayNumber = 1;
49
50         this._requestNodePromise = null;
51
52         this._recordingState = WI.Canvas.RecordingState.Inactive;
53         this._recordingFrames = [];
54         this._recordingBufferUsed = 0;
55     }
56
57     // Static
58
59     static fromPayload(payload)
60     {
61         let contextType = null;
62         switch (payload.contextType) {
63         case CanvasAgent.ContextType.Canvas2D:
64             contextType = WI.Canvas.ContextType.Canvas2D;
65             break;
66         case CanvasAgent.ContextType.BitmapRenderer:
67             contextType = WI.Canvas.ContextType.BitmapRenderer;
68             break;
69         case CanvasAgent.ContextType.WebGL:
70             contextType = WI.Canvas.ContextType.WebGL;
71             break;
72         case CanvasAgent.ContextType.WebGL2:
73             contextType = WI.Canvas.ContextType.WebGL2;
74             break;
75         case CanvasAgent.ContextType.WebGPU:
76             contextType = WI.Canvas.ContextType.WebGPU;
77             break;
78         case CanvasAgent.ContextType.WebMetal:
79             contextType = WI.Canvas.ContextType.WebMetal;
80             break;
81         default:
82             console.error("Invalid canvas context type", payload.contextType);
83         }
84
85         return new WI.Canvas(payload.canvasId, contextType, {
86             domNode: payload.nodeId ? WI.domManager.nodeForId(payload.nodeId) : null,
87             cssCanvasName: payload.cssCanvasName,
88             contextAttributes: payload.contextAttributes,
89             memoryCost: payload.memoryCost,
90             backtrace: Array.isArray(payload.backtrace) ? payload.backtrace.map((item) => WI.CallFrame.fromPayload(WI.mainTarget, item)) : [],
91         });
92     }
93
94     static displayNameForContextType(contextType)
95     {
96         switch (contextType) {
97         case WI.Canvas.ContextType.Canvas2D:
98             return WI.UIString("2D");
99         case WI.Canvas.ContextType.BitmapRenderer:
100             return WI.unlocalizedString("Bitmap Renderer");
101         case WI.Canvas.ContextType.WebGL:
102             return WI.unlocalizedString("WebGL");
103         case WI.Canvas.ContextType.WebGL2:
104             return WI.unlocalizedString("WebGL2");
105         case WI.Canvas.ContextType.WebGPU:
106             return WI.unlocalizedString("Web GPU");
107         case WI.Canvas.ContextType.WebMetal:
108             return WI.unlocalizedString("WebMetal");
109         default:
110             console.error("Invalid canvas context type", contextType);
111         }
112     }
113
114     static resetUniqueDisplayNameNumbers()
115     {
116         WI.Canvas._nextUniqueDisplayNameNumber = 1;
117     }
118
119     // Public
120
121     get identifier() { return this._identifier; }
122     get contextType() { return this._contextType; }
123     get cssCanvasName() { return this._cssCanvasName; }
124     get contextAttributes() { return this._contextAttributes; }
125     get extensions() { return this._extensions; }
126     get backtrace() { return this._backtrace; }
127     get shaderProgramCollection() { return this._shaderProgramCollection; }
128     get recordingCollection() { return this._recordingCollection; }
129     get recordingFrameCount() { return this._recordingFrames.length; }
130     get recordingBufferUsed() { return this._recordingBufferUsed; }
131
132     get recordingActive()
133     {
134         return this._recordingState !== WI.Canvas.RecordingState.Inactive;
135     }
136
137     get memoryCost()
138     {
139         return this._memoryCost;
140     }
141
142     set memoryCost(memoryCost)
143     {
144         if (memoryCost === this._memoryCost)
145             return;
146
147         this._memoryCost = memoryCost;
148
149         this.dispatchEventToListeners(WI.Canvas.Event.MemoryChanged);
150     }
151
152     get displayName()
153     {
154         if (this._cssCanvasName)
155             return WI.UIString("CSS canvas \u201C%s\u201D").format(this._cssCanvasName);
156
157         if (this._domNode) {
158             let idSelector = this._domNode.escapedIdSelector;
159             if (idSelector)
160                 return WI.UIString("Canvas %s").format(idSelector);
161         }
162
163         if (!this._uniqueDisplayNameNumber)
164             this._uniqueDisplayNameNumber = this.constructor._nextUniqueDisplayNameNumber++;
165         return WI.UIString("Canvas %d").format(this._uniqueDisplayNameNumber);
166     }
167
168     requestNode()
169     {
170         if (!this._requestNodePromise) {
171             this._requestNodePromise = new Promise((resolve, reject) => {
172                 WI.domManager.ensureDocument();
173
174                 CanvasAgent.requestNode(this._identifier).then((result) => {
175                     this._domNode = WI.domManager.nodeForId(result.nodeId);
176                     if (!this._domNode) {
177                         reject(`No DOM node for identifier: ${result.nodeId}.`);
178                         return;
179                     }
180                     resolve(this._domNode);
181                 }).catch(reject);
182             });
183         }
184
185         return this._requestNodePromise;
186     }
187
188     requestContent()
189     {
190         return CanvasAgent.requestContent(this._identifier).then((result) => result.content).catch((error) => console.error(error));
191     }
192
193     requestCSSCanvasClientNodes(callback)
194     {
195         if (!this._cssCanvasName) {
196             callback([]);
197             return;
198         }
199
200         if (this._cssCanvasClientNodes) {
201             callback(this._cssCanvasClientNodes);
202             return;
203         }
204
205         WI.domManager.ensureDocument();
206
207         CanvasAgent.requestCSSCanvasClientNodes(this._identifier, (error, clientNodeIds) => {
208             if (error) {
209                 callback([]);
210                 return;
211             }
212
213             clientNodeIds = Array.isArray(clientNodeIds) ? clientNodeIds : [];
214             this._cssCanvasClientNodes = clientNodeIds.map((clientNodeId) => WI.domManager.nodeForId(clientNodeId));
215             callback(this._cssCanvasClientNodes);
216         });
217     }
218
219     requestSize()
220     {
221         function calculateSize(domNode) {
222             function getAttributeValue(name) {
223                 let value = Number(domNode.getAttribute(name));
224                 if (!Number.isInteger(value) || value < 0)
225                     return NaN;
226                 return value;
227             }
228
229             return {
230                 width: getAttributeValue("width"),
231                 height: getAttributeValue("height")
232             };
233         }
234
235         function getPropertyValue(remoteObject, name) {
236             return new Promise((resolve, reject) => {
237                 remoteObject.getProperty(name, (error, result) => {
238                     if (error) {
239                         reject(error);
240                         return;
241                     }
242                     resolve(result);
243                 });
244             });
245         }
246
247         return this.requestNode().then((domNode) => {
248             let size = calculateSize(domNode);
249             if (!isNaN(size.width) && !isNaN(size.height))
250                 return size;
251
252             // Since the "width" and "height" properties of canvas elements are more than just
253             // attributes, we need to invoke the getter for each to get the actual value.
254             //  - https://html.spec.whatwg.org/multipage/canvas.html#attr-canvas-width
255             //  - https://html.spec.whatwg.org/multipage/canvas.html#attr-canvas-height
256             let remoteObject = null;
257             return WI.RemoteObject.resolveNode(domNode).then((object) => {
258                 remoteObject = object;
259                 return Promise.all([getPropertyValue(object, "width"), getPropertyValue(object, "height")]);
260             }).then((values) => {
261                 let width = values[0].value;
262                 let height = values[1].value;
263                 values[0].release();
264                 values[1].release();
265                 remoteObject.release();
266                 return {width, height};
267             });
268         });
269     }
270
271     startRecording(singleFrame)
272     {
273         let handleStartRecording = (error) => {
274             if (error) {
275                 console.error(error);
276                 return;
277             }
278
279             this._recordingState = WI.Canvas.RecordingState.ActiveFrontend;
280
281             // COMPATIBILITY (iOS 12.1): Canvas.event.recordingStarted did not exist yet
282             if (InspectorBackend.domains.Canvas.hasEvent("recordingStarted"))
283                 return;
284
285             this._recordingFrames = [];
286             this._recordingBufferUsed = 0;
287
288             this.dispatchEventToListeners(WI.Canvas.Event.RecordingStarted);
289         };
290
291         // COMPATIBILITY (iOS 12.1): `frameCount` did not exist yet.
292         if (InspectorBackend.domains.Canvas.startRecording.supports("singleFrame")) {
293             CanvasAgent.startRecording(this._identifier, singleFrame, handleStartRecording);
294             return;
295         }
296
297         if (singleFrame) {
298             const frameCount = 1;
299             CanvasAgent.startRecording(this._identifier, frameCount, handleStartRecording);
300         } else
301             CanvasAgent.startRecording(this._identifier, handleStartRecording);
302     }
303
304     stopRecording()
305     {
306         CanvasAgent.stopRecording(this._identifier, (error) => {
307             if (error)
308                 console.error(error);
309         });
310     }
311
312     saveIdentityToCookie(cookie)
313     {
314         if (this._cssCanvasName)
315             cookie[WI.Canvas.CSSCanvasNameCookieKey] = this._cssCanvasName;
316         else if (this._domNode)
317             cookie[WI.Canvas.NodePathCookieKey] = this._domNode.path;
318
319     }
320
321     enableExtension(extension)
322     {
323         // Called from WI.CanvasManager.
324
325         this._extensions.add(extension);
326
327         this.dispatchEventToListeners(WI.Canvas.Event.ExtensionEnabled, {extension});
328     }
329
330     cssCanvasClientNodesChanged()
331     {
332         // Called from WI.CanvasManager.
333
334         if (!this._cssCanvasName)
335             return;
336
337         this._cssCanvasClientNodes = null;
338
339         this.dispatchEventToListeners(WI.Canvas.Event.CSSCanvasClientNodesChanged);
340     }
341
342     recordingStarted(initiator)
343     {
344         // Called from WI.CanvasManager.
345
346         if (initiator === RecordingAgent.Initiator.Console)
347             this._recordingState = WI.Canvas.RecordingState.ActiveConsole;
348         else if (initiator === RecordingAgent.Initiator.AutoCapture)
349             this._recordingState = WI.Canvas.RecordingState.ActiveAutoCapture;
350         else {
351             console.assert(initiator === RecordingAgent.Initiator.Frontend);
352             this._recordingState = WI.Canvas.RecordingState.ActiveFrontend;
353         }
354
355         this._recordingFrames = [];
356         this._recordingBufferUsed = 0;
357
358         this.dispatchEventToListeners(WI.Canvas.Event.RecordingStarted);
359     }
360
361     recordingProgress(framesPayload, bufferUsed)
362     {
363         // Called from WI.CanvasManager.
364
365         this._recordingFrames.push(...framesPayload.map(WI.RecordingFrame.fromPayload));
366
367         this._recordingBufferUsed = bufferUsed;
368
369         this.dispatchEventToListeners(WI.Canvas.Event.RecordingProgress);
370     }
371
372     recordingFinished(recordingPayload)
373     {
374         // Called from WI.CanvasManager.
375
376         let initiatedByUser = this._recordingState === WI.Canvas.RecordingState.ActiveFrontend;
377
378         // COMPATIBILITY (iOS 12.1): Canvas.event.recordingStarted did not exist yet
379         if (!initiatedByUser && !InspectorBackend.domains.Canvas.hasEvent("recordingStarted"))
380             initiatedByUser = !!this.recordingActive;
381
382         let recording = recordingPayload ? WI.Recording.fromPayload(recordingPayload, this._recordingFrames) : null;
383         if (recording) {
384             recording.source = this;
385             recording.createDisplayName(recordingPayload.name);
386
387             this._recordingCollection.add(recording);
388         }
389
390         this._recordingState = WI.Canvas.RecordingState.Inactive;
391         this._recordingFrames = [];
392         this._recordingBufferUsed = 0;
393
394         this.dispatchEventToListeners(WI.Canvas.Event.RecordingStopped, {recording, initiatedByUser});
395     }
396
397     nextShaderProgramDisplayNumber()
398     {
399         // Called from WI.ShaderProgram.
400
401         return this._nextShaderProgramDisplayNumber++;
402     }
403 };
404
405 WI.Canvas._nextUniqueDisplayNameNumber = 1;
406
407 WI.Canvas.FrameURLCookieKey = "canvas-frame-url";
408 WI.Canvas.CSSCanvasNameCookieKey = "canvas-css-canvas-name";
409
410 WI.Canvas.ContextType = {
411     Canvas2D: "canvas-2d",
412     BitmapRenderer: "bitmaprenderer",
413     WebGL: "webgl",
414     WebGL2: "webgl2",
415     WebGPU: "webgpu",
416     WebMetal: "webmetal",
417 };
418
419 WI.Canvas.RecordingState = {
420     Inactive: "canvas-recording-state-inactive",
421     ActiveFrontend: "canvas-recording-state-active-frontend",
422     ActiveConsole: "canvas-recording-state-active-console",
423     ActiveAutoCapture: "canvas-recording-state-active-auto-capture",
424 };
425
426 WI.Canvas.Event = {
427     MemoryChanged: "canvas-memory-changed",
428     ExtensionEnabled: "canvas-extension-enabled",
429     CSSCanvasClientNodesChanged: "canvas-css-canvas-client-nodes-changed",
430     RecordingStarted: "canvas-recording-started",
431     RecordingProgress: "canvas-recording-progress",
432     RecordingStopped: "canvas-recording-stopped",
433 };