| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import {assert} from 'chrome://resources/js/assert.js'; |
| import {CustomElement} from 'chrome://resources/js/custom_element.js'; |
| |
| import type {AngleFeature, BrowserBridge, ClientInfo, FeatureStatus, Problem} from './browser_bridge.js'; |
| import {getTemplate} from './info_view.html.js'; |
| |
| // <if expr="enable_vulkan"> |
| import {VulkanInfo} from './vulkan_info.js'; |
| // </if> |
| |
| /** |
| * Given a blob and a filename, prompts user to |
| * save as a file. |
| */ |
| const saveData = (function() { |
| const a = document.createElement('a'); |
| a.style.display = 'none'; |
| document.body.appendChild(a); |
| return function saveData(blob: Blob, fileName: string) { |
| const url = window.URL.createObjectURL(blob); |
| a.href = url; |
| a.download = fileName; |
| a.click(); |
| }; |
| }()); |
| |
| function getProblemTextAndUrl(problem: Problem) { |
| let text = problem.description; |
| let url = ''; |
| const pattern = ' Please update your graphics driver via this link: '; |
| const pos = text.search(pattern); |
| if (pos > 0) { |
| url = text.substring(pos + pattern.length); |
| text = text.substring(0, pos); |
| } |
| return {text, url}; |
| } |
| |
| /** |
| * Calls a function to insert an element between every element |
| * of an existing array |
| */ |
| function insertBetweenElements<Type>( |
| array: Type[], fn: (i: number) => Type): Type[] { |
| const newArray = array.slice(0, 1); |
| for (let i = 1; i < array.length; ++i) { |
| newArray.push(fn(i), array[i] as Type); |
| } |
| return newArray; |
| } |
| |
| /** Inserts <span>, </span> between every element in array */ |
| function separateByCommas(array: HTMLElement[], comma = ', ') { |
| return insertBetweenElements(array, () => createElem('span', comma)); |
| } |
| |
| /** |
| * Conditionally add elements to an array |
| * |
| * ```js |
| * const array = [ |
| * "carrots", |
| * "potatoes", |
| * ...addIf(haveFruit, () => ["apple", "cherry"]), |
| * ] |
| * ``` |
| * |
| * The function is not called if `cond` is false. |
| */ |
| function addIf<T>(cond: boolean, fn: () => T[]) { |
| return cond ? fn() : []; |
| } |
| |
| /** |
| * Word wraps a string, prefixing each line. |
| */ |
| function wordWrap(s: string, prefix = ' ', maxLength?: number) { |
| maxLength = maxLength || (80 - prefix.length); |
| const lines: string[] = []; |
| const words = s.split(' '); |
| const line: string[] = []; |
| let length = 0; |
| for (const word of words) { |
| if (length + word.length + 1 >= maxLength) { |
| lines.push(line.join(' ')); |
| line.length = 0; |
| length = 0; |
| } |
| line.push(word); |
| length += word.length + 1; |
| } |
| if (line.length) { |
| lines.push(line.join(' ')); |
| } |
| return lines.map(s => `${prefix}${s}`).join('\n'); |
| } |
| |
| interface Attributes { |
| [key: string]: string|Attributes; |
| } |
| |
| /** |
| * Creates an HTMLElement with optional attributes and children |
| * |
| * Examples: |
| * |
| * ```js |
| * br = createElem('br'); |
| * p = createElem('p', 'hello world'); |
| * a = createElem('a', {href: 'https://google.com', textContent: 'Google'}); |
| * ul = createElement('ul', {}, [ |
| * createElem('li', 'apple'), |
| * createElem('li', 'banana'), |
| * ]); |
| * h1 = createElem('h1', { style: { color: 'red' }, textContent: 'Title'}) |
| * ``` |
| */ |
| function createElem( |
| tag: string, attrs: Attributes|string = {}, children: HTMLElement[] = []) { |
| const elem = document.createElement(tag); |
| if (typeof attrs === 'string') { |
| elem.textContent = attrs; |
| } else { |
| const elemAsAttribs = elem as unknown as Attributes; |
| for (const [key, value] of Object.entries(attrs)) { |
| if (typeof value === 'function' && key.startsWith('on')) { |
| const eventName = key.substring(2).toLowerCase(); |
| elem.addEventListener(eventName, value, {passive: false}); |
| } else if (typeof value === 'object') { |
| for (const [k, v] of Object.entries(value)) { |
| (elemAsAttribs[key] as Attributes)[k] = v; |
| } |
| } else if (elemAsAttribs[key] === undefined) { |
| elem.setAttribute(key, value); |
| } else { |
| elemAsAttribs[key] = value; |
| } |
| } |
| } |
| for (const child of children) { |
| elem.appendChild(child); |
| } |
| return elem; |
| } |
| |
| export interface Data { |
| description: string; |
| id?: string; |
| value: string; |
| } |
| |
| export interface ArrayData { |
| description: string; |
| value: Data[]; |
| } |
| |
| /** Creates the td elements for a table row */ |
| function createInfoElements( |
| data: Data|ArrayData, padSize: number): HTMLElement[] { |
| const desc = createElem('td', {}, [ |
| createElem('span', data.description.padEnd(padSize)), |
| createHidden(':'), |
| ]); |
| |
| if (Array.isArray(data.value)) { |
| return [ |
| desc, |
| createElem('td', {}, [createInfoTable((data as ArrayData).value)]), |
| ]; |
| } else { |
| return [ |
| desc, |
| createElem('td', { |
| textContent: data.value.toString().trim(), |
| id: (data as Data).id!, |
| }), |
| ]; |
| } |
| } |
| |
| /** Creates a table from the given data */ |
| function createInfoTable(data: Data[]|ArrayData[]) { |
| const longestDesc = Math.min( |
| 32, |
| (data as Data[]) |
| .reduce( |
| (longest, {description}) => Math.max(longest, description.length), |
| 0)); |
| return createElem('table', {className: 'info-table'}, [ |
| createElem( |
| 'tbody', {}, |
| data.map( |
| data => createElem('tr', {}, createInfoElements(data, longestDesc)), |
| )), |
| ]); |
| } |
| |
| /** |
| * Creates a hidden span that will only be used when the when |
| * the user copies or downloads text. |
| */ |
| function createHidden(textContent: string) { |
| return createElem('span', {className: 'copy', textContent}); |
| } |
| |
| /** |
| * Given a string or Attributes returns the `textContent` |
| * and the attributes with `textContent` removed |
| */ |
| function separateTextContentFromAttributes(attrs: Attributes|string = {}) { |
| return typeof attrs === 'string' ? {textContent: attrs, attribs: {}} : { |
| textContent: attrs['textContent'] as string || '', |
| attribs: {...attrs, textContent: ''}, |
| }; |
| } |
| |
| /** |
| * Creates a list item with a hidden `* ` span prepended for copy |
| */ |
| function createLi(attrs: Attributes|string = {}, children: HTMLElement[] = []) { |
| const {textContent, attribs} = separateTextContentFromAttributes(attrs); |
| return createElem('li', attribs, [ |
| createHidden('* '), |
| createElem('span', textContent), |
| ...children, |
| ]); |
| } |
| |
| /** |
| * Creates a heading tag with hidden text for copying |
| * so the copy will be like markdown. |
| */ |
| function createHeading( |
| tag: string, padChar: string, attrs: Attributes|string = {}, |
| children: HTMLElement[] = []) { |
| const {textContent, attribs} = separateTextContentFromAttributes(attrs); |
| return createElem(tag, attribs, [ |
| createHidden('\n\n'), |
| createElem('span', textContent), |
| createHidden(`\n${''.padEnd(textContent.length, padChar)}`), |
| ...children, |
| ]); |
| } |
| |
| /** |
| * Creates a link pair with an anchor tag that is visible |
| * in the page and hidden text for copying so the copy |
| * will appear as (href) |
| */ |
| function createLinkPair(textContent: string, href: string) { |
| return [ |
| createElem('a', { |
| className: 'hide-on-copy', |
| textContent, |
| href, |
| }), |
| createHidden(`(${href})`), |
| ]; |
| } |
| |
| /** |
| * Get a string data value |
| */ |
| function getDataValue(data: Data|ArrayData): string { |
| return Array.isArray(data.value) ? |
| data.value.map(data => getDataValue(data)).join(',') : |
| data.value; |
| } |
| |
| /** |
| * Go through Datas and find ones that start with 'GPUx' |
| * return the first with who's value ends itn '*ACTIVE*' |
| * or else the first one. |
| * @param data |
| * @returns |
| */ |
| function getActiveGPU(data: Data[]|ArrayData[]) { |
| // get list of GPUs |
| const gpus = |
| [...data].filter(({description}) => /^GPU\d+$/.test(description)); |
| // get list of active GPUs |
| const active = gpus.filter(data => getDataValue(data).endsWith('*ACTIVE*')); |
| const all = [...active, ...gpus]; |
| // get the first one |
| if (all.length > 0) { |
| const gpu = getDataValue(all[0]!); |
| const parts = gpu.split(', ')[0]!.split('='); |
| return parts.length === 2 && parts[0]! === 'VENDOR' ? parseInt(parts[1]!) : |
| 0; |
| } |
| return 0; |
| } |
| |
| /** convert a value to a string or empty string if null or undefined */ |
| function safeString(value: any) { |
| return typeof value === 'undefined' || value === null ? '' : value.toString(); |
| } |
| |
| const kSections = { |
| featureStatus: ['Graphics Feature Status', 'ul'], |
| clientInfo: ['Version Information', 'div'], |
| basicInfo: ['Driver Information', 'div'], |
| workarounds: ['Driver Bug Workarounds', 'ul'], |
| problems: ['Problems Detected', 'ul'], |
| angleFeatures: ['ANGLE Features', 'ul'], |
| dawnInfo: ['Dawn Info', 'ul'], |
| compositorInfo: ['Compositor Information', 'div'], |
| gpuMemoryBufferInfo: ['GpuMemoryBuffers Status', 'div'], |
| displayInfo: ['Display(s) Information', 'div'], |
| videoAccelerationInfo: ['Video Acceleration Information', 'div'], |
| vulkanInfo: ['Vulkan Information', 'div'], |
| devicePerfInfo: ['Device Performance Information', 'div'], |
| diagnostics: ['Diagnostics', 'div'], |
| basicInfoForHardwareGpu: ['Driver Information for Hardware GPU', 'div'], |
| featureStatusForHardwareGpu: |
| ['Graphics Feature Status for Hardware GPU', 'ul'], |
| workaroundsForHardwareGpu: ['Driver Bug Workarounds for Hardware GPU', 'ul'], |
| problemsForHardwareGpu: ['Problems Detected for Hardware GPU', 'ul'], |
| logMessages: ['Log Messages', 'ul'], |
| } as const; |
| |
| interface Section { |
| div: HTMLElement; |
| list: HTMLElement; |
| wrap: HTMLElement; |
| } |
| |
| type Sections = { |
| [key in keyof typeof kSections]: Section |
| }; |
| |
| /** |
| * @fileoverview This view displays information on the current GPU |
| * hardware. Its primary usefulness is to allow users to copy-paste |
| * their data in an easy to read format for bug reports. |
| */ |
| export class InfoViewElement extends CustomElement { |
| browserBridge?: BrowserBridge; |
| sections?: Sections; |
| |
| static override get template() { |
| return getTemplate(); |
| } |
| |
| addBrowserBridgeListeners(browserBridge: BrowserBridge) { |
| browserBridge.addEventListener( |
| 'gpuInfoUpdate', this.refresh.bind(this, browserBridge)); |
| browserBridge.addEventListener( |
| 'logMessagesChange', this.refresh.bind(this, browserBridge)); |
| browserBridge.addEventListener( |
| 'clientInfoChange', this.refresh.bind(this, browserBridge)); |
| this.refresh(browserBridge); |
| } |
| |
| /** |
| * public interface for testing |
| */ |
| getInfo(category: string, feature: string = ''): string|string[] { |
| const gpuInfo = this.browserBridge?.gpuInfo; |
| if (!gpuInfo) { |
| throw new Error('no gpuInfo'); |
| } |
| |
| switch (category) { |
| case 'feature-status-for-hardware-gpu-list': |
| return safeString( |
| gpuInfo.featureStatusForHardwareGpu?.featureStatus[feature]); |
| case 'feature-status-list': |
| return safeString(gpuInfo.featureStatus?.featureStatus[feature]); |
| case 'active-gpu-for-hardware': |
| return safeString(getActiveGPU(gpuInfo.basicInfoForHardwareGpu)); |
| case 'active-gpu': |
| return safeString(getActiveGPU(gpuInfo.basicInfo)); |
| case 'workarounds': |
| return (gpuInfo.featureStatus || gpuInfo.featureStatusForHardwareGpu) |
| ?.workarounds || |
| []; |
| default: |
| throw new Error(`unknown category: ${category}`); |
| } |
| } |
| |
| getSelectionText(all: boolean) { |
| const dynamicStyle = this.getRequiredElement('#dynamic-style'); |
| dynamicStyle.textContent = ` |
| #content { white-space: pre !important; } |
| .copy { display: initial; } |
| .hide-on-copy { display: none; } |
| `; |
| const contentDiv = this.getRequiredElement('#content'); |
| |
| // document.getSelection doesn't work through shadowDom |
| // and shadowRoot getSelection is non-standard chromium |
| const shadowDoc = this.shadowRoot! as unknown as Document; |
| const selection = shadowDoc.getSelection()!; |
| |
| if (all) { |
| selection.removeAllRanges(); |
| selection.selectAllChildren(contentDiv); |
| } else { |
| const position = |
| selection.anchorNode?.compareDocumentPosition(selection.focusNode!); |
| const [startNode, startOffset, endNode, endOffset] = |
| ((position || 0) & Node.DOCUMENT_POSITION_FOLLOWING) ? |
| [ |
| selection.anchorNode!, |
| selection.anchorOffset, |
| selection.focusNode!, |
| selection.focusOffset, |
| ] : |
| [ |
| selection.focusNode!, |
| selection.focusOffset, |
| selection.anchorNode!, |
| selection.anchorOffset, |
| ]; |
| if (startOffset === 0) { |
| // Given the selection between > and < |
| // |
| // * >abc |
| // * def< |
| // |
| // We need to move the start of the selection back to the parent |
| // otherwise the selection above will copied as |
| // |
| // abc |
| // * def |
| // |
| // since the * (the list item's bullet) is not selectable directly. |
| const li = startNode.parentElement?.closest('li'); |
| selection.setBaseAndExtent( |
| li || startNode.parentNode!, 0, endNode, endOffset); |
| } |
| } |
| |
| // Get text and remove superfluous lines and whitespace. |
| const text = selection.toString() |
| .replace(/\s*\n\s*\n\s*\n+/g, '\n\n') |
| .replace(/\t/g, ' ') |
| .trim(); |
| |
| if (all) { |
| shadowDoc.getSelection()?.removeAllRanges(); |
| } |
| |
| dynamicStyle.textContent = ''; |
| return text; |
| } |
| |
| |
| connectedCallback() { |
| // Add handler to 'download report to clipboard' button |
| const downloadButton = this.getRequiredElement('#download-to-file'); |
| assert(downloadButton); |
| downloadButton.onclick = (() => { |
| const text = this.getSelectionText(true); |
| const blob = new Blob([text], {type: 'text/text'}); |
| const filename = `about-gpu-${ |
| new Date().toISOString().replace(/[^a-z0-9-]/ig, '-')}.txt`; |
| saveData(blob, filename); |
| }); |
| |
| // Add a copy handler to massage the text for plain text. |
| document.addEventListener('copy', (event) => { |
| const text = this.getSelectionText(false); |
| event.clipboardData!.setData('text/plain', text); |
| event.preventDefault(); |
| }); |
| |
| const contentDiv = this.getRequiredElement('#content'); |
| this.sections = Object.fromEntries(Object.entries(kSections).map( |
| ([propName, [title, tag]]) => { |
| const div = createHeading('h3', '=', title); |
| const list = createElem(tag); |
| const wrap = createElem('div', {}, [ |
| div, |
| list, |
| ]); |
| contentDiv.appendChild(wrap); |
| return [propName, {div, list, wrap}]; |
| })) as Sections; |
| } |
| |
| /** |
| * Updates the view based on its currently known data |
| */ |
| refresh(browserBridge: BrowserBridge) { |
| this.browserBridge = browserBridge; |
| let clientInfo: ClientInfo; |
| function createSourcePermalink( |
| revisionIdentifier: string, filepath: string): string { |
| if (revisionIdentifier.length !== 40) { |
| // If the revision id isn't a hash, just use the 0.0.0.0 version |
| // from the Chrome version string "Chrome/0.0.0.0". |
| revisionIdentifier = clientInfo.version.split('/')[1]!; |
| } |
| return `https://chromium.googlesource.com/chromium/src/+/${ |
| revisionIdentifier}/${filepath}`; |
| } |
| |
| const sections = this.sections!; |
| |
| // Client info |
| if (browserBridge.clientInfo) { |
| clientInfo = browserBridge.clientInfo; |
| |
| this.setTable_(sections.clientInfo, [ |
| {description: 'Data exported', value: (new Date()).toISOString()}, |
| {description: 'Chrome version', value: clientInfo.version}, |
| {description: 'Operating system', value: clientInfo.operating_system}, |
| { |
| description: 'Software rendering list URL', |
| value: createSourcePermalink( |
| clientInfo.revision_identifier, |
| 'gpu/config/software_rendering_list.json'), |
| }, |
| { |
| description: 'Driver bug list URL', |
| value: createSourcePermalink( |
| clientInfo.revision_identifier, |
| 'gpu/config/gpu_driver_bug_list.json'), |
| }, |
| {description: 'ANGLE commit id', value: clientInfo.angle_commit_id}, |
| { |
| description: '2D graphics backend', |
| value: clientInfo.graphics_backend, |
| }, |
| {description: 'Command Line', value: clientInfo.command_line}, |
| ]); |
| } else { |
| sections.clientInfo.list.textContent = '... loading ...'; |
| } |
| |
| const gpuInfo = browserBridge.gpuInfo; |
| if (gpuInfo) { |
| // Not using jstemplate here for blocklist status because we construct |
| // href from data, which jstemplate can't seem to do. |
| if (gpuInfo.featureStatus) { |
| this.appendFeatureInfo_( |
| gpuInfo.featureStatus, sections.featureStatus.list, |
| sections.problems, sections.workarounds); |
| } else { |
| sections.featureStatus.list.textContent = ''; |
| sections.problems.list.hidden = true; |
| sections.workarounds.list.hidden = true; |
| } |
| |
| const hideHardware = !gpuInfo.featureStatusForHardwareGpu; |
| sections.basicInfoForHardwareGpu.div.hidden = hideHardware; |
| sections.featureStatusForHardwareGpu.div.hidden = hideHardware; |
| sections.problemsForHardwareGpu.div.hidden = hideHardware; |
| sections.workaroundsForHardwareGpu.div.hidden = hideHardware; |
| if (!hideHardware) { |
| this.appendFeatureInfo_( |
| gpuInfo.featureStatusForHardwareGpu, |
| sections.featureStatusForHardwareGpu.list, |
| sections.problemsForHardwareGpu, |
| sections.workaroundsForHardwareGpu); |
| this.setTable_( |
| sections.basicInfoForHardwareGpu, gpuInfo.basicInfoForHardwareGpu); |
| } |
| |
| this.setTable_(sections.basicInfo, gpuInfo.basicInfo); |
| this.setTable_(sections.compositorInfo, gpuInfo.compositorInfo); |
| this.setTable_(sections.gpuMemoryBufferInfo, gpuInfo.gpuMemoryBufferInfo); |
| this.setTable_(sections.displayInfo, gpuInfo.displayInfo); |
| this.setTable_( |
| sections.videoAccelerationInfo, gpuInfo.videoAcceleratorsInfo); |
| |
| this.updateSectionList_( |
| sections.angleFeatures, gpuInfo.ANGLEFeatures, |
| angleFeature => this.createAngleFeatureEl_(angleFeature)); |
| |
| this.updateSection_(sections.dawnInfo, () => { |
| const show = !!gpuInfo.dawnInfo && gpuInfo.dawnInfo.length > 0; |
| if (show) { |
| this.createDawnInfoEl_(sections.dawnInfo.list, gpuInfo.dawnInfo!); |
| } |
| return show; |
| }); |
| |
| this.updateSectionTable_(sections.diagnostics, gpuInfo.diagnostics); |
| |
| // <if expr="enable_vulkan"> |
| this.setTable_( |
| sections.vulkanInfo, |
| gpuInfo.vulkanInfo ? [{ |
| 'description': 'info', |
| 'value': new VulkanInfo(gpuInfo.vulkanInfo).toString(), |
| 'id': 'vulkan-info-value', |
| }] : |
| []); |
| // </if> |
| this.setTable_(sections.devicePerfInfo, gpuInfo.devicePerfInfo); |
| } else { |
| sections.basicInfo.list.textContent = '... loading ...'; |
| sections.diagnostics.div.hidden = true; |
| sections.featureStatus.list.textContent = ''; |
| sections.problems.div.hidden = true; |
| sections.dawnInfo.div.hidden = true; |
| } |
| |
| // Log messages |
| sections.logMessages.list.textContent = ''; |
| browserBridge.logMessages.forEach(messageObj => { |
| sections.logMessages.list.appendChild( |
| createElem('li', `${messageObj.header}: ${messageObj.message}`)); |
| }); |
| } |
| |
| /** |
| * Clears a section and then updates it by calling fn. If fn returns false |
| * it hides the section. |
| */ |
| private updateSection_(section: Section, fn: () => boolean) { |
| section.list.textContent = ''; |
| const show = fn(); |
| section.div.hidden = !show; |
| } |
| |
| /** |
| * Clears and and updates a section from a list. If the list is empty it |
| * hides the section |
| */ |
| private updateSectionList_<T>( |
| section: Section, list: T[]|undefined, fn: (item: T) => HTMLElement) { |
| this.updateSection_(section, () => { |
| if (list) { |
| for (const item of list) { |
| section.list.appendChild(fn(item)); |
| } |
| } |
| return !!list && list.length > 0; |
| }); |
| } |
| |
| /** Update a table, hiding it of the table has no elements */ |
| private updateSectionTable_( |
| section: Section, inputData: Data[]|ArrayData[]|undefined) { |
| this.updateSection_(section, () => { |
| this.setTable_(section, inputData); |
| return !!inputData && inputData.length > 0; |
| }); |
| } |
| |
| private appendFeatureInfo_( |
| featureInfo: FeatureStatus, featureStatusList: HTMLElement, |
| problems: Section, workarounds: Section) { |
| // Feature map |
| const featureLabelMap: Record<string, string> = { |
| '2d_canvas': 'Canvas', |
| 'gpu_compositing': 'Compositing', |
| 'webgl': 'WebGL', |
| 'multisampling': 'WebGL multisampling', |
| 'texture_sharing': 'Texture Sharing', |
| 'video_decode': 'Video Decode', |
| 'rasterization': 'Rasterization', |
| 'opengl': 'OpenGL', |
| 'metal': 'Metal', |
| 'vulkan': 'Vulkan', |
| 'multiple_raster_threads': 'Multiple Raster Threads', |
| 'native_gpu_memory_buffers': 'Native GpuMemoryBuffers', |
| 'protected_video_decode': 'Hardware Protected Video Decode', |
| 'surface_control': 'Surface Control', |
| 'vpx_decode': 'VPx Video Decode', |
| 'webgl2': 'WebGL2', |
| 'canvas_oop_rasterization': 'Canvas out-of-process rasterization', |
| 'raw_draw': 'Raw Draw', |
| 'video_encode': 'Video Encode', |
| 'direct_rendering_display_compositor': |
| 'Direct Rendering Display Compositor', |
| 'webgpu': 'WebGPU', |
| 'skia_graphite': 'Skia Graphite', |
| 'webnn': 'WebNN', |
| 'trees_in_viz': 'TreesInViz', |
| }; |
| |
| const statusMap: Record<string, {label: string, class: string}> = { |
| 'disabled_software': { |
| 'label': 'Software only. Hardware acceleration disabled', |
| 'class': 'feature-yellow', |
| }, |
| 'disabled_off': {'label': 'Disabled', 'class': 'feature-red'}, |
| 'disabled_off_ok': {'label': 'Disabled', 'class': 'feature-yellow'}, |
| 'unavailable_software': { |
| 'label': 'Software only, hardware acceleration unavailable', |
| 'class': 'feature-yellow', |
| }, |
| 'unavailable_off': {'label': 'Unavailable', 'class': 'feature-red'}, |
| 'unavailable_off_ok': { |
| 'label': 'Unavailable', |
| 'class': 'feature-yellow', |
| }, |
| 'enabled_readback': { |
| 'label': 'Hardware accelerated but at reduced performance', |
| 'class': 'feature-yellow', |
| }, |
| 'enabled_force': { |
| 'label': 'Hardware accelerated on all pages', |
| 'class': 'feature-green', |
| }, |
| 'enabled': {'label': 'Hardware accelerated', 'class': 'feature-green'}, |
| 'enabled_on': {'label': 'Enabled', 'class': 'feature-green'}, |
| 'enabled_force_on': {'label': 'Force enabled', 'class': 'feature-green'}, |
| }; |
| |
| // feature status list |
| featureStatusList.textContent = ''; |
| for (const featureName in featureInfo.featureStatus) { |
| const featureStatus = featureInfo.featureStatus[featureName]!; |
| |
| const label = featureLabelMap[featureName]; |
| if (!label) { |
| console.info('Missing featureLabel for', featureName); |
| } |
| |
| const statusInfo = statusMap[featureStatus]; |
| if (!statusInfo) { |
| console.info('Missing status for ', featureStatus); |
| } |
| |
| featureStatusList.appendChild(createLi({}, [ |
| createElem('span', `${label}: `), |
| |
| createElem( |
| 'span', |
| statusInfo ? { |
| textContent: statusInfo['label'], |
| className: statusInfo['class'], |
| } : |
| { |
| textContent: 'Unknown', |
| className: 'feature-red', |
| }, |
| ), |
| ])); |
| } |
| |
| // problems list |
| this.updateSectionList_( |
| problems, featureInfo.problems, |
| problem => this.createProblemEl_(problem)); |
| |
| // driver bug workarounds list |
| this.updateSectionList_( |
| workarounds, featureInfo.workarounds, |
| workaround => createLi(workaround)); |
| } |
| |
| private createProblemEl_(problem: Problem): HTMLElement { |
| const {text, url} = getProblemTextAndUrl(problem); |
| return createLi({}, [ |
| createElem('span', text), |
| |
| // add bug separator |
| ...addIf( |
| problem.crBugs.length > 0, () => [createElem('span', ':\n ')]), |
| |
| // add bugs |
| ...separateByCommas(problem.crBugs.map((id) => { |
| const bugId = parseInt(id); |
| const href = `http://crbug.com/${bugId}`; |
| return createElem('span', {}, createLinkPair(bugId.toString(), href)); |
| })), |
| |
| // add affectedGpuSettings |
| ...addIf( |
| problem.affectedGpuSettings.length > 0, |
| () => |
| [createElem('br'), |
| createHidden(' '), |
| createElem( |
| 'i', {}, |
| [ |
| createElem( |
| 'span', |
| problem.tag === 'disabledFeatures' ? |
| 'Disabled Features: ' : |
| 'Applied Workarounds: '), |
| ...separateByCommas( |
| problem.affectedGpuSettings.map( |
| (textContent) => createElem('span', { |
| textContent, |
| className: problem.tag === 'disabledFeatures' ? |
| 'feature-red' : |
| 'feature-yellow', |
| }), |
| ), |
| ',\n '), |
| ]), |
| ]), |
| |
| // add driver update link |
| ...addIf( |
| !!url, |
| () => |
| [createElem('br'), |
| createHidden(' '), |
| createElem( |
| 'b', {className: 'bg-yellow'}, |
| [ |
| createElem( |
| 'span', 'Please update your graphics drive via '), |
| createElem('a', {textContent: 'this link', href: url}), |
| ]), |
| ]), |
| |
| // for copy spacing |
| createElem('span', '\n\n'), |
| |
| ]); |
| } |
| |
| private createAngleFeatureEl_(angleFeature: AngleFeature) { |
| return createLi({}, [ |
| // Name comes first, bolded |
| createElem('b', angleFeature.name), |
| |
| // If there's a category, it follows the name in parentheses |
| ...addIf( |
| !!angleFeature.category, |
| () => |
| [createElem('span', ` (${angleFeature.category})`), |
| ]), |
| |
| // Follow with a colon, and the status (colored) |
| createElem('span', ': '), |
| createElem( |
| 'span', |
| angleFeature.status === 'enabled' ? |
| {className: 'feature-green', textContent: 'Enabled'} : |
| {className: 'feature-red', textContent: 'Disabled'}), |
| |
| // for copy spacing |
| createElem('span', '\n\n'), |
| ]); |
| } |
| |
| private setTable_(section: Section, inputData: Data[]|ArrayData[]|undefined) { |
| section.list.textContent = ''; |
| section.list.appendChild(createInfoTable(inputData || [])); |
| } |
| |
| private createDawnInfoEl_(dawnInfoList: HTMLElement, gpuDawnInfo: string[]) { |
| dawnInfoList.textContent = ''; |
| let inProcessingToggles = false; |
| |
| for (let i = 0; i < gpuDawnInfo.length; ++i) { |
| const infoString = gpuDawnInfo[i]!; |
| let infoEl: HTMLElement; |
| |
| if (infoString.startsWith('<')) { |
| // GPU type and backend type. |
| // Add an empty line for the next adaptor. |
| dawnInfoList.appendChild(createElem('br')); |
| |
| // e.g. <Discrete GPU> D3D12 backend |
| infoEl = createHeading('h3', '-', infoString); |
| inProcessingToggles = false; |
| } else if (infoString.startsWith('[')) { |
| // e.g. [Enabled Toggle Names] |
| infoEl = createHeading('h4', '-', { |
| className: 'dawn-info-header', |
| textContent: infoString, |
| }); |
| |
| if (infoString === '[WebGPU Status]' || |
| infoString === '[Adapter Supported Features]') { |
| inProcessingToggles = false; |
| } else { |
| inProcessingToggles = true; |
| } |
| } else if (inProcessingToggles) { |
| // Each toggle takes 3 strings |
| infoEl = createLi({}, [ |
| // The toggle name comes first, bolded. |
| createElem('b', `${infoString}: \n `), |
| |
| // URL |
| ...createLinkPair(gpuDawnInfo[++i]!, gpuDawnInfo[i]!), |
| |
| // Description, italicized |
| createElem('i', `:\n${wordWrap(gpuDawnInfo[++i]!)}`), |
| |
| // for copy spacing |
| createElem('span', '\n\n'), |
| ]); |
| } else { |
| // Display supported extensions |
| infoEl = createLi(infoString); |
| } |
| |
| dawnInfoList.appendChild(infoEl); |
| } |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'info-view': InfoViewElement; |
| } |
| } |
| |
| customElements.define('info-view', InfoViewElement); |