Delete WebMetal implementation in favor of WebGPU
[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         default:
79             console.error("Invalid canvas context type", payload.contextType);
80         }
81
82         return new WI.Canvas(payload.canvasId, contextType, {
83             domNode: payload.nodeId ? WI.domManager.nodeForId(payload.nodeId) : null,
84             cssCanvasName: payload.cssCanvasName,
85             contextAttributes: payload.contextAttributes,
86             memoryCost: payload.memoryCost,
87             backtrace: Array.isArray(payload.backtrace) ? payload.backtrace.map((item) => WI.CallFrame.fromPayload(WI.mainTarget, item)) : [],
88         });
89     }
90
91     static displayNameForContextType(contextType)
92     {
93         switch (contextType) {
94         case WI.Canvas.ContextType.Canvas2D:
95             return WI.UIString("2D");
96         case WI.Canvas.ContextType.BitmapRenderer:
97             return WI.unlocalizedString("Bitmap Renderer");
98         case WI.Canvas.ContextType.WebGL:
99             return WI.unlocalizedString("WebGL");
100         case WI.Canvas.ContextType.WebGL2:
101             return WI.unlocalizedString("WebGL2");
102         case WI.Canvas.ContextType.WebGPU:
103             return WI.unlocalizedString("Web GPU");
104         default:
105             console.error("Invalid canvas context type", contextType);
106         }
107     }
108
109     static resetUniqueDisplayNameNumbers()
110     {
111         WI.Canvas._nextUniqueDisplayNameNumber = 1;
112     }
113
114     // Public
115
116     get identifier() { return this._identifier; }
117     get contextType() { return this._contextType; }
118     get cssCanvasName() { return this._cssCanvasName; }
119     get contextAttributes() { return this._contextAttributes; }
120     get extensions() { return this._extensions; }
121     get backtrace() { return this._backtrace; }
122     get shaderProgramCollection() { return this._shaderProgramCollection; }
123     get recordingCollection() { return this._recordingCollection; }
124     get recordingFrameCount() { return this._recordingFrames.length; }
125     get recordingBufferUsed() { return this._recordingBufferUsed; }
126
127     get recordingActive()
128     {
129         return this._recordingState !== WI.Canvas.RecordingState.Inactive;
130     }
131
132     get memoryCost()
133     {
134         return this._memoryCost;
135     }
136
137     set memoryCost(memoryCost)
138     {
139         if (memoryCost === this._memoryCost)
140             return;
141
142         this._memoryCost = memoryCost;
143
144         this.dispatchEventToListeners(WI.Canvas.Event.MemoryChanged);
145     }
146
147     get displayName()
148     {
149         if (this._cssCanvasName)
150             return WI.UIString("CSS canvas \u201C%s\u201D").format(this._cssCanvasName);
151
152         if (this._domNode) {
153             let idSelector = this._domNode.escapedIdSelector;
154             if (idSelector)
155                 return WI.UIString("Canvas %s").format(idSelector);
156         }
157
158         if (!this._uniqueDisplayNameNumber)
159             this._uniqueDisplayNameNumber = this.constructor._nextUniqueDisplayNameNumber++;
160         return WI.UIString("Canvas %d").format(this._uniqueDisplayNameNumber);
161     }
162
163     requestNode()
164     {
165         if (!this._requestNodePromise) {
166             this._requestNodePromise = new Promise((resolve, reject) => {
167                 WI.domManager.ensureDocument();
168
169                 CanvasAgent.requestNode(this._identifier).then((result) => {
170                     this._domNode = WI.domManager.nodeForId(result.nodeId);
171                     if (!this._domNode) {
172                         reject(`No DOM node for identifier: ${result.nodeId}.`);
173                         return;
174                     }
175                     resolve(this._domNode);
176                 }).catch(reject);
177             });
178         }
179
180         return this._requestNodePromise;
181     }
182
183     requestContent()
184     {
185         return CanvasAgent.requestContent(this._identifier).then((result) => result.content).catch((error) => console.error(error));
186     }
187
188     requestCSSCanvasClientNodes(callback)
189     {
190         if (!this._cssCanvasName) {
191             callback([]);
192             return;
193         }
194
195         if (this._cssCanvasClientNodes) {
196             callback(this._cssCanvasClientNodes);
197             return;
198         }
199
200         WI.domManager.ensureDocument();
201
202         CanvasAgent.requestCSSCanvasClientNodes(this._identifier, (error, clientNodeIds) => {
203             if (error) {
204                 callback([]);
205                 return;
206             }
207
208             clientNodeIds = Array.isArray(clientNodeIds) ? clientNodeIds : [];
209             this._cssCanvasClientNodes = clientNodeIds.map((clientNodeId) => WI.domManager.nodeForId(clientNodeId));
210             callback(this._cssCanvasClientNodes);
211         });
212     }
213
214     requestSize()
215     {
216         function calculateSize(domNode) {
217             function getAttributeValue(name) {
218                 let value = Number(domNode.getAttribute(name));
219                 if (!Number.isInteger(value) || value < 0)
220                     return NaN;
221                 return value;
222             }
223
224             return {
225                 width: getAttributeValue("width"),
226                 height: getAttributeValue("height")
227             };
228         }
229
230         function getPropertyValue(remoteObject, name) {
231             return new Promise((resolve, reject) => {
232                 remoteObject.getProperty(name, (error, result) => {
233                     if (error) {
234                         reject(error);
235                         return;
236                     }
237                     resolve(result);
238                 });
239             });
240         }
241
242         return this.requestNode().then((domNode) => {
243             let size = calculateSize(domNode);
244             if (!isNaN(size.width) && !isNaN(size.height))
245                 return size;
246
247             // Since the "width" and "height" properties of canvas elements are more than just
248             // attributes, we need to invoke the getter for each to get the actual value.
249             //  - https://html.spec.whatwg.org/multipage/canvas.html#attr-canvas-width
250             //  - https://html.spec.whatwg.org/multipage/canvas.html#attr-canvas-height
251             let remoteObject = null;
252             return WI.RemoteObject.resolveNode(domNode).then((object) => {
253                 remoteObject = object;
254                 return Promise.all([getPropertyValue(object, "width"), getPropertyValue(object, "height")]);
255             }).then((values) => {
256                 let width = values[0].value;
257                 let height = values[1].value;
258                 values[0].release();
259                 values[1].release();
260                 remoteObject.release();
261                 return {width, height};
262             });
263         });
264     }
265
266     startRecording(singleFrame)
267     {
268         let handleStartRecording = (error) => {
269             if (error) {
270                 console.error(error);
271                 return;
272             }
273
274             this._recordingState = WI.Canvas.RecordingState.ActiveFrontend;
275
276             // COMPATIBILITY (iOS 12.1): Canvas.event.recordingStarted did not exist yet
277             if (InspectorBackend.domains.Canvas.hasEvent("recordingStarted"))
278                 return;
279
280             this._recordingFrames = [];
281             this._recordingBufferUsed = 0;
282
283             this.dispatchEventToListeners(WI.Canvas.Event.RecordingStarted);
284         };
285
286         // COMPATIBILITY (iOS 12.1): `frameCount` did not exist yet.
287         if (InspectorBackend.domains.Canvas.startRecording.supports("singleFrame")) {
288             CanvasAgent.startRecording(this._identifier, singleFrame, handleStartRecording);
289             return;
290         }
291
292         if (singleFrame) {
293             const frameCount = 1;
294             CanvasAgent.startRecording(this._identifier, frameCount, handleStartRecording);
295         } else
296             CanvasAgent.startRecording(this._identifier, handleStartRecording);
297     }
298
299     stopRecording()
300     {
301         CanvasAgent.stopRecording(this._identifier, (error) => {
302             if (error)
303                 console.error(error);
304         });
305     }
306
307     saveIdentityToCookie(cookie)
308     {
309         if (this._cssCanvasName)
310             cookie[WI.Canvas.CSSCanvasNameCookieKey] = this._cssCanvasName;
311         else if (this._domNode)
312             cookie[WI.Canvas.NodePathCookieKey] = this._domNode.path;
313
314     }
315
316     enableExtension(extension)
317     {
318         // Called from WI.CanvasManager.
319
320         this._extensions.add(extension);
321
322         this.dispatchEventToListeners(WI.Canvas.Event.ExtensionEnabled, {extension});
323     }
324
325     cssCanvasClientNodesChanged()
326     {
327         // Called from WI.CanvasManager.
328
329         if (!this._cssCanvasName)
330             return;
331
332         this._cssCanvasClientNodes = null;
333
334         this.dispatchEventToListeners(WI.Canvas.Event.CSSCanvasClientNodesChanged);
335     }
336
337     recordingStarted(initiator)
338     {
339         // Called from WI.CanvasManager.
340
341         if (initiator === RecordingAgent.Initiator.Console)
342             this._recordingState = WI.Canvas.RecordingState.ActiveConsole;
343         else if (initiator === RecordingAgent.Initiator.AutoCapture)
344             this._recordingState = WI.Canvas.RecordingState.ActiveAutoCapture;
345         else {
346             console.assert(initiator === RecordingAgent.Initiator.Frontend);
347             this._recordingState = WI.Canvas.RecordingState.ActiveFrontend;
348         }
349
350         this._recordingFrames = [];
351         this._recordingBufferUsed = 0;
352
353         this.dispatchEventToListeners(WI.Canvas.Event.RecordingStarted);
354     }
355
356     recordingProgress(framesPayload, bufferUsed)
357     {
358         // Called from WI.CanvasManager.
359
360         this._recordingFrames.push(...framesPayload.map(WI.RecordingFrame.fromPayload));
361
362         this._recordingBufferUsed = bufferUsed;
363
364         this.dispatchEventToListeners(WI.Canvas.Event.RecordingProgress);
365     }
366
367     recordingFinished(recordingPayload)
368     {
369         // Called from WI.CanvasManager.
370
371         let initiatedByUser = this._recordingState === WI.Canvas.RecordingState.ActiveFrontend;
372
373         // COMPATIBILITY (iOS 12.1): Canvas.event.recordingStarted did not exist yet
374         if (!initiatedByUser && !InspectorBackend.domains.Canvas.hasEvent("recordingStarted"))
375             initiatedByUser = !!this.recordingActive;
376
377         let recording = recordingPayload ? WI.Recording.fromPayload(recordingPayload, this._recordingFrames) : null;
378         if (recording) {
379             recording.source = this;
380             recording.createDisplayName(recordingPayload.name);
381
382             this._recordingCollection.add(recording);
383         }
384
385         this._recordingState = WI.Canvas.RecordingState.Inactive;
386         this._recordingFrames = [];
387         this._recordingBufferUsed = 0;
388
389         this.dispatchEventToListeners(WI.Canvas.Event.RecordingStopped, {recording, initiatedByUser});
390     }
391
392     nextShaderProgramDisplayNumber()
393     {
394         // Called from WI.ShaderProgram.
395
396         return this._nextShaderProgramDisplayNumber++;
397     }
398 };
399
400 WI.Canvas._nextUniqueDisplayNameNumber = 1;
401
402 WI.Canvas.FrameURLCookieKey = "canvas-frame-url";
403 WI.Canvas.CSSCanvasNameCookieKey = "canvas-css-canvas-name";
404
405 WI.Canvas.ContextType = {
406     Canvas2D: "canvas-2d",
407     BitmapRenderer: "bitmaprenderer",
408     WebGL: "webgl",
409     WebGL2: "webgl2",
410     WebGPU: "webgpu",
411 };
412
413 WI.Canvas.RecordingState = {
414     Inactive: "canvas-recording-state-inactive",
415     ActiveFrontend: "canvas-recording-state-active-frontend",
416     ActiveConsole: "canvas-recording-state-active-console",
417     ActiveAutoCapture: "canvas-recording-state-active-auto-capture",
418 };
419
420 WI.Canvas.Event = {
421     MemoryChanged: "canvas-memory-changed",
422     ExtensionEnabled: "canvas-extension-enabled",
423     CSSCanvasClientNodesChanged: "canvas-css-canvas-client-nodes-changed",
424     RecordingStarted: "canvas-recording-started",
425     RecordingProgress: "canvas-recording-progress",
426     RecordingStopped: "canvas-recording-stopped",
427 };