Path: blob/main/src/vs/editor/browser/gpu/viewGpuContext.ts
5240 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 { Color } from '../../../base/common/color.js';9import { BugIndicatingError } from '../../../base/common/errors.js';10import { Disposable } from '../../../base/common/lifecycle.js';11import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js';12import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js';13import { observableValue, runOnChange, type IObservable } from '../../../base/common/observable.js';14import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';15import { TextureAtlas } from './atlas/textureAtlas.js';16import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';17import { INotificationService, IPromptChoice, Severity } from '../../../platform/notification/common/notification.js';18import { IThemeService } from '../../../platform/theme/common/themeService.js';19import { GPULifecycle } from './gpuDisposable.js';20import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js';21import { RectangleRenderer } from './rectangleRenderer.js';22import type { ViewContext } from '../../common/viewModel/viewContext.js';23import { DecorationCssRuleExtractor } from './css/decorationCssRuleExtractor.js';24import { Event } from '../../../base/common/event.js';25import { EditorOption, type IEditorOptions } from '../../common/config/editorOptions.js';26import { DecorationStyleCache } from './css/decorationStyleCache.js';27import { InlineDecorationType } from '../../common/viewModel/inlineDecorations.js';2829export class ViewGpuContext extends Disposable {30/**31* The hard cap for line columns rendered by the GPU renderer.32*/33readonly maxGpuCols = 2000;3435readonly canvas: FastDomNode<HTMLCanvasElement>;36readonly ctx: GPUCanvasContext;3738static device: Promise<GPUDevice>;39static deviceSync: GPUDevice | undefined;4041readonly rectangleRenderer: RectangleRenderer;4243private static readonly _decorationCssRuleExtractor = new DecorationCssRuleExtractor();44static get decorationCssRuleExtractor(): DecorationCssRuleExtractor {45return ViewGpuContext._decorationCssRuleExtractor;46}4748private static readonly _decorationStyleCache = new DecorationStyleCache();49static get decorationStyleCache(): DecorationStyleCache {50return ViewGpuContext._decorationStyleCache;51}5253private static _atlas: TextureAtlas | undefined;5455/**56* The shared texture atlas to use across all views.57*58* @throws if called before the GPU device is resolved59*/60static get atlas(): TextureAtlas {61if (!ViewGpuContext._atlas) {62throw new BugIndicatingError('Cannot call ViewGpuContext.textureAtlas before device is resolved');63}64return ViewGpuContext._atlas;65}66/**67* The shared texture atlas to use across all views. This is a convenience alias for68* {@link ViewGpuContext.atlas}.69*70* @throws if called before the GPU device is resolved71*/72get atlas(): TextureAtlas {73return ViewGpuContext.atlas;74}7576readonly canvasDevicePixelDimensions: IObservable<{ width: number; height: number }>;77readonly devicePixelRatio: IObservable<number>;78readonly contentLeft: IObservable<number>;7980constructor(81context: ViewContext,82@IInstantiationService private readonly _instantiationService: IInstantiationService,83@INotificationService private readonly _notificationService: INotificationService,84@IConfigurationService private readonly configurationService: IConfigurationService,85@IThemeService private readonly _themeService: IThemeService,86) {87super();8889this.canvas = createFastDomNode(document.createElement('canvas'));90this.canvas.setClassName('editorCanvas');9192// Adjust the canvas size to avoid drawing under the scroll bar93this._register(Event.runAndSubscribe(configurationService.onDidChangeConfiguration, e => {94if (!e || e.affectsConfiguration('editor.scrollbar.verticalScrollbarSize')) {95const verticalScrollbarSize = configurationService.getValue<IEditorOptions>('editor').scrollbar?.verticalScrollbarSize ?? 14;96this.canvas.domNode.style.boxSizing = 'border-box';97this.canvas.domNode.style.paddingRight = `${verticalScrollbarSize}px`;98}99}));100101this.ctx = ensureNonNullable(this.canvas.domNode.getContext('webgpu'));102103// Request the GPU device, we only want to do this a single time per window as it's async104// and can delay the initial render.105if (!ViewGpuContext.device) {106ViewGpuContext.device = GPULifecycle.requestDevice((message) => {107const choices: IPromptChoice[] = [{108label: nls.localize('editor.dom.render', "Use DOM-based rendering"),109run: () => this.configurationService.updateValue('editor.experimentalGpuAcceleration', 'off'),110}];111this._notificationService.prompt(Severity.Warning, message, choices);112}).then(ref => {113ViewGpuContext.deviceSync = ref.object;114if (!ViewGpuContext._atlas) {115ViewGpuContext._atlas = this._instantiationService.createInstance(TextureAtlas, ref.object.limits.maxTextureDimension2D, undefined, ViewGpuContext.decorationStyleCache);116}117return ref.object;118});119}120121const dprObs = observableValue(this, getActiveWindow().devicePixelRatio);122this._register(addDisposableListener(getActiveWindow(), 'resize', () => {123dprObs.set(getActiveWindow().devicePixelRatio, undefined);124}));125this.devicePixelRatio = dprObs;126this._register(runOnChange(this.devicePixelRatio, () => ViewGpuContext.atlas?.clear()));127128// Clear decoration CSS caches when theme changes as CSS variables may have different values129this._register(this._themeService.onDidColorThemeChange(() => {130ViewGpuContext.decorationCssRuleExtractor.clear();131ViewGpuContext.atlas?.clear();132}));133134const canvasDevicePixelDimensions = observableValue(this, { width: this.canvas.domNode.width, height: this.canvas.domNode.height });135this._register(observeDevicePixelDimensions(136this.canvas.domNode,137getActiveWindow(),138(width, height) => {139this.canvas.domNode.width = width;140this.canvas.domNode.height = height;141canvasDevicePixelDimensions.set({ width, height }, undefined);142}143));144this.canvasDevicePixelDimensions = canvasDevicePixelDimensions;145146const contentLeft = observableValue(this, 0);147this._register(this.configurationService.onDidChangeConfiguration(e => {148contentLeft.set(context.configuration.options.get(EditorOption.layoutInfo).contentLeft, undefined);149}));150this.contentLeft = contentLeft;151152this.rectangleRenderer = this._register(this._instantiationService.createInstance(RectangleRenderer, context, this.contentLeft, this.devicePixelRatio, this.canvas.domNode, this.ctx, ViewGpuContext.device));153}154155/**156* This method determines which lines can be and are allowed to be rendered using the GPU157* renderer. Eventually this should trend all lines, except maybe exceptional cases like158* decorations that use class names.159*/160public canRender(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean {161const data = viewportData.getViewLineRenderingData(lineNumber);162163// Check if the line has simple attributes that aren't supported164if (165data.containsRTL ||166data.maxColumn > this.maxGpuCols167) {168return false;169}170171// Check if all inline decorations are supported172if (data.inlineDecorations.length > 0) {173let supported = true;174for (const decoration of data.inlineDecorations) {175if (decoration.type !== InlineDecorationType.Regular) {176supported = false;177break;178}179const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName);180supported &&= styleRules.every(rule => {181// Pseudo classes aren't supported currently182if (rule.selectorText.includes(':')) {183return false;184}185for (const r of rule.style) {186if (!supportsCssRule(r, rule.style)) {187return false;188}189}190return true;191});192if (!supported) {193break;194}195}196return supported;197}198199return true;200}201202/**203* Like {@link canRender} but returns detailed information about why the line cannot be rendered.204*/205public canRenderDetailed(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] {206const data = viewportData.getViewLineRenderingData(lineNumber);207const reasons: string[] = [];208if (data.containsRTL) {209reasons.push('containsRTL');210}211if (data.maxColumn > this.maxGpuCols) {212reasons.push('maxColumn > maxGpuCols');213}214if (data.inlineDecorations.length > 0) {215let supported = true;216const problemTypes: InlineDecorationType[] = [];217const problemSelectors: string[] = [];218const problemRules: string[] = [];219for (const decoration of data.inlineDecorations) {220if (decoration.type !== InlineDecorationType.Regular) {221problemTypes.push(decoration.type);222supported = false;223continue;224}225const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName);226supported &&= styleRules.every(rule => {227// Pseudo classes aren't supported currently228if (rule.selectorText.includes(':')) {229problemSelectors.push(rule.selectorText);230return false;231}232for (const r of rule.style) {233if (!supportsCssRule(r, rule.style)) {234// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any235problemRules.push(`${r}: ${rule.style[r as any]}`);236return false;237}238}239return true;240});241if (!supported) {242continue;243}244}245if (problemTypes.length > 0) {246reasons.push(`inlineDecorations with unsupported types (${problemTypes.map(e => `\`${e}\``).join(', ')})`);247}248if (problemRules.length > 0) {249reasons.push(`inlineDecorations with unsupported CSS rules (${problemRules.map(e => `\`${e}\``).join(', ')})`);250}251if (problemSelectors.length > 0) {252reasons.push(`inlineDecorations with unsupported CSS selectors (${problemSelectors.map(e => `\`${e}\``).join(', ')})`);253}254}255return reasons;256}257}258259/**260* A list of supported decoration CSS rules that can be used in the GPU renderer.261*/262const gpuSupportedDecorationCssRules = [263'color',264'font-weight',265'opacity',266'text-decoration',267'text-decoration-color',268'text-decoration-line',269'text-decoration-style',270'text-decoration-thickness',271];272273function supportsCssRule(rule: string, style: CSSStyleDeclaration) {274if (!gpuSupportedDecorationCssRules.includes(rule)) {275return false;276}277// Check for values that aren't supported278switch (rule) {279case 'text-decoration':280case 'text-decoration-line': {281const value = style.getPropertyValue(rule);282// Only line-through is supported currently283return value === 'line-through';284}285case 'text-decoration-color': {286const value = style.getPropertyValue(rule);287// Support var(--something, initial/inherit) which falls back to currentcolor288if (/^var\(--[^,]+,\s*(?:initial|inherit)\)$/.test(value)) {289return true;290}291// Support parsed color values292return Color.Format.CSS.parse(value) !== null;293}294case 'text-decoration-style': {295const value = style.getPropertyValue(rule);296// Only 'initial' (solid) is supported297return value === 'initial';298}299case 'text-decoration-thickness': {300const value = style.getPropertyValue(rule);301// Only pixel values and 'initial' are supported302return value === 'initial' || /^\d+(\.\d+)?px$/.test(value);303}304default: return true;305}306}307308309