Path: blob/main/src/vs/editor/browser/gpu/viewGpuContext.ts
3294 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as nls from '../../../nls.js';6import { addDisposableListener, getActiveWindow } from '../../../base/browser/dom.js';7import { createFastDomNode, type FastDomNode } from '../../../base/browser/fastDomNode.js';8import { BugIndicatingError } from '../../../base/common/errors.js';9import { Disposable } from '../../../base/common/lifecycle.js';10import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js';11import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js';12import { observableValue, runOnChange, type IObservable } from '../../../base/common/observable.js';13import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';14import { TextureAtlas } from './atlas/textureAtlas.js';15import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';16import { INotificationService, IPromptChoice, Severity } from '../../../platform/notification/common/notification.js';17import { GPULifecycle } from './gpuDisposable.js';18import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js';19import { RectangleRenderer } from './rectangleRenderer.js';20import type { ViewContext } from '../../common/viewModel/viewContext.js';21import { DecorationCssRuleExtractor } from './css/decorationCssRuleExtractor.js';22import { Event } from '../../../base/common/event.js';23import { EditorOption, type IEditorOptions } from '../../common/config/editorOptions.js';24import { DecorationStyleCache } from './css/decorationStyleCache.js';25import { InlineDecorationType } from '../../common/viewModel/inlineDecorations.js';2627export class ViewGpuContext extends Disposable {28/**29* The hard cap for line columns rendered by the GPU renderer.30*/31readonly maxGpuCols = 2000;3233readonly canvas: FastDomNode<HTMLCanvasElement>;34readonly ctx: GPUCanvasContext;3536static device: Promise<GPUDevice>;37static deviceSync: GPUDevice | undefined;3839readonly rectangleRenderer: RectangleRenderer;4041private static readonly _decorationCssRuleExtractor = new DecorationCssRuleExtractor();42static get decorationCssRuleExtractor(): DecorationCssRuleExtractor {43return ViewGpuContext._decorationCssRuleExtractor;44}4546private static readonly _decorationStyleCache = new DecorationStyleCache();47static get decorationStyleCache(): DecorationStyleCache {48return ViewGpuContext._decorationStyleCache;49}5051private static _atlas: TextureAtlas | undefined;5253/**54* The shared texture atlas to use across all views.55*56* @throws if called before the GPU device is resolved57*/58static get atlas(): TextureAtlas {59if (!ViewGpuContext._atlas) {60throw new BugIndicatingError('Cannot call ViewGpuContext.textureAtlas before device is resolved');61}62return ViewGpuContext._atlas;63}64/**65* The shared texture atlas to use across all views. This is a convenience alias for66* {@link ViewGpuContext.atlas}.67*68* @throws if called before the GPU device is resolved69*/70get atlas(): TextureAtlas {71return ViewGpuContext.atlas;72}7374readonly canvasDevicePixelDimensions: IObservable<{ width: number; height: number }>;75readonly devicePixelRatio: IObservable<number>;76readonly contentLeft: IObservable<number>;7778constructor(79context: ViewContext,80@IInstantiationService private readonly _instantiationService: IInstantiationService,81@INotificationService private readonly _notificationService: INotificationService,82@IConfigurationService private readonly configurationService: IConfigurationService,83) {84super();8586this.canvas = createFastDomNode(document.createElement('canvas'));87this.canvas.setClassName('editorCanvas');8889// Adjust the canvas size to avoid drawing under the scroll bar90this._register(Event.runAndSubscribe(configurationService.onDidChangeConfiguration, e => {91if (!e || e.affectsConfiguration('editor.scrollbar.verticalScrollbarSize')) {92const verticalScrollbarSize = configurationService.getValue<IEditorOptions>('editor').scrollbar?.verticalScrollbarSize ?? 14;93this.canvas.domNode.style.boxSizing = 'border-box';94this.canvas.domNode.style.paddingRight = `${verticalScrollbarSize}px`;95}96}));9798this.ctx = ensureNonNullable(this.canvas.domNode.getContext('webgpu'));99100// Request the GPU device, we only want to do this a single time per window as it's async101// and can delay the initial render.102if (!ViewGpuContext.device) {103ViewGpuContext.device = GPULifecycle.requestDevice((message) => {104const choices: IPromptChoice[] = [{105label: nls.localize('editor.dom.render', "Use DOM-based rendering"),106run: () => this.configurationService.updateValue('editor.experimentalGpuAcceleration', 'off'),107}];108this._notificationService.prompt(Severity.Warning, message, choices);109}).then(ref => {110ViewGpuContext.deviceSync = ref.object;111if (!ViewGpuContext._atlas) {112ViewGpuContext._atlas = this._instantiationService.createInstance(TextureAtlas, ref.object.limits.maxTextureDimension2D, undefined, ViewGpuContext.decorationStyleCache);113}114return ref.object;115});116}117118const dprObs = observableValue(this, getActiveWindow().devicePixelRatio);119this._register(addDisposableListener(getActiveWindow(), 'resize', () => {120dprObs.set(getActiveWindow().devicePixelRatio, undefined);121}));122this.devicePixelRatio = dprObs;123this._register(runOnChange(this.devicePixelRatio, () => ViewGpuContext.atlas?.clear()));124125const canvasDevicePixelDimensions = observableValue(this, { width: this.canvas.domNode.width, height: this.canvas.domNode.height });126this._register(observeDevicePixelDimensions(127this.canvas.domNode,128getActiveWindow(),129(width, height) => {130this.canvas.domNode.width = width;131this.canvas.domNode.height = height;132canvasDevicePixelDimensions.set({ width, height }, undefined);133}134));135this.canvasDevicePixelDimensions = canvasDevicePixelDimensions;136137const contentLeft = observableValue(this, 0);138this._register(this.configurationService.onDidChangeConfiguration(e => {139contentLeft.set(context.configuration.options.get(EditorOption.layoutInfo).contentLeft, undefined);140}));141this.contentLeft = contentLeft;142143this.rectangleRenderer = this._instantiationService.createInstance(RectangleRenderer, context, this.contentLeft, this.devicePixelRatio, this.canvas.domNode, this.ctx, ViewGpuContext.device);144}145146/**147* This method determines which lines can be and are allowed to be rendered using the GPU148* renderer. Eventually this should trend all lines, except maybe exceptional cases like149* decorations that use class names.150*/151public canRender(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean {152const data = viewportData.getViewLineRenderingData(lineNumber);153154// Check if the line has simple attributes that aren't supported155if (156data.containsRTL ||157data.maxColumn > this.maxGpuCols158) {159return false;160}161162// Check if all inline decorations are supported163if (data.inlineDecorations.length > 0) {164let supported = true;165for (const decoration of data.inlineDecorations) {166if (decoration.type !== InlineDecorationType.Regular) {167supported = false;168break;169}170const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName);171supported &&= styleRules.every(rule => {172// Pseudo classes aren't supported currently173if (rule.selectorText.includes(':')) {174return false;175}176for (const r of rule.style) {177if (!supportsCssRule(r, rule.style)) {178return false;179}180}181return true;182});183if (!supported) {184break;185}186}187return supported;188}189190return true;191}192193/**194* Like {@link canRender} but returns detailed information about why the line cannot be rendered.195*/196public canRenderDetailed(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] {197const data = viewportData.getViewLineRenderingData(lineNumber);198const reasons: string[] = [];199if (data.containsRTL) {200reasons.push('containsRTL');201}202if (data.maxColumn > this.maxGpuCols) {203reasons.push('maxColumn > maxGpuCols');204}205if (data.inlineDecorations.length > 0) {206let supported = true;207const problemTypes: InlineDecorationType[] = [];208const problemSelectors: string[] = [];209const problemRules: string[] = [];210for (const decoration of data.inlineDecorations) {211if (decoration.type !== InlineDecorationType.Regular) {212problemTypes.push(decoration.type);213supported = false;214continue;215}216const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName);217supported &&= styleRules.every(rule => {218// Pseudo classes aren't supported currently219if (rule.selectorText.includes(':')) {220problemSelectors.push(rule.selectorText);221return false;222}223for (const r of rule.style) {224if (!supportsCssRule(r, rule.style)) {225problemRules.push(`${r}: ${rule.style[r as any]}`);226return false;227}228}229return true;230});231if (!supported) {232continue;233}234}235if (problemTypes.length > 0) {236reasons.push(`inlineDecorations with unsupported types (${problemTypes.map(e => `\`${e}\``).join(', ')})`);237}238if (problemRules.length > 0) {239reasons.push(`inlineDecorations with unsupported CSS rules (${problemRules.map(e => `\`${e}\``).join(', ')})`);240}241if (problemSelectors.length > 0) {242reasons.push(`inlineDecorations with unsupported CSS selectors (${problemSelectors.map(e => `\`${e}\``).join(', ')})`);243}244}245return reasons;246}247}248249/**250* A list of supported decoration CSS rules that can be used in the GPU renderer.251*/252const gpuSupportedDecorationCssRules = [253'color',254'font-weight',255'opacity',256];257258function supportsCssRule(rule: string, style: CSSStyleDeclaration) {259if (!gpuSupportedDecorationCssRules.includes(rule)) {260return false;261}262// Check for values that aren't supported263switch (rule) {264default: return true;265}266}267268269