blob: a634fe13f7467a8e91d479a48b3e20ac8210fe9f [file] [log] [blame]
/*
* Copyright (C) 2017 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
WI.Canvas = class Canvas extends WI.Object
{
constructor(identifier, contextType, {domNode, cssCanvasName, contextAttributes, memoryCost, backtrace} = {})
{
super();
console.assert(identifier);
console.assert(contextType);
this._identifier = identifier;
this._contextType = contextType;
this._domNode = domNode || null;
this._cssCanvasName = cssCanvasName || "";
this._contextAttributes = contextAttributes || {};
this._extensions = new Set;
this._memoryCost = memoryCost || NaN;
this._backtrace = backtrace || [];
this._clientNodes = null;
this._shaderProgramCollection = new WI.ShaderProgramCollection;
this._recordingCollection = new WI.RecordingCollection;
this._nextShaderProgramDisplayNumber = null;
this._requestNodePromise = null;
this._recordingState = WI.Canvas.RecordingState.Inactive;
this._recordingFrames = [];
this._recordingBufferUsed = 0;
}
// Static
static fromPayload(payload)
{
let contextType = null;
switch (payload.contextType) {
case InspectorBackend.Enum.Canvas.ContextType.Canvas2D:
contextType = WI.Canvas.ContextType.Canvas2D;
break;
case InspectorBackend.Enum.Canvas.ContextType.BitmapRenderer:
contextType = WI.Canvas.ContextType.BitmapRenderer;
break;
case InspectorBackend.Enum.Canvas.ContextType.WebGL:
contextType = WI.Canvas.ContextType.WebGL;
break;
case InspectorBackend.Enum.Canvas.ContextType.WebGL2:
contextType = WI.Canvas.ContextType.WebGL2;
break;
case InspectorBackend.Enum.Canvas.ContextType.WebGPU:
contextType = WI.Canvas.ContextType.WebGPU;
break;
case InspectorBackend.Enum.Canvas.ContextType.WebMetal:
contextType = WI.Canvas.ContextType.WebMetal;
break;
default:
console.error("Invalid canvas context type", payload.contextType);
}
return new WI.Canvas(payload.canvasId, contextType, {
domNode: payload.nodeId ? WI.domManager.nodeForId(payload.nodeId) : null,
cssCanvasName: payload.cssCanvasName,
contextAttributes: payload.contextAttributes,
memoryCost: payload.memoryCost,
backtrace: Array.isArray(payload.backtrace) ? payload.backtrace.map((item) => WI.CallFrame.fromPayload(WI.mainTarget, item)) : [],
});
}
static displayNameForContextType(contextType)
{
switch (contextType) {
case WI.Canvas.ContextType.Canvas2D:
return WI.UIString("2D");
case WI.Canvas.ContextType.BitmapRenderer:
return WI.UIString("Bitmap Renderer", "Canvas Context Type Bitmap Renderer", "Bitmap Renderer is a type of rendering context associated with a <canvas> element");
case WI.Canvas.ContextType.WebGL:
return WI.unlocalizedString("WebGL");
case WI.Canvas.ContextType.WebGL2:
return WI.unlocalizedString("WebGL2");
case WI.Canvas.ContextType.WebGPU:
return WI.unlocalizedString("Web GPU");
case WI.Canvas.ContextType.WebMetal:
return WI.unlocalizedString("WebMetal");
}
console.assert(false, "Unknown canvas context type", contextType);
return null;
}
static displayNameForColorSpace(colorSpace)
{
switch(colorSpace) {
case WI.Canvas.ColorSpace.SRGB:
return WI.unlocalizedString("sRGB");
case WI.Canvas.ColorSpace.DisplayP3:
return WI.unlocalizedString("Display P3");
}
console.assert(false, "Unknown canvas color space", colorSpace);
return null;
}
static resetUniqueDisplayNameNumbers()
{
Canvas._nextContextUniqueDisplayNameNumber = 1;
Canvas._nextDeviceUniqueDisplayNameNumber = 1;
}
static supportsRequestContentForContextType(contextType)
{
switch (contextType) {
case Canvas.ContextType.WebGPU:
case Canvas.ContextType.WebMetal:
return false;
}
return true;
}
// Public
get identifier() { return this._identifier; }
get contextType() { return this._contextType; }
get cssCanvasName() { return this._cssCanvasName; }
get contextAttributes() { return this._contextAttributes; }
get extensions() { return this._extensions; }
get backtrace() { return this._backtrace; }
get shaderProgramCollection() { return this._shaderProgramCollection; }
get recordingCollection() { return this._recordingCollection; }
get recordingFrameCount() { return this._recordingFrames.length; }
get recordingBufferUsed() { return this._recordingBufferUsed; }
get recordingActive()
{
return this._recordingState !== WI.Canvas.RecordingState.Inactive;
}
get memoryCost()
{
return this._memoryCost;
}
set memoryCost(memoryCost)
{
if (memoryCost === this._memoryCost)
return;
this._memoryCost = memoryCost;
this.dispatchEventToListeners(WI.Canvas.Event.MemoryChanged);
}
get displayName()
{
if (this._cssCanvasName)
return WI.UIString("CSS canvas \u201C%s\u201D").format(this._cssCanvasName);
if (this._domNode) {
let idSelector = this._domNode.escapedIdSelector;
if (idSelector)
return WI.UIString("Canvas %s").format(idSelector);
}
if (this._contextType === Canvas.ContextType.WebGPU) {
if (!this._uniqueDisplayNameNumber)
this._uniqueDisplayNameNumber = Canvas._nextDeviceUniqueDisplayNameNumber++;
return WI.UIString("Device %d").format(this._uniqueDisplayNameNumber);
}
if (!this._uniqueDisplayNameNumber)
this._uniqueDisplayNameNumber = Canvas._nextContextUniqueDisplayNameNumber++;
return WI.UIString("Canvas %d").format(this._uniqueDisplayNameNumber);
}
requestNode()
{
if (!this._requestNodePromise) {
this._requestNodePromise = new Promise((resolve, reject) => {
WI.domManager.ensureDocument();
let target = WI.assumingMainTarget();
target.CanvasAgent.requestNode(this._identifier, (error, nodeId) => {
if (error) {
resolve(null);
return;
}
this._domNode = WI.domManager.nodeForId(nodeId);
if (!this._domNode) {
resolve(null);
return;
}
resolve(this._domNode);
});
});
}
return this._requestNodePromise;
}
requestContent()
{
if (!Canvas.supportsRequestContentForContextType(this._contextType))
return Promise.resolve(null);
let target = WI.assumingMainTarget();
return target.CanvasAgent.requestContent(this._identifier).then((result) => result.content).catch((error) => console.error(error));
}
requestClientNodes(callback)
{
if (this._clientNodes) {
callback(this._clientNodes);
return;
}
WI.domManager.ensureDocument();
let wrappedCallback = (error, clientNodeIds) => {
if (error) {
callback([]);
return;
}
clientNodeIds = Array.isArray(clientNodeIds) ? clientNodeIds : [];
this._clientNodes = clientNodeIds.map((clientNodeId) => WI.domManager.nodeForId(clientNodeId));
callback(this._clientNodes);
};
let target = WI.assumingMainTarget();
// COMPATIBILITY (iOS 13): Canvas.requestCSSCanvasClientNodes was renamed to Canvas.requestClientNodes.
if (!target.hasCommand("Canvas.requestClientNodes")) {
target.CanvasAgent.requestCSSCanvasClientNodes(this._identifier, wrappedCallback);
return;
}
target.CanvasAgent.requestClientNodes(this._identifier, wrappedCallback);
}
requestSize()
{
function calculateSize(domNode) {
function getAttributeValue(name) {
let value = Number(domNode.getAttribute(name));
if (!Number.isInteger(value) || value < 0)
return NaN;
return value;
}
return {
width: getAttributeValue("width"),
height: getAttributeValue("height")
};
}
function getPropertyValue(remoteObject, name) {
return new Promise((resolve, reject) => {
remoteObject.getProperty(name, (error, result) => {
if (error) {
reject(error);
return;
}
resolve(result);
});
});
}
return this.requestNode().then((domNode) => {
if (!domNode)
return null;
let size = calculateSize(domNode);
if (!isNaN(size.width) && !isNaN(size.height))
return size;
// Since the "width" and "height" properties of canvas elements are more than just
// attributes, we need to invoke the getter for each to get the actual value.
// - https://html.spec.whatwg.org/multipage/canvas.html#attr-canvas-width
// - https://html.spec.whatwg.org/multipage/canvas.html#attr-canvas-height
let remoteObject = null;
return WI.RemoteObject.resolveNode(domNode).then((object) => {
remoteObject = object;
return Promise.all([getPropertyValue(object, "width"), getPropertyValue(object, "height")]);
}).then((values) => {
let width = values[0].value;
let height = values[1].value;
values[0].release();
values[1].release();
remoteObject.release();
return {width, height};
});
});
}
startRecording(singleFrame)
{
let target = WI.assumingMainTarget();
let handleStartRecording = (error) => {
if (error) {
console.error(error);
return;
}
this._recordingState = WI.Canvas.RecordingState.ActiveFrontend;
// COMPATIBILITY (iOS 12.1): Canvas.recordingStarted did not exist yet
if (target.hasEvent("Canvas.recordingStarted"))
return;
this._recordingFrames = [];
this._recordingBufferUsed = 0;
this.dispatchEventToListeners(WI.Canvas.Event.RecordingStarted);
};
// COMPATIBILITY (iOS 12.1): `frameCount` did not exist yet.
if (target.hasCommand("Canvas.startRecording", "singleFrame")) {
target.CanvasAgent.startRecording(this._identifier, singleFrame, handleStartRecording);
return;
}
if (singleFrame) {
const frameCount = 1;
target.CanvasAgent.startRecording(this._identifier, frameCount, handleStartRecording);
} else
target.CanvasAgent.startRecording(this._identifier, handleStartRecording);
}
stopRecording()
{
let target = WI.assumingMainTarget();
target.CanvasAgent.stopRecording(this._identifier);
}
saveIdentityToCookie(cookie)
{
if (this._cssCanvasName)
cookie[WI.Canvas.CSSCanvasNameCookieKey] = this._cssCanvasName;
else if (this._domNode)
cookie[WI.Canvas.NodePathCookieKey] = this._domNode.path;
}
enableExtension(extension)
{
// Called from WI.CanvasManager.
this._extensions.add(extension);
this.dispatchEventToListeners(WI.Canvas.Event.ExtensionEnabled, {extension});
}
clientNodesChanged()
{
// Called from WI.CanvasManager.
this._clientNodes = null;
this.dispatchEventToListeners(Canvas.Event.ClientNodesChanged);
}
recordingStarted(initiator)
{
// Called from WI.CanvasManager.
if (initiator === InspectorBackend.Enum.Recording.Initiator.Console)
this._recordingState = WI.Canvas.RecordingState.ActiveConsole;
else if (initiator === InspectorBackend.Enum.Recording.Initiator.AutoCapture)
this._recordingState = WI.Canvas.RecordingState.ActiveAutoCapture;
else {
console.assert(initiator === InspectorBackend.Enum.Recording.Initiator.Frontend);
this._recordingState = WI.Canvas.RecordingState.ActiveFrontend;
}
this._recordingFrames = [];
this._recordingBufferUsed = 0;
this.dispatchEventToListeners(WI.Canvas.Event.RecordingStarted);
}
recordingProgress(framesPayload, bufferUsed)
{
// Called from WI.CanvasManager.
this._recordingFrames.pushAll(framesPayload.map(WI.RecordingFrame.fromPayload));
this._recordingBufferUsed = bufferUsed;
this.dispatchEventToListeners(WI.Canvas.Event.RecordingProgress);
}
recordingFinished(recordingPayload)
{
// Called from WI.CanvasManager.
let initiatedByUser = this._recordingState === WI.Canvas.RecordingState.ActiveFrontend;
// COMPATIBILITY (iOS 12.1): Canvas.recordingStarted did not exist yet
if (!initiatedByUser && !InspectorBackend.hasEvent("Canvas.recordingStarted"))
initiatedByUser = !!this.recordingActive;
let recording = recordingPayload ? WI.Recording.fromPayload(recordingPayload, this._recordingFrames) : null;
if (recording) {
recording.source = this;
recording.createDisplayName(recordingPayload.name);
this._recordingCollection.add(recording);
}
this._recordingState = WI.Canvas.RecordingState.Inactive;
this._recordingFrames = [];
this._recordingBufferUsed = 0;
this.dispatchEventToListeners(WI.Canvas.Event.RecordingStopped, {recording, initiatedByUser});
}
nextShaderProgramDisplayNumberForProgramType(programType)
{
// Called from WI.ShaderProgram.
if (!this._nextShaderProgramDisplayNumber)
this._nextShaderProgramDisplayNumber = {};
this._nextShaderProgramDisplayNumber[programType] = (this._nextShaderProgramDisplayNumber[programType] || 0) + 1;
return this._nextShaderProgramDisplayNumber[programType];
}
};
WI.Canvas._nextContextUniqueDisplayNameNumber = 1;
WI.Canvas._nextDeviceUniqueDisplayNameNumber = 1;
WI.Canvas.FrameURLCookieKey = "canvas-frame-url";
WI.Canvas.CSSCanvasNameCookieKey = "canvas-css-canvas-name";
WI.Canvas.ContextType = {
Canvas2D: "canvas-2d",
BitmapRenderer: "bitmaprenderer",
WebGL: "webgl",
WebGL2: "webgl2",
WebGPU: "webgpu",
WebMetal: "webmetal",
};
WI.Canvas.ColorSpace = {
SRGB: "srgb",
DisplayP3: "display-p3",
};
WI.Canvas.RecordingState = {
Inactive: "canvas-recording-state-inactive",
ActiveFrontend: "canvas-recording-state-active-frontend",
ActiveConsole: "canvas-recording-state-active-console",
ActiveAutoCapture: "canvas-recording-state-active-auto-capture",
};
WI.Canvas.Event = {
MemoryChanged: "canvas-memory-changed",
ExtensionEnabled: "canvas-extension-enabled",
ClientNodesChanged: "canvas-client-nodes-changed",
RecordingStarted: "canvas-recording-started",
RecordingProgress: "canvas-recording-progress",
RecordingStopped: "canvas-recording-stopped",
};